本节摘要:异常值检测和非平稳补救是时序分析的两个"清理"环节。本节给出基于 STL 残差、滚动 Z-score、Isolation Forest 三种异常检测方法,并讨论 KPSS / ADF / PP 三类平稳性检验在"是否需要差分"决策中的角色。
阅读完本节,你应当能够:
首先澄清一个关键区分:
业务突变不应当被当作异常值剔除——它就是真实业务的一部分。处理办法通常是把它纳入模型(Prophet 变点检测、ARIMAX 干预变量),而不是删掉。
异常值检测只用于真实的数据错误或罕见事件(黑天鹅)。
STL 分解后,残差应当近似白噪声。如果某个残差点显著偏离 ±3σ,大概率是异常值或业务突变:
from statsmodels.tsa.seasonal import STL stl = STL(s, period=7, robust=True).fit() resid = stl.resid.dropna() # 异常检测:残差绝对值 > 3 倍标准差 threshold = 3 * resid.std() outliers = resid[resid.abs() > threshold] print(outliers)
robust=True 让 STL 对异常值鲁棒,这意味着 STL 已经"容忍"了大部分异常点——残差里如果还有 ±3σ 之外的点,多半是结构性突变。
滚动 Z-score 用滚动均值和标准差作为"局部基线":
window = 30 rolling_mean = s.rolling(window=window, center=True).mean() rolling_std = s.rolling(window=window, center=True).std() z_score = (s - rolling_mean) / rolling_std # 异常检测:|z| > 3 outliers = s[z_score.abs() > 3]
滚动 Z-score 的优势是对局部水平变化敏感——序列的水平从 100 涨到 200 后,100 附近的"正常"在滚动视角下也是异常。
⚠️ 常见坑:滚动 Z-score 在窗口边缘会失效(数据不足)。要么
min_periods=window强制完整窗口,要么用center=True让边缘不偏。
Isolation Forest 是机器学习的异常检测方法,对多维特征友好。时序场景下要先把序列转为"特征矩阵":
from sklearn.ensemble import IsolationForest import pandas as pd # 构造特征:y 本身 + 滚动统计量 + lag 特征 df_feat = pd.DataFrame({"y": s}) df_feat["rolling_mean_7"] = s.rolling(7).mean() df_feat["rolling_std_7"] = s.rolling(7).std() df_feat["lag1"] = s.shift(1) df_feat["lag7"] = s.shift(7) df_feat = df_feat.dropna() # 训练 Isolation Forest iso = IsolationForest(contamination=0.01, random_state=0) iso.fit(df_feat) labels = iso.predict(df_feat) # -1 = 异常, 1 = 正常 outliers = df_feat[labels == -1] print(outliers)
contamination=0.01 表示预期异常占 1%。业务上根据实际异常率调整(罕见故障场景 0.1%,一般监控 1–5%)。
三种策略各有适用场景:
| 策略 | 适用场景 | 风险 |
|---|---|---|
| 删除 | 极端异常(明显数据错误) | 丢失真实业务信号 |
| 替换为 NaN + 插值 | 偶尔缺失 | 业务节奏被平滑 |
| Winsorize(缩尾) | 想保留信号但降低影响 | 仍会扭曲分布 |
| 保留并显式建模 | 业务突变(重要事件) | 模型变复杂 |
经验法则:先 Winsorize 试一次;如果效果不好,再考虑删除或插值。永远不要"全自动"删除异常——业务方对每个被删除的点都要知情。
回到 3.3 节的差分决策。三种主流检验:
from statsmodels.tsa.stattools import adfuller adf_stat, adf_p, *_ = adfuller(s) print(f"ADF p = {adf_p:.4f}")
from statsmodels.tsa.stattools import kpss kpss_stat, kpss_p, *_ = kpss(s, regression="c") print(f"KPSS p = {kpss_p:.4f}")
from statsmodels.tsa.stattools import adfuller # PP 检验可以用 newey-west 调整(在 statsmodels 中用 regression='ctt' 选项)
| ADF | KPSS | 结论 |
|---|---|---|
| p < 0.05(平稳) | p > 0.05(平稳) | 序列平稳 |
| p > 0.05(非平稳) | p < 0.05(非平稳) | 序列非平稳 |
| p < 0.05 | p < 0.05 | 模糊(趋势平稳 vs 差分平稳) |
| p > 0.05 | p > 0.05 | 模糊(数据不够) |
组合原则:
如果平稳性检验结论是"非平稳",差分几次?经验流程:
def find_d(s, max_d=3): for d in range(max_d + 1): s_diff = s.diff(d).dropna() if d > 0 else s stat, p, *_ = adfuller(s_diff) print(f"d={d}: ADF stat={stat:.3f}, p={p:.4f}") if p < 0.05: return d return max_d optimal_d = find_d(s) print(f"推荐 d = {optimal_d}")