本节摘要:识别趋势有三种主流工具——简单移动平均(SMA)、LOESS 局部加权回归、参数回归(线性/多项式)。本节对比三种工具的稳健性、对异常值的容忍度、对窗口的敏感性,并用一段合成数据演示差异。
阅读完本节,你应当能够:
本节进入"识别"环节:用一段含异常点的合成数据对比三种工具的稳健性,并教你用 Ljung-Box 检验帮选窗口/带宽。
| 工具 | 数学含义 | 关键超参 | 优点 | 缺点 |
|---|---|---|---|---|
| 简单移动平均 (SMA) | 窗口内取平均 | 窗口大小 | 简单、快、可解释 | 滞后、矩形窗有边界效应 |
| 加权移动平均 (WMA) | 中心点权重大 | 权重函数 | 比 SMA 滞后小 | 仍对异常值敏感 |
| 指数加权 (EWMA) | 权重按指数衰减 | 衰减系数 α | 对最新值敏感 | 仍对异常值敏感 |
| LOESS | 局部加权回归 | 带宽(bandwidth) | 局部拟合,非参数 | 计算慢、带宽需调 |
| 参数回归 | 整段拟合一个函数 | 函数族 + 阶数 | 解释性强 | 对异常值敏感、对函数族敏感 |
下面这段代码用 statsmodels 和 scipy 演示同一段含噪数据在三种方法下的趋势估计:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.nonparametric.smoothers_lowess import lowess # 合成:S 形趋势 + 高斯噪声 + 2 个异常点 rng = np.random.default_rng(7) n = 200 t = np.arange(n) trend_true = 1000 / (1 + np.exp(-0.05 * (t - 100))) y = trend_true + rng.normal(0, 15, n) y[40] = 2000 # 注入异常 y[150] = -500 # 注入异常 s = pd.Series(y, index=pd.date_range("2024-01-01", periods=n)) # 方法 1:简单移动平均(中心化,窗口 15) s_sma = s.rolling(window=15, center=True, min_periods=1).mean() # 方法 2:LOESS 局部加权回归(带宽 0.2 = 用 20% 的局部数据) loess_fit = lowess(s.values, np.arange(n), frac=0.2, it=3, return_sorted=False) s_loess = pd.Series(loess_fit, index=s.index) # 方法 3:参数回归(三阶多项式) coef = np.polyfit(t, y, deg=3) s_poly = pd.Series(np.polyval(coef, t), index=s.index) fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(s.index, s.values, ".", alpha=0.4, label="原始") ax.plot(s.index, trend_true, "k--", label="真实趋势") ax.plot(s.index, s_sma, label="SMA(15)") ax.plot(s.index, s_loess, label="LOESS") ax.plot(s.index, s_poly, label="多项式(deg=3)") ax.legend() plt.tight_layout()
从图上可以看到三种方法对异常值的反应:
💡 关键直觉:异常值多时优先 LOESS,异常值少且解释性优先时选参数回归。在工业界 LOESS 是 STL 分解的核心组件,被 statsmodels 的
seasonal_decompose(..., model="additive")默认采用。
三种方法都涉及"看多远的过去"——SMA 的窗口、LOESS 的带宽、参数回归的多项式阶数。这个超参没有银弹,但有经验法则:
# 用 Ljung-Box 检验帮窗口选择 from statsmodels.stats.diagnostic import acorr_ljungbox from statsmodels.tsa.seasonal import seasonal_decompose for window in [7, 14, 30, 60]: decomp = seasonal_decompose(s, model="additive", period=window, extrapolate_trend="freq") resid = decomp.resid.dropna() p = acorr_ljungbox(resid, lags=[10], return_df=True)["lb_pvalue"].iloc[0] print(f"period={window}, lag=10 的残差 Ljung-Box p 值 = {p:.3f}") # 选 p 值最大的那个 period:残差最白,窗口最合适
LOESS 在序列两端的"边界"会变窄——因为窗口往左或往右没有足够邻居。statsmodels 的 lowess 默认会用 return_sorted=True 返回的拟合在外侧偏短,业务上需要的话可以加 it=3(迭代 3 次稳健拟合)来减少异常值对边界的影响。
最后一条容易忽略的原则:趋势估计的"窗口/带宽"应当与业务关注周期对齐。
不是技术问题,是问题对齐问题。
