Matplotlib 一文上手
0. 前言
不知道有多少朋友跟我一样,matplotlib 的介绍看了一篇又一篇,但是每次看完了把手上的代码调试通过后,下次遇到了新的画图需求,捡起来发现又全部忘记了。
学习的时候总是觉得,怎么看都只是学了个皮毛,需要设置坐标轴就去查一下怎么设置坐标轴,想要加一个颜色标尺就去查一查怎么加,知识总是东一块西一块。倒不是说查到的资料很零散,而是即使查到的资料把该讲的都讲到了,到自己要用的时候,脑子里还是没有形成一个完整的知识脉络,不能做到 “我想这么画,既然 matplotlib 的设计是这样子,那么可以这么画” ,而只能是 “我想这么画,搜搜相近的效果,发现这在 matplotlib 里面要这么画” 。
更糟糕的是,查到的资料,这么画也行,那样画也可以, 完全看不出来章法,这般死记硬背完全没办法记住,以至于每次遇到的代码片段都得像秘宝一样揣在某个角落,下次要用的时候再翻出来,此时肯定要重新理解一遍,往往倒不如直接拷贝过来接上同结构的数据,然后求神拜佛地试着运行一下,边调试调试,说不定又要再去搜下一段秘宝。
最近腾了一点时间,认真学习了 matplotlib 的用法,发现实际上它的逻辑概念是非常清晰、简单的,当然也非常容易理解、一下就能掌握。到网上各种找快速入门、拷贝即用的代码,反倒是舍近求远了。我既惊讶于其如此容易理解,又惊讶于,可能是它太易用了,导致太多为了使用而用的教程如此泛滥,反倒埋没了它本来的精炼的面目;又因为,特别是现在这时代,一旦掌握了可视化数据的能力,对任何与数据沾边的工作都会带来非常大的提升。因此,我决定写下这篇总结分享,希望跟我有同样疑惑的朋友也能够解决这个问题,能够专注在如何分析、展示数据的思考上,而不是被工具绊住了手脚。
1. 基本概念和API结构
闲话说完,来看看应该怎么来理解 matplotlib,以及如何正确使用。
1.1 MATLAB ?
matplotlib 实际上有两套绘图接口,两者是等价的,但是接口表现形式不同。一种是类MATLAB的接口,一种是面向对象(Object-Oriented,简称OO)形式的接口。
第一种类MATLAB的接口,实际上就是matplotlib.pyplot,也就是常见的 “plt”。官方称它为“有状态的接口”,意思是绘图状态是全局共享的,接口中函数都会改变它,类似 如下代码 :
1 | import matplotlib.pyplot as plt |
这里plt.subplot()函数切换了plt.plot()所作用的区域,因此同样都是plt.plot(),但却能画出两个图来。
如果是熟悉 MATLAB 绘图的朋友,相信对这套接口当然能信手拈来,很自然地就能用上 matplotlib 来进行绘图操作。但如果是不熟悉 MATLAB 绘图的朋友,例如我,则很容易被这套接口绕得糊里糊涂的。
这套接口很不 “Python” ,十分有理由相信它只是用来兼容 MATLAB 绘图的语法,让较大基数的人群可以从 MATLAB 无缝地迁移到 matplotlib 上的一种做法。实际上并不鼓励在 Python 中使用它。
1.2 OO, the correct way
Matplotlib 的第二套绘图接口可就 “Python” 多了,官方直接称为 “OO 接口” ,这套接口中,整个绘图被分为了几个对象,图中的元素都是依附在这些对象上的一些子对象或者子属性,只要我们知道了整张图在概念上是怎么通过这些对象组织起来的,那么很容易就能知道应该如何实现我们想要的效果。对象如下:
1 | graph LR |
也就是说:
例如:
1 | import matplotlib.pyplot as plt |
既然是一对多,再来一个多Axes的例子:
1 | import matplotlib.pyplot as plt |
是的,就是这么简单。使用 matplotlib 常用到的属性,都是依附在 Figure 或者 Axes 中。通常只要下面三步,就能实现自己想要的效果:
- 找到想修改的属性叫什么
- 在 Figure 或者 Axes 中找到对应的 API
- 看看 API 怎么用
其中,针对第1步,常用的属性已经列在下面了:
针对第2步,直接到官网上查,已经足够快方便了,当然要上网搜搜代码片段也没问题:
- 其实大部分常用属性都在 Axes 里:matplotlib-axes
针对第3步,这才终于回到我们手上的“秘宝”——各类画图功能、样式的示例代码片段。当我们已知所画的图是由 Figure 和 Axes 所组成,并且常用属性都可以通过他们来配置时,再来读这些代码片段,我们就能跳过片段前后的繁琐或不相关内容,直接去读真正使得效果起作用的代码,究竟是对 Figure 还是 Axes 的什么属性,利用什么 API,起到了什么作用。这样既提高了对“秘宝”的利用效率,也加深了对 matplotlib 的认识,这样长久以往,我们就能对如何画图的理解越来越深入、经验越来越丰富,而不会反而越来越疏离。
怎么理解这个 Axes 呢?考虑到 “Axes” 是 “Axis” 的复数形式,有人建议将其理解为“一套坐标轴/坐标域”,虽然在官方文档里没有明确这么解释,但我觉得这么理解还是非常到位、准确的。
有时,我们查到的代码片段,是以 “类MATLAB”的接口给出的,不容易直接在“OO接口”中找到对应内容。不用着急,只需记住以下原则,就可以有迹可循:
- “类MATLAB”接口更高层,实际上是使用“OO接口”来实现并封装的,没有包含什么特别的、“OO接口”实现不了的内容;
- “类MATLAB”接口所谓的“有状态”,就是指它默默的帮我们在 Figure 和当前生效的 Axes[x] 上进行了操作而已;
- 通常
plt.xxx()的 API,都能在 Figure(即fig)或者 Axes(即ax)下找到同名的 API。试一试,或者搜一搜,很容易就能找到。
2. 举个例子
本文不是 matplotlib 哪个具体 API 的教程,只希望在指出整体性的概念,以便于理解。但为了说明地更充分些,还是需要举个具体的例子。
既然是例子,那就直接用上面的示例图吧,正好能覆盖这些常用的属性。成果如下:
首先,观察到是散点图,那么先来点数据,大约在 0.5 到 3.5 的范围内(x),生成 0.5 到 3.5 的值(y),简单估算下约 50 个样本数:
1 | import numpy as np |
然后套用上面例子先画个默认图:
1 | import matplotlib.pyplot as plt |
这里可以看到,因为要画散点图,所以原来例子中的 ax.plot() 已经换为了 ax.scatter()。想画其他类型怎么办?或者想知道有哪些类型可以选?查一下就知道了。
接下来,我们按如下顺序,一项项把其他元素加进来吧:
- title
- tick 和 tick label
- axis label
- grid
- legend
2.1 title
简单搜一下,就能看到如何设置title。我们这里想设置整图的 title,那自然是设置 fig:
1 | fig.suptitle('Anatomy of a Figure') |
实际上,官网的教程完整多了,还有示例代码可以下载。
2.2 tick 和 tick label
观察目标图,tick 设置的目标是:
- x 轴和 y 轴都是从 0 到 4;
- Major tick 间隔为 1;
- Minor tick 间隔为 0.25,这两个术语也是示例图告诉我们的
1 | import matplotlib.ticker as mticker |
首先,我们的需求是实现了,但是显示上显然有点问题:
- 副坐标和主坐标重叠了
- 而且主坐标的刻度也不够突出
我们后续会解决这些问题,先继续。
官网教程在此。
2.3 axis label
这个简单,前面都查到了:
1 | ax.set_xlabel('X axis label') |
2.4 grid
至此应该轻车熟路了,找到 Axis 对象的API,注意我们需要画虚线:
1 | ax.xaxis.grid(True, which='major', linestyle='--') |
2.4.x Axis
上述 4 节,实际上都属于 Axis 的一部分,比较完整、统一:的示例在此。
回到 2.2 的问题,要解决的话,我们可以给 minor ticks 的显示增加一个逻辑:如果跟 major ticks 重合的话就不画了。当然方法有很多,这里以示例目的为主,最重要是向大家介绍 matplotlib.ticker.FuncFormatter ,利用它,我们可以在 ticker 上基本做到随心所欲的格式定制:
1 | def minor_skip_format(val, pos): |
最好把图的大小调整一下,更方正一点,坐标也不用挤在一起:
1 | #fig, ax = plt.subplots() # 把一开始的实例化换一下 |
最后处理坐标刻度。默认估计是 1 左右,试验了一下,8 差不多:
1 | ax.tick_params(which='major', length=8) |
2.5 legend
要引入 legend,直接 ax.legend() 一句话就可以了,legend 的内容是由所画的图所决定的。这里还要额外加上要画的曲线:
1 | X = np.linspace(0.5, 3.5, 100) |
colors, line type and markers
上面 legend 的例子里,我们看到在 ax.plot() 参数里有比较特别的参数 'b-' 和 'r--',这是什么意思呢?这其实也是从 MATLAB 继承过来的,示例这里是指定了颜色和线条样式。这种参数在网上很多的示例里面都有,也不太容易记忆。为了避免频繁查阅,这里给出了备查表:
| colors 颜色 | |
|---|---|
| b | blue,蓝,默认 |
| g | green,绿 |
| r | red,红 |
| c | cyan,青 |
| m | magenta |
| y | yellow,黄 |
| k | black,黑 |
| w | white,白 |
颜色有很丰富的设置接口,可以参考这个示例,关于各类颜色名字,这里列得更全。
| line type 线条样式 | |
|---|---|
| - | 实线,默认 |
| – | 虚线 |
| -. | 横-点交替 |
| : | 点 |
线条样式也有很灵活的设置方法,参考这里。
除了线条以外,其实还能以其他符号来画我们的曲线,例如如果把最开始的散点图改成 ax.scatter(x, y, marker='s'),那么就可以得到:
| markers 标记 | |
|---|---|
| . | 圆点 |
| , | 一个像素点 |
| o | 圆 |
| v, ^, <, > | 三角,分别指向下、上、左、右四个方向 |
| 1, 2, 3, 4 | 三叉形,分别指向下、上、左、右四个方向 |
| s, p, 8 | 方形(square),五边形(pentagon),8边形 |
| *, +, x | 分别就是*,+和x |
| h, H | 竖向6边形,横向6边形 |
| d, D | 瘦型钻石和钻石 |
| |, _ | 竖线,横线 |
标记也有很多类型,这里列得更全。
总结
上述是我自己一步步模仿着示例图搭起来的,其实自始至终就是找到绘图的元素对象,而绝大多数情况都是 Axes,然后用 API 实现自己想要的效果而已,常见的属性也确实都是前面示例图给出的那些,这个示例只是实践了一下。
最后,在搭建示例的过程中,还干脆直接发现了官方的示例代码:Anatomy of a figure,可以看到,legend 示例中的曲线,我用了官方的示例然后做了点简化。主要区别:
- 官方示例包含 Text Annotate 文字标注的方法,值得一看;
- 官方示例实现了很多圆圈,利用的是
ax.add_artists()接口,属于比较高级的手法了。关于 Artist,可以继续看下文的介绍。
3. 官网资料
官网资料非常详尽、非常细致,而且循序渐进、分类合理、导航充分,我获益良多,觉得大部分情况都没有必要再去找别的资料了。
教程,即“应如何使用”,包含“介绍”、“中级”、“高级”三大内容,还有“颜色”、“文字”两大专题,以及“额外工具集”一节。
- “介绍”主要就是讲本文介绍的基本概念,以及基础使用。其中这一篇很值得读,对常见样式的使用给出了很好的例子。
- “中级”进一步介绍了几个概念,有常用到的 legend,还有各类 Layout(也就是多个 Axes 在 Figure 中的排布方法),还有 “Artist” 这个概念,很值得想了解 matplotlib 绘制逻辑的朋友一看。
- 颜色是画图中很重要的一个维度,相当于在数据坐标轴外平添了一个正交的坐标轴,而且人对颜色也更为敏感,对读者理解要展示的内容尤其有帮助。有需求的话可以参看颜色专题 。
- 灵活的文本设置也能给图提供精准的注解,也有对应的专题可以参看,其中还包括对 TeX 公式的支持。
教程入口:https://matplotlib.org/3.2.2/tutorials/index.html
整体文档入口:https://matplotlib.org/3.2.2/contents.html
3.1 Artist
Matplotlib 实际的绘图概括为三个抽象:
- Canvas,画布,实际绘画的区域
- Renderer,应该怎么在画布 Canvas 上绘画
- Artist,应该怎么利用 Renderer 在画布 Canvas 上画
其中 Canvas 和 Renderer都属于 Backend,什么是 Backend 呢?Matplotlib 支持在很多平台进行绘图输出,例如在 python REPL 里弹出绘图框,在 Jupyter notebook 里嵌入绘图结果,或者将绘图写成不同格式的静态图片文件,它还支持在 GUI 应用里直接嵌入绘图,显然,在各类绘图应用场景下,matplotlib 实际依赖的绘图接口肯定都是不一样的,在不同的操作系统下肯定也不同。这些都属于 backend。Canvas,就是实际画的图数据的抽象,Renderer,就是绘制动作的抽象,两者类似于画布和画笔,给实际要画的内容提供了工具。谁知道实际要画什么内容?“艺术家” Artist 咯。
Artist 就是实际的线条、矩形、圆等等有内在逻辑的绘图元素,以及这些元素的容器,也就是常见的自嵌套的结构。我们作为用户,只是利用预设好的这些绘图元素,拼接设置成我们想要的样式,从而获得想要的绘图而已。
1 | graph TD |
我们画图一般的流程是:
- 首先通过
plt.figure(),先创建出 Figure,并将其绑定到 Canvas 上; - 然后通过
fig.add_subplot(),在 Figure 中添加 Axes; - 由此可以看到,上述两步通常也有其他等价的步骤,可以是一步到位的
plt.subplots(),也可以是另外的API如fig.add_axes()等。实际用哪个 API 可能没那么重要,关键是知道它们背后做了什么; - 接下来利用 Axes 提供的各类方法,添加一个个的 Artist,为我们的图添加一系列绘图元素。
知道这一层抽象可能不见得在我们平时画图过程中能产生什么作用,但是在遇到问题时还是有一点帮助的,起码查资料时能有一个模糊的方向,指不定就能一下子节省好几天没头苍蝇乱撞的时间呢。