2.1 多子图绘制


文档摘要

2.1 多子图绘制 本节摘要:多子图就是把一张画布(Figure)切成多个绘图区域(Axes),让几个相关的图并排或交错地放在一起,方便横向比较。核心工具是 做规整网格、 做不规整布局,再配合共享坐标轴、间距、总标题这些细节。本节还会讲清两个关键选择:什么时候该规整、什么时候该不规整,以及共享坐标轴为什么能让比较更可靠。学完你能把原来要开三个窗口对比的三张图,收进一张画布里。 本节地图 阅读完本节,你应当能够: 说清 Figure 和 Axes 的关系,以及为什么多子图就是"一个 Figure 装多个 Axes" 用 创建任意行数、列数的规整子图网格 用 创建跨行跨列的不规整布局 用 、 让子图共享坐标轴,减少重复、方便对比 给子图和整张画布分别加标题,并处理间距和重叠

2.1 多子图绘制

本节摘要:多子图就是把一张画布(Figure)切成多个绘图区域(Axes),让几个相关的图并排或交错地放在一起,方便横向比较。核心工具是 plt.subplots 做规整网格、GridSpec 做不规整布局,再配合共享坐标轴、间距、总标题这些细节。本节还会讲清两个关键选择:什么时候该规整、什么时候该不规整,以及共享坐标轴为什么能让比较更可靠。学完你能把原来要开三个窗口对比的三张图,收进一张画布里。

本节地图

阅读完本节,你应当能够:

  1. 说清 Figure 和 Axes 的关系,以及为什么多子图就是"一个 Figure 装多个 Axes"
  2. plt.subplots 创建任意行数、列数的规整子图网格
  3. GridSpec 创建跨行跨列的不规整布局
  4. sharexsharey 让子图共享坐标轴,减少重复、方便对比
  5. 给子图和整张画布分别加标题,并处理间距和重叠

一、为什么一张图不够用

我们先把动机说清楚。很多时候,数据是成组出现的:一家公司四个季度的营收、三支股票在同一时间段的走势、同一个实验在不同参数下的曲线。你可以为它们各画一张图,再在报告里并排贴四张;但更省事的做法,是让它们在一张画布上并列出现。

并列的好处不只是省版面。人眼对"并排的差异"比"翻页后的差异"敏感得多:两张图肩并肩,纵轴刻度对齐,谁的峰值更高、谁先见顶,一眼就能比出来。这就是多子图存在的根本理由——它把"比较"这件事变得可视化

要理解多子图,先得回到第 1 章的对象模型。我们把 Figure 想象成一张画纸,Axes 是画纸上被切出来的一个个区域。过去你习惯用 plt.plot 直接画,其实是 Matplotlib 偷偷帮你建了一个 Figure 和一个 Axes。到了多子图,我们不再让库代劳,而是显式地"领一张画纸、切几个区域、逐个画"。

二、先跑通最小例子

别急着背参数。先看一段能直接跑的最小代码,感受一下"一次领多个区"的手感。

import matplotlib.pyplot as plt import numpy as np x = np.linspace(0, 10, 100) fig, axes = plt.subplots(2, 2, figsize=(8, 6)) # 2 行 2 列,一次领 4 个 Axes axes[0, 0].plot(x, np.sin(x)) axes[0, 1].plot(x, np.cos(x)) axes[1, 0].plot(x, x ** 2) axes[1, 1].plot(x, np.log(x + 1)) fig.suptitle('四张函数图') # 整张画布的总标题 plt.tight_layout() plt.show()

plt.subplots(2, 2) 返回两个东西:fig 是那张画纸,axes 是一个二维数组,axes[0, 0]axes[1, 1] 就是四个绘图区。之后你想在哪块画,就调用哪个 axes 的方法。这个"领区再画"的动作,是多子图与单图最大的区别。

这里有个容易踩的点:axes 的形状会随行列数变。一行三列时它是一维的,用 axes[0];两行两列时是二维的,用 axes[0, 0]。如果你希望无论几行几列都统一用二维下标,就把 squeeze 设为 False。我自己的习惯是:写循环批量画图时统一用 squeeze=False,这样 axes 永远是二维数组,遍历起来不会一会儿一维一会儿二维。

三、规整与不规整:subplots 和 GridSpec

subplots 好用,但它只能切出"整齐的豆腐块"——每行每列一样宽高。现实里我们常常想要不规整的布局:上面一条宽宽的总览图,下面并排两个细节图;或者左边一个大图,右边摞两个小图。这时候就要请出 GridSpec

