【问题标题】:Have 2 Ipywidgets acting on one matplotlib plot in Jupyter - Python在 Jupyter 中的一个 matplotlib 图上有 2 个 Ipywidgets - Python
【发布时间】:2018-09-08 19:13:48
【问题描述】:

以下代码模拟机器学习、线性回归过程。

它旨在允许用户在 Jupyter 笔记本中手动和直观地进行回归,以更好地了解线性回归过程。

函数的第一部分 (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

【问题讨论】:

  • 线和散点图都在同一个图中。因此,如果该图中的任何内容发生变化,则需要重新绘制。从这个意义上说,您想要实现的目标并不太清楚。
  • 我希望能够在不改变散点图的情况下改变斜线。但是如果不在函数中使用 plt.plot() ,我无法让它重绘。如果我使用坐标轴,它不会画任何东西

标签: python numpy matplotlib jupyter ipywidgets


【解决方案1】:

interactive 函数并不能真正让您访问这种粒度级别。它总是运行整个scatterplt 回调。基本上,interactive 的目的是让一类问题变得非常简单——一旦你摆脱了这类问题,它就不再适用了。

然后,您必须退回到小部件机制的其余部分。最初这可能有点难以理解,因此,为了尽量减少跳跃,我将首先解释 interactive 在幕后所做的事情。

当您调用interactive(func, widget) 时,它会创建widget 并在widget 更改时绑定回调。回调在 Output 小部件 (docs) 中运行 funcOutput 小部件捕获func 的全部输出。 interactive 然后将widget 和输出小部件打包到VBox(用于堆叠小部件的容器)中。

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

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

为了满足(1),我们可能应该创建一个类来维护状态。为了满足(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。特别是,Chakri Cherukuri 有一个很棒的example of linear regression,这与您正在尝试做的事情有些相似。

【讨论】:

  • 这里的答案让我感到惊讶。非常感谢您花这么多时间来创建一个可行的解决方案并为我提供其他选择的建议。 Pascal,你是这个库的主要贡献者。你非常好心,你花时间制作这些伟大的图书馆,帮助我学习。非常感谢
  • 非常感谢您的客气话。祝你好运!
【解决方案2】:

除了使用interactive/interact,您还可以使用interact_manual(有关更多信息,请参阅the docs)。 您得到的是一个按钮,您可以在满意时手动运行该功能。

你需要这两行

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

第一次运行时,您应该会看到:

单击按钮后,它将显示完整的输出:

【讨论】:

【解决方案3】:

部分问题是很难修改 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])

【讨论】:

  • 非常感谢!这很棒。我回家后会测试它。我在使用 Matplotlib,但我愿意使用任何工具。我知道的工具越多,我就越能选择最适合该任务的工具。我真的很感谢你的帮助。 :-)
  • 没问题。 Matplotlib 非常适合它的功能,但对于交互性,bqplot 和 ipywidget 框架很高兴使用。可以通过单击和拖动绘图上的点来控制图形上的斜率。我可以稍后更新答案。
猜你喜欢
  • 2012-09-27
  • 1970-01-01
  • 2021-08-22
  • 1970-01-01
  • 2020-11-17
  • 2021-05-11
  • 2021-05-27
  • 1970-01-01
  • 2018-06-20
相关资源
最近更新 更多