Matplotlib 子图

import matplotlib.pyplot as plt
import numpy as np

入门

下面,我们将指数函数和对数函数的图像绘制在同一个绘图窗口中。

ex = np.linspace(-1, 1, num=101)
ey = np.exp(ex)

lnx = np.linspace(0.02, 1, num=51)
lny = np.log(lnx)

使用 subplot 可以指定子图的排列方式以及索引。

plt.subplot(121)
plt.plot(ex, ey)

plt.subplot(122)
plt.plot(lnx, lny)
[<matplotlib.lines.Line2D at 0x7fa93eb835d0>]


subplot 返回 Axes 对象。因此,上面的绘图代码也可以这样来写:

ax1 = plt.subplot(121)
ax1.plot(ex, ey)

ax2 = plt.subplot(122)
ax2.plot(lnx, lny)
[<matplotlib.lines.Line2D at 0x7fa93ea6ba50>]


但是,subplot 方法不能够直接返回 Figure 对象,这会在设置 Figure 对象属性的时候造成一些麻烦。因此,我们要介绍更一般的方法。

创建指定排布的子图

Matplotlib 中,我们使用 subplots 创建可以指定子图排布的窗口。

fig, ax = plt.subplots()


如果不给 subplots 指定任何参数,那么默认就会创建一个 Axes 对象,即只有一个绘图区域。

subplots 函数返回两个值:一是绘图窗口 Figure 对象,二是指向每个子图绘图区域的 Axes 数组。

当然,我们也可以用 Figure 对象来创建子图绘图区域。下面的代码与上面的等价:

fig = plt.figure()
ax = fig.subplots()


一般地,我们在绘制子图时先要确定好子图的排布情况。例如现在我们要绘制 $6$ 幅子图,按照 $2 \times 3$ 排列:

fig, ax = plt.subplots(nrows=2, ncols=3)


nrowsncols 分别指子图排布的行数和列数。

我们通过简单的下标索引就能够确定每个子图绘图区域的 Axes 对象。

ax[0, 0].plot(ex, ey)
fig


如果子图不多,也可以采用下面的方法直接得到每个子图对象,而不需要索引:

fig, (ax1, ax2) = plt.subplots(2, 1)


上面的 Figure 对象只包含 $2$ 个 Axes 子图,可以直接以元祖的形式来接收返回值。

共享坐标轴

sharexsharey 参数分别表示是否共享同一个 $x$ 轴或 $y$ 轴。

fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)


同理:

fig, (ax1, ax2) = plt.subplots(1, 2, sharey=True)


如果我们的子图排布较为复杂,可以这样来共享坐标轴:

指定每一列共享 $x$ 轴:

fig, ax = plt.subplots(2, 2, sharex=True)


指定每一行共享 $y$ 轴:

fig, ax = plt.subplots(2, 2, sharey=True)


$x$ 轴和 $y$ 轴均共享:

fig, ax = plt.subplots(2, 2, sharex=True, sharey=True)


自定义布局

如果我们只希望画 $3$ 副子图,又希望其中一副能够完整填充空白区域。一个简单的方法就是直接使用 subplot

plt.subplot(221)
plt.plot(ex, ey)

plt.subplot(222)
plt.plot(lnx, lny)

plt.subplot(212)
plt.plot(ex, ey)
plt.plot(lnx, lny)
[<matplotlib.lines.Line2D at 0x7fa93e1ec310>]


上面代码的前两部分将 Figure 对象看作是 $2 \times 2$ 的矩阵,然后只用到第一行。然后再将 Figure 对象看作是 $2 \times 1$ 的矩阵,然后继续绘制第二行。这种绘制方法较为简单,但是需要确定好布局,否则会出现运行时错误。

另一种方法是采用 Figure 对象的 add_subplot 方法。首先,我们需要导入 GridSpec 子图布局模块。

from matplotlib.gridspec import GridSpec

首先,我们创建 Figure 对象。最好我们设置它的布局为约束布局,这样当我们在向其中添加一些元素或修改属性时,Matplotlib 会帮助我们自动调整间距,避免重叠。

fig = plt.figure(constrained_layout=True)
<Figure size 432x288 with 0 Axes>

然后,我们创建子图布局对象:

gs = GridSpec(3, 3, figure=fig)

子图布局对象的作用就是辅助我们去构建我们想要的绘图区域 Axes 的形状。配合 Figure 对象的 add_subplot 方法以及矩阵的索引与切片机制,我们可以自由地创建各种各样的 Axes 绘图区域。

