这里主要关注 Python Matplotlib 绘图的技术性内容,不讨论非技术性内容,例如论文中的绘图要求或偏好。

Matplotlib 介绍

matplotlib 是模仿 MATLAB 风格的 Python 2D 绘图库(3D 的功能比较弱,还是基于 2D 引擎勉强实现的),同时提供了两套 api:

  • 一个是面向过程的,主要调用 matplotlib.pyplot 的函数;
  • 一个是面向对象的,主要使用 matplotlib.figure.Figure 和 matplotlib.axes.Axes 两类对象的方法进行细节操作。

面向过程的 api 适合简易使用的场景,但是不容易弄清原理;为了更复杂的绘图要求,这里主要采用面向对象的 api。 (MATLAB 绘图也是一样的问题,而且主要都在使用面向过程的接口)

关于绘图的呈现方式,在 Jupyter 中很可能会自动绘图而不需要 plt.show(),这与魔法指令 %matplotlib inline 有关。

在笔记中,需要使用如下模块:

1
2
3
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

由于 matplotlib 的内容太多太杂,这里只会介绍最容易理解的,最本质的面向对象的接口,然后作为辅助会介绍其它等价的,或者更方便的调用方式。但是 matplotlib 的面向对象其实有很多层,在每一层都可能为同一个功能提供了等价的接口,这里为了简化笔记,主要考虑 Figure 和 Axes 层面提供的接口。

由于个人目前不需要使用各种统计图,因此这里不作讨论。

简单例子

面向过程风格的绘图例子如下,主要是 plt 的函数调用:

1
2
3
4
5
plt.plot(
np.linspace(0, 5, 100),
(lambda x: np.cos(2 * np.pi * x) * np.exp(-x))(np.linspace(0, 5, 100)),
)
plt.title("eg-1-1")

面向对象风格的绘图例子如下,主要是画布和子图的方法调用:

1
2
3
4
5
6
fig, ax = plt.subplots()
ax.plot(
np.linspace(0, 5, 100),
(lambda x: -np.cos(2 * np.pi * x) * np.exp(-x))(np.linspace(0, 5, 100)),
)
ax.set_title("eg-1-2")

基本概念

对于一个图像,matplotlib 将其分成了两层主要的概念:

  • Figure 层,可以理解为整个画布,控制整体的尺寸,标题等;
  • Axes 层,可以理解为一个子图或者子图的数组,这里的子图通常有两个坐标轴,以及具体数据图像和细节组成,因此名称为 Axes。一个 Figure 对象可以包括多个子图,达到多个子图并列放置的效果。

这两个概念非常难以翻译,因此下面主要使用英文。

一个 Figure 上主要有如下的元素:

  • Title 标题,这里存在 Figure 标题和 Axes 标题的区分
  • Legend 图例
  • x Axis, y Axis 坐标轴
  • xlabel, ylabel 坐标轴标签
  • Grid 网格
  • Line 线条
  • Markers 标记
  • Major tick, Minor tick 主刻度,副刻度
  • Major tick label 主刻度标签
  • Spine 边框线

各个元素如下图所示 https://matplotlib.org/stable/_images/anatomy.png

也可以参考下面这张图 https://github.com/guanyingc/latex_paper_writing_tips

创建 Figure 和 Axes

创建 Figure

创建 Figure 对象的常见写法如下

1
2
fig = plt.figure()
fig = plt.figure(figsize=(4, 3))

常用参数的含义:

  • figsize 尺寸,依次为横向宽度和纵向高度,单位为英寸,例如 figsize=(5,2.7)
  • dpi 分辨率
  • facecolor 背景颜色
  • edgecolor 边框颜色
  • frameon 是否显示边框

关于 dpi 选项,需要一些额外的说明:

  • 默认 dpi 通常为 100(具体取值由配置决定)。对于 PNG 等位图,在 figsize 不变时,dpi 越大,像素尺寸越大,文件通常也越大;对于 PDF、SVG 等矢量格式,曲线和文字并不依赖 dpi 提高清晰度。
  • 在交互式显示时通常不必设置过大的 dpi;导出位图时再按用途指定,例如 dpi=300。

添加 Axes

Figure 可以暂时不含 Axes;但通常的数据绘图要在 Axes 的坐标轴体系中完成。

在Figure上添加一个 Axes 对象可以使用下面的语句

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

多个 Axes 在同一个 Figure 上是按照矩阵格式排列布局的,例如 3 行 2 列一共 6 个位置,当然并不要求填满所有位置。 可以使用如下方式,依次添加 Axes 对象,需要给出整体的行列数以及自身的序号(从 1 开始)

1
2
3
4
5
6
7
8
fig = plt.figure()

ax1 = fig.add_subplot(3,2,1)
ax2 = fig.add_subplot(3,2,2)
ax3 = fig.add_subplot(3,2,3)
ax4 = fig.add_subplot(3,2,4)
ax5 = fig.add_subplot(3,2,5)
ax6 = fig.add_subplot(3,2,6)

