Skip to content

Matplotlib:从绘图基础到工程实践 ​

Matplotlib 的难点不是记住多少绘图函数,而是把数据含义、图形对象、视觉编码和输出环境连接起来。本手册先建立对象模型,再学习图表表达,最后处理复用、批处理和交互。

学习路线与分篇目录 ​

text
Data -> Figure / Axes -> Chart -> Axis / Style -> Layout -> Export
                             |                              |
                             +-> Statistics / Animation     +-> Reuse / Batch

首次学习按章节顺序阅读;制作报告优先完成第 1~8 章,再看第 14 章;服务器出图重点看第 8、13、15 章。每个可执行 Python 示例均自带导入和数据,不需要共享前文变量;含随机数据时使用固定种子的局部随机生成器。

第一篇:入门——先理解一张图如何产生 ​

第二篇:基础——让数据得到正确表达 ​

第三篇:表达与布局——组织并交付图形 ​

第四篇:进阶——统计表达、投影与交互 ​

第五篇:工程——从一次绘图到可维护代码 ​

第六篇:实战与速查——形成独立交付能力 ​

1.1 适用范围 ​

Matplotlib 是以 Python 为接口的绘图库,适合探索分析、静态报表、论文插图和服务端图片生成。它提供精细的布局、刻度、文本和导出控制,也能完成基础动画与三维可视化。

交互式 Web 仪表盘、复杂三维场景或高吞吐实时流不是其主要优势;先确认是否需要静态交付,再选择 Matplotlib、Web 图表库或专门的三维工具。

1.2 推荐基线与安装 ​

本文以 Python 3.11+、Matplotlib 3.9+ 为推荐基线,采用兼容 Matplotlib 3.9 的主要写法,不承诺所有未来版本均无需修改。NumPy 是基础示例依赖,其他库按章节安装。

依赖本文建议范围用途
Matplotlib3.9+绘图;箱线图使用新版参数
NumPy1.26+数组、采样、随机数据
Pandas2.2+,可选表格与日期数据整理
Seaborn0.13+,可选统计绘图与 KDE
Pillow兼容当前 Python 的版本GIF、JPEG 等图像写出
ffmpeg可选外部程序MP4 编码,不是仅装同名 Python 包
ipympl可选Jupyter 中的 widget 交互后端
bash
python -m venv .venv
# 激活虚拟环境后,确认 python 指向该环境
python -m pip install "matplotlib>=3.9" "numpy>=1.26"
# 运行生态协作或动画导出示例时再安装
python -m pip install "pandas>=2.2" "seaborn>=0.13" scipy pillow
python -m pip check

Windows 命令提示符可执行 .venv\Scripts\activate.bat 激活;其他终端使用对应的环境激活方式。若需要镜像源,在安装命令后增加 -i https://pypi.tuna.tsinghua.edu.cn/simple,无需更改全局配置。

版本下限不等于“全部最新版都能安装”

Python 版本、操作系统和二进制包支持共同决定可安装版本。安装器会依据包的 Python 兼容声明筛选发行版;Python 3.11 也不保证支持未来所有最新版。项目应锁定实际验证过的完整依赖组合,而不是把这里的开放下限当作锁文件。

1.3 验证环境,不把显示问题当安装失败 ​

python
import sys
import matplotlib
import numpy as np

print("Python:", sys.version)
print("Matplotlib:", matplotlib.__version__)
print("NumPy:", np.__version__)
print("Backend:", matplotlib.get_backend())

桌面脚本需要可用的 GUI 后端;Notebook 可以内嵌展示静态图;无界面服务器通常只保存图片。导入成功和窗口能够弹出是两项不同的验证。

先分清“包含关系”和“类型关系”

Figure 是整张图,Axes 是绘图区而非单根坐标轴;Axis 管理单个方向的刻度、刻度标签和轴标签。Artist 是可参与渲染的对象基类,Figure、Axes、Axis 本身也都是 Artist,而不是四个平行层级。

text
Figure (Artist)
+-- Axes (Artist)
|   +-- XAxis / YAxis (Axis -> Artist)
|   +-- Line2D / Collection / Patch / Text (Artist)
|   +-- Legend (Artist)
+-- Axes ...
+-- Figure-level Text / Legend (Artist)

这是常见二维图的简化包含关系,不是完整继承树;三维绘图区还有 ZAxis,colorbar 通常也会占用一个独立 Axes。

接口调用方式适用场景与风险
pyplot 状态式plt.plot(...) 操作当前绘图区临时探索方便,多图时容易画到错误对象
显式对象式ax.plot(...)、fig.savefig(...)适合函数复用、组合布局、服务端出图

本文使用 pyplot 创建图形和控制显示,但把绘图、布局和保存交给显式对象。ax.plot() 返回 Line2D 列表,ax.scatter() 返回集合对象;保存返回值就能在后续注释、图例或动画中操作它们。

2.1 子图返回值的形状 ​

plt.subplots() 返回 (fig, axes),默认 squeeze=True 会压缩多余维度:

