Bring your Jupyter Notebook to life with interactive widgets

????❶ Getting startedTo start using the library we need to install the ipywidgets extension.

If using conda, we type this command in the terminal:conda install -c conda-forge ipywidgetsFor pip, it will be a two-step process: 1.

install and 2.

enable:pip install ipywidgetsjupyter nbextension enable –py widgetsnbextensionAdding a widgetIn order to incorporate widgets in the notebook we have to import the module, as shown below:import ipywidgets as widgetsTo add a slider, we can define the minimum and maximum values, the interval size (step), a description and an initial value:widgets.

IntSlider( min=0, max=10, step=1, description='Slider:', value=3)Demo: SliderDisplaying itThe display() function renders a widget object in an input cell.

First import:from IPython.

display import displayThen pass the widget as a parameter in the display() function:slider = widgets.

IntSlider()display(slider)Getting/Setting its valueTo read the value of a widget, we will query its value property.

Similarly, we can set a widget’s value:Demo: ValueLinking two widgetsWe can synchronise the values of two widgets by using the jslink() function.

slider = widgets.

IntSlider()text = widgets.

IntText()display(slider, text)widgets.

jslink((slider, 'value'), (text, 'value'))Demo: LinkingWidgets ListFor a full list of widgets you can check out the documentation, or run the following command:print(dir(widgets))❷ Handling Widget EventsThe widgets can respond to events, which are raised when a user interacts with them.

A simple example is clicking on a button — we are expecting an action to take place.

Let’s see how this works…Depending on its specific features, each widget exposes different events.

An event handler will be executed every time the event is fired.

Event handler is a callback function in response to an event, that operates asynchronously and handles the inputs received.

Here we will create a simple button called btn.

The on_click method is invoked when the button is clicked.

Our event handler, btn_eventhandler, will print a short message with the button’s caption — note that the input argument of the handler, obj, is the button object itself which allows us to access its properties.

To bind the event with the handler, we assign the latter to the button’s on_click method.

btn = widgets.

Button(description='Medium')display(btn)def btn_eventhandler(obj): print('Hello from the {} button!'.

format(obj.

description))btn.

on_click(btn_eventhandler)Demo: Button Event HandlerWhat will bring us nicely to the next section is that the output appears in the same cell as the button itself.

So let’s move on to see how we can add more flexibility to our notebook!❸ Controlling Widget OutputIn this section we will explore how to use widgets to control a dataframe.

The sample dataset I have chosen is ‘Number of International Visitors to London’ which shows totals of London’s visitors with regards to nights, visits and spend, broken down by year, quarter, purpose, duration, mode and country.

Initially, we will get the data and load it into a dataframe:import pandas as pdimport numpy as npurl = "https://data.

london.

gov.

uk/download/number-international-visitors-london/b1e0f953-4c8a-4b45-95f5-e0d143d5641e/international-visitors-london-raw.

csv"df_london = pd.

read_csv(url)df_london.

sample(5)Suppose we would like to filter the dataframe by year.

We will first define a dropdown and populate it with the list of unique year values.

In order to do this, we will create a generic function, unique_sorted_values_plus_ALL, which will find the unique values, sort them and then add the ALL item at the start, so the user could remove the filter.

ALL = 'ALL'def unique_sorted_values_plus_ALL(array): unique = array.

unique().

tolist() unique.

sort() unique.

insert(0, ALL) return uniqueNow we will initialise the dropdown:dropdown_year = widgets.

Dropdown(options = unique_sorted_values_plus_ALL(df_london.

year))The dropdown widget exposes the observe method, which takes a function that will be invoked when the value of the dropdown changes.

As such, we will next create the observer handler to filter the dataframe by the selected values — note that the input argument of the handler, change, contains information about the changes that took place which allows us to access the new value (change.

new).

If the new value is ALL we remove the filter, otherwise we apply it:def dropdown_year_eventhandler(change): if (change.

new == ALL): display(df_london) else: display(df_london[df_london.

year == change.

new])We will then bind the handler to the dropdown:dropdown_year.

observe(dropdown_year_eventhandler, names='value')Using a dropdown to filter a dataframeSo far so good, but the output of all the queries is accumulating in this very same cell; i.

e.

if we select a new year from the dropdown, a new dataframe will render underneath the first one, on the same cell.

The desired behaviour though, is to refresh the contents of the dataframe each time.

Capturing Widget OutputThe solution to this is to capture the cell output in a special kind of widget, namely Output, and then display it in another cell.

We will slightly tweak the code to:create a new instance of Outputoutput_year = widgets.

Output()call the clear_output method within the event handler to clear the previous selection on each iteration, and capture the output of the dataframe in a with block.

def dropdown_year_eventhandler(change): output_year.

clear_output() with output_year: display(df_london[df_london.

year == change.

new])We will then display the output in a new cell:display(output_year)This is how it works:Demo: Capturing output in a new cellAs you can see the output is rendered in a new cell and the filtering is working as expected!.????❹ Linking Widget OutputsContinuing the previous example, let’s assume we would also like to filter by purpose too.

If we go ahead and add another dropdown, we will quickly realise the dataframe only responds to the filter by the dropdown which was recently changed.

What we need to do is to link the two together so it can work on both values (i.

e.

year and purpose).

Let’s see how it should work:Firstly we need a common output for both dropdowns:output = widgets.

Output()Here are the two dropdowns:dropdown_year = widgets.

Dropdown(options = unique_sorted_values_plus_ALL(df_london.

year))dropdown_purpose = widgets.

