在 Jupyter Notebook 中使用 matplotlib 绘制动态变化的图形

Posted

技术标签:

【中文标题】在 Jupyter Notebook 中使用 matplotlib 绘制动态变化的图形【英文标题】:Plot dynamically changing graph using matplotlib in Jupyter Notebook 【发布时间】:2017-02-01 04:37:45 【问题描述】:

我有一个 M x N 二维数组:第 i 行表示在时间 i 的 N 个点的值。

我想以图表的形式可视化点 [数组的 1 行],其中值会在一小段时间间隔后更新。因此,图表一次显示 1 行,然后将值更新到下一行,依此类推。

我想在 jupyter 笔记本中执行此操作。寻找参考代码。

我尝试了以下操作但没有成功:

http://community.plot.ly/t/updating-graph-with-new-data-every-100-ms-or-so/812

https://pythonprogramming.net/live-graphs-matplotlib-tutorial/

Create dynamic updated graph with Python

Update Lines in matplotlib

【问题讨论】:

【参考方案1】:

这里有一个替代的,可能更简单的解决方案:

%matplotlib notebook
import numpy as np
import matplotlib.pyplot as plt

m = 100
n = 100
matrix = np.random.normal(0,1,m*n).reshape(m,n)

fig = plt.figure()
ax = fig.add_subplot(111)
plt.ion()

fig.show()
fig.canvas.draw()

for i in range(0,100):
    ax.clear()
    ax.plot(matrix[i,:])
    fig.canvas.draw()

【讨论】:

这行得通,但是在完全渲染之前,笔记本中的情节看起来非常小。当它完成渲染时,它会调整大小以正确的大小。知道为什么吗? 该图没有出现在 jupyter 实验室中。在只有一条消息:javascript output is disabled in JupyterLab【参考方案2】:

对于一个线程正在抽取数据并且我们希望 Jupyter notebook 不断更新图形而不阻塞任何东西的场景,我一直在寻找一个好的答案。在浏览了大约十几个相关答案后,以下是一些发现:

注意

如果您想要实时图表,请不要使用以下魔法。如果笔记本使用以下内容,则图表更新不起作用:

%load_ext autoreload
%autoreload 2

在导入 matplotlib 之前,您的笔记本中需要以下魔法:

%matplotlib notebook

方法一:使用 FuncAnimation

这有一个缺点,即即使您的数据尚未更新,也会发生图表更新。下面的示例显示了另一个线程在 Jupyter notebook 通过FuncAnimation 更新图表时更新数据。

%matplotlib notebook

from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from random import randrange
from threading import Thread
import time

class LiveGraph:
    def __init__(self):
        self.x_data, self.y_data = [], []
        self.figure = plt.figure()
        self.line, = plt.plot(self.x_data, self.y_data)
        self.animation = FuncAnimation(self.figure, self.update, interval=1000)
        self.th = Thread(target=self.thread_f, daemon=True)
        self.th.start()

    def update(self, frame):
        self.line.set_data(self.x_data, self.y_data)
        self.figure.gca().relim()
        self.figure.gca().autoscale_view()
        return self.line,

    def show(self):
        plt.show()

    def thread_f(self):
        x = 0
        while True:
            self.x_data.append(x)
            x += 1
            self.y_data.append(randrange(0, 100))   
            time.sleep(1)  

g = LiveGraph()
g.show()

方法二:直接更新

第二种方法是在数据从另一个线程到达时更新图形。这是有风险的,因为 matplotlib 不是线程安全的,但只要只有一个线程进行更新,它似乎就可以工作。

%matplotlib notebook

from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from random import randrange
from threading import Thread
import time

class LiveGraph:
    def __init__(self):
        self.x_data, self.y_data = [], []
        self.figure = plt.figure()
        self.line, = plt.plot(self.x_data, self.y_data)

        self.th = Thread(target=self.thread_f, daemon=True)
        self.th.start()

    def update_graph(self):
        self.line.set_data(self.x_data, self.y_data)
        self.figure.gca().relim()
        self.figure.gca().autoscale_view()

    def show(self):
        plt.show()

    def thread_f(self):
        x = 0
        while True:
            self.x_data.append(x)
            x += 1
            self.y_data.append(randrange(0, 100))  

            self.update_graph()

            time.sleep(1)  


