[关闭]
@1007477689 2020-04-14T08:40:47.000000Z 字数 6421 阅读 615

Matplotlib 优雅作图笔记

转载 Python


本篇文章转自“可乐学人”微信公众号,原文地址在下贴出
https://mp.weixin.qq.com/s/2ZLcqqpDLkPDkg5suESYyw


MatplotlibPython 中最基本也最重要的可视化工具,可以画出拥有出版质量(Publication-Quality)的图表。如今 Matplotlib 已经衍生出了很多高层库,但如果需要对图表进行更加精细的个性化设置,就必须深入学习 Matplotlib,官网的教程和案例就是最好的学习资源。


高效性:永远永远面向对象

高效的前提是充分理解。这部分首先简单介绍一下 Matplotlib 的两种接口,在理解 Matplotlib 逻辑的基础上,实现高效作图就是顺理成章的事了。

一、Matplot的两种接口

Matplotlib 有两种接口:基于 Matlab 的和基于面向对象的。基于 Matlab 的是 pyplot 提供的,比较简单,但容易混乱;基于面向对象的方法结构清晰,是 Matplotlib 的精髓。

  1. 基于 Matlab 的:自动创建和管理图和坐标系,用 pyplot 函数作图;
  2. 基于面向对象的:显示创建图和坐标系,再调用对象的方法来作图。

网上好多资料都是把两种接口混合使用的,这对理解 Maplotlib 的作图逻辑很不友好。Python 中,万物皆对象,为了高效作图,我们首先需要做的就是学习使用面向对象的接口。

简单来说,Figure 是画布,Axes 是画布上一个个区域,所有线(line)、点(marker)、文字(text)等基础类元素都是寄生在容器类元素上的。官网也贴心地给出了这些对象的关系,一目了然:
此处输入图片的描述

使用面向对象接口时,正确的作图流程应该是:

  1. 创建 figure 实例;
  2. figure 上创建 axes
  3. axes 上添加基础类对象。

或者简化为:

  1. 创建 figure 对象和 axes 对象;
  2. 为每个容器类元素添加基础类元素。

再浓缩成指导思想就是:

  1. 先找对象;
  2. 再解决问题。

二、高效作图第一步:创建容器类对象

容器类对象有四类:Figure, Axes, Axis, Tick。这四个是由层级顺序的,一个Figure包含多个Axes,一个Axes包含多个Axis,一个Axis包含多个Tick

具体而言,Figure 就是整个画布,包含了所有坐标系和各种基础类对象;Axes 就是我们正常理解中的“图像”,每个 Axes 都有标题、横轴、纵轴等,AxesMatplotlib 作图逻辑中最最重要的对象;Axis 是坐标轴,用来限制图像范围、生成刻度和刻度标签,刻度的位置由 Locator 对象决定,刻度标签的格式由 Formatter 控制。

Figure和Axes是作图必备,Axistick 则是精细调整时才需要考虑的。因此高效作图第一步就是,先把 FigureAxes 创建出来。

创建图和坐标系的方法有两种,要么先画图再画坐标系,要么一起画,我个人倾向于一起画,为啥?因为快呗,符合我优雅的人设。

I.优雅地创建图和坐标系

  1. layout = (3, 2)
  2. # 坐标系的布局
  3. fig, axes = plt.subplots(*layout)
  4. # 添加图和坐标系

II. 坐标系索引的两种方式

1. 矩阵索引

  1. ax1 = axes[0][0]
  2. #第一个坐标系
  3. ax2 = axes[0][1]
  4. #第二个坐标系

2. 遍历

  1. for ax in axes.flat:
  2. pass

三、高效作图第二步:添加基础类对象

基础类对象就是图中所有的点、线、图例、标题这些。需要注意的是,基础类是寄生于容器类的。因此,添加基础类对象时,要先声明容器类对象,也就是说,画画时必须先说清楚在哪里画,这也是基于 Matlab 的接口和基于面向对象的接口最明显的区别。

各容器类可以添加的基础类总结如下:

I. 图

  1. fig.legend()
  2. # 图-图例

II. 坐标系

  1. ax.plot()
  2. # 坐标系-线
  3. ax.scatter()
  4. # 坐标系-点
  5. ax.grid()
  6. #坐标系-网格
  7. ax.legend()
  8. # 坐标系-图例
  9. ax.text()
  10. # 坐标系-文字
  11. ax.set_title('Title')
  12. # 坐标系-标题

III. 坐标轴

  1. ax.set_xlabel('xlabel')
  2. # 坐标系-坐标轴-标签

IV. 刻度

  1. ax.set_xticklabels(['one', 'two', 'three', 'four', 'five'])
  2. # 坐标系-坐标轴-刻度-标签

可以看到,大部分对象都是捆绑在 Axes 上的,这也验证了之前说的,AxesMatplotlib 作图的核心元素。

优雅地添加基础类对象

另外,作为一个优雅的人,如果需要设置很多属性值(property),写一堆 ax. 就太不优雅了,这时候可以使用 ax.set() 来统一设置,简化代码:

  1. props = {'title': 'Title',
  2. 'xlabel': 'xlabel',
  3. 'xticklabels': xticklabels_list}
  4. # 坐标系-标题
  5. # 坐标系-坐标轴-标签
  6. # 坐标系-坐标轴-刻度-标签
  7. ax.set(**props)

四、我是例子

光说不练假把式,接下来用个实例来说明一下。数据就不介绍了。

在导入包和数据后,通过如下一段简单的代码,就可以生成一个五脏俱全的图像:

I. 创建 figureaxes

  1. fig, ax = plt.subplots()
  2. # 添加图和坐标系

II. 添加基础类对象

  1. ax.plot(df.index, df['MC_Price'])
  2. # 坐标系-线
  3. ax.plot(df.index, df['DT_Price'])
  4. # 坐标系-线
  5. ax.plot(df.index, df['TT_Price'])
  6. # 坐标系-线
  7. ax.plot(df.index, df['WT_Price'])
  8. # 坐标系-线
  9. props = {'title': 'Title', #坐标系-标题
  10. 'xlabel': 'xlabel', # 坐标系-坐标轴-标签
  11. 'ylabel': 'ylabel'} # 坐标系-坐标轴-标签
  12. ax.set(**props)

美观性:先全局,再局部

美是很主观的,但也有一些统一的欣赏标准,比如在数据可视化领域十分有影响力的书《The visual display of quantitative information》中,作者提出了最大化 “data/ink ratio” 的原则,即,用最少的油墨表示最多的信息。说白了就是大道至简,就是奥卡姆剃刀;说黑了就是AIC准则,就是正则化。

网上的很多教程都是用很繁琐的语句来对图像进行美化,这样写出的代码又臭又长,而且把画图的代码和美化图像的代码混杂到了一起。

其实,几乎所有的配置都可以通过全局参数进行统一声明,因此,先对大局进行统一设置,再在细节上进行微调,这样写出的代码才更加清晰直观,画出的图像也很好看。

一、全局美化格式

美化格式,无非就是美化元素的属性值,包括字体、字号、子图边距、网格类型等。全局美化有两种方式,一是通过 plt.style.use() 使用官方预定义的样式,二是通过 mpl.rcParams 自定义样式。

官方预定义的样式有很多,用 plt.style.available 可以查看所有可用样式,时间紧迫时可以用这种方法。

我更喜欢用第二种方法,因为可以把自己想要的格式显示地声明出来,每个细节都是自己掌控的。用 mpl.rcParams.keys() 可以查看所有可以全局定义的属性,用 mpl.rcParams.update() 可以实现一行代码更新参数。

我摘录了几个常用的属性,一般情况下设置这些就够了:

  1. params = {
  2. "font.size": 12, # 全局字号
  3. "font.family": "STIXGeneral", # 全局字体
  4. "figure.subplot.wspace": 0.2, # 图-子图-宽度百分比
  5. "figure.subplot.hspace": 0.4, # 图-子图-高度百分比
  6. "axes.spines.right": False, # 坐标系-右侧线
  7. "axes.spines.top": False, # 坐标系-上侧线
  8. "axes.titlesize": 12, # 坐标系-标题-字号
  9. "axes.labelsize": 12, # 坐标系-标签-字号
  10. "legend.fontsize": 12, # 图例-字号
  11. "xtick.labelsize": 10, # 刻度-标签-字号
  12. "ytick.labelsize": 10, # 刻度-标签-字号
  13. "xtick.direction": "in", # 刻度-方向
  14. "ytick.direction": 'in' # 刻度-方向
  15. }

另外,也可以提前对画图时的参数进行预定义。比如我要画四条线,就可以统一定义如下:

  1. style_dict = {
  2. 'MC_Price':dict(linestyle=':', marker='o',markersize=6,color='#fdae61'),
  3. 'WT_Price':dict(linestyle='-',marker='*',markersize=6,color='#d7191c'),
  4. 'DT_Price':dict(linestyle='--',marker='s',markersize=6,color='#abdda4'),
  5. 'TT_Price':dict(linestyle='-.',marker='v',markersize=6,color='#2b83ba')
  6. }

这样一来,画图的时候,直接用如下代码就可以方便地完成作图,实现了作图代码和美化代码的分离。

  1. ax.plot(x,y,**style_dict[key])

我这里的配色方案是通过 http://colorbrewer2.org/ 这个网站生成的,当然,也可以用 Matplotlib 自带的配色方案或者其他包提供的方案。另外,关于全局参数的设置,高端玩家也可以自己写 matplotlibrc 文件,我就不再展开了,因为我也不会。