行列数第二个返回值
1 × 1一个 Axes,不可直接下标索引
1 × N 或 N × 1,N 大于 1一维数组
M × N,M、N 均大于 1二维数组
任意行列数且 squeeze=False始终为二维数组,例如 axes[0, 0]

闭环顺序是:准备数据 → 创建图形 → 添加 Artist → 配置表达 → 保存 → 显示 → 关闭。创建图形的调用方负责最终关闭,复用绘图函数不应自行关闭外部传入的画布。

python
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(1, 8)
y = np.array([10, 12, 9, 14, 18, 17, 20])
fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
try:
    line, = ax.plot(x, y, marker="o", linewidth=2, label="Sales")
    ax.set(title="Weekly Sales", xlabel="Day", ylabel="Units")
    ax.grid(axis="y", alpha=0.25)
    ax.legend()
    fig.savefig("weekly_sales.png", dpi=150)
    plt.show()
finally:
    plt.close(fig)

预期得到一条带图例的每日销量曲线,并在当前工作目录生成图片。这里 figsize 的单位是英寸,7 × 4 英寸以 150 dpi 保存时约为 1050 × 600 像素;若使用紧边界裁剪,最终像素尺寸会变化。

plt.show() 在普通桌面脚本中通常阻塞到窗口关闭。Notebook 或非阻塞模式下,应在交互结束后再关闭对象;不要把这个脚本末尾的清理时机机械搬到长驻界面中。

4.1 先问问题,再选图 ​

需要回答的问题图形表达边界
随时间或有序位置如何变化?折线图连接暗示顺序,绘制前按横坐标排序
两个数值变量有何关系?散点图相关不代表因果,点云也可能是非线性关系
不同类别谁高谁低?柱状图通常从零开始,避免夸大柱高差异
一个数值变量如何分布?直方图分箱改变外观,钟形不等于已证明正态
少量类别如何组成整体?饼图类别多或差异小时改用条形图
多组数据的中位数和离散程度?箱线图必须结合样本量及分布形状解释
矩阵各位置的数值是什么?热力图必须解释行列语义、色标范围
同尺度指标的轮廓如何?雷达图轴顺序和量纲会影响视觉结论
两个自变量决定怎样的曲面?三维曲面图透视和遮挡可能隐藏信息

后五类在第 9、10 章展开。以下四种基础图各保留一个示例;组合图要等第 7 章建立布局知识后再使用。

4.2 折线:趋势与采样顺序 ​

第 3 章就是完整折线模板。linewidth 使用点单位,markersize 是线图标记的线性尺寸;稀疏采样可以保留标记,密集曲线不必为每个点画标记。缺失值不要随意补零,必要时用 NaN 让线断开。

4.3 散点:位置、面积和颜色 ​

python
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
x = rng.normal(50, 10, 120)
y = 0.7 * x + rng.normal(0, 5, x.size)
fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
points = ax.scatter(x, y, s=36, c=y, cmap="viridis", alpha=0.7)
ax.set(title="Relationship", xlabel="Input", ylabel="Response")
fig.colorbar(points, ax=ax, label="Response")
plt.show()
plt.close(fig)

s 表示标记面积,单位为 points²,不是半径;面积翻倍不意味着直径翻倍。若编码业务规模,应提供大小图例。c 在这里传数值数组,配合 cmap 映射颜色;仅指定统一颜色时使用 color 更清晰。颜色归一化见第 9 章。

4.4 柱状:离散类别之间的比较 ​

python
import matplotlib.pyplot as plt

categories = ["A", "B", "C", "D"]
values = [23, 35, 30, 28]
fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
bars = ax.bar(categories, values, width=0.65, color="steelblue")
ax.bar_label(bars, fmt="%.0f", padding=3)
ax.set(title="Category Comparison", xlabel="Category", ylabel="Units")
ax.set_ylim(0, 42)
plt.show()
plt.close(fig)

bar() 返回柱形容器,可交给 bar_label();增加上方空间避免标签被裁掉。分组柱需要横坐标偏移,堆叠柱需要明确累计基线。长类别名称优先考虑水平条形图 barh(),而不是无限旋转标签。

4.5 直方:从原始观测到区间计数 ​

python
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
samples = rng.normal(loc=60, scale=10, size=500)
fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
counts, edges, patches = ax.hist(samples, bins=20, edgecolor="white")
ax.set(title="Sample Distribution", xlabel="Value", ylabel="Count")
plt.show()
plt.close(fig)

直方图的输入是观测数据,柱状图的输入通常是已汇总类别值。bins 可为分箱数量或边界数组;比较两组分布时应使用相同分箱边界。density=True 使总面积为 1,而不是让每根柱高等于区间概率,密度也可能大于 1。

5.1 范围、定位器与格式器 ​

Axes 决定显示范围和尺度;Axis 上的 Locator 决定“刻度放哪里”,Formatter 决定“显示成什么文本”。标签拥挤应先减少刻度,再考虑旋转。

任务对象式入口注意事项
显示范围ax.set_xlim()、ax.set_ylim()裁剪范围可能隐藏数据,应说明原因
固定类别刻度ax.set_xticks(positions, labels)位置和标签同时设置,避免仅改标签
刻度样式ax.tick_params()控制旋转、字号和长度
数值格式ax.yaxis.set_major_formatter()用 Formatter 控制单位和精度
相等坐标比例ax.set_aspect("equal")几何图中一个单位应有相同长度

5.2 日期与对数轴的独立示例 ​

python
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np

dates = [datetime(2026, 1, 1) + timedelta(days=i) for i in range(30)]
values = np.geomspace(1, 1000, len(dates))
fig, ax = plt.subplots(figsize=(8, 4), layout="constrained")
ax.plot(dates, values, label="Growth")
locator = mdates.AutoDateLocator(minticks=3, maxticks=7)
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
ax.set_yscale("log")
ax.set(title="Growth on Log Scale", xlabel="Date", ylabel="Value (log scale)")
ax.grid(which="major", alpha=0.25)
ax.legend()
plt.show()
plt.close(fig)

日期请传 datetime、NumPy datetime64 或 Pandas 日期值,不要把日期字符串当作连续时间;字符串会成为等间距类别,缺失日期的间隔也会丢失。涉及跨时区数据时先统一时区语义。

对数轴上的等间距表示相同比例,不是相同差值。普通 log 尺度要求正值,零和负数不能按普通对数表示;先确认数据语义,再选择屏蔽、线性轴或适当设置线性阈值的 symlog,不要偷偷加常数改变业务含义。

6.1 样式应服务于比较 ​

优先用位置表达大小,用颜色区分类别;同时用线型或标记辅助区分,避免只有颜色传达信息。字体大小、线宽和标记尺寸使用点单位,输出图的物理尺寸决定它们在报告中是否可读。

以下示例把线型、图例、峰值注释和简化边框放在同一张图中,不再为每项样式重复一份模板。

python
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(1, 6)
y = np.array([2, 4, 3, 7, 6])
fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
ax.plot(x, y, color="teal", linestyle="--", marker="o",
        linewidth=2, markersize=6, label="Observed")
ax.axhline(5, color="gray", linestyle=":", label="Target")
peak = int(np.argmax(y))
ax.annotate("Peak", xy=(x[peak], y[peak]), xytext=(-35, 18),
            textcoords="offset points", arrowprops={"arrowstyle": "->"})
ax.text(0.02, 0.95, "Sample data", transform=ax.transAxes, va="top")
ax.set(title="Annotated Trend", xlabel="Day", ylabel="Value", ylim=(0, 9))
ax.spines[["top", "right"]].set_visible(False)
ax.grid(axis="y", alpha=0.25)
ax.legend(loc="lower right", frameon=False)
plt.show()
plt.close(fig)

annotate() 的 xy 默认是数据坐标;示例文字使用相对目标点的点单位偏移,缩放后仍保持间距。ax.transAxes 则把绘图区左下角映射到 (0, 0),右上角映射到 (1, 1),适合角落说明。

图例解释系列,colorbar 解释连续数值,两者不能互相替代。给绘图对象设置 label 后调用 ax.legend();标签以下划线开头的对象默认不进入自动图例。

6.2 中文字体:检测再设置,不假定系统已安装 ​

python
import matplotlib.pyplot as plt
from matplotlib import font_manager

candidates = ["Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", "SimHei"]
available = {font.name for font in font_manager.fontManager.ttflist}
chosen = next((name for name in candidates if name in available), None)
if chosen is None:
    raise RuntimeError("请先安装可用中文字体,或通过 font_manager.addfont 加载字体文件")

with plt.rc_context({"font.family": [chosen], "font.size": 11}):
    fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
    ax.plot([1, 2, 3], [-1, 2, 1], label="销量变化")
    ax.set(title="中文标签示例", xlabel="日期序号", ylabel="变化量")
    ax.legend()
    plt.show()
    plt.close(fig)

字体名称被发现不代表每个字形都覆盖;仍须检查输出。通过字体文件加载时,使用实际存在且许可允许的文件路径,并在每个进程中注册;不要写死其他机器的字体路径。

负号与字体是两个问题

默认使用 Unicode 负号。仅在所用字体缺少该字形时,才考虑在局部样式中设置 axes.unicode_minus=False,改用 ASCII 连字符;它不能解决中文缺字。rc_context 退出后恢复样式,工程化使用见第 13 章。

7.1 规则网格与共享轴 ​

python
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 200)
fig, axes = plt.subplots(2, 1, sharex=True, sharey=True, squeeze=False,
                         figsize=(7, 5), layout="constrained")
for ax, y, name in zip(axes[:, 0], [np.sin(x), np.cos(x)], ["sin", "cos"]):
    ax.plot(x, y, label=name)
    ax.set_ylabel("Amplitude")
    ax.legend()
    ax.grid(alpha=0.2)
axes[1, 0].set_xlabel("Phase (rad)")
plt.show()
plt.close(fig)

共享轴同步范围、尺度和刻度定位/格式配置,不只是隐藏重复标签。只在数据单位和比较目标一致时共享;标签通常仅保留外侧,可通过 tick_params(labelbottom=True) 恢复某个子图的显示。共享设置一般应在创建子图时决定,之后不能简单取消共享关系。

7.2 不规则布局与嵌套网格 ​

python
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(1, 8)
y = np.array([10, 12, 9, 14, 18, 17, 20])
fig = plt.figure(figsize=(9, 6), layout="constrained")
grid = fig.add_gridspec(2, 3, height_ratios=[2, 1])
trend_ax = fig.add_subplot(grid[0, :])
bar_ax = fig.add_subplot(grid[1, 0])
scatter_ax = fig.add_subplot(grid[1, 1:])
trend_ax.plot(x, y, marker="o")
trend_ax.set(title="Daily Trend", xlabel="Day", ylabel="Units")
bar_ax.bar(["First", "Last"], [y[0], y[-1]])
bar_ax.set(title="Endpoints", ylabel="Units")
scatter_ax.scatter(x, y, color="teal")
scatter_ax.set(title="Observations", xlabel="Day", ylabel="Units")
plt.show()
plt.close(fig)

GridSpec 描述槽位,add_subplot() 才创建绘图区。需要局部更细网格时,可对单个槽位调用 subgridspec();只有确实需要不同区域布局规则时才嵌套,避免布局层级难以维护。

默认推荐 layout="constrained",便于处理标题、标签和色条。fig.tight_layout() 是另一种布局策略,不要与 constrained layout 混用;需要完全手动控制时不启用自动布局,再调用 fig.subplots_adjust(wspace=..., hspace=...)。

8.1 输出格式怎么选 ​

格式用途设计取舍
PNG屏幕、报告、透明背景无损栅格图,像素数由英寸和 dpi 决定
JPEG必须使用照片类压缩格式时有损压缩、不支持透明,线条和文字可能有伪影
PDF / SVG打印、论文、可缩放图形线条和文字通常为矢量,图像或栅格化对象仍是位图
python
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
try:
    ax.plot([0, 1, 2], [1, 3, 2], linewidth=2)
    ax.set(title="Export Formats", xlabel="Time (s)", ylabel="Value")
    fig.savefig("export.png", dpi=200, bbox_inches="tight", transparent=True)
    fig.savefig("export.jpg", dpi=200, facecolor="white",
                pil_kwargs={"quality": 95})
    fig.savefig("export.pdf")
    fig.savefig("export.svg")
    plt.show()
finally:
    plt.close(fig)

JPEG 质量参数应通过 pil_kwargs 传给 Pillow,而不是直接向 savefig() 传 quality。文件后缀通常决定格式;显式指定 format 时,务必让后缀与实际编码一致。

8.2 像素、版面和关闭责任 ​

  • figsize=(6, 4) 是 6 × 4 英寸,不是厘米或像素;厘米除以 2.54 转换为英寸。
  • dpi=300 表示每英寸 300 像素,只对栅格输出及矢量文件中的栅格化部分有直接作用,不能修复原始低分辨率图像。
  • bbox_inches="tight" 会按内容裁剪,可能改变最终尺寸;要求精确像素时应固定尺寸、不用紧裁剪并检查边界。
  • 论文不是“dpi 越高越好”:先满足版面宽度、字号、配色和期刊格式要求,再选择输出分辨率。

不依赖全局“当前图形”保存

阻塞式 show() 结束后,图形通常已从 pyplot 管理中注销;再调用状态式保存可能新建空图。保留 Figure 引用通常仍能显式保存,但最稳妥的示例顺序是 fig.savefig() 在 plt.show() 前,完成后 plt.close(fig)。关闭解除 pyplot 管理和窗口资源,不会自动删除仍被变量引用的对象。

9.1 饼图:少量类别的整体构成 ​

python
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(5, 5), layout="constrained")
ax.pie([35, 25, 20, 20], labels=["A", "B", "C", "D"],
       autopct="%1.1f%%", startangle=90)
ax.set(title="Composition", aspect="equal")
plt.show()
plt.close(fig)

输入应非负且总和大于零;默认会归一化。类别必须构成同一整体,百分比不能代替绝对规模。类别多、差异小时优先条形图,避免用立体效果扭曲面积。

9.2 案例二:成绩分布与箱线图 ​

python
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(7)
scores = 100 * rng.beta(8, 3, size=300)
fig, axes = plt.subplots(1, 2, figsize=(9, 4), layout="constrained")
try:
    axes[0].hist(scores, bins=np.arange(0, 101, 10), edgecolor="white")
    axes[0].set(title="Score Distribution", xlabel="Score", ylabel="Count", xlim=(0, 100))
    axes[1].boxplot(scores, tick_labels=["Class A"], whis=1.5, showfliers=True)
    axes[1].set(title="Score Summary", ylabel="Score", ylim=(0, 100))
    fig.savefig("case2_score_distribution.pdf")
    plt.show()
finally:
    plt.close(fig)

这是有界模拟成绩,不是实测数据。箱体从第一四分位数 Q1 到第三四分位数 Q3,中线为中位数,IQR = Q3 − Q1。默认须延伸到仍处于 Q1 − 1.5 IQR 与 Q3 + 1.5 IQR 内的最远观测值,而不是直接画到理论阈值。

