@1007477689
2020-04-14T16:40:47.000000Z
字数 6421
阅读 745
转载
Python
本篇文章转自“可乐学人”微信公众号,原文地址在下贴出
https://mp.weixin.qq.com/s/2ZLcqqpDLkPDkg5suESYyw
Matplotlib 是 Python 中最基本也最重要的可视化工具,可以画出拥有出版质量(Publication-Quality)的图表。如今 Matplotlib 已经衍生出了很多高层库,但如果需要对图表进行更加精细的个性化设置,就必须深入学习 Matplotlib,官网的教程和案例就是最好的学习资源。
高效的前提是充分理解。这部分首先简单介绍一下 Matplotlib 的两种接口,在理解 Matplotlib 逻辑的基础上,实现高效作图就是顺理成章的事了。
Matplotlib 有两种接口:基于 Matlab 的和基于面向对象的。基于 Matlab 的是 pyplot 提供的,比较简单,但容易混乱;基于面向对象的方法结构清晰,是 Matplotlib 的精髓。
网上好多资料都是把两种接口混合使用的,这对理解 Maplotlib 的作图逻辑很不友好。Python 中,万物皆对象,为了高效作图,我们首先需要做的就是学习使用面向对象的接口。
Matplotlib 的所有对象都叫“Artist”,Artist可以分为两大类别:“容器类Containers”和“基础类Primitives”。
简单来说,Figure 是画布,Axes 是画布上一个个区域,所有线(line)、点(marker)、文字(text)等基础类元素都是寄生在容器类元素上的。官网也贴心地给出了这些对象的关系,一目了然:
使用面向对象接口时,正确的作图流程应该是:
或者简化为:
再浓缩成指导思想就是:
容器类对象有四类:Figure, Axes, Axis, Tick。这四个是由层级顺序的,一个Figure包含多个Axes,一个Axes包含多个Axis,一个Axis包含多个Tick。
具体而言,Figure 就是整个画布,包含了所有坐标系和各种基础类对象;Axes 就是我们正常理解中的“图像”,每个 Axes 都有标题、横轴、纵轴等,Axes 是 Matplotlib 作图逻辑中最最重要的对象;Axis 是坐标轴,用来限制图像范围、生成刻度和刻度标签,刻度的位置由 Locator 对象决定,刻度标签的格式由 Formatter 控制。
Figure和Axes是作图必备,Axis 和 tick 则是精细调整时才需要考虑的。因此高效作图第一步就是,先把 Figure 和 Axes 创建出来。
创建图和坐标系的方法有两种,要么先画图再画坐标系,要么一起画,我个人倾向于一起画,为啥?因为快呗,符合我优雅的人设。
layout = (3, 2)
# 坐标系的布局
fig, axes = plt.subplots(*layout)
# 添加图和坐标系
ax1 = axes[0][0]
#第一个坐标系
ax2 = axes[0][1]
#第二个坐标系
for ax in axes.flat:
pass
基础类对象就是图中所有的点、线、图例、标题这些。需要注意的是,基础类是寄生于容器类的。因此,添加基础类对象时,要先声明容器类对象,也就是说,画画时必须先说清楚在哪里画,这也是基于 Matlab 的接口和基于面向对象的接口最明显的区别。
各容器类可以添加的基础类总结如下:
fig.legend()
# 图-图例
ax.plot()
# 坐标系-线
ax.scatter()
# 坐标系-点
ax.grid()
#坐标系-网格
ax.legend()
# 坐标系-图例
ax.text()
# 坐标系-文字
ax.set_title('Title')
# 坐标系-标题
ax.set_xlabel('xlabel')
# 坐标系-坐标轴-标签
ax.set_xticklabels(['one', 'two', 'three', 'four', 'five'])
# 坐标系-坐标轴-刻度-标签
可以看到,大部分对象都是捆绑在 Axes 上的,这也验证了之前说的,Axes 是 Matplotlib 作图的核心元素。
另外,作为一个优雅的人,如果需要设置很多属性值(property),写一堆 ax.
就太不优雅了,这时候可以使用 ax.set()
来统一设置,简化代码:
props = {'title': 'Title',
'xlabel': 'xlabel',
'xticklabels': xticklabels_list}
# 坐标系-标题
# 坐标系-坐标轴-标签
# 坐标系-坐标轴-刻度-标签
ax.set(**props)
光说不练假把式,接下来用个实例来说明一下。数据就不介绍了。
在导入包和数据后,通过如下一段简单的代码,就可以生成一个五脏俱全的图像:
figure
和 axes
fig, ax = plt.subplots()
# 添加图和坐标系
ax.plot(df.index, df['MC_Price'])
# 坐标系-线
ax.plot(df.index, df['DT_Price'])
# 坐标系-线
ax.plot(df.index, df['TT_Price'])
# 坐标系-线
ax.plot(df.index, df['WT_Price'])
# 坐标系-线
props = {'title': 'Title', #坐标系-标题
'xlabel': 'xlabel', # 坐标系-坐标轴-标签
'ylabel': 'ylabel'} # 坐标系-坐标轴-标签
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()
可以实现一行代码更新参数。
我摘录了几个常用的属性,一般情况下设置这些就够了:
params = {
"font.size": 12, # 全局字号
"font.family": "STIXGeneral", # 全局字体
"figure.subplot.wspace": 0.2, # 图-子图-宽度百分比
"figure.subplot.hspace": 0.4, # 图-子图-高度百分比
"axes.spines.right": False, # 坐标系-右侧线
"axes.spines.top": False, # 坐标系-上侧线
"axes.titlesize": 12, # 坐标系-标题-字号
"axes.labelsize": 12, # 坐标系-标签-字号
"legend.fontsize": 12, # 图例-字号
"xtick.labelsize": 10, # 刻度-标签-字号
"ytick.labelsize": 10, # 刻度-标签-字号
"xtick.direction": "in", # 刻度-方向
"ytick.direction": 'in' # 刻度-方向
}
另外,也可以提前对画图时的参数进行预定义。比如我要画四条线,就可以统一定义如下:
style_dict = {
'MC_Price':dict(linestyle=':', marker='o',markersize=6,color='#fdae61'),
'WT_Price':dict(linestyle='-',marker='*',markersize=6,color='#d7191c'),
'DT_Price':dict(linestyle='--',marker='s',markersize=6,color='#abdda4'),
'TT_Price':dict(linestyle='-.',marker='v',markersize=6,color='#2b83ba')
}
这样一来,画图的时候,直接用如下代码就可以方便地完成作图,实现了作图代码和美化代码的分离。
ax.plot(x,y,**style_dict[key])
我这里的配色方案是通过 http://colorbrewer2.org/ 这个网站生成的,当然,也可以用 Matplotlib 自带的配色方案或者其他包提供的方案。另外,关于全局参数的设置,高端玩家也可以自己写 matplotlibrc
文件,我就不再展开了,因为我也不会。
在全局设置好格式以后,就可以肆无忌惮地画图了,而且一般不需要再进行调整,画出的图就可以很好看很好看了。
但有时候在画出图后,仍需对子图间距、坐标轴范围、图例位置、网格透明度等进行局部微调。这部分不需要过分操心,碰到问题随用随查就可以了,或者可以常备一份cheatsheet,也可以快捷地找到解决问题的方法。
有一点需要提醒一下,无论什么时候,都要记住,先找对象,再解决问题,这样才可以对画出的图像心中有数。比如下面的几个例子,都要基于 fig
或者 ax
,而不是不分红橙黄绿地使用 plt
。
fig.subplots_adjust(left = 0.09, bottom = 0.1, right = 0.99, top = 0.99, wspace = 0.1)
# 调整子图的位置和间距
ax.set_xlim(min_value, max_value)
# 调整坐标轴范围
ax.legend(loc = 'upper right')
#调整图例位置
ax.grid(linestyle = "--", alpha = 0.2)
# 调整网格的线型和透明度
光练一遍假把式,接下来对我们之前做出的图进行美化。通过全局设置部分的代码以及如下局部设置的代码,就可以生成一幅比较优雅的图片了。
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)
# 网格线
使用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中使用下面的命令,就可以优雅地插入图片了。