本节摘要:残差是趋势、季节、周期都被剥掉之后剩下的"什么都不是"的部分。理想残差应当是白噪声(无自相关、零均值、方差恒定)。本节教你怎么用 ACF / Ljung-Box 检验判断残差是否"够白"。
阅读完本节,你应当能够:
本节把"残差"作为最后一位嫌疑犯单独拎出来。它不是"没用的东西"——而是对模型诊断极其关键的信号源。读完后你会在建模流程里加一道"残差必查"的工序。
残差是时间序列分解的"最后一类嫌疑人"——它既不是趋势、也不是季节、也不是周期,而是把所有"有结构"的成分都剥掉之后,剩下的随机扰动。在加法模型里:
y(t) = T(t) + S(t) + C(t) + ε(t)
ε(t) 就是残差。乘法模型里残差以比值形式出现,但本质一样:剥完已知结构,剩下的不可解释部分。
💡 关键直觉:残差不是"没用的东西"。它对模型诊断极其关键——如果残差还有自相关,说明你的分解没把信号剥干净;如果残差呈现异方差,说明业务还有"水平依赖的波动"没被建模。
教科书把"理想残差"定义为白噪声(white noise),它必须同时满足:
E[ε(t)] = 0(任何时间点残差的期望是 0)Var[ε(t)] = σ²(波动幅度不随时间变化——同方差性)k ≥ 1,Cov[ε(t), ε(t+k)] = 0(今天的残差和昨天的无关)第三条最常被违反。很多分析师做分解后只画趋势图就完事,从不检查残差——这种做法几乎一定会把模型调优的信号丢掉。
最直接的检验是看残差的 ACF:
import numpy as np import pandas as pd from statsmodels.tsa.seasonal import seasonal_decompose from statsmodels.tsa.stattools import acf # 构造一段已知信号 + 白噪声 rng = np.random.default_rng(0) n = 365 trend = np.linspace(50, 100, n) weekly = 10 * np.sin(2 * np.pi * np.arange(n) / 7) y = trend + weekly + rng.normal(0, 1, n) s = pd.Series(y, index=pd.date_range("2024-01-01", periods=n)) # 用 seasonal_decompose 分解(加法) decomp = seasonal_decompose(s, model="additive", period=7) resid = decomp.resid.dropna() # 残差的 ACF:理想情况下应当都在 0 附近 resid_acf = acf(resid, nlags=20, fft=True) print({f"lag_{i}": round(resid_acf[i], 3) for i in range(1, 11)}) # 预期:所有 lag 的 ACF 都在 ±0.1 之间
如果某个 lag 处的 ACF 显著大于 0(粗略经验:|ACF| > 0.2 且样本量够大),说明残差里还有自相关结构。最常见的情况是 lag=1 显著为正——意味着今天残差和昨天残差相关,这通常表示趋势没剥干净(用更长窗口的 MA 重新估计趋势)或季节周期没选对。
肉眼判断 ACF 容易看走眼。Ljung-Box 检验给出一个统计量 Q 和对应的 p 值:
from statsmodels.stats.diagnostic import acorr_ljungbox lb = acorr_ljungbox(resid, lags=[5, 10, 15], return_df=True) print(lb) # 解读:lag=10 处的 p 值如果 < 0.05,说明残差在 10 阶以内不白
如果 Ljung-Box 检验拒绝白噪声,工程师的下一步通常是:
| 残差不白的模式 | 修正方向 |
|---|---|
| lag=1 显著为正 | 趋势窗口不够长;改用更稳健的 STL 分解 |
| 季节 lag 处显著 | 季节周期选错;尝试 MSTL 或更长周期 |
| 残差方差随时间变化 | 考虑乘法分解或 GARCH 类模型 |
把趋势、季节、周期都剥掉以后,留下的残差里藏着两个东西:纯随机噪声 + 异常事件。
残差越大,不代表模型越差。残差大可能说明数据本身的不可解释部分多(比如金融市场被突发事件主导)。残差小也不代表模型好(可能过拟合)。判断模型质量,要看残差是否"像白噪声 + 偶尔异常"——这是健康模型的指纹。