须外点(fliers)只表示超出该统计规则,不等于错误数据;隐藏它们不会删除原始观测。箱线图也不能充分展示多峰分布,应结合直方图与样本量。

tick_labels 从 Matplotlib 3.9 引入,替代旧的 labels;本文使用默认竖直方向,避免依赖已进入弃用流程的 vert。新参数 orientation 从 3.10 才提供,不可无条件用于 3.9 基线。

9.3 颜色:先确定数值语义与映射范围 ​

数据语义色图归一化选择
从低到高的连续大小顺序色图,如 viridis默认线性 Normalize
围绕有意义中心的偏离发散色图,如 RdBu_r对称范围,或 TwoSlopeNorm 指定中心
无顺序的类别分类色图,如 tab10显式类别到颜色映射,不暗示连续大小
正数跨多个数量级顺序色图LogNorm,数据和有效上下界必须为正

不同图之间要比较数值,应共享同一个范围或归一化对象;各自自动缩放会让相同颜色代表不同数值。使用 TwoSlopeNorm(vmin=-2, vcenter=0, vmax=8) 时,两侧映射斜率不同,应解释中心为何有意义。传 Normalize 实例时在实例中设置范围,不另传 vmin/vmax。

LogNorm 不能把零和负值解释成普通对数;按数据语义屏蔽无效值或选择其他归一化方式,不要为了上色随意加常数。色条应由实际绘图返回的 mappable 创建。

9.4 案例三:相关性热力图 ​

python
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(9)
base = rng.normal(size=300)
data = np.column_stack([base, 0.8 * base + rng.normal(0, 0.4, 300),
                        -base + rng.normal(0, 0.3, 300), rng.normal(size=300)])
corr = np.corrcoef(data, rowvar=False)
labels = ["A", "B", "C", "D"]
fig, ax = plt.subplots(figsize=(6, 5), layout="constrained")
try:
    image = ax.imshow(corr, cmap="RdBu_r", vmin=-1, vmax=1)
    ax.set_xticks(range(4), labels)
    ax.set_yticks(range(4), labels)
    ax.set_title("Pearson Correlation")
    for row in range(4):
        for col in range(4):
            ax.text(col, row, f"{corr[row, col]:.2f}", ha="center", va="center",
                    color="white" if abs(corr[row, col]) > 0.6 else "black")
    fig.colorbar(image, ax=ax, label="Correlation")
    fig.savefig("case3_corr_heatmap.png", dpi=200)
    plt.show()
finally:
    plt.close(fig)

行是 300 个观测,列是 4 个变量,因此指定 rowvar=False,得到 4 × 4 矩阵。缺失值和常量列可能产生 NaN,不能当作零相关;实际数据须先确定缺失处理方式。Pearson 描述线性关系,接近零不保证独立,强相关也不证明因果。

10.1 雷达图:先统一尺度与方向 ​

python
import matplotlib.pyplot as plt
import numpy as np

labels = ["Speed", "Quality", "Coverage", "Stability", "Usability"]
scores = np.array([80, 90, 65, 85, 75])
angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False)
closed_angles = np.append(angles, angles[0])
closed_scores = np.append(scores, scores[0])
fig, ax = plt.subplots(figsize=(6, 5), subplot_kw={"projection": "polar"},
                       layout="constrained")
ax.plot(closed_angles, closed_scores, marker="o", label="Product A")
ax.fill(closed_angles, closed_scores, alpha=0.15)
ax.set_xticks(angles, labels)
ax.set_ylim(0, 100)
ax.set_title("Comparable Scores", pad=25)
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.15))
plt.show()
plt.close(fig)

指标均为 0 ~ 100 分且越大越好,重复第一个点是为了闭合轮廓。原始金额、耗时、准确率不能直接放在同一径向尺度上;归一化要明确基准,并将“越小越好”的指标转换为一致方向。轴顺序影响面积与形状,不要把多边形面积当作客观综合评分;精确比较优先点图或条形图。

10.2 三维曲面:投影不等于更准确 ​

python
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 60)
y = np.linspace(-3, 3, 60)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
fig = plt.figure(figsize=(7, 5), layout="constrained")
ax = fig.add_subplot(111, projection="3d")
surface = ax.plot_surface(X, Y, Z, cmap="viridis", rcount=60, ccount=60)
ax.set(title="Radial Surface", xlabel="X", ylabel="Y", zlabel="Z")
ax.view_init(elev=25, azim=35)
fig.colorbar(surface, ax=ax, shrink=0.65, label="Z")
plt.show()
plt.close(fig)

X、Y、Z 形状必须一致。三维投影有遮挡和透视变形,旋转视角可能改变判断,必要时补充等高线、二维热力图或切片。提高网格分辨率会增加内存和渲染开销;plot_surface 的采样上限也可能使输入网格被降采样。

11.1 案例五:更新现有 Artist 的模拟监控 ​

python
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation, PillowWriter, FFMpegWriter