GridSpec 的思路像装修时的"格子纸":先在纸上画一个 3 行 3 列的网格,然后告诉工人"第 1 行整行都归我""第 2 行前两列归我""最后一列竖着打通"。每个格子是逻辑位置,实际占多少面积由你说了算。

import matplotlib.gridspec as gridspec fig = plt.figure(figsize=(9, 7)) gs = gridspec.GridSpec(3, 3) # 先画一张 3x3 的格子纸 ax1 = fig.add_subplot(gs[0, :]) # 第 1 行整行 ax2 = fig.add_subplot(gs[1, :-1]) # 第 2 行,去掉最后一列 ax3 = fig.add_subplot(gs[1:, -1]) # 最后一列,从第 2 行往下 ax4 = fig.add_subplot(gs[2, 0]) ax5 = fig.add_subplot(gs[2, 1])

gs[0, :] 这种切片写法,和 Python 列表切片是同一个直觉:冒号表示"整段",:-1 表示"到倒数第二个为止",1: 表示"从第二个开始到最后"。一旦你习惯了这个语法,不规整布局就从"要查文档的难题"降级成"写几行切片的事"。

图 2.1 子图布局对比

图 2.1 子图布局对比

选哪个,取决于你心里那套"主次"。规整的 2 乘 2 适合四个地位对等的指标;不规整的总览加细节,适合"先给读者一张全景,再让他钻进两个局部"。我见过不少人为了炫技,用 GridSpec 拼出一个谁也看不懂的迷宫,这就本末倒置了——布局是为了服务"比较",不是为复杂而复杂。

四、共享坐标轴与细节

把几张图排在一起后,立刻会冒出一个新问题:四张图各有各的纵轴,刻度还不一样,并排看反而费劲。这时候就该共享坐标轴。

subplots 的两个参数 sharexsharey 就是干这个的。取值有 'all''row''col'Falsesharex='col' 表示同一列的图共享横轴,只有最下面一行显示横轴刻度;sharey='row' 表示同一行共享纵轴,只有最左边一列显示纵轴刻度。共享之后,图与图之间的刻度线被省掉,画面干净了,而且因为刻度一致,跨图比较才有意义。

fig, axes = plt.subplots(2, 2, figsize=(8, 6), sharex='col', sharey='row') # 只有最底行需要写 x 轴标签,最左列需要写 y 轴标签 axes[1, 0].set_xlabel('时间') axes[0, 0].set_ylabel('数值')

一个反直觉的地方是:共享坐标轴之后,你只需要在"代表格"上设置标签,Matplotlib 会自动同步。别傻乎乎地给四个子图都写一遍 set_xlabel,那样反而可能显示重复。

再谈间距和标题。子图一多,标题、标签就容易打架。plt.tight_layout() 能自动收紧间距,是我的默认动作;还不够就用 gridspec_kw 里的 hspacewspace 手动调。总标题用 fig.suptitle,注意它常会和第一行子图的标题挤在一起,需要 plt.subplots_adjust(top=0.9) 给顶部留点呼吸空间。

tight_layout 之外,新版 Matplotlib 还提供了 constrained_layout,它比 tight_layout 更智能,能同时处理图例、颜色条这类元素的避让,画布里塞了颜色条或大图例时尤其好用。另一个常被忽略的细节是子图的宽高比例:gridspec_kw 里的 width_ratiosheight_ratios 能让不同行、不同列占不同的宽度高度,比如让第一列宽一些放主图、第二列窄一些放辅助图。默认的等分网格,很多时候并不是你真正想要的——主图本该占更多版面,等分了反而主次不分。

还要提醒一句调用顺序:tight_layoutconstrained_layout 要放在所有绘图和标题设置完成之后再调用,因为它需要知道最终的元素尺寸才能计算避让。如果你先调了布局、后面又加标题或图例,新加的元素就可能和已有内容重叠,而布局函数已经"算完了",不会回头再处理。

需求 手段 说明
规整网格 plt.subplots(行, 列) 返回 Figure 和 Axes 数组
不规整布局 GridSpec 加切片 支持跨行、跨列
同列比趋势 sharex='col' 省掉中间横轴刻度
同行比量级 sharey='row' 省掉中间纵轴刻度
自动收紧间距 plt.tight_layout() 多数情况的默认动作
总标题 fig.suptitle() 记得给顶部留空间

五、工程实践里的取舍

多子图这个主题,代码不难,难的是"该不该用、怎么排"的判断。给你几条我自己的经验。

先定布局再动手。很多人上来就 subplots(2, 2),画到第三个图才发现"其实我想要上面一宽条加下面三小格"。改布局等于重写,所以动手前先在纸上把格子画出来,几行几列、谁跨谁,想清楚再敲代码。

