matplotlib学习笔记

Matplotlib 是 Python 强大的绘图库之一,可以用简单的代码绘制出精美的图表。很多人第一次接触 Matplotlib 可能会觉得参数很多、不知从何下手。其实,画图就像做菜,数据是原料,Matplotlib 提供的各种函数和参数就像调味料和摆盘工具,我们可以根据需要一步步“烹饪”出可口的图表。本篇笔记旨在通过实例演示 Matplotlib 基础绘图的常用技巧,包括画线、设置样式、添加图例、调整坐标轴、标注特殊点,以及创建多窗口子图和布局等。风格上力求通俗易懂,带着一点“下厨”的比喻,希望能让初中高级程序员都轻松上手 Matplotlib。

一、基础绘图

首先,我们从最基本的折线图开始。假设我们想绘制几个正弦函数曲线,例如 \sin(x)、\cos(x) 和 \sin(2x),以此熟悉 Matplotlib 画线的基本用法和样式设置。准备好数据后,只需调用 plt.plot(x, y) 就能画出曲线,默认情况下 Matplotlib 会将数据点顺次连成线并显示。下面的代码生成了 -π 到 π 范围内的 1000 个点,并计算上述三个函数的取值,然后使用 plt.plot 依次绘制三条曲线,并通过参数定制每条线的外观:

import numpy as np
import matplotlib.pyplot as plt

# 准备数据:x 为 -π 到 π 的等间隔点,计算 sinx、cosx、sin2x 三个函数值
x = np.linspace(-np.pi, np.pi, 1000)    # 生成 -π 到 π 区间的 1000 个点
sinx = np.sin(x)                        # sin(x) 数组
cosx = np.cos(x)                        # cos(x) 数组
sin2x = np.sin(2 * x)                   # sin(2x) 数组

# 绘制三条曲线,并设置线型、颜色、透明度等属性
plt.plot(x, sinx, linestyle='-', color=(0.3, 0.7, 0.1), alpha=0.3, label='sinx')     # 绿实线,30%不透明度
plt.plot(x, cosx, linestyle='-.', color='black', label='cosx')                       # 黑色点划线
plt.plot(x, sin2x, linestyle='--', linewidth=3, color='#334455', label='sin2x')      # 蓝灰色虚线,线宽3

上面的代码绘制了三条函数曲线。plt.plot 是最基本的绘图命令,我们传入 x 和 y 数组即可绘制折线。我们还能通过可选参数调整线条样式: * linestyle 指定线型,比如 ’-’ 实线、’—’ 虚线、’-.’ 点划线、’:’ 点线等。 * color 可以接受颜色名称(如 ‘black’、‘red’)、十六进制颜色码(如 #334455),或 RGB 元组([0,1]区间)来定义颜色。 * linewidth 调整线宽,默认值为 1,数值越大线越粗。 * alpha 设置透明度,取值 0~1,0 表示完全透明,1 表示不透明。上例中将第一条 sin 曲线的 alpha 设为 0.3,让它变得半透明。 * label 用于给曲线命名,稍后我们会用它生成图例。

短短几行代码,我们就“炒”出了三道数据曲线“大菜”。默认情况下,这些曲线会绘制在同一个坐标系中,不同颜色和线型让它们彼此区分开来。此时如果调用 plt.show(),Matplotlib 会弹出一个窗口显示我们绘制的图形。不过在展示最终效果之前,我们还需要进一步“调味”来美化图表。

image-20250423210648364

如果一次绘制多条曲线,适当使用不同颜色、线型区分它们是必要的;当曲线较多或有重叠时,可以通过设置较高的透明度 (alpha 接近 1) 或降低透明度来更好地区分重叠区域。此外,提前为每条曲线指定 label,方便后续生成图例说明曲线含义。

二、坐标轴与刻度调整

有了基本的曲线,我们通常需要对坐标轴和刻度进行调整,以使图表更加专业、美观。Matplotlib 会自动根据数据范围设置坐标轴的取值范围和刻度标记,但我们可以根据需求手动调整。

首先,如果我们只关心某一段区间的数据,可以通过 限制坐标轴范围 来“聚焦”关注点。例如,只想看 x 在 [0,π] 范围内、y 在 [0, 1.1] 的部分,可以这样设置:

plt.xlim(0, np.pi)   # 将 x 轴显示范围限制在 0 到 π
plt.ylim(0, 1.1)     # 将 y 轴显示范围限制在 0 到 1.1

上述 plt.xlim 和 plt.ylim 会剪裁我们之前绘制的曲线,只显示指定的区间。在实际使用中,这种操作相当于给“镜头”拉近或移动焦点,让我们更专注于感兴趣的部分。不过在本例中,我们希望完整展示 πππ 的曲线,因此不对坐标范围做裁剪。(注:如果已经设置了限制,移除或调整可以使用 plt.xlim(None, None) 恢复默认全范围。)

image-20250423210727186

接下来,我们来调整坐标轴的刻度和样式。默认的刻度往往是简单的数字,但对于数学函数而言,用 π\pi 等符号作为刻度标记会使图表更具可读性。下面的代码设置自定义的刻度位置和标签,并移动坐标轴位置:

# 设置刻度位置和标签(包含 LaTeX 数学符号)
plt.xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi],        # x轴刻度的位置
           [r'$-\pi$', r'$-\frac{\pi}{2}$', r'$0$', r'$\frac{\pi}{2}$', r'$\pi$'],  # 对应标签
           fontsize=14)                                  # 刻度标签字体大小
plt.yticks([-1, -0.5, 0, 0.5, 1], fontsize=14)            # 设置y轴刻度及字体大小(标签使用默认数字)

# 移动坐标轴位置,使原点居中
ax = plt.gca()  # 获取当前Axes对象
ax.spines['top'].set_color('none')      # 隐藏上边框
ax.spines['right'].set_color('none')    # 隐藏右边框
ax.spines['bottom'].set_position(('data', 0))  # 将下边框(x轴)移动到 y=0 处
ax.spines['left'].set_position(('data', 0))    # 将左边框(y轴)移动到 x=0 处

这里我们使用了 plt.xticks 和 plt.yticks 函数来自定义刻度。对于 x 轴,我们指定了5个刻度位置:π,;π/2,;0,;π/2,;π-π,; -π/2,; 0,; π/2,; π,并通过列表传入对应的刻度标签字符串。注意,这些字符串前面有前缀 r,并包裹在 ...... 中,这是 Matplotlib 支持的 LaTeX 数学语法,可以直接在标签中显示数学符号,例如 -\frac{\pi}{2}。这样我们的 x 轴刻度就以 π 的倍数形式呈现,更加直观。fontsize=14 则增大了刻度数字的字体,便于阅读。对于 y 轴,我们选择了几个典型值 -1 到 1 作为刻度,同样调大了字体;这里没有特别指定标签列表,因此默认使用这些值的数字形式作为标签。

接下来,通过 plt.gca() 获取当前的 Axes 对象(即绘图的坐标系),我们可以对坐标轴的外观做细节调整。Matplotlib 每个坐标轴有上下左右四条脊(line),默认情况下绘制在图表边框。上述代码将 top 和 right 两条脊的颜色设为 ‘none’,即隐藏顶部和右侧的边框线,只保留底部和左侧的坐标轴线。随后两行使用 set_position((‘data’, 0)) 将底部x轴移动到数据坐标中的 y=0 位置,以及左侧y轴移动到 x=0 位置。效果就是让 x轴和y轴在数据坐标的原点处相交,而不是位于图表边缘。这样处理后,我们的坐标系看起来更像数学课本中的直角坐标系:原点在中央,轴线穿过整个图表。这对于展示函数图像(尤其是包含正负值)非常有帮助,可以清楚地看到曲线与坐标轴的交点和对称性。

image-20250423210800179

三、图例与标注

绘制多条曲线时,图例(Legend)可以帮助观者分辨每一条曲线代表什么含义。我们在绘制时已经给每条曲线指定了 label,现在只需一行代码就能让 Matplotlib 自动生成图例:

plt.legend(['sinx', 'cosx', 'sin2x'], loc='upper right')  # 显示图例并指定在右上角

plt.legend 会在图表上绘制一个小框,展示每种线型颜色对应的标签。在上面的调用中,我们手动提供了标签列表 [‘sinx’, ‘cosx’, ‘sin2x’],对应之前绘制的三条曲线,并将图例的位置指定在坐标系的右上角(loc=‘upper right’)。实际上,如果之前 plt.plot 时传入了 label 参数,调用 plt.legend() 时不提供列表,Matplotlib 也会使用那些标签。但手动传入列表可以控制顺序或覆盖某些标签。图例的位置也可根据需要选择 ‘upper/lower’ 和 ‘left/center/right’ 的组合,或使用坐标点指定,自由度很高。通常我们会将图例放置在不干扰数据的位置,比如曲线稀疏的角落。

