matplotlib使用指南

matplotlib使用指南

声明

本文主要翻译matplotlib的官方文档,若有错误,欢迎批评指正。

本文包含了一些基本的使用方法以及适合快速上手的实例,帮助你快速熟悉matplotlib。

导入matplotlib模块的方法为

import matplotlib.pyplot as plt
import numpy as np

一个简单的例子

matplotlib将你的数据绘制在Figure上,每一个Figure能够包含一个或多个Axes。每一个Axes相当于一个绘图区域,并且该区域内的每一个点都可以通过x-y坐标,极坐标或x-y-z坐标(三维情况下)定位。

创建带有一个Axes的Figure对象的最简单方法是使用pyplot.subplots,之后,我们便可以使用Axes.plot将数据绘制在Axes上:

fig, ax = plt.subplots()  # 创建包含一个axes的figure对象
ax.plot([1,2,3], [1,2,4]) # 将数据绘制在axes上

事实上,对于Axes中的每一个绘图函数,在matplotlib.pyplot模块中都存在与之对应的函数。我们可以直接通过matplotlib.pyplot自动创建Axes及其对应的Figure,并在Axes上进行绘图,并不需要显式创建Axes。因此,上述绘图的例子可以简化为:

plt.plot([1,2,3], [1,2,4])

Figure示意图

../../_images/anatomy.png

Figure

Figure负责追踪所有的子Axes,一些特殊的artists(如title,legend)和canvas(canvas是最终呈现图像的对象)。一个Figure可以包含多个Axes,但至少要包含一个Axes。

创建Figure的方法有以下几种:

fig = plt.figure() # 一个空的figure,不带有Axes
fig, ax = plt.subplots() # 带有一个Axes的figure
fig, axs = plt.subplots(2, 2) # 带有4个Axes的figure,且Axes的排序方式为2x2

在创建Figure的时候,同时创建与之对应的Axes是常用的方式。但也可以之后在创建Axes,然后加入到Figure当中,用于更复杂的Axes布局。

Axes

Axes就是我们进行绘图的区域,一个Figure可以包含多个Axes,但一个Axes只能存在于一个Figure当中。一个Axes包含两个Axis对象(三维情况下包含三个),Axis用于控制数据的显示范围。每个Axes还可以控制当前区域的标题、x-y坐标轴的名称。

fig, ax = plt.subplots()
ax.set_xlim([0, 5]) 	# 控制x轴显示范围
ax.set_xlabel('x')		# 设置x轴名称
ax.set_ylim([-1, 3])	# 控制y轴显示范围
ax.set_ylabel('y')		# 设置y轴名称
ax.set_title('ax-title')# 设置标题

Axis

Axis是类似数轴的对象,用于设置数值的显示范围、刻度标记以及刻度标签。刻度的位置由Locator对象决定,刻度标签(字符串)由Formatter对象生成。正确设置LocatorFormatter能够对刻度标记的位置和标签进行精细化的控制。

Artist

几乎所有你能够在图像上看到的对象都是一个artist(即使是Figure,Axes和Axis对象)。以及Text,Line2D,collections,Patch对象都是artist。当图片被渲染的时候,所有的artist对象都将会被绘制到Canvas之上。大多数的artist对象都被绑定到一个Axes之上,因此一个artist无法被多个Axes分享或移动到另一个Axes之上。

绘图函数的输入

所有的绘图函数都期望获得numpy.arraynumpy.ma.masked_array作为输入。如果将类似数组(array-like)的对象作为输入,例如pandas.DataFramenumpy.matrix,则绘图函数可能不会按照你预想的方式进行工作。因此,最好的做法是先将数据转化成numpy.array对象。

# 将DataFrame转换成array
a = pandas.DataFrame(np.random.rand(4, 5), columns = list('abcde'))
a_asarray = a.values

# 将matrix转换成array
b = np.matrix([[1, 2], [3, 4]])
b_asarray = np.asarray(b)

面向对象接口与pyplot接口

正如在最开始的例子中所提到的,使用matplotlib由两个基本的方式:

  • 面向对象风格(object-oriented style,OO style):显式创建Figure和Axes,并调用这些对象的成员函数。
  • 依赖pyplot自动创建和管理Figure和Axes对象,以及使用pyplot函数进行绘图。
# 一个OO风格的示例
x = np.linspace(0, 2, 100)