from live_graph import LiveGraph

g = LiveGraph()
g.show()

【讨论】:

【参考方案3】:

我对此进行了探索并制作了以下内容,主要是自我记录:

import matplotlib.pyplot as plt
%matplotlib notebook

print('This text appears above the figures')
fig1 = plt.figure(num='DORMANT')
print('This text appears betweeen the figures')
fig2 = plt.figure()
print('This text appears below the figures')

fig1.canvas.set_window_title('Canvas active title')
fig1.suptitle('Figure title', fontsize=20)

# Create plots inside the figures
ax1 = fig1.add_subplot(111)
ax1.set_xlabel('x label')
ax2 = fig2.add_subplot(111)

# Loop to update figures
end = 40
for i in range(end):
    ax2.cla()  # Clear only 2nd figure's axes, figure 1 is ADDITIVE
    ax1.set_title('Axes title')  # Reset as removed by cla()

    ax1.plot(range(i,end), (i,)*(end-i))
    ax2.plot(range(i,end), range(i,end), 'rx')
    fig1.canvas.draw()
    fig2.canvas.draw()

【讨论】:

【参考方案4】:

通过对@Shital Shah 解决方案的适度修改,我创建了一个更通用的框架,可以简单地应用于各种场景:

import matplotlib
from matplotlib import pyplot as plt

class LiveLine:
    def __init__(self, graph, fmt=''):
        # LiveGraph object
        self.graph = graph
        # instant line
        self.line, = self.graph.ax.plot([], [], fmt)
        # holder of new lines
        self.lines = []

    def update(self, x_data, y_data):
        # update the instant line
        self.line.set_data(x_data, y_data)
        self.graph.update_graph()

    def addtive_plot(self, x_data, y_data, fmt=''):
        # add new line in the same figure
        line, = self.graph.ax.plot(x_data, y_data, fmt)
        # store line in lines holder
        self.lines.append(line)
        # update figure
        self.graph.update_graph()
        # return line index
        return self.lines.index(line)

    def update_indexed_line(self, index, x_data, y_data):
        # use index to update that line
        self.lines[index].set_data(x_data, y_data)
        self.graph.update_graph()


class LiveGraph:
    def __init__(self, backend='nbAgg', figure_arg=, window_title=None, 
                 suptitle_arg='t':None, ax_label='x':'', 'y':'', ax_title=None):

        # save current backend for later restore
        self.origin_backend = matplotlib.get_backend()

        # check if current backend meets target backend
        if self.origin_backend != backend:
            print("original backend:", self.origin_backend)
            # matplotlib.use('nbAgg',warn=False, force=True)
            plt.switch_backend(backend)
            print("switch to backend:", matplotlib.get_backend())

        # set figure
        self.figure = plt.figure(**figure_arg)
        self.figure.canvas.set_window_title(window_title)
        self.figure.suptitle(**suptitle_arg)

        # set axis
        self.ax = self.figure.add_subplot(111)
        self.ax.set_xlabel(ax_label['x'])
        self.ax.set_ylabel(ax_label['y'])
        self.ax.set_title(ax_title)

        # holder of lines
        self.lines = []

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def close(self):
        # check if current beckend meets original backend, if not, restore it
        if matplotlib.get_backend() != self.origin_backend:
            # matplotlib.use(self.origin_backend,warn=False, force=True)
            plt.switch_backend(self.origin_backend)
            print("restore to backend:", matplotlib.get_backend())

    def add_line(self, fmt=''):
        line = LiveLine(graph=self, fmt=fmt)
        self.lines.append(line)
        return line

    def update_graph(self):
        self.figure.gca().relim()
        self.figure.gca().autoscale_view()
        self.figure.canvas.draw()

使用以上 2 类,您可以简单地重现 @Graham S 的示例:

import numpy as np

m = 100
n = 100
matrix = np.random.normal(0,1,m*n).reshape(m,n)

with LiveGraph(backend='nbAgg') as h:
    line1 = h.add_line()
    for i in range(0,100):
        line1.update(range(len(matrix[i,:])), matrix[i,:])