最后,我们常常需要在图表中标注特殊点或额外的信息,以突出关键内容。Matplotlib 提供了多种方法,比如 plt.annotate 可以添加箭头和文本说明;这里我们用更简单的 plt.scatter 来绘制特殊标记符号。假设我们想强调 sin(x) 在 x=pi/2 处的峰值 1.0,以及 -sin(x) 在 x=-pi/2 处的谷值 -1.0(对应图中的两个极值点)。可以使用散点图在这些坐标处打上星形标记:

# 标出特殊的极值点,使用红星标记
plt.scatter([np.pi/2, -np.pi/2], [1.0, -1.0],       # 两个点的坐标 (x列表, y列表)
            marker='*', s=200,                      # 星形标记,点的大小为200
            edgecolors='green', facecolor='red',    # 边缘为绿色,填充为红色
            zorder=2)                               # 提升图层顺序,以免被曲线遮挡

plt.scatter 用于绘制散点,这里我们传入两个点的 x 坐标列表和 y 坐标列表,各包含我们想标注的点。参数 marker=’*’ 使散点的形状为星形(五角星),s=200 控制星星的尺寸(200 相当于较大的点)。我们将星标设置为红色填充、绿色边框,醒目地标出位置。由于这些点可能与曲线重合,我们通过 zorder=2 提升其绘制层级,确保星星盖在曲线之上而不是被遮住(Matplotlib 默认折线的 zorder 为 1)。执行完这些步骤后,调用 plt.show() 就会弹出窗口显示最终绘制的图形。完整的图表应该包含三条曲线、中心的坐标轴、精美的刻度标记、右上的图例,以及用星星标出的两个特殊点。

image-20250423210111119

图 1:使用 Matplotlib 绘制 sin x、cos x 和 sin 2x 函数曲线的效果图。绿色半透明实线表示 sin x,黑色点划线表示 cos x,蓝灰色粗虚线表示 sin 2x。坐标轴移到了中心位置,并使用 pi 等符号标记刻度。右上角的图例清晰地标明了各曲线名称,而红色星形标记突出了 sin x 在 x=pi/2 处的峰值 (1.0) 和 sin x 在 x=-pi/2 处的谷值 (-1.0)。

通过上述流程,我们已经完成了一张函数曲线的绘制。从代码可以看到,Matplotlib 的调用过程实际上非常灵活:你可以在绘制完曲线后再去调整坐标轴和添加图例、标注等,不一定要按固定顺序。但最终展示 (plt.show()) 前的所有设置都会反映在图表上。因此,推荐的习惯做法是先绘图、再装饰:即先用 plt.plot 等把主要数据绘制出来,然后根据需要调整轴、刻度、添加图例和注释,最后再显示或保存图表。这样思路清晰,也方便逐步调试图表的外观。

四、多窗口与布局

Matplotlib 不仅能在一个坐标系里绘制多条曲线,还允许我们创建多个图形窗口同时显示不同的图表。这在需要对比不同数据集,或者分别展示不同步骤的结果时非常有用。接下来,我们通过一个简单示例来体验如何管理多个绘图窗口以及设置图表的标题、网格和布局。

假如我们有两组数据想分别绘制在不同窗口中。我们可以使用 plt.figure() 创建新的图形窗口。plt.figure 可以指定一个名称或编号来标识窗口,以及窗口大小 figsize 和背景色 facecolor 等参数。如果再次调用 plt.figure 并传入已有的名称,那么后续绘图命令会作用在该已存在的窗口。请看下面的代码:

plt.figure('A', figsize=(8, 4), facecolor='lightgray')  # 创建名为 'A' 的绘图窗口,尺寸8x4英寸,背景灰色
plt.plot([1, 2, 3], [3, 2, 1])                         # 在 Figure A 中绘制一条线

plt.figure('B', figsize=(8, 4), facecolor='lightgray')  # 创建名为 'B' 的绘图窗口
plt.plot([1, 2, 3], [1, 2, 3])                         # 在 Figure B 中绘制一条线

plt.figure('A')                                        # 切换回窗口 A
plt.plot([1, 2, 3], [1, 2, 3])                         # 在 Figure A 中再绘制一条线

plt.title('the line', fontsize=30)   # 给窗口 A 的图表加标题
plt.xlabel('x')                     # 设置x轴标签
plt.ylabel('y')                     # 设置y轴标签

plt.grid(linestyle=':')             # 添加网格线(细点线样式)
plt.tight_layout()                  # 自动调整布局,防止标题和标签遮挡

plt.show()                          # 显示所有打开的图形窗口

代码解析:首先,我们创建了窗口 A,大小 8x4 英寸,背景色为浅灰,然后在其上绘制了一条折线(数据点为 (1,3), (2,2), (3,1))。接着创建窗口 B(同样大小和背景),并在上面绘制另一条折线((1,1), (2,2), (3,3))。然后再次调用 plt.figure(‘A’),由于名为 A 的窗口已经存在,这行代码的作用是将当前绘图对象切换回 A。于是接下来的 plt.plot([1,2,3],[1,2,3]) 会添加一条新曲线到窗口 A 原有的图表中。这样,窗口 A 上就有两条折线,而窗口 B 上有一条折线,各自在独立的窗口中显示。

接下来,我们对窗口 A 当前的坐标系设置了标题和轴标签。plt.title(‘the line’, fontsize=30) 为曲线图加上标题“the line”,字号设置较大以醒目显示;plt.xlabel(‘x’) 和 plt.ylabel(‘y’) 则为横轴和纵轴添加了名称。这些标题/标签会出现在窗口 A 的图表上,而窗口 B 由于未设置将保持没有标题和轴标签的状态(当然你也可以在切换到 B 时分别设置属于它自己的标题和标签)。

为了让图表阅读起来更方便,我们打开了网格线:plt.grid(linestyle=’:’) 在图表背景添加了网格辅助线,这些虚点线可以帮助对齐和估读数值,又不会过于显眼干扰主图形。最后调用 plt.tight_layout() 自动调整子图布局。这一步在只有单个轴的情况下主要是防止标题或标签文字过大而被窗口边缘裁剪;当一个窗口中有多幅子图(subplot)时,tight_layout 会调整子图间距,防止它们的刻度标签相互重叠,是个非常实用的一键排版工具。

image-20250423210450895

运行上述代码,Matplotlib 将会弹出两个窗口A和B,分别显示不同的曲线图。窗口 A 上有标题和两条折线,其中一条呈下降趋势,另一条上升趋势交叉形成一个 “X” 形状;窗口 B 则只有一条从左下到右上升的直线。灰色背景让窗口略有区分度,而网格线和大字号标题则提升了可读性。由于我们调用了一次 plt.show(),所以所有尚未显示的图形窗口都会同时显示出来。

需要注意,plt.figure() 如果不指定名称,Matplotlib 会使用默认编号 (如 Figure 1, Figure 2 …) 来标识不同窗口。我们在这里用字母 ‘A’ 和 ‘B’ 来命名窗口,直观且便于在代码中引用特定窗口。实际应用中,如果要同时比较几张图,可以开多个窗口分别显示;而在写脚本批量生成图表时,通常是循环内每次创建新 figure,绘图并保存然后关闭,而不会像这里一样同时弹出多个窗口。

当图表元素较多时,善用网格线 (plt.grid) 有助于读者对齐数值,但要注意线型应当柔和(如细虚线)以免喧宾夺主。使用 plt.tight_layout() 可以省去手动调整布局的麻烦,强烈建议在添加了标题、标签或多个子图后调用一下,这对保存图片尤为重要,避免轴标签被截掉。如果需要在一张窗口里绘制多张子图,可以使用 plt.subplots() 或 plt.subplot() 创建网格布局的子图,然后分别绘制;每个子图也能用 set_title 设置标题。无论是多窗口还是多子图,合理安排布局和标题都能让读者更容易比较和理解图中的信息。


matplotlib 中文字符乱码问题

image-20250424142400653

# 打印系统可用的字体
from matplotlib.font_manager import FontManager
fm = FontManager()
mat_fonts = set(f.name for f in fm.ttflist)
print(mat_fonts)

# 设置一个中文字体
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']

image-20250424142426927

五、子图

有时候我们希望将不同的图表并排或分块展示——就像在一页PPT上放多张幻灯片,或者在一面“照片墙”上挂多张照片一样。这时候,就需要用到 Matplotlib 的**子图(subplot)**功能,将多个绘图区域组合在一个画布中。接下来我将介绍如何创建多子图布局、共享坐标轴以及美化子图。

1. 多图布局基础用法

Matplotlib 中的子图指的是在同一个 Figure (图像窗口或画布)内划分出的多个小型绘图区域(Axes)。可以把 Figure 想象成一块画布,而子图就是画布上的一个个网格区域,允许我们在每个区域绘制不同的数据图。这样一来,我们无需打开多个窗口,就能在一张图里以矩阵布局展示多幅图表。

