2.1 多子图绘制 Matplotlib 进阶绘图:2.1 多子图绘制详解 在数据可视化中,多子图 (Subplots) 是一种极其重要的技术。当我们需要在同一张图中展示多个相关但又独立的数据集,或者从不同角度分析同一数据集时,多子图能够有效地组织和呈现信息,避免图表过于杂乱,提升信息传达的效率和清晰度。Matplotlib 提供了强大的多子图绘制功能,允许用户灵活地创建和定制各种布局的子图。 本篇文章将深入探讨 Matplotlib 中多子图的绘制方法,包括基础概念、核心函数 的使用、不同布局的创建、子图的精细化定制以及共享坐标轴等高级特性。通过理论结合实践的方式,帮助读者全面掌握 Matplotlib 多子图绘制技巧,提升数据可视化能力。 2.1.
在数据可视化中,多子图 (Subplots) 是一种极其重要的技术。当我们需要在同一张图中展示多个相关但又独立的数据集,或者从不同角度分析同一数据集时,多子图能够有效地组织和呈现信息,避免图表过于杂乱,提升信息传达的效率和清晰度。Matplotlib 提供了强大的多子图绘制功能,允许用户灵活地创建和定制各种布局的子图。
本篇文章将深入探讨 Matplotlib 中多子图的绘制方法,包括基础概念、核心函数 plt.subplots() 的使用、不同布局的创建、子图的精细化定制以及共享坐标轴等高级特性。通过理论结合实践的方式,帮助读者全面掌握 Matplotlib 多子图绘制技巧,提升数据可视化能力。
什么是子图 (Subplot)?
子图,顾名思义,就是在一张大的画布 (Figure) 上划分出的多个小的绘图区域 (Axes)。每个子图都可以独立绘制不同的图表,拥有自己的坐标轴、标题、标签等元素。可以将 Figure 比作一张画纸,而 Axes 则是画纸上被划分出来的不同区域,每个区域可以绘制不同的画作。
多子图的应用场景
多子图在数据分析和可视化中有着广泛的应用场景,以下列举一些常见的例子:
比较不同数据集: 当需要对比多个相关数据集的特征或趋势时,可以将它们分别绘制在不同的子图中,并排展示,方便直观比较。例如,对比不同产品的销售额、不同地区的温度变化等。
展示数据的不同方面: 对于同一个数据集,我们可能需要从多个角度进行分析和展示。例如,对于股票数据,可以同时展示股价走势图、成交量柱状图、MACD 指标图等,这些图表可以绘制在不同的子图中,全面呈现股票市场的状况。
构建复杂信息图表: 在制作信息图表 (Infographics) 时,常常需要将多种图表类型组合在一起,以更丰富、更生动的方式传递信息。多子图功能可以灵活地将不同类型的图表 (如折线图、柱状图、散点图、饼图等) 整合到同一张图中。
节省空间,提高信息密度: 相比于绘制多张独立的图表,使用多子图可以将相关信息集中在一张图中展示,节省空间,提高信息密度,方便用户快速获取和理解信息。
Figure 和 Axes 对象
在 Matplotlib 中,绘制图表的核心在于理解 Figure 和 Axes 这两个关键对象。
Figure (画布): Figure 对象代表整个图表画布,它是所有绘图元素的顶层容器。一个 Figure 对象可以包含一个或多个 Axes 对象。你可以把它想象成一张空白的画布。
Axes (坐标轴域/子图): Axes 对象是实际进行绘图的区域,它包含了坐标轴、刻度、标签以及图表本身 (线条、柱状、散点等)。每个 Axes 对象都对应一个子图。你可以把它想象成画布上被划分出来的一个个绘图区域。
当我们使用 Matplotlib 绘制图表时,首先需要创建一个 Figure 对象,然后在 Figure 对象上创建一个或多个 Axes 对象,最后在 Axes 对象上进行具体的绘图操作。
可以用 Mermaid 的 graph TD 图来形象地表示 Figure 和 Axes 的关系:
上图清晰地展示了 Figure 对象包含多个 Axes 对象,而每个 Axes 对象又包含具体的绘图元素 (如线条、点、文本等)。
plt.subplots() 函数:创建多子图的核心Matplotlib 提供了多种创建子图的方式,其中最常用且功能最强大的是 plt.subplots() 函数。 plt.subplots() 函数可以一次性创建 Figure 对象和一组 Axes 对象,并以 NumPy 数组的形式返回 Axes 对象,方便用户进行后续的绘图操作。
plt.subplots() 函数的基本用法
plt.subplots() 函数的基本语法如下:
fig, axes = plt.subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, subplot_kw=None, gridspec_kw=None, **fig_kw)
让我们逐一解释 plt.subplots() 函数的主要参数:
nrows (int, default: 1): 子图的行数。
ncols (int, default: 1): 子图的列数。
sharex (bool or {'none', 'all', 'row', 'col'}, default: False): 控制子图之间 x 轴的共享方式。
False 或 'none': 每个子图的 x 轴独立。
'all': 所有子图共享同一个 x 轴。
'row': 同一行的子图共享 x 轴。
'col': 同一列的子图共享 x 轴。
sharey (bool or {'none', 'all', 'row', 'col'}, default: False): 控制子图之间 y 轴的共享方式。参数取值与 sharex 类似。
squeeze (bool, default: True): 控制返回的 axes 对象数组的形状。
True: 如果只创建了一个子图 (nrows=1 且 ncols=1),则 axes 返回一个 Axes 对象,而不是数组。如果只创建一行或一列子图,则 axes 返回一个一维 NumPy 数组。
False: axes 始终返回一个二维 NumPy 数组,即使只创建了一个子图。
subplot_kw (dict, optional): 传递给 add_subplot() 方法的关键字参数字典,用于定制每个子图的创建。例如,可以设置子图的投影方式 (projection='polar', projection='3d' 等)。
gridspec_kw (dict, optional): 传递给 GridSpec 构造函数的关键字参数字典,用于更精细地控制子图的布局。例如,可以调整子图之间的间距、设置子图的宽度和高度比例等。
**fig_kw (dict, optional): 传递给 figure() 函数的关键字参数字典,用于定制 Figure 对象的属性。例如,可以设置 Figure 的大小 (figsize)、标题 (suptitle) 等。
返回值:
plt.subplots() 函数返回两个对象:
fig (Figure object): 创建的 Figure 对象。
axes (Axes object or array of Axes objects): 创建的 Axes 对象或 Axes 对象数组。axes 的形状取决于 nrows、ncols 和 squeeze 参数。
代码示例 2.1.2.1:创建一个简单的 2x2 子图
import matplotlib.pyplot as plt import numpy as np # 创建数据 x = np.linspace(0, 10, 100) y1 = np.sin(x) y2 = np.cos(x) y3 = x**2 y4 = np.log(x + 1) # 创建 2x2 子图 fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6)) # figsize 设置 Figure 大小 # 在不同的子图中绘制不同的图表 axes[0, 0].plot(x, y1, 'r-') axes[0, 0].set_title('Subplot 1: sin(x)') axes[0, 1].plot(x, y2, 'b--') axes[0, 1].set_title('Subplot 2: cos(x)') axes[1, 0].plot(x, y3, 'g-.') axes[1, 0].set_title('Subplot 3: x^2') axes[1, 1].plot(x, y4, 'm:') axes[1, 1].set_title('Subplot 4: log(x+1)') # 调整子图布局,避免标题重叠 plt.tight_layout() plt.show()
代码详解:
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6)): 调用 plt.subplots() 函数创建了一个 2 行 2 列的子图网格。nrows=2 和 ncols=2 指定了子图的行列数。figsize=(8, 6) 设置了 Figure 对象的大小为 8 英寸宽,6 英寸高。函数返回的 fig 是 Figure 对象,axes 是一个 2x2 的 NumPy 数组,其中 axes[i, j] 表示第 i 行第 j 列的 Axes 对象 (子图)。
axes[0, 0].plot(x, y1, 'r-'): 使用 axes[0, 0] 访问第一个子图 (第一行第一列),并在该子图上绘制红色实线 r- 的 sin(x) 曲线。
axes[0, 0].set_title('Subplot 1: sin(x)'): 使用 axes[0, 0].set_title() 方法为第一个子图设置标题。
类似地,代码分别在 axes[0, 1]、axes[1, 0]、axes[1, 1] 子图中绘制了 cos(x)、x^2、log(x+1) 曲线,并设置了相应的标题。
plt.tight_layout(): 调用 plt.tight_layout() 函数自动调整子图之间的间距和 Figure 边缘的空白,以避免子图标题、坐标轴标签等元素重叠。这是一个非常有用的函数,尤其是在绘制多子图时。
plt.show(): 显示绘制的图表。
运行上述代码,你将看到一个包含 4 个子图的 Figure,每个子图分别展示了 sin(x)、cos(x)、x^2、log(x+1) 曲线。
代码示例 2.1.2.2:创建一行子图 (1x3)
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(12, 4)) axes[0].plot(x, y1) axes[0].set_title('Subplot 1') axes[1].plot(x, y2) axes[1].set_title('Subplot 2') axes[2].plot(x, y3) axes[2].set_title('Subplot 3') plt.tight_layout() plt.show()
代码详解:
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(12, 4)): 创建了一个 1 行 3 列的子图网格。由于只有一行,axes 返回的是一个一维 NumPy 数组,可以直接使用索引 axes[0]、axes[1]、axes[2] 访问子图。代码示例 2.1.2.3:创建一列子图 (3x1)
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(6, 8)) axes[0].plot(x, y1) axes[0].set_title('Subplot 1') axes[1].plot(x, y2) axes[1].set_title('Subplot 2') axes[2].plot(x, y3) axes[2].set_title('Subplot 3') plt.tight_layout() plt.show()
代码详解:
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(6, 8)): 创建了一个 3 行 1 列的子图网格。同样,axes 返回的是一个一维 NumPy 数组。squeeze 参数的影响
在上面的例子中,当创建一行或一列子图时,axes 返回的是一维数组。这是因为 plt.subplots() 函数的默认参数 squeeze=True 起作用了。
当 squeeze=True 时:
如果只创建了一个子图 (1x1),axes 返回一个 Axes 对象 (不是数组)。
如果只创建一行或一列子图,axes 返回一个一维 NumPy 数组。
如果创建多行多列子图,axes 返回一个二维 NumPy 数组。
当 squeeze=False 时:
axes 始终返回一个二维 NumPy 数组,即使只创建了一个子图或一行/一列子图。代码示例 2.1.2.4:squeeze=False 的用法
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(12, 4), squeeze=False) # 即使是一行子图,axes 仍然是二维数组,需要使用 axes[0, i] 访问子图 axes[0, 0].plot(x, y1) axes[0, 0].set_title('Subplot 1') axes[0, 1].plot(x, y2) axes[0, 1].set_title('Subplot 2') axes[0, 2].plot(x, y3) axes[0, 2].set_title('Subplot 3') plt.tight_layout() plt.show()
代码详解:
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(12, 4), squeeze=False): 设置 squeeze=False。
axes[0, 0].plot(...): 即使只创建了一行子图,也需要使用 axes[0, i] 的形式访问子图,因为 axes 是一个 2 维数组 (形状为 (1, 3))。
在大多数情况下,使用默认的 squeeze=True 即可,它能更方便地处理不同布局的子图。只有在需要统一处理 Axes 对象数组时,才可能需要设置 squeeze=False。
gridspec_kw 和 subplot_kwplt.subplots() 函数提供了 gridspec_kw 和 subplot_kw 参数,允许用户更精细地定制子图的布局和属性。
gridspec_kw 参数
gridspec_kw 参数是一个字典,它会被传递给 GridSpec 构造函数。GridSpec 用于定义子图网格的布局。通过 gridspec_kw,我们可以实现更复杂的子图布局,例如:
hspace, wspace):代码示例 2.1.3.1:调整子图间距
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6), gridspec_kw={'hspace': 0.4, 'wspace': 0.3}) # 调整间距 axes[0, 0].plot(x, y1) axes[0, 1].plot(x, y2) axes[1, 0].plot(x, y3) axes[1, 1].plot(x, y4) plt.tight_layout() # 即使手动调整了间距,tight_layout 仍然可以微调 plt.show()
代码详解:
gridspec_kw={'hspace': 0.4, 'wspace': 0.3}: 设置 gridspec_kw 字典,其中 hspace=0.4 表示垂直方向子图之间的间距为子图高度的 40%,wspace=0.3 表示水平方向子图之间的间距为子图宽度的 30%。
使用 GridSpec 对象进行更高级的布局控制: gridspec_kw 还可以接受一个预先创建好的 GridSpec 对象,从而实现更灵活的子图布局,例如创建非均匀网格、合并子图等。但这超出了本文的范围,感兴趣的读者可以查阅 Matplotlib 文档了解 GridSpec 的详细用法。
subplot_kw 参数
subplot_kw 参数也是一个字典,它会被传递给 add_subplot() 方法,用于定制每个子图的属性。通过 subplot_kw,我们可以设置子图的投影方式、坐标轴类型等。
代码示例 2.1.3.2:创建极坐标子图
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 5), subplot_kw={'projection': 'polar'}) # 设置极坐标投影 theta = np.linspace(0, 2*np.pi, 100) r = np.sin(2*theta) axes[0].plot(theta, r) axes[0].set_title('Polar Subplot 1') axes[1].plot(theta, -r) axes[1].set_title('Polar Subplot 2') plt.tight_layout() plt.show()
代码详解:
subplot_kw={'projection': 'polar'}: 设置 subplot_kw 字典,其中 projection='polar' 指定子图使用极坐标投影。这样创建的 Axes 对象就是极坐标轴对象,可以使用极坐标相关的绘图方法。除了 'polar',projection 参数还可以设置为 '3d' (创建 3D 子图)、'rectilinear' (默认的笛卡尔坐标系) 等。
sharex 和 sharey 参数在某些情况下,我们希望多个子图共享同一个坐标轴,例如,当需要对比不同子图的数据范围时,共享坐标轴可以方便地进行视觉比较。plt.subplots() 函数提供了 sharex 和 sharey 参数来控制子图之间 x 轴和 y 轴的共享方式。
sharex 和 sharey 参数的取值:
False 或 'none': 默认值,每个子图的 x 轴 (或 y 轴) 独立,刻度和范围可以不同。
'all': 所有子图共享同一个 x 轴 (或 y 轴)。所有子图的 x 轴 (或 y 轴) 刻度和范围都相同,并且只有最底行 (或最左列) 的子图显示 x 轴 (或 y 轴) 标签。
'row': 同一行的子图共享 x 轴。同一行子图的 x 轴刻度和范围相同,并且只有最底行的子图显示 x 轴标签。
'col': 同一列的子图共享 y 轴。同一列子图的 y 轴刻度和范围相同,并且只有最左列的子图显示 y 轴标签。
'xy' 或 True: 等同于 sharex='all' 和 sharey='all',所有子图同时共享 x 轴和 y 轴。
代码示例 2.1.4.1:共享 x 轴 (sharex='col')
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6), sharex='col') # 同列子图共享 x 轴 axes[0, 0].plot(x, y1) axes[0, 1].plot(x, y2) axes[1, 0].plot(x, y3) axes[1, 1].plot(x, y4) # 由于共享 x 轴,只有最后一行子图显示 x 轴标签 axes[1, 0].set_xlabel('X-axis') axes[1, 1].set_xlabel('X-axis') # 仍然可以为每个子图设置独立的 y 轴标签 axes[0, 0].set_ylabel('Y1') axes[0, 1].set_ylabel('Y2') axes[1, 0].set_ylabel('Y3') axes[1, 1].set_ylabel('Y4') plt.tight_layout() plt.show()
代码详解:
sharex='col': 设置 sharex='col',表示同一列的子图共享 x 轴。
axes[1, 0].set_xlabel('X-axis') 和 axes[1, 1].set_xlabel('X-axis'): 只需要在最后一行的子图上设置 x 轴标签,所有共享 x 轴的子图都会显示该标签。
axes[0, 0].set_ylabel('Y1') 等: y 轴仍然是独立的,可以为每个子图设置不同的 y 轴标签。
代码示例 2.1.4.2:共享 y 轴 (sharey='row')
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6), sharey='row') # 同行子图共享 y 轴 axes[0, 0].plot(x, y1) axes[0, 1].plot(x, y2) axes[1, 0].plot(x, y3) axes[1, 1].plot(x, y4) # 由于共享 y 轴,只有第一列子图显示 y 轴标签 axes[0, 0].set_ylabel('Y-axis') axes[1, 0].set_ylabel('Y-axis') # 仍然可以为每个子图设置独立的 x 轴标签 axes[0, 0].set_xlabel('X1') axes[0, 1].set_xlabel('X2') axes[1, 0].set_xlabel('X3') axes[1, 1].set_xlabel('X4') plt.tight_layout() plt.show()
代码示例 2.1.4.3:共享 x 轴和 y 轴 (sharexy='all')
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6), sharex='all', sharey='all') # 所有子图共享 x 和 y 轴 axes[0, 0].plot(x, y1) axes[0, 1].plot(x, y2) axes[1, 0].plot(x, y3) axes[1, 1].plot(x, y4) # 只需在最左下角的子图上设置 x 和 y 轴标签 axes[1, 0].set_xlabel('X-axis') axes[1, 0].set_ylabel('Y-axis') plt.tight_layout() plt.show()
代码详解:
sharex='all', sharey='all': 设置 sharex='all' 和 sharey='all',表示所有子图共享 x 轴和 y 轴。
axes[1, 0].set_xlabel('X-axis') 和 axes[1, 0].set_ylabel('Y-axis'): 只需要在最左下角的子图 (通常是最后一个子图) 上设置 x 轴和 y 轴标签,所有子图都会共享这些标签。
共享坐标轴可以有效地减少重复的坐标轴元素,使图表更加简洁,并方便比较不同子图的数据范围。
为了使多子图图表更易于理解,我们需要为每个子图添加标题,并为整个 Figure 添加一个总标题。
为子图添加标题: 使用 Axes.set_title() 方法为每个子图设置标题。我们在之前的例子中已经多次使用过 set_title() 方法。
为 Figure 添加总标题: 使用 Figure.suptitle() 方法为整个 Figure 对象设置总标题。总标题通常显示在 Figure 的顶部中央位置。
代码示例 2.1.5.1:添加子图标题和总标题
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6)) axes[0, 0].plot(x, y1) axes[0, 0].set_title('Subplot 1: Sine Wave') # 子图标题 axes[0, 1].plot(x, y2) axes[0, 1].set_title('Subplot 2: Cosine Wave') # 子图标题 axes[1, 0].plot(x, y3) axes[1, 0].set_title('Subplot 3: Quadratic Function') # 子图标题 axes[1, 1].plot(x, y4) axes[1, 1].set_title('Subplot 4: Logarithmic Function') # 子图标题 fig.suptitle('Comparison of Different Functions', fontsize=16) # Figure 总标题 plt.tight_layout() plt.subplots_adjust(top=0.9) # 调整顶部空间,避免总标题与子图标题重叠 plt.show()
代码详解:
fig.suptitle('Comparison of Different Functions', fontsize=16): 调用 fig.suptitle() 方法为 Figure 对象 fig 设置总标题,fontsize=16 设置总标题字体大小。
plt.subplots_adjust(top=0.9): 由于总标题会占用 Figure 的顶部空间,可能导致与子图标题重叠。plt.subplots_adjust(top=0.9) 函数可以调整子图在 Figure 中的位置,top=0.9 表示将子图的顶部边界设置为 Figure 高度的 90%,从而在顶部留出足够的空间显示总标题。可以根据实际情况调整 top 值。
总结
本文详细介绍了 Matplotlib 中多子图绘制的核心概念和方法,重点讲解了 plt.subplots() 函数的用法,包括:
使用 nrows 和 ncols 参数创建不同行列数的子图网格。
使用 sharex 和 sharey 参数控制子图之间坐标轴的共享方式。
使用 gridspec_kw 和 subplot_kw 参数定制子图的布局和属性。
使用 Axes.set_title() 和 Figure.suptitle() 方法添加子图标题和总标题。
多子图绘制的最佳实践
明确目的,选择合适的布局: 在绘制多子图之前,首先要明确你的目的是什么,希望通过多子图展示哪些信息。根据目的选择合适的子图布局,例如,对比不同数据集可以使用并排排列的子图,展示数据不同方面可以使用网格布局。
合理设置 Figure 大小: Figure 的大小会影响子图的大小和整体视觉效果。根据子图的数量和内容,合理设置 figsize 参数,确保子图清晰可辨,整体图表比例协调。
充分利用共享坐标轴: 在需要对比数据范围或趋势时,尽可能使用共享坐标轴,可以简化图表,方便视觉比较。