本节摘要:季节性有两种识别方式——视觉的(子序列图)和统计的(ACF / 周期图)。本节用真实形态的日数据演示两种方法,让读者掌握"既能看到形状、又能用统计量确认"的双重保险。
阅读完本节,你应当能够:
本节是季节性"双路对照"的第一路:子序列图(看形状)。读完后你能用子序列图把"周一到周日"或"1 月到 12 月"的模式画清楚,并用热图判断季节性是否稳定。
2.2 节已经介绍过子序列图的画法。回到这段代码,做更精细的展示:
import pandas as pd import numpy as np import matplotlib.pyplot as plt rng = np.random.default_rng(3) n = 365 * 2 trend = np.linspace(100, 200, n) weekly = np.tile([0, -10, -10, -5, 5, 25, 30], n // 7 + 1)[:n] y = trend + weekly + rng.normal(0, 3, n) s = pd.Series(y, index=pd.date_range("2023-01-02", periods=n)) # 把每个观测分配到对应的"周几 + 一年中的第几周" df = s.to_frame("y") df["weekday"] = df.index.dayofweek df["week_of_year"] = df.index.isocalendar().week # 季节性子序列图:每行是一个星期几,每列是不同周 import seaborn as sns pivot = df.pivot_table(index="week_of_year", columns="weekday", values="y") fig, ax = plt.subplots(figsize=(10, 6)) sns.heatmap(pivot, cmap="viridis", ax=ax) ax.set_title("季节性子序列热图:纵轴=第几周,横轴=星期几,颜色=销量") plt.tight_layout()
热图能直观展示"周一低、周末高"是否在每一年都稳定。如果热图的横列(不同周)颜色一致,说明季节性是稳定的;如果颜色在不同周之间变化大,说明季节性受外部因素调节(软季节性)。
子序列图是时间域的。**周期图(periodogram)**是频率域的——它把序列拆成不同频率的正弦/余弦波,告诉你"哪个频率的能量最强"。
from scipy.signal import periodogram # 估计功率谱 freqs, power = periodogram(s.values, fs=1.0) # fs=1 表示每天 1 个采样点 # 找能量最大的几个频率 top_idx = np.argsort(power)[::-1][:5] for i in top_idx: if freqs[i] > 0: period = 1 / freqs[i] print(f"频率 {freqs[i]:.4f} (周期 {period:.2f} 天) 功率 {power[i]:.2f}") # 预期:周期 7 天的频率功率最大
periodogram 的横轴是频率(每天几个周期),纵轴是功率(能量)。最强的峰应该出现在 1/7 ≈ 0.143 频率处,对应"7 天周期"。
💡 关键直觉:子序列图回答"形状长什么样",周期图回答"周期有多长"。两者结合:先用周期图确认周期长度,再用子序列图看清"那个周期里值怎么变化"。
| 视觉 | 统计 | 结论 |
|---|---|---|
| 子序列图有明显分组 | ACF 在 lag=7 处峰显著 | 强季节性 |
| 子序列图有分组但不明显 | ACF 在 lag=7 处峰不显著 | 弱 / 软季节性 |
| 子序列图无分组 | ACF 在 lag=7 处无峰 | 无季节性 |
| 子序列图形状随时间变化 | 周期图显示多个峰 | 多重季节性 |
我们换一个更复杂的真实场景——某网站的小时级流量。它同时有日季节性(24 小时周期)和周季节性(7 天周期):
# 假设有 28 天 × 24 小时 = 672 个观测 n = 28 * 24 rng = np.random.default_rng(0) t = np.arange(n) daily = 50 * np.sin(2 * np.pi * t / 24 - 1) # 日周期 weekly = 30 * np.sin(2 * np.pi * t / (24 * 7) - 0.5) # 周周期 y = 1000 + daily + weekly + rng.normal(0, 10, n) s = pd.Series(y, index=pd.date_range("2024-01-01", periods=n, freq="H")) freqs, power = periodogram(s.values, fs=1.0) top_idx = np.argsort(power)[::-1][:3] for i in top_idx: if freqs[i] > 0: period = 1 / freqs[i] print(f"周期 {period:.2f} 小时 功率 {power[i]:.2f}") # 预期:周期 24 小时最强、168 小时(7 天)次强
这是一个典型"双重季节性"场景。处理这种序列,SARIMA 显得力不从心(只能处理一个季节周期);更合适的是 MSTL(Multiple STL)或 Prophet 的多 Fourier 项。第七章会展开。
如果你的数据是工作日开盘价、且数据只包含工作日,那么"7 天周期"是假的——相邻两个数据点之间没有"周末"。这种情况下:
m=5,而不是 m=7。这是一个工程上极容易犯错的点——机械地套"7 天周季节性"会让模型把周末当周期来学,结果自然离谱。