创建子图布局有几种方法,最常用的是使用 plt.subplots 或 plt.subplot:

  • plt.subplots 一次性创建整个子图网格,并返回 Figure 对象和包含各子图 Axes 对象的数组。推荐使用这种方法,代码结构清晰便于后续操作。
  • plt.subplot 则是一次创建单个子图(指定位置),需要多次调用来构建网格。它本质上相当于逐步往当前 Figure 添加子图,适合简单快速的绘图,但当子图较多时代码会略显繁琐。

下面通过一个简单示例演示两种方法的用法。假设我们要在同一窗口内上下排列两幅图:第一幅绘制 sin(x)\sin(x) 曲线,第二幅绘制 cos(x)\cos(x) 曲线。

方法1:使用 plt.subplot 分步添加子图

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2*np.pi, 100)           # 生成0到2π之间的100个点
y_sin = np.sin(x)
y_cos = np.cos(x)

plt.figure()                              # 创建一个新的Figure窗口
plt.subplot(2, 1, 1)                      # 将画布划分为2行1列子图,激活第1个子图
plt.plot(x, y_sin, color='g')             # 在第1个子图绘制sin曲线
plt.title("sin(x) 曲线")                   # 设置第1个子图的标题

plt.subplot(2, 1, 2)                      # 激活第2个子图(2行1列布局的第二格)
plt.plot(x, y_cos, color='r')             # 绘制cos曲线
plt.title("cos(x) 曲线")                   # 设置第2个子图标题

plt.tight_layout()                       # 自动调整子图间距,防止标题或标签重叠
plt.show()

上面的代码中,我们首先使用 plt.figure() 新建了一个画布,然后通过两次 plt.subplot(2, 1, i) 分别创建了上下两个子图。参数 (2, 1, i) 表示总共2行1列的网格,i 表示当前激活的是第几个子图(按行优先顺序编号)。在第一个子图上,我们绘制了 sin(x) 曲线并设置了标题;随后切换到第二个子图,绘制 cos(x) 曲线并设置标题。plt.tight_layout() 用于自动调整子图之间的间隙,使得布局更紧凑、美观。在调用 plt.show() 后,两幅子图将会同时显示在同一窗口的上下两个区域。

这种逐个 plt.subplot 添加子图的方法直观简单,但当子图增多时需要小心编号,避免放错位置或重复。下面我们介绍更为简洁的 plt.subplots 用法。

image-20250424194251112

方法2:使用 plt.subplots 一次性创建网格

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2*np.pi, 100)
y_sin = np.sin(x)
y_cos = np.cos(x)

fig, axes = plt.subplots(2, 1)            # 创建2行1列的子图网格,返回Figure和Axes对象列表
axes[0].plot(x, y_sin, color='g')         # 在axes[0](第1个子图)绘制sin曲线
axes[0].set_title("sin(x) 曲线")           # 设置第1个子图标题
axes[1].plot(x, y_cos, color='r')         # 在axes[1](第2个子图)绘制cos曲线
axes[1].set_title("cos(x) 曲线")           # 设置第2个子图标题

plt.tight_layout()
plt.show()

可以看到,plt.subplots(2, 1) 直接生成了包含2行1列子图的图表。它返回了一个 fig(Figure对象)和一个 axes 数组。这里 axes 是Python的数组类型(当只有一行或一列时,axes为一维数组;有多行多列时则是二维数组)。我们通过 axes[0] 访问第一个子图,axes[1] 访问第二个子图,对它们分别调用 plot 方法进行绘制。随后使用每个 Axes 对象的 set_title 方法设置子图的标题。最终使用 plt.tight_layout() 调整布局并显示。

image-20250424194326236

使用 plt.subplots 有几个好处:

  • 代码简洁:无需手动编号子图位置,避免了出错的可能。
  • Axes对象易于操作:得到的 axes 数组可以方便地在循环中迭代,或者通过索引来访问特定子图,从而对一组子图进行批量设置,比如统一的样式等。
  • 可同时获取Figure:返回的 fig 对象可用于设置整体属性(如总体标题、保存图像文件等)。

值得注意的是,如果我们创建多行多列的网格布局,例如 plt.subplots(2, 2),得到的 axes 将是一个 2x2 的数组,此时访问右上角的子图要用 axes[0, 1],左下角则是 axes[1, 0],以此类推。

通过以上两种方法,我们已经可以在一个窗口中放置多个子图了。接下来,我们看看如何让这些子图共享坐标轴,从而在比较数据时更方便。

2. 共享坐标轴的技巧

当多个子图在横轴或纵轴上具有相同的刻度范围时,我们可以使用 Matplotlib 的共享坐标轴功能,使它们的刻度和缩放保持一致。这样做有几个好处:一是避免重复显示相同的刻度标签,让界面更干净;二是在交互模式下缩放或拖动其中一个子图时,其他共享轴的子图会同步变化,方便对比。

Matplotlib 提供了 sharex 和 sharey 参数来控制子图是否共享 x轴或y轴。我们可以在调用 plt.subplots 时通过参数设定,也可以在使用 fig.add_subplot 或 plt.subplot 创建子图时传入已有 Axes 来共享。

最常用的用法是在 plt.subplots 中指定:

fig, axes = plt.subplots(2, 1, sharex=True)  # 创建2行1列子图,纵向排列,共享X轴

这样生成的两个子图将共享同一套 X 轴刻度。对于上下排列的子图,共享 X 轴后,上方子图的横轴标签会自动被隐藏,只在最下方子图显示 X 轴刻度,从而避免重复。它们的 X 轴范围也保持一致,这样在比较比如正弦和余弦周期时就对齐了。同理,sharey=True 则会让子图共享 Y 轴刻度和范围,通常用于左右并排的子图具有相同量纲时,比如比较两种数据分布的差异。

我们来看一个具体例子,加深理解共享坐标轴的效果。假设我们横向并排绘制两条正弦曲线:左边绘制 sin(x)\sin(x),右边绘制 2sin(x)2\sin(x)(振幅是前者的两倍)。我们让它们共享Y轴,以方便对比振幅差异:

x = np.linspace(0, 2*np.pi, 100)
y1 = np.sin(x)
y2 = 2 * np.sin(x)

fig, axes = plt.subplots(1, 2, sharey=True)   # 创建1行2列的子图,左右并排,共享Y轴
axes[0].plot(x, y1, label='sin(x)')
axes[0].set_title("sin(x)")
axes[1].plot(x, y2, label='2 sin(x)', color='orange')
axes[1].set_title("2*sin(x)")

plt.tight_layout()
plt.show()

以上代码生成了左右两个子图。由于设置了 sharey=True,两幅图共用相同的Y轴刻度范围。这样一来,即使右侧函数的振幅是左侧的两倍,两张图的Y轴刻度对齐后,我们可以直接观察到右侧曲线在纵向上是左侧的两倍高。同时,Matplotlib 会自动隐藏右侧子图多余的Y刻度标签(只在左侧子图显示Y轴刻度),使图表看起来更简洁。若没有共享Y轴,每个子图会根据自身数据自动缩放Y范围,左图Y轴可能是[-1,1],右图是[-2,2],这样刻度不统一对比起来就不直观。

image-20250424194536755

除了简单的布尔值 True/False,sharex/sharey 还支持更细粒度的控制,例如 ‘row’ 或 ‘col’,允许在同一行或同一列的子图间共享坐标轴,以及 ‘all’(等价于 True)在所有子图间共享。对于大部分场景,直接使用 True 就足够了,Matplotlib 会帮助我们处理标签的显示。当然,如果你需要对子图的刻度进行更多自定义,也可以手动调整哪些刻度标签可见,例如通过 axes[i].xaxis.set_visible() 等高级用法,但这超出了本篇讨论范围。

小贴士:共享坐标轴在数据交互分析中特别有用。例如在交互模式下(如Jupyter Notebook中),当我们缩放或移动其中一个共享轴的子图,其他相关子图会同步缩放/移动,保持对齐状态,从而方便我们同时观察多处细节。

3. 美化子图:标题、标签、图例和布局

有了多子图布局和共享坐标轴的技巧,我们还需要对每个子图进行适当的美化和注释,让读者容易理解每张图传递的信息。美化主要包括以下几个方面:

  • 子图标题和坐标轴标签:为每个子图添加描述性的标题(title),并为X轴、Y轴注明含义(xlabel, ylabel)。这就像每张“小画”下面的标题说明,告诉读者这幅图展示的是什么。如果所有子图的X或Y轴表示的都是相同的量(例如时间、频率等),可以只在最下方或最左侧的子图注明即可,避免重复标注。
  • 刻度美化:根据需要调整刻度标签的格式和密度。例如多个子图共享X轴时,上面的子图我们通常去掉X轴刻度标签,只保留底部,以免拥挤。Matplotlib已经在 sharex/sharey 时为我们做了这些自动处理,但我们也可以通过 Axes.set_xticks、Axes.set_xticklabels 等函数自定义刻度。
  • 图例(Legend):如果某个子图中绘制了多条曲线或多种数据,对应的图例能帮助区分不同颜色/样式代表的含义。使用 ax.legend() 可以自动根据前面 plot 时传入的 label 显示图例。如果每个子图只有一条线,图例不是必须的,此时可以通过标题或注释直接说明。但在演示代码中,我们依然会示范如何添加图例,以备读者在需要时参考。
  • 整体标题和布局调整:当整张图包含多个子图时,往往还需要一个总标题来概括说明整个图表的主题。这可以使用 plt.suptitle 或 fig.suptitle() 来添加。此外,为了防止子图之间的元素重叠、保证整体美观,我们可以使用 plt.tight_layout() 或 plt.subplots_adjust() 来调整子图间的间距。tight_layout 会自动调整间距以适应正常显示,但有时总标题可能与子图重叠,这种情况下可以在调用 tight_layout() 时留出一点顶部空隙,例如 plt.tight_layout(rect=[0, 0, 1, 0.95]),确保 rect 区域内的是子图,空出的 5% 高度用于放总标题。

