Using Voilà to Turn Your Jupyter Notebook into a Web Application
Voilà is a framework that allows web applications to be generated directly from a Jupyter notebook by turning your Jupyter cell outputs into html. This makes it a really appealing option for data scientists who are looking to build quick web applications to interact with their models and data visualizations.
While Voilà is technically as easy to use as it sounds, there is a catch: in order to build interactivity into the web apps, you need to make use of widgets and widget-based visualization tools. In this article, I will go through a step-by-step tutorial on how to use widgets to build interactivity into your Voilà application. Your resulting application will have an interface for users to do some basic data exploration on the Iris dataset as well as interact with a K-Nearest Neighbors machine learning model for predicting Iris species.
Part I: Installation and Initializing Your App
Below are the required packages for this tutorial. Follow the links for installation guides.
- ipywidgets: widget package
- Plotly: data visualization tool with widget-based plotting capabilities
- Voilà: framework for turning Jupyter notebooks into standalone web applications
Now that everything is installed, let’s get familiar with the Jupyter-to-browser workflow with Voilà. Voilà web applications display contents of markdown cells as well as any cell’s output. So let’s start with the header, which should be written in a markdown cell:
# Iris Species Classification
Now, go to your terminal and navigate to the directory containing your Jupyter notebook and enter the following:
$ voila <filename>.ipynb
You should see a new browser tab or window open up with your title displayed. It will look something like this:
Nice. Now let’s start adding some interactive components.
Part II: Building an Interactive Visualization
First, load the Iris dataset.
import pandas as pd
from sklearn.datasets import load_irisdata = load_iris()df = pd.DataFrame(data = data[‘data’], columns = data.feature_names)species_dict = {0:’setosa’, 1: ‘versicolor’, 2: ‘virginica’}df[‘species’] = [species_dict[x] for x in data.target]
The dataframe should look something like this:
Now, we’ll build the visualization that we plan to display in the web app. For now, we’ll build the static version, but notice that we’re utilizing a subclass of Plotly’s graph objects called the FigureWidget. Using this class of visualization will allow us to add the user interactivity that we’re looking for.
# set default x and y axis values
x = ‘sepal length (cm)’
y = ‘sepal width (cm)’# create df subsets by species for use in traces
df_setosa = df[df.species == ‘setosa’]
df_versicolor = df[df.species == ‘versicolor’]
df_virginica = df[df.species == ‘virginica’]# create traces for each species
setosa = go.Scatter(x=df_setosa[x], y=df_setosa[y],
mode=’markers’,
name=’setosa’)
versicolor = go.Scatter(x=df_versicolor[x], y=df_versicolor[y],
mode=’markers’,
name=’versicolor’)
virginica = go.Scatter(x=df_virginica[x], y=df_virginica[y],
mode=’markers’,
name=’virginica’)# create the graph object as a FigureWidget
g = go.FigureWidget(data=[setosa, versicolor, virginica],
layout=go.Layout(
title={
‘text’: “Iris Measurements”,
‘y’:0.85,
‘x’:0.5,
‘xanchor’: ‘center’,
‘yanchor’: ‘top’},
xaxis_title=x,
yaxis_title=y,
legend_title = “Iris Species”
))# use g.show() to see a preview of the scatterplot
Now, we want users to be able to modify the variables on the x and y axis. To do this, we can create dropdown menu widgets for each axis. We don’t need to display them in the notebook at this point, but if you want to test them out, use the command specified below.
from ipywidgets import Dropdownselect_x = Dropdown(options = data.feature_names,
layout = Layout(width = ‘160px’))
select_y = Dropdown(options = data.feature_names[::-1],
layout = Layout(width = ‘160px’))
# use HBox([select_x, select_y]) to see a preview of the widgets
The next step is to specify how the widget should interact with the visualization. The first step is to write a function that specifies which values should be updated any time a widget value is changed. To interpret the function, recall that our figure is stored as a variable named g and that we are looking to update two of its parameters each time a widget value is changed: data (we want to change our x and y axes) and layout (this parameter specifies our axis labels, which we also want updated to reflect the data being displayed).
def response(change): with g.batch_update():
# this first line is saying: g.data[0].x = df_setosa[select_x.value]
g.data[1].x = df_versicolor[select_x.value]
g.data[2].x = df_virginica[select_x.value] g.data[0].y = df_setosa[select_y.value]
g.data[1].y = df_versicolor[select_y.value]
g.data[2].y = df_virginica[select_y.value] g.layout = go.Layout(
title={
‘text’: “Iris Measurements”,
‘y’:0.85,
‘x’:0.5,
‘xanchor’: ‘center’,
‘yanchor’: ‘top’},
xaxis_title=select_x.value,
yaxis_title=select_y.value,
legend_title = “Iris Species”
)
Our final step in creating the interactive visualization is to tell the widgets to call the response function any time either widget is changed. We can do this using IPYWidgets’ observe function:
select_x.observe(response, names=”value”)
select_y.observe(response, names = “value”)
Now all we have to do is display the widgets along with the visualization. We can do this using what IPYWidgets calls “container widgets,” which essentially allow you to specify with what format you want your widgets displayed.
from ipywidgets import HBox, VBoxcontainer = HBox([select_x, select_y])
VBox([container, g])
You should see an interactive output in your Jupyter notebook. Save the changes in your notebook and refresh or restart your Voilà application. You should now be able to see your interactive visualization in the browser.
Part III: Enabling User Interaction with a Machine Learning Model
The second component of our web app will be an interface where the user can input custom Iris measurements and find out which species the model assigns to those measurements.
For reference, here is the code used to build the model being used in this example. However, in the actual notebook used to build your web app, plan to have your model saved as a file and loaded in.
# in a separate notebook...from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics
import picklefeatures = [‘sepal length (cm)’ , ‘sepal width (cm)’, ‘petal length (cm)’, ‘petal width (cm)’]
X_train, X_test, y_train, y_test = train_test_split(df[features], df.species, test_size = 0.25, stratify = df[‘species’], random_state = 42)knn = KNeighborsClassifier(n_neighbors=8)
knn.fit(X_train, y_train)pred = knn.predict(X_test)
print(‘accuracy = ‘, metrics.accuracy_score(y_test, pred))# use pickle to save the model
with open('knn.pickle', 'wb') as to_write:
pickle.dump(knn, to_write)
Alright, back to the main notebook. Let’s load the model:
knn_model = pickle.load(open(‘knn.pickle’, ‘rb’))
Now let’s create four widgets, which will allow the user to type in their custom Iris measurements:
sepal_length = BoundedFloatText(value = 8, min = 4.0, max = 8.0, step = 0.2, description = ‘sepal length’)sepal_width = BoundedFloatText(value = 8, min = 1.8, max = 4.6, step = 0.2, description = ‘sepal width’)petal_length = BoundedFloatText(value = 8, min = 0.8, max = 7, step = 0.2, description = ‘petal length’)petal_width = BoundedFloatText(value = 8, min = 0.0, max = 2.8, step = 0.2, description = ‘petal width’)
Before, we used the observe function to connect the widgets to specify how we wanted the widgets to interact with our other components. This example will show how to make use of a different function called interact.
Now that we’ve created our four new widgets above, we can write a function to specify how we want the widget values to be used. Notice that a new widget is created within this model called out. This widget will allow the predicted Iris species to be displayed.
from ipywidgets import Output, HTMLdef run_model(sepal_length, sepal_width, petal_length, petal_width):
result = knn.predict([[sepal_length, sepal_width, petal_length, petal_width]])[0]
out = Output(layout={‘border’: ‘1px solid white’})
with out:
display(HTML(value=f’Predicted species: <b>{result.capitalize()}</b>’))
return out
Lastly, we can put it all together using the interact function:
interact(run_model,
sepal_length = BoundedFloatText(value = 8, min = 4.0, max = 8.0, step = 0.2, description = ‘sepal length’),
sepal_width = BoundedFloatText(value = 8, min = 1.8, max = 4.6, step = 0.2, description = ‘sepal width’),
petal_length = BoundedFloatText(value = 8, min = 0.8, max = 7, step = 0.2, description = ‘petal length’),
petal_width = BoundedFloatText(value = 8, min = 0.0, max = 2.8, step = 0.2, description = ‘petal width’),
);
Part IV: Additional Styling
Voilà comes with a few built in styling methods:
- Themes allow you to do make easy styling changes like switching to dark mode, which can be done by making a modification to your terminal command:
$ voila <filename>.ipynb --theme=dark
- Templates can be helpful for making further modifications to the layout of your app. The most popular template seems to be the gridstack template, which allows you to drag and reshape each component in the browser view. This can also be implemented using the command line:
$ voila <filename>.ipynb --template=gridstack
Part V: Deployment
Voilà apps can be deployed using Binder, Heroku, Google App Engine, or a private server. See here for more information.