请注意,默认后端是nbAgg,您可以传递其他后端,例如qt5Agg。完成后,它将恢复到您原来的后端。

还有@Tom Hale 的例子:

with LiveGraph(figure_arg='num':'DORMANT2', window_title='Canvas active title', 
                suptitle_arg='t':'Figure title','fontsize':20, 
                ax_label='x':'x label', 'y':'', ax_title='Axes title') as g:
    with LiveGraph() as h:
        line1 = g.add_line()
        line2 = h.add_line('rx')
        end = 40
        for i in range(end):
            line1.addtive_plot(range(i,end), (i,)*(end-i))
            line2.update(range(i,end), range(i,end))

此外,您可以更新@Tom Hale 示例的加法图中的特定行:

import numpy as np

with LiveGraph(figure_arg='num':'DORMANT3', window_title='Canvas active title', 
                suptitle_arg='t':'Figure title','fontsize':20, 
                ax_label='x':'x label', 'y':'', ax_title='Axes title') as g:
        line1 = g.add_line()
        end = 40
        for i in range(end):
            line_index = line1.addtive_plot(range(i,end), (i,)*(end-i))

        for i in range(100):
            j = int(20*(1+np.cos(i)))
            # update line of index line_index
            line1.update_indexed_line(line_index, range(j,end), (line_index,)*(end-j))

请注意,第二个 for 循环仅用于更新索引为 line_index 的特定行。您可以将该索引更改为其他行的索引。

就我而言,我在机器学习训练循环中使用它来逐步更新学习曲线。

import numpy as np
import time

# create a LiveGraph object
g = LiveGraph()

# add 2 lines
line1 = g.add_line()
line2 = g.add_line()

# create 2 list to receive training result
list1 = []
list2 = []

# training loop
for i in range(100):
    # just training
    time.sleep(0.1)

    # get training result
    list1.append(np.random.normal())
    list2.append(np.random.normal())

    # update learning curve
    line1.update(np.arange(len(list1)), list1)
    line2.update(np.arange(len(list2)), list2)


# don't forget to close
g.close()

【讨论】:

【参考方案5】:

另一个简单的解决方案,基于IPython.display 函数displayclear_output。我找到了here。这是代码(基于@graham-s 的answer):

from IPython.display import display, clear_output
import numpy as np
import matplotlib.pyplot as plt

%matplotlib inline

m = 100
n = 100
matrix = np.random.normal(0, 1, size=(m, n))

fig = plt.figure()
ax = fig.add_subplot(111)

for i in range(m):
    ax.clear()
    ax.plot(matrix[i, :])
    display(fig)
    clear_output(wait=True)
    plt.pause(0.2)

它使用%matplotlib inline 而不是notebook,并且不会产生@MasterScrat 提到的小图像。适用于 Jupyter Notebook 和 Jupyter Lab。有时图像闪烁不是很好,但可用于快速调查。

如果您需要保持不同帧之间的坐标轴范围,请在ax.clear() 之后添加ax.set_xlim/ax.set_ylim

【讨论】:

【参考方案6】:

除了@0aslam0,我还使用了来自here 的代码。我刚刚更改了 animate 函数以每次下次获取下一行。它绘制了所有 N 个点的动画演化(M 步)。

from IPython.display import html
import numpy as np
from matplotlib import animation
N = 5
M = 100
points_evo_array = np.random.rand(M,N)

# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
ax = plt.axes(xlim=(0, M), ylim=(0, np.max(points_evo_array)))
lines = []

lines = [ax.plot([], [])[0] for _ in range(N)]

def init():    
    for line in lines:
        line.set_data([], [])
    return lines

def animate(i):
    for j,line in enumerate(lines):
        line.set_data(range(i), [points_evo_array[:i,j]])
    return lines

# call the animator.  blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate,np.arange(1, M), init_func=init, interval=10, blit=True)

HTML(anim.to_html5_video())

希望对你有用

【讨论】:

