用 matplotlib 画出规范的论文插图

我最近所写的论文中基本上放弃了 Origin,而转为用 matplotlib 画几乎所有的插图。相比专业的 Origin,MPL 基本可以替代所有的功能,甚至单论功能还略有胜出。从可定制性角度,两者也接近,但 MPL 没有 Origin 图形化操作的直观性,这方面有所欠缺。而且 MPL 默认的主题和格式都与论文所要求的质量相去甚远,不像 Origin 一样基本默认格式就能凑合用了。

从我自己的研究领域来看,插图的规范性,主要有几个方面的问题需要设置:

  1. 尺寸,包括图形尺寸、线宽等
  2. 标注,包括对图线的标注、legend等
  3. 图层,叠加不同的坐标轴等
  4. 文字样式、字号等
  5. 输出格式

一个基本图形的示例

下面给出了一个示例的代码,通过自定义各种格式基本上可以说符合正式出版的要求:

 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
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'STIXGeneral'
plt.rcParams['mathtext.fontset'] = 'stix'

## DATA FOR DEMO
X = np.linspace(0, 2, 50)
Y1 = np.sin(3*X)
Y2 = np.cos(2*X)

fig, ax = plt.subplots(figsize=(7/2.54, 4.5/2.54))
ax.grid(True)
c1 = '#023474'
c2 = '#ef0107'

ax.plot(X, Y1, 'o-', c=c1, mec='none', ms=3, label='Line $y=\sin(3\\times x)$')
ax.plot(X, Y2, 's-', c=c2, mec='none', ms=3, label='Line $y=\cos(2\\times x)$')

ax.set_xlabel('X', fontsize=9)
ax.set_ylabel('Y', fontsize=9)
ax.set_ylim(-1.1, 1.1)
ax.tick_params(axis='x', labelsize=7)
ax.tick_params(axis='y', labelsize=7, color=c1)
ax.legend(loc=0, fontsize=7, numpoints=1, frameon=False)

plt.show()
# plt.savefig('demo.svg', bbox_inches='tight')

效果如下图所示:

single

看起来在 MPL 中为了画图需要写很多行代码,在这里连空行一共是 27 行。但其实画图相关的代码是中间的 10 行左右,其它的都是导入模块和产生演示用的数据的。这些画图的代码做了这么几件事情:

  1. 定义 fig 和 axes。我喜欢直接用 plt.subplots 一次性定义两者,并且限制 fig 的尺寸。在一开始就定义好 fig 的尺寸,而不是用默认的,主要的好处是线宽、文字大小等都很统一,后期不需要缩放大小就可以直接插到论文中。注意在定义尺寸时用的是英制单位,我是用厘米的数值再除以 2.54 的转换系数。
  2. 在 12 行中打开 grid 的显示。这个似乎在 Matlab 中是默认打开的,是否需要取决于个人喜好和期刊要求了。
  3. 在 13、14 行定义了自己的颜色。这两个颜色取自阿森纳的队标(来源),我个人认为搭起来刚好不太传统也足够正式。MPL 默认的颜色基本就是 RGB,非常简单粗暴,当然也有人喜欢。
  4. 真正在「画图」的只有 16、17 两行,它最重要的工作是定义图线的数据和 label,注意 label 中使用了 LaTeX 公式。mecms 分别是 markeredgecolor 和 markersize 的缩写。
  5. 再后面一段定义了坐标轴的 label,坐标轴的范围,tick 的文字大小,还有 legend 的格式。值得一提的是 legend 的可定选项相当多,最好按实际的需要定义。
  6. 最后是显示图形或者保存。保存的格式,我的经验是 PDF(或 EPS) 用于 LaTeX 格式,SVG 用于 Word 或网页等。

输出图形

MPL 提供的 savefig 函数可以输出多种格式,最实用的是 PNG、PDF、EPS 还有 SVG。如果论文用的是 LaTeX,那么 PDF 和 EPS 是最佳的选择,直接输出的格式基本就可以了,不需要额外的定制。

如果是用于 Word 中,我目前的经验是 EMF 的兼容性最好,可惜从某个版本开始就不再提供 EMF 格式的支持,因此需要用 SVG 中转一下,这里需要用到 Inkscape 这个开源的矢量编辑软件。Inkscape 原生的格式正是 SVG,所以可以直接用它打开,另存为 EMF,但这样比较慢,因为 Inkscape 本身启动就很慢,而且莫名地在高分屏上占用内存非常大。更好的办法是用它的命令行工具,甚至更方便地,把命令行的调用也放到 Python 中:

1
2
3
import subprocess
subprocess.call('<inkscape_exe_path> ' + svg_name + '.svg '
                '--export-emf=' + emf_name + '.emf')

这样用起来就相当于用 Python 直接输出了 EMF 文件了。

双坐标轴

有些时候为了对比的需要,会把两条相同 X 不同 Y 的曲线画在一起,这在 Origin 中一般是用多个图层,MPL 中则可以用 twinx 解决。如果同样用上面的 demo 数据,部分画图代码的示例如下:

 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
fig, ax = plt.subplots(figsize=(7/2.54, 4.5/2.54))
ax2 = ax.twinx()
ax.grid(True)
ax2.grid(True)
c1 = '#023474'
c2 = '#ef0107'

ax.plot(X, Y1, 'o-', c=c1, mec='none', ms=3, label='$y=\sin(3\\times x)$')
ax.set_xlabel('X', fontsize=9)
ax.set_ylabel('Y1', fontsize=9, color=c1)
ax.set_ylim(-1.1, 1.1)

ax2.plot(X, Y2, 's-', c=c2, mec='none', ms=3, label='$y=10 \cos(2\\times x)$')
ax2.set_ylabel('Y2', fontsize=9, color=c2)
ax2.set_ylim(-11, 11)

ax.spines['left'].set_color(c1)
for label in ax.get_yticklabels(): label.set_color(c1)
ax.tick_params(axis='x', labelsize=7)
ax.tick_params(axis='y', labelsize=7, color=c1)

ax2.spines['left'].set_visible(False)
ax2.spines['right'].set_color(c2)
for label in ax2.get_yticklabels(): label.set_color(c2)
ax2.tick_params(axis='x', labelsize=7)
ax2.tick_params(axis='y', labelsize=7, color=c2)

h1, l1 = ax.get_legend_handles_labels()
h2, l2 = ax2.get_legend_handles_labels()
ax.legend(h1+h2, l1+l2, loc=0, fontsize=7, numpoints=1)

效果如下图所示:

double

相比相同 XY 坐标轴的情况,这里其实也非常类似,主要区别有两点:

  1. 设置格式需要分别在不同的 axes 中。
  2. 需要设置 spine 的显示,避免遮盖,以及[可选的]设置不同颜色,便于识别。
  3. legend 这里用了 trick 把两个显示在了一起,也可以选择分别显示。

字体

字体、backend 等等都是一个大坑,这里也只敢讲一点最基本的东西。

在上面两个 demo 的图片中,细心的人可能就已经注意到了,坐标轴上数字的字体是不同的,后者是 LaTeX 标志性的 Computer Modern 字体,原因在于后者用了 LaTeX 引擎来产生。MPL 本身提供了用(用户自己的) LaTeX 的选项,或者用它内嵌的 LaTeX 解析器。如果用默认的后者,虽然也可以正确地显示 LaTeX 公式等,单独显示的时候效果也还不错,但如果跟其他文字显示在一起,就非常地不协调,字号和对齐等都不够完善,这一点在 stackoverflow 上也有讨论,比如这个

如果用外部 LaTeX,按下面的设置可以实现文字部分用非衬线字体,数学部分用 CM 字体:

1
2
3
plt.rcParams['text.usetex']= True
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = 'Calibri'

这个设置仅供参考,如果对 LaTeX 字体设置熟悉的话,应该可以设置出更复杂的更美观的效果。例如用 LaTeX 实现所有字体都用非衬线字体(当然我觉得数学公式还是得用衬线字体来排):

1
2
3
4
5
6
7
8
plt.rcParams['text.usetex'] = True
plt.rcParams['text.latex.preamble'] = [
       r'\usepackage{siunitx}',
       r'\sisetup{detect-all}',
       r'\usepackage{helvet}',
       r'\usepackage{sansmath}',
       r'\sansmath'
]

启用外部 LaTeX 可能会遇到一个问题,我用的是 MikTeX,虽然设置了自动安装本机没有的包,但第一次启用时,仍然提示我没有找到 type1cm 这个包,手动下载的时候提示 404 了,所以自己去网上找了一下,这里可以下载到我手动从 CTAN 下载的版本,解压到 latex 文字夹就可以用了。

当然,我自己的观点是,在绝大多数情况下,画个小图没有必要请出 LaTeX 这个牛刀,而且用 LaTeX 时编译需要的时间也会长很多(十几秒到一分钟不等)。因此我更喜欢另一种开箱即用效果也不差的方案,也就是第一个 demo 中用的方案,把所有的字体都设为 STIX 字体(类似 Times 字体)。


基本上把这次想讲的点都写了一遍了,这次挖的坑大致写到这里,以后如果有补充的再写新的吧。

links

social