x = np.linspace(0, 2 * np.pi, 200)
fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
line, = ax.plot(x, np.zeros_like(x))
status = ax.text(0.02, 0.95, "", transform=ax.transAxes, va="top")
ax.set(title="Simulated Monitor", xlabel="Phase", ylabel="Value",
       xlim=(0, 2 * np.pi), ylim=(-1.2, 1.2))

def init():
    line.set_ydata(np.zeros_like(x))
    status.set_text("Frame 0")
    return line, status

def update(frame):
    line.set_ydata(np.sin(x + frame / 10))
    status.set_text(f"Frame {frame}")
    return line, status

ani = FuncAnimation(fig, update, init_func=init, frames=60, interval=80,
                    blit=fig.canvas.supports_blit, cache_frame_data=False)
# 可选导出,必须在显示前进行;按需取消注释
# ani.save("case5_monitor.gif", writer=PillowWriter(fps=12))
# ani.save("case5_monitor.mp4", writer=FFMpegWriter(fps=12))
plt.show()
plt.close(fig)

保留 ani 引用直到播放或保存完成,否则动画可能被回收。启用 blit 时,初始化和更新函数必须返回所有改变的 Artist;后端不支持则禁用。blit 缓存背景,坐标范围或布局变化时不一定适用。interval 是请求的刷新间隔,不是实时保证;编码文件的播放速度由 fps 决定。

GIF 需要 Pillow,MP4 需要可被找到的外部 ffmpeg 和可用编码器;安装同名 Python 包不等于安装外部程序。Notebook 静态内嵌后端不播放 GUI 动画,可选择 ipympl 的 widget 后端,或通过 ani.to_jshtml() 生成 HTML 并显示。不要在非阻塞播放刚开始时关闭图形。

模拟波形不是真实数据流

此例只有有限帧的函数计算。真实监控需另行设计采集线程或进程、有限缓冲区、时间戳、降采样和背压;GUI 更新应留在 GUI 事件线程,不能在回调中长时间阻塞网络或磁盘。

11.2 事件连接与释放 ​

python
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([0, 1, 2], [1, 3, 2])
label = ax.text(0.02, 0.95, "Click inside axes", transform=ax.transAxes)

def on_click(event):
    if event.inaxes is not ax or event.xdata is None or event.ydata is None:
        return
    label.set_text(f"x={event.xdata:.2f}, y={event.ydata:.2f}")
    fig.canvas.draw_idle()

connection = fig.canvas.mpl_connect("button_press_event", on_click)
try:
    plt.show()
finally:
    fig.canvas.mpl_disconnect(connection)
    plt.close(fig)

draw_idle() 请求在事件循环空闲时重绘,避免每次事件强制同步渲染。离开绘图区时数据坐标可能为空,需要先判断。事件示例要求交互后端;长驻界面应在组件销毁时断开连接,而不是在非阻塞显示后立即断开。

12.1 数据整理与渲染分工 ​

Pandas 负责清洗、索引、分组与聚合,NumPy 负责数值运算,Matplotlib 管理图形对象和输出;Seaborn 在 Matplotlib 上提供更高层的统计表达。不要让绘图函数顺便修改原始数据。

python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

rng = np.random.default_rng(12)
df = pd.DataFrame({"value": rng.normal(0, 1, 300)})
bins = np.linspace(-4, 4, 25)
fig, axes = plt.subplots(1, 2, figsize=(9, 4), sharex=True, sharey=True,
                         layout="constrained")
df["value"].plot.hist(bins=bins, density=True, ax=axes[0], edgecolor="white")
sns.histplot(data=df, x="value", bins=bins, stat="density", kde=True, ax=axes[1])
for ax, title in zip(axes, ["Pandas Histogram", "Seaborn Histogram + KDE"]):
    ax.set(title=title, xlabel="Value", ylabel="Density")
plt.show()
plt.close(fig)

本例的 KDE 计算需要 SciPy,安装命令见第 1 章。统一分箱和密度统计后才有共同比较基础。KDE 是受带宽影响的平滑估计,不是观测真值;对非负或有界变量,曲线可能超出合法范围,应说明或调整估计方法。

12.2 选择可组合的接口 ​

  • Pandas 默认绘图后端下,给 plot() 传 ax,即可复用已有布局。
  • Seaborn 的 axes-level 函数,如 histplot()、scatterplot(),接收 ax,适合与 GridSpec 组合。
  • displot()、relplot() 等 figure-level 函数管理自己的图形,不应当作普通子图函数塞进外部 Axes。
  • 避免全局主题影响其他图形;可将 sns.axes_style("whitegrid") 返回的配置交给局部 rc_context。

13.1 接收 Axes,返回 Artist ​

可复用函数只把数据画到调用方提供的 Axes 上,并返回可继续修改的 Artist;不寻找“当前 Figure”,不自行显示、保存或关闭。输出位置、格式与生命周期属于调用方职责。

13.2 无界面批处理模板 ​

本例在新的 Python 进程运行;若 Notebook 已导入 pyplot,不要依赖后置切换后端。

