3

The following code simulates a machine learning, linear regression process.

It is meant to allow the user to do the regression manually and visually in a Jupyter notebook to get a better feel for the linear regression process.

The first section (x,y) of the function generates a plot to perform the regression on.

The next section (a,b) generates the line to play with, for the simulated regression.

I want to be able to change the slope slider without the scatter plot being regenerated.

Any guidance will be very helpful and welcome. :-)

import numpy as np import ipywidgets as widgets from ipywidgets import interactive import matplotlib.pyplot as plt def scatterplt(rand=3, num_points=20, slope=1): x = np.linspace(3, 9, num_points) y = np.linspace(3, 9, num_points) #add randomness to scatter pcent_rand = rand pcent_decimal = pcent_rand/100 x = [n*np.random.uniform(low=1-pcent_decimal, high=1+ pcent_decimal) for n in x] y = [n*np.random.uniform(low=1-pcent_decimal, high=1+ pcent_decimal) for n in y] #plot regression line a = np.linspace(0, 9, num_points) b = [(slope * n) for n in a] #format & plot the figure plt.figure(figsize=(9, 9), dpi=80) plt.ylim(ymax=max(x)+1) plt.xlim(xmax=max(x)+1) plt.scatter(x, y) plt.plot(a, b) plt.show() #WIDGETS interactive_plot = interactive(scatterplt, rand = widgets.FloatSlider( value=3, min=0, max=50, step=3, description='Randomness:', num_points=(10, 50, 5) ), num_points = widgets.IntSlider( value=20, min=10, max=50, step=5, description='Number of points:' ), slope=widgets.FloatSlider( value=1, min=-1, max=5, step=0.1, description='Slope' ) ) interactive_plot 
2
  • Both, the line and the scatter plot reside in the same figure. So if anything in that figure changes it needs to be redrawn. In that sense it's not too clear what you are trying to achieve. Commented Sep 10, 2018 at 0:35
  • I want to be able to change the slope line without the scatter plot changing. But I can’t get it to redraw without using plt.plot() in the function. If I use axes instead it doesn’t draw anything Commented Sep 10, 2018 at 5:59

3 Answers 3

3

The interactive function does not really give you access to this level of granularity. It always runs the entire scatterplt callback. Basically, the point of interactive is to make a class of problems really easy -- once you move out of that class of problems, it's not really applicable.

You then have to fall back to the rest of the widget machinery. This can be a bit hard to understand initially, so, to minimize the jump, I'll start by explaining what interactive does under the hood.

When you call interactive(func, widget), it creates widget and binds a callback to whenever that widget changes. The callback runs func in an Output widget (docs). The Output widget captures the entire output of func. interactive then packs widget and the output widget into a VBox (a container for stacking widgets).

Back to what you want to do now. Your application has the following criteria:

  1. we need to maintain some form of internal state: the application needs to remember the x and y locations of the random variates
  2. we need different behaviour to run based on what slider was triggered.

To satisfy (1), we should probably create a class to maintain the state. To satisfy (2), we need different callbacks to run based on what slider was called.

Something like this seems to do what you need:

import numpy as np import ipywidgets as widgets import matplotlib.pyplot as plt class LinRegressDisplay: def __init__(self, rand=3.0, num_points=20, slope=1.0): self.rand = rand self.num_points = num_points self.slope = slope self.output_widget = widgets.Output() # will contain the plot self.container = widgets.VBox() # Contains the whole app self.redraw_whole_plot() self.draw_app() def draw_app(self): """ Draw the sliders and the output widget This just runs once at app startup. """ self.num_points_slider = widgets.IntSlider( value=self.num_points, min=10, max=50, step=5, description='Number of points:' ) self.num_points_slider.observe(self._on_num_points_change, ['value']) self.slope_slider = widgets.FloatSlider( value=self.slope, min=-1, max=5, step=0.1, description='Slope:' ) self.slope_slider.observe(self._on_slope_change, ['value']) self.rand_slider = widgets.FloatSlider( value=self.rand, min=0, max=50, step=3, description='Randomness:', num_points=(10, 50, 5) ) self.rand_slider.observe(self._on_rand_change, ['value']) self.container.children = [ self.num_points_slider, self.slope_slider, self.rand_slider , self.output_widget ] def _on_num_points_change(self, _): """ Called whenever the number of points slider changes. Updates the internal state, recomputes the random x and y and redraws the plot. """ self.num_points = self.num_points_slider.value self.redraw_whole_plot() def _on_slope_change(self, _): """ Called whenever the slope slider changes. Updates the internal state, recomputes the slope and redraws the plot. """ self.slope = self.slope_slider.value self.redraw_slope() def _on_rand_change(self, _): self.rand = self.rand_slider.value self.redraw_whole_plot() def redraw_whole_plot(self): """ Recompute x and y random variates and redraw whole plot Called whenever the number of points or the randomness changes. """ pcent_rand = self.rand pcent_decimal = pcent_rand/100 self.x = [ n*np.random.uniform(low=1-pcent_decimal, high=1+pcent_decimal) for n in np.linspace(3, 9, self.num_points) ] self.y = [ n*np.random.uniform(low=1-pcent_decimal, high=1+pcent_decimal) for n in np.linspace(3, 9, self.num_points) ] self.redraw_slope() def redraw_slope(self): """ Recompute slope line and redraw whole plot Called whenever the slope changes. """ a = np.linspace(0, 9, self.num_points) b = [(self.slope * n) for n in a] self.output_widget.clear_output(wait=True) with self.output_widget as f: plt.figure(figsize=(9, 9), dpi=80) plt.ylim(ymax=max(self.y)+1) plt.xlim(xmax=max(self.x)+1) plt.scatter(self.x, self.y) plt.plot(a, b) plt.show() app = LinRegressDisplay() app.container # actually display the widget 