得到的空给框架如下(这里的刻度尺都是动态的,会随着填入数据的范围而调整)

对于上面函数中小的个位数索引(即总位置不超过 9 个),可以直接拼成一个三位数参数,例如

1
2
3
4
ax1 = fig.add_subplot(3,2,1)

# i.e.
ax1 = fig.add_subplot(321)

注意:如果反复添加 Axes,它们可能会相互重叠显示,显得非常混乱。

一次性创建 Figure 并添加 Axes

对于多行多列的矩阵布局,先创建 Figure,然后一个个添加 Axes 非常繁琐,可以使用pyplot 提供的 plt.subplots 接口一次性完成。

例如,创建一个Figure并添加一个Axes

1
2
3
fig, ax = plt.subplots()

type(ax) # matplotlib.axes._axes.Axes

例如,创建一个Figure并添加多个Axes

1
2
3
4
fig, axes = plt.subplots(3,2)

type(axes) # numpy.ndarray
print(axes.shape) # (3,2)

通过访问这个数组的分量来实现对每一个 Axes 的修改。

需要注意的是,还有一个名称非常相似的接口 plt.subplot,它是面向过程绘图的接口,含义是为当前的 Figure 添加一个 Axes。

绘制曲线

使用 Axes 的 plot 方法绘制曲线图,假设 x,y,x2,y2 都是一维的数组,那么可以使用下面几种方式绘制曲线:

  • ax.plot(y):一条散点连成的曲线,横坐标取从0开始的整数,纵坐标取自y
  • ax.plot(x,y):一条散点连成的曲线,横坐标取自x,纵坐标取自y
  • ax.plot(x,y,x2,y2):两条曲线,第一条横坐标取自x,纵坐标取自y,第二条横坐标取自x2,纵坐标取自y2

还可以附加一些细节参数,例如颜色,曲线样式,图例等,下面依次介绍。

颜色 color

几个常用的颜色及其缩写如下:

  • 蓝色 blue-b
  • 绿色 green-g
  • 红色 red-r
  • 白色 white-w
  • 黑色 black-k(因为b已经被blue占用)
  • 金黄色 yellow-y
  • 蓝绿色(青色)cyan-c
  • 品红色 magenta-m

可以使用关键字参数设置固定的颜色 color='red' 或者使用十六进制的颜色编码如 color='#32CD32'。支持如下的简写:

  • 将常用颜色使用简写,例如 color='r'
  • 将关键字 color 简写为 c,例如 c='r'

例如:(这里白色的线和背景重了看不出来)

1
2
3
4
5
6
7
8
9
fig, ax = plt.subplots(figsize=(7, 5))

data = 10 + 0.4 * np.random.randn(9, 100)
colors = ["blue", "green", "red", "white", "black", "yellow", "cyan", "m",'#32CD32']

for i, item in enumerate(colors):
ax.plot(data[i] - i, color=item, label=item)

ax.legend()

曲线样式 linestyle

支持的曲线样式如下:

  • 实线(默认):solid,简写 -
  • 短虚线(点虚线):dotted,简写 :
  • 长虚线(破折线):dashed,简写 --
  • 长短交错虚线(点划线):dashdot,简写 -.
  • 不画线(什么也没有):None,简写 "" 或者 ''

可以使用关键字参数 linestyle='dotted' 设置曲线样式。支持如下的简写:

  • 将曲线样式使用简写,例如 linestyle=':'
  • 将关键字 linestyle 简写为 ls,例如 ls='-.'

例如

1
2
3
4
5
6
7
8
9
10
11
fig, ax = plt.subplots(figsize=(7, 5))


style = ["solid", "dotted", "dashed", "dashdot", "None" ]

data = 10 + 0.4 * np.random.randn(len(style), 100)

for i, item in enumerate(style):
ax.plot(data[i] - i,linestyle=item,label=item)

ax.legend()

还支持调整线的宽度(linewidth,简称 lw),线宽的默认值是 1.5(MATLAB 的线宽默认是 0.5),例如

1
2
3
4
5
6
fig, ax = plt.subplots(figsize=(4, 3))

data = np.sin(np.linspace(0, 2 * np.pi, 100))

ax.plot(data,lw=5)
ax.plot(data+0.5)

设置线宽是高频使用的选项,无论是 matplotlib 还是 MATLAB 绘图,因为前者的默认线宽 1.5 略粗,后者的默认线宽 0.5 略细。

绘图标记 marker

常用的点的标记如下:

  • 点 '.'
  • 像素点 ','
  • 实心圆 'o' (小写字母 o)
  • 星号 '*'
  • 乘号 'x'
  • 加号 '+'
  • 菱形 'D',瘦菱形 'd'
  • 正方形 's'
  • 三角形 '^','<','v','>'(不同的四个朝向)

可以使用关键字参数 marker='*' 设置绘图标记。

例如:(这里像素点根本看不清)

1
2
3
4
5
6
7
8
9
10
11
fig, ax = plt.subplots(figsize=(7, 5))

