在实际数据分析和报告中,我们经常需要将多个图表组合在一起,以便进行比较或展示多维信息。 Matplotlib 提供了灵活的子图系统,支持从简单的规则网格到复杂的自定义布局。 本章将深入介绍子图创建、布局调整和高级用法。
plt.subplot(nrows, ncols, index) 是最早的子图创建方法,它将画布分割成规则的网格,并按索引(从1开始)选中当前子图:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2*np.pi, 100)
plt.figure(figsize=(8,6))
plt.subplot(2,2,1) # 2行2列,第1个
plt.plot(x, np.sin(x))
plt.title('sin(x)')
plt.subplot(2,2,2) # 第2个
plt.plot(x, np.cos(x))
plt.title('cos(x)')
plt.subplot(2,2,3) # 第3个
plt.plot(x, np.sin(x) * np.cos(x))
plt.title('sin(x)cos(x)')
plt.subplot(2,2,4) # 第4个
plt.plot(x, np.sin(x)**2)
plt.title('sin²(x)')
plt.tight_layout()
plt.show()
subplot 后绘制的图表都会进入对应的子图区域。plt.subplots() 一次性创建所有子图,并返回 Figure 对象和 Axes 对象数组,便于后续操作:
fig, axes = plt.subplots(2, 3, figsize=(12, 6)) # 2行3列
# axes 是一个二维数组,可以通过索引访问
for i in range(2):
for j in range(3):
ax = axes[i, j]
ax.plot(x, np.sin(x + i + j))
ax.set_title(f'子图 ({i+1},{j+1})')
plt.tight_layout()
plt.show()
fig, axes = plt.subplots() 更符合面向对象风格,推荐用于复杂图表。在 subplots() 中使用 sharex 和 sharey 参数可以让子图共享坐标轴,方便比较:
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(8,6))
x = np.linspace(0, 2*np.pi, 100)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x) * np.cos(x)
y4 = np.sin(x)**2
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.show()
自动调整布局:plt.tight_layout() 或 fig.tight_layout() 可以自动调整子图参数,避免标签重叠。
手动控制:plt.subplots_adjust() 允许精确调整间距:
fig, axes = plt.subplots(2, 2, figsize=(8,6))
# 手动调整 left, bottom, right, top 以及 wspace, hspace
plt.subplots_adjust(left=0.1, bottom=0.1, right=0.95, top=0.95,
wspace=0.3, hspace=0.3)
# 或者使用 tight_layout
plt.tight_layout(pad=2.0, w_pad=1.0, h_pad=1.0) # pad 是整体边距,w_pad/h_pad 是子图间间距
tight_layout() 足够,复杂布局时可使用 subplots_adjust。GridSpec 允许创建不规则的子图网格,例如一个子图跨越多行或多列:
import matplotlib.gridspec as gridspec
fig = plt.figure(figsize=(8,6))
gs = gridspec.GridSpec(3, 3, figure=fig) # 3行3列的网格
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[2, 0]) # 第三行,第一列
ax5 = fig.add_subplot(gs[2, 1]) # 第三行,第二列
# 绘制一些内容
ax1.plot([1,2,3], [1,4,9])
ax1.set_title('跨列')
ax2.scatter(np.random.randn(50), np.random.randn(50))
ax2.set_title('中间区域')
ax3.bar(['A','B','C'], [3,7,5])
ax3.set_title('右侧跨行')
ax4.hist(np.random.randn(100), bins=20)
ax4.set_title('左下')
ax5.pie([30,40,30], labels=['X','Y','Z'])
ax5.set_title('中下')
plt.tight_layout()
plt.show()
GridSpec 的切片语法非常灵活,可以轻松实现杂志级布局。subplot2grid 是另一种创建不规则子图的方法,语法类似 subplot 但更直观:
plt.figure(figsize=(10,6))
# 创建3x3网格,ax1位于(0,0),跨3列
ax1 = plt.subplot2grid((3,3), (0,0), colspan=3)
ax2 = plt.subplot2grid((3,3), (1,0), colspan=2)
ax3 = plt.subplot2grid((3,3), (1,2), rowspan=2)
ax4 = plt.subplot2grid((3,3), (2,0))
ax5 = plt.subplot2grid((3,3), (2,1))
ax1.plot([1,2,3], [1,2,3])
ax1.set_title('ax1: 跨3列')
ax2.scatter(np.random.randn(50), np.random.randn(50))
ax2.set_title('ax2')
ax3.bar(['A','B','C'], [3,7,5])
ax3.set_title('ax3: 跨2行')
ax4.hist(np.random.randn(100))
ax4.set_title('ax4')
ax5.pie([30,40,30], labels=['X','Y','Z'])
ax5.set_title('ax5')
plt.tight_layout()
plt.show()
subplot2grid 的参数简单明了:网格形状、起始位置、跨行列数。使用 fig.suptitle() 为整个图形添加一个总标题:
fig, axes = plt.subplots(2, 2, figsize=(8,6))
fig.suptitle('主标题:多个子图的对比', fontsize=16)
axes[0,0].plot(x, np.sin(x))
axes[0,0].set_title('sin(x)')
# ... 其他子图
plt.tight_layout()
plt.show()
通过 width_ratios 和 height_ratios 可以调整各列/行的相对宽度:
fig = plt.figure(figsize=(8,6))
gs = gridspec.GridSpec(2, 2, width_ratios=[1, 2], height_ratios=[1, 1.5])
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])
ax3 = fig.add_subplot(gs[1, :]) # 第二行合并
ax1.pie([30,70], labels=['A','B'])
ax2.plot([1,2,3], [1,4,9])
ax3.bar(['X','Y','Z'], [5,8,6])
plt.tight_layout()
plt.show()
除了创建时共享,也可以在之后使用 ax.sharex() 和 ax.sharey() 建立链接:
fig, axes = plt.subplots(2, 2)
axes[0,0].plot(x, np.sin(x))
axes[1,0].plot(x, np.cos(x))
axes[0,1].plot(x, np.sin(2*x))
axes[1,1].plot(x, np.cos(2*x))
# 共享第一列的 x 轴
axes[1,0].sharex(axes[0,0])
# 共享第一行的 y 轴
axes[0,1].sharey(axes[0,0])
plt.show()
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.gridspec as gridspec
# 生成数据
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x) * np.cos(x)
fig = plt.figure(figsize=(12, 8))
gs = gridspec.GridSpec(3, 3, width_ratios=[1,1,1.5], height_ratios=[1,1,1.5])
# 左上:折线图
ax1 = fig.add_subplot(gs[0, 0])
ax1.plot(x, y1, 'b-')
ax1.set_title('sin(x)')
ax1.set_xlabel('x')
ax1.set_ylabel('sin(x)')
# 中上:散点图
ax2 = fig.add_subplot(gs[0, 1])
ax2.scatter(x, y2, c='r', s=10)
ax2.set_title('cos(x) 散点')
ax2.set_xlabel('x')
ax2.set_ylabel('cos(x)')
# 右上:柱状图(跨两行)
ax3 = fig.add_subplot(gs[0:2, 2])
categories = ['A', 'B', 'C', 'D']
values = [15, 24, 18, 30]
ax3.bar(categories, values, color='lightgreen')
ax3.set_title('柱状图')
ax3.set_ylabel('数值')
# 左中:直方图
ax4 = fig.add_subplot(gs[1, 0])
data = np.random.randn(500)
ax4.hist(data, bins=20, color='steelblue', edgecolor='black')
ax4.set_title('直方图')
# 中中:饼图
ax5 = fig.add_subplot(gs[1, 1])
sizes = [35, 25, 20, 20]
labels = ['A', 'B', 'C', 'D']
ax5.pie(sizes, labels=labels, autopct='%1.1f%%')
ax5.set_title('饼图')
# 下方跨列:热力图
ax6 = fig.add_subplot(gs[2, :])
matrix = np.random.rand(10, 10)
im = ax6.imshow(matrix, cmap='viridis', aspect='auto')
ax6.set_title('热力图')
plt.colorbar(im, ax=ax6)
fig.suptitle('综合报表:多种图表组合', fontsize=16)
plt.tight_layout()
plt.show()
与单图相同,使用 savefig 保存:
fig.savefig('multi_panel_figure.png', dpi=300, bbox_inches='tight')
掌握子图布局技巧后,你可以创建任意复杂的多图组合,让数据故事更加丰富。下一章我们将学习如何自定义图表的样式和颜色,使你的图表更具个性和专业感。