先看四种最常用的子图布局结构,然后给出对应的 6 个案例代码。

先放一段所有案例共用的"数据准备 + 中文显示"前置代码,之后每个案例只写核心部分。
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 中文显示两行标配:指定中文字体 + 修复负号乱码
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
# 示例数据:12个月的销售流水(模拟)
np.random.seed(42)
月份 = np.arange(1, 13)
df = pd.DataFrame({
"月份": 月份,
"销售额": np.random.randint(80, 200, 12),
"订单量": np.random.randint(300, 800, 12),
"退货量": np.random.randint(10, 60, 12),
})
df["利润"] = (df["销售额"] * 0.2 - df["退货量"] * 0.05).round(1)
df["单价"] = (df["销售额"] * 1000 / df["订单量"]).round(1)
案例 1:一行两列 ------ 最基础的并排对比
返回值是 fig, (ax1, ax2),一维时用元组解包最顺手。
python
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.plot(df["月份"], df["销售额"], marker="o", color="steelblue")
ax1.set_title("销售额趋势") # 子图设置标题用 ax.set_title,不是 plt.title
ax2.bar(df["月份"], df["订单量"], color="darkseagreen")
ax2.set_title("订单量分布")
fig.suptitle("月度经营概览") # 整张大图的标题用 fig.suptitle
plt.tight_layout() # 自动收紧,防止标签互相遮挡
plt.show()
要点 :plt. 前缀管"整张图",ax. 前缀管"某个子图"------写多子图时一律改用面向对象的 ax. 写法。
案例 2:两行两列 ------ axes 变二维数组
行、列都大于 1 时,axes 是 2×2 的数组,用 axes[行, 列] 索引。
python
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes[0, 0].plot(df["月份"], df["销售额"], color="steelblue")
axes[0, 0].set_title("销售额")
axes[0, 1].hist(df["利润"], bins=8, color="salmon", edgecolor="white")
axes[0, 1].set_title("利润分布")
axes[1, 0].scatter(df["订单量"], df["销售额"], color="darkseagreen")
axes[1, 0].set_title("订单量 vs 销售额")
axes[1, 1].barh(df["月份"].astype(str), df["退货量"], color="gray")
axes[1, 1].set_title("退货量(横向条形图)")
plt.tight_layout()
plt.show()
要点:一张图里放了折线、直方图、散点、横向条形四种图型,讲清"子图就是各自的独立画布"。
案例 3:sharex 共享横轴 ------ 价格在上、成交量在下
股票软件的经典布局:两个子图共用一条 x 轴,缩放平移联动。柱状图按涨跌涂色(红涨绿跌)。
python
收盘价 = pd.Series([100, 102, 101, 105, 103, 108, 107, 110, 109, 112, 111, 115])
涨跌 = 收盘价.diff() # 与前一天差值,正涨负跌
fig, (ax1, ax2) = plt.subplots(
2, 1, figsize=(10, 6), sharex=True, # 共享 x 轴
gridspec_kw={"height_ratios": [2, 1]} # 上图高度是下图 2 倍
)
ax1.plot(月份, 收盘价, marker="o", color="dimgray")
ax1.set_title("收盘价走势")
颜色 = ["red" if v >= 0 else "green" for v in 涨跌.fillna(0)] # 红涨绿跌
ax2.bar(月份, df["订单量"], color=颜色)
ax2.set_title("成交量(红=上涨日,绿=下跌日)")
ax2.set_xlabel("月份") # x 轴标签只标在最下面一张
plt.tight_layout()
plt.show()
要点 :sharex=True 后上图的 x 刻度标签自动隐藏;gridspec_kw={"height_ratios"} 控制上下高度比。
案例 4:子图比数据多 ------ 隐藏多余画布
3 组数据放进 2×2 的网格,第 4 个空位用 axis("off") 关掉,画布才干净。
python
指标 = ["销售额", "订单量", "利润"]
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
for ax, 列名 in zip(axes.flat, 指标): # axes.flat 把二维数组摊平成一维遍历
ax.plot(df["月份"], df[列名], marker="o")
ax.set_title(列名)
axes[1, 1].axis("off") # 关掉右下角空白子图的坐标轴
fig.suptitle("三项指标巡览")
plt.tight_layout()
plt.show()
要点 :axes.flat(或 axes.ravel())是循环遍历所有子图的标准姿势;ax.axis("off") 隐藏空位。
案例 5:subplot_mosaic ------ 一大两小的仪表盘布局
plt.subplots 只能切等大的格子;要让主图占左侧整列、两个辅图叠在右边,用 subplot_mosaic 按"字母草图"排版。
python
fig, axes = plt.subplot_mosaic(
[["主图", "辅图1"],
["主图", "辅图2"]],
figsize=(10, 6)
)
axes["主图"].plot(df["月份"], df["销售额"], marker="o", color="steelblue")
axes["主图"].set_title("销售额主图(占左侧整列)")
axes["辅图1"].hist(df["利润"], bins=8, color="salmon")
axes["辅图1"].set_title("利润分布")
axes["辅图2"].scatter(df["订单量"], df["销售额"], color="darkseagreen")
axes["辅图2"].set_title("量额关系")
plt.tight_layout()
plt.show()
要点 :网格里相同字母自动合并成一个子图,返回值是字典------用名字取子图,比数字下标更不易错。
案例 6:批量生成 + 保存 ------ DataFrame 有多少列就画多少张
真实工作场景:列数不固定,按列数自动算行列数,最后 savefig 存成图片文件。
python
数据列 = ["销售额", "订单量", "退货量", "利润", "单价"]
n = len(数据列)
行数 = (n + 2) // 3 # 每行 3 张,自动计算需要几行
fig, axes = plt.subplots(行数, 3, figsize=(12, 4 * 行数))
axes = np.atleast_2d(axes) # 只有1行时保证仍能 axes[i, j] 索引
for i, 列名 in enumerate(数据列):
ax = axes[i // 3, i % 3] # 一维序号换算成二维位置
ax.plot(df["月份"], df[列名], marker="o")
ax.set_title(列名)
for j in range(n, 行数 * 3): # 多出来的空位全部关掉
axes[j // 3, j % 3].axis("off")
fig.suptitle("全指标巡览")
plt.tight_layout()
plt.savefig("全指标巡览.png", dpi=150) # 存图放在 show() 之前
plt.show()
要点 :i // 3, i % 3 是"序号 → 行列位置"的通用换算;savefig 必须在 show() 之前调用,否则保存的是空白图。
速查对照:
| 需求 | 写法 |
|---|---|
| 等分网格 | plt.subplots(行, 列) |
| 一维解包 | fig, (ax1, ax2) = plt.subplots(1, 2) |
| 二维索引 | axes[i, j] |
| 共享轴 | sharex=True / sharey=True |
| 高度比例 | gridspec_kw={"height_ratios": [2, 1]} |
| 不等分合并 | plt.subplot_mosaic(...) |
| 遍历子图 | axes.flat |
| 隐藏空位 | ax.axis("off") |
| 整图标题 / 子图标题 | fig.suptitle / ax.set_title |
| 防遮挡 / 保存 | tight_layout() / savefig(...) |
多个子图,读出的顺序,按"从左到右、从上到下"的行优先顺序(先行后列) 。看个 2×3 的直观编号,再讲为什么。

答案:行优先(先行后列)
以案例 4 / 案例 6 里的写法为例:
python
fig, axes = plt.subplots(2, 3)
for ax, 列名 in zip(axes.flat, 数据列):
...
配对结果一定是:
text
序号0 → axes[0,0] ←→ 数据列[0] 左上
序号1 → axes[0,1] ←→ 数据列[1] 中上
序号2 → axes[0,2] ←→ 数据列[2] 右上
序号3 → axes[1,0] ←→ 数据列[3] 左下
序号4 → axes[1,1] ←→ 数据列[4] 中下
序号5 → axes[1,2] ←→ 数据列[5] 右下
为什么是这个顺序
axes 本质是一个 NumPy 数组,axes.flat(以及 axes.ravel()、axes.flatten())默认按 C 语言风格的行主序(row-major) 展开------先把第 0 行的所有列走完,再进入下一行。zip 只是按两个序列各自的顺序一一配对,所以子图的"出厂顺序"决定了配对结果。
python
fig, axes = plt.subplots(2, 3)
print([tuple(x) for x in np.ndindex(axes.shape)]) # 显式查看遍历顺序
# [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]
三个相关细节
axes.flat/ravel()/flatten()顺序一致,可以互换用;想验证就打印:
python
for i, ax in enumerate(axes.flat):
ax.set_title(f"序号{i}") # 跑一遍,编号直接画在图上
-
zip 以短的为准 :数据列只有 5 个而子图有 6 个时,第 6 个空位不会被遍历到------所以案例 6 里要单独再循环一次把
axes[1,2]关掉。 -
例外是 subplot_mosaic :它返回的是字典不是数组,不能用 zip 配位置,要用名字取:
python
axes["主图"].plot(...) # 按字母草图里的名字取,与顺序无关
想要列优先(先列后行)的顺序,也有办法:axes.T.flat(转置后再摊平)或 axes.flatten(order="F")。