Имейте 2 Ipywidgets, действующих на одном сюжете matplotlib в Jupyter - Python - PullRequest
0 голосов
/ 08 сентября 2018

Следующий код имитирует машинное обучение, процесс линейной регрессии.

Он предназначен для того, чтобы пользователь мог выполнять регрессию вручную и визуально в блокноте 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

Ответы [ 3 ]

0 голосов
/ 10 сентября 2018

Вместо использования interactive / interact вы также можете использовать interact_manual (для получения дополнительной информации см. документы ). То, что вы получаете, это кнопка, которая позволяет вам вручную запускать функцию, когда вы счастливы.

Вам нужны эти две строки

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

При первом запуске вы должны увидеть это: enter image description here

После того, как вы нажмете кнопку, она покажет вам полный вывод: enter image description here

0 голосов
/ 15 сентября 2018

Отчасти проблема заключается в том, что трудно изменить отдельные элементы фигуры Matplotlib, т. Е. Гораздо проще перерисовать весь график с нуля.Перерисовка всей фигуры не будет супер быстрой или плавной.Поэтому вместо этого я покажу вам пример того, как это сделать в BQplot (как предложил Паскаль Бюньон).Это не Matplotlib, как я полагаю, вы, вероятно, хотели, но он демонстрирует метод отделения инструкций и вычислений наклона и случайности от каждого отдельного слайдера, в то же время используя стандартные интерактивные виджеты.

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])
0 голосов
/ 10 сентября 2018

Функция interactive на самом деле не дает вам доступа к этому уровню детализации. Он всегда запускает весь scatterplt обратный вызов. По сути, смысл interactive состоит в том, чтобы сделать класс проблем действительно легким - как только вы выйдете из этого класса проблем, это не очень применимо.

Затем вам придется вернуться к остальной части механизма виджетов. Поначалу это может быть немного трудно понять, поэтому, чтобы минимизировать скачок, я начну с объяснения того, что interactive делает под капотом.

Когда вы вызываете interactive(func, widget), он создает widget и связывает обратный вызов с каждым изменением widget. Обратный вызов выполняется func в виджете Output ( docs ). Виджет Output захватывает весь вывод 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 . В частности, у Чакри Черукури есть отличный пример линейной регрессии , который чем-то похож на то, что вы пытаетесь сделать.

...