什么是 Matplotlib

Matplotlib 是一个使用 Python 作图的库,它一开始的目的是为了代替 Matlab 的作图部分。Matploplib 有一个方便的命令式界面,用来在 iPython 中快速作 图,还包含一个面向对象的界面用来写脚本和作复杂图形。

相对 Gnuplot,Matplotlib 有很多优点:

  • 直接使用 Python,可以和数据处理方便的结合,扩展起来也很直接。比如 可以实现 Mathematica 的 Adaptive Plotting

  • Multiplot 的效果比较好。在 Gnuplot 里实现 multiplot 对齐是一件非 常变态的事。

  • 可以使用 LaTeX 渲染文本。

也有很多很多缺点:

  • 两种界面共存,API 比较混乱

  • 有很多不同的方法可以实现相同的功能,这个一般不是坏事。但是 Matplotlib 的文档做的很烂,所以我在看文档时经常因为这个原因感到迷 茫。

  • 没有拟合功能。有些 Python 库有这个功能,比如 Numpy 可以拟合多项 式,但是方便程度比 Gnuplot 差太远了。我一般是在 Gnuplot 里拟合后 在 Matplotlib 里面用。

  • API 比较混乱

  • API 真的比较混乱

所以我一直是怀着又爱又恨的心情使用 Matplotlib 的…​

使用 Matplotlib

Matplotlib 的命令式界面比较简单,基本和 Gnuplot 功能差不多,这里就不介 绍了。要使用面向对象界面,首先当然要 import Matplotlib 的库。

import matplotlib.pyplot as Plt

然后就可以开始画图了。一般来说还要再 import 一个数值计算的库,因为 Python 标准库里的 Math 库虽然提供了很多函数,但是它缺少一个重要功能: 产生一个浮点数序列(也就是 range 的 float 版)。Python 有很多数值计 算的库,其中最常用的是 numpy。我比较非主流, 一般用 mpmath。mpmath 是一个arbitrary precision 库, 所以比 numpy 慢得多,功能也比 numpy 少。

注意 Matplotlib 并没有直接给一个函数作图的功能,它只能给一系列坐标做图。 所以如果想要给一个函数作图的话要先自己生成数据。比如

import mpmath as Mp
X = Mp.arange(0, 1.1, 0.1)  # 0, 0.1, 0.2, ... , 1
Y = [Mp.power(num, 2) for num in X] # y = x^2
Plt.plot(X, Y)

如果我们这时候执行这个 Python 脚本,虽然 Matplotlib 暗地里已经画了图, 但是我们什么都看不见。我们需要在后面加一行

Plt.show()

这时候再执行的话就会弹出来一个窗口,里面显示了一个很囧的图。这个例子里 我们只用了 11 个采样。如果把 arange 那行改成

X = Mp.arange(0, 1.1, 0.01)

效果会好很多。

一个 plot 函数可以画任意多条曲线。

X1 = Mp.arange(0, 1, 0.01)
Y1 = [Mp.power(num, 2) for num in X1] # y = x^2
X2 = [0, 1]
Y2 = [0, 1]  # y = x
X3 = X1
Y3 = [Mp.sqrt(num) for num in X3] # y = x^(1/2)
Plt.plot(X1, Y1, X2, Y2, X3, Y3)
Plt.show()

如果我们把这个图放大(比如把窗口最大化)然后观察第三条曲线靠近 x = 0 的 部分,会发现那条曲线刚开始的一小段是直的,非常囧。这是因为 0.01 的采样 间隔对那一段来说还是太大了,我们可以对那一段单独进行惨无人道的密集采样。

X3 = Mp.arange(0, 0.02, 0.001) + Mp.arange(0.02, 1, 0.01)

当然我们也可以实现 adaptive plotting 一劳永逸地解决这类问题。

画完后,我们可以把图保存下来。

Plt.savefig("test.pdf")

好吧,为了避免激起民愤,我现在要承认一个悲惨的事实:到现在为止我们使用 的其实还是 Matplotlib 的命令式界面,只不过我们没有使用 from blabla import *matplotlib.pyplot 的命名空间掺和到脚本自己的命名空间里。 (主要是因为写这个时我比较晕,写着写着就写错了…​ 不过我决定把以上保留, 作为 Matplotlib API 混乱的证据…​)

Matplotlib 里的常用类的包含关系为 Figure ← Axes ← (Line2D, Text, etc.) (官方文档里居然连这个都没有说明??!!)。其实在 Figure 的上面 还有一个 FigureCanvas 类,大概相当于 Gnuplot 里的 terminal,不过我从来 没有用过。

所以使用面向对象界面画图的一般步骤就是

  1. 建立一个 Figure 对象

  2. 在这个 Figure 里建立一个或 n 个 Axes 对象

  3. 在这些 Axes 里分别画图。

翻译成 Python 语就是

import matplotlib.pyplot as Plt
import mpmath as Mp

X1 = Mp.arange(0, 1, 0.01)
Y1 = [Mp.power(num, 2) for num in X1] # y = x^2
X2 = [0, 1]
Y2 = [0, 1]  # y = x
X3 = Mp.arange(0, 0.02, 0.001) + Mp.arange(0.02, 1, 0.01)
Y3 = [Mp.sqrt(num) for num in X3] # y = x^(1/2)

Fig = Plt.figure()                      # Create a `figure' instance
Ax = Fig.add_subplot(111)               # Create a `axes' instance in the figure
Ax.plot(X1, Y1, X2, Y2, X3, Y3)         # Plot in the axes
Fig.savefig("test.pdf")

