Data Visualization with Python HoloViz: Interactive Plots and Widgets in Jupyter
By: Ernest Kim
Tags: Data Science, Data Visualization, Python, Dashboard, Bokeh

The Goal of the Visualization

With data representing a ground “truth” of binary classification, and predicted values (floats ranging from 0 to 1.0), I’m going to put together a dashboard in order to:

Generate Hard Predictions Show a confusion matrix Evaluate the Classifier through the AUC curve and a Precision-Recall Curve

The “Data”

Basically, I created an artificial set of binary categories (85% / 15%), threw random data at each bin, and then set the values between [0,1]. This should look a lot like the result of a binary classifier from scikit-learn.

Generating Hard Predictions

This is the key part of the interactive portion of this visualization. At the top of the dashboard, there will be a handy slider whose values will represent cut-off values (above that value, assume category 1, below, assume 0).

By default, I initialize the cutoff value to maximize the F1-Score. In theory, if we could have perfect precision and recall, this quantity should be 1.

As the slider moves through the various cutoff values, the rest of the visualization should convey changes in the various other metrics. One of the best ways to capture this relationship is through a confusion matrix, or a 2x2 table showing the results of the prediction against the actual values.

The Confusion Matrix

To achieve this visual element, I’ll be using the hv.HeatMap plot, along with some tricks to make it behave. Getting customized axes and tick marks proved to be rather difficult, so instead, I’ll also use hv.Labels to make it explicitly clear what the confusion matrix is showing:

A Confusion Matrix! Hopefully the diagonal has the big values.

The tricky part here was disabling the axes and positioning things correctly. The heat map is really a 2x2, with ranges (0,1) on both x and y. So in order to place something in the top left quadrant, you need to refer to it with a tuple corresponding to (x,y): (0,1,VAL) where VAL is the actual value of the heat map or the corresponding label. I created two lists and used a map to sort things in the right order.

AUC and Precision-Recall Curves

The code for generating these curves is pretty simple as I’ll continue leaning on HoloViews:

# for the AUC, we need only plot our FP vs TP

hv.Curve(data[:,[3,1]]). \

opts(xrotation=45,

xlabel='False Positive',

ylabel='True Positive')

AUC Curve… My fake data is a little too easy to “classify”

# for the PR curve, we need only plot recall vs precision

hv.Curve(data[:,[6,5]]). \

opts(xlim=(0,1),

ylim=(0,1))

Very Precision, Much Recall.

A Layout and Putting it Together

The individual components are pretty easy to slap together, but now I’ll bring it all into a single view. The slider will now iterate through the various cut-off values while the rest of the plots update.

In this way, we can see different F1 Scores, changes to the confusion matrix, and where on the PR curve each cutoff will land.

For this section, I’ll be introducing two complications. The big one, wrapping everything in a class, is a practice I use to keep things organized. With the second, I’ll be using a layout instead of running each widget in a separate cell. The layouts that come with panel are fairly simple and do a good job of letting you track widgets as you add them. For this dashboard, I’ll often refer to a widget by it’s position in the layout rather than directly:

Hey would you look at that, an interactive dashboard!

The class code shouldn’t be too horrible. There are basically five components:

(During Initialization) Defining all of the data I’ll be using. I try to pre-calculate everything I’ll need beforehand (or optimize its calculation) to smooth out the user experience. (During Initialization) Initialize the widgets with default values or some initial plot. (During Initialization) Assign watchers. Because everything is wrapped in a class, the actual callbacks can come later. Define callbacks. Remember, these are the functions that are triggered by interacting with some widget and subsequently, modify other widgets or layouts. Create plotting functions. These functions basically create a plot when called. During a callback, plots will get created for each change induced by the watchers.

The actual plotting code should be fairly straightforward. Personally, the hardest part of plotting is getting the display options to look right. HoloViews does a pretty good job laying out the options either in the docstrings or the help function, invoked with: hv.help(hv.Curve) # or any hv plot

You may notice under the ### layouts ### section, I actually use several layouts. You can use a pn.gridspec to make one super layout, but I find it’s simplest to think in rows and columns. The pn.layout.row and pn.layout.column also do a great job at centering and dealing with margins, saving a lot of headache. Using these also makes referring to or updating widgets in those layouts a lot easier.

Lastly, I want to point out that if you intend to work primarily in a notebook, you do not need to use classes or layouts. Widgets in different cells will still update as long as the linking code (callback/watcher) is working properly.

Again, follow along with the notebook at: https://github.com/ernestk-git/holoviz_extended/blob/master/Panel_Interactive.ipynb