{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# How to use ipywidgets to make your Jupyter notebook interactive\n", "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?\n", "\n", "1. 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.\n", "1. Modify the same cell, execute it and view the results, then modify it again, ten times.\n", "1. Parameterize the notebook (using something like [Papermill](https://papermill.readthedocs.io/en/latest/) and execute the notebook with ten different sets of parameters.\n", "1. Some combination of the above.\n", "\n", "These all are non-ideal if we want quick interaction and the ability to explore the data. They are also prone to typing errors. 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?\n", "\n", "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.\n", "\n", "## What are widgets?\n", "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 be configured to execute code on certain actions, allowing you to update cells without a user having to re-execute them or even modify any code.\n", "\n", "## Getting started\n", "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](https://ipywidgets.readthedocs.io/en/latest/user_install.html). But for a basic install, just use pip\n", "\n", "```\n", "pip install ipywidgets\n", "```\n", "\n", "or for conda\n", "\n", "```\n", "conda install -c conda-forge ipywidgets\n", "```\n", "\n", "This should be all that you need to do in most situations to get things running. \n", "\n", "## Example\n", "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](https://data.cityofchicago.org/Community-Economic-Development/Business-Licenses-Current-Active/uupf-x98q) - 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. " ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "df = pd.read_csv('https://data.cityofchicago.org/resource/uupf-x98q.csv')" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
LEGAL NAMEZIP CODEBUSINESS ACTIVITY
0DE LA TORRE AUTO SALES INC.60621Motor Vehicle Repair - Engine and Transmissio...
1SITEL ARM CORP.33131Debt Collecting - Administrative Commercial Of...
2SEVEN NINE ELEVEN FOOD MART, INC.60632Retail Sale of Tobacco
3WARM BELLY BAKERY, LLC60607Sale of Food Prepared Onsite With Dining Area
4VICKIE, INC.60639Tavern - Consumption of Liquor on Premise
\n", "
" ], "text/plain": [ " LEGAL NAME ZIP CODE \\\n", "0 DE LA TORRE AUTO SALES INC. 60621 \n", "1 SITEL ARM CORP. 33131 \n", "2 SEVEN NINE ELEVEN FOOD MART, INC. 60632 \n", "3 WARM BELLY BAKERY, LLC 60607 \n", "4 VICKIE, INC. 60639 \n", "\n", " BUSINESS ACTIVITY \n", "0 Motor Vehicle Repair - Engine and Transmissio... \n", "1 Debt Collecting - Administrative Commercial Of... \n", "2 Retail Sale of Tobacco \n", "3 Sale of Food Prepared Onsite With Dining Area \n", "4 Tavern - Consumption of Liquor on Premise " ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df[['LEGAL NAME', 'ZIP CODE', 'BUSINESS ACTIVITY']].head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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. " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[60618, 60622, 60639, 60609, 60614, 60608, 60619, 60607]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "zips = df.groupby('ZIP CODE').count()['ID'].sort_values(ascending=False)\n", "zips = list(zips[zips > 20].index)\n", "zips" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
LEGAL NAMEADDRESSLICENSE TERM EXPIRATION DATE
8CENVEO WORLDWIDE LIMITED3001 N ROCKWELL ST12/15/2022
580SUPAROSSA ON WESTERN INC.3737 N WESTERN AVE12/15/2022
643ERRO INC2933 W MONTROSE AVE 1 #12/15/2022
640JAMES INSTRUMENTS INC3727 N KEDZIE AVE 1ST12/15/2022
609MANJU J SUTHAR3011 W IRVING PARK RD 1ST12/15/2022
............
542DANIEL J ACOSTA2827 N MILWAUKEE AVE # 1ST07/15/2021
329GRANITE STYLE DESIGN COMPANY3111 N ROCKWELL ST07/15/2021
405IAN HEPBURN[REDACTED FOR PRIVACY]07/15/2021
181LOGAN-AVONDALE VFW # 29783007 N KEDZIE AVE 1ST03/15/2022
786CUTTING EDGES BEAUTY STUDIO, INC.2941 W IRVING PARK RD 101/15/2023
\n", "

78 rows × 3 columns

\n", "
" ], "text/plain": [ " LEGAL NAME ADDRESS \\\n", "8 CENVEO WORLDWIDE LIMITED 3001 N ROCKWELL ST \n", "580 SUPAROSSA ON WESTERN INC. 3737 N WESTERN AVE \n", "643 ERRO INC 2933 W MONTROSE AVE 1 # \n", "640 JAMES INSTRUMENTS INC 3727 N KEDZIE AVE 1ST \n", "609 MANJU J SUTHAR 3011 W IRVING PARK RD 1ST \n", ".. ... ... \n", "542 DANIEL J ACOSTA 2827 N MILWAUKEE AVE # 1ST \n", "329 GRANITE STYLE DESIGN COMPANY 3111 N ROCKWELL ST \n", "405 IAN HEPBURN [REDACTED FOR PRIVACY] \n", "181 LOGAN-AVONDALE VFW # 2978 3007 N KEDZIE AVE 1ST \n", "786 CUTTING EDGES BEAUTY STUDIO, INC. 2941 W IRVING PARK RD 1 \n", "\n", " LICENSE TERM EXPIRATION DATE \n", "8 12/15/2022 \n", "580 12/15/2022 \n", "643 12/15/2022 \n", "640 12/15/2022 \n", "609 12/15/2022 \n", ".. ... \n", "542 07/15/2021 \n", "329 07/15/2021 \n", "405 07/15/2021 \n", "181 03/15/2022 \n", "786 01/15/2023 \n", "\n", "[78 rows x 3 columns]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df.loc[df['ZIP CODE'] == zips[0]].sort_values(by='LICENSE TERM EXPIRATION DATE', ascending=False)[['LEGAL NAME', 'ADDRESS', 'LICENSE TERM EXPIRATION DATE']]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "## Using widgets\n", "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.\n", "\n", "### Widget types\n", "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](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html) describe 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.\n", "\n", "A widget is just an object that once instantiated can be displayed in a Jupyter notebook. Once displayed, it will render itself (and its underlying content) and (possibly) allow user interaction.\n", "\n", "For our form, we will need to gather four pieces of information:\n", "1. The zip code to filter\n", "1. The column to sort on\n", "1. Whether the sort is ascending or descending\n", "1. The columns to display.\n", "\n", "These four pieces of information will be captured by the following form elements:\n", "1. A selection dropdown\n", "1. A selection dropdown\n", "1. A checkbox\n", "1. A multi-selection list\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "5393711da97044e49b5342a009dcc432", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Dropdown(description='Zip Code:', options=(60618, 60622, 60639, 60609, 60614, 60608, 60619, 60607), value=6061…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import ipywidgets as widgets\n", "\n", "\n", "widgets.Dropdown(\n", " options=zips,\n", " value=zips[0],\n", " description='Zip Code:',\n", " disabled=False,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Of course, just creating an object doesn't allow us to use it, so we need to assign it to a variable, and the ```display``` function can be used to render it." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "7c4b14f8d8b0408d9ffd0052076e8b34", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Dropdown(description='Zip Code:', options=(60618, 60622, 60639, 60609, 60614, 60608, 60619, 60607), value=6061…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "zips_dropdown = widgets.Dropdown(\n", " options=zips,\n", " value=zips[0],\n", " description='Zip Code:',\n", " disabled=False,\n", ")\n", "\n", "display(zips_dropdown)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can easily do the same for the columns." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "af760b6659f74b98b68ac70a0803957b", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Dropdown(description='Sort Column:', index=4, options=('ID', 'LICENSE ID', 'ACCOUNT NUMBER', 'SITE NUMBER', 'L…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "columns_dropdown = widgets.Dropdown(\n", " options=df.columns,\n", " value=df.columns[4],\n", " description='Sort Column:',\n", " disabled=False,\n", ")\n", "\n", "display(columns_dropdown)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And for boolean values, you have a few options. You can do a ```CheckBox``` or ```ToggleButton```. I'll go with the first." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "09fe1aee946b4d4fb08a97b3f2f48767", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Checkbox(value=False, description='Ascending?')" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "sort_checkbox = widgets.Checkbox(\n", " value=False,\n", " description='Ascending?',\n", " disabled=False)\n", "display(sort_checkbox)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally for this example, we want to be able to select all the columns we want to see in the output. We'll use a ```SelectMultiple``` for that. Note that if you use the shift and ctrl (or Command on a Mac) keys to select multiple options." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "15f715409de74996ad29f6f62fcddbf0", "version_major": 2, "version_minor": 0 }, "text/plain": [ "SelectMultiple(description='Visible:', index=(4,), options=('ID', 'LICENSE ID', 'ACCOUNT NUMBER', 'SITE NUMBER…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "columns_selectmultiple = widgets.SelectMultiple(\n", " options=df.columns,\n", " value=['LEGAL NAME'],\n", " rows=10,\n", " description='Visible:',\n", " disabled=False\n", ")\n", "display(columns_selectmultiple)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Last, we will show a button that we can click to force updates. (Note that we won't end up needing this in the end, there's a simpler way to interact with our elements, but buttons can be useful for many situations)." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "1c8e7ca359a84e29990ecfe26d61875b", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Button(description='Run', icon='check', style=ButtonStyle(), tooltip='Run report')" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "button = widgets.Button(\n", " description='Run',\n", " disabled=False,\n", " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", " tooltip='Run report',\n", " icon='check' # (FontAwesome names without the `fa-` prefix)\n", ")\n", "display(button)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Handling output\n", "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 is displayed will render the results. " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e7a4ff4b01fb4b3685c0d8294087a7a9", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Output(layout=Layout(border='1px solid black'))" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "out = widgets.Output(layout={'border': '1px solid black'})\n", "out" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Hooking it all up\n", "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? \n", "\n", "First, let's create a simple layout with all the items together." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b9a3e71025df4434a123c96abfc459f9", "version_major": 2, "version_minor": 0 }, "text/plain": [ "VBox(children=(Dropdown(description='Zip Code:', options=(60618, 60622, 60639, 60609, 60614, 60608, 60619, 606…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "box = widgets.VBox([zips_dropdown, columns_dropdown, sort_checkbox, columns_selectmultiple, button])\n", "display(box)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Handling events\n", "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." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def on_button_clicked(b):\n", " with out:\n", " print(\"Button clicked.\")\n", "\n", "button.on_click(on_button_clicked, False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A better way to hook things up\n", "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. Your function will be called each time the element is updated. " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "77509575b9be492fa8a3c839e610cf4f", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(IntSlider(value=5, description='x', max=15, min=-5), Output()), _dom_classes=('widget-in…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from ipywidgets import interact\n", "\n", "def my_function(x):\n", " print(x*x)\n", " \n", "interact(my_function, x=5);" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "6d19fa57181b4250809241c763ffad58", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(IntSlider(value=10, description='x', max=30, min=-10), Checkbox(value=False, description…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def my_function2(x, y):\n", " if y:\n", " print(x*x)\n", " else:\n", " print(x)\n", "\n", "interact(my_function2,x=10,y=False);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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 elements, 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 elements. With just a few lines of code, we now have an interactive tool for looking at and filtering this data.\n", "\n", "But first, I'll make a cell with an output to receive the display." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d1e73fb4f6bc4971927f6d0ed45e3f4f", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Output()" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "report_output = widgets.Output()\n", "display(report_output)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "scrolled": false }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "159b09adc13b4e2489752b9996408da8", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(Dropdown(description='Zip Code:', options=(60618, 60622, 60639, 60609, 60614, 60608, 606…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from ipywidgets import interactive\n", "\n", "def filter_function(zipcode, sort_column, sort_ascending, view_columns):\n", " filtered = df.loc[df['ZIP CODE'] == zipcode].sort_values(by=sort_column, ascending=sort_ascending)[list(view_columns)]\n", " with report_output:\n", " report_output.clear_output()\n", " display(filtered)\n", " \n", "interactive(filter_function, zipcode=zips_dropdown, sort_column=columns_dropdown,\n", " sort_ascending=sort_checkbox, view_columns=columns_selectmultiple) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.6" }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }