Notes on Matplotlib

0. 摘要

本文重点不在于具体画图操作,更在意 matplotlib 的基本概念,或者说画图的组件结构。在系统地学习了【莫烦Python】Matplotlib Python 画图教程 后,进行一些思考和总结,整个 matplotlib 包就清楚了,希望以后再画图的时候不再挠头。

1. 基本概念

最基本的就是我们看到的,一张图中可以含有多个坐标系,可以想象为:在一张白纸上,用笔画坐标系。所以有两个最基本的概念:figure(白纸),axes(坐标系)。

1.1 figure

Notes on Matplotlib_第1张图片
一个 figure 就是输出图的窗口(框),相当于一个容器,其中包含图象。如上图,一个框包含了 5 张图象。有点像 word 中的画布。如果不声明 plt.figure(),系统会有一个默认的 figure,所以你直接 plt.plot(x, y) 时,也可以画出图象。

1.2 axes

直译的话,axes 是坐标轴的意思,但是目前来看,它是一个坐标系,更确切地说,它是一张图象。上图中的 5 个图象就是 5 个 axes 。ps:就当是在画布上画了 5 个坐标系,坐标系中可以画线。我们画出来的图象实例,如散点或者曲线(上图中的蓝色直线),其实是由 axes 对象操作的,画在当前的 axes 对象对应的坐标系中。

刚开始对程序中“时而 plt.plot(x, y) 时而 ax.plot(x, y)”感到迷惑,图象到底是怎么画出来的?

由 figure 和 axes 的基本概念以及两者之间的关系来看,图象是由 axes 画出来的。那为什么 plt.plot(x, y) 也可以?

还是“默认”的问题,既然“不声明 plt.figure(),系统会有一个默认的 figure”,那么 figure 中自然也可以有默认的 axes。调用函数 axes = figure.get_axes() 就可以拿到当前 figure 中所有 axes 对象的列表。

from matplotlib import pyplot as plt
import numpy as np

x = np.linspace(-1, 1, 50)
y = 2 * x + 1

figure = plt.figure()
plt.plot(x, y)
plt.show()

axess = figure.get_axes()
print(len(axess))  # 1
print(axess[0])  # AxesSubplot(0.125,0.11;0.775x0.77)

可以看到,即使没有明确地声明 axes 坐标系,figure 中依然存在一个 AxesSubplot。所以我猜测 plt.plot(x, y) 其实是 figure.get_axes()[0].plot(x, y),即:自动调用默认的 axes 进行画图的。

但进一步的试验打脸了,如果把 plt.plot(x, y) 一句注释掉,axess 就是空的,也就是说,不画图像,就没有 axes 哦!(也不会出现框框,啥反应没有)。这至少说明 plt.plot(x, y) 画图的时候发现没有 axes,暗地里执行了 plt.axes(),创建了一个 axes。

更常看到的是 ax = plt.gca() 获取当前 axes,# gca = ‘get current axes’1,如果没有,也会自动创建一个。所以 plt.plot(x, y) 也可能是 plt.gca().plot(x, y)。反正是偷偷建了一个 axes。

2. 组件结构

明白了基本的两个概念之后,我们看一看 matplotlib 包是如何组织这些组件的,画出来的形形色色的图象是怎样的结构。

2.1 plt.xxx() vs. ax.set_xxx() && 设置坐标

由以上对 plt.plot(x, y)ax.plot(x, y) 的分析可知,两者联系紧密。再由下一节的坐标轴设置可知,plt.xxx() 对坐标图象的设置很可能也是由类似 plt.gca().plot(x, y)plt.gca().set_xxx() 操作完成的。

具体看下面两段代码,结果是一样的:

from matplotlib import pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 50)
y1 = 2 * x + 1
y2 = x ** 2

plt.figure()