以上这些美化细节,可以根据具体需求灵活运用。总的原则是:清晰易读。标题要准确,标签要清楚,图例必要时给出,刻度简洁不重复,布局紧凑不拥挤。接下来,我们通过一个综合示例来实战演练这些技巧。

3.1 矩阵式布局(subplots)

在 Matplotlib 中,最常用也最简单的多子图布局方式就是“矩阵式”网格布局。所谓矩阵式布局,就是把整张图像划分成若干行×列的网格,每个网格区域放置一个子图,排列方式就像矩阵一样整齐对齐。我们通常使用 plt.subplots() 函数一次性创建一个矩阵布局的子图网格。这个函数会根据指定的行列数量,在同一 Figure 内生成对应数量的 Axes 对象,并以均匀大小排列。例如 plt.subplots(2, 3) 将创建2行3列(共6个)等大小的子图。矩阵布局适合需要展示多个内容相似的图表进行并排对比的场景,比如同一组数据的不同变换结果等。

使用 plt.subplots() 非常方便:它会返回一个 Figure 对象和包含多个 Axes 子图的数组。我们可以通过索引这个 Axes 数组来操作各个子图,例如设置每个子图的标题、绘制曲线等。下面的代码演示了创建一个2×2矩阵子图布局,并在每个子图上绘制一条简单的直线(斜率各不相同):

import matplotlib.pyplot as plt
import numpy as np

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(6,4))  # 创建2行2列的子图网格
# 逐一绘制每个子图
x = np.linspace(0, 5, 100)
for i, ax in enumerate(axes.flat):  # axes.flat 将2x2的Axes矩阵展平成一维迭代
    ax.plot(x, (i+1)*x, label=f'y={i+1}x')        # 在第 i 个子图绘制线性函数 y=(i+1)x
    ax.set_title(f"子图 {i+1}")                   # 设置子图标题,例如 "子图 1"
    ax.legend(fontsize=8)                        # 添加图例
plt.tight_layout()                               # 自动调整布局以防止标签重叠
plt.show()

image-20250424201555252

上图演示了一个2×2的矩阵式子图布局。其中每个子图大小相等,彼此之间自动留有默认间距。我们使用返回的 axes 数组来逐个访问和控制各子图,例如给每个子图设置了标题“子图 1/2/3/4”。像这样矩阵网格排列的方式优点是非常快捷明了:只需一行代码就生成所需布局,每个子图默认对齐美观,适合快速绘制规整对比的多图情景。此外,plt.subplots() 还提供了 sharex、sharey 等参数方便子图共享坐标轴,以及 constrained_layout=True 或后续调用 plt.tight_layout() 来自动调整子图间距,避免标签重叠。

当然,矩阵式布局也有局限。它的缺点在于子图大小和排列方式相对固定,所有网格单元大小相同,难以满足不规则布局需求。例如,如果想让某个子图占据更大的区域(跨行跨列)或者各子图尺寸比例不一致,plt.subplots() 就无法直接实现。此外,当子图数量较多时,矩阵布局可能会显得拥挤,调节子图间的间距需要手动调用 plt.subplots_adjust() 等方法。不过,对于大多数常规用途,矩阵式布局已经足够简洁实用,是绘制多子图时的首选方案。

适用场景:矩阵式布局适合用于简单规整的多图对比场景,例如同类指标在不同条件下的曲线比较、不同数据集的相似图形展示等。总之,当子图数量不多且布局要求均匀对齐时,使用 subplots 能大大简化绘图流程。

**小结:**矩阵式布局通过 plt.subplots 快速创建规则网格中的子图,胜在简单易用;但如果需要更复杂的布局(不等大小的子图或非均匀排列),就需要考虑下面将介绍的更灵活方法了。

3.2 网格布局(GridSpec)

当矩阵式的均匀网格无法满足需求时,可以使用 Matplotlib 提供的 GridSpec 网格布局来自定义更灵活的子图排列。GridSpec 是 matplotlib.gridspec 模块中的一个类,用于细粒度地控制子图所在网格的位置和跨越范围。它的原理是先将整个图形按照指定的行列数划分成更细的网格单元,然后通过指定某个子图占用哪些单元格(可以是一个或多个相邻单元格)来实现各种不规则布局。简单来说,GridSpec 就像在图形上搭建了一个**“网格板”**,你可以任意指定每张子图在这块网格板上覆盖的区域大小。

使用 GridSpec 一般需要两步:首先创建一个 GridSpec 实例,设定总的行数 nrows 和列数 ncols(以及可选的宽高比、间距等参数);然后用这个 GridSpec 来添加子图,指定每个子图占据的网格位置。可以使用 Figure.add_subplot(GridSpec[pos]) 或 plt.subplot(GridSpec[pos]) 来创建子图,其中 pos 可以是类似切片的索引(比如 spec[0, :] 表示第0行所有列)。GridSpec 允许通过切片语法让一个子图跨越多行或多列,从而实现大小不一的复杂布局。下面通过代码示例展示 GridSpec 的用法:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np

fig = plt.figure(figsize=(6,4))
# 创建一个3行4列的GridSpec网格布局
spec = gridspec.GridSpec(nrows=3, ncols=4, figure=fig)

# 基于GridSpec在特定位置添加子图:
ax1 = fig.add_subplot(spec[0, :])   # 占据第0行的所有4列(整整一行)
ax2 = fig.add_subplot(spec[1, :2])  # 占据第1行的前2列
ax3 = fig.add_subplot(spec[1, 2:])  # 占据第1行的后2列
ax4 = fig.add_subplot(spec[2, 0], projection='polar')  # 占据第2行第0列,设置为极坐标投影
ax5 = fig.add_subplot(spec[2, 1:])  # 占据第2行剩余的3列

# 在每个子图上绘制示例曲线以示区别
x = np.linspace(0, 2*np.pi, 100)
ax1.plot(x, np.sin(x), color='C0')
ax1.set_title("ax1: 正弦曲线")
ax2.plot(x, np.sin(2*x), color='C1')
ax2.set_title("ax2: sin(2x)")
ax3.plot(x, np.cos(x), color='C2')
ax3.set_title("ax3: 余弦曲线")
theta = np.linspace(0, 2*np.pi, 60)
r = np.abs(np.sin(2*theta))
ax4.plot(theta, r, color='C3')
ax4.set_title("ax4: 极坐标")
ax5.plot(x, np.cos(x/2), color='C4')
ax5.set_title("ax5: cos(x/2)")

plt.tight_layout()
plt.show()

image-20250424201716238

上图展示了使用 GridSpec 创建的一个复杂布局示例。其中,ax1 占据顶上一整行,是一个横跨4列的长条形子图;中间一行被拆分成两个子图 ax2 和 ax3,各占据该行的一半空间;最后一行左下角 ax4 占据单独一个小网格,并被设置为极坐标投影(绘制了一个极坐标图);最后一行的其余三格被 ax5 合并成一个宽幅子图。可以看到,通过灵活地指定 GridSpec 索引,同一张图中可以同时拥有大小不同的多个子图。GridSpec 幕后实际上也是细分网格,只不过允许我们自由组合这些网格单元,从而突破了矩阵式布局“每个子图同尺寸”的限制。

GridSpec 优势在于高度的灵活性:我们可以定制子图的相对大小(通过设定 width_ratios 和 height_ratios 参数来调整每列每行的宽度比例),也可以精确控制子图间的间隔(通过 wspace、hspace 或 left、right 等参数调整边距)。它非常适合布局要求不均匀的情形,例如某个子图需要比其他子图宽两倍,或想在一旁放置几个小图辅助主图等。在上面的例子里,如果用普通的 subplots 是很难直接实现这种布局的,而 GridSpec 则轻松完成。

GridSpec 的劣势主要是使用稍显复杂。相较于 subplots 一行代码生成网格,GridSpec 需要先创建网格规格再逐个添加子图,代码量增加的同时也需要对布局有更清晰的规划。此外,GridSpec 的布局仍然受限于网格划分本身:虽然你可以通过增加总行列数来提高布局精度,但本质上子图边界仍对齐于某个网格线。如果需要完全自由的位置(比如不规则偏移、不对齐于统一网格),那 GridSpec 也无能为力,此时就需要用到更底层的自由布局方法。

