本节摘要:普通横截面回归在时间序列上几乎必然失败,因为它把"今天"和"昨天"当成独立样本。本节用三个反例(股价随机游走、销量周季节性、气温年周期)证明"专门方法"不是理论洁癖,而是工程刚需。
阅读完本节,你应当能够:
本节用三个真实反例钉死"为什么必须用专门方法":股价随机游走、销量周季节性、趋势漂移。读完后你会带着"朴素基线常常胜出"的反直觉结论进入下一节。
如果有人告诉你"用利率、市盈率、美元指数预测明天收盘价",多半是个伪命题。我们用一段合成数据做实验:
import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression rng = np.random.default_rng(7) n = 500 # 纯随机游走:今天的值 = 昨天的值 + 一个均值为 0 的冲击 price = 100 + np.cumsum(rng.normal(0, 1, size=n)) df = pd.DataFrame({ "lag1": price[:-1], # 昨天的价格 "lag2": price[:-2] if n > 2 else None, # 前天的价格 "y": price[1:] # 今天的价格(要预测的目标) }).dropna() X = df[["lag1", "lag2"]] y = df["y"] model = LinearRegression().fit(X, y) print("R^2 =", model.score(X, y)) # 通常 >0.99
模型的 R² 接近 1,听起来很厉害。但这是因为 lag1(昨天的值)和今天几乎完全一样——回归的"信息"就是 y≈lag1,并不是真的从 lag1、lag2 中提炼出什么"规律"。这种"假高分"对预测未来毫无价值,因为明天的价格不会因为"模型 R² 高"就乖乖落到拟合线上。
更糟糕的是:股价的下一步增量,统计上接近均值为 0 的随机噪声。任何回归方法都找不到"可被复用的结构性关系"——这就是为什么"股价预测"基本是个伪命题(短线技术分析另说,本质是订单流博弈,不是时序统计)。
把上面同样的逻辑放到零售销量上:
rng = np.random.default_rng(11) n_days = 365 * 2 # 两年日数据 trend = np.linspace(100, 200, n_days) # 缓慢上升 weekly = np.tile([0, -10, -10, -5, 5, 25, 30], n_days // 7 + 1)[:n_days] # 周内效应 noise = rng.normal(0, 4, n_days) sales = trend + weekly + noise # 错误做法:把"星期几"作为分类变量做回归 df = pd.DataFrame({"sales": sales, "weekday": np.tile(np.arange(7), n_days // 7 + 1)[:n_days]}) print(df.groupby("weekday")["sales"].mean())
输出会显示:周一到周二销量平均低 10–15,周五周六高 25–30。如果用横截面回归只看"历史销量 → 未来销量",它不会区分"明天是周几",因为横截面里没有"明天是周几"这个特征能塞进特征矩阵。
而时序分析的指数平滑、Prophet、STL 分解等方法,天生就知道"周季节性"是 7 天一循环,把这个结构直接编码进模型。这就是为什么在零售场景,没有"季节性感知"的模型几乎一定输给简单季节性模型——你给它的训练集再大也没用。
最隐蔽的失败模式是趋势漂移。某公司 2020 年开始做促销,销量从月均 1000 件增长到 2024 年的 5000 件。如果用 2018–2019 训练一个回归模型、部署到 2024,预测会稳定在 800–1200 区间——模型完全没看到趋势变化,预测带已经被锚死在旧水平。
解决办法是重新训练或显式建模趋势(差分、Prophet 的 trend 项、线性回归加时间特征)。这也是为什么时序分析强调"持续重训"或"滚动预测"——不能假设历史分布永远成立。
工程上一个反直觉但稳定的结论:对很多日度业务数据,"明天 ≈ 今天" + 一个季节项,往往能跑赢没做特征工程的复杂模型。我们看一段基准对比:
from sklearn.metrics import mean_absolute_error # 朴素季节性预测:明天的销量 ≈ 上周同一天 naive_pred = sales[7:] naive_true = sales[7:] mae_naive = mean_absolute_error(sales[7:], sales[:-7]) # 真正的"上周同一天"基线 # 一个简单指数平滑 from statsmodels.tsa.holtwinters import ExponentialSmoothing es = ExponentialSmoothing(sales, trend="add", seasonal="add", seasonal_periods=7).fit() pred = es.forecast(30) print("基线 MAE:", mean_absolute_error(sales[7:], sales[:-7])) print("指数平滑测试 MAE:", mean_absolute_error(sales[-30:], pred))
在我的实验里,朴素季节性 MAE 通常在 4–5 左右,而复杂回归模型(如果没加"星期几"特征)MAE 经常落在 12–18。这不是模型的错,是它没拿到正确的信息。时序分析的方法族(指数平滑、ARIMA、Prophet)从模型结构上就把"星期几"这种周期信息嵌进去了,所以同等数据条件下表现更稳。
回到时序侦探的视角,上面三个反例可以归纳为三条经验法则:
y_t ≈ y_{t-1} 当基线就够。changepoint_prior_scale、ARIMA 的差分阶数 d,都是为此设计。y_t ≈ y_{t-7} 就能跑赢未加周期特征的复杂模型。