markers = [".", ",", "o", "*", "x", "D", "d", "s", "^"]
data = np.sin(np.linspace(0, 2 * np.pi, 10 * len(markers)))

ax.plot(data)

for i, item in enumerate(markers):
ax.plot(i * 10, data[i * 10], label=item, marker=item)

ax.legend()

关于绘图标记,我们还可以指定标记的大小(markersize 简写 ms),内部填充颜色(markerfacecolor 简写 mfc),边框填充颜色(markeredgecolor 简写 mec),例如

1
2
3
4
5
fig, ax = plt.subplots(figsize=(4, 3))

data = np.random.randn(7)

ax.plot(data, marker = 'o', ms = 10, mec = 'r')

MATLAB 默认是不会填充标记的内部,呈现的是空心的标记,但是 matplotlib 的默认行为是填充,可以通过 markerfacecolor="none" 选项阻止填充。

如果数据很多,不需要在每一个数据点绘制标记,否则标记会堆叠在一起,掩盖数据,可以通过 markevery 选项控制,例如

1
2
3
4
5
6
7
fig, ax = plt.subplots()
ax.plot(
np.linspace(0, 5, 100),
(lambda x: -np.cos(2 * np.pi * x) * np.exp(-x))(np.linspace(0, 5, 100)),
marker="o",
markevery=5,
)

markevery 选项如果提供的是一个值,代表间隔,如果提供两个数组成的元组,则第一个元素为起始的偏移量,第二个元素为间隔,可以用来错开多条曲线的标记。

使用 linestyle='',marker='o' 可以达到散点图的效果,例如

1
2
3
4
5
fig, ax = plt.subplots(figsize=(4, 3))

data = np.random.randn(7)

ax.plot(data, ls='',marker = 'o', ms = 5)

当然也可以使用专门的散点图接口 ax.scatter。

fmt 参数

对于曲线常用的标记、样式、颜色三种设置,可以把相应的简写合并为一个字符串参数,这里每一个部分都是可选的,顺序也是任意的。

1
'[marker][line][color]'

例如:

  • o:r 圆点,点虚线,红色
  • go-- 绿色圆点,破折线

示例如下

1
2
3
4
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 2 * np.pi, 20)
ax.plot(x,np.sin(x),'o:r')

图像细节

标题和轴标签

支持两层标题:Figure 可以使用 suptitle 设置标题,Axes 可以使用 set_title 设置标题。

例如

1
2
3
4
5
6
7
8
9
10
fig, axes = plt.subplots(1,2,figsize=(7, 3))

x = np.linspace(0, 2 * np.pi, 100)

axes[0].plot(x,np.sin(x))
axes[1].plot(x,np.cos(x))

axes[0].set_title("sin(x)")
axes[1].set_title("cos(x)")
fig.suptitle("fig-title")

可以给每一个坐标轴都可以加上轴标签:

1
2
ax.set_xlabel("x")
ax.set_ylabel("y")

例如

1
2
3
4
5
6
7
8
9
10
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 2 * np.pi, 100)

ax.plot(x,np.sin(x))

ax.set_xlabel("x")
ax.set_ylabel("y")

ax.set_title("sin(x)")

对于标题和轴标签这类的文本选项,有很多额外的选项:

  • 位置:例如 loc="left",对于标题和 x 轴包括 "left","center","right" 选项,对于 y 轴包括 "top","center","bottom" 选项,默认都是居中
  • 颜色:例如 color='r'
  • 字体:例如 fontfamily = 'sans-serif',注意默认不支持中文显示。
  • 字体大小:例如 fontsize=18
  • 字体样式:例如 fontstyle='oblique'

图例

对每一条曲线加上 label,然后使用 Axes.legend() 显示图例,注意 legend() 调用必须在所有 label 之后。

例如

1
2
3
4
5
6
7
8
9
10
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 2 * np.pi, 100)

ax.plot(x,np.sin(x),label='sin(x)')
ax.plot(x,np.cos(x),label='cos(x)')

ax.legend()
ax.set_xlabel("xlabel",loc='right')
ax.set_ylabel("ylabel",loc='top')

可以对图例进行细节调整,包括加上单独的标题,移除边框,指定位置等。(当然这些细节也可以通过 GUI 界面进行调整)

例如

1
2
3
4
5
6
7
8
9
10
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 2 * np.pi, 100)

ax.plot(x, np.sin(x), label="sin(x)")
ax.plot(x, np.cos(x), label="cos(x)")

ax.legend(title="Legends", loc="best", frameon=False, fontsize=10)
ax.set_xlabel("xlabel", loc="right")
ax.set_ylabel("ylabel", loc="top")

matplotlib 图例的默认样式与 MATLAB 有很多差异,例如图例边框的形状和颜色,图例背景透明度等。

网格线

函数原型为

1
Axes.grid(visible=None, which='major', axis='both', **kwargs)