plt.plot(x, y2)
plt.plot(x, y1, color='red', linewidth=1.0, linestyle='--')
# 标题
plt.title('title')
# 坐标轴范围
plt.xlim([-1, 2])
plt.ylim([-2, 3])
# 坐标轴标签
plt.xlabel('I am X')
plt.ylabel('I am Y')
# ticks 是坐标轴的数字记号
new_ticks = np.linspace(-1, 2, 5)
print(new_ticks)
plt.xticks(new_ticks)  # 设置 xticks,没有提供符号,就直接显示当前位置数值
plt.yticks(  # set yticks
	[-1, 0, 1, 2, 3],  # 位置,下面提供符号,显示符号
	[r'$v\ b$', r'$b\ \alpha$', r'$m$', r'$g$', r'$vg$']  # 显示的符号,tex 公式
)

plt.show()
from matplotlib import pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 50)
y1 = 2 * x + 1
y2 = x ** 2

plt.figure()
ax = plt.gca()

ax.plot(x, y2)
ax.plot(x, y1, color='red', linewidth=1.0, linestyle='--')
# 标题
ax.set_title('title')
# 坐标轴范围
ax.set_xlim([-1, 2])
ax.set_ylim([-2, 3])
# 坐标轴标签
ax.set_xlabel('I am X')
ax.set_ylabel('I am Y')
# ticks 是坐标轴的数字记号
new_ticks = np.linspace(-1, 2, 5)
print(new_ticks)
ax.set_xticks(new_ticks)  # 设置 xticks,没有提供符号,就直接显示当前位置数值
ax.set_yticks([-1, 0, 1, 2, 3])
ax.set_yticklabels([r'$v\ b$', r'$b\ \alpha$', r'$m$', r'$g$', r'$vg$'])
# 警告说新版本不支持放在一起了,就用set_yticklabels
plt.show()

2.2 继续设置坐标轴(spines)

ax.spines 就是坐标轴的线(ax 对象的属性),默认是 x 轴和 y 轴各两条,默认 left 和 bottom 带刻度(主轴),right 和 top 不带刻度,当然,这可以改变。可以设置这些线的位置、颜色和可见性等各种属性。

ax = plt.gca()
# spine: 脊梁,就是上下左右四周的四个线
ax.spines['right'].set_color('none')  # 颜色 none,消失
ax.spines['top'].set_color('none')  # 这个的作用主要还是设置颜色吧
ax.spines['right'].set_visible(False)  # 这样意思更明确吧
ax.spines['top'].set_visible(False)
# 设置 x, y 轴是 spine 的哪根线
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
# 再设置 spines 的位置,就设置了原点位置
ax.spines['bottom'].set_position(['data', 0])  # data, 定位方式为:数据值
ax.spines['left'].set_position(['data', 0])  # 还有 axes, 定位方式:坐标轴比例

这样一来,xticklabels 的概念也就清晰了:

for label in ax.get_xticklabels() + ax.get_yticklabels():  # 再看 ax.set_yticklabels() 是多么自然
	label.set_fontsize(12)  # 设置 ticks 的字体
	label.set_bbox(dict(facecolor='white', edgecolor='None', alpha=0.3))  # ticks 的显示块

2.3 annotate, text, legend,… 成了和 plot 和 scatter 一样的概念

我们常看到 plt.plot(x, y) 以及 plt.scatter(x, y),知道它们是画线和散点。经过上面对 axes 的理解,我们又知道了画线和散点其实就是在 axes 坐标系中根据坐标点 (x, y) 画图,由 axes 对象操作。但其他像 annotate, text, legend 的东西也是经常用到的,它们是什么?

# annotate #############  plt.annotate 也一样
ax.annotate(  # 这么说,annotate 是和 scatter、plot 一样的概念
	text=r'$2x+1=%s$' % y0,  # 注释字符串
	xy=(x0, y0),  # 注释点(xycoords='data')
	xycoords='data',  # 以数据点为标准找位置
	xytext=(+30, -30),  # 这个就是下一行的 offset 值,横坐标+30, 纵坐标-30
	textcoords='offset points',  # 注释的位置,相对点进行 offset
	fontsize=16,
	arrowprops=dict(  # 箭头属性
		arrowstyle='->',  # 简头样式
		connectionstyle='arc3, rad=.2'  # 弧度之类的
	)
)

# text #############  plt.text 也一样
ax.text(
	x=-1.0, y=1.5,
	s=r'$This\ is\ some\ text.\ \mu\ \sigma_i\ \alpha_t$',
	fontdict=dict(size=12, color='red')
)

# handle:就是线的句柄,plot 的返回值,不知道为啥要带','
line1, = plt.plot(x, y2, label='up')  # 先设置 label
line2, = plt.plot(x, y1, color='red', linewidth=1.0, linestyle='--', label='down')
# plt.legend()  # 简单的默认图例
plt.gca().legend(  # 这样一看,legend 也和 plot 是一样的概念
	handles=[line1, ],  # handles 为空,则自动默认 plt.legend()
	labels=['aaa'],
	loc='best'
)

结构一下清晰了不少,原来 annotate, text, legend 这些奇奇怪怪的东西,就plot 一条线一样,都是往 figure 里的 axes 上敷东西,顺便带上一些属性呈现不同个性。就连直方图上标的数值都是用 ax.text() 敷上去的。

2.4 3D 图不过是换了 axes

import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits import mplot3d

fig = plt.figure()
ax = mplot3d.Axes3D(fig)  # 创建 3d 的 axes,并放入 figure

x = np.arange(-6, 6, 0.2)
y = np.arange(-6, 6, 0.2)
x, y = np.meshgrid(x, y)
r = np.sqrt(x ** 2 + y ** 2)
z = np.sin(r * np.pi / 0.8)

ax.plot_surface(  # 这和 2d 的 ax.xxx() 一样的概念,只不过换成了 3d 坐标
	x, y, z,
	rstride=1, cstride=1,  # 颜色变化跨度
	cmap=plt.get_cmap('rainbow')
)
ax.contourf(  # 这和 2d 的 ax.xxx() 一样的概念,只不过换成了 3d 坐标
	x, y, z,
	zdir='z',  # 沿着 z 轴投影
	offset=-2,  # 投影位置
	cmap='rainbow'
)

ax.set_zlim3d(-5, 5)  # 就连设置属性都是一样的概念

plt.show()

看,还是 axes 调用 ax.xxx() 往坐标系敷东西。

2.5 多子图(多 axes)

开头就说过“在一张白纸上画多个坐标系”,那多子图就是一个 figure 里放多个 axes 对象咯!有多种方法画多子图,包括:plt.subplot2grid, gridspec.GridSpec, plt.subplot(s) 以及 figure.add_axes 等。这里重点不在于画多子图的方法,而在于理解多子图的结构不同的方法仅仅是 axes 的排列方式不同,本质是一样的。

2.5.1 AxesSubplot & Axes

老是说 axes 是一个坐标系,那么它在代码编程中到底是个啥?这里先给出答案:在 figure.add_axes 方法中,axes 是 Axes,其他方法中是 AxesSubplot,甚至是默认的 axes 也是 AxesSubplotAxesSubplot & Axes 有什么区别吗?figure.add_axes 这么特别,有什么特殊用途吗?

2.5.1.1 AxesSubplot

对于 AxesSubplot 以及其属性,就不多说了,这里也不打算深入探究,钻得太深容易陷进去。反正就是一个坐标系,可以往里画线画点。print(ax) 可以得到类似 AxesSubplot(0.125,0.53;0.352273x0.35) 的输出,数字依次是[left, bottom; width*height],不同多子图方法可能就是通过调整子图的这些参数来排版子图的。

2.5.1.2 Axes

对于 Axesprint(ax) 也可以得到类似 Axes(0.2,0.6;0.25x0.25) 的输出,参数的意义是一样的。

2.5.2 多子图示例

感觉 gridspec.GridSpec 最方便,可以通过切片索引的方式像表格一样方便地布局子图,下面是画出开头图片的代码:

plt.figure()
gs = gridspec.GridSpec(3, 3)

ax1 = plt.subplot(gs[0, :])  # 切片索引方式确定行列
ax1.plot(
	[1, 2],
	[1, 2]
)
ax1.set_title('ax1_title')
print(ax1)  # AxesSubplot(0.125,0.653529;0.775x0.226471)