As a final note, the animation remains a bit jarring when you move the sliders. For better interactivity, I suggest looking at bqplot. In particular, Chakri Cherukuri has a great example of linear regression that is somewhat similar to what you are trying to do.

Sign up to request clarification or add additional context in comments.

2 Comments

It amazes me how generous people can be with answers here. Thank you very much for taking so much time to create a working solution and to give me advice for other options. Pascal, you are a key contributor to this library. It is very kind of you, someone who spends time making these great libraries, to help me learn. I am very grateful
Thanks very much for the kind words. Good luck with this!
3

Part of the problem is that it is difficult to modify individual elements in a Matplotlib figure i.e. it is much easier to redraw the whole plot from scratch. Redrawing the whole figure will not be super quick or smooth. So instead, I am showing you an example of how to do it in BQplot (as suggested by Pascal Bugnion). Its not Matplotlib as I guess you probably wanted but it does demonstrate a method of separating the slope and randomness instructions and calculations from each individual slider whilst still using the standard interactive widgets.

enter image description here

import bqplot as bq import numpy as np import ipywidgets as widgets def calcSlope(num_points, slope): a = np.linspace(0, 9, num_points) b = a * slope line1.x = a line1.y = b def calcXY(num_points, randNum): x = np.linspace(3, 9, num_points) y = x #add randomness to scatter x = np.random.uniform(low=1-randNum/100, high=1+ randNum/100, size=(len(x))) * x y = np.random.uniform(low=1-randNum/100, high=1+ randNum/100, size=(len(y))) * y #format & plot the figure x_sc.min = x.min() x_sc.max = x.max() + 1 scat.x = x scat.y = y def rand_int(rand): calcXY(num_i.children[0].value, rand) def num_points_int(num_points): calcXY(num_points, rand_i.children[0].value) calcSlope(num_points, slope_i.children[0].value) def slope_int(slope): calcSlope(num_i.children[0].value, slope) rand_i = widgets.interactive(rand_int, rand = widgets.FloatSlider( value=3, min=0, max=50, step=3, description='Randomness:', num_points=(10, 50, 5) ) ) num_i = widgets.interactive(num_points_int, num_points = widgets.IntSlider( value=20, min=10, max=50, step=5, description='Number of points:' ) ) slope_i = widgets.interactive(slope_int, slope=widgets.FloatSlider( value=1, min=-1, max=5, step=0.1, description='Slope' ) ) # Create the initial bqplot figure x_sc = bq.LinearScale() ax_x = bq.Axis(label='X', scale=x_sc, grid_lines='solid', tick_format='0f') ax_y = bq.Axis(label='Y', scale=x_sc, orientation='vertical', tick_format='0.2f') line1 = bq.Lines( scales={'x': x_sc, 'y': x_sc} , colors=['blue'],display_legend = False, labels=['y1'],stroke_width = 1.0) scat = bq.Scatter(scales={'x': x_sc, 'y': x_sc} , colors=['red'],display_legend = False, labels=['y1'],stroke_width = 1.0) calcSlope(num_i.children[0].value, slope_i.children[0].value) calcXY(num_i.children[0].value, rand_i.children[0].value) m_fig = dict(left=100, top=50, bottom=50, right=100) fig = bq.Figure(axes=[ax_x, ax_y], marks=[line1,scat], fig_margin=m_fig, animation_duration = 1000) widgets.VBox([rand_i,num_i,slope_i,fig]) 

2 Comments

Thank you so much! This is great. I’ll test it when I get home. I WAS using Matplotlib but I am open to using any tool. The more tools I know the more I can select the best fit for the task. I really appreciate your help. :-)
No problem. Matplotlib is great for what it does but for interactivity, bqplot and ipywidget framework is a pleasure to work with. The slope on the graph could be controlled by clicking and dragging points on the plot. I could update the answer later.
2

Instead of using interactive/interact you can also use interact_manual (see the docs for more info). What you get is a button that lets you manually run the function once you are happy.

You need these two lines

from ipywidgets import interactive, interact_manual interactive_plot = interact_manual(scatterplt, ... 

The first time you run it, you should see this: enter image description here

After you click the button, it will show you the full output: enter image description here

1 Comment

Another option is debouncing, that will auto execute after X msec. I've put some code here github.com/jupyter-widgets/ipywidgets/issues/663 but with my current environment, I cannot seem to get this working.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.