其中主要参数:

  • visible 可见性
  • which 绘制主刻度还是辅助刻度的网格线,支持选项 'major','minor','both',默认 'major' 只绘制主刻度的网格线
  • axis 绘制从 x 轴或 y 轴发出的网格线,支持选项 'both','x','y',默认 'both'

可以简单地使用 ax.grid() 开启网格线,还可以指定网格线更复杂的细节(样式,宽度,颜色,透明度 alpha 等)。

例如

1
2
3
4
5
6
7
8
9
10
11
12
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 2 * np.pi, 100)

ax.plot(x,np.sin(x),label='sin(x)')
ax.plot(x,np.cos(x),label='cos(x)')

ax.grid(ls=':', lw=1, color='r',alpha=0.3)

ax.legend()
ax.set_xlabel("xlabel",loc='right')
ax.set_ylabel("ylabel",loc='top')

轴的单位长度比

可以手动设置 x 轴和 y 轴的单位长度的比例,默认是 auto,也就是会根据作图需要自动调整,可以改成相等或者某个固定的值。

1
2
3
4
5
ax.set_aspect('auto')

ax.set_aspect('equal')

ax.set_aspect(2)

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fig, ax = plt.subplots(figsize=(4, 4))

x = np.linspace(-1,1,100)

ax.plot(x,x,label='$x$')
ax.plot(x,x**2,label='$x^2$')
ax.plot(x,x**3,label='$x^3$')

ax.grid(ls=':', lw=1, color='r',alpha=0.3)

ax.legend()
ax.set_xlabel("xlabel",loc='right')
ax.set_ylabel("ylabel",loc='top')

ax.set_aspect('equal')

边框隐藏

默认情况下作图会绘制四个边框,有时我们需要去掉某些边框,有两种方案:

  • 设置边框透明度为 0
  • 设置边框颜色为空

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
fig, ax = plt.subplots(figsize=(4, 4))

x = np.linspace(0,1,100)
ax.plot(x,x**2,label='$x^2$')

ax.legend()
ax.set_xlabel("xlabel",loc='right')
ax.set_ylabel("ylabel",loc='top')

ax.spines['right'].set_alpha(0)
ax.spines['top'].set_color('none')

ax.set_aspect('equal')

坐标轴范围

通常 Axes 的坐标轴范围会根据数据范围自动生成,并且相对数据本身范围有一定的冗余,可以使用下面的代码使得数据轴范围更紧。

1
2
3
ax.autoscale(tight=True)

plt.autoscale(tight=True)

某些数据图需要紧贴的效果,某些数据图则需要冗余,需要根据情况自行选择。

我们也可以手动指定坐标轴的范围(数据如果不在范围内就不会显示)

1
2
ax.set_xlim(-1,2)
ax.set_ylim(0,3)

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fig, ax = plt.subplots(figsize=(4, 4))

x = np.linspace(-1,1,100)
ax.plot(x,x**2,label='$x^2$')
ax.plot(x,x+1,label='$x+1$')

ax.legend()
ax.set_xlabel("xlabel",loc='right')
ax.set_ylabel("ylabel",loc='top')

ax.spines['right'].set_alpha(0)
ax.spines['top'].set_color('none')

ax.set_aspect('equal')

ax.set_xlim(-1,2)
ax.set_ylim(0,3)

坐标轴刻度

默认线性坐标轴通常只显示主刻度;某些样式或对数坐标轴会启用次刻度。如果需要,可以显式关闭。

最直接的面向对象写法如下

1
ax.minorticks_off()

如需分别控制两条轴,也可以把对应轴的次刻度定位器设为 matplotlib.ticker.NullLocator()。

可以指定一条轴上使用哪些刻度,使用 ax.set_xticks 方法传入一个数组。

例如

1
2
3
4
5
6
7
8
9
10
11
12
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 2 * np.pi, 100)

ax.plot(x,np.sin(x),label='sin(x)')
ax.plot(x,np.cos(x),label='cos(x)')

ax.legend()
ax.set_xlabel("xlabel",loc='right')
ax.set_ylabel("ylabel",loc='top')

ax.set_xticks([0,np.pi,2*np.pi])

我们可以同时指定刻度位置和标签。单独使用 set_xticklabels 容易使标签与刻度位置失配,不推荐这样做;应将标签作为 set_xticks 的第二个参数传入。例如

1
2
3
4
5
6
7
8
9
10
11
12
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 2 * np.pi, 100)

ax.plot(x,np.sin(x),label='sin(x)')
ax.plot(x,np.cos(x),label='cos(x)')

ax.legend()
ax.set_xlabel("xlabel",loc='right')
ax.set_ylabel("ylabel",loc='top')

ax.set_xticks([0, np.pi, 2 * np.pi], [r"$0$", r"$\pi$", r"$2\pi$"])

坐标轴刻度朝向

关于坐标轴的刻度朝向,matplotlib 默认刻度朝外(但是 MATLAB 默认刻度朝内),可以用下面的命令调整当前绘图窗口的刻度朝向

1
2
3
4
5
6
7
8
ax.tick_params(axis='x', direction='in')
ax.tick_params(axis='y', direction='in')

