在 Jupyter 中有 2 个 Ipywidgets 作用于一个 matplotlib 图 - Python
Have 2 Ipywidgets acting on one matplotlib plot in Jupyter - Python
下面的代码模拟了一个机器学习、线性回归的过程。
旨在允许用户在 Jupyter notebook 中手动和可视化地进行回归,以更好地感受线性回归过程。
函数的第一部分 (x,y) 生成一个图来执行回归。
下一部分 (a,b) 生成用于模拟回归的直线。
我希望能够在不重新生成散点图的情况下更改斜率滑块。
任何指导都将非常有帮助和受欢迎。 :-)
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
interactive
函数并不能真正让您访问这种级别的粒度。它总是 运行 整个 scatterplt
回调。基本上,interactive
的目的是让 class 的问题变得非常简单——一旦你摆脱了 class 的问题,它就不再适用了。
然后您必须回退到其余的小部件机制。最初这可能有点难以理解,因此,为了尽量减少跳跃,我将首先解释 interactive
在幕后做了什么。
当您调用 interactive(func, widget)
时,它会创建 widget
并在 widget
发生变化时绑定回调。 Output
小部件 (docs) 中的回调 运行s func
。 Output
小部件捕获 func
的全部输出。 interactive
然后将 widget
和输出小部件打包到 VBox
(用于堆叠小部件的容器)中。
回到你现在想做的事。您的申请符合以下条件:
- 我们需要维护某种形式的内部状态:应用程序需要记住随机变量的 x 和 y 位置
- 根据触发的滑块,我们需要与 运行 不同的行为。
为了满足 (1),我们可能应该创建一个 class 来维护状态。为了满足 (2),我们需要根据调用的滑块对 运行 进行不同的回调。
像这样的东西似乎可以满足您的需要:
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
最后一点,当您移动滑块时,动画仍然有点不和谐。为了获得更好的交互性,我建议查看 bqplot. In particular, Chakri Cherukuri has a great example of linear regression,它与您正在尝试做的有点相似。
除了使用 interactive
/interact
,您还可以使用 interact_manual
(有关详细信息,请参阅 the docs)。
您得到的是一个按钮,可以让您在满意后手动 运行 该功能。
你需要这两行
from ipywidgets import interactive, interact_manual
interactive_plot = interact_manual(scatterplt,
...
你第一次 运行 它,你应该看到这个:
点击按钮后,它会显示完整的输出:
部分问题是很难修改 Matplotlib 图中的单个元素,即从头开始重新绘制整个图要容易得多。重新绘制整个图形不会非常快速或流畅。因此,我将向您展示如何在 BQplot 中执行此操作的示例(如 Pascal Bugnion 所建议的那样)。它不是我猜你可能想要的 Matplotlib,但它确实展示了一种方法,将斜率和随机性指令和计算与每个单独的滑块分开,同时仍然使用标准的交互式小部件。
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])
下面的代码模拟了一个机器学习、线性回归的过程。
旨在允许用户在 Jupyter notebook 中手动和可视化地进行回归,以更好地感受线性回归过程。
函数的第一部分 (x,y) 生成一个图来执行回归。
下一部分 (a,b) 生成用于模拟回归的直线。
我希望能够在不重新生成散点图的情况下更改斜率滑块。
任何指导都将非常有帮助和受欢迎。 :-)
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
interactive
函数并不能真正让您访问这种级别的粒度。它总是 运行 整个 scatterplt
回调。基本上,interactive
的目的是让 class 的问题变得非常简单——一旦你摆脱了 class 的问题,它就不再适用了。
然后您必须回退到其余的小部件机制。最初这可能有点难以理解,因此,为了尽量减少跳跃,我将首先解释 interactive
在幕后做了什么。
当您调用 interactive(func, widget)
时,它会创建 widget
并在 widget
发生变化时绑定回调。 Output
小部件 (docs) 中的回调 运行s func
。 Output
小部件捕获 func
的全部输出。 interactive
然后将 widget
和输出小部件打包到 VBox
(用于堆叠小部件的容器)中。
回到你现在想做的事。您的申请符合以下条件:
- 我们需要维护某种形式的内部状态:应用程序需要记住随机变量的 x 和 y 位置
- 根据触发的滑块,我们需要与 运行 不同的行为。
为了满足 (1),我们可能应该创建一个 class 来维护状态。为了满足 (2),我们需要根据调用的滑块对 运行 进行不同的回调。
像这样的东西似乎可以满足您的需要:
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
最后一点,当您移动滑块时,动画仍然有点不和谐。为了获得更好的交互性,我建议查看 bqplot. In particular, Chakri Cherukuri has a great example of linear regression,它与您正在尝试做的有点相似。
除了使用 interactive
/interact
,您还可以使用 interact_manual
(有关详细信息,请参阅 the docs)。
您得到的是一个按钮,可以让您在满意后手动 运行 该功能。
你需要这两行
from ipywidgets import interactive, interact_manual
interactive_plot = interact_manual(scatterplt,
...
你第一次 运行 它,你应该看到这个:
点击按钮后,它会显示完整的输出:
部分问题是很难修改 Matplotlib 图中的单个元素,即从头开始重新绘制整个图要容易得多。重新绘制整个图形不会非常快速或流畅。因此,我将向您展示如何在 BQplot 中执行此操作的示例(如 Pascal Bugnion 所建议的那样)。它不是我猜你可能想要的 Matplotlib,但它确实展示了一种方法,将斜率和随机性指令和计算与每个单独的滑块分开,同时仍然使用标准的交互式小部件。
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])