Python Matplotlib 绘图笔记
这里主要关注 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 | import numpy as np |
由于 matplotlib 的内容太多太杂,这里只会介绍最容易理解的,最本质的面向对象的接口,然后作为辅助会介绍其它等价的,或者更方便的调用方式。但是 matplotlib 的面向对象其实有很多层,在每一层都可能为同一个功能提供了等价的接口,这里为了简化笔记,主要考虑 Figure 和 Axes 层面提供的接口。
由于个人目前不需要使用各种统计图,因此这里不作讨论。
简单例子
面向过程风格的绘图例子如下,主要是 plt 的函数调用:
1 | plt.plot( |
面向对象风格的绘图例子如下,主要是画布和子图的方法调用:
1 | fig, ax = plt.subplots() |
基本概念
对于一个图像,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 | fig = plt.figure() |
常用参数的含义:
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 | fig = plt.figure() |
多个 Axes 在同一个 Figure 上是按照矩阵格式排列布局的,例如 3 行 2 列一共 6 个位置,当然并不要求填满所有位置。 可以使用如下方式,依次添加 Axes 对象,需要给出整体的行列数以及自身的序号(从 1 开始)
1 | fig = plt.figure() |
得到的空给框架如下(这里的刻度尺都是动态的,会随着填入数据的范围而调整)
对于上面函数中小的个位数索引(即总位置不超过 9 个),可以直接拼成一个三位数参数,例如
1 | ax1 = fig.add_subplot(3,2,1) |
注意:如果反复添加 Axes,它们可能会相互重叠显示,显得非常混乱。
一次性创建 Figure 并添加 Axes
对于多行多列的矩阵布局,先创建 Figure,然后一个个添加 Axes 非常繁琐,可以使用pyplot 提供的 plt.subplots 接口一次性完成。
例如,创建一个Figure并添加一个Axes
1 | fig, ax = plt.subplots() |
例如,创建一个Figure并添加多个Axes
1 | fig, axes = plt.subplots(3,2) |
通过访问这个数组的分量来实现对每一个 Axes 的修改。
需要注意的是,还有一个名称非常相似的接口
plt.subplot,它是面向过程绘图的接口,含义是为当前的 Figure 添加一个 Axes。
绘制曲线
使用 Axes 的 plot 方法绘制曲线图,假设 x,y,x2,y2 都是一维的数组,那么可以使用下面几种方式绘制曲线:
ax.plot(y):一条散点连成的曲线,横坐标取从0开始的整数,纵坐标取自yax.plot(x,y):一条散点连成的曲线,横坐标取自x,纵坐标取自yax.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 | fig, ax = plt.subplots(figsize=(7, 5)) |
曲线样式 linestyle
支持的曲线样式如下:
- 实线(默认):solid,简写
- - 短虚线(点虚线):dotted,简写
: - 长虚线(破折线):dashed,简写
-- - 长短交错虚线(点划线):dashdot,简写
-. - 不画线(什么也没有):None,简写
""或者''
可以使用关键字参数 linestyle='dotted' 设置曲线样式。支持如下的简写:
- 将曲线样式使用简写,例如
linestyle=':' - 将关键字
linestyle简写为ls,例如ls='-.'
例如
1 | fig, ax = plt.subplots(figsize=(7, 5)) |
还支持调整线的宽度(linewidth,简称 lw),线宽的默认值是 1.5(MATLAB 的线宽默认是 0.5),例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
设置线宽是高频使用的选项,无论是 matplotlib 还是 MATLAB 绘图,因为前者的默认线宽 1.5 略粗,后者的默认线宽 0.5 略细。
绘图标记 marker
常用的点的标记如下:
- 点
'.' - 像素点
',' - 实心圆
'o'(小写字母 o) - 星号
'*' - 乘号
'x' - 加号
'+' - 菱形
'D',瘦菱形'd' - 正方形
's' - 三角形
'^','<','v','>'(不同的四个朝向)
可以使用关键字参数 marker='*' 设置绘图标记。
例如:(这里像素点根本看不清)
1 | fig, ax = plt.subplots(figsize=(7, 5)) |
关于绘图标记,我们还可以指定标记的大小(markersize 简写 ms),内部填充颜色(markerfacecolor 简写 mfc),边框填充颜色(markeredgecolor 简写 mec),例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
MATLAB 默认是不会填充标记的内部,呈现的是空心的标记,但是 matplotlib 的默认行为是填充,可以通过
markerfacecolor="none"选项阻止填充。
如果数据很多,不需要在每一个数据点绘制标记,否则标记会堆叠在一起,掩盖数据,可以通过 markevery 选项控制,例如
1 | fig, ax = plt.subplots() |
markevery 选项如果提供的是一个值,代表间隔,如果提供两个数组成的元组,则第一个元素为起始的偏移量,第二个元素为间隔,可以用来错开多条曲线的标记。
使用 linestyle='',marker='o' 可以达到散点图的效果,例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
当然也可以使用专门的散点图接口
ax.scatter。
fmt 参数
对于曲线常用的标记、样式、颜色三种设置,可以把相应的简写合并为一个字符串参数,这里每一个部分都是可选的,顺序也是任意的。
1 | '[marker][line][color]' |
例如:
o:r圆点,点虚线,红色go--绿色圆点,破折线
示例如下
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
图像细节
标题和轴标签
支持两层标题:Figure 可以使用 suptitle 设置标题,Axes 可以使用 set_title 设置标题。
例如
1 | fig, axes = plt.subplots(1,2,figsize=(7, 3)) |
可以给每一个坐标轴都可以加上轴标签:
1 | ax.set_xlabel("x") |
例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
对于标题和轴标签这类的文本选项,有很多额外的选项:
- 位置:例如
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 | fig, ax = plt.subplots(figsize=(4, 3)) |
可以对图例进行细节调整,包括加上单独的标题,移除边框,指定位置等。(当然这些细节也可以通过 GUI 界面进行调整)
例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
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 | fig, ax = plt.subplots(figsize=(4, 3)) |
轴的单位长度比
可以手动设置 x 轴和 y 轴的单位长度的比例,默认是 auto,也就是会根据作图需要自动调整,可以改成相等或者某个固定的值。
1 | ax.set_aspect('auto') |
例如
1 | fig, ax = plt.subplots(figsize=(4, 4)) |
边框隐藏
默认情况下作图会绘制四个边框,有时我们需要去掉某些边框,有两种方案:
- 设置边框透明度为 0
- 设置边框颜色为空
例如:
1 | fig, ax = plt.subplots(figsize=(4, 4)) |
坐标轴范围
通常 Axes 的坐标轴范围会根据数据范围自动生成,并且相对数据本身范围有一定的冗余,可以使用下面的代码使得数据轴范围更紧。
1 | ax.autoscale(tight=True) |
某些数据图需要紧贴的效果,某些数据图则需要冗余,需要根据情况自行选择。
我们也可以手动指定坐标轴的范围(数据如果不在范围内就不会显示)
1 | ax.set_xlim(-1,2) |
例如
1 | fig, ax = plt.subplots(figsize=(4, 4)) |
坐标轴刻度
默认线性坐标轴通常只显示主刻度;某些样式或对数坐标轴会启用次刻度。如果需要,可以显式关闭。
最直接的面向对象写法如下
1 | ax.minorticks_off() |
如需分别控制两条轴,也可以把对应轴的次刻度定位器设为 matplotlib.ticker.NullLocator()。
可以指定一条轴上使用哪些刻度,使用 ax.set_xticks 方法传入一个数组。
例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
我们可以同时指定刻度位置和标签。单独使用 set_xticklabels 容易使标签与刻度位置失配,不推荐这样做;应将标签作为 set_xticks 的第二个参数传入。例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
坐标轴刻度朝向
关于坐标轴的刻度朝向,matplotlib 默认刻度朝外(但是 MATLAB 默认刻度朝内),可以用下面的命令调整当前绘图窗口的刻度朝向
1 | ax.tick_params(axis='x', direction='in') |
例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
坐标轴比例
默认的坐标轴比例是线性的,但是我们可以指定对数,双对数等,使用 ax.set_yscale('log') 方法可以设置 y 轴为对数刻度,x 轴也同理。
例如
1 | fig, axes = plt.subplots(1, 2, figsize=(7, 3)) |
批量设置 Axes 属性
前文中对 Axes 使用了各种 set_xxx 方法,实际可以打包为一个字典进行统一设置,例如
1 | ax.set( |
字体设置
设置字体为 Times New Roman
1 | matplotlib.rcParams['font.family'] = 'serif' |
让 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 | def text(self, x, y, s, fontdict=None, **kwargs): |
例如
1 | fig, ax = plt.subplots(figsize=(4, 3)) |
局部放大图
局部放大图需要额外添加一个 Axes,下面使用 Axes.inset_axes 提供一个示例。
1 | import numpy as np |
图像保存
使用 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=300transparent:是否透明,例如transparent=True设置透明背景,默认为Falsebbox_inches:边界框控制,可以设为"tight"或None,可以使用bbox_inches='tight'裁剪外部的空白部分pad_inches:边框内间距,通常与bbox_inches搭配使用,控制图像的边框留白细节facecolor:背景色,影响绘图方框之外部分的颜色,例如facecolor='grey'
在批量绘图或脚本长期运行时,绘图保存或使用完毕后应及时调用 plt.close(fig) 关闭,以免积累内存并触发 “More than 20 figures” 警告。
1 | fig, ax = plt.subplots() |
如果使用面向过程的写法,不带参数的 plt.close() 会关闭当前 Figure:
1 | plt.plot([0, 1], [0, 1]) |
关闭所有由 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 | import numpy as np |
FuncAnimation
FuncAnimation 的原理是首先绘制第一帧图像,然后逐帧调用更新函数进行修改,达到动画效果。
官方文档的示例如下
1 | %matplotlib widget |
这里的参数含义依次为:
fig=fig:指定第一帧图像func=update:指定更新函数,调用更新函数时会传递当前帧数 frame,额外的参数传递可以通过偏函数实现,启用blit=True时,更新函数必须返回所有被修改 Artist 的可迭代对象,例如
1 | def update(frame, art, *, y=None): |
frames=40:一共绘制 40 帧,这里还可以传递迭代器,事实上直接使用数字类似于frames=range(40)interval=30:两帧之间的间隔时间,单位毫秒,默认值为 200
还有一些可能需要的参数:参考官方文档
repeat: 是否重复,默认为 Truerepeat_delay: 重复时的延时init_func: 初始化函数,可以用于在第一帧之前清空图像fargs: 传递给更新函数的额外参数,但是更建议用偏函数封装blit:优化绘图效率,进行块状更新,默认为 False
例如
1 | %matplotlib widget |
ArtistAnimation
和前面的基于第一帧不断进行更新不同,ArtistAnimation 的原理是预先产生每一帧图像组成的列表,然后逐帧播放。
1 | %matplotlib widget |
注意这里不接受frames参数,也可以换成 FuncAnimation 等价实现
1 | %matplotlib widget |
注意:
- 如果要提高输出动画的质量,首先需要完善绘图的细节,例如增加点数,然后在保存时可以指定 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 | matplotlib.style.use('ggplot') |
上面的样式修改是全局的,并且是持续性的。通常更建议使用 with 语句创建一个单独的上下文,在其中修改样式,退出后自动恢复原状。例如
1 | with matplotlib.style.context('dark_background'): |
这里的样式实际是一个 name.mplstyle 文件,内置样式通常存放在如下目录中
1 | site-packages\matplotlib\mpl-data\stylelib |
.mplstyle 文件实质就是一个 INI 格式的键值对文件,例如 dark_background.mplstyle 的完整内容如下
1 | # Set black background default line colors to white. |
我们可以自行提供一个样式文件,然后手动导入,例如
1 | with matplotlib.style.context("./my_style.mplstyle"): |
也可以通过传入一个字典进行样式修改,例如
1 | custom_style = {'lines.linewidth': 3, 'axes.titlesize': 14} |
可以考虑使用第三方库 SciencePlots,它提供了科研绘图风格的一些样式文件。
matplotlib.rcParams
matplotlib.rcParams 是一个全局生效的键值对字典实例,用于保存和配置绘图参数,优先级高于之前加载的样式。
下面的代码中的
matplotlib.rcParams都可以替换为plt.rcParams,后者的使用更加方便。
可以直接对字典进行修改,例如
1 | matplotlib.rcParams['font.size'] = 14 |
也可以用下面的接口设置某一类参数
1 | matplotlib.rc('lines', linewidth=1, linestyle='--') |
效果等价于
1 | matplotlib.rcParams['lines.linewidth'] = 1 |
这里的修改都是持续生效的,可以用下面的函数将其恢复为默认值。
1 | matplotlib.rcdefaults() |
与 matplotlib.style.context 类似,更建议使用 matplotlib.rc_context 创建一个临时的上下文,此时可以传入改动的参数字典
1 | with matplotlib.rc_context({'font.size': 18, 'axes.titlesize': 20}): |
常见配置选项
下面是一些经常进行全局修改的绘图选项。
设置衬线字体和支持 LaTeX
1 | text.usetex: True |
调整字号(这里统一设置为 14 是为了省事,实际绘图时根据需要,可以把特定文字调大或调小)
1 | font.size: 14 |
调整线宽,轴线宽和 marker 大小
1 | lines.linewidth: 1.0 |
调整 marker 大小和样式(默认实心,这里将其设置为空心)
1 | lines.markersize: 5 |
设置坐标轴刻度朝内(默认朝外)
1 | xtick.direction: in |
调整图例样式(默认 edgecolor 是灰色,framealpha 是 0.8)
1 | legend.facecolor: white |
将其汇总整理如下(保存为单独的文件 myplot.mplstyle)
这里使用 constrained layout。它比 figure.autolayout 所启用的 tight layout 更灵活,尤其适合多子图、colorbar 和嵌套布局。单张图也可以在创建时显式写成 plt.subplots(layout="constrained");不要再对同一张图调用 tight_layout()。
1 | savefig.dpi: 300 |
或者直接复制如下代码片段并运行
1 | import matplotlib.pyplot as plt |
测试代码如下
1 | import numpy as np |
Example 1
Example 2