二、局部美化格式

在全局设置好格式以后,就可以肆无忌惮地画图了,而且一般不需要再进行调整,画出的图就可以很好看很好看了。

但有时候在画出图后,仍需对子图间距、坐标轴范围、图例位置、网格透明度等进行局部微调。这部分不需要过分操心,碰到问题随用随查就可以了,或者可以常备一份cheatsheet,也可以快捷地找到解决问题的方法。

有一点需要提醒一下,无论什么时候,都要记住,先找对象,再解决问题,这样才可以对画出的图像心中有数。比如下面的几个例子,都要基于 fig 或者 ax,而不是不分红橙黄绿地使用 plt

  1. fig.subplots_adjust(left = 0.09, bottom = 0.1, right = 0.99, top = 0.99, wspace = 0.1)
  2. # 调整子图的位置和间距
  3. ax.set_xlim(min_value, max_value)
  4. # 调整坐标轴范围
  5. ax.legend(loc = 'upper right')
  6. #调整图例位置
  7. ax.grid(linestyle = "--", alpha = 0.2)
  8. # 调整网格的线型和透明度

三、我也是例子

光练一遍假把式,接下来对我们之前做出的图进行美化。通过全局设置部分的代码以及如下局部设置的代码,就可以生成一幅比较优雅的图片了。

优雅地创建Figure和Axes

fig, ax = plt.subplots()

优雅地添加基础类对象

ax.plot(df.index, df['MC_Price'], **style_dict['MC_Price'])
ax.plot(df.index, df['DT_Price'], **style_dict['DT_Price'])
ax.plot(df.index, df['TT_Price'], **style_dict['TT_Price'])
ax.plot(df.index, df['WT_Price'], **style_dict['WT_Price'])
props = {'xlabel': 'xlabel', # 坐标轴-标签
         'ylabel': 'ylabel'}  # 坐标轴-标签
ax.set(**props)

优雅地局部美化格式

fig.legend(('MC','DT','TT','WT'), frameon = False,
            loc = 'upper center', ncol = 4, 
            handlelength = 4) 
# 图例
ax.fill_between(df.index, df['MC_up'], df['MC_down'],
                alpha = 0.15, linewidth = 0, color = '#fdae61') 
# 阴影
ax.grid(linestyle = "--", alpha = 0.2) 
# 网格线

交互性:无缝融合LaTex

使用Matplotlib作图有两个目的,要么是要插入到论文里的,要么是其他目的。这部分就介绍一下如何优雅地使用Matplotlib和LaTex来为论文作图。

一、确定图片的长和宽

为了确保作出的图可以无缝插入到LaTex 中,必须避免对Matplotlib生成的图像进行二次缩放,因为已经生成图像后再进行缩放,不仅会缩放长宽,而且字号也会跟着缩放,这是很麻求烦的。

因此,在Matplotlib中作图时就要考虑到最终生成的图的大小,写作时不加修改地直接导入就行了。

先在LaTex 文档中插入\showthe\textwidth命令来获得最终需要的图片的宽度:

\documentclass{article}
\begin{document}
    \showthe\textwidth
    \end{document}

编译结束后,在.log文件中就可以找到这样的字眼,这个443.86319pt就是最终导入时图片的宽度。

> 443.86319pt.
l.204     \showthe\textwidth

但是,还有两个问题没解决:

Matplotlib中图片的宽度是用inch做单位的,这里用的是pt;
图片的高度还没确定。
我参考了Embed-Publication-Matplotlib-Latex这篇文章给出的解决方案:先进行单位换算,再用黄金比例0.618来确定图片的高度。

核心代码如下,完整代码还需要在公众号后台回复一下,因为有点小长。

fig_width_pt = 443.86319pt
inches_per_pt = 1 / 72.27
golden_ratio = (5**.5 - 1) / 2

fig_width_in = fig_width_pt * inches_per_pt
fig_height_in = fig_width_in * golden_ratio

按格式导出图片

Matplotlib导出的图可以有很多格式,论文作图时,一定要导出矢量图,也就是以.svg或者.pdf为后缀的,这类图片放大时不会失真。一般而言,SVG格式用于Word,PDF格式用于LaTeX。

还需要注意的是,为了去掉Matplotlib作图时多余的空白部分,导出图片时要传入bbox_inches='tight'参数。导出图片的代码如下:

fig.savefig('example.pdf', format='pdf', bbox_inches='tight')
将导出的图片保存在LaTex的项目文件夹中,然后只要在LaTex中使用下面的命令,就可以优雅地插入图片了。

\begin{figure}
  \centering
  \includegraphics{example.pdf}
\end{figure}

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注