注意这时保存的重任就落在了 figure 对象那瘦小的肩膀上。

装饰

我们画完图以后一般要加上标题、图例这些元素。这些功能散布在 Matplotlib API 的各处。

  • 加标题可以使用 Axes 对象的 set_title 方法。

  • 坐标轴的标签可以使用 Axes 对象的 set_xlabelset_ylabel 方法。

图例可以使用 Axes 对象的 legend 方法创建。这 个函数会使用 Line2D 对象的 label 属性。注意 上一节最后的那一段代码,Ax.plot() 这一行我们并没有使用 plot 的返回值,实际上它的返回值正是 Line2D 对象的列表,所以我们可以这样设置 label:

Ax.plot(X1, Y1, label="blabla")

也可以这样

Lines = Ax.plot(X1, Y1)
Lines[0].set_label("blabla")

如果我们在一个 plot 调用里画多条线,同时设 label,这些线都会有相同的 label。设置完这些以后,只需调用 Ax.legend(),图例就会出现在图的右上角。如果我们使用上一节 的那个例子,右上角并不是放图例的好地方,这时我们可以使用 legendloc`参数。这个参数可以是一个字符串, 比如 `"right""lower left" 之类,也可以是一 个整数,具体定义请见 官方文档。这里我们可以简单地使用 0 或者 "best" 让 Matplotlib 自动选择位置,

Ax.legend(loc=0)

现在我们的图基本成型,可以插入到文档里了。这时我们可能想调整图的大小, 我们可以修改 Figure 构造函数的调用,

Fig = Plt.figure(figsize=(4,3))

这样图的大小就会是宽 4 高 3,单位传说是英寸。也可以使用 Figset_figwidthset_figheight 方法。如果你把图的尺寸改得比较小的话,可能会 发生一件非常奇妙的事,就是坐标轴的标签被图的边界遮住了,或者干脆不见了。 解决办法就是调整绘图区域的四个边界在图中的位置,比如下面的代码

Fig.subplots_adjust(left=0.15, top=0.9)

把绘图区域的左边界放在图左边 15% 的位置,上边界放在高的 90% 的位置。

Matplotlib 中的文本

Matplotlib 有很强的文本功能。它可以处理普通的文本,也可以使用 LaTeX 渲 染数学公式。Matplotlib 还带了一个 LaTeX 引擎,可以在系统里没有 LaTeX 的 时候渲染数学公式。以下代码使用默认字体(貌似是 Verdana)显示 x 轴的标题

Ax.set_xlabel("x")

如果你要使用 LaTeX 来排这个标题,只需要使用 Python raw string,并把标 题放在数学模式里,

Ax.set_xlabel(r"$x$")

注意即便是使用 raw string,不在数学模式里的部分仍然不会使用 LaTeX。

Matplotlib 的 Axes 类有一个 text 方法,可以 在一个 Axes 对象的任意位置放置一个文本。 Figure 也有一个 figtext 方法用来在 Figure 对象的任意位置放置文本。具体用法请见 官方文档Axes 对象还有一个超级强大的 annotate 方法, 可以满足你对标注的最变态需求。

要改变文本的默认字体,需要使用 pyplot 模块的 rc 函数,

Plt.rc("font", family="Helvetica")

不过如果你指定的字体是 OpenType 的话,貌似不能嵌入到 PDF 里…​

三维做图

Matplotlib 使用实例

高级用法

Artists 模块

变换与坐标系

Matplotlib 里常用的 Python 技巧

读取 Gnuplot 格式的数据

Gnuplot 使用的数据格式是每个采样占一行,不同的维度之间用空格或者 tab 分隔。而 Matplotlib 使用的数据结构正好是这种结构的转置。Python 自带一 个 zip 函数,可以用来转置二维列表。

>>> zip([1,2,3], [4,5,6])
[(1, 4), (2, 5), (3, 6)]

但这个方法有一个问题:假设我们共有 n 个采样,每个采样有两个维度,那么 zip 的参数应该是是 n 个一维列表。但是一般来说我们会把从文件中读出来的数 据存在一个 \$n \times 2\$ 的二维列表里。这时我们可以使用 Python 的 * 语法:

>>> l = [[1, 2, 3], [4, 5, 6]]
>>> zip(*l)
[(1, 4), (2, 5), (3, 6)]

所以我们可以用以下的代码来读取数据

DataFile = open("/path/to/data.txt", 'r')
Data = [[float(x) for x in line.strip().split()] \
        for line in DataFile.readlines()]
DataFile.close()
Data = zip(*Data)
Ax.plot(*Data)

对倒数第二行不满的同学请自行替换之~~

Double Map

Python 内建了一个很有用的 map 函数,用来把一个函数作用在一 个列表的每一个元素上。List comprehension 基本上就是这个函数的一个语法糖。 但是 map 只对一元函数和一维列表有用。如果我们要画一个二元 函数,我们肯定希望有 map 的二维版本。这个可以简单地用两个 map 嵌套实现:

def maap(f, a, b):
    return map(lambda x: map(lambda y: f(x, y), b), a)

这样如果 \$a = (a_0, a_1, \ldots, a_{n-1})\$, \$b = (b_0, b_1, \ldots, b_{m-1})\$,maap(func a, b) 返回一个矩阵 A, \$A_{ij} = f(a_i, b_j)\$.