时序预测是最古老也最实用的机器学习任务之一:从库存补货、销量预估到服务器容量规划,几乎每个业务都有「预测下一个周期」的需求。方法栈从统计模型一路演进到大模型,但真正决定效果的往往不是模型,而是对问题形态的理解与特征工程。
时序预测问题定义
时序预测的任务:给定历史观测 $y_1, y_2, \dots, y_t$,预测未来 $y_{t+1}, \dots, y_{t+h}$($h$ 为预测水平)。正确的建模第一步是定义问题,它决定了方法选型:
预测水平(Forecast Horizon, h):短期(小时/天)、中期(周/月)、长期(年)。统计模型擅长短期;长期预测依赖趋势分解与领域知识。
预测粒度(Frequency):分钟/小时/日/周/月,粒度影响特征工程与模型结构。
数据形态:单变量(只预测目标序列)vs 多变量(用多个相关序列互作特征);平稳 vs 带趋势/季节;是否含外生变量(促销、节假日、天气)。
| 问题形态 | 典型场景 | 推荐方法 |
|---|---|---|
| 单变量、短中期 | 简单销量预测 | ARIMA / ETS |
| 单变量、强季节 | 日度零售 | 季节分解 + ETS |
| 多变量 + 外生 | 销量 + 促销/天气 | LightGBM / XGBoost |
| 长序列 + 复杂模式 | 能源、流量 | LSTM / TCN / Transformer |
| 大规模多序列 | 全 SKU 预测 | 全局模型(LightGBM 单模型) |
import numpy as np
import pandas as pd
def make_series(n=365*2, period=7, trend=0.02, noise=1.0):
"""构造含趋势与周期的演示序列。"""
t = np.arange(n)
y = trend * t + 10 * np.sin(2 * np.pi * t / period)
y += noise * np.random.randn(n)
return pd.Series(y, index=pd.date_range("2024-01-01", periods=n, freq="D"))
series = make_series()
print(series.head())
起始铁律:先判断数据形态,再选模型。带强趋势/季节的数据用 ARIMA 之前必须先差分或做季节分解;多变量场景先想清楚「哪些外生变量对未来有预测力」——用不可预测的变量(如未来天气)做特征反而引入噪声。
经典统计方法:ARIMA 与 ETS
ARIMA(Autoregressive Integrated Moving Average) 是单变量时序的统计基准:由自回归项 AR(p)、差分 I(d)、移动平均 MA(q) 组成,记作 ARIMA(p,d,q)。带季节性的扩展是 SARIMA(p,d,q)(P,D,Q,s)。
$$y_t = c + \sum_{i=1}^{p} \phi_i y_{t-i} + \varepsilon_t + \sum_{j=1}^{q} \theta_j \varepsilon_{t-j}$$
ETS(Error-Trend-Seasonality,指数平滑三指数族):把序列分解为误差、趋势、季节三部分,用指数平滑递推。ETS 对趋势与季节的建模比 ARIMA 更直观,适合强季节、可解释优先的场景。
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.seasonal import seasonal_decompose
# 季节分解:把序列拆成 trend / seasonal / resid
series = make_series()
result = seasonal_decompose(series, model="additive", period=7)
trend, seasonal, resid = result.trend, result.seasonal, result.resid
# ARIMA 拟合与预测
model = ARIMA(series, order=(1, 1, 1), seasonal_order=(1, 1, 1, 7))
fit = model.fit()
forecast = fit.forecast(steps=14) # 预测未来 14 天
print(forecast)
参数选择:p/q/d 用 ACF/PACF 图目测或用 AIC / BIC 准则网格搜索;季节性周期 s 由业务周期决定(日数据 s=7,月数据 s=12)。auto_arima(pmdarima 库)是工程上快速定参的标准工具。
| 对比 | ARIMA | ETS |
|---|---|---|
| 建模方式 | 差分 + 自回归/移动平均 | 误差/趋势/季节指数平滑 |
| 季节处理 | SARIMA 显式周期 | 季节分量 |
| 可解释性 | 中 | 高 |
| 长期预测 | 趋势外推、易发散 | 相对稳健 |
| 适用 | 平稳化后的单变量 | 强季节单变量 |
统计模型的定位是基线与小数据场景:数据量小(<1 年)、单变量、要可解释时,ARIMA/ETS 仍是首选;数据量足够且模式复杂时,统计模型让位给 ML/DL。
分解与平稳性
时序建模前需要诊断两个性质:平稳性(Stationarity) 与季节性(Seasonality)。
平稳性要求序列的均值、方差不随时间变化。趋势与季节性都是非平稳的来源。处理手段:差分(Differencing) 消除趋势,季节差分 消除季节,或对原始序列做变换(log/Box-Cox)。
ADF 检验(Augmented Dickey-Fuller) 是判断平稳性的标准工具:p 值 < 0.05 拒绝非平稳原假设,即序列平稳。
from statsmodels.tsa.stattools import adfuller
def check_stationarity(series: pd.Series) -> bool:
result = adfuller(series.dropna())
print(f"ADF p-value = {result[1]:.4f}")
return result[1] < 0.05 # True 表示平稳
print("原始序列平稳?", check_stationarity(series))
diff = series.diff().dropna()
print("一阶差分后平稳?", check_stationarity(diff))
分解方法论:经典的是 STL(Seasonal-Trend decomposition using Loess),把序列拆为趋势、季节、残差三部分,各部分可分别建模。分解的两个目的:
- 诊断:看清趋势强度、季节模式、异常点
- 建模:对趋势建模(线性/多项式回归),对季节分量建模(one-hot 或周期特征),对残差建模(平稳模型)
原始序列 ──STL──→ [趋势 Trend] + [季节 Seasonal] + [残差 Resid]
│ │ │
线性/多项式回归 周期哑变量 ARIMA/平滑
└──────────────┴──────────────┘
预测 = 三部分求和
分解是把复杂序列「拆成简单部分各击破」的核心思想,也是后续特征工程的基础。任何时序建模前都应先做分解诊断——它能告诉你模型该用多长的季节周期、趋势是否单调、以及是否存在异常点需要剔除。
机器学习方法:LightGBM 特征工程
当有外生变量、多序列、数据量大时,回归式监督学习成为主流:把时序预测改造成「用历史窗口特征预测未来目标」的表格回归问题。
核心是特征工程。LightGBM/XGBoost 这类树模型不需要归一化,但对特征的信息量极度敏感:
| 特征族 | 特征示例 | 作用 |
|---|---|---|
| 滞后特征(Lag) | y_{t-1}, y_{t-7}, y_{t-14} | 捕获自相关 |
| 滚动统计(Rolling) | 7 日均值、30 日标准差、最大/最小 | 捕获局部水平与波动 |
| 时间特征 | 星期几、月份、小时、节假日 | 捕获日历规律 |
| 外生变量 | 促销力度、天气、竞品活动 | 驱动变化的因素 |
| 差分特征 | y_t - y_{t-1},周环比 | 捕获变化率 |
import lightgbm as lgb
import numpy as np
import pandas as pd
def build_features(df: pd.DataFrame, lags=[1, 7, 14], windows=[7, 30]):
df = df.copy()
df["dayofweek"] = df.index.dayofweek
df["month"] = df.index.month
df["is_holiday"] = df.index.isin(holidays)
for lag in lags:
df[f"lag_{lag}"] = df["y"].shift(lag)
for w in windows:
df[f"rolling_mean_{w}"] = df["y"].rolling(w).mean()
df[f"rolling_std_{w}"] = df["y"].rolling(w).std()
return df.dropna()
train = build_features(df_train)
X, y = train.drop("y", axis=1), train["y"]
model = lgb.LGBMRegressor(
n_estimators=800, learning_rate=0.05, num_leaves=63,
subsample=0.9, colsample_bytree=0.9, reg_alpha=0.1,
random_state=42,
)
model.fit(X, y, eval_set=[(X_val, y_val)],
callbacks=[lgb.early_stopping(50)])
print("特征重要度 TOP5:", model.feature_importances_)
防数据泄露(Label Leakage) 是时序特征工程的头号陷阱:
- 滚动统计必须只用历史数据(
shift后再 rolling,禁止用未来) - 划分训练/验证集不能随机打乱,必须按时间顺序(TimeSeriesSplit)
- 特征计算的「窗口边界」要留出预测水平 h 的空隙(gap),否则训练时看到未来的标签
from sklearn.model_selection import TimeSeriesSplit
# 时序交叉验证:保证只学过去、不偷看未来
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, val_idx in tscv.split(X):
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
# 训练 + 评估(绝不 shuffle)
LightGBM 路线是多数生产时序系统的首选:可解释(特征重要性)、抗过拟合(正则化)、支持外生变量、训练快。深度方法的优势在于长依赖与复杂模式,但训练成本高、可解释性差,通常作为进阶选项。
深度方法:LSTM 与 TCN
深度学习捕捉时序中的复杂非线性模式,适合长序列与多维特征。
LSTM:通过门控机制建模长程依赖。对时序而言通常用序列到序列结构:用过去 $w$ 步的特征窗口预测未来 $h$ 步。
TCN(Temporal Convolutional Network):基于因果卷积(causal convolution)+ 膨胀卷积(dilated convolution),感受野随层数指数增长,并行性好、梯度稳定,训练比 LSTM 快且内存占用低。
import torch
import torch.nn as nn
class TCNBlock(nn.Module):
"""膨胀因果卷积块:dilation 指数扩大感受野。"""
def __init__(self, in_ch, out_ch, kernel_size=3, dilation=2):
super().__init__()
self.conv = nn.Conv1d(
in_ch, out_ch, kernel_size, dilation=dilation,
padding=(kernel_size - 1) * dilation,
)
self.chomp = nn.ConstantPad1d((0, -(kernel_size - 1) * dilation), 0)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.2)
def forward(self, x):
return self.dropout(self.relu(self.chomp(self.conv(x))))
# 3 层 TCN,dilation = 1,2,4 → 感受野 = 1 + 2*(2^0+2^1+2^2)*kernel
| 对比 | LSTM | TCN | Transformer |
|---|---|---|---|
| 长程依赖 | 门控记住 | 膨胀感受野 | 全局注意力 |
| 并行训练 | 差(串行) | 好 | 最好 |
| 训练速度 | 慢 | 快 | 中 |
| 参数量 | 中 | 中 | 高 |
| 适用 | 中等长度 | 长序列、效率优先 | 超长序列、多变量 |
训练要点:时序深度学习必须先做归一化(StandardScaler/MinMaxScaler),且缩放器只用训练集统计量拟合,防止验证/测试泄露;预测输出是未来 $h$ 步时,损失函数用 MAE(对离群更稳健)或 Pinball Loss(分位数)。
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_norm = scaler.fit_transform(X_train) # 只用训练集 fit
X_val_norm = scaler.transform(X_val) # 验证集复用同一缩放器
X_test_norm = scaler.transform(X_test)
深度方法上线前必须回答一个现实问题:它比 LightGBM 好在哪? 如果数据集不大、模式不复杂,LSTM 很可能打不过特征工程充分的 LightGBM。深度方法的价值场景是:多变量、长依赖、非线性强、数据量百万级以上。
Transformer 与时序基础模型
Transformer 把注意力机制带入时序,关键改进是**通道独立(Channel-Independent)与分块注意力(Patch)**设计——时序的每个变量单独建模,把序列切块后注意力在 patch 间计算,兼顾效率与长程依赖。代表工作包括 PatchTST、TimesNet、Autoformer。
时序基础模型(Time Series Foundation Models) 是 2023 年后的新方向:在大规模多领域时序语料上预训练,通过 Zero-shot / Few-shot 适配新任务。代表包括 TimeGPT、TimesFM、Moirai、Chronos。
# TimesFM(Google)时序基础模型:Zero-shot 预测
import torch
from transformers import TimesFMForPrediction
model = TimesFMForPrediction.from_pretrained("google/timesfm-2.0-500m")
context = torch.tensor([series_values], dtype=torch.float32)
output = model.generate(
input_ids=context, # 历史序列(context)
num_return_sequences=1,
max_new_tokens=14, # 预测未来 14 步
temperature=0.0, # 确定性预测
)
print(output[0].tolist())
| 模型 | 类型 | 核心思想 | 适用 |
|---|---|---|---|
| LSTM | 循环 | 门控长程记忆 | 中小数据 |
| TCN | 卷积 | 膨胀因果卷积 | 长序列高效 |
| PatchTST | 注意力 | Patch + 通道独立 | 多变量 |
| TimesNet | 注意力 | 周期内/周期间二维分解 | 强周期数据 |
| Chronos | 基础模型 | token 化时序 + LLM 架构 | Zero-shot |
| TimesFM | 基础模型 | 大规模预训练 | Zero-shot 基线 |
时序基础模型的工程定位是零样本基线:接入成本极低、可解释性依赖上下文 prompt,但长期预测精度仍未必超过「充分特征工程 + LightGBM」或专门训练的深度模型。建议作为「快速原型与对照组」,再决定是否专门训练。
多步预测与滚动预测
预测未来 $h$ 步有三条主流策略,它们的偏差-方差权衡完全不同:
直接多步(Direct Multi-step):为每个步长训练一个模型(或一个模型多输出头)。无误差累积,但每个步长模型只能看到「固定窗口」,无法利用已预测的值。实现简单,是生产默认。
递归多步(Recursive / Iterated):用单步模型迭代——把上一步的预测当作下一步的特征。能利用自身预测,但误差会累积,长水平下方差放大。
直接递归混合(DirRec):前几步直接预测、后续递归,在两者间折中。
def recursive_forecast(model, history, steps, exog_future=None):
"""递归预测:上一步预测值作为下一步的滞后特征。"""
preds = []
for s in range(steps):
features = build_last_window(history, s, exog_future)
yhat = model.predict(features)[0]
preds.append(yhat)
history = np.append(history, yhat) # 用预测值续历史
return preds
滚动预测(Rolling Forecast) 是生产中最常见的部署形态:每经过一个周期(如每天),用最新数据重新训练/预测,保证模型紧跟最新趋势。评估也应采用滚动方式——回测(Backtesting):在历史区间上滚动训练 + 预测 + 计算误差,模拟真实部署。
| 策略 | 误差累积 | 模型数量 | 工程复杂度 | 推荐 |
|---|---|---|---|---|
| 直接多步 | 无 | h 个 | 中 | 生产默认 |
| 递归 | 有 | 1 个 | 低 | 步长短时 |
| 直接+递归 | 部分 | h 个 | 中高 | 折中 |
| 滚动重训 | 取决于模型 | 持续 | 高 | 上线形态 |
工程经验:短期预测(h ≤ 7)直接多步 + 单模型多输出头最简单;长期预测(h ≥ 30)优先「先拟合趋势 + 季节,再对残差滚动预测」的组合,可显著抑制误差累积。
评估指标
时序评估指标的选择直接影响模型选型方向,且不同业务对不同方向的误差敏感度不同。
| 指标 | 公式 | 特性 | 适用 |
|---|---|---|---|
| MSE | $\frac{1}{n}\sum(y-\hat y)^2$ | 放大异常 | 大误差代价高 |
| RMSE | $\sqrt{MSE}$ | 与 y 同量纲 | 通用默认 |
| MAE | $\frac{1}{n}\sum | y-\hat y | $ |
| MAPE | $\frac{1}{n}\sum\frac{ | y-\hat y | }{ |
| SMAPE | 对称化 MAPE | 处理零值 | 零值多的序列 |
| Pinball | 分位数损失 | 非对称 | 业务决策/库存 |
MAPE 的陷阱:当真实值 $y$ 接近 0 时,MAPE 会爆炸(除以很小的数),所以零值多的序列用 SMAPE 或 MASE(用基准模型归一化的绝对误差)。
Pinball Loss 是分位数预测的损失函数,直接优化「第 q 分位预测」,让预测结果携带不确定性区间,适合库存(避免缺货与积压的权衡):
$$\text{Pinball}(y, \hat y_q) = \begin{cases} q(y - \hat y_q) & y \ge \hat y_q \ (q-1)(y - \hat y_q) & y < \hat y_q \end{cases}$$
import numpy as np
def pinball_loss(y_true, y_pred, q=0.8):
diff = y_true - y_pred
return np.mean(np.where(diff >= 0, q * diff, (q - 1) * diff))
# 输出 80% 分位预测:只关心「以 80% 概率不被超卖的库存量」
inventory_q80 = model.predict(x) # 模型输出 q=0.8 分位
loss = pinball_loss(y_true, inventory_q80, q=0.8)
指标要与业务决策绑定:库存场景用 Pinball(不对称成本:缺货 vs 积压成本不同);容量规划用 P99(极端负载);销量预测用 MAPE/SMAPE(相对误差)。单一的 MSE 往往掩盖「系统性低估还是高估」的重要信息,建议同时看偏倚(Bias = mean(y - ŷ))。
生产部署与案例
时序模型的生产化比静态 ML 多了「时间维度」的复杂性。完整闭环:
在线推理:每周期滚动预测。常用 Scheduled Retraining(定时重训) 或 Retrain on Drift(漂移触发重训)。推理链路需要考虑模型更新期间的一致性。
# 生产滚动预测服务的简化实现(FastAPI)
from fastapi import FastAPI
import joblib
app = FastAPI()
model = joblib.load("forecast_model.pkl")
@app.post("/forecast")
def forecast(body: dict):
"""输入:最新历史序列 + 外生变量;输出:未来 14 步预测 + 分位数。"""
history = body["history"] # 最近窗口的观测值
exog = body.get("exog", {}) # 促销、节假日等
point = model.predict(history, exog) # 点预测
q_low = model.predict_quantile(history, exog, 0.1) # 10% 分位
q_high = model.predict_quantile(history, exog, 0.9) # 90% 分位
return {"forecast": point, "interval": [q_low, q_high]}
监控与漂移:时序模型天然会「过期」——趋势突变、季节模式改变(疫情、政策)都会让模型失效。监控体系应包含:
- 预测误差监控:实际值出来后对比预测,MAPE 突增告警
- 分布漂移:输入特征分布与训练期对比(PSI/KL)
- 回滚机制:保留多版本模型,劣化时回滚到最近的好版本
# 生产监控告警配置示例
monitoring:
metrics:
- mape_7d: { threshold: 0.25, action: "warn" }
- mape_30d: { threshold: 0.35, action: "retrain" }
drift:
feature_psi: { threshold: 0.25, action: "investigate" }
retraining:
schedule: "daily" # 每日滚动重训
data_window: "90d" # 用最近 90 天数据
案例:零售库存补货预测。业务目标:对 5000 个 SKU 预测未来 7 天销量,指导补货,降低缺货率与积压。
| 环节 | 做法 |
|---|---|
| 数据 | 2 年日度销量 + 促销日历 + 节假日 + 天气 |
| 方法 | LightGBM 全局模型(单模型学所有 SKU + SKU ID 特征) |
| 特征 | 滞后 1/7/14 天、7/30 日滚动均值、星期/月份/节假日 |
| 输出 | 点预测 + q=0.8 分位(补货量取 80% 分位防缺货) |
| 部署 | 每日 6 点批量预测 5000 SKU,写入库存系统 |
| 监控 | MAPE、缺货率、积压率按月复盘 |
分位数实战意义:补货量用点预测容易在促销季缺货;用 80% 分位预测补货,虽然略增库存,但缺货损失(丢失顾客)远大于积压成本(仓储),整体收益更高——这就是 Pinball 指标与业务决策挂钩的典型体现。
生产铁律:预测的价值在决策,不在分数。一个 MAPE 稍高但能稳定输出可靠分位区间的模型,远胜一个「平均误差好看但关键时刻离谱」的模型。上线前务必把预测结果接到决策动作上验证收益。
总结
| 方法族 | 代表方法 | 数据需求 | 优点 | 局限 |
|---|---|---|---|---|
| 统计 | ARIMA / ETS | 小、单变量 | 可解释、快 | 外生/多变量弱 |
| 机器学习 | LightGBM / XGBoost | 中、多特征 | 特征灵活、可解释 | 长依赖弱 |
| 深度学习 | LSTM / TCN | 大、多变量 | 长依赖、非线性 | 训练贵、解释难 |
| 注意力 | PatchTST / TimesNet | 大 | 全局依赖 | 数据门槛高 |
| 基础模型 | TimesFM / Chronos | Zero-shot | 快速基线 | 长期精度存疑 |
时序预测的方法选型应遵循「由简入繁」:先统计模型建基线,再上特征工程充分的 LightGBM,数据量大、模式复杂再考虑深度方法,最后用滚动回测与多指标评估决定最终方案。预测的成败不取决于模型多么先进,而取决于对业务周期、特征信息与误差结构的理解深度。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。