8.3 时间序列可视化 时间序列的图形三层拆:趋势层(聚合折线加置信带)、季节层(按周期分面对齐形态)、异常层(残差窗口与标注)。第 3.2 节的聚合语法是本节的引擎。 实战产线第三条。时间序列最容易犯的错不是画不出,而是把三层混在一条线里读——趋势当季节、季节当异常。本节用 flights 数据把三层依次剥开。 趋势层:聚合折线与置信带 季节层:按周期对齐形态 分面对齐是季节层的另一种表达(第 3.2 节的案例已示范 col='year' 版),两种形态选一即可:曲线叠图适合强调"形状一致",分面适合比较"幅度变化"。 异常层:残差定位 完整案例收束:三层一页 背景:给航空运力规划做一页汇报,要求一页内说清趋势、季节、异常三层。
时间序列的图形三层拆:趋势层(聚合折线加置信带)、季节层(按周期分面对齐形态)、异常层(残差窗口与标注)。第 3.2 节的聚合语法是本节的引擎。
实战产线第三条。时间序列最容易犯的错不是画不出,而是把三层混在一条线里读——趋势当季节、季节当异常。本节用 flights 数据把三层依次剥开。
import seaborn as sns import matplotlib.pyplot as plt flights = sns.load_dataset('flights') ax = sns.lineplot(data=flights, x='year', y='passengers') ax.set_ylabel('月度乘客数(均值)') # 输出说明:均值线单调上升,置信带随年份整体变宽—— # 季节波动的绝对幅度在放大(乘性季节),不是样本量在变化
# 对数轴:乘性季节变加性 ax = sns.lineplot(data=flights, x='year', y='passengers') ax.set_yscale('log') ax.set_ylabel('乘客数(对数轴)') # 输出说明:对数轴上置信带宽基本恒定—— # 乘性结构的诊断信号:对数后宽度稳定,说明波动与总量成正比
# 各年份的月度曲线全部铺开:季节形态的原始证据 ax = sns.lineplot(data=flights, x='month', y='passengers', hue='year', palette='viridis', legend='full', estimator=None, errorbar=None) ax.legend(bbox_to_anchor=(1.02, 1), title='年份') ax.set_xlabel('月份') # 输出说明:12 条曲线形状高度一致——7、8 月双峰、2 月谷底; # 颜色由浅到深对应年份推进,曲线整体上移即趋势,形状不变即季节稳定
分面对齐是季节层的另一种表达(第 3.2 节的案例已示范 col='year' 版),两种形态选一即可:曲线叠图适合强调"形状一致",分面适合比较"幅度变化"。
import pandas as pd import numpy as np # 用"年均值加各月多年平均偏移"做一个朴素的可加模型,残差即异常候选 flights['year_mean'] = flights.groupby('year')['passengers'].transform('mean') month_avg = flights.groupby('month')['passengers'].mean() flights['expected'] = flights['year_mean'] + flights['month'].map(month_avg) \ - flights['passengers'].mean() flights['residual'] = flights['passengers'] - flights['expected'] ax = sns.scatterplot(data=flights, x='year', y='residual', hue='month', legend=False, palette='crest') ax.axhline(0, color='gray', linestyle=':') ax.axhline(20, color='crimson', linestyle='--', linewidth=1) ax.axhline(-20, color='crimson', linestyle='--', linewidth=1) ax.set_ylabel('残差(实际减拟合)') # 输出说明:残差在正负 20 的红线内基本随机散布; # 早年残差偏负、晚年偏正——提示可加模型低估了增长,乘性模型更合适
# 岭脊式检查:残差的月度分布 g = sns.displot(data=flights, x='residual', col='month', col_wrap=4, kind='kde', height=2) # 输出说明:7、8 月的残差分布最宽——旺季的不可预测性更高, # 排班冗余应向旺季倾斜,这是图形直接给出的运营建议
背景:给航空运力规划做一页汇报,要求一页内说清趋势、季节、异常三层。
操作:
fig, axes = plt.subplots(3, 1, figsize=(9, 9), sharex=False) sns.lineplot(data=flights, x='year', y='passengers', ax=axes[0]) axes[0].set_title('趋势层:年均值与置信带') sns.lineplot(data=flights, x='month', y='passengers', hue='year', palette='viridis', estimator=None, errorbar=None, legend=False, ax=axes[1]) axes[1].set_title('季节层:月度形态逐年对齐') sns.scatterplot(data=flights, x='year', y='residual', s=18, color='#4c78a8', ax=axes[2]) axes[2].axhline(0, color='gray', linestyle=':') axes[2].set_title('异常层:残差随机性检查') plt.tight_layout()
结果解读:三张纵排图各有分工——第一张回答"总量去哪"、第二张回答"年内怎么摆、是否稳定"、第三张回答"模型之外还剩什么"。规划会上的结论:十年增长约 4 倍、季节双峰稳定、旺季波动更大且线性拟合在两端有系统偏差。变式:把第三层的残差换成一阶差分序列,可以检查"增长率"是否稳定——同一份数据,第三层换个统计量就是另一个管理问题。
⚠️ 常见坑:sharex 默认共享在纵排多图里会把月份轴与年份轴强行统一刻度,本例已用 sharex=False 规避;时间列若是字符串而非 datetime,折线的 x 轴会按字符串排序,跨年数据必须先转时间类型。
💡 关键直觉:对数轴是时间序列的显微镜——乘性结构在对数轴下变加性,置信带宽度从"随增长放大"变成"恒定",很多结构性判断在换轴的一瞬间完成。
本节要点回顾
- 三层分离:趋势、季节、异常各有专门图形,混在一条线里读必然误判;
- 对数轴诊断乘性:带宽在对数轴下恒定即乘性季节;
- 曲线叠图看形状、分面看幅度:季节层两种表达的分工;
- 残差检查随机性:有结构的残差说明模型欠了那一层;
- 时间列先转类型:字符串排序会打乱时间轴。
实战三章收官。第 9 章摊开性能边界与选型决策,给全书收束。