The inspiration of my previous kernel density estimation post was a blog post by Michael Lerner , who used myJSAnimation tools to do a nice interactive demo of the relationship between kernel density estimation and histograms.
This was on the heels of Brian Granger 's excellent PyData NYC Keynote where he live-demoed the brand new IPython interactive tools. This new functionality is very cool. Wes McKinney remarked that day on Twitter that "IPython's interact machinery is going to be a huge deal". I completely agree: the ability to quickly generate interactive widgets to explore your data is going to change the way a lot of people do their daily scientific computing work.
But there's one issue with the new widget framework as it currently stands: unless you're connected to an IPython kernel (i.e. actually running IPython to view your notebook), the widgets are useless. Don't get me wrong: they're incredibly cool when you're actually interacting with data. But the bread-and-butter of this blog and many others is static notebook views: for this purpose, widgets with callbacks to the Python kernel aren't so helpful.
This is where ipywidgets
comes in.
I've been thinking about this issue for the past few weeks, but this morning as I was biking to work through Seattle's current cold-snap, the idea came to me. Interactive widgets can be done, and done rather easily. I got to work, and after doing a few things I couldn't put off, decided to forego my real research for the day and instead focus on the betterment of humanity (or at least the growing portion of humanity who use the IPython notebook for their work). For anyone who follows me on Twitter , you might say I chose option B . The result is ipywidgets
: a rough attempt at what, with some effort, might become a very useful library. You can view the current progress on my GitHub page. To run the code in this notebook, clone that repository and type python setup.py install
. The package is pure python, very lightweight, and should install painlessly on most systems.
The basic idea of creating these widgets is this: you set up a function that does something interesting, you specify the range of parameter choices, and call a function which pre-generates the results and displays a javascript slider which allows you to interact with these results. Here's a quick example of how it works:
First we set up a function which takes some arguments and plots something:
In [1]:
%matplotlib inlineimport numpy as npimport matplotlib.pyplot as pltdef plot(amplitude, color): fig, ax = plt.subplots(figsize=(4, 3), subplot_kw={'axisbg':'#EEEEEE', 'axisbelow':True}) ax.grid(color='w', linewidth=2, linestyle='solid') x = np.linspace(0, 10, 1000) ax.plot(x, amplitude * np.sin(x), color=color, lw=5, alpha=0.4) ax.set_xlim(0, 10) ax.set_ylim(-1.1, 1.1) return fig
Next, you import some tools from ipywidgets
, and interact with your plot:
In [2]:
from ipywidgets import StaticInteract, RangeWidget, RadioWidgetStaticInteract(plot, amplitude=RangeWidget(0.1, 1.0, 0.1), color=RadioWidget(['blue', 'green', 'red']))
Out[2]:
blue: green: red:
That's all there is to it!
Because this is a static view, all the output must be pre-generated and saved in the notebook. The way this works is to save the generated frames within divs that are hidden and shown whenever the widget is changed. Here's a rough sample that gives the idea of what's going on with the HTML and Javascript in the background:
First we define a javascript function which takes the values from HTML5 input
blocks, and shows and hides items based on those inputs.
In [3]:
JS_FUNCTION = """<script type="text/javascript"> function interactUpdate(div){ var outputs = div.getElementsByTagName("div"); var controls = div.getElementsByTagName("input"); var value = ""; for(i=0; i<controls.length; i++){ if((controls[i].type == "range") || controls[i].checked){ value = value + controls[i].getAttribute("name") + controls[i].value; } } for(i=0; i<outputs.length; i++){ var name = outputs[i].getAttribute("name"); if(name == value){ outputs[i].style.display = 'block'; } else if(name != "controls"){ outputs[i].style.display = 'none'; } } }</script>"""
Next we create a few divs with different outputs: here our outputs are simply the numbers one through 4, along with their text and roman numeral representations. We also define the input slider with the appropriate callback:
In [4]:
WIDGETS = """<div> <div name="num1", style="display:block"> <p style="font-size:20px;">1: one (I)</p> </div> <div name="num2", style="display:none"> <p style="font-size:20px;">2: two (II)</p> </div> <div name="num3", style="display:none"> <p style="font-size:20px;">3: three (III)</p> </div> <div name="num4", style="display:none"> <p style="font-size:20px;">4: four (IV)</p> </div><input type="range" name="num" min="1" max="4", step="1" style="width:200px", onchange="interactUpdate(this.parentNode);" value="1"></div>"""
Now if we run and view this script, we see a simple slider which shows and hides the div
s in the way we expect.
In [5]:
from IPython.display import HTMLHTML(JS_FUNCTION + WIDGETS)
Out[5]:
1: one (I)
That's all there is to it!
After writing some Python scripts to generate the HTML and Javascript code for us, we have our static interactive widgets.
Now that this is in place, let's look at a few more examples of what this allows:
The simplest thing we can do is simply save some textual output. Let's write a function which displays the first Fibonacci numbers, and tie it to a slider:
In [6]:
def show_fib(N): sequence = "" a, b = 0, 1 for i in range(N): sequence += "{0} ".format(a) a, b = b, a + b return sequencefrom ipywidgets import StaticInteract, RangeWidgetStaticInteract(show_fib, N=RangeWidget(1, 100))
Out[6]:
0
A slightly silly example, sure, but it shows just how easy it is to do this!
For some fancier mathematical gymnastics, we can turn to SymPy. SymPy is a package which does Symbolic computation in Python. It has the ability to nicely render its output in the IPython notebook. Let's take a look at a simple factoring example with Sympy (the following requires SymPy 0.7 or greater):
In [7]:
# Initialize notebook printingfrom sympy import init_printinginit_printing()# Create a factorization functionfrom sympy import Symbol, Eq, factorx = Symbol('x')def factorit(n): return Eq(x ** n - 1, factor(x ** n - 1))# Make it interactive!from ipywidgets import StaticInteract, RangeWidgetStaticInteract(factorit, n=RangeWidget(2, 20))
Out[7]:
By moving the slider, we see the factorization of the resulting polynomial.
And of course, you can use this to display matplotlib plots. Keep in mind, though, that every image must be pre-generated and stored in the notebook, so if you have a large number of settings (or combinations of multiple settings), the notebook size will blow up very quickly!
For this example, I want to quickly revisit the post which inspired me, and compare the kernel density estimation of a distribution with a couple kernels and bandwidths. We'll make the figure smaller so as to not blow-up the size of this notebook:
In [8]:
%matplotlib inlinefrom sklearn.neighbors import KernelDensityimport numpy as npnp.random.seed(0)x = np.concatenate([np.random.normal(0, 1, 1000), np.random.normal(1.5, 0.2, 300)])def plot_KDE_estimate(kernel, b): bandwidth = 10 ** (0.1 * b) x_grid = np.linspace(-3, 3, 1000) kde = KernelDensity(bandwidth=bandwidth, kernel=kernel) kde.fit(x[:, None]) pdf = np.exp(kde.score_samples(x_grid[:, None])) fig, ax = plt.subplots(figsize=(4, 3), subplot_kw={'axisbg':'#EEEEEE', 'axisbelow':True}) ax.grid(color='w', linewidth=2, linestyle='solid') ax.hist(x, 60, histtype='stepfilled', normed=True, edgecolor='none', facecolor='#CCCCFF') ax.plot(x_grid, pdf, '-k', lw=2, alpha=0.5) ax.text(-2.8, 0.48, "kernel={0}\nbandwidth={1:.2f}".format(kernel, bandwidth), fontsize=14, color='gray') ax.set_xlim(-3, 3) ax.set_ylim(0, 0.601) return fig
In [9]:
from ipywidgets import StaticInteract, RangeWidgetStaticInteract(plot_KDE_estimate, kernel=RadioWidget(['gaussian', 'tophat', 'exponential'], delimiter="<br>"), b=RangeWidget(-14, 8, 2))
Out[9]:
gaussian:
tophat:
exponential:
Sliding the slider adjusts the bandwidth, and the radio buttons change the kernel used.
I'm pretty excited about these tools. I think they'll allow for some really interesting demos and visualizations, especially if more time can be spent polishing them. The package is still very rough: it offers little flexibility in how the widgets are displayed, it only implements radio buttons and a slider, and it still hiccups at times when the slider items are floating-point values (this is due to differences in Javascript's and Python's default representations of floating point numbers). Regardless, I hope you have fun playing with this, and I hope to see some posts using this on other blogs in the near future!
Happy Hacking!
This post was written entirely in the IPython notebook. You can download this notebook, or see a static view here .
聯(lián)系客服