适用场景:当子图大小不统一或者需要子图跨越多行多列时,应考虑 GridSpec。例如制作学术论文中的复杂分格图、一张图里包含主图和数个小图拼合的情况,GridSpec 能让布局过程更加顺手。另外,如果只是略微调整部分子图大小,其实也可以通过 plt.subplots() 的 gridspec_kw 参数传递 width_ratios 等来实现,但布局一复杂还是直接使用 GridSpec 更直观明了。

3.3 自由布局(add_axes)

除了以上按网格划分区域的方式,有时候我们希望对子图的位置和大小进行完全自由地控制。Matplotlib 提供的底层方法 Figure.add_axes()(或者等价的 plt.axes() 函数)允许我们通过指定归一化坐标直接在图形上放置子图,这被称为自由布局。自由布局并不依赖统一的网格划分,我们可以在图表的任意位置插入一个新的 Axes。add_axes 需要一个 [left, bottom, width, height] 列表参数,这四个值都是相对于图形 Figure 的比例(0~1范围的小数),表示子图相对于整幅图形左下角的位置和宽高。例如 [0.1, 0.1, 0.8, 0.8] 就表示在距离图形左边10%、下边10%的位置放置一个宽80%、高80%的子图。

自由布局的最大特点就是灵活性:不拘泥于任何网格,你想把子图放在哪儿、多大,都由你来决定。常见用法之一是在主图中嵌入小图(inset)以展示局部放大细节。这种情形下,小图往往需要精确地放置在主图某个角落上,而具体的位置大小可能是任意的,就可以用 add_axes 手动调整。下面代码演示了创建一个主子图和一个嵌入的小子图:

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2*np.pi, 400)
y_main = np.sin(x)
y_inset = y_main + 0.1*np.random.randn(len(x))  # 带一点噪声的正弦,作为插入图的数据

fig = plt.figure(figsize=(6,4))
ax_main = fig.add_axes([0.1, 0.1, 0.8, 0.8])   # 主图,占据Figure的大部分区域
ax_main.plot(x, y_main, color='C0')
ax_main.set_title("主图: 正弦波")
ax_main.set_xlim(0, 2*np.pi)
ax_main.set_ylim(-1.1, 1.1)

ax_inset = fig.add_axes([0.55, 0.55, 0.35, 0.3])  # 插入的小图,指定相对位置和大小
ax_inset.plot(x, y_inset, color='C1')
ax_inset.set_title("插入图")
ax_inset.set_xlim(0, np.pi)    # 小图显示主图局部 (0~π 区间)
ax_inset.set_ylim(-1.1, 1.1)

plt.show()

image-20250424201842221

以上的输出图中,大的正弦波曲线是主 Axes(ax_main),占据了图形绝大部分区域;右上角嵌入了一个小的 Axes(ax_inset),显示了主图前半段正弦波加噪声的细节。通过手动调整 add_axes 的参数,我们将小图精确地定位在主图内部。自由布局让我们能够在同一张 Figure 中任意叠加多个坐标系:不仅可以并排放置,还可以相互重叠(比如在主图上叠加一个局部放大图,就像上例一样)。这种方法在需要添加数据细节放大图、局部区域对比图,或者在图中某处添加特殊的辅助坐标系(如色彩条、示意图等)时非常有用。

自由布局给予了最大限度的控制,但也意味着需要开发者自己计算和调整位置,因此有一些注意点和不足:首先,所有数值都是相对比例,如果日后更改图形尺寸或者保存为不同大小,可能需要重新调整这些参数以达到理想布局。其次,由于不再有网格参考,对齐多个自由放置的子图会比较耗费精力——比如想让两个轴的宽度精确相等、水平对齐,就需要手动保证它们的坐标参数一致。相对于前面的自动网格布局,add_axes 缺少自动排版功能,如果有重叠也不会发出警告,所以使用时要特别仔细。不过,只要布局需求明确,通过反复调整 [left, bottom, width, height] 参数,也可以实现非常精美的组合图形。

适用场景:自由布局适用于那些常规网格难以覆盖的特殊设计。例如:在主要图表中嵌入放大图、在角落添加额外的信息图表、绘制不规则排版的可视化作品等。当需要完全自定义子图的位置(甚至允许子图区域彼此重叠)时,fig.add_axes() 是唯一的选择。此外,一些Matplotlib附加功能(如双Y轴、极坐标与笛卡尔坐标混合等)背后也是通过自由布局原理实现的。

**优点:**最大程度的灵活,想放哪就放哪,能够实现最复杂的布局需求;对于精细排版或特殊图形(比如Inset小图)来说,这是唯一能精确定位的方法。

**缺点:**需要手动计算坐标,使用成本较高。不当使用可能导致子图互相重叠或布局失调;当子图很多时,手动调整每个的参数也比较繁琐。另外,自由布局方式基本无法享受 Matplotlib 提供的自动布局调整功能(如 tight_layout 对手动添加的轴作用有限),需要用户自行保证美观对齐。

以上我们介绍了三种 Matplotlib 中常见的子图布局方式:从简单易用的矩阵式布局,到灵活强大的网格布局,再到完全自定义位置的自由布局。在实际绘图中,应根据需求选择合适的方法:规则排列的多子图优先用 subplots,非规则排版用 GridSpec,而需要精确定位时才考虑 add_axes。灵活运用这些布局技巧,可以帮助我们制作出丰富多样的复合图形,更有效地传达数据故事。

4. 实战示例:综合案例分析

假设我们想要综合展示多条正弦/余弦函数曲线的特性。我们打算在一张图的2行2列网格中展示4个子图,包括 sin(x)\sin(x)cos(x)\cos(x)sin(2x)\sin(2x) 以及 sin(x)+cos(x)\sin(x)+\cos(x) 四条曲线,各占一个子图。为了便于比较,所有子图共享相同的X轴刻度范围。我们将为每个子图添加标题、图例等,并给整张图加一个总标题。以下是示例代码(对应 02_figure.py 文件的功能):

image-20250424193859186

生成的2x2子图布局示例:分别绘制 sin(x)\sin(x)cos(x)\cos(x)sin(2x)\sin(2x)sin(x)+cos(x)\sin(x)+\cos(x) 曲线,每个子图都有自己的标题和图例,且共享横轴。总标题显示在顶部。

import numpy as np
import matplotlib.pyplot as plt

# 1. 准备数据
x = np.linspace(-np.pi, np.pi, 400)       # 在[-π, π]范围内生成400个点
y_sin   = np.sin(x)
y_cos   = np.cos(x)
y_sin2  = np.sin(2 * x)
y_sum   = y_sin + y_cos                   # sin(x)+cos(x) 曲线

# 2. 创建2行2列的子图布局,所有子图共享X轴
fig, axes = plt.subplots(2, 2, sharex=True, figsize=(8, 6))  
fig.suptitle("Subplots Demo", fontsize=16)            # 设置整体图表的标题

# 3. 绘制各子图
axes[0, 0].plot(x, y_sin, color='g', label='sin(x)')
axes[0, 0].set_title("sin(x) 曲线")                   # 子图1标题
axes[0, 0].set_ylabel("Amplitude")                    # 子图1 Y轴标签
axes[0, 0].legend(loc='best')                         # 子图1 图例

axes[0, 1].plot(x, y_cos, color='r', label='cos(x)')
axes[0, 1].set_title("cos(x) 曲线")                   # 子图2标题
axes[0, 1].legend(loc='best')                         # 子图2 图例

axes[1, 0].plot(x, y_sin2, color='b', label='sin(2x)')
axes[1, 0].set_title("sin(2x) 曲线")                  # 子图3标题
axes[1, 0].set_xlabel("X 值")                         # 子图3 X轴标签
axes[1, 0].set_ylabel("Amplitude")                    # 子图3 Y轴标签
axes[1, 0].legend(loc='best')                         # 子图3 图例

axes[1, 1].plot(x, y_sum, color='purple', label='sin(x)+cos(x)')
axes[1, 1].set_title("sin(x)+cos(x) 曲线")            # 子图4标题
axes[1, 1].set_xlabel("X 值")                         # 子图4 X轴标签
axes[1, 1].legend(loc='best')                         # 子图4 图例

# 4. 调整布局并显示
plt.tight_layout(rect=[0, 0, 1, 0.95])                # 调整子图间距,并为总标题留出顶部空隙
plt.show()

