0%

matplotlib-intro

Matplotlib 一文上手

0. 前言

不知道有多少朋友跟我一样,matplotlib 的介绍看了一篇又一篇,但是每次看完了把手上的代码调试通过后,下次遇到了新的画图需求,捡起来发现又全部忘记了。

学习的时候总是觉得,怎么看都只是学了个皮毛,需要设置坐标轴就去查一下怎么设置坐标轴,想要加一个颜色标尺就去查一查怎么加,知识总是东一块西一块。倒不是说查到的资料很零散,而是即使查到的资料把该讲的都讲到了,到自己要用的时候,脑子里还是没有形成一个完整的知识脉络,不能做到 “我想这么画,既然 matplotlib 的设计是这样子,那么可以这么画” ,而只能是 “我想这么画,搜搜相近的效果,发现这在 matplotlib 里面要这么画”

更糟糕的是,查到的资料,这么画也行,那样画也可以, 完全看不出来章法,这般死记硬背完全没办法记住,以至于每次遇到的代码片段都得像秘宝一样揣在某个角落,下次要用的时候再翻出来,此时肯定要重新理解一遍,往往倒不如直接拷贝过来接上同结构的数据,然后求神拜佛地试着运行一下,边调试调试,说不定又要再去搜下一段秘宝。

最近腾了一点时间,认真学习了 matplotlib 的用法,发现实际上它的逻辑概念是非常清晰、简单的,当然也非常容易理解、一下就能掌握。到网上各种找快速入门、拷贝即用的代码,反倒是舍近求远了。我既惊讶于其如此容易理解,又惊讶于,可能是它太易用了,导致太多为了使用而用的教程如此泛滥,反倒埋没了它本来的精炼的面目;又因为,特别是现在这时代,一旦掌握了可视化数据的能力,对任何与数据沾边的工作都会带来非常大的提升。因此,我决定写下这篇总结分享,希望跟我有同样疑惑的朋友也能够解决这个问题,能够专注在如何分析、展示数据的思考上,而不是被工具绊住了手脚。

1. 基本概念和API结构

闲话说完,来看看应该怎么来理解 matplotlib,以及如何正确使用。

1.1 MATLAB ?

matplotlib 实际上有两套绘图接口,两者是等价的,但是接口表现形式不同。一种是类MATLAB的接口,一种是面向对象(Object-Oriented,简称OO)形式的接口。

第一种类MATLAB的接口,实际上就是matplotlib.pyplot,也就是常见的 “plt”。官方称它为“有状态的接口”,意思是绘图状态是全局共享的,接口中函数都会改变它,类似 如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import matplotlib.pyplot as plt

def f(t):
return np.exp(-t) * np.cos(2*np.pi*t)

t1 = np.arange(0.0, 5.0, 0.1)
t2 = np.arange(0.0, 5.0, 0.02)

plt.figure()
plt.subplot(211)
plt.plot(t1, f(t1), 'bo', t2, f(t2), 'k')

plt.subplot(212)
plt.plot(t2, np.cos(2*np.pi*t2), 'r--')
plt.show()

这里plt.subplot()函数切换了plt.plot()所作用的区域,因此同样都是plt.plot(),但却能画出两个图来。

如果是熟悉 MATLAB 绘图的朋友,相信对这套接口当然能信手拈来,很自然地就能用上 matplotlib 来进行绘图操作。但如果是不熟悉 MATLAB 绘图的朋友,例如我,则很容易被这套接口绕得糊里糊涂的。

这套接口很不 “Python” ,十分有理由相信它只是用来兼容 MATLAB 绘图的语法,让较大基数的人群可以从 MATLAB 无缝地迁移到 matplotlib 上的一种做法。实际上并不鼓励在 Python 中使用它。

1.2 OO, the correct way

Matplotlib 的第二套绘图接口可就 “Python” 多了,官方直接称为 “OO 接口” ,这套接口中,整个绘图被分为了几个对象,图中的元素都是依附在这些对象上的一些子对象或者子属性,只要我们知道了整张图在概念上是怎么通过这些对象组织起来的,那么很容易就能知道应该如何实现我们想要的效果。对象如下:

1
2
graph LR
Figure(Figure, 全图, 缩写 fig) -->|包含一个或多个| Axes(Axes, 子图, 缩写 ax)

也就是说:

例如

1
2
3
4
import matplotlib.pyplot as plt

fig, ax = plt.subplots() # fig 就是 Figure,ax 就是 Axes
ax.plot([1, 2, 3, 4], [1, 4, 2, 3]) # 画一些数据,前面是 x 数据,后面是 y 数据

既然是一对多,再来一个多Axes的例子:

1
2
3
4
5
import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(1, 2) # nrows=1, ncols=2
ax1.plot([1, 2, 3, 4], [1, 4, 2, 3])
ax2.plot([1, 2, 3, 4], [2, 1, 4, 3])

是的,就是这么简单。使用 matplotlib 常用到的属性,都是依附在 Figure 或者 Axes 中。通常只要下面三步,就能实现自己想要的效果:

  1. 找到想修改的属性叫什么
  2. 在 Figure 或者 Axes 中找到对应的 API
  3. 看看 API 怎么用

