这里主要关注 Python Matplotlib 绘图的技术性内容 ,不讨论非技术性内容,例如论文中的绘图要求或偏好。
Matplotlib 介绍 matplotlib 是模仿 MATLAB 风格的 Python 2D 绘图库(3D 的功能比较弱,还是基于 2D 引擎勉强实现的),同时提供了两套 api:
一个是面向过程的,主要调用 matplotlib.pyplot 的函数;
一个是面向对象的,主要调用 matplotlib 的两个子类:matplotlib.figure.Figure 和 matplotlib.axes.Axes ,使用它们的方法进行细节操作。
面向过程的 api 适合简易使用的场景,但是不容易弄清原理;为了更复杂的绘图要求,这里主要采用面向对象的 api。 (MATLAB 绘图也是一样的问题,而且主要都在使用面向过程的接口)
关于绘图的呈现方式,在 Jupyter 中很可能会自动绘图而不需要 plt.show(),这与魔法指令 %matplotlib inline 有关。
在笔记中,需要使用如下模块:
1 2 3 import numpy as npimport matplotlibimport 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 边框线
各个元素如下图所示
创建 Figure 对象的函数原型如下
1 2 3 4 5 def figure (num=None , figsize=None , dpi=None , facecolor=None , edgecolor=None , frameon=True ) ... fig = plt.figure() fig = plt.figure(figsize=(4 ,3 ))
常用参数的含义:
figsize 尺寸,依次为横向宽度和纵向高度,单位为英寸,例如 figsize=(5,2.7)
dpi 分辨率
facecolor 背景颜色
edgecolor 边框颜色
frameon 是否显示边框
关于 dpi 选项,需要一些额外的说明:
默认的 dpi 为 100,通常范围为 100-300。dpi 值越大,则图像越清晰,文件越大。
在交互式显示时,通常不建议设置过大的 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 ) ax1 = fig.add_subplot(321 )
注意:如果反复添加 Axes,它们可能会相互重叠显示,显得非常混乱。
对于多行多列的矩阵布局,先创建 Figure,然后一个个添加 Axes 非常繁琐,可以使用pyplot 提供的 plt.subplots 接口一次性完成。
例如,创建一个Figure并添加一个Axes
1 2 3 fig, ax = plt.subplots() type (ax)
例如,创建一个Figure并添加多个Axes
1 2 3 4 fig, axes = plt.subplots(3 ,2 ) type (axes) print (axes.shape)
通过访问这个数组的分量来实现对每一个 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' 可以达到散点图的效果,并且可能比专门的散点图 plt.scatter 更好,例如
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 )
fmt 参数 对于曲线常用的标记、样式、颜色三种设置,可以把相应的简写合并为一个字符串参数,这里每一个部分都是可选的,顺序也是任意的。
例如:
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' )
边框隐藏 默认情况下作图会绘制四个边框,有时我们需要去掉某些边框,有两种方案:
例如:
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 )
坐标轴刻度 默认坐标轴会显示主级和次级的坐标,但是有时次级坐标会显得太密集,可以手动关闭。
例如基于 plt 的写法如下
1 2 3 4 from matplotlib.ticker import NullLocatorplt.gca().xaxis.set_minor_locator(NullLocator()) plt.gca().yaxis.set_minor_locator(NullLocator())
基于面向对象的写法如下
1 2 ax.xaxis.set_minor_locator(NullLocator()) ax.yaxis.set_minor_locator(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])
我们可以在指定刻度的基础上,将刻度的数值换成另外的文字,此时使用 ax.set_xticklabels 方法传入一个数组。例如
1 2 3 4 5 6 7 8 9 10 11 12 13 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]) ax.set_xticklabels(['0' ,'$\pi/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 matplotlib.rcParams['font.sans-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,下面提供一个示例
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 import numpy as npimport matplotlib.pyplot as pltimport matplotlib as mplfrom mpl_toolkits.axes_grid1.inset_locator import mark_insetfrom mpl_toolkits.axes_grid1.inset_locator import inset_axest = np.linspace(0 , 2 * np.pi, 200 ) a = np.sin(t) fig, ax1 = plt.subplots() line1 = 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 = inset_axes( ax1, width="20%" , height="20%" , loc="lower left" , bbox_to_anchor=(0.05 , 0.05 , 0.9 , 0.9 ), bbox_transform=ax1.transAxes, ) 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([]) mark_inset(ax1, ax2, loc1=2 , loc2=4 , fc="none" , ec="k" , lw=1 ) plt.show()
图像保存 对于面向过程的接口,使用 plt.savefig 方法可以保存当前图像;对于面向对象的接口,使用 fig.savefig 方法可以保存对应图像。
最基本的用法只需要提供文件名即可,会根据文件名后缀自动选择保存格式,例如 .png、.pdf、.svg 等。(如果不含后缀,会自动加上 .png 后缀)
1 2 3 plt.savefig("sine_curve.png" ) fig.savefig("sine_curve.png" )
当然也可以添加一些参数,例如
1 2 3 plt.savefig("sine_curve.png" , dpi=300 , bbox_inches='tight' , transparent=False ) fig.savefig("sine_curve.png" , dpi=300 , bbox_inches='tight' , transparent=False )
常见的可选参数及其含义如下:
dpi:分辨率,例如 dpi=300
transparent:是否透明,例如 transparent=true 设置透明背景,默认为 false
bbox_inches:边界框控制,包括 tight / standard / None 三种选择,可以使用 bbox_inches='tight' 裁剪外部的空白部分
pad_inches:边框内间距,通常与 bbox_inches 搭配使用,控制图像的边框留白细节
facecolor:背景色,影响绘图方框之外部分的颜色,例如 facecolor='grey'
动画 这里记录一下 Python 的 matplotlib 的动画效果,注意我们需要分别考虑如下三种运行环境,它们对于动画的支持是不一样的:
浏览器启动 Jupyter Notebook 或 Jupyter Lab
直接运行 Python 脚本
VSCode 启动 Jupyter(目前仍然不支持动画)
例如浏览器启动 Jupyter 可能需要相关的插件(例如 ipympl 插件),并且需要魔法命令
在启动内核后,至少需要执行这个魔法命令一次,否则无法播放动画,两个同时播放的动画甚至还会相互影响。
在直接运行 Python 脚本时,Jupyter 的魔法指令是语法错误,对于动画而言,直接运行 Python 脚本反而比 Jupyter 更方便。
matplotlib 绘制动画主要包括两种方法:FuncAnimation 和 ArtistAnimation,下面的代码都需要导入如下的库
1 2 3 import numpy as npimport matplotlib.pyplot as pltimport 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 notebook 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 ): x = t[:frame] y = z[:frame] data = np.stack([x, y]).T scat.set_offsets(data) 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,额外的参数传递可以通过偏函数实现,返回值类型必须为元组,例如
1 2 3 4 def update (frame, art, *, y=None ): ... ani = animation.FuncAnimation(fig=fig, update=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 notebook 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 notebook 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 = plt.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 notebook 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 = plt.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
除了导入子模块,其实也可以通过 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 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 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' ] = 2 matplotlib.rcParams['lines.linestyle' ] = '--'
这里的修改都是持续生效的,可以用下面的函数将其恢复为默认值。
与 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
调整图例样式
1 2 3 legend.facecolor: white legend.edgecolor: black legend.framealpha: 1.0
将其汇总整理如下(保存为单独的文件 myplot.mplstyle)
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.autolayout: 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 pltplt.rcParams.update({ "savefig.dpi" : 300 , "figure.autolayout" : 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 npimport matplotlib.pyplot as pltfrom matplotlib.ticker import NullLocatorh = 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.xaxis.set_minor_locator(NullLocator()) ax.yaxis.set_minor_locator(NullLocator()) ax.legend() fig.savefig("python-plot-1-default.png" ) 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.xaxis.set_minor_locator(NullLocator()) ax.yaxis.set_minor_locator(NullLocator()) ax.legend() fig.savefig("python-plot-1-science.png" ) 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" ) 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" )
Example 1
Example 2