Dropdown(options = unique_sorted_values_plus_ALL(df_london.

purpose))Then we create a new function, common_filtering, that will be called by both the event handlers.

This function will apply a filter on the dataframe for both year AND purpose:We are clearing the output, then we check if any of the values is ALL, in which case we consider that the respective filter is removed.

When both filters are present, in the else statement, we apply the & operation in both filters.

Finally we capture the output:def common_filtering(year, purpose): output.

clear_output() if (year == ALL) & (purpose == ALL): common_filter = df_london elif (year == ALL): common_filter = df_london[df_london.

purpose == purpose] elif (purpose == ALL): common_filter = df_london[df_london.

year == year] else: common_filter = df_london[(df_london.

year == year) & (df_london.

purpose == purpose)] with output: display(common_filter)We amend the event handlers to call the common_filtering function and pass the change.

new value as well as the current value of the other dropdown:def dropdown_year_eventhandler(change): common_filtering(change.

new, dropdown_purpose.

value)def dropdown_purpose_eventhandler(change): common_filtering(dropdown_year.

value, change.

new)We bind the handlers to the dropdowns, and that’s it!dropdown_year.

observe(dropdown_year_eventhandler, names='value')dropdown_purpose.

observe(dropdown_purpose_eventhandler, names='value')Code snippet:Filter a dataframe based on two valuesHere is the demo:Demo: Filter a dataframe based on two values❺ Creating a DashboardWe have put the basis for our dashboard so far by filtering and displaying the data of the London dataset.

We will carry on by colouring the numeric values based on a user selected value.

A useful numeric widget is the BoundedFloatText; we will give it a min, max and initial value, and the incremental step.

bounded_num = widgets.

BoundedFloatText(min=0, max=100000, value=5, step=1)In order to colour the dataframe cells, we will define this function:def colour_ge_value(value, comparison): if value >= comparison: return 'color: red' else: return 'color: black'Now we will minimally amend the common_filtering function to:add new num input parameter:def common_filtering(year, purpose, num):apply the styling by calling the colour_ge_value function for the three numeric columns:with output: display(common_filter .

style.

applymap( lambda x: colour_ge_value(x, num), subset=['visits','spend', 'nights']))The existing event handlers need to be adjusted to pass the bounded_num.

value:def dropdown_year_eventhandler(change): common_filtering(change.

new, dropdown_purpose.

value, bounded_num.

value)def dropdown_purpose_eventhandler(change): common_filtering(dropdown_year.

value, change.

new, bounded_num.

value)And finally we will plug-in the event handler of the new widget:def bounded_num_eventhandler(change): common_filtering(dropdown_year.

value, dropdown_purpose.

value, change.

new)bounded_num.

observe(bounded_num_eventhandler, names='value')Code snippet:Colour dataframe valuesHere is the demo:Demo: Colour dataframe valuesPlottingNext we will be adding a new graph to plot a basic univariate density of the number of visits (KDE → Kernel Density Estimation).

We will use seaborn, so let’s import the libraries:import seaborn as snsimport matplotlib.

pyplot as pltContinuing the previous use-case, we will capture the plot in a new output variable:plot_output = widgets.

Output()We will now amend the common_filtering function to plot the new diagram:first we clear the output:plot_output.

clear_output()and then we call the kdeplot method of seaborn by passing the number of visits:with plot_output: sns.

kdeplot(common_filter['visits'], shade=True) plt.

show()Lastly, the only thing we need to do is to display the outputs in a new cell:display(output)display(plot_output)Code snippet:Controlling a graphHere is the demo:Demo: Controlling a graph❻ Dashboard LayoutUp until now our user interface is functional but is taking up a lot of real estate.

We will first arrange the input widgets horizontally.

The HBox will add widgets to it one at a time from left-to-right:input_widgets = widgets.

HBox([dropdown_year, dropdown_purpose, bounded_num])display(input_widgets)HBoxNext we will create a container for the output.

Tab is great for this.

The 1st tab will host the dataframe and the 2nd one the graph.

tab = widgets.

Tab([output, plot_output])tab.

set_title(0, 'Dataset Exploration')tab.

set_title(1, 'KDE Plot')display(tab)TabFinally we will stack the input widgets and the tab on top of each other with a VBox.

dashboard = widgets.

VBox([input_widgets, tab])display(dashboard)VBoxIt feels a bit ‘jammed’, so as a last step, we will polish our dashboard by adding some space.

We will define a Layout giving 50px margin between the items.

item_layout = widgets.

Layout(margin='0 0 50px 0')We will call this layout for each item:input_widgets = widgets.

HBox([dropdown_year, dropdown_purpose, bounded_num],layout=item_layout)tab = widgets.

Tab([output, plot_output],layout=item_layout)and ta da….

Our finished dashboard:DashboardFinal demoDemo: Final DashboardPS: For presentation purposes, in some of these demos I have used a subset of the dataset i.

e.

: df_london = df_london.

sample(250).

Go furtherThere are a few third party widgets you can use too, with most popular ones being:• 2-D charting: bqplot• 3-D visualisation: pythreejs and ipyvolume• Mapping: ipyleaflet and gmaps.

You can also build your own custom widgets!.For more info take a look here.

RecapWe saw a fairly wide range of widgets in action but we still have only scratched the surface here — we can build really complex and extensive GUIs using ipywidgets.

I hope you all agree they deserve a place in any Data Scientist’s toolbox as they enhance our productivity and add a lot of value during data exploration.

Thanks for reading!I regularly write about Technology & Data on Medium — if you would like to read my future posts then please ‘Follow’ me!.

. More details

Leave a Reply