时间序列基础:过去为何能预测未来 本节摘要:过去的业绩确实能预测未来的结果——前提是你先检查平稳性。时间序列(Time Series)打破标准 ML 依赖的假设:样本不独立(今天的股价依赖昨天的),分布不恒定(三月和十二月的销量长得不一样)。你抓起标准 ML 工具箱——随机训练/测试切分、交叉验证——每一步都是错的。一个在随机交叉验证下拿 95% 准确率的模型,在恰当的按时间评估下可能只有 55%。这差别不是技术细节,而是「纸面上能跑」与「生产里能用」的差别。
本节摘要:过去的业绩确实能预测未来的结果——前提是你先检查平稳性。时间序列(Time Series)打破标准 ML 依赖的假设:样本不独立(今天的股价依赖昨天的),分布不恒定(三月和十二月的销量长得不一样)。你抓起标准 ML 工具箱——随机训练/测试切分、交叉验证——每一步都是错的。一个在随机交叉验证下拿 95% 准确率的模型,在恰当的按时间评估下可能只有 55%。这差别不是技术细节,而是「纸面上能跑」与「生产里能用」的差别。本节讲透时间数据为何不同:时间序列是趋势、季节性、残差的组合;平稳性(Stationarity)为何是多数预测方法的前提,如何用差分(Differencing)修复;自相关如何告诉你该用多少滞后特征;滞后特征如何把单列时间序列变成标准监督学习问题;以及为何随机切分在时间序列上是非法的、必须用滚动向前验证(Walk-Forward Validation)。我们会从零实现滞后特征生成、滚动向前切分、自相关函数、简单自回归模型,并对比 ARIMA。
阅读完本节,你应当能够:
你有按时间排序的数据。日销量、小时温度、每分钟 CPU 使用率、周股价。你想预测下一个值、下一周、下一季度。
你抓起标准 ML 工具箱:随机训练/测试切分、交叉验证、特征矩阵输入、预测输出。每一步都是错的。
时间序列打破标准 ML 依赖的假设。样本不独立——今天的温度依赖昨天的。随机切分把未来信息泄漏进过去。在回测里好看的特在生产里失败,因为它们依赖随时间漂移的模式。
一个在随机交叉验证下拿 95% 准确率的模型,在恰当按时间评估下可能只有 55%。差别不是技术细节,而是「纸面能跑」与「生产能用」的差别。
标准 ML 假设 i.i.d.——独立同分布。每个样本从同一分布独立抽取。时间序列两者都违反:
这些违反不是小事。它们改变你如何构造特征、如何评估模型、哪些算法有效。
标准 ML 里样本可互换,打乱它们什么也不改变。时间序列里顺序就是一切,打乱摧毁信号。
每个时间序列是以下成分的组合:
时间序列在统计性质(均值、方差、自相关)不随时间变化时是平稳的(Stationary)。多数预测方法假设平稳性。
为何重要: 非平稳序列的均值会漂移。一月数据训练的模型学到的均值,与二月将显示的不同。它会系统性出错。
如何检查: 在窗口上算滚动均值和滚动标准差。若它们漂移,序列非平稳。
如何修复: 差分(Differencing)。不建模原始值,建模相邻值之差:
diff[t] = value[t] - value[t-1]
若一轮差分未使序列平稳,再差分一次(二阶差分)。多数真实序列最多需要两轮。
例子:
原始序列:[100, 102, 106, 112, 120]
一阶差分:[2, 4, 6, 8](仍向上趋势)
二阶差分:[2, 2, 2](常数——平稳)
原始序列有二次趋势。一阶差分把它变线性。二阶差分把它变平。实践中很少需要超过两轮。
正式检验: 增广迪基-富勒检验(Augmented Dickey-Fuller, ADF)是平稳性的标准统计检验。零假设是「序列非平稳」。p 值低于 0.05 意味着你可以拒绝零假设、认定平稳。我们不从零实现 ADF(它需要渐近分布表),但代码里的滚动统计方法给了一个实用的可视化检查。
自相关(Autocorrelation)度量时间 t 的值与时间 t-k(k 步之前)的值相关性多大。自相关函数(ACF)对每个滞后 k 画出这个相关性。
ACF 告诉你:
PACF(偏自相关函数) 移除间接相关。若今天与 3 天前相关只是因为两者都与昨天相关,则滞后 3 的 PACF 会是零而 ACF 不是。
标准 ML 模型需要特征矩阵 X 和目标 y。时间序列给你一列值。桥梁是滞后特征(Lag Features)。
取序列 [10, 12, 14, 13, 15] 造滞后 1 和滞后 2 特征:
| lag_2 | lag_1 | target |
|---|---|---|
| 10 | 12 | 14 |
| 12 | 14 | 13 |
| 14 | 13 | 15 |
现在你有一个标准回归问题。任何 ML 模型(线性回归、随机森林、梯度提升)都能从滞后预测目标。
你还可以工程的其他特征:
多少滞后? 用自相关函数。若 ACF 显著到滞后 10,至少用 10 个滞后。若有周季节性,包含滞后 7(可能还有 14)。更多滞后给模型更多历史,但也更多特征要拟合,增加过拟合风险。
目标对齐陷阱。 造滞后特征时,目标必须是时间 t 的值,所有特征必须用时间 t-1 或更早的值。如果你不小心把时间 t 的值当特征,你有了一个完美预测器——和一个完全无用的模型。这是时间序列特征工程最常见的 bug。
这是本节最重要的概念。标准 k 折交叉验证随机把样本分到训练和测试。对时间序列,这泄漏未来信息。
滚动向前验证:
每个测试折只含所有训练数据之后的数据。无未来泄漏。这给你模型部署后表现的诚实估计。
扩展窗口(Expanding Window)用所有历史数据训练(窗口增长)。滑动窗口(Sliding Window)用固定大小训练窗口(窗口滑动)。当你相信老数据仍相关时用扩展;当世界变化、老数据有害时用滑动。
ARIMA 是经典时间序列模型。它有三个分量:
ARIMA(p, d, q) 组合三者。你基于 ACF/PACF 分析或自动搜索(auto-ARIMA)选 p、d、q。
我们不从零实现 ARIMA——它需要超出本课范围的数值优化。关键是理解每个分量的作用,这样你能解读 ARIMA 结果并知道何时用它。
| 方法 | 最适合 | 处理季节性 | 处理外部特征 |
|---|---|---|---|
| 滞后特征 + ML | 有许多外部特征的表格 | 配日历特征 | 是 |
| ARIMA | 单变量序列,短期 | SARIMA 变体 | 否(ARIMAX 有限) |
| 指数平滑 | 简单趋势 + 季节性 | 是(Holt-Winters) | 否 |
| Prophet | 商业预测,节假日 | 是(傅里叶项) | 有限 |
| 神经网络(LSTM、Transformer) | 长序列,多序列 | 学习得到 | 是 |
对多数实际问题,滞后特征 + 梯度提升是最强起点。它天然处理外部特征、不要求平稳、易调试。
单步预测预测下一个时间步。多步预测预测多个步。有三种策略:
递归(迭代):预测下一步,把预测当输入再预测下下步。简单但误差累积——每个预测用前一个预测,错误叠加。
直接:为每个范围训一个单独模型。模型 1 预测 t+1,模型 5 预测 t+5。无误差累积,但每个模型训练样本更少且不共享信息。
多输出:训一个模型同时输出所有范围。跨范围共享信息,但需要支持多输出的模型(或自定义损失)。
对多数实际问题,短范围(1~5 步)从递归开始,长范围用直接。
| 错误 | 为何发生 | 如何修 |
|---|---|---|
| 随机训练/测试切分 | 标准 ML 习惯 | 用滚动向前或时间切分 |
| 用未来特征 | 时间 t 的特征被误纳入 | 审计每个特征的时间对齐 |
| 过拟合季节性 | 模型背日历模式 | 测试集留出一整个季节循环 |
| 忽略尺度变化 | 营收翻倍但模式不变 | 建模百分比变化而非绝对值 |
| 滞后特征太多 | 「历史越多越好」 | 用 ACF 定相关滞后 |
| 不差分 | 「模型会搞定」 | 树模型能处理趋势;线性模型需平稳 |
code/time_series.py 从零实现核心构件。
def make_lag_features(series, n_lags): n = len(series) X = np.full((n, n_lags), np.nan) for lag in range(1, n_lags + 1): X[lag:, lag - 1] = series[:-lag] valid = ~np.isnan(X).any(axis=1) return X[valid], series[valid]
这把 1D 序列转成特征矩阵,每行有最近 n_lags 个值作为特征、当前值作为目标。
def walk_forward_split(n_samples, n_splits=5, min_train=50): assert min_train < n_samples, "min_train 必须小于 n_samples" step = max(1, (n_samples - min_train) // n_splits) for i in range(n_splits): train_end = min_train + i * step test_end = min(train_end + step, n_samples) if train_end >= n_samples: break yield slice(0, train_end), slice(train_end, test_end)
每个切分确保训练数据严格在测试数据之前。训练窗口随每折扩展。
纯 AR 模型就是滞后特征上的线性回归:
class SimpleAR: def __init__(self, n_lags=5): self.n_lags = n_lags self.weights = None self.bias = None def fit(self, series): X, y = make_lag_features(series, self.n_lags) # 用正规方程求解 X_b = np.column_stack([np.ones(len(X)), X]) theta = np.linalg.lstsq(X_b, y, rcond=None)[0] self.bias = theta[0] self.weights = theta[1:] return self
这在概念上与第 02 节的线性回归完全相同,只是应用到同一变量的时间滞后版本上。
代码算滚动统计以可视化和数值化评估平稳性:
def check_stationarity(series, window=50): rolling_mean = np.array([ series[max(0, i - window):i].mean() for i in range(1, len(series) + 1) ]) rolling_std = np.array([ series[max(0, i - window):i].std() for i in range(1, len(series) + 1) ]) return rolling_mean, rolling_std
若滚动均值漂移或滚动标准差变化,序列非平稳。施差分再检查。
代码还通过比较序列前半和后半检查平稳性。若均值差超过半个标准差,或方差比超过 2 倍,序列被标记为非平稳。
def autocorrelation(series, max_lag=20): n = len(series) mean = series.mean() var = series.var() acf = np.zeros(max_lag + 1) for k in range(max_lag + 1): cov = np.mean((series[:n-k] - mean) * (series[k:] - mean)) acf[k] = cov / var if var > 0 else 0 return acf
用 sklearn,你直接把滞后特征喂给任意回归器:
from sklearn.linear_model import Ridge from sklearn.ensemble import GradientBoostingRegressor X, y = make_lag_features(series, n_lags=10) for train_idx, test_idx in walk_forward_split(len(X)): model = Ridge(alpha=1.0) model.fit(X[train_idx], y[train_idx]) predictions = model.predict(X[test_idx])
from statsmodels.tsa.arima.model import ARIMA model = ARIMA(train_series, order=(5, 1, 2)) fitted = model.fit() forecast = fitted.forecast(steps=30)
code/time_series.py 演示两种方法并用滚动向前验证对比。
sklearn 提供 TimeSeriesSplit,实现滚动向前验证:
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_index, test_index in tscv.split(X): X_train, X_test = X[train_index], X[test_index] y_train, y_test = y[train_index], y[test_index] model.fit(X_train, y_train) score = model.score(X_test, y_test)
这与我们从零的 walk_forward_split 等价,但集成进了 sklearn 的交叉验证框架。可与 cross_val_score 配合:
from sklearn.model_selection import cross_val_score scores = cross_val_score(model, X, y, cv=TimeSeriesSplit(n_splits=5)) print(f"平均分: {scores.mean():.4f} +/- {scores.std():.4f}")
时间序列预测用回归指标,但带时间感知的语境:
|y_true - y_pred| 的平均。易在原始单位解读。「平均预测偏差 3.2 度。」|误差 / 真值| * 100 的平均。尺度无关,适合跨序列比较。但真值为零时未定义。代码演示在滞后特征上加滚动统计(7 天和 14 天窗口的均值、标准差、最小、最大)。这些给模型近期趋势和波动信息,是滞后特征单独抓不到的。
例如,若滚动均值在升,提示上升趋势;若滚动标准差在增,提示波动加剧。这些是树模型能学、线性模型不能的模式。
| 维度 | 从零实现 | sklearn / statsmodels |
|---|---|---|
| 灵活 | 可看清 ACF、差分内部 | TimeSeriesSplit 集成 CV 框架 |
| 模型 | 简单 AR(线性回归) | 任意回归器 + ARIMA |
| 适用 | 理解原理 | 生产 |
建模前先立基线:
若你的花哨 ML 模型输给季节性朴素基线,你有 bug。最常见原因:特征里的未来泄漏、错误的评估方法、或序列确实随机不可预测。
先画图。 建模前先画原始序列,看趋势、季节性、离群点、结构性断点(行为的突变)。30 秒目视检查常胜过一小时自动分析。
先差分,后建模。 若序列有明显趋势,造滞后特征前先差分。树模型能处理趋势,但线性模型不能,差分从无坏处。
至少留一整个季节循环。 若有周季节性,测试集至少一整周。月度则至少一整月。否则你无法评估模型是否抓到季节模式。
生产中监控。 时间序列模型随世界变化而退化。滚动跟踪预测误差。误差开始增长时,用近期数据重训。
当心制度变化(Regime Change)。 用疫情前数据训的模型预测不了疫情后行为。把已知制度变化的指示符加为特征,或用会遗忘老数据的滑动窗口。
对偏斜序列取对数。 营收、价格、计数常右偏。取对数稳定方差、把乘法模式变加法(线性模型能处理)。在对数空间预测,再指数化回原始单位。
本节产出:
outputs/prompt-time-series-advisor.md——一个帮你框定时间序列问题的提示词。code/time_series.py——滞后特征、滚动向前验证、AR 模型、平稳性检查。平稳性实验:生成带线性趋势的序列,用滚动统计检查平稳性,施一阶差分再检查。二次趋势需要几轮差分?
滞后选择:在季节性序列(周期=7)上算 ACF。哪些滞后自相关最高?只用这些滞后造特征(不用连续滞后)。对比用滞后 1 到 7,准确率有提升吗?
滚动向前 vs 随机切分:在滞后特征上训 Ridge 回归。分别用随机 80/20 切分和滚动向前验证评估。随机切分高估了多少性能?
特征工程:在滞后特征上加滚动均值(窗口=7)、滚动标准差(窗口=7)、星期几特征。用滚动向前验证对比有无这些额外特征的准确率。
多步预测:修改 AR 模型预测 5 步而非 1 步。对比两种策略:(a) 预测一步、把预测当输入再预测下一步(递归);(b) 为每个范围训单独模型(直接)。哪个更准?
diff[t]=value[t]-value[t-1] 修复,多数序列最多两轮。下一节,我们讲异常检测——为何要把问题框定为「建模正常」而非「分类异常」,以及 Z 分数、IQR、孤立森林如何找离群点。