什么是 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 \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)\$.