# 注意:即使是OO风格, 我们仍然使用`.pyplot.figure`创建figure对象
fig, ax = plt.subplots()  # Create a figure and an axes.
ax.plot(x, x, label='linear')  # Plot some data on the axes.
ax.plot(x, x**2, label='quadratic')  # Plot more data on the axes...
ax.plot(x, x**3, label='cubic')  # ... and some more.
ax.set_xlabel('x label')  # Add an x-label to the axes.
ax.set_ylabel('y label')  # Add a y-label to the axes.
ax.set_title("Simple Plot")  # Add a title to the axes.
ax.legend()  # Add a legend.
# 一个pyplot风格的示例
x = np.linspace(0, 2, 100)

plt.plot(x, x, label='linear')  # Plot some data on the (implicit) axes.
plt.plot(x, x**2, label='quadratic')  # etc.
plt.plot(x, x**3, label='cubic')
plt.xlabel('x label')
plt.ylabel('y label')
plt.title("Simple Plot")
plt.legend()

事实上,存在第三种使用matplotlib的方法,但该方法的应用场景是将matplotlib集成到一个GUI应用当中。这里只是顺带提及,详细请参考Embedding Matplotlib in graphical user interfaces

matplotlib的文档和示例使用了OO和pyplot这两种不同的风格进行演示,你可以任意选择一种你喜欢的风格书写代码。但推荐的使用方式是:在交互式终端的情况下(如jupyter notebook),使用pyplot风格;在非交互式情况下(如写python脚本的时候),使用OO风格。

后端(backends)

什么是后端?

许多网站和邮件列表的文档都提到了“后端“一词,许多新用户经常对这个术语感到困惑。matplotlib针对不同的应用场景,能够输出不同格式的结果。一些人在交互式的python shell中使用matplotlib绘图;一些人使用jupyter notebooks将matplotlib绘制的图像嵌入到代码的当中,而不是弹出单独的图片窗口;一些人将matplotlib集成到GUI程序中(如wxpython、pygtk)以创建富应用(rich application);以及其它的应用场景。

为了支持不同的应用场景,matplotlib就需要有不同的功能组件(处理机制)来针对不同场景,输出符合要求的结果。这些不同的功能组件就称为”后端“。而前端指的是面向用户的代码,即绘图的代码。后端的作用就是根据前端用户代码,生成图像,并返回给用户。matplotlib中有两种不同类型的后端:

  • 交互式后端(user interface backends/interactive backends):例如pygtk,wxpython,tkinter,qt4,macosx等
  • 非交互式后端(hardcopy backends/non-interactive backends):用于创建图像文件(如PNG,SVG,PDF)

如何配置后端?

一共有三种不同的方法来配置matplotlib的后端:

  1. matplotlibrc文件中的rcParams['backend'](默认是agg)参数
  2. MPLBACKEND环境变量
  3. matplotlib.use()函数

需要注意的是如果同时使用了上述3种方法中的多种方法对后端进行配置,那么会优先选择序号大的方法。也就是说,如果使用函数matplotlib.use()配置了后端,那么matplotlib将无视MPLBACKENDrcParams['backend']中的参数值。

如果没有显式指定后端,matplotlib将基于系统中的可用后端和GUI事件循环(event loop)是否已经正在运行,自动检测可用的后端。在Linux系统中,如果环境变量DISPLAY未被设置,那么事件循环将会被识别为"headless",从而导致回退到非交互式后端(agg)。

详细的配置方法如下:

  1. 配置matplotlibrc文件中的rcParams['backend'](默认是agg)参数

    backend : qt5agg   # use pyqt5 with antigrain (agg) rendering
    
  2. 配置MPLBACKEND环境变量

    你可以在当前shell或针对单个脚本中设置该环境变量

    # On Unix
    > export MPLBACKEND=qt5agg
    > python simple_plot.py
    
    > MPLBACKEND=qt5agg python simple_plot.py
    
    # On Windows,只能针对当前shell设置该环境变量
    > set MPLBACKEND=qt5agg
    > python simple_plot.py
    

    设置MPLBACKEND环境变量将覆盖掉matplotlibrc文件中的配置,即使matplotlibrc文件存在于你当前的工作目录之下。因此,将MPLBACKEND环境变量设置为全局的环境变量(即在文件.bashrc或.profile中定义)是不妥当的,如果这样做了,可能会导致一些反直觉的结果。

  3. 如果你的脚本依赖于特定的后端,你可以使用matplotlib.use()函数

    import matplotlib
    matplotlib.use('qt5agg')
    

    应该在Figure创建之前就调用该函数,否则matplotlib可能无法转换后端,从而导致ImportError。使用该方法指定后端,如果其它用户想要更改使用其它的后端,那么就需要对原有的代码进行改动。因此,你应该尽量避免显式调用matplotlib.use()指定后端,除非不得不这么做。