上述代码一步一步地展示了如何构建一个综合图表:

  • 数据准备:生成了 x 以及对应的4组 y 数据。其中 y_sum 是将正弦和余弦相加,作为第四个子图的数据。这部分在实际应用中可以替换为任何你需要对比展示的多个数据系列。
  • 创建子图网格:我们使用 plt.subplots(2, 2, sharex=True, figsize=(8,6)) 创建了2行2列的网格子图,一次性返回 fig 和 axes。这里设置了 sharex=True,因此4个子图共享X轴刻度。此外通过 figsize 参数调整整张图的尺寸(8x6英寸),以获得合适的宽高比。
  • 添加总标题:调用 fig.suptitle(“Subplots Demo”, …) 增加了整个图的标题。在本例中我们用了英文,为的是避免中文显示可能遇到的字体问题。如果需要中文标题(例如“多个子图综合示例”),可以在代码开头通过 matplotlib.rcParams 设置中文字体,或者直接使用支持中文的字体库,这里不展开细述。
  • 绘制各子图内容:接下来,我们对 axes 数组的每个元素依次调用 plot 绘制曲线,并设置标题和标签。比如 axes[0,0] 代表第一行第一列的子图,绘制的是 y_sin 数据并标注标题“sin(x) 曲线”。我们还设置了该子图的Y轴标签为”Amplitude”(幅度),并调用 legend 显示图例。同样的方法应用到其余三个子图。需要注意的是,在设置子图网格标题时,我们在中文后加了“曲线”二字,以示该图所表示的是函数曲线;你也可以根据实际内容起更有意义的标题。
  • 图例说明:每个子图我们都调用了 legend(loc=‘best’)。由于我们在 plot 时传入了 label 参数,Matplotlib 会自动创建相应的图例条目。对于只有一条线的子图,图例并不是必须的,因为标题已经说明了一切;这里添加图例主要是演示用法。如果某个子图中有多条曲线,图例就非常重要,可以使用 loc=‘best’ 让 Matplotlib 自动选择最佳位置,或手动指定位置避免遮挡关键内容。
  • 布局调整:最后,我们调用了 plt.tight_layout(rect=[0, 0, 1, 0.95]) 来优化子图的布局。tight_layout 会自动调整子图之间的空白以防止内容重叠。这里特别传入了 rect=[0, 0, 1, 0.95],意思是将绘图区域限定在画布的0%到95%高度之间,留出上方5%的空白用于放置总标题(否则总标题可能会被压缩到看不见)。紧接着 plt.show() 显示最终效果。

运行这段代码,你将得到一个包含4个子图的综合图表。每个子图都有清晰的标题和刻度,底部共享的X轴刻度为 -3.14(-π)到 3.14(π),整体标题位于顶部中央。通过这样的布局,我们可以在一张图中方便地对比 sin(x)\sin(x)cos(x)\cos(x) 及其变体的差异。例如,从图中可以观察到 sin(2x)\sin(2x) 相对于 sin(x)\sin(x) 周期缩短了一半,而 sin(x)+cos(x)\sin(x)+\cos(x) 曲线则表现为相位相差90度的正弦波叠加。

在实际绘图时,子图的数量和排列应根据要表达的内容来决定。不要为了追求“一页展示所有”而把过多的子图挤在一起,这会使每个子图过于袖珍,难以看清细节。对于子图较多的情况,可以适当增大 figsize,或拆分成多张图。此外,确保同一组子图使用一致的样式和比例尺,如果需要比较不同数据集,共享坐标轴能够避免观众被不同的刻度迷惑。最后,善用标题和注释,让读者一眼就能抓住重点。

六、统计图形

在数据可视化中,统计图形用于展示数据的分布、比较及关系。本节将介绍 Matplotlib 中几种常用的统计图形,包括散点图、填充图、条形图和直方图的用法。我们将通过示例代码演示如何绘制这些图形,并结合工程应用场景给出一些实用建议和常见的参数调优技巧。

1 散点图

散点图(Scatter Plot)用于显示二维数据分布及其特征,常用来观察数据点在平面上的分布形态。通过 plt.scatter 函数,我们可以绘制出数据点云,并利用颜色、大小、形状等视觉元素映射数据的不同特征值。例如,可以在散点图中同时体现每个点的类别(用不同形状区分)、某连续属性的大小(用颜色深浅或大小表示)等。散点图广泛应用于聚类结果可视化、异常检测等场景,因为它能够直观展示不同组数据的聚集程度和离散情况。

下面的示例代码模拟了两个类别的人群样本数据。我们生成了两个正态分布集群(Group A 和 Group B),并假设每个样本点有三个特征:二维平面坐标 (x, y)、年龄值 age(用于映射颜色)、以及一个随机大小特征(用于映射点的大小)。我们使用不同的散点形状和颜色来区分这两类样本,并通过颜色渐变表示年龄特征。

import numpy as np
import matplotlib.pyplot as plt

# 1. 模拟两类人群的二维正态分布数据
np.random.seed(0)  # 设置随机种子,保证结果可重复
n = 50
# Group A: 均值在(0,0)附近的正态分布点
x1 = np.random.normal(0, 1, n)
y1 = np.random.normal(0, 1, n)
# Group B: 均值在(5,5)附近的正态分布点
x2 = np.random.normal(5, 1, n)
y2 = np.random.normal(5, 1, n)

# 为每个点生成一个年龄特征,用于映射颜色
age1 = np.random.normal(30, 5, n)   # Group A 的年龄,大致分布在30左右
age2 = np.random.normal(50, 5, n)   # Group B 的年龄,大致分布在50左右

# 生成每个点的大小特征(50~100范围内随机),用于映射散点尺寸
size1 = np.random.rand(n) * 50 + 50
size2 = np.random.rand(n) * 50 + 50

# 2. 绘制散点图
plt.figure(figsize=(6,4))
# 绘制 Group A 散点,marker='o'表示圆形标记,cmap 设置颜色映射表
plt.scatter(x1, y1, c=age1, cmap='viridis', s=size1, marker='o', label='Group A')
# 绘制 Group B 散点,marker='^'表示三角形标记
plt.scatter(x2, y2, c=age2, cmap='viridis', s=size2, marker='^', label='Group B')
plt.xlabel('Feature X')    # X轴标签
plt.ylabel('Feature Y')    # Y轴标签
plt.title('Scatter Plot with Two Groups')  # 图标题
plt.legend()               # 显示图例,以区分两组
# 添加颜色栏以体现年龄特征,注明颜色对应的含义
cbar = plt.colorbar()
cbar.set_label('Age')
plt.show()

上述代码中,我们分别用 plt.scatter 绘制了两组数据:Group A 使用圆形点(marker=‘o’)绘制,Group B 使用三角形点(marker=’^‘)绘制。参数 c 传入每个点的年龄数组,并指定了 cmap=‘viridis’ 颜色映射,这样散点的颜色会随年龄值变化(年龄小的点偏蓝紫色,年龄大的点偏黄绿色)。我们还使用参数 s 设置了散点尺寸,使得每个点大小有所区别。通过 plt.colorbar() 添加的颜色栏,我们可以直观地看到颜色与年龄数值的对应关系。

image-20250424211551349

图4.1:使用 plt.scatter 绘制的散点图示例。图中蓝色圆点和绿色三角分别代表两类不同人群样本(Group A 和 Group B),颜色由年龄特征映射(右侧颜色栏表示年龄大小),点的面积大小也有所区别。可以看出,两组数据在二维空间中明显分开,形成各自的聚类,中间几乎没有重叠。这种可视化有助于我们直观了解聚类结果和类别分布特征,同时通过颜色深浅也能观察另一个维度(年龄)的取值范围。

在绘制散点图时,还有一些常用参数和技巧需要注意:

  • 颜色映射 (cmap): cmap 用于指定颜色图谱,例如 ‘viridis’、‘plasma’、‘rainbow’ 等。当散点颜色用于表示连续数值时,应选择感知线性的色谱(如 Viridis),保证颜色变化与数值变化对应合理,避免过亮或过暗难以分辨。【注意】如果类别是离散的,可以预先将不同类别映射为固定颜色,而不使用连续色谱。
  • 标记样式 (marker): 常用的 marker 形状有圆圈 ‘o’、三角形 ’^‘、方形 ‘s’ 等。选择不同的标记可以区分不同类别,但标记种类不宜过多,避免图形过于杂乱。一般来说,两三种形状已经足够区分类别,更多类别时更常用颜色区分。
  • 叠放次序 (zorder): 当一个图中有多层绘图元素时,zorder 参数决定了它们的叠放顺序,数值大的会覆盖数值小的。默认情况下,后绘制的元素在上层。但如果我们希望先绘制的散点不被后绘制的遮挡,可以通过设置 zorder 实现。例如,plt.scatter(x1, y1, …, zorder=2) 和 plt.scatter(x2, y2, …, zorder=1) 会让第一组散点始终覆盖在第二组之上。【小贴士】在散点很多且可能重叠时,也可以通过降低点的透明度(参数如 alpha=0.7)来缓解遮挡问题,使密集区域的颜色更深,从而辨识分布密度。

散点图在工程上非常有用。例如,在聚类分析中,我们可以将不同簇的样本用不同颜色或形状的散点表示出来,观察簇间的分离情况;在异常检测中,散点图能帮助发现远离主要簇的离群点(outliers)。总之,当我们需要研究两个连续特征的关系,并辅以其他特征进行标注时,散点图提供了一种直观有效的手段。

2 填充图