ax1 = fig.add_subplot(gs[0, :])
ax2 = fig.add_subplot(gs[1, :-1])
ax3 = fig.add_subplot(gs[1:, -1])
ax4 = fig.add_subplot(gs[-1, 0])
ax5 = fig.add_subplot(gs[-1, -2])

fig


上面的 Figure 的坐标轴数字都没有挤在一起,说明了约束布局的重要性。

我们可以定义下面的函数,稍微美化一下这个绘图窗口:

def format_axes(fig):
    for i, ax in enumerate(fig.axes):
        ax.text(0.5, 0.5, "ax%d" % (i+1), va="center", ha="center")
        ax.tick_params(labelbottom=False, labelleft=False)

结果如下:

format_axes(fig)
fig.suptitle("GridSpec")
fig


子图的属性与窗口的属性

在概述中,我们就强调了 Figure 对象和 Axes 对象的本质不同。而直到学习到绘制子图,我们才能够渐渐地明白子图的属性和窗口的属性是有很大不同的,是会产生不同的效果的。

设置图例

图例的设置是一个非常经典的例子。假设目前我们一共有 $4$ 组数据,每组数据一共有 $3$ 个指标 $5$ 个系列。现在,我们先生成这个 $4 \times 3 \times 5$ 的矩阵。

data = np.abs(np.random.randn(4, 3, 5))

由于一共有 $4$ 组数据,于是我们用 subplots 方法来生成一个 $2 \times 2$ 布局的子图绘制窗口。

fig, ax = plt.subplots(2, 2, constrained_layout=True)


下面绘制系列条形图:

x = np.arange(1, 6)
width = 0.24

ax[0, 0].bar(x - width, data[0, 0, :], width=width, label='A')
ax[0, 0].bar(x, data[0, 1, :], width=width, label='B')
ax[0, 0].bar(x + width, data[0, 2, :], width=width, label='C')

ax[0, 1].bar(x - width, data[1, 0, :], width=width, label='A')
ax[0, 1].bar(x, data[1, 1, :], width=width, label='B')
ax[0, 1].bar(x + width, data[1, 2, :], width=width, label='C')

ax[1, 0].bar(x - width, data[2, 0, :], width=width, label='A')
ax[1, 0].bar(x, data[2, 1, :], width=width, label='B')
ax[1, 0].bar(x + width, data[2, 2, :], width=width, label='C')

ax[1, 1].bar(x - width, data[3, 0, :], width=width, label='A')
ax[1, 1].bar(x, data[3, 1, :], width=width, label='B')
ax[1, 1].bar(x + width, data[3, 2, :], width=width, label='C')

fig


现在,我们给每个子图添加图例:

ax[0, 0].legend()
ax[0, 1].legend()
ax[1, 0].legend()
ax[1, 1].legend()

fig


然而,实际上我们没有必要为每个子图都添加图例。这个问题的本质在于,legend 是从属于子图的 Axes 对象,还是从属于 Figure 对象?

如果我们只需要绘制从属于 Figure 对象的一份图例,Figure 对象就必须知道数据和对应的标签,这需要捕获 Bar 对象。

fig, ax = plt.subplots(2, 2, constrained_layout=True)

rects1 = ax[0, 0].bar(x - width, data[0, 0, :], width=width, label='A')
rects2 = ax[0, 0].bar(x, data[0, 1, :], width=width, label='B')
rects3 = ax[0, 0].bar(x + width, data[0, 2, :], width=width, label='C')

ax[0, 1].bar(x - width, data[1, 0, :], width=width, label='A')
ax[0, 1].bar(x, data[1, 1, :], width=width, label='B')
ax[0, 1].bar(x + width, data[1, 2, :], width=width, label='C')

ax[1, 0].bar(x - width, data[2, 0, :], width=width, label='A')
ax[1, 0].bar(x, data[2, 1, :], width=width, label='B')
ax[1, 0].bar(x + width, data[2, 2, :], width=width, label='C')

ax[1, 1].bar(x - width, data[3, 0, :], width=width, label='A')
ax[1, 1].bar(x, data[3, 1, :], width=width, label='B')
ax[1, 1].bar(x + width, data[3, 2, :], width=width, label='C')

lg = fig.legend((rects1, rects2, rects3), ('A', 'B', 'C'))


下面我们来美化一下这个图例。我们希望它显示在窗口右上角,并且不要遮挡图像。

lg.set_bbox_to_anchor(bbox=(0.13, 0, 1, 1))
fig


其中,参数 bbox 接收的四元组中,前两个分量分别代表横坐标和纵坐标的相对位置。我们是希望图例向右偏移一些不遮挡图像,因此给横坐标一个较小的增量。后两个分量代表宽和高,一般来说均设置为 $1$,这样不会有任何调整。

Previous
Next