内置后端及其它

matplotlib的内置后端、如何使用非内置后端等信息请参考:详细信息

什么是交互(interactive)模式

使用交互式后端允许我们直接向屏幕输出图像。是否与何时向屏幕输出图像?向屏幕输出图像之后,python脚本或shell是否继续运行?这些问题都与我们所调用的函数和方法直接相关。在matplotlib中可以通过一个状态(布尔)变量的取值来控制当前是否处于“交互模式”。就像其它配置参数一样,我们可以在matplotlibrc文件里修改状态变量。我们也可以通过matplotlib.interactive()来设置该变量的值,以及通过matplotlib.is_isteractive()查看该变量的值。另一种开启交互模式的方法是matplotlib.pyplot.ion(),与之对应的关闭方法为matplotlib.pyplot.ioff()

注意

  1. 交互功能以及show()的角色和行为,在matplotlib更新到1.0版本后,发生了重大变化。并在1.0.1版本中修复了一些bug。这里我们将描述在1.0.1版本中基本交互式后端的行为,以及macosx中的部分例外情况。
  2. “交互模式”能够在ipython和普通的python shell中正常运行,但不能在IDLE IDE中运行。如果默认的后端不支持交互功能,那么我们可以显式配置一个支持交互的后端,就像上述“如何配置后端?”中描述的那样。

交互模式的示例

启动一个普通的python shell或者不带任何选项参数的ipython,执行下述代码:

import matplotlib.pyplot as plt
plt.ion() 
plt.plot([1.6, 2.7])

此时,将弹出一个绘图窗口。并且,终端提示符仍然处于激活状态。因此,我们可以追加新的命令,例如:

plt.title("interactive test")
plt.xlabel("index")

可以发现追加新的命令之后,绘图窗口会自动更新内容。即使使用面向对象接口更新图像,大多数的可交互后端也能够自动更新。例如:

ax = plt.gca() # 获取当前axes对象, gca = get current axes ?
ax.plt([3.1, 2.2])

如果你正在使用某些后端(例如macosx),或更老版本的matplotlib,在追加新的命令之后,绘图窗口的图形可能不会立即自动更新。在这种情况下,你需要手动调用plt.draw()来刷新图像。

非交互模式的示例

重新启动一个新的会话窗口,并且关闭交互模式:

import matplotlib.pyplot as plt
plt.ioff() 
plt.plot([1.6, 2.7])

执行完上述命令之后,不会像“交互模式”那样,立马弹出一个绘图窗口(除非你正在使用macosx后端,但这是异常情况)。为了弹出绘图窗口,我们需要手动输入:

plt.show()

弹出绘图窗口之后,你会发现此时无法像“交互模式”那样继续追加新的命令。这是因为plt.show()阻塞了命令的输入,直到你手动关闭绘图窗口。

但这样的功能有什么用呢?被迫使用阻塞函数?

假设你需要编写一个脚本将某个文件的内容绘制成图,并输出到屏幕显示。你想要先查看绘制好的图像,然后再结束脚本。如果没有show()这样的阻塞命令存在,那么运行脚本之后,绘制好的图像将只会在屏幕上“一闪而过”,随后脚本便停止运行。

此外,非交互模式将所有的实际绘图工作延迟到show()调用之后。这样更有效率,而不需要每次输入一个命令之后,就立即更新图像。

在1.0版本之前,通常情况下,在一个脚本内show()只能被调用一次。而在1.0.1和更新的版本中,该限制被移除了,因此,我们可以像这样书写脚本:

import numpy as np
import matplotlib.pyplot as plt

plt.ioff()
for i in range(3):
    plt.plot(np.random.rand(10))
    plt.show()

这段代码将按照顺序绘制三张图,并且每次只显示一张图。之后图像只会在之前的图像关闭之后显示。

小结

如果启用交互模式,那么pyplot函数将自动向屏幕输出图像。如果使用面向对象方法而不是pyplot函数,那么当我们需要更新图像的时候调用函数draw()

如果我们需要在脚本结束前显示图像,就可以使用非交互模式。同样地,利用show()的阻塞特性,我们可以按照顺序显示一系列图像,新的图像只会在旧的图像手动关闭后才显示。

性能(performance)

无论是在交互模式中,还是在脚本中进行绘图,图像的渲染时间将会是实际任务中不得不解决的性能瓶颈。matplotlib提供了一些减少图像渲染时间的方法,代价是图像外观会发生一些改变(在我们所能容忍的范围内)。图像的类型将直接影响到我们能够使用哪些方法来减少渲染时间。