填充图是通过填充两条曲线之间的区域来表示数据范围或不等式关系的图形。在 Matplotlib 中,可以使用 plt.fill_between 函数对两条曲线(或一条曲线与坐标轴之间)进行区域填充。这种图形常用于表示上下边界(如最大值最小值范围)、置信区间带、以及满足某种条件的区域等。例如,我们可以用填充图直观展示两个函数曲线之间的区域差异,或强调某指标在阈值以上的区域范围。

以下示例演示了如何绘制两条正弦曲线之间的填充区域。我们使用 y1 = sin(x) 和 y2 = cos(x/2)/2 两条曲线作为上、下边界,通过 plt.fill_between 将两曲线围成的区域填充颜色,并区分出哪条曲线在上、哪条在下:

import numpy as np
import matplotlib.pyplot as plt

# 1. 生成数据
x = np.linspace(0, 10, 500)      # 定义曲线的自变量范围 [0, 10]
y1 = np.sin(x)                   # 曲线1: sin(x)
y2 = np.cos(x/2) / 2             # 曲线2: cos(x/2)/2

# 2. 绘制曲线
plt.figure(figsize=(6,4))
plt.plot(x, y1, label='sin(x)', color='orange')
plt.plot(x, y2, label='cos(x/2)/2', color='orangered')

# 3. 填充两曲线之间的区域
# 使用 where 参数区分区域:y1>=y2 时填充蓝色(y1在上方),否则填充绿色(y2在上方)
plt.fill_between(x, y1, y2, where=y1 >= y2, color='lightblue', alpha=0.5, label='sin(x) above')
plt.fill_between(x, y1, y2, where=y1 <  y2, color='lightgreen', alpha=0.5, label='cos(x/2)/2 above')

plt.xlabel('x')
plt.ylabel('y')
plt.title('Fill Between sin(x) and cos(x/2)/2')
plt.legend(loc='upper right')
plt.show()

在这段代码中,我们通过两次调用 plt.fill_between,分别填充了 y1 在上方和 y2 在上方这两种情况对应的区域:当 y1 >= y2 时填充淡蓝色,当 y1 < y2 时填充淡绿色。where 参数接受一个布尔数组,用于指示在哪些 x 范围满足条件,从而只在满足条件的区间进行填充。我们还设置了 alpha=0.5 使填充颜色半透明,这样既能突出显示填充区域,又不至于完全遮盖住网格线或下面的曲线。【小贴士】plt.fill_between 默认对整个区域填充颜色,如果不使用 where,当两曲线交叉时填充区域会贯通所有x范围。合理使用 where 可以让我们对不同情况的区域使用不同颜色,更加清晰地表达上下边界关系。此外,如果希望在曲线交叉处精确地分割填充区域,可以将 interpolate=True 参数设为 True,以确保在交点处颜色正确衔接。

image-20250424211723235

图4.2:使用 plt.fill_between 绘制两条曲线之间的填充图示例。黄色曲线为 y=sin(x)y=\sin(x),橙红色曲线为 y=cos(x/2)/2y=\cos(x/2)/2。淡蓝色区域表示在该区域内 sin(x)\sin(x) 曲线高于 cos(x/2)/2\cos(x/2)/2 曲线;淡绿色区域表示 cos(x/2)/2\cos(x/2)/2 高于 sin(x)\sin(x) 的部分。通过这种颜色区分,我们清晰地看出两条曲线的上下包络情况:例如在 x[0,2]x\approx [0,2] 区间内橙红色曲线在上(绿色填充区),而在 x[2,7]x\approx [2,7] 区间内黄色曲线在上(蓝色填充区)。填充图有效地展示了两曲线之间的差异区域和边界关系。

填充图在实际工程中有多种用途:

  • 当我们有一组数据的上下限曲线时,可以使用填充图将上下边界之间的区域涂色,直观表示出可能的变化范围。例如,传感器读数的最大值-最小值区间、算法预测的置信区间带等。
  • 在比较两条曲线时,填充它们之间的区域可以突出显示它们的差异。正如上例所示,不同颜色的填充清楚地告诉读者在哪些区间哪条曲线占据更高的值。
  • 可以用于表示不等式解集。例如,对于满足 f(x)>g(x)f(x) > g(x) 的区域进行颜色填充,从而将满足条件的区域一目了然地标示出来。

注意: 填充图通常需要搭配透明度 (alpha) 调节效果。如果填充颜色不透明,可能会遮挡网格线、数据曲线等背景信息,使图表难以阅读。一般可将 alpha 设置为 0.2~0.5 之间,具体值需根据颜色深浅和背景对比度来调整。此外,若填充区域本身也需要在多个图层下展示(比如多个不同区间的填充),要注意图层次序,必要时可以通过前面提到的 zorder 参数控制填充层在曲线的下方或上方。

3 条形图(柱状图)

条形图(Bar Chart,也称柱状图)适合用于比较不同类别(离散分类)的数据大小。例如,不同产品的销量比较,不同时段的统计值比较等。Matplotlib 提供了 plt.bar 函数来绘制条形图,通过传入类别的位置和对应的值即可绘制出竖直的柱形。在实际应用中,我们经常会需要比较多组数据在相同类别下的差异,例如不同年份、不同类别下若干指标的并列比较。这时,可以采用分组条形图的方式:在同一类别位置上并排放置多个柱子,以不同颜色或纹理加以区分。

下面的示例展示了如何绘制一个分组条形图,比较两个系列的数据。我们以“苹果”和“橘子”的月销量为例进行对比。在同一个月份下,我们将苹果和橘子的销量柱状条并排绘制:

import numpy as np
import matplotlib.pyplot as plt

# 1. 定义类别(例如月份)及两组数据
categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May']   # 月份类别
apple_sales = np.array([30, 35, 40, 45, 50])       # 苹果销量(单位:吨)
orange_sales = np.array([25, 30, 35, 30, 40])      # 橘子销量(单位:吨)

# 2. 确定每组柱子的横坐标位置
x = np.arange(len(categories))    # 为每个类别生成索引 0,1,2,3,...
width = 0.4                       # 每个柱子的宽度
# 注意:width 的值决定了分组柱状图之间的间隔与重叠情况

# 3. 绘制分组柱状图
plt.figure(figsize=(6,4))
plt.bar(x - width/2, apple_sales, width, label='Apples', color='skyblue', edgecolor='black')
plt.bar(x + width/2, orange_sales, width, label='Oranges', color='orange', edgecolor='black')

# 4. 调整 x 轴刻度为类别名称,并添加标签和标题
plt.xticks(x, categories)              # 将x轴刻度位置设置为每个类别索引,并替换为月份标签
plt.ylabel('Sales (tons)')            # Y轴标签:销量(吨)
plt.title('Monthly Fruit Sales Comparison')  # 图表标题
plt.legend()                          # 显示图例(苹果/橘子)
plt.show()

在上面的代码中,我们首先使用 np.arange 生成了每个月份对应的基本 x 位置索引(例如 Jan 对应0,Feb对应1,以此类推)。然后,通过将苹果销量的柱子绘制在 x - width/2 位置、橘子销量的柱子绘制在 x + width/2 位置,实现了在每个类别处并排放置两根柱子。这样,宽度为 width 的两个柱子正好以类别中心为对称分布。我们选择 width = 0.4(意味着每个柱子占0.4的单位宽度),柱子之间会留有一定间隔,视觉上更清晰。plt.xticks 用于将数值型的 x 轴刻度替换为我们定义的月份字符串,使读者能够看懂每个柱子对应的类别。

image-20250424211849199

图4.3:分组条形图示例。每个月份有两根并列的柱子,蓝色表示苹果销量,橙色表示橘子销量。通过在绘制时对 x 坐标加减固定偏移,我们将两组柱状数据在水平方向并排展示,方便比较同一月份两种水果的销售情况。从图中可以直观地比较每个月苹果和橘子的销售差异,例如4月苹果销量高于橘子,而某些月份两者接近。柱状图适合用于这种离散类别下不同组数据的对比分析。

在使用条形图进行多组数据对比时,有以下技巧和注意事项:

  • 分组柱状图的偏移计算: 如果有更多组数据,需要将它们均匀地分布在该类别位置附近。比如有3组数据时,可以用 x - width, x, x + width 三个位置(或 x - width, x, x + width 的适当缩放)放置三根柱子。总之,偏移量乘以组数需要与柱宽相匹配,确保柱子既不重叠也不离散过远。一个通用做法是:对于第 i 组数据,用 x + (i - (n_groups-1)/2) * width 计算偏移,这样柱群以类别刻度为中心对称分布。
  • 颜色和图例: 为了便于区分不同数据系列,应该为每组柱子选择明显不同的颜色,并使用 label 参数添加图例说明颜色与系列的对应关系(如“蓝色代表苹果,橙色代表橘子”)。颜色选择上尽量保持一致的风格,比如都用饱和度相近但色调不同的颜色,避免过于花哨影响阅读。
  • 柱子样式: 可以通过参数控制柱子的外观,例如 edgecolor=‘black’ 给柱子加黑色边框,让不同柱子之间界线更清晰;alpha 参数可以调整透明度,如果柱子有重叠(如堆叠柱状图的情况)可以用透明度区分。还有一些高级用法,例如使用 hatch 参数填充柱子纹理,当打印成黑白文档时,不同纹理可以区分系列。
  • 适用场景:条形图适合用于离散分类的数据比较,例如不同部门的业绩、不同产品的销量、不同实验组的结果比较等。它直观地以柱子的长度表示数值大小,对比不同类别很方便。但需要注意,若类别过多,横轴会变得拥挤,可能需要旋转刻度标签或缩小柱宽。此外,条形图一般用于离散数据,不适合表示连续数据的分布(这时应使用直方图,见4.4节)。