python
import matplotlib
matplotlib.use("Agg")  # 必须先于 pyplot 导入
import matplotlib.pyplot as plt
import numpy as np
from pathlib import Path

def draw_trend(ax, x, y, *, label):
    x, y = np.asarray(x), np.asarray(y)
    if x.ndim != 1 or y.ndim != 1 or x.shape != y.shape:
        raise ValueError("x 和 y 必须是一维且长度相同")
    line, = ax.plot(x, y, label=label, linewidth=1.8)
    ax.set(xlabel="Step", ylabel="Value")
    ax.grid(alpha=0.2)
    return line

rng = np.random.default_rng(42)
output = Path("batch_output")
output.mkdir(parents=True, exist_ok=True)
x = np.arange(100)
with plt.rc_context({"font.family": "DejaVu Sans", "font.size": 10}):
    for index in range(3):
        y = rng.normal(loc=index, scale=0.5, size=x.size).cumsum()
        fig, ax = plt.subplots(figsize=(6, 4), layout="constrained")
        try:
            line = draw_trend(ax, x, y, label=f"Run {index}")
            line.set_alpha(0.85)
            ax.legend()
            fig.savefig(output / f"trend_{index}.png", dpi=150)
        finally:
            plt.close(fig)

Agg 是无 GUI 栅格渲染后端,不产生交互窗口。try/finally 确保保存失败也执行清理;文件名避免循环覆盖。服务接口也可输出到内存缓冲区。

rc_context 退出时恢复进入上下文前的样式配置,不是重置为出厂默认值;它不撤销已创建的 Artist,不恢复后端,也不是线程隔离机制。并行批处理优先使用独立进程和各自的图形,避免多线程共享 pyplot 状态。

13.3 性能优化与信息损失 ​

瓶颈可选措施代价与约束
密集折线和标记减少标记、聚合、路径简化检查是否丢失峰值与转折
海量散点使用二维分箱或 hexbin从单点改为密度表达,语义改变
SVG / PDF 太大密集对象设置 rasterized=True局部转为栅格,清晰度受 dpi 影响
动画卡顿更新已有 Artist、固定范围、按后端启用 blit背景改变时需重建缓存
批处理内存增长关闭图形、释放多余引用关闭窗口不等于删除所有对象
输出慢控制像素数、减少重复布局不能牺牲最终版面的可读性

降采样不是无损优化

固定每十点取一点可能漏掉尖峰、突变和周期信号。趋势可按时间分桶聚合,异常监控可保留每桶极值。标明已压缩数据,保留原始输入,并验证关键事件仍可见。

每类案例只保留一处完整实现,基础章节解释机制,本章组织交付验收。

案例完整实现学习目标与验收
一:销售与利润本章 14.1日期有序、利润计算正确、单位清楚
二:成绩分布第 9 章 9.2能解释分箱、中位数与须外点
三:相关性热力图第 9 章 9.44 × 4 对称矩阵,色标固定 −1 ~ 1
四:论文多子图本章 14.2同单位共享轴,最终尺寸字号可读
五:模拟监控第 11 章 11.1曲线与状态同步,不冒充真实数据流

14.1 案例一:销售与利润报告 ​

python
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np
import pandas as pd

rng = np.random.default_rng(1)
df = pd.DataFrame({
    "date": pd.date_range("2026-01-01", periods=30, freq="D"),
    "sales": rng.integers(80, 180, size=30),
    "cost": rng.integers(50, 120, size=30),
}).sort_values("date")
df["profit"] = df["sales"] - df["cost"]
fig, axes = plt.subplots(2, 1, sharex=True, figsize=(9, 6), layout="constrained")
try:
    df.plot(x="date", y=["sales", "cost"], ax=axes[0], x_compat=True)
    axes[0].set(title="Daily Sales and Cost", ylabel="Amount (CNY)")
    colors = np.where(df["profit"] >= 0, "seagreen", "tomato")
    axes[1].bar(df["date"], df["profit"], width=0.8, color=colors)
    axes[1].axhline(0, color="black", linewidth=0.8)
    axes[1].set(title="Profit", xlabel="Date", ylabel="Profit (CNY)")
    locator = mdates.AutoDateLocator(minticks=3, maxticks=7)
    axes[1].xaxis.set_major_locator(locator)
    axes[1].xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
    for ax in axes:
        ax.grid(axis="y", alpha=0.2)
    fig.savefig("case1_sales_report.png", dpi=200)
    plt.show()
finally:
    plt.close(fig)

这是合成数据。x_compat=True 使日期绘制与 Matplotlib 日期定位器兼容。一天多条记录时先确定是否按日求和,缺失日期不能自动当作零收入。正负利润由颜色和零基线两侧的位置共同表达。

14.2 案例四:论文级多子图对比 ​

使用合成信号练习排版,不代表真实实验结论;尺寸、字号与格式服从投稿要求。