子图数量别贪多。一张画布塞九个图,每个图小得像邮票,读者一个也看不清。我一般控制在六个以内;再多,就考虑拆成两张画布,或者干脆用动画滚动展示(第 2.5 节会讲)。信息密度高不等于信息传达好。

共享坐标轴要克制。共享能带来可比性,但如果两个图的数据量级差了好几个数量级(比如一个是百分比、一个是几百万),硬共享纵轴会让小量级那条线被压成一条贴地线。这种时候宁可不共享,各自用合适的刻度,再在图里标清楚单位。

关于接口,我倾向全部用面向对象的 axes 写法,而不是混着 plt 的旧式 subplot。面向对象写法里,每个图都明确落在哪个 axes 上,代码长一点,但读起来不会"这行到底画在哪"。多子图这种场景,最怕的就是接口混用导致的"画错区"。

最后补一句反过来的提醒:不是所有多图都要塞进一张画布。当几张图之间没有可比性、甚至量纲都不同,硬把它们并排,反而会让读者误以为它们可以横向比较。多子图服务于"比较",没有比较需求时,分开独立成图更诚实。

⚠️ 常见坑:用旧式 plt.subplot(2, 2, 3) 时,索引从 1 开始且按行优先;而 subplots 返回的 axes 用二维下标且从 0 开始。两者混着记,最容易把图画错格子。我的建议是:彻底忘掉旧式写法,只用 subplotsGridSpec,一套下标逻辑走到底。
💡 关键直觉:把多子图想象成"给一面墙排版画框"。先量好墙(Figure 尺寸),再画好每个画框的位置(布局),最后才往框里挂画(axes.plot)。顺序反了,就得反复拆框重来。

六、常被问到的几个细节

subplotsubplots 到底用哪个

旧式的 plt.subplot(2, 2, 1) 一次只建一个子图,靠"从 1 开始、按行优先"的索引定位;新式的 plt.subplots(2, 2) 一次把全部子图建好,返回一个 axes 数组。功能上新写法完全覆盖旧写法,还更直观。我的建议是只用 subplotsGridSpec,把旧写法彻底放下,少记一套容易出错的规则。

一行或一列时,axes 该怎么取

subplots(1, 3) 返回的 axes 默认是一维数组,用 axes[0]axes[1] 取;subplots(2, 2) 返回二维数组,用 axes[0, 0] 取。如果你嫌这种"一会儿一维一会儿二维"的规则麻烦,就统一加 squeeze=False,让 axes 永远是二维数组,遍历和索引的写法就固定下来了,写循环批量画图时尤其省心。

共享坐标轴之后,还能单独调某个子图吗

能。共享坐标轴只是让刻度和范围保持一致,并隐藏了重复的刻度标签,你仍然可以对任意一个 axesset_xlimset_ylim,只是改动会同步到共享该轴的兄弟子图。如果某个子图确实要"破例"独立,就单独把它建成 sharex=False,别为它影响整组。

子图太多、挤成一团怎么办

三个办法依次试:先调大 figsize 给整张画布更多空间;再用 plt.tight_layout() 自动收紧间距;还不够,就用 gridspec_kw 里的 hspacewspace 手动拉开。另外要提醒一句,子图超过六个时,先想想是不是该拆成两张画布,别硬塞——信息密度高不等于传达效果好。

想画"上面一个大图、下面三个小图"怎么办

这类"总览加细节"正是 GridSpec 的主场。先 GridSpec(2, 3) 画一张两行三列的虚拟格子,上面的大图占 gs[0, :] 整行,下面三个小图分别占 gs[1, 0]gs[1, 1]gs[1, 2]。要点是先想清楚虚拟网格的尺寸,再用切片把区域划出来,剩下的交给 tight_layout 收拾间距。

本章回顾

  • 一个 Figure 装多个 Axes:多子图不是画多张图,而是把一张画布切成多个绘图区。
  • 规整用 subplotsplt.subplots(行, 列) 一次领回 Figure 和 Axes 数组,逐个画即可。
  • 不规整用 GridSpec:在虚拟格子上用切片指定跨行跨列,实现总览加细节式布局。
  • 共享坐标轴省事又可比sharexsharey 让刻度一致,比较才有意义。
  • 标题分两层set_title 管子图,suptitle 管整张画布,注意留顶部空间。
  • 先布局后动手:动手前画好格子草图,比画到一半改布局省时间。
  • 统一面向对象写法:只用 axes 接口,避免 plt 旧式写法导致"画错区"。

布局定好了,下一个问题接踵而至:每个区里的图,颜色该怎么定?这就是第 2.2 节要解决的——颜色系统与色彩映射。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U