plt.subplot(gs[1, :2])
plt.subplot(gs[1:, 2])
plt.subplot(gs[2, 0])
plt.subplot(gs[2, 1])

plt.show()

可以看到子图1是 AxesSubplot(0.125,0.653529;0.775x0.226471).

2.5.3 figure.add_axes 多图实现图中图

2.5.1 中问道“figure.add_axes 这么特别,有什么特殊用途吗?”,接下来就展示一下图中图的用途,说一下它的特别之处。
Notes on Matplotlib_第2张图片

from matplotlib import pyplot as plt

figure = plt.figure()
x = [1, 2, 3, 4, 5, 6, 7]
y = [1, 3, 4, 2, 5, 8, 6]

left, bottom, width, height = 0.1, 0.1, 0.8, 0.8
ax1 = figure.add_axes([left, bottom, width, height])
ax1.plot(x, y, 'r')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_title('title')
# 这不是平级的吗
left, bottom, width, height = 0.2, 0.6, 0.25, 0.25
ax2 = figure.add_axes([left, bottom, width, height])
ax2.plot(x, y, 'b')
ax2.set_xlabel('x')
ax2.set_ylabel('y')
ax2.set_title('title inside1')
print(ax2)  # Axes(0.2,0.6;0.25x0.25)

一张图,上面又贴了一张小图。看看代码才知道,这并不是什么图中带图,而是两个平级的图,先画的图在下面,后画的小图在上面覆盖一下,就成了图中带图的样子。print(ax2) 会打印出 Axes(0.2,0.6;0.25x0.25)。仔细想想,这种 Axes 应该是比 AxesSubplot 更低级的概念,后者带了 Subplot,就是子图的意思,可见这种 AxesSubplot 是专门为子图设计的,它本身就是一张图,位置也由 gridspec.GridSpec 之类的布局工具方便地确定了。而 Axes 是坐标系的概念,声明的时候就要用参数说明具体位置和大小。

既然可以自由地明确地说明坐标系的位置和大小,自然也就可以手动布局多子图了:
Notes on Matplotlib_第3张图片

# 虽然不太方便
figure = plt.figure()
x = [1, 2, 3, 4, 5, 6, 7]
y = [1, 3, 4, 2, 5, 8, 6]
# 1, 1
left, bottom, width, height = 0.075, 0.625, 0.4, 0.32
ax1 = figure.add_axes([left, bottom, width, height])
ax1.plot(x, y, 'r')
ax1.set_xlabel('x1')
ax1.set_ylabel('y1')
ax1.set_title('title 1')
# 1, 2
left, bottom, width, height = 0.575, 0.625, 0.4, 0.32
ax2 = figure.add_axes([left, bottom, width, height])
ax2.plot(x, y, 'g')
ax2.set_xlabel('x2')
ax2.set_ylabel('y2')
ax2.set_title('title 2')
# 2, 1
left, bottom, width, height = 0.075, 0.125, 0.4, 0.32
ax3 = figure.add_axes([left, bottom, width, height])
ax3.plot(x, y, 'b')
ax3.set_xlabel('x3')
ax3.set_ylabel('y3')
ax3.set_title('title 3')
# 2, 2
left, bottom, width, height = 0.575, 0.125, 0.4, 0.32
ax4 = figure.add_axes([left, bottom, width, height])
ax4.plot(x, y, 'k')
ax4.set_xlabel('x4')
ax4.set_ylabel('y4')
ax4.set_title('title 4')
plt.show()

Summary

关于课程中诸如主次坐标、动画等其他的概念就不多说了,了解画图基本概念的目的以达到,其他的花里胡哨的东西只是翻翻文档的事,画图再也不用一头雾水地瞎改参数、瞎查博客,搞得乱七八糟的了。


  1. fig = plt.gcf() 可获取当前 figure. 还有一点就是,一旦调用 plt.figure() 就会生成新的 figure,后续的默认操作也是在新 figure 上,除非用 figure 句柄指定 figure 对象。plt.show() 会打印出当前所有的 figure,并清空,后续 plt.xxx() 会创建新的 figure. ↩︎

你可能感兴趣的:(matplotlib,python,开发语言)