其中,针对第1步,常用的属性已经列在下面了:

针对第2步,直接到官网上查,已经足够快方便了,当然要上网搜搜代码片段也没问题:

针对第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
2
3
4
import numpy as np
np.random.seed(20200802)
x = np.random.rand(50) * 3 + 0.5
y = np.random.rand(50) * 3 + 0.5

然后套用上面例子先画个默认图:

1
2
3
4
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.scatter(x, y)

这里可以看到,因为要画散点图,所以原来例子中的 ax.plot() 已经换为了 ax.scatter()。想画其他类型怎么办?或者想知道有哪些类型可以选?查一下就知道了

接下来,我们按如下顺序,一项项把其他元素加进来吧:

  1. title
  2. tick 和 tick label
  3. axis label
  4. grid
  5. 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
2
3
4
5
6
7
8
9
10
11
12
13
import matplotlib.ticker as mticker

ax.set_xlim([0, 4]) # 设置x轴范围
ax.set_ylim([0, 4]) # y轴同理

ax.xaxis.set_major_locator(mticker.MultipleLocator(1)) # 设置x轴主坐标间隔为1
ax.xaxis.set_major_formatter(mticker.FormatStrFormatter('%d')) # 设置x轴主坐标显示格式
ax.xaxis.set_minor_locator(mticker.MultipleLocator(0.25)) # 设置x轴副坐标间隔为0.25
ax.xaxis.set_minor_formatter(mticker.FormatStrFormatter('%.2f')) # 设置x轴副坐标显示格式,留2位小数
ax.yaxis.set_major_locator(mticker.MultipleLocator(1)) # y轴同理
ax.yaxis.set_major_formatter(mticker.FormatStrFormatter('%d'))
ax.yaxis.set_minor_locator(mticker.MultipleLocator(0.25))
ax.yaxis.set_minor_formatter(mticker.FormatStrFormatter('%.2f'))

首先,我们的需求是实现了,但是显示上显然有点问题:

  • 副坐标和主坐标重叠了
  • 而且主坐标的刻度也不够突出

我们后续会解决这些问题,先继续。

官网教程在此

2.3 axis label

这个简单,前面都查到了:

1
2
ax.set_xlabel('X axis label')
ax.set_ylabel('Y axis label')

2.4 grid

至此应该轻车熟路了,找到 Axis 对象的API,注意我们需要画虚线:

1
2
ax.xaxis.grid(True, which='major', linestyle='--')
ax.yaxis.grid(True, which='major', linestyle='--')

2.4.x Axis

上述 4 节,实际上都属于 Axis 的一部分,比较完整、统一:的示例在此

回到 2.2 的问题,要解决的话,我们可以给 minor ticks 的显示增加一个逻辑:如果跟 major ticks 重合的话就不画了。当然方法有很多,这里以示例目的为主,最重要是向大家介绍 matplotlib.ticker.FuncFormatter ,利用它,我们可以在 ticker 上基本做到随心所欲的格式定制:

1
2
3
4
5
6
7
8
def minor_skip_format(val, pos):
if val % 1 == 0:
return ''
else:
return '%.2f' % val

ax.xaxis.set_minor_formatter(mticker.FuncFormatter(minor_skip_format))
ax.yaxis.set_minor_formatter(mticker.FuncFormatter(minor_skip_format))

最好把图的大小调整一下,更方正一点,坐标也不用挤在一起:

1
2
3
#fig, ax = plt.subplots()					# 把一开始的实例化换一下
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(1, 1, 1) # nrows=1, ncols=1, index=1(1x1,取第一个)

最后处理坐标刻度。默认估计是 1 左右,试验了一下,8 差不多:

1
ax.tick_params(which='major', length=8)

2.5 legend

要引入 legend,直接 ax.legend() 一句话就可以了,legend 的内容是由所画的图所决定的。这里还要额外加上要画的曲线:

1
2
3
4
5
6
X = np.linspace(0.5, 3.5, 100)
Y1 = 3+np.cos(X)
Y2 = 1+np.cos(1+X/0.75)/2
ax.plot(X, Y1, 'b-', label="Blue signal")
ax.plot(X, Y2, 'r--', label="Red signal")
ax.legend()

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
2
3
4
graph TD
Artist --> Primitives元素
Artist --> Containers容器
Containers容器 .-> Artist

我们画图一般的流程是:

  1. 首先通过 plt.figure() ,先创建出 Figure,并将其绑定到 Canvas 上;
  2. 然后通过 fig.add_subplot(),在 Figure 中添加 Axes;
  3. 由此可以看到,上述两步通常也有其他等价的步骤,可以是一步到位的 plt.subplots(),也可以是另外的API如 fig.add_axes() 等。实际用哪个 API 可能没那么重要,关键是知道它们背后做了什么;
  4. 接下来利用 Axes 提供的各类方法,添加一个个的 Artist,为我们的图添加一系列绘图元素。

知道这一层抽象可能不见得在我们平时画图过程中能产生什么作用,但是在遇到问题时还是有一点帮助的,起码查资料时能有一个模糊的方向,指不定就能一下子节省好几天没头苍蝇乱撞的时间呢。