ax.tick_params(axis='x', direction='out')
ax.tick_params(axis='y', direction='out')

ax.tick_params(axis='x', direction='inout')
ax.tick_params(axis='y', direction='inout')

例如

1
2
3
4
5
6
7
8
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 3, 100)
y = np.exp(x)

ax.tick_params(axis='x', direction='in')
ax.tick_params(axis='y', direction='in')
ax.plot(x,y)

坐标轴比例

默认的坐标轴比例是线性的,但是我们可以指定对数,双对数等,使用 ax.set_yscale('log') 方法可以设置 y 轴为对数刻度,x 轴也同理。

例如

1
2
3
4
5
6
7
8
9
10
11
12
fig, axes = plt.subplots(1, 2, figsize=(7, 3))

x = np.linspace(0, 3, 100)
y = np.exp(x)

axes[0].plot(x,y)
axes[1].plot(x,y)

axes[0].set_title("linear")
axes[1].set_title("log")

axes[1].set_yscale("log")

批量设置 Axes 属性

前文中对 Axes 使用了各种 set_xxx 方法,实际可以打包为一个字典进行统一设置,例如

1
2
3
4
5
6
7
8
9
ax.set(
title="Title",
xlim=(0, 2),
ylim=(-1, 1),
xlabel="X",
ylabel="Y",
xticks=[0, 1, 2],
yticks=[-1, 0, 1],
)

字体设置

设置字体为 Times New Roman

1
2
matplotlib.rcParams['font.family'] = 'serif'
matplotlib.rcParams['font.serif'] = ['Times New Roman']

让 text 支持 LaTeX(默认为 false)

1
matplotlib.rcParams['text.usetex'] = True

如果使用的 LaTeX 命令不被支持,需要加入额外的 LaTeX 宏包,例如

1
matplotlib.rcParams["text.latex.preamble"] = r"\usepackage{amsmath,amssymb,amsfonts}"

修改字体以支持中文

1
matplotlib.rcParams['font.sans-serif']=['SimHei']

如果负号被显示为方块,可以修改下面的配置

1
matplotlib.rcParams['axes.unicode_minus'] = False

显然这里要求系统中已经安装 LaTeX 或者对应字体,否则无法使用。

添加文字

可以使用 ax.text 向指定的 Axes 对象添加文字,位置是在 Axes 的 xy 轴坐标,文字可以有很多细节处理,这里略过。

1
2
def text(self, x, y, s, fontdict=None, **kwargs):
...

例如

1
2
3
4
5
6
7
8
9
fig, ax = plt.subplots(figsize=(4, 3))

x = np.linspace(0, 3, 100)
y = np.exp(x)

ax.plot(x,y)

ax.plot(1,np.e,'o:r')
ax.text(1+0.2,np.e-0.2,"(1,e)")

局部放大图

局部放大图需要额外添加一个 Axes,下面使用 Axes.inset_axes 提供一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import numpy as np
import matplotlib.pyplot as plt

t = np.linspace(0, 2 * np.pi, 200)
a = np.sin(t)

fig, ax1 = plt.subplots()
ax1.plot(
t,
a,
"-o",
linewidth=1.0,
markevery=3,
markerfacecolor="none",
markersize="5",
markeredgewidth=1,
label="sin(x)",
)
ax1.set_xlabel("$x$")
ax1.set_ylabel("$y$")
ax1.legend()

ax2 = ax1.inset_axes([0.05, 0.05, 0.2, 0.2])
ax2.plot(
t,
a,
"-o",
linewidth=1.0,
markevery=3,
markerfacecolor="none",
markersize=5,
markeredgewidth=1,
)
ax2.set_xlim(2.8, 3.2)
ax2.set_ylim(0, 0.2)
ax2.set_xticks([])
ax2.set_yticks([])

ax1.indicate_inset_zoom(ax2, edgecolor="black", linewidth=1)

plt.show()

图像保存

使用 fig.savefig 保存对应的 Figure。相较于依赖“当前 Figure”的 plt.savefig,这种写法更明确。

最基本的用法只需要提供文件名即可,会根据文件名后缀自动选择保存格式,例如 .png、.pdf、.svg 等。(如果不含后缀,会自动加上 .png 后缀)

1
fig.savefig("sine_curve.png")

当然也可以添加一些参数,例如

1
fig.savefig("sine_curve.png", dpi=300, bbox_inches='tight', transparent=False)

常见的可选参数及其含义如下:

  • dpi:分辨率,例如 dpi=300
  • transparent:是否透明,例如 transparent=True 设置透明背景,默认为 False
  • bbox_inches:边界框控制,可以设为 "tight" 或 None,可以使用 bbox_inches='tight' 裁剪外部的空白部分
  • pad_inches:边框内间距,通常与 bbox_inches 搭配使用,控制图像的边框留白细节
  • facecolor:背景色,影响绘图方框之外部分的颜色,例如 facecolor='grey'

