Appearance
Matplotlib:从绘图基础到工程实践
Matplotlib 的难点不是记住多少绘图函数,而是把数据含义、图形对象、视觉编码和输出环境连接起来。本手册先建立对象模型,再学习图表表达,最后处理复用、批处理和交互。
学习路线与分篇目录
text
Data -> Figure / Axes -> Chart -> Axis / Style -> Layout -> Export
| |
+-> Statistics / Animation +-> Reuse / Batch首次学习按章节顺序阅读;制作报告优先完成第 1~8 章,再看第 14 章;服务器出图重点看第 8、13、15 章。每个可执行 Python 示例均自带导入和数据,不需要共享前文变量;含随机数据时使用固定种子的局部随机生成器。
第一篇:入门——先理解一张图如何产生
第二篇:基础——让数据得到正确表达
第三篇:表达与布局——组织并交付图形
第四篇:进阶——统计表达、投影与交互
第五篇:工程——从一次绘图到可维护代码
第六篇:实战与速查——形成独立交付能力
1. 定位、环境与依赖边界
1.1 适用范围
Matplotlib 是以 Python 为接口的绘图库,适合探索分析、静态报表、论文插图和服务端图片生成。它提供精细的布局、刻度、文本和导出控制,也能完成基础动画与三维可视化。
交互式 Web 仪表盘、复杂三维场景或高吞吐实时流不是其主要优势;先确认是否需要静态交付,再选择 Matplotlib、Web 图表库或专门的三维工具。
1.2 推荐基线与安装
本文以 Python 3.11+、Matplotlib 3.9+ 为推荐基线,采用兼容 Matplotlib 3.9 的主要写法,不承诺所有未来版本均无需修改。NumPy 是基础示例依赖,其他库按章节安装。
| 依赖 | 本文建议范围 | 用途 |
|---|---|---|
| Matplotlib | 3.9+ | 绘图;箱线图使用新版参数 |
| NumPy | 1.26+ | 数组、采样、随机数据 |
| Pandas | 2.2+,可选 | 表格与日期数据整理 |
| Seaborn | 0.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 checkWindows 命令提示符可执行 .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 可以内嵌展示静态图;无界面服务器通常只保存图片。导入成功和窗口能够弹出是两项不同的验证。
2. Figure、Axes、Axis、Artist 与两种接口
先分清“包含关系”和“类型关系”
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] |
3. 最小绘图闭环
闭环顺序是:准备数据 → 创建图形 → 添加 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. 图表选型与四种基础图
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. 坐标轴、刻度、日期与对数尺度
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. 样式、中文字体、图例与注释
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. 子图、GridSpec 与共享轴
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. 保存格式、尺寸与生命周期
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. 统计图、热力图与颜色归一化
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. 极坐标雷达图与三维曲面
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. 动画、事件与模拟监控
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. Pandas 与 Seaborn 协作边界
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. 函数复用、样式隔离、批处理与性能
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. 五类案例的整合与验收
每类案例只保留一处完整实现,基础章节解释机制,本章组织交付验收。
| 案例 | 完整实现 | 学习目标与验收 |
|---|---|---|
| 一:销售与利润 | 本章 14.1 | 日期有序、利润计算正确、单位清楚 |
| 二:成绩分布 | 第 9 章 9.2 | 能解释分箱、中位数与须外点 |
| 三:相关性热力图 | 第 9 章 9.4 | 4 × 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 统一交付检查表
- 数据来源、缺失处理和聚合方式明确;模拟数据有标注。
- 图表回答实际问题;单位、范围、色标与分箱支持合理比较。
- 在最终报告尺寸检查字号、图例遮挡和标签裁剪。
- 静态图先保存再显示;动画记录后端与 writer;保留数据和依赖版本。
- 创建者负责关闭图形,复用函数不关闭外部传入的画布。
15. 常见问题与排错顺序
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. 参数速查、资源与后续路线
16.1 按职责查参数,不按函数背参数
| 职责 | 常用入口或参数 | 关键约束 |
|---|---|---|
| 创建画布 | plt.subplots(figsize=..., layout=..., squeeze=...) | 英寸尺寸;返回形状见第 2 章 |
| 线条与标记 | linewidth、linestyle、marker、markersize | 宽度及线性标记尺寸用点单位 |
| 散点编码 | s、c、cmap、norm | s 为 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 官方资源与版本核对
- Matplotlib Quick start:对象式与状态式接口入门。
- Matplotlib API:参数、返回值与版本变更。
- Matplotlib Gallery:按任务寻找可改造示例。
- 后端说明:GUI、Notebook 与无界面渲染。
- 动画 API:动画对象、writer 与生命周期。
- 颜色选择指南:顺序、发散及分类色图。
- NumPy 文档:数组形状、数值运算与随机生成器。
- Pandas 可视化指南:日期、表格与绘图后端。
- Seaborn 接口概览:axes-level 与 figure-level 边界。
stable 指向官方当前稳定版,可能晚于本地安装版本。先记录环境版本,再切换到对应版本文档核对参数;画廊代码不是跨版本兼容承诺。
16.3 从会画图到能交付
- 入门:独立创建、保存并关闭一张图,说明 Figure、Axes、Axis 的职责。
- 基础:为趋势、关系、类别和分布选图,解释坐标单位与分箱。
- 表达:组织子图、注释和色条,在目标尺寸检查字体与布局。
- 进阶:解释统计图与归一化的假设,区分模拟动画和真实数据流。
- 工程:将绘图封装为接收 Axes 的函数,隔离样式并安全批处理。
- 交付:完成五类案例验收,记录数据来源、依赖版本、输出格式和已知限制。
学习成果不是记住最多的 API,而是能解释每个视觉选择,并在另一台具备约定依赖的机器上重现结果。