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 进阶核心 多子图布局 subplot() subplots() GridSpec 不规则网格布局 坐标轴定制 刻度定位器 刻度格式化 网格线 坐标轴范围 图例与标题 图例位置 标题样式 轴标签 次要标题 文本与注释 text() annotate() figtext() 箭头与框 核心原则:让图表自己会说话 清晰 > 美观 | 信息密度适中 | 重点突出 典型应用场景 📊 销售分析报告 📈 科研论文配图 📉 数据监控看板

嗯,到这里 Matplotlib 进阶的内容就讲完了。这些技巧我在实际项目中反复使用,确实能让图表质量提升一个档次。记住,好的图表不是花里胡哨,而是让读者在 3 秒内抓住核心信息。

专注资料整理