4. Matplotlib进阶:多子图布局、坐标轴定制、图例与标题、文本与注释
Matplotlib 基础用法大家都会,但真正做出能用的图表,还得靠进阶技巧。我个人习惯把这一章叫做「让图表说话」——因为光有数据不够,你得让读者一眼看懂你想表达什么。
本章核心:掌握多子图布局、坐标轴精细控制、图例与标题优化、文本注释技巧,让你的图表从「能看」变成「好用」。
4.1 多子图布局:一张画布讲多个故事
实际项目中,我们经常需要把多个图表放在一起对比。比如分析销售数据时,我想同时看月度趋势、品类分布和区域对比。这时候就需要多子图布局。
Matplotlib 提供了两种方式:subplot() 和 subplots()。我个人更推荐后者,因为它返回的是数组,操作起来更顺手。
import matplotlib.pyplot as plt
import numpy as np
# 创建一个 2x2 的子图网格
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# 在第一个子图上画折线图
x = np.linspace(0, 10, 100)
axes[0, 0].plot(x, np.sin(x))
axes[0, 0].set_title('正弦波')
# 在第二个子图上画柱状图
categories = ['A', 'B', 'C', 'D']
values = [23, 45, 56, 78]
axes[0, 1].bar(categories, values)
axes[0, 1].set_title('分类数据')
# 在第三个子图上画散点图
axes[1, 0].scatter(np.random.randn(50), np.random.randn(50))
axes[1, 0].set_title('随机散点')
# 在第四个子图上画饼图
axes[1, 1].pie([30, 25, 25, 20], labels=['产品A', '产品B', '产品C', '产品D'])
axes[1, 1].set_title('市场份额')
plt.tight_layout() # 自动调整子图间距
plt.show()
我的经验:tight_layout() 这行代码我几乎每次都加。不加的话,子图标题经常和坐标轴标签重叠,看着特别乱。有一次做报告,忘了加这行,被领导说「图表太挤了」——从那以后我就记住了。
还有一种更灵活的布局方式:GridSpec。它允许你创建不规则的子图网格,比如让某个子图跨多行或多列。
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(12, 8))
gs = GridSpec(3, 3, figure=fig)
# 左上角的大图
ax1 = fig.add_subplot(gs[0:2, 0:2])
ax1.plot([1, 2, 3], [4, 5, 6])
ax1.set_title('主图区域')
# 右上角的小图
ax2 = fig.add_subplot(gs[0, 2])
ax2.bar([1, 2, 3], [4, 5, 6])
# 底部横跨三列
ax3 = fig.add_subplot(gs[2, :])
ax3.scatter([1, 2, 3], [4, 5, 6])
plt.show()
4.2 坐标轴定制:让数据自己说话
坐标轴是图表的骨架。默认设置很多时候不够用,比如你想调整刻度密度、修改刻度标签、或者添加辅助线。
我遇到过最典型的情况:画时间序列图时,日期标签挤成一团,根本看不清。这时候就需要手动调整。
import matplotlib.dates as mdates
from datetime import datetime, timedelta
# 生成日期数据
dates = [datetime.now() + timedelta(days=i) for i in range(30)]
values = np.random.randn(30).cumsum()
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(dates, values)
# 定制 x 轴
ax.xaxis.set_major_locator(mdates.WeekdayLocator()) # 每周一个主刻度
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) # 日期格式
ax.xaxis.set_minor_locator(mdates.DayLocator()) # 每天一个次刻度
# 旋转标签防止重叠
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha='right')
# 添加网格线
ax.grid(True, linestyle='--', alpha=0.7)
# 设置坐标轴范围
ax.set_xlim(dates[0], dates[-1])
ax.set_ylim(values.min() - 1, values.max() + 1)
plt.show()
避坑指南:我曾经在设置坐标轴范围时,直接用 ax.set_xlim(min(dates), max(dates)),结果发现图表边缘的数据点被切掉了一半。后来才意识到,set_xlim 的参数应该稍微留点边距,比如 ax.set_xlim(dates[0] - timedelta(days=1), dates[-1] + timedelta(days=1))。
4.3 图例与标题:图表的「说明书」
图例和标题是读者理解图表的第一道门槛。很多人觉得随便写个标题就行,其实不然。
我总结了一个原则:标题要回答「这是什么数据」,图例要回答「每条线代表什么」。
fig, ax = plt.subplots(figsize=(10, 6))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), label='sin(x)', linewidth=2)
ax.plot(x, np.cos(x), label='cos(x)', linewidth=2, linestyle='--')
ax.plot(x, np.sin(x) * np.cos(x), label='sin(x)*cos(x)', linewidth=1, alpha=0.7)
# 标题定制
ax.set_title('三角函数对比图', fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('X 轴 (弧度)', fontsize=12)
ax.set_ylabel('Y 轴 (函数值)', fontsize=12)
# 图例定制
ax.legend(loc='upper right', fontsize=10, frameon=True,
shadow=True, fancybox=True, borderpad=1)
# 添加次要标题
ax.text(0.5, 1.05, '数据范围:0 到 10π', transform=ax.transAxes,
ha='center', fontsize=10, color='gray')
plt.show()
我的习惯:图例位置我一般用 loc='best',让 Matplotlib 自动选择最佳位置。但如果数据线在某个区域特别密集,我会手动指定位置,比如 loc='upper left'。另外,frameon=True 加上阴影效果,图例会更有层次感。
4.4 文本与注释:画龙点睛之笔
图表中的文本注释,就像演讲中的重点强调。用得好了,读者一眼就能抓住关键信息。
Matplotlib 提供了 text()、annotate() 和 figtext() 三种方式。我个人最常用的是 annotate(),因为它可以画箭头指向特定数据点。
fig, ax = plt.subplots(figsize=(10, 6))
x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x/5)
ax.plot(x, y, linewidth=2, color='#2E86AB')
# 标注最大值点
max_idx = np.argmax(y)
ax.annotate(f'峰值: {y[max_idx]:.3f}',
xy=(x[max_idx], y[max_idx]),
xytext=(x[max_idx] + 1, y[max_idx] + 0.1),
arrowprops=dict(arrowstyle='->', color='red', lw=1.5),
fontsize=10, color='red', fontweight='bold')
# 标注特殊区域
ax.axvspan(2, 4, alpha=0.2, color='yellow', label='关键区间')
ax.text(3, 0.3, '衰减区域', ha='center', fontsize=9,
bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.5))
# 添加公式注释
ax.text(0.95, 0.95, '$y = \\sin(x) \\cdot e^{-x/5}$',
transform=ax.transAxes, ha='right', va='top',
fontsize=12, fontstyle='italic',
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.3))
plt.show()
关键技巧:annotate() 的 xy 参数是数据坐标,xytext 是注释文本的坐标。你可以用 arrowprops 控制箭头样式。我建议箭头颜色和文本颜色保持一致,这样视觉上更统一。
4.5 综合案例:一份完整的销售分析报告
把上面这些技巧串起来,我们做一个实际案例。假设你要给老板汇报季度销售数据,需要同时展示趋势、对比和关键节点。
# 模拟数据
months = ['1月', '2月', '3月', '4月', '5月', '6月']
sales_a = [120, 135, 110, 145, 160, 155]
sales_b = [90, 105, 95, 120, 130, 125]
sales_c = [60, 55, 70, 65, 80, 75]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# 左图:趋势对比
ax1.plot(months, sales_a, 'o-', label='产品A', linewidth=2, color='#E74C3C')
ax1.plot(months, sales_b, 's--', label='产品B', linewidth=2, color='#3498DB')
ax1.plot(months, sales_c, '^-.', label='产品C', linewidth=2, color='#2ECC71')
# 标注最高点
max_month = months[sales_a.index(max(sales_a))]
ax1.annotate(f'最高: {max(sales_a)}',
xy=(max_month, max(sales_a)),
xytext=(max_month, max(sales_a) + 15),
arrowprops=dict(arrowstyle='->', color='#E74C3C'),
color='#E74C3C', fontweight='bold')
ax1.set_title('2024上半年销售趋势', fontsize=14, fontweight='bold')
ax1.set_xlabel('月份')
ax1.set_ylabel('销售额 (万元)')
ax1.legend(loc='upper left')
ax1.grid(True, alpha=0.3)
# 右图:总量对比
totals = [sum(sales_a), sum(sales_b), sum(sales_c)]
bars = ax2.bar(['产品A', '产品B', '产品C'], totals,
color=['#E74C3C', '#3498DB', '#2ECC71'], alpha=0.8)
# 在柱状图上添加数值标签
for bar, total in zip(bars, totals):
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5,
f'{total}万', ha='center', fontweight='bold')
ax2.set_title('上半年总销售额对比', fontsize=14, fontweight='bold')
ax2.set_ylabel('总销售额 (万元)')
# 添加整体注释
fig.suptitle('2024年上半年销售分析报告', fontsize=16, fontweight='bold', y=1.02)
fig.text(0.5, -0.02, '数据来源:销售系统 | 统计周期:2024.01-2024.06',
ha='center', fontsize=9, color='gray')
plt.tight_layout()
plt.show()
避坑指南:我曾经在柱状图上加数值标签时,直接用 bar.get_height() 作为 y 坐标,结果标签和柱状图顶部重叠了。后来我改成 bar.get_height() + 5,留出一点间距,看起来就舒服多了。
4.6 本章知识体系
下面这张图总结了本章的核心知识点,方便你快速回顾:
嗯,到这里 Matplotlib 进阶的内容就讲完了。这些技巧我在实际项目中反复使用,确实能让图表质量提升一个档次。记住,好的图表不是花里胡哨,而是让读者在 3 秒内抓住核心信息。