问题
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
回答1:
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:
- we need to maintain some form of internal state: the application needs to remember the x and y locations of the random variates
- 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.
回答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:
After you click the button, it will show you the full output:
回答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.
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])
来源:https://stackoverflow.com/questions/52238567/have-2-ipywidgets-acting-on-one-matplotlib-plot-in-jupyter-python