4. 绘制第一张时间序列趋势图

在环境监测场景中,我们最常面对的数据形态是“时间—数值”对:某监测站点每小时上报的 PM2.5 浓度、某断面每天记录的氨氮含量、某园区每分钟采集的噪声分贝。把这些数据画成一条随时间延伸的折线,是趋势报告最基础也最核心的图形。本节以一张“某站点 24 小时 PM2.5 浓度趋势图”为例,完整走通从数据到成图的全过程,重点讲清 Matplotlib 的两层对象结构、plot() 的关键参数、日期坐标轴的格式化方法,以及标题、轴标签与图片保存的规范做法。

一、先理解 Figure 与 Axes:画布与坐标系

初学者最容易混淆的是“图”和“坐标系”。Matplotlib 采用两层结构:Figure(画布)是整个输出图像的容器,决定图片尺寸、分辨率和背景;Axes(坐标系/子图)是真正承载数据、坐标轴、刻度、标题的绘图区域。一张 Figure 上可以放一个或多个 Axes,但每个 Axes 才是“画线的地方”。

推荐使用面向对象写法,而不是依赖 plt.plot() 的隐式状态:

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd

# 示例数据(合理假设,非实测)
df = pd.DataFrame({
    "time": pd.date_range("2024-06-01 00:00", periods=24, freq="h"),
    "pm25": [38, 35, 33, 31, 30, 34, 42, 55, 61, 58, 52, 49,
             47, 45, 48, 53, 60, 66, 71, 68, 62, 57, 50, 44]
})

fig, ax = plt.subplots(figsize=(10, 4.5), dpi=120)

这里 fig 是画布,ax 是唯一的坐标系。figsize=(10, 4.5) 以英寸为单位控制宽高比,时间序列横向展开,宽高比约 2.2:1 比较适合 24 小时曲线;dpi=120 决定屏幕预览与导出时的像素密度。后续所有绘图、刻度、标签操作都作用在 ax 上,逻辑清晰、便于复用。

二、plot 基本参数:让曲线“会说话”

把时间列转为 datetime 类型后直接传入 plot(),Matplotlib 会自动识别为日期轴:

ax.plot(df["time"], df["pm25"],
        color="#1f77b4", linewidth=1.8,
        linestyle="-", marker="o", markersize=3.5,
        label="PM2.5 小时均值")

几个参数值得逐一说明:color 用十六进制色值保证配色稳定;linewidth 控制线宽,趋势图 1.5~2.0 之间既清晰又不笨重;marker="o" 与 markersize 让每个采样点可见,便于读者判断数据密度与缺失;label 为图例准备文字。若数据存在缺测,Pandas 中的 NaN 会让折线自动断开,这恰好是环境监测中“缺测不插值”的诚实表达,不要用 interpolate() 悄悄补平。

当采样点很密(如分钟级、上万点)时,应去掉 marker 或把 markersize 调小,否则点会糊成一条粗带,反而看不清趋势。

三、日期坐标轴格式化:把时间刻度读明白

默认的日期刻度往往过密、格式冗长。正确做法是“定位器 + 格式器”配合:定位器决定刻度出现在哪些时间点,格式器决定刻度文字怎么写。

ax.xaxis.set_major_locator(mdates.HourLocator(interval=3))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
ax.xaxis.set_minor_locator(mdates.HourLocator(interval=1))

fig.autofmt_xdate(rotation=0, ha="center")

HourLocator(interval=3) 表示每 3 小时一个主刻度,24 小时正好 8 个刻度,疏密适中;DateFormatter("%H:%M") 只显示时分,因为本图只覆盖同一天,重复显示日期反而冗余。若跨天,则应改为 "%m-%d %H:%M" 或使用 DayLocator。次刻度设为每小时,可让读者对时间间隔有更细的感知。autofmt_xdate() 用于自动处理标签重叠,本例标签较短,设 rotation=0 保持水平即可。

Figure(画布 figsize=10×4.5, dpi=120) Axes(坐标系 ax) 标题:某站点 24 小时 PM2.5 浓度趋势 时间(2024-06-01,主刻度每 3 小时) PM2.5 浓度(μg/m³) 00:00 03:00 06:00 09:00 12:00 15:00 18:00 PM2.5 小时均值 保存:fig.savefig("pm25_trend.png", dpi=200, bbox_inches="tight") → 导出前先确认刻度不重叠、标签完整

四、标题与轴标签:让图能脱离正文被读懂

一张合格的趋势图,标题应包含“对象 + 时间范围 + 指标”,轴标签必须带单位。环境监测数据的单位缺失是常见硬伤,μg/m³、mg/L、dB(A) 都不能省。

ax.set_title("某站点 24 小时 PM2.5 浓度趋势", fontsize=14, pad=12)
ax.set_xlabel("时间(2024-06-01)", fontsize=11)
ax.set_ylabel("PM2.5 浓度(μg/m³)", fontsize=11)
ax.grid(True, axis="y", linestyle="--", alpha=0.4)
ax.legend(loc="upper right", frameon=True)
ax.set_ylim(20, 80)

pad 控制标题与绘图区的间距,避免标题贴住曲线;grid(axis="y") 只画水平网格,便于读数又不干扰时间轴;set_ylim(20, 80) 显式设定纵轴范围,比自动缩放更利于多张图横向对比。若纵轴不从 0 开始,应在图注中说明,避免读者误判幅度。

五、保存为图片:交付前的最后一道关

保存必须用 fig.savefig(),而不是 plt.savefig(),前者明确作用于当前画布,避免多图场景下存错对象。关键参数:dpi 决定清晰度,报告插图建议 200~300;bbox_inches="tight" 自动裁掉多余白边,防止轴标签被切;facecolor="white" 保证透明背景下导出为白底,插入 Word 或 PPT 不出现黑底。

fig.savefig("pm25_trend.png", dpi=200,
            bbox_inches="tight", facecolor="white")
plt.close(fig)

保存后应打开图片做一次目视检查:横轴刻度是否重叠、单位是否完整、图例是否遮挡曲线、曲线是否被裁切。若发现刻度拥挤,回到定位器把 interval 调大;若标签被切,确认 bbox_inches="tight" 已生效。完成这一步,你才真正拥有了一张可以放进环境监测趋势报告的第一张时间序列图。

常见错误:用 plt.plot() 后直接 plt.savefig(),在多子图或循环出图时容易保存到错误的画布;以及忘记 plt.close(fig),批量生成几十张图后内存持续上涨。