在 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 funcOutput 小部件捕获 func 的全部输出。 interactive 然后将 widget 和输出小部件打包到 VBox(用于堆叠小部件的容器)中。

回到你现在想做的事。您的申请符合以下条件:

  1. 我们需要维护某种形式的内部状态:应用程序需要记住随机变量的 x 和 y 位置
  2. 根据触发的滑块,我们需要与 运行 不同的行为。

为了满足 (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])