Matplotlib
什么是 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,不过我从来没有用过。
所以使用面向对象界面画图的一般步骤就是
- 建立一个 Figure 对象
- 在这个 Figure 里建立一个或 n 个 Axes 对象
- 在这些 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_xlabel
和set_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()
,图例就会出现在图的右上角。如果我们使用上一节的那个例子,右上角并不是放图例的好地方,这时我们可以使用 legend
的 loc`参数。这个参数可以是一个字符串,比如 `"right"
,"lower left"
之类,也可以是一个整数,具体定义请见 官方文档。这里我们可以简单地使用 0
或者 "best"
让 Matplotlib 自动选择位置,
Ax.legend(loc=0)
现在我们的图基本成型,可以插入到文档里了。这时我们可能想调整图的大小,我们可以修改 Figure
构造函数的调用,
Fig = Plt.figure(figsize=(4,3))
这样图的大小就会是宽 4 高 3,单位传说是英寸。也可以使用 Fig
的 set_figwidth
和 set_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 × 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 = (a0, a1, …, an − 1), b = (b0, b1, …, bm − 1),maap(func a, b)
返回一个矩阵 A, Aij = f(ai, bj).