我也看到了那个帖子。但是这篇文章的日期是 2013 年,并且谈到了 IPython。我提到的帖子是 2016 年发布的,讨论了 Jupyter(IPython 的重命名和更新版本) 我实际上直接从我提到的那个网站下载了 Jupyter notebook,只是稍微修复了代码并在我的机器上的 Jupyter 中运行它看起来工作正常 好的。你能告诉你机器的规格吗?对我来说,动画不起作用。我必须将其转换为 HTML 视频才能看到动画。 其实我在 Macbook Air CPU Intel i3 上工作过。据我记得我在使用 ffmpeg lib 时遇到了问题,但安装后它抛出了conda,它开始正常工作。我还更新了代码添加行 HTML(anim.to_html5_video()) 现在它可以使用 display_animation(...) 并且看起来更简单:) 什么是函数 HTML() 当我尝试运行代码时它会引发错误“名称'HTML'未定义”。是否需要额外进口?【参考方案7】:

这是一个处理实时绘图/记录数据的库 (joystick),尽管我不确定它是否与 jupyter 一起使用。您可以使用通常的pip install joystick 安装它。

如果没有关于您的数据的更多详细信息,很难制定可行的解决方案。这是一个选项:

import joystick as jk
import numpy as np

class test(jk.Joystick):
   # initialize the infinite loop decorator
    _infinite_loop = jk.deco_infinite_loop()

    def _init(self, *args, **kwargs):
        """
        Function called at initialization, see the docs
        """
        # INIT DATA HERE
        self.shape = (10, 4) # M, N
        self.data = np.random.random(self.shape)
        self.xaxis = range(self.shape[1])
        ############
        # create a graph frame
        self.mygraph = self.add_frame(
                   jk.Graph(name="TheName", size=(500, 500), pos=(50, 50),
                            fmt="go-", xnpts=self.shape[1], freq_up=5, bgcol="w",
                            xylim=(0, self.shape[1]-1, None, None)))

    @_infinite_loop(wait_time=0.5)
    def _generate_fake_data(self):  # function looped every 0.5 second
        """
        Loop starting with the simulation start, getting data and
        pushing it to the graph every 0.5 seconds
        """
        # NEW (RANDOM) DATA
        new_data = np.random.random(self.shape[1])
        # concatenate data
        self.data = np.vstack((self.data, new_data))
        # push new data to the graph
        self.mygraph.set_xydata(self.xaxis, self.data[-1])

t = test()
t.start()

t.stop()
t.exit()

此代码将创建一个每秒自动更新 5 次 (freq_up=5) 的图表,同时每 0.5 秒 (wait_time=0.5) (随机)生成新数据并将其推送到图表中进行显示。

如果您不希望 Y 轴摆动,请输入 t.mygraph.xylim = (0, t.shape[1]-1, 0, 1)

【讨论】:

【参考方案8】:

我对 matplotlib 或 jupyter 了解不多。但是,图表让我感兴趣。我刚刚做了一些谷歌搜索,发现了这个post。似乎您必须将图形呈现为 HTML 视频才能看到动态图形。

我试过那个帖子。 This 是笔记本,如果你想试试。请注意,内核(python 2)需要一些时间来构建视频。你可以阅读更多关于它的信息here。

现在您想逐行显示图表。我试过this。在那个笔记本中,我有一个有 10 行的 dump_data。我随机取一个并将它们绘制并显示为视频。

了解 jupyter 很有趣。希望这会有所帮助。

【讨论】:

恕我直言,只写 “看看这个外部页面,或者那个” 在 SO 是一个非常糟糕的做法。两年后,这些链接将失效,您的回答将毫无用处。

以上是关于在 Jupyter Notebook 中使用 matplotlib 绘制动态变化的图形的主要内容,如果未能解决你的问题,请参考以下文章

Jupyter Lab 在错误的路径中打开,与 Jupyter Notebook 不同,两者在“jupyter_notebook_config.py”中具有相同的映射。

如何在Jupyter Notebook中使用Python虚拟环境?

在 jupyter notebook 中使用 joblib 时不显示打印输出

解决不能再jupyter notebook中使用tensorflow

Jupyter Lab 中的 Jupyter Notebook 扩展

Jupyter notebook 中 从url中load 数据集, 会存在哪里?