线段简化(line segment simplification)

对于包含线段的图像(例如,经典的折线图、多边形的轮廓等),可以通过变量rcParams["path.simplify"](默认值:True)以及rcParams["path.simplify_threshold"](默认值:1/9)来控制图像的渲染时间。rcParams["path.simplify"]是个布尔变量,用于控制是否启用线段简化功能。rcParams["path.simplify_threshold"]用于控制有多少线段会被简化,更高的阈值(threshold)意味着更快的渲染速度。

以下脚本首先展示了未启用线段简化和启用了线段简化的结果:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

# Setup, and create the data to plot
y = np.random.rand(100000)
y[50000:] *= 2
y[np.geomspace(10, 50000, 400).astype(int)] = -1
mpl.rcParams['path.simplify'] = True

mpl.rcParams['path.simplify_threshold'] = 0.0
plt.plot(y)
plt.show()

mpl.rcParams['path.simplify_threshold'] = 1.0
plt.plot(y)
plt.show()

matplotlib默认将简化阈值设置为1/9。如果你想要更改默认设置,那么可以到matplotlibrc文件中进行设置。或者,你可能有这样的需求:在不同的应用场景中,应用不同的绘图风格。例如,在交互模式时,将线段简化阈值设置为最大值1;在有印刷图像需求时,将线段简化阈值设置为最小值0。我们可以为不同的应用场景事先配置好不同的风格表单(style sheets),等到需要时直接应用该风格表单即可。详细可参见Customizing Matplotlib with style sheets and rcParams

线段简化的原理:通过迭代的方式,依次将线段合并成单个向量,直到下一个线段与当前向量的垂直距离大于path.simplify_threshold。距离的计算在显示坐标空间(display-coordinate space)中进行。

注意

在2.1版本中,对线段简化如何实现进行了一些改动。对于2.1之前的版本,渲染时间仍然能通过配置这些参数提高渲染效率,但对于某些数据而言,2.1及之后的版本,渲染效率能够获得更大的改善。

标记简化(marker simplification)

标记同样能够被简化,尽管不如线段简化功能强大。标记简化只能应用于Line2D对象,通过设置markevery属性来实现:

plt.plot(x, y, markevery=10)
# 或者
ax.plot(x, y, markevery=10)

markevery参数允许朴素下采样(naive subsampling),或尝试沿着x轴方向进行均匀采样,详细内容请查看Markevery Demo

将折线划分成更小的块(splitting lines into smaller chunks)

如果你正在使用Agg后端,那么你可用通过rcParams["agg.path.chunksize"](默认值:0)参数指定一个块大小(chunk size)。任何顶点数大于agg.path.chunksize的折线将会被划分成多个折线,划分后各个折线的顶点数不大于agg.path.chunksize。如果chunksize设置为0,意味着不对原始折线进行任何划分。对于某些类型的数据,合理设置一个chunksize能够极大提高渲染效率。

下面的脚本,展示了chunkszie=0和10000的图像对比。因为在图像比较大的时候,比较容易看出二者的区别,因此请尝试最大化绘图窗口,进行观察。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams['path.simplify_threshold'] = 1.0

# Setup, and create the data to plot
y = np.random.rand(100000)
y[50000:] *= 2
y[np.geomspace(10, 50000, 400).astype(int)] = -1
mpl.rcParams['path.simplify'] = True

mpl.rcParams['agg.path.chunksize'] = 0
plt.plot(y)
plt.show()

mpl.rcParams['agg.path.chunksize'] = 10000
plt.plot(y)
plt.show()

图例(legends)

图例的默认行为是在图像中找到最佳一个位置,使得被图列覆盖住的数据点最少(即loc='best')。当数据点非常多的时候,这种查找行为非常消耗计算资源。在这种情况下,手动指定图例的位置是更好的选择。

使用fast风格

fast风格能够自动为简化和分块参数设置一个较合理的值,依次来提高绘图效率。使用方法如下:

import matplotlib.style as mplstyle
mplstyle.use('fast')

fast风格是一种非常轻量级的配置,能够很好地与其它风格配合使用。但需要注意的是,要确保fast风格是最后一个应用生效的风格,以此保证其它风格不会覆盖掉fast风格的配置。

mplstyle.use(['dark_background', 'ggplot', 'fast'])

关于matplotlib内置风格和如何自定义风格的详细内容,参见Customizing Matplotlib with style sheets and rcParams

posted @ 2021-03-31 22:57  SleepyCat  阅读(770)  评论(1编辑  收藏  举报