Matplotlib 进阶:子图与样式定制 (Advanced Matplotlib)
章节概述
当你需要在一张图中展示多个维度的数据时——比如同时对比 C 程序在不同编译器、不同优化级别、不同输入规模下的性能——单一子图已经不够用了。本章教你 subplots 的多子图布局、共享坐标轴、双 Y 轴、以及 Matplotlib 的完整样式定制链。这些技巧是制作论文级图表的基本功。
核心理念:多子图布局相当于 C 语言中的多维数组——你需要精确控制每个单元的位置和内容。
plt.subplots(nrows, ncols)返回一个ax数组(类似int arr[rows][cols]),每个元素都是独立的坐标系对象,就像每个数组元素可独立赋值。
第一节:子图布局 (Subplots)
1.1 基础多子图:subplots()
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 100)
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# axes 是一个 2×2 的 numpy 数组,类似于 C 的 int axes[2][2]
axes[0, 0].plot(x, np.sin(x), 'r-')
axes[0, 0].set_title('sin(x)')
axes[0, 1].plot(x, np.cos(x), 'b-')
axes[0, 1].set_title('cos(x)')
axes[1, 0].plot(x, np.tan(x), 'g-')
axes[1, 0].set_title('tan(x)')
axes[1, 0].set_ylim(-5, 5) # tan 值域较大,限制范围
axes[1, 1].plot(x, np.sin(x) * np.cos(x), 'm-')
axes[1, 1].set_title('sin(x)·cos(x)')
fig.suptitle('Trigonometric Functions', fontsize=16)
fig.tight_layout()
fig.savefig('trig_subplots.png')
axes[row, col]的二维索引方式直接对应 C 的arr[row][col]。当只有一行或一列时,axes是一维数组,用axes[i]访问。
1.2 非均匀子图:GridSpec
当子图需要不同大小时,GridSpec 提供灵活的网格布局:
import matplotlib.gridspec as gridspec
fig = plt.figure(figsize=(12, 6))
gs = gridspec.GridSpec(2, 3, figure=fig)
# 左上角:占据 (row=0:2, col=0:1),即两行高度、一列宽度
ax_big = fig.add_subplot(gs[0:2, 0:2])
# 右上:两行一列中的上半行
ax_top = fig.add_subplot(gs[0, 2])
# 右下:两行一列中的下半行
ax_bottom = fig.add_subplot(gs[1, 2])
x = np.linspace(0, 10, 200)
ax_big.plot(x, np.sin(x))
ax_big.set_title('Main Plot')
ax_top.scatter(range(20), np.random.randn(20))
ax_top.set_title('Scatter')
ax_bottom.hist(np.random.randn(500), bins=30)
ax_bottom.set_title('Histogram')
fig.tight_layout()
fig.savefig('gridspec.png')
GridSpec相当于 CSS Grid 布局——定义行和列的轨道,然后让每个子图占据指定范围。这比 C 语言中用像素坐标手算位置的传统绑图方式灵活得多。
1.3 嵌套子图:subplot2grid 与 GridSpec 子网格
fig = plt.figure(figsize=(10, 8))
# 主坐标系
ax_main = plt.subplot2grid((3, 3), (0, 0), colspan=2, rowspan=2)
# 右侧柱状图
ax_bar = plt.subplot2grid((3, 3), (0, 2), rowspan=2)
# 底部大图
ax_bottom = plt.subplot2grid((3, 3), (2, 0), colspan=3)
ax_main.plot(np.random.randn(100).cumsum())
ax_bar.bar(['A', 'B', 'C', 'D'], [3, 7, 2, 5])
ax_bottom.hist(np.random.randn(1000), bins=40, alpha=0.7)
fig.savefig('nested_grid.png')第二节:共享坐标轴与双 Y 轴
2.1 共享 X 轴或 Y 轴
当多个子图展示同一维度的数据时,共享坐标轴可以节省空间并使对比更直观:
fig, axes = plt.subplots(3, 1, figsize=(8, 10), sharex=True)
x = np.linspace(0, 10, 200)
for i, freq in enumerate([1, 2, 3]):
axes[i].plot(x, np.sin(freq * x))
axes[i].set_ylabel(f'sin({freq}x)')
axes[2].set_xlabel('x') # 只在最下方设置 xlabel
fig.savefig('shared_x.png')
sharex=True让所有子图共享同一个 X 轴。当缩放或平移一个子图时,其他子图同步变化——类似于 C 语言中用同一个指针引用数组的不同视图。
共享后,Matplotlib 自动隐藏中间子图的刻度标签,只保留底部的标签。
2.2 双 Y 轴:twinx()
当两组数据量级不同但 X 轴相同时,用双 Y 轴可以将它们放在同一张图上:
fig, ax1 = plt.subplots(figsize=(8, 5))
x = np.linspace(0, 10, 100)
y1 = np.sin(x) * 100
y2 = np.exp(x) * 0.1
color1 = 'tab:blue'
ax1.set_xlabel('x')
ax1.set_ylabel('sin(x)·100', color=color1)
ax1.plot(x, y1, color=color1)
ax1.tick_params(axis='y', labelcolor=color1)
ax2 = ax1.twinx() # 创建共享 X 轴的第二个 Y 轴
color2 = 'tab:red'
ax2.set_ylabel('exp(x)·0.1', color=color2)
ax2.plot(x, y2, color=color2)
ax2.tick_params(axis='y', labelcolor=color2)
# 可选:第三个 Y 轴
# ax3 = ax1.twinx()
# ax3.spines['right'].set_position(('outward', 60))
fig.tight_layout()
fig.savefig('twin_axes.png')2.3 C 程序基准测试的多维度对比
典型的 C 性能测试场景——同时展示时间、内存、和 CPU 利用率的对比:
# C 程序输出格式:n time_ms memory_kb cpu_percent
import sys, numpy as np
import matplotlib.pyplot as plt
data = np.loadtxt(sys.stdin)
n, time, memory, cpu = data[:, 0], data[:, 1], data[:, 2], data[:, 3]
fig, (ax1, ax3) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
# 上图:时间 + 内存(双 Y 轴)
ax1.plot(n, time, 'b-o', label='Time (ms)')
ax1.set_ylabel('Time (ms)', color='b')
ax1.tick_params(axis='y', labelcolor='b')
ax2 = ax1.twinx()
ax2.plot(n, memory, 'r-s', label='Memory (KB)')
ax2.set_ylabel('Memory (KB)', color='r')
ax2.tick_params(axis='y', labelcolor='r')
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
# 下图:CPU 利用率
ax3.plot(n, cpu, 'g-^')
ax3.set_ylabel('CPU (%)')
ax3.set_xlabel('Input Size (n)')
ax3.fill_between(n, 0, cpu, alpha=0.2, color='g')
fig.tight_layout()
fig.savefig('benchmark_multi.png')第三节:样式定制 —— 从颜色到字体
3.1 线的属性:颜色、线型、线宽、标记
Matplotlib 提供了极其精细的线属性控制:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 50)
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(x, np.sin(x),
color='#2E86AB', # 十六进制颜色
linestyle='-', # '-' '--' '-.' ':'
linewidth=2, # 线宽(点)
marker='o', # 标记形状
markersize=6,
markerfacecolor='white',
markeredgewidth=1.5,
markeredgecolor='#2E86AB',
label='sin(x)',
alpha=0.8)
ax.legend()
fig.savefig('styled_line.png')常用线型与标记速查:
| 线型 | 字符 | 标记 | 字符 |
|---|---|---|---|
| 实线 | - | 圆点 | o |
| 虚线 | -- | 方形 | s |
| 点划线 | -. | 三角形 | ^ v < > |
| 点线 | : | 星形 | * |
| 无线 | '' | 菱形 | D |
3.2 颜色映射:给数据赋予视觉含义
import numpy as np
import matplotlib.pyplot as plt
n = 200
x = np.random.randn(n)
y = np.random.randn(n)
colors = np.sqrt(x**2 + y**2) # 离原点距离决定颜色
fig, ax = plt.subplots(figsize=(7, 6))
scatter = ax.scatter(x, y, c=colors, cmap='plasma', s=50, alpha=0.7)
fig.colorbar(scatter, ax=ax, label='Distance from origin')
ax.axhline(y=0, color='gray', linewidth=0.5)
ax.axvline(x=0, color='gray', linewidth=0.5)
ax.set_aspect('equal')
fig.savefig('colormap.png')Matplotlib 内置数十种颜色映射(colormap),常用的有
viridis、plasma、inferno、magma、coolwarm、RdYlBu。选择原则:连续数据用感知均匀的 colormap(viridis),有零点对称的数据用发散型(coolwarm)。
3.3 文字元素:标题、轴标签、刻度、注释
fig, ax = plt.subplots(figsize=(8, 5))
x = np.linspace(0, 2, 100)
ax.plot(x, x**2, 'b-', label='f(x)=x²')
ax.set_title('Quadratic Function', fontsize=16, fontweight='bold', pad=15)
ax.set_xlabel('x', fontsize=13)
ax.set_ylabel('f(x)', fontsize=13)
ax.legend(fontsize=12, loc='upper left', framealpha=0.8)
# 自定义刻度
ax.set_xticks([0, 0.5, 1.0, 1.5, 2.0])
ax.set_xticklabels(['0', '0.5', '1', '1.5', '2'], fontsize=11)
ax.tick_params(axis='both', which='major', labelsize=11, direction='in')
# 文本注释
ax.text(1.0, 3.0, 'minimum?', fontsize=11,
ha='center', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
# 箭头注释
ax.annotate('f(1)=1', xy=(1, 1), xytext=(1.3, 0.5),
arrowprops=dict(arrowstyle='->', color='red'),
fontsize=11, color='red')
ax.grid(True, linestyle='--', alpha=0.4)
fig.tight_layout()
fig.savefig('text_elements.png')3.4 全局样式与样式表
Matplotlib 提供预定义的样式表,一行代码即可切换整体外观:
import matplotlib.pyplot as plt
import matplotlib.style as mplstyle
# 查看可用样式
print(mplstyle.available[:10])
# ['Solarize_Light2', '_classic_test_patch', ... 'seaborn-v0_8', 'tableau-colorblind10']
# 使用样式
plt.style.use('seaborn-v0_8-darkgrid')
# 或:plt.style.use('ggplot')
# 或:plt.style.use('fivethirtyeight')
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]
plt.plot(x, y, 'o-')
plt.savefig('styled_global.png')创建自定义样式文件 my_style.mplstyle:
lines.linewidth: 2.0
lines.markersize: 8
font.size: 14
axes.grid: True
axes.grid.axis: both
grid.alpha: 0.3
figure.figsize: 10, 6
savefig.dpi: 150
savefig.bbox: tightplt.style.use('my_style.mplstyle')
# 之后所有的绑图都使用这个样式样式文件相当于 C 项目中的
.h头文件——定义全局常量,避免在每个源文件中重复设置。团队项目中使用统一的样式表确保图表外观一致。
第四节:实战 —— 多面板 C 基准测试报告
4.1 完整的多面板性能报告
将多个 C 程序的基准测试结果汇总到一张信息密集的图中:
C 程序(full_bench.c):
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#define MAX_N 1000000
void bench_sort(int n) {
int *arr = malloc(n * sizeof(int));
for (int i = 0; i < n; i++) arr[i] = rand();
// ... 排序算法 ...
free(arr);
printf("sort %d %f\n", n, /* time */ 0.0);
}
void bench_hash(int n) {
// ... 哈希表操作 ...
printf("hash %d %f\n", n, /* time */ 0.0);
}
void bench_tree(int n) {
// ... 树操作 ...
printf("tree %d %f\n", n, /* time */ 0.0);
}
int main() {
for (int n = 1000; n <= MAX_N; n *= 10) {
bench_sort(n);
bench_hash(n);
bench_tree(n);
}
return 0;
}Python 绑图脚本:
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# 读取管道数据
data = np.loadtxt(sys.stdin, dtype=[('algo', 'U10'), ('n', int), ('time', float)])
algos = np.unique(data['algo'])
colors = {'sort': '#2196F3', 'hash': '#4CAF50', 'tree': '#FF5722'}
fig = plt.figure(figsize=(14, 10))
gs = gridspec.GridSpec(2, 2, figure=fig)
# 面板 1:时间对比(双对数坐标)
ax1 = fig.add_subplot(gs[0, 0])
for algo in algos:
mask = data['algo'] == algo
ax1.loglog(data['n'][mask], data['time'][mask], 'o-',
color=colors.get(algo, 'gray'), label=algo, markersize=6)
ax1.set_title('Performance Comparison (log-log)')
ax1.set_xlabel('Input Size (n)')
ax1.set_ylabel('Time (ms)')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 面板 2:柱状图(最大输入规模下的时间对比)
ax2 = fig.add_subplot(gs[0, 1])
max_n = data['n'].max()
max_data = data[data['n'] == max_n]
ax2.bar(max_data['algo'], max_data['time'],
color=[colors.get(a, 'gray') for a in max_data['algo']])
ax2.set_title(f'Time at n={max_n:,}')
ax2.set_ylabel('Time (ms)')
# 面板 3:加速比(相对最慢算法的加速倍数)
ax3 = fig.add_subplot(gs[1, 0])
for algo in algos:
mask = data['algo'] == algo
n_vals = data['n'][mask]
t_vals = data['time'][mask]
ax3.plot(n_vals, t_vals, 'o-', color=colors.get(algo, 'gray'), label=algo)
ax3.set_title('Time vs Input Size')
ax3.set_xlabel('n')
ax3.set_ylabel('Time (ms)')
# 面板 4:汇总表格
ax4 = fig.add_subplot(gs[1, 1])
ax4.axis('off')
summary = [f'{algo}: {t:.2f} ms' for algo, t in
zip(max_data['algo'], max_data['time'])]
summary_text = '\n'.join(summary)
ax4.text(0.1, 0.5, f"Summary at n={max_n:,}:\n\n{summary_text}",
fontsize=12, va='center', fontfamily='monospace')
ax4.set_title('Summary')
fig.suptitle('C Program Benchmark Report', fontsize=18, fontweight='bold')
fig.tight_layout()
fig.savefig('benchmark_report.png', dpi=150)练习
以下题目用于验证本章所学内容:
| 题号 | 题目 | 链接 | 涉及知识点 |
|---|---|---|---|
| — | 本章无对应力扣题 | — | 请用动手练习题自检 |