Matplotlib 默认使用英文字体,直接使用中文标签(如上例的“苹果”、“橘子”)可能出现乱码或方块。如果希望在图表中正确显示中文,需要配置支持中文的字体。例如,可以在代码最前设置 plt.rcParams[‘font.sans-serif’]=[‘SimHei’] 来指定使用黑体字体(需确保系统已安装),或者使用 matplotlib.font_manager 找到可用的中文字体。配置好字体后,再设置中文标签,就能正常显示了。

4 直方图

直方图(Histogram)用于展示连续型数据的分布情况。它通过将数据按数值大小分组(binning),统计每个区间内的数据频数,然后以矩形条的高度表示频数多少。直方图能够帮助我们直观地观察数据分布的形状,例如是否对称、是否偏斜、有无多峰(bimodal),以及数据的离散或集中程度等。这对于数据分析非常重要,在工程上经常用来判断数据是否满足某种分布假设(如正态分布)、检测异常值、或为后续处理(如阈值选择、分箱决策)提供依据。

我们通过一个图像像素亮度分布的例子来说明直方图的绘制和参数调节。假设有一幅灰度图像,我们想了解其像素强度值的分布情况。下面的代码首先构造了一幅100×100的人工图像:左半部分像素偏暗(接近灰度值50),右半部分像素偏亮(接近灰度值200)。然后,我们计算所有像素的灰度值分布,并绘制直方图:

import numpy as np
import matplotlib.pyplot as plt

# 1. 构造示例灰度图像(左半部分暗,右半部分亮)
left_half = np.clip(np.random.normal(50, 5, (100, 50)), 0, 255)    # 左侧像素值约 ~50±5
right_half = np.clip(np.random.normal(200, 5, (100, 50)), 0, 255)  # 右侧像素值约 ~200±5
image = np.concatenate([left_half, right_half], axis=1)            # 合并左右两半
image = image.astype(np.uint8)  # 转为0-255范围内的整数灰度值

# 2. 计算像素值并绘制直方图
pixels = image.flatten()        # 将二维图像展开为一维像素值数组
plt.figure(figsize=(5,4))
plt.hist(pixels, bins=50, color='lightgray', edgecolor='black')
plt.xlabel('Pixel Intensity')   # X轴:像素强度值
plt.ylabel('Frequency')         # Y轴:频数(出现次数)
plt.title('Image Pixel Intensity Distribution')
plt.show()

代码解释:我们使用 np.random.normal 分别生成了左、右两半区域的像素值,left_half 以均值50、标准差5生成(偏暗),right_half 以均值200、标准差5生成(偏亮),并用 np.clip 确保值在0-255有效灰度范围内。合并成完整图像后,使用 image.flatten() 获取所有像素的值列表,然后调用 plt.hist 绘制直方图。其中 bins=50 将像素值范围分成50个区间(桶),每个矩形条表示一个区间内的像素数量频数。我们将柱子填充色设为浅灰色,并使用 edgecolor=‘black’ 给每个柱加黑色边框,以便在相邻柱子紧贴时仍能分辨边界。

image-20250424212004757

图4.4:图像像素亮度值的直方图。可以看到直方图呈现出双峰特征:一处峰值在大约50附近(对应图像中偏暗的左半部分像素强度),另一处峰值在大约200附近(对应偏亮的右半部分)。Y轴上的高度表示拥有该灰度值的像素数量。通过直方图,我们能够判断出这幅图像包含两种主要亮度区域,而且亮度分布相对集中在这两个峰值附近。这对于图像分析很有用,例如我们可以选择在灰度值约125附近作为阈值,将图像分为亮暗两部分。

在绘制和解释直方图时,需要注意以下事项:

  • 选择合适的分箱数量 (bins): bins 决定了将数据划分为多少个区间。分箱太少可能会掩盖数据分布的细节,而分箱过多则会使直方图显得过于毛糙难以看清趋势。一般可以根据数据量和分布情况调整 bins(默认是10);也可使用诸如 ‘auto’、‘fd’ 等规则让 Matplotlib 自动决定分箱数。 可以多尝试几个不同的分箱数,选择能够清晰反映分布特征的那一个。同时确保所选的 bins 覆盖了数据的完整范围,必要时可以用 range=(min, max) 明确指定区间范围。
  • 颜色与边框:为了美观和清晰度,直方图的柱子颜色通常使用单一的浅色填充(如上例的浅灰)且配合深色边框,这样既能反映频数高低,又不会因为颜色过多分散注意力。edgecolor=‘black’ 或 edgecolor=‘white’ 是常见选择,看背景配色而定。如果背景是白色,黑色边框能清楚界定柱子边缘;如果背景是深色,则可以用白色边框。
  • 频数还是密度:默认情况下,plt.hist 绘制的是频数(每个区间的数据点个数)。如果需要绘制概率密度(使直方图面积为1),可以使用参数 density=True。在统计分析中查看分布形状时,有时使用密度更直观,可以方便地与概率密度函数进行比较。
  • 数据分布形状分析:绘制直方图后,我们可以根据其形状来判断数据分布的特征。例如,单峰且对称的直方图可能表示数据接近正态分布;若峰偏向一侧且尾部拖长,表示分布存在偏态(skewness);直方图的峰的尖锐或平坦程度反映了峰度(kurtosis)。在工程应用中,这些信息可以帮助我们选择合适的模型或变换。例如,发现数据右偏长尾,可以考虑对数据取对数以正态化;发现数据有双峰,可能意味着来自两种不同成分,需要分别分析。
  • 应用场景:直方图在各行业都有应用,如分析传感器读数的噪声分布、测量产品尺寸误差的频率、统计实验结果的分布等。以图像处理为例,直方图可以用于自动确定图像阈值(OTSU算法正是利用直方图的分布寻找最佳阈值),或均衡化图像(直方图均衡也是基于亮度分布进行像素值映射)。通过直方图了解数据的分布形态,往往是数据分析和特征工程的第一步。

最后需要指出的是,条形图(柱状图)和直方图虽然外观相似,但用途不同:条形图针对分类数据,每根柱子独立表示一个类别的数值;直方图针对连续数据,每根柱子表示一个区间的频数。绘制直方图前通常需要有大量样本数据,否则如果数据太少,直方图的形状可能没有意义。总之,合理地使用直方图能帮助我们深入理解数据,为后续的数据处理和建模奠定基础。

Ge Yuxu • AI & Engineering

脱敏说明:本文所有出现的表名、字段名、接口地址、变量名、IP地址及示例数据等均非真实,仅用于阐述技术思路与实现步骤,示例代码亦非公司真实代码。示例方案亦非公司真实完整方案,仅为本人记忆总结,用于技术学习探讨。
    • 文中所示任何标识符并不对应实际生产环境中的名称或编号。
    • 示例 SQL、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

Data Desensitization Notice: All table names, field names, API endpoints, variable names, IP addresses, and sample data appearing in this article are fictitious and intended solely to illustrate technical concepts and implementation steps. The sample code is not actual company code. The proposed solutions are not complete or actual company solutions but are summarized from the author's memory for technical learning and discussion.
    • Any identifiers shown in the text do not correspond to names or numbers in any actual production environment.
    • Sample SQL, scripts, code, and data are for demonstration purposes only, do not contain real business data, and lack the full context required for direct execution or reproduction.
    • Readers who wish to reference the solutions in this article for actual projects should adapt them to their own business scenarios and data security standards, using configurations that comply with internal naming and access control policies.

版权声明:本文版权归原作者所有,未经作者事先书面许可,任何单位或个人不得以任何方式复制、转载、摘编或用于商业用途。
    • 若需非商业性引用或转载本文内容,请务必注明出处并保持内容完整。
    • 对因商业使用、篡改或不当引用本文内容所产生的法律纠纷,作者保留追究法律责任的权利。

Copyright Notice: The copyright of this article belongs to the original author. Without prior written permission from the author, no entity or individual may copy, reproduce, excerpt, or use it for commercial purposes in any way.
    • For non-commercial citation or reproduction of this content, attribution must be given, and the integrity of the content must be maintained.
    • The author reserves the right to pursue legal action against any legal disputes arising from the commercial use, alteration, or improper citation of this article's content.

Copyright © 1989–Present Ge Yuxu. All Rights Reserved.