python
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 300)
signals = [np.sin(x), np.cos(x), np.sin(2 * x), np.cos(2 * x)]
titles = ["(a) sin(x)", "(b) cos(x)", "(c) sin(2x)", "(d) cos(2x)"]
with plt.rc_context({"font.family": "DejaVu Sans", "font.size": 10}):
    fig, axes = plt.subplots(2, 2, figsize=(7, 5), sharex=True, sharey=True,
                             squeeze=False, layout="constrained")
    try:
        for ax, y, title in zip(axes.flat, signals, titles):
            ax.plot(x, y, color="black", linewidth=1.5)
            ax.set(title=title, ylim=(-1.2, 1.2))
            ax.grid(alpha=0.2)
        fig.supxlabel("Phase (rad)")
        fig.supylabel("Amplitude")
        fig.savefig("case4_paper_subplots.svg")
        fig.savefig("case4_paper_subplots.png", dpi=300)
        plt.show()
    finally:
        plt.close(fig)

14.3 统一交付检查表 ​

  1. 数据来源、缺失处理和聚合方式明确;模拟数据有标注。
  2. 图表回答实际问题;单位、范围、色标与分箱支持合理比较。
  3. 在最终报告尺寸检查字号、图例遮挡和标签裁剪。
  4. 静态图先保存再显示;动画记录后端与 writer;保留数据和依赖版本。
  5. 创建者负责关闭图形,复用函数不关闭外部传入的画布。
text
Data -> Shape / Units -> Axes / Scale -> Layout / Font -> Backend -> Output
现象优先检查与修复
导入失败核对解释器,用 python -m pip 安装并打印版本;检查 Python 兼容范围
无窗口检查 GUI 后端和显示调用;服务器保存文件,导入成功不等于 GUI 可用
保存为空图保留 Figure 引用,显式保存;先绘图、保存,再显示
子图无法索引squeeze 默认可能返回标量;使用标量或统一 squeeze=False
x/y 长度不符用共同观测掩码过滤,不要各自删除缺失数据后硬拼
日期间隔不对转为日期对象并使用日期 Locator / Formatter
刻度警告同时设置位置和标签,或使用 Formatter
中文乱码或负号方框检查字体与字形覆盖,负号开关不能解决中文缺字
图例为空Artist 设置有效 label 后再建图例;连续数值用色条
相关矩阵 NaN检查行列方向、常量列、缺失值,不把 NaN 当零
对数映射失败检查非正值及范围,按语义屏蔽或更换尺度
箱线参数报错tick_labels 需要 3.9+,勿同时传新旧参数
JPEG 参数报错quality 经由 pil_kwargs 传入;透明图优先 PNG
导出模糊或裁剪核对英寸、dpi、源图质量与布局;紧裁剪改变尺寸
动画静止保留引用,检查事件循环;按后端禁用 blit 或编码导出
GIF / MP4 导出失败检查 Pillow、外部 ffmpeg、编码器与路径权限
内存增长finally 关闭图形,释放不再使用的引用与缓存

先缩减到单个绘图区和少量确定数据,再逐步恢复布局与样式;不要靠隐藏警告掩盖数据或版本问题。

16.1 按职责查参数,不按函数背参数 ​

职责常用入口或参数关键约束
创建画布plt.subplots(figsize=..., layout=..., squeeze=...)英寸尺寸;返回形状见第 2 章
线条与标记linewidth、linestyle、marker、markersize宽度及线性标记尺寸用点单位
散点编码s、c、cmap、norms 为 points²;数值颜色须解释色标
类别柱形width、bottom、bar_label()区分类别宽度和累计基线
分布统计bins、density、whis、tick_labels分箱与须影响解读;注意版本下限
轴范围与尺度set_xlim()、set_ylim()、set_yscale()范围隐藏数据;log 不能表示非正值
刻度与标签Locator、Formatter、tick_params()先决定位置,再格式化显示
图例与色条ax.legend()、fig.colorbar()系列身份与数值映射不同
注释定位xycoords、textcoords、transform区分数据坐标、绘图区比例与点单位
布局sharex、sharey、GridSpec、layout共享轴须可比较;布局引擎勿混用
导出dpi、bbox_inches、transparent、pil_kwargs格式、尺寸和生命周期见第 8 章
动画frames、interval、blit、writer屏幕调度与编码帧率不同

16.2 官方资源与版本核对 ​

stable 指向官方当前稳定版,可能晚于本地安装版本。先记录环境版本,再切换到对应版本文档核对参数;画廊代码不是跨版本兼容承诺。

16.3 从会画图到能交付 ​

  1. 入门:独立创建、保存并关闭一张图,说明 Figure、Axes、Axis 的职责。
  2. 基础:为趋势、关系、类别和分布选图,解释坐标单位与分箱。
  3. 表达:组织子图、注释和色条,在目标尺寸检查字体与布局。
  4. 进阶:解释统计图与归一化的假设,区分模拟动画和真实数据流。
  5. 工程:将绘图封装为接收 Axes 的函数,隔离样式并安全批处理。
  6. 交付:完成五类案例验收,记录数据来源、依赖版本、输出格式和已知限制。

学习成果不是记住最多的 API,而是能解释每个视觉选择,并在另一台具备约定依赖的机器上重现结果。