在批量绘图或脚本长期运行时,绘图保存或使用完毕后应及时调用 plt.close(fig) 关闭,以免积累内存并触发 “More than 20 figures” 警告。

1
2
3
4
fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
fig.savefig("line.png")
plt.close(fig)

如果使用面向过程的写法,不带参数的 plt.close() 会关闭当前 Figure:

1
2
3
plt.plot([0, 1], [0, 1])
plt.savefig("line.png")
plt.close()

关闭所有由 pyplot 管理的 Figure 可以使用 plt.close("all")。

在交互式环境中,如果还需要查看或操作 Figure,不要提前关闭;确认使用完毕后,既可以手动关闭窗口,也可以调用 plt.close(fig)。在 Jupyter 中批量生成图片时仍建议主动关闭。

动画

这里记录一下 Python 的 matplotlib 的动画效果,注意我们需要分别考虑如下两种运行环境,它们对于动画的支持是不一样的:

  • Jupyter Notebook、JupyterLab 或 VS Code 的 Notebook
  • 直接运行 Python 脚本

在 Jupyter 前端中,可以安装 ipympl 并启用交互式 widget 后端

1
%matplotlib widget

也可以使用 ani.to_jshtml() 或 ani.to_html5_video() 生成可嵌入 Notebook 的 HTML,而不依赖交互式后端。

在直接运行 Python 脚本时,Jupyter 的魔法指令是语法错误,对于动画而言,直接运行 Python 脚本反而比 Jupyter 更方便。

matplotlib 绘制动画主要包括两种方法:FuncAnimation 和 ArtistAnimation,下面的代码都需要导入如下的库

1
2
3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

FuncAnimation

FuncAnimation 的原理是首先绘制第一帧图像,然后逐帧调用更新函数进行修改,达到动画效果。

官方文档的示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
%matplotlib widget
fig, ax = plt.subplots()
t = np.linspace(0, 3, 40)
g = -9.81
v0 = 12
z = g * t**2 / 2 + v0 * t
v02 = 5
z2 = g * t**2 / 2 + v02 * t

scat = ax.scatter(t[0], z[0], c="b", s=5, label=f'v0 = {v0} m/s')
line2 = ax.plot(t[0], z2[0], label=f'v0 = {v02} m/s')[0]
ax.set(xlim=[0, 3], ylim=[-4, 10], xlabel='Time [s]', ylabel='Z [m]')
ax.legend()

def update(frame):
# for each frame, update the data stored on each artist.
x = t[:frame]
y = z[:frame]
# update the scatter plot:
data = np.stack([x, y]).T
scat.set_offsets(data)
# update the line plot:
line2.set_xdata(t[:frame])
line2.set_ydata(z2[:frame])
return (scat, line2)


ani = animation.FuncAnimation(fig=fig, func=update, frames=40, interval=30)
ani.save('demo.gif')
plt.show()

这里的参数含义依次为:

  • fig=fig:指定第一帧图像
  • func=update:指定更新函数,调用更新函数时会传递当前帧数 frame,额外的参数传递可以通过偏函数实现,启用 blit=True 时,更新函数必须返回所有被修改 Artist 的可迭代对象,例如
1
2
3
4
def update(frame, art, *, y=None):
...

ani = animation.FuncAnimation(fig=fig, func=partial(update, art=ln, y="foo"))
  • frames=40:一共绘制 40 帧,这里还可以传递迭代器,事实上直接使用数字类似于frames=range(40)
  • interval=30:两帧之间的间隔时间,单位毫秒,默认值为 200

还有一些可能需要的参数:参考官方文档

  • repeat: 是否重复,默认为 True
  • repeat_delay: 重复时的延时
  • init_func: 初始化函数,可以用于在第一帧之前清空图像
  • fargs: 传递给更新函数的额外参数,但是更建议用偏函数封装
  • blit:优化绘图效率,进行块状更新,默认为 False

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%matplotlib widget
w = 0.1
Lambda = 4
k = 2 * np.pi / Lambda

def update(t, w, k):
x = np.linspace(0, 10, 100)
y = np.cos(w * t - k * x)
line.set_data((x, y))
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)
return (line,)

fig, ax = plt.subplots()
(line,) = ax.plot([], [], lw=2)
ani = animation.FuncAnimation(fig=fig, func=update, frames=100, fargs=(w, k))
ani.save('demo2.gif')
plt.show()

ArtistAnimation

和前面的基于第一帧不断进行更新不同,ArtistAnimation 的原理是预先产生每一帧图像组成的列表,然后逐帧播放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%matplotlib widget
fig, ax = plt.subplots()

def f(x, y):
return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 400)
y = np.linspace(0, 2 * np.pi, 300).reshape(-1, 1)

img_list = []
for i in range(100):
x += np.pi / 15.
y += np.pi / 20.
im = ax.imshow(f(x, y), animated=True)
img_list.append([im])

ani = animation.ArtistAnimation(fig, img_list, interval=200, blit=True)
plt.show()

