7.4 异常值检测与非平稳处理


7.4 异常值检测与非平稳处理

本节摘要:异常值检测和非平稳补救是时序分析的两个"清理"环节。本节给出基于 STL 残差、滚动 Z-score、Isolation Forest 三种异常检测方法,并讨论 KPSS / ADF / PP 三类平稳性检验在"是否需要差分"决策中的角色。

学习目标

阅读完本节,你应当能够:

  1. 用 STL 残差和滚动 Z-score 两种统计方法检测异常值。
  2. 用 Isolation Forest 做基于机器学习的异常检测。
  3. 用 ADF / KPSS / PP 检验判断序列平稳性,并决定差分阶数。

一、异常值 vs 业务突变

首先澄清一个关键区分:

  • 异常值(outlier):观测中的极端值,可能是数据错误(传感器漂移、埋点漏报)。
  • 业务突变(regime change):业务本身发生结构性变化(政策变更、产品发布),不是数据问题。

业务突变不应当被当作异常值剔除——它就是真实业务的一部分。处理办法通常是把它纳入模型(Prophet 变点检测、ARIMAX 干预变量),而不是删掉。

异常值检测只用于真实的数据错误罕见事件(黑天鹅)

二、方法 1:基于 STL 残差的异常检测

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σ 之外的点,多半是结构性突变。

三、方法 2:滚动 Z-score

滚动 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 让边缘不偏。

四、方法 3:基于 Isolation Forest

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 试一次;如果效果不好,再考虑删除或插值。永远不要"全自动"删除异常——业务方对每个被删除的点都要知情。

六、平稳性检验:ADF / KPSS / PP

回到 3.3 节的差分决策。三种主流检验:

1. ADF(Augmented Dickey-Fuller)

  • 原假设 H₀:序列有单位根(非平稳)。
  • p < 0.05 → 拒绝 H₀ → 序列平稳。
from statsmodels.tsa.stattools import adfuller adf_stat, adf_p, *_ = adfuller(s) print(f"ADF p = {adf_p:.4f}")

2. KPSS(Kwiatkowski-Phillips-Schmidt-Shin)

  • 原假设 H₀:序列平稳(与 ADF 相反)。
  • p < 0.05 → 拒绝 H₀ → 序列非平稳。
from statsmodels.tsa.stattools import kpss kpss_stat, kpss_p, *_ = kpss(s, regression="c") print(f"KPSS p = {kpss_p:.4f}")

3. PP(Phillips-Perron)

  • 与 ADF 类似,但对序列自相关和异方差不敏感。
  • 适合 ADF 检验结果不显著但又怀疑非平稳的场景。
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 模糊(数据不够)

组合原则

  • ADF 拒绝 + KPSS 不拒绝 → 强证据平稳。
  • ADF 不拒绝 + KPSS 拒绝 → 强证据非平稳。
  • 两边一致拒绝 → 可能是趋势平稳(围绕趋势平稳)需要 detrend。
  • 两边一致不拒绝 → 数据量不够。

八、差分阶数的兜底决策

如果平稳性检验结论是"非平稳",差分几次?经验流程:

  1. 跑 ADF,p > 0.05 → 一阶差分。
  2. 跑 ADF on diff1,p > 0.05 → 二阶差分。
  3. 跑 ADF on diff2,p > 0.05 → 三阶差分(几乎不会到这一步)。
  4. 如果一阶差分后 ADF p > 0.05 但二阶过拟合(残差方差增大)→ 回一阶,用 SARIMA 的季节差分。
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}")

本节要点回顾

  • 异常值 vs 业务突变:前者是数据问题,后者是业务本身。处理方式不同。
  • 三种异常检测方法:STL 残差(统计最稳)、滚动 Z-score(局部敏感)、Isolation Forest(机器学习)。
  • 异常处理策略:Winsorize 优先,删除要业务方知情,插值会平滑业务节奏。
  • 平稳性检验组合:ADF + KPSS 共同判读,模糊结果要查数据量。
  • 差分阶数:从 d=0 起步,ADF 不拒绝就加 1,超过 2 阶几乎都是过拟合。
  • 教程收尾:到这里,从"什么是时间序列"到"差分兜底"已经覆盖完整工作流。后续可扩展到 GARCH、状态空间、神经网络等更专门的话题。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U