Have you ever created a Python-based Jupyter notebook and analyzed data that you want to explore in a number of different ways? For example, you may want to look at a plot of data, but filter it ten different ways. What are your options to view these ten different results?
- Copy and paste a cell, changing the filter for each cell, then executing the cell. You will end up with ten different cells with ten different values.
- Modify the same cell, execute it and view the results, then modify it again, ten times.
- Parameterize the notebook (perhaps using a tool like Papermill) and execute the entire notebook with ten different sets of parameters.
- Some combination of the above.
These all are non-ideal if we want quick interaction and the ability to explore the data. Those options are also prone to typing errors or lots of extra editing work. They may work great for the original developer of a notebook, but allowing a user who doesn’t undestand Python syntax to modify variables and re-execute cells may not be the best option. What if you could just give the user a simple form, with a button, and they could modify the form and see the results they want?
It turns out you can do this pretty easily right in Jupyter, without creating a full webapp. This is possible with ipywidgets
, also known just as widgets. I’ll show you the basics in this article of building a few simple forms to view and analyze some data.
What are widgets?
Jupyter widgets are special bits of code that will embed JavaScript and html in your notebook and present a visual representation in your brower when executed in a notebook. These components allow a user to interact with the widgets. The widgets can execute code on certain actions, allowing you to update cells without a user having to re-execute them or even modify any code.
Getting started
First, you need to make sure that ipywidgets
is installed in your environment. This will depend a bit on which Jupyter environment you are using. For older Jupyter and JupyterLab installs, make sure to check the details in the docs. But for a basic install, just use pip
pip install ipywidgets
or for conda
conda install -c conda-forge ipywidgets
This should be all that you need to do in most situations to get things running.
Example
Instead of going through all the widgets and getting into details right away, let’s grab some interesting data and explore it manually. Then we’ll use widgets to make a more interactive version of some of this data exploration. Let’s grab some data from the Chicago Data Portal – specifically their dataset of current active business licenses. Note that if you just run the code as below, you’ll only get 1000 rows of data. Check the documentation on how to to grab all the data.
Note: all of this code was written in a Jupyter notebook using Python 3.8.6. While this article shows the output, the best way to experience widgets is to interact with them in your own environment. You can download a notebook of this article here.
import pandas as pd df = pd.read_csv('https://data.cityofchicago.org/resource/uupf-x98q.csv') df[['LEGAL NAME', 'ZIP CODE', 'BUSINESS ACTIVITY']].head()
As we can see from the data, the business activity is pretty verbose, but the zip code is an easy way to do some simple searches and filters of data. For our smaller data set, let’s just grab the zip codes that have 20 or more businesses.
zips = df.groupby('ZIP CODE').count()['ID'].sort_values(ascending=False) zips = list(zips[zips > 20].index) zips
[60618, 60622, 60639, 60609, 60614, 60608, 60619, 60607]
Now, a reasonable scenario for filtering data might be create a report filtering by zip code, showing the legal name and address of a business, ordered by expiration date of the license. This would be a pretty simple (even if somewhat messy) expression in pandas. For example, in this data set we can take the top zip code and look at a few columns like this.
df.loc[df['ZIP CODE'] == zips[0]].sort_values(by='LICENSE TERM EXPIRATION DATE', ascending=False)[['LEGAL NAME', 'ADDRESS', 'LICENSE TERM EXPIRATION DATE']]
Now what if someone wanted to be able to run this report for different zip codes, looking at different columns, and sorting by other columns? The user would have to be comfortable editing the cell above, rerunning it, and maybe executing other cells to look for the column names and other values.
Using widgets
Instead, we can use widgets to make a form that allows this interaction to be executed visually. In this article you will learn enough about widgets to build a form and dynamically show the results.
Widget types
Since most of us are familiar with forms in our web browsers, it makes sense to think about widgets as parts of typical forms. Widgets can represent numerical, boolean, or text values. They can be selectors of pre-existing lists, or can accept free text (or password text). You can also use them to display formatted output or images. The full list of widgets describes them in more detail. You can also create your own custom widgets, but for our purposes, we will be able to do all the work with standard widgets.
A widget is just an object that can be displayed in a Jupyter notebook once created. It will render itself (and its underlying content) and (possibly) allow user interaction.
Making a form
For our form, we will need to gather four pieces of information:
- The zip code to filter
- The column to sort on
- Whether the sort is ascending or descending
- The columns to display.
These four pieces of information will be captured by the following form elements:
- A selection dropdown
- A selection dropdown
- A checkbox
- A multi-selection list
These three widgets will provide a quick intro to widgets, and once you know how to instantiate and use one widget, the others are quite similar. Before we can create a widget, we need to import the library. Let’s look at dropdowns first.
import ipywidgets as widgets widgets.Dropdown( options=zips, value=zips[0], description='Zip Code:', disabled=False, )
zips_dropdown = widgets.Dropdown( options=zips, value=zips[0], description='Zip Code:', disabled=False, ) display(zips_dropdown)
We can easily do the same for the columns.
columns_dropdown = widgets.Dropdown( options=df.columns, value=df.columns[4], description='Sort Column:', disabled=False, ) display(columns_dropdown)
sort_checkbox = widgets.Checkbox( value=False, description='Ascending?', disabled=False) display(sort_checkbox)
columns_selectmultiple = widgets.SelectMultiple( options=df.columns, value=['LEGAL NAME'], rows=10, description='Visible:', disabled=False ) display(columns_selectmultiple)
button = widgets.Button( description='Run', disabled=False, button_style='', # 'success', 'info', 'warning', 'danger' or '' tooltip='Run report', icon='check' # (FontAwesome names without the `fa-` prefix) ) display(button)
Handling output
Before we hook our button up to a function, we need to make sure we can capture the output of our function. If we want to view a DataFrame
, or print text, or log some information to stdout, we need to be able to capture that information and clear it, if necessary. This is what the Output
widget is for. Note that you don’t have to use an output widget, but if you want your output to appear in a certain cell, you will need to use this. The cell where the Output
widget is displayed will render the results.
out = widgets.Output(layout={'border': '1px solid black'})
Hooking it all up
Now that we’ve generated all our user interface components, how do we display them all in one spot and hook them up to generate actions?
First, let’s create a simple layout with all the items together.
box = widgets.VBox([zips_dropdown, columns_dropdown, sort_checkbox, columns_selectmultiple, button]) display(box)
Handling events
For widgets that can produce events, you can provide a function that will receive the event. For a Button
, the event is on_click
, and it requires a function that will take a single argument, the Button
itself. If we use the Output
we created above (as a context manager using a with
statement), clicking the button will cause the text “Button clicked” to be appended to the cell output. Note that the cell that receives the output will be the one where the Output
was rendered.
def on_button_clicked(b): with out: print("Button clicked.") button.on_click(on_button_clicked, False)
A better way to hook things up
The above example is simple, but doesn’t show us how we’d get the values from the other inputs. Another way to do that is to use interact
. It works as both a function or a function decorator to automatically create widgets that allow you to interactively change the inputs to a function. Based on the named argument type, it will generate a widget that allows you to change that value. Using interact
is a quick way to provide user interaction around a function. The function will be called each time a widget is updated. As you move the slider, the square of the number will be printed if the checkbox is checked, and the number will just be printed unchanged otherwise.
def my_function2(x, y): if y: print(x*x) else: print(x) interact(my_function2,x=10,y=False);
Note that you can provide more information to interact
to provide more appropriate user interface elements (see the docs for examples). But since we already made widgets, we could just use those instead. The best way to do that is to use another function, interactive
. interactive
is like interact, but allows you to interact with the widgets that were created (or supply them directly), and to display values when you want. Since we already made some widgets, we can just let interactive
know about them by providing each of them as keyword arguments. The first argument is a function, and that function’s arguments need to match the subsequent keyword arguments to interactive. Each time we change one of the values in the form, the function will be invoked with the values from the form widgets. With just a few lines of code, we now have an interactive tool for looking at and filtering this data.
But first, I’ll make a cell with an output to receive the display.
report_output = widgets.Output() display(report_output)
from ipywidgets import interactive def filter_function(zipcode, sort_column, sort_ascending, view_columns): filtered = df.loc[df['ZIP CODE'] == zipcode].sort_values(by=sort_column, ascending=sort_ascending)[list(view_columns)] with report_output: report_output.clear_output() display(filtered) interactive(filter_function, zipcode=zips_dropdown, sort_column=columns_dropdown, sort_ascending=sort_checkbox, view_columns=columns_selectmultiple)
Now, the same form created earlier above is rendered in the cell. The output will appear in whichever cell the display(report_output)
line was executed. As you modify any of the form elements, the resulting filtered DataFrame
will be displayed in that cell.
Summary
This has been just a quick overview of using ipywidgets
to make Jupyter notebooks more interactive. Even if you are comfortable editing Python code and re-executing cells to update and explore data, widgets may be a great way to make that exploration more dynamic and convenient, along with being less error prone. If you need to share notebooks with people who are not comfortable editing Python code, widgets can be a lifesaver and really help the data come alive.
Just reading about these widgets is not nearly as interesting as running examples and working with them yourself. Give these examples a try and then try using widgets in your own notebooks.
I downloaded your notebook file but could not load it, I got:
Unreadable Notebook: C:\Users\msime\mesNotebooks\miscTests\jupyter_ipywidgets.ipynb
NotJSONError(“Notebook does not appear to be JSON: ‘\n\n\n\n\n\n\n<html lang…”)ur
Help most welcome !
Michel, it looks like you probably downloaded the Github web page. Try downloading the raw file (or just check out the entire repo using git). The raw file is located here. Hope that helps!
ipywidgets are amazing and you can do a lot of cool stuff with them. Although this article only serves as an introduction/example, I’d really suggest you use the ipydatagrid pakage for the particular use case presented here.
https://github.com/bloomberg/ipydatagrid
Thanks for pointing out ipydatagrid, Troels. I’ll check it out.
Yes, it does work with the raw file.
Thanks for the tip
This was great! I love your very specific and easy-to-follow writing style, and found this article very helpful.