注意这里不接受frames参数,也可以换成 FuncAnimation 等价实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%matplotlib widget
fig, ax = plt.subplots()

def f(x, y):
return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 400)
y = np.linspace(0, 2 * np.pi, 300).reshape(-1, 1)

im = ax.imshow(f(x, y), animated=True)

def update(*args):
global x, y
x += np.pi / 15.
y += np.pi / 20.
im.set_array(f(x, y))
return im,

ani = animation.FuncAnimation(fig=fig, func=update, interval=200, frames=100, blit=True)
ani.save("demo3.gif",dpi=300)
plt.show()

注意:

  • 如果要提高输出动画的质量,首先需要完善绘图的细节,例如增加点数,然后在保存时可以指定 dpi,默认的 dpi 为 100,通常范围为 100-300。dpi 值越大,则图像越清晰,文件越大。
  • 在保存动画文件时,可能会报警告MovieWriter ffmpeg unavailable; using Pillow instead.,这是因为没有找到ffmpeg,手动下载即可:conda install ffmpeg

自定义绘图样式

Matplotlib 允许从下面两个层次进行绘图样式的定制:

  • matplotlib.style 模块
  • matplotlib.rcParams 字典

前者适合整套方案的切换,后者则适合精细控制单个参数,后者的优先级更高。

相比于在每一次绘图时进行的参数微调,这里讨论的两种方式都会产生全局性的影响。

matplotlib.style

1
import matplotlib.style

除了导入子模块,其实也可以通过 plt.style 直接访问。

下面介绍几个常用的功能。

列出可用的所有样式名称:(这是一个列表)

1
matplotlib.style.available

输出示例:

1
['classic', 'bmh', 'ggplot', 'dark_background', 'Solarize_Light2', ...]

加载一个或多个样式表(theme),会覆盖当前全局参数,例如

1
2
matplotlib.style.use('ggplot')
matplotlib.style.use(['dark_background', 'seaborn-v0_8'])

上面的样式修改是全局的,并且是持续性的。通常更建议使用 with 语句创建一个单独的上下文,在其中修改样式,退出后自动恢复原状。例如

1
2
with matplotlib.style.context('dark_background'):
plt.plot([1, 2, 3])

这里的样式实际是一个 name.mplstyle 文件,内置样式通常存放在如下目录中

1
site-packages\matplotlib\mpl-data\stylelib

.mplstyle 文件实质就是一个 INI 格式的键值对文件,例如 dark_background.mplstyle 的完整内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Set black background default line colors to white.

lines.color: white
patch.edgecolor: white

text.color: white

axes.facecolor: black
axes.edgecolor: white
axes.labelcolor: white
axes.prop_cycle: cycler('color', ['8dd3c7', 'feffb3', 'bfbbd9', 'fa8174', '81b1d2', 'fdb462', 'b3de69', 'bc82bd', 'ccebc4', 'ffed6f'])

xtick.color: white
ytick.color: white

grid.color: white

figure.facecolor: black
figure.edgecolor: black

### Boxplots
boxplot.boxprops.color: white
boxplot.capprops.color: white
boxplot.flierprops.color: white
boxplot.flierprops.markeredgecolor: white
boxplot.whiskerprops.color: white

我们可以自行提供一个样式文件,然后手动导入,例如

1
2
with matplotlib.style.context("./my_style.mplstyle"):
plt.plot([1, 2, 3])

也可以通过传入一个字典进行样式修改,例如

1
2
3
custom_style = {'lines.linewidth': 3, 'axes.titlesize': 14}
with matplotlib.style.context(custom_style):
plt.plot([1, 2, 3])

可以考虑使用第三方库 SciencePlots,它提供了科研绘图风格的一些样式文件。

matplotlib.rcParams

matplotlib.rcParams 是一个全局生效的键值对字典实例,用于保存和配置绘图参数,优先级高于之前加载的样式。

下面的代码中的 matplotlib.rcParams 都可以替换为 plt.rcParams,后者的使用更加方便。

可以直接对字典进行修改,例如

1
2
matplotlib.rcParams['font.size'] = 14
matplotlib.rcParams['lines.linewidth'] = 1

也可以用下面的接口设置某一类参数

1
matplotlib.rc('lines', linewidth=1, linestyle='--')

效果等价于

1
2
matplotlib.rcParams['lines.linewidth'] = 1
matplotlib.rcParams['lines.linestyle'] = '--'

这里的修改都是持续生效的,可以用下面的函数将其恢复为默认值。

1
matplotlib.rcdefaults()

与 matplotlib.style.context 类似,更建议使用 matplotlib.rc_context 创建一个临时的上下文,此时可以传入改动的参数字典

1
2
with matplotlib.rc_context({'font.size': 18, 'axes.titlesize': 20}):
plt.plot([1, 2, 3])

常见配置选项

下面是一些经常进行全局修改的绘图选项。

设置衬线字体和支持 LaTeX

1
2
3
text.usetex: True
font.family: serif
font.serif: Times New Roman

调整字号(这里统一设置为 14 是为了省事,实际绘图时根据需要,可以把特定文字调大或调小)

1
2
3
4
5
6
font.size: 14
axes.labelsize: 14
axes.titlesize: 14
xtick.labelsize: 14
ytick.labelsize: 14
legend.fontsize: 14

调整线宽,轴线宽和 marker 大小

1
2
lines.linewidth: 1.0
axes.linewidth: 1.0

调整 marker 大小和样式(默认实心,这里将其设置为空心)

1
2
3
lines.markersize: 5
lines.markerfacecolor: none
lines.markeredgecolor: auto

设置坐标轴刻度朝内(默认朝外)

1
2
xtick.direction: in
ytick.direction: in

调整图例样式(默认 edgecolor 是灰色,framealpha 是 0.8)

1
2
3
legend.facecolor: white
legend.edgecolor: black
legend.framealpha: 1.0

将其汇总整理如下(保存为单独的文件 myplot.mplstyle)

这里使用 constrained layout。它比 figure.autolayout 所启用的 tight layout 更灵活,尤其适合多子图、colorbar 和嵌套布局。单张图也可以在创建时显式写成 plt.subplots(layout="constrained");不要再对同一张图调用 tight_layout()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
savefig.dpi: 300
figure.constrained_layout.use: True
text.usetex: True
font.family: serif
font.serif: Times New Roman
pdf.fonttype: 42
ps.fonttype: 42
font.size: 14
axes.labelsize: 14
axes.titlesize: 14
xtick.labelsize: 14
ytick.labelsize: 14
legend.fontsize: 14
lines.linewidth: 1.0
lines.markersize: 5
axes.linewidth: 1.0
lines.markerfacecolor: none
lines.markeredgecolor: auto
xtick.direction: in
ytick.direction: in
legend.facecolor: white
legend.edgecolor: black
legend.framealpha: 1.0

或者直接复制如下代码片段并运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import matplotlib.pyplot as plt


plt.rcParams.update({
"savefig.dpi": 300,
"figure.constrained_layout.use": True,
"text.usetex": True,
"font.family": "serif",
"font.serif": ["Times New Roman"],
"pdf.fonttype": 42,
"ps.fonttype": 42,
"font.size": 14,
"axes.labelsize": 14,
"axes.titlesize": 14,
"xtick.labelsize": 14,
"ytick.labelsize": 14,
"legend.fontsize": 14,
"lines.linewidth": 1.0,
"lines.markersize": 5,
"axes.linewidth": 1.0,
"lines.markerfacecolor": "none",
"lines.markeredgecolor": "auto",
"xtick.direction": "in",
"ytick.direction": "in",
"legend.facecolor": "white",
"legend.edgecolor": "black",
"legend.framealpha": 1.0,
})

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import numpy as np
import matplotlib.pyplot as plt
# %% Example 1
h = np.array([1 / 4, 1 / 8, 1 / 16, 1 / 32, 1 / 64, 1 / 128])

fig, ax = plt.subplots()
ax.loglog(h, h, "o-", label="1st order")
ax.loglog(h, h**2, "s-", label="2nd order")
ax.loglog(h, h**4, "^-", label="4th order")
ax.set_xlabel("h")
ax.set_ylabel("Error")
ax.set_title("Default Matplotlib Style")
ax.grid(True)
ax.minorticks_off()
ax.legend()
fig.savefig("python-plot-1-default.png")
plt.close(fig)

with plt.style.context("./myplot.mplstyle"):
fig, ax = plt.subplots()
ax.loglog(h, h, "o-", label="1st order")
ax.loglog(h, h**2, "s-", label="2nd order")
ax.loglog(h, h**4, "^-", label="4th order")
ax.set_xlabel("h")
ax.set_ylabel("Error")
ax.set_title("Scientific Style")
ax.grid(True)
ax.minorticks_off()
ax.legend()
fig.savefig("python-plot-1-science.png")
plt.close(fig)


# %% Example 2
def func(x, p):
return x ** (2 * p + 1) / (1 + x ** (2 * p))


x = np.linspace(0.75, 1.25, 201)
plist = [10, 15, 20, 30, 50, 100]

fig, ax = plt.subplots()
for p in plist:
ax.plot(x, func(x, p), label=f"p={p}")
ax.autoscale(tight=True)
ax.set_xlabel("$x$")
ax.set_ylabel("$y$")
ax.set_title("Default Matplotlib Style")
ax.legend()
fig.savefig("python-plot-2-default.png")
plt.close(fig)

with plt.style.context("./myplot.mplstyle"):
fig, ax = plt.subplots()
for p in plist:
ax.plot(x, func(x, p), label=f"$p={p}$")
ax.autoscale(tight=True)
ax.set_xlabel("$x$")
ax.set_ylabel("$y$")
ax.set_title("Scientific Style")
ax.legend()
fig.savefig("python-plot-2-science.png")
plt.close(fig)

Example 1

python-plot-1-default python-plot-1-science

Example 2

python-plot-2-default python-plot-2-science