AI 工程基础体系 · 第 53/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

时间序列机器学习:窗口、特征、回测、漂移与泄漏防护

时间序列机器学习处理的不是一组可以任意打乱的独立样本,而是一条带有时间顺序、可用性约束和状态变化的数据流。预测系统真正要回答的问题不是“模型能否拟合历史数据”,而是:

在预测时刻 tt,系统当时实际能看到哪些信息?用这些信息训练出的模型,在未来时刻 t+ht+h 是否仍然有效?

其中,tt 是预测基准时间,hh 是预测步长或预测时距。对销量预测而言,可能在周一预测下周销量;对风控而言,可能在交易完成后预测未来 24 小时风险;对生成式 AI 应用而言,可能根据截至某一时间点的文档和事件流生成预测或决策建议。

这两个问题分别对应两类核心约束:

  1. 时间因果约束:训练样本不能使用预测时刻之后的信息。
  2. 部署可用性约束:特征不仅要在数据表中存在,还必须在真实预测时刻及时、稳定、合法地获得。

一、先形式化任务:预测的对象、时点和信息集

设时间序列观测为:

y1,y2,,yTy_1, y_2, \ldots, y_T

如果要预测 t+ht+h 时刻的值,则预测目标是:

yt+hy_{t+h}

预测模型只能使用预测时刻 tt 之前可用的信息。将这些信息表示为信息集:

It\mathcal{I}_t

它可能包括:

  • 截至 tt 的历史目标值;
  • 截至 tt 已产生并已到达的外生变量;
  • 日历信息,例如星期几、节假日;
  • 截至 tt 发布的业务规则或文档;
  • 模型、特征处理器和参数的当前版本。

理想的预测函数写作:

y^t+h=fθ(It)\hat{y}_{t+h} = f_{\theta}(\mathcal{I}_t)

这里:

  • fθf_{\theta} 是模型;
  • θ\theta 是训练得到的参数;
  • y^t+h\hat{y}_{t+h} 是对未来目标的预测。

关键点在于,It\mathcal{I}_t 不是“数据库中时间戳小于等于 tt 的所有数据”。真实系统还需要考虑数据发布时间数据修订时间

例如,某天的 GDP 统计值对应观测期 tt,但在 t+30t+30 才首次发布;如果回测直接使用最终修订后的 GDP,就模拟了当时不可能拥有的信息。这类问题称为可用性泄漏,或更具体地说是“发布延迟与修订历史未建模”。

1. 预测时间、事件时间和到达时间

时间序列系统中至少应区分三个时间:

  • 事件时间:事实发生的时间,例如订单创建时间;
  • 发布时间或到达时间:数据进入预测系统的时间;
  • 预测时间:模型作出预测的时间。

假设某传感器在 10:00 采集数据,但网络延迟到 10:05 才到达系统:

  • 事件时间是 10:00;
  • 到达时间是 10:05;
  • 若模型在 10:02 运行,就不能使用这条数据;
  • 若模型在 10:06 运行,才可以使用。

因此,时间序列数据最好保存类似如下字段:

event_time      事件发生时间
available_time  数据可被预测系统使用的时间
value           观测值
revision        数据版本或修订号

只按 event_time 排序而忽略 available_time,是生产回测中最常见的隐蔽错误之一。


二、窗口:把连续历史变成训练样本

1. 滑动窗口的基本形式

机器学习模型通常接收固定形状的输入,因此需要把连续序列切分成样本。

设:

  • 输入窗口长度为 LL
  • 预测步长为 hh
  • 当前预测锚点为 tt

最基本的单变量输入是:

Xt=[ytL+1,ytL+2,,yt]X_t = [y_{t-L+1}, y_{t-L+2}, \ldots, y_t]

目标为:

zt=yt+hz_t = y_{t+h}

于是得到训练样本:

(Xt,zt)(X_t, z_t)

例如,L=3L=3h=1h=1 时,序列

[10,12,11,14,15][10, 12, 11, 14, 15]

可以生成:

输入窗口 目标
[10,12,11][10,12,11] 14
[12,11,14][12,11,14] 15

窗口向右移动一个时间单位,所以称为滑动窗口

如果 h=2h=2,则第一个窗口 [10,12,11][10,12,11] 的目标变为 1515,中间的 1414 是预测时刻之后的值,不能放进输入窗口。

2. 窗口边界必须严格计算

对于长度为 TT 的序列,使用单步预测、输入长度 LL、预测步长 hh 时,合法锚点满足:

LtThL \leq t \leq T-h

样本数量为:

TLh+1T-L-h+1

T=10,L=3,h=2T=10,L=3,h=2 为例:

  • 第一个输入使用位置 1,2,31,2,3,目标是位置 55
  • 最后一个输入使用位置 6,7,86,7,8,目标是位置 1010
  • 一共有 1032+1=610-3-2+1=6 个样本。

一个常见错误是先构造所有窗口,再随机划分训练集和测试集。相邻窗口会共享大量历史值,导致测试窗口的输入可能与训练窗口高度重叠。重叠本身不一定是泄漏,因为生产中相邻时刻确实共享历史;但如果划分方式让测试目标所在时期的信息参与了训练特征处理或标签构造,就会造成时间穿越。

3. 滚动窗口与扩展窗口

窗口不只表示单个输入样本,也可以表示训练集随时间变化的范围。

扩展窗口从固定起点开始,不断增加历史数据:

训练:[1, 2, 3]       验证:[4]
训练:[1, 2, 3, 4]    验证:[5]
训练:[1, 2, 3, 4, 5] 验证:[6]

滚动窗口保持固定训练长度:

训练:[1, 2, 3] 验证:[4]
训练:[2, 3, 4] 验证:[5]
训练:[3, 4, 5] 验证:[6]

选择取决于数据生成机制:

  • 数据规律较稳定,且旧数据仍有参考价值:扩展窗口通常更充分;
  • 业务发生结构变化,旧数据会误导模型:滚动窗口可能更合适;
  • 训练成本高、数据量大:滚动窗口还能限制计算规模。

窗口长度不是越大越好。较大的 LL 能捕获长周期和慢变化,但会:

  • 增加输入维度和训练成本;
  • 引入更多过时信息;
  • 降低可用样本数量;
  • 对深度模型增加显存和延迟。

较小的 LL 则可能无法识别周期性。例如日销量具有 7 天周期,而 L=3L=3,模型没有足够历史观察完整周周期。

4. 多步预测的窗口定义

预测未来 HH 个点时,目标可以是:

zt=[yt+1,yt+2,,yt+H]\mathbf{z}_t = [y_{t+1}, y_{t+2}, \ldots, y_{t+H}]

这对应直接多输出模型:

z^t=fθ(Xt)\hat{\mathbf{z}}_t=f_{\theta}(X_t)

常见方法有三类:

直接法

为每个预测步训练一个模型:

y^t+h=fh(Xt)\hat{y}_{t+h}=f_h(X_t)

优点是每个模型直接优化自己的预测距离;缺点是模型数量随 HH 增加。

递归法

先预测 t+1t+1,再把预测值作为下一步输入:

y^t+1=f(Xt)\hat{y}_{t+1}=f(X_t)

y^t+2=f([Xt[2:],y^t+1])\hat{y}_{t+2}=f([X_t[2:],\hat{y}_{t+1}])

优点是模型数量少;缺点是误差会逐步累积,而且训练时使用真实历史值、推理时使用预测值,存在训练—推理分布差异。

多输出法

一次输出整个未来向量:

[y^t+1,,y^t+H][\hat{y}_{t+1},\ldots,\hat{y}_{t+H}]

深度学习中的序列到序列模型、带注意力的模型通常采用这种形式。它可以学习不同预测步之间的相关性,但输出维度、损失设计和缺失未来协变量处理更复杂。


三、特征:不只是“把列放进模型”

时间序列特征必须同时满足三个条件:

  1. 与目标存在可建模的关系;
  2. 在预测时刻可获得;
  3. 构造过程不会使用未来信息。

1. 滞后特征

滞后特征把历史目标转换为普通表格列:

lagk(t)=ytk\text{lag}_k(t)=y_{t-k}

例如:

lag_1(t) = y[t-1]
lag_7(t) = y[t-7]

如果预测 yty_tlag_1 必须是 yt1y_{t-1},而不是当前值 yty_t

对于预测 yt+hy_{t+h},以 tt 为预测基准时,输入中的最后一个目标值仍应是 yty_t。如果将 shift(-1) 生成的“下一期值”作为特征,就直接泄漏了标签。

2. 滚动统计特征

滚动均值可以表示近期水平:

rollmeanw(t)=1wi=1wyti\text{rollmean}_w(t) = \frac{1}{w}\sum_{i=1}^{w}y_{t-i}

注意上式使用的是 tt 之前的 ww 个值。如果代码先执行:

df["rolling_mean_7"] = df["y"].rolling(7).mean()

那么默认窗口可能包含当前 yty_t。如果目标正是预测 yty_t,当前值已经进入特征,造成泄漏。

安全写法通常是:

df["rolling_mean_7"] = (
    df["y"].shift(1).rolling(window=7).mean()
)

处理顺序是:

  1. shift(1):先排除当前目标;
  2. rolling(7):再对过去 7 个观测计算均值。

滚动标准差、最大值、最小值、分位数也遵循同样原则。

3. 差分、变化率和趋势

差分特征为:

Δyt=ytyt1\Delta y_t = y_t-y_{t-1}

二阶差分为:

Δ2yt=(ytyt1)(yt1yt2)\Delta^2 y_t=(y_t-y_{t-1})-(y_{t-1}-y_{t-2})

它们可以减弱趋势,使模型更关注变化。对正值序列还可以使用对数变化率:

rt=log(yt)log(yt1)r_t=\log(y_t)-\log(y_{t-1})

但变换会改变误差解释。模型预测的是对数空间值时,反变换回原始空间可能产生偏差,不能简单认为:

exp(E[logY])=E[Y]\exp(E[\log Y])=E[Y]

因为指数函数是非线性的。若业务指标在原始空间评估,应在原始空间计算指标,并根据需要进行偏差校正。

4. 日历特征与周期编码

星期几、月份、是否节假日等是确定性特征。对于周期变量,直接使用整数可能给模型制造错误的距离关系:

星期一 = 0
星期日 = 6

模型可能误认为星期日与星期一相距 6,而不是相邻。周期编码可使用:

sint=sin(2πk/P)\text{sin}_t=\sin(2\pi k/P)

cost=cos(2πk/P)\text{cos}_t=\cos(2\pi k/P)

其中:

  • PP 是周期长度;
  • kk 是周期中的位置。

例如星期周期取 P=7P=7。正弦和余弦同时存在,才能表示周期上的位置。

5. 外生变量与未来可知性

外生变量是影响目标、但不由目标本身生成的变量,例如:

  • 温度、降雨;
  • 价格和促销计划;
  • 库存;
  • 广告预算;
  • 利率和宏观指标。

外生变量要区分两种情况:

未来已知

例如已排定的促销计划和节假日。预测未来 7 天销量时,可以使用未来 7 天的计划值。

未来未知

例如真实未来温度。此时不能把未来实际温度直接放入预测模型,而应使用:

  • 天气预报;
  • 情景预测;
  • 单独的天气模型;
  • 或仅使用截至预测时刻已知的天气信息。

把未来实际观测值当作未来特征,是一种尤其容易被忽略的泄漏。


四、一个可运行的因果特征示例

下面的示例生成带趋势和周期的序列,使用滞后值和因果滚动均值预测下一时刻。它依赖 Python、NumPy、pandas 和 scikit-learn。

import numpy as np
import pandas as pd

from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer

# 1. 构造按天排列的示例数据
rng = np.random.default_rng(7)
n = 240
date = pd.date_range("2024-01-01", periods=n, freq="D")

y = (
    100
    + 0.15 * np.arange(n)                 # 趋势
    + 8 * np.sin(2 * np.pi * np.arange(n) / 7)  # 周期
    + rng.normal(0, 2, n)                 # 噪声
)

df = pd.DataFrame({"date": date, "y": y})

# 2. 构造只使用过去数据的特征
df["lag_1"] = df["y"].shift(1)
df["lag_7"] = df["y"].shift(7)

# 先 shift(1),再 rolling,确保不包含当前 y
df["mean_7"] = df["y"].shift(1).rolling(7).mean()
df["std_7"] = df["y"].shift(1).rolling(7).std()

# 预测下一天,因此标签是当前值的后移
df["target"] = df["y"].shift(-1)

features = ["lag_1", "lag_7", "mean_7", "std_7"]
model_data = df.dropna(subset=features + ["target"]).reset_index(drop=True)

# 3. 按时间切分,不能随机打乱
split = int(len(model_data) * 0.8)
train = model_data.iloc[:split]
test = model_data.iloc[split:]

# 4. 缺失值处理器只在训练集上拟合
model = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("regressor", RandomForestRegressor(
        n_estimators=200,
        random_state=7,
        min_samples_leaf=3,
        n_jobs=-1,
    )),
])

model.fit(train[features], train["target"])
pred = model.predict(test[features])

print("test rows:", len(test))
print("MAE:", round(mean_absolute_error(test["target"], pred), 3))

这个示例中有几个必须保留的因果关系:

  • target = y.shift(-1) 表示用 tt 时刻信息预测 t+1t+1
  • lag_1lag_7 只引用过去;
  • mean_7 先排除当前值,再计算过去窗口;
  • 训练集在时间上早于测试集;
  • 中位数填充器在训练集上拟合,不能用全数据的中位数。

如果把 mean_7 改成 df["y"].rolling(7).mean(),对于预测当前 y 的任务就会引入当前目标。这个错误在训练集和测试集上都可能让指标变好,但部署时无法复现同样的特征。


五、回测:模拟模型在历史上“当时的表现”

1. 回测不是一次训练—测试切分

回测是按照历史时间顺序,反复模拟“当时训练、当时预测”的过程。

一次简单的时间切分如下:

时间 ─────────────────────────────────────────>

训练集             测试集
[过去的数据]       [未来的一段数据]

它可以检验一次时间外推,但无法回答模型在不同历史阶段是否稳定。

更接近生产的滚动回测是:

折 1: 训练 [1..T1]       验证 [T1+gap+1 .. T2]
折 2: 训练 [1..T2]       验证 [T2+gap+1 .. T3]
折 3: 训练 [1..T3]       验证 [T3+gap+1 .. T4]

每一折必须独立模拟以下动作:

  1. 截止训练结束时间收集数据;
  2. 构造训练特征;
  3. 拟合缺失值处理、缩放器、编码器和模型;
  4. 在验证区间生成预测;
  5. 使用真实未来标签计算指标;
  6. 记录该时间段的数据分布和系统状态。

2. 为什么需要 gap

当训练样本和验证样本的标签或输入窗口重叠时,验证结果可能过于乐观。

设每个样本使用过去 LL 个点预测未来 hh 步。若训练样本的最后一个预测目标和验证样本的第一个输入或目标在时间上相邻,通常不一定构成泄漏;但在以下场景中需要间隔 gap

  • 标签需要经过一段时间才能确定;
  • 特征存在延迟;
  • 预测窗口较长;
  • 相邻样本共享未来区间;
  • 业务要求训练和验证之间模拟真实冷却期。

gap 的含义是训练结束和验证开始之间人为留出的时间。它不应被当作“固定万能值”,应由标签确认延迟、特征到达延迟和部署节奏共同决定。

scikit-learn 提供了面向时间顺序的 TimeSeriesSplit,可用于基本的扩展训练窗口和时间切分。但它只负责索引切分,不会自动知道你的业务发布时间、特征延迟、标签成熟时间或数据修订历史;这些约束仍需自行编码。

3. 训练、验证和测试的职责

应至少区分:

  • 训练集:拟合模型参数;
  • 验证集:选择超参数、特征和阈值;
  • 最终测试集:在决策完成后进行一次尽量无偏的最终评估。

时间序列中也可以用多轮滚动验证代替单个验证集。重要的是:不能在看过最终测试结果后继续修改模型,再把同一测试集称为最终成绩。

如果模型每天重训,回测也应模拟每天重训;如果模型每周更新,回测就不应假设每天获得一次参数更新。回测频率、训练窗口、特征快照、模型版本和预测时点应与生产流程一致。


六、回测指标:误差数字必须有业务含义

1. MAE、MSE 和 RMSE

平均绝对误差:

MAE=1ni=1nyiy^iMAE=\frac{1}{n}\sum_{i=1}^{n}|y_i-\hat{y}_i|

它与目标变量同单位,对异常值相对不敏感,适合解释平均偏差。

均方误差:

MSE=1ni=1n(yiy^i)2MSE=\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat{y}_i)^2

均方根误差:

RMSE=MSERMSE=\sqrt{MSE}

RMSE 对大误差惩罚更强。如果少数严重缺货或风险漏报的成本远高于普通误差,RMSE 或加权损失可能更接近业务目标。

2. MAPE 的边界

平均绝对百分比误差:

MAPE=1niyiy^iyiMAPE=\frac{1}{n}\sum_i\left|\frac{y_i-\hat y_i}{y_i}\right|

yi=0y_i=0 时不可定义;当 yiy_i 很接近 0 时会产生极大权重。因此,间歇性需求、低流量指标和计数型数据不应不加判断地使用 MAPE。

可以考虑:

  • MAE;
  • WAPE;
  • sMAPE;
  • 对数尺度误差;
  • 按业务成本加权的误差。

每种指标都对应不同的风险偏好,而不是单纯的“越小越好”。

3. 分位数预测与区间覆盖

点预测只给出一个值,但库存、容量规划和风控通常需要不确定性。

分位数损失,也称 pinball loss,对分位数 q(0,1)q\in(0,1) 定义为:

Lq(y,y^)={q(yy^),yy^(1q)(y^y),y<y^L_q(y,\hat y)= \begin{cases} q(y-\hat y), & y\geq \hat y \\ (1-q)(\hat y-y), & y<\hat y \end{cases}

预测 q=0.9q=0.9 的含义不是“90% 的点都准确”,而是希望:

P(Yy^0.9)0.9P(Y\leq \hat y_{0.9})\approx 0.9

如果输出 80% 预测区间,应检查实际目标落在区间内的比例是否接近 80%,还要观察区间宽度。覆盖率过高但区间极宽,可能没有决策价值。

4. 不能只看总体平均值

整体 MAE 可能掩盖重要失败:

  • 高峰期误差远大于普通期;
  • 新用户误差远大于老用户;
  • 未来第 7 天误差远大于第 1 天;
  • 某些地区或权限范围内误差异常;
  • 缺失特征时模型退化严重。

回测应按时间段、预测步长、业务分组和数据质量状态分层报告。


七、数据泄漏:模型“看到了不该看到的答案”

数据泄漏是指训练、特征处理或模型选择过程中使用了在真实预测时刻不可获得的信息,导致离线评估高估真实效果。

它不是只有一种形式。

1. 目标泄漏

目标泄漏是特征直接或间接包含目标本身。

例如预测订单是否退款,却使用了退款完成时间、最终退款金额或“订单已关闭”状态。模型可能获得极高准确率,但这些字段在预测时尚未发生。

时间序列中的典型写法错误包括:

# 错误示例:预测当前 y 时包含当前 y
df["mean_7"] = df["y"].rolling(7).mean()

应改为:

df["mean_7"] = df["y"].shift(1).rolling(7).mean()

2. 未来窗口泄漏

使用中心滚动窗口会同时查看过去和未来:

df["bad"] = df["y"].rolling(7, center=True).mean()

在预测中间日期时,该窗口会包含未来观测。除非任务明确是离线平滑或事后分析,否则不能将其作为预测特征。

同样,使用全序列计算未来差分、未来最大值、未来排名,也会泄漏。

3. 预处理泄漏

标准化、缺失值填充、特征选择和降维都可能泄漏。

错误流程:

scaler.fit(all_data)
train_scaled = scaler.transform(train)
test_scaled = scaler.transform(test)

如果测试区间均值和方差参与了 fit,模型间接获得了未来分布信息。

正确流程是:

scaler.fit(train)
train_scaled = scaler.transform(train)
test_scaled = scaler.transform(test)

在交叉验证中,预处理器必须在每一折的训练部分单独拟合。使用 scikit-learn 的 Pipeline 可以把变换器和模型绑定,减少手动调用顺序错误,但 Pipeline 不会替你修复错误的时间特征。

4. 标签成熟泄漏

有些标签在事件发生后并不会立即确定。

例如:

  • 预测用户未来 30 天是否流失;
  • 预测未来 7 天是否发生坏账;
  • 预测设备未来 24 小时是否故障。

在预测时刻 tt,只有当 t+30t+30t+7t+7 或故障观察窗口结束后,标签才成熟。若回测使用了尚未成熟的标签,训练集构造可能把未来状态混入样本筛选或特征生成。

应显式区分:

prediction_time
label_start_time
label_end_time
label_available_time

5. 样本归一化和聚合泄漏

按用户、商品或设备计算全历史统计量也可能跨越预测时点:

# 可能泄漏:使用了未来订单
user_mean = df.groupby("user_id")["amount"].mean()

如果该均值用于早期样本,里面已经包含未来订单。安全做法是按每个预测时点只聚合截至该时点已到达的数据,或者使用数据库窗口函数限定时间边界。

6. 训练样本重叠与实体泄漏

如果同一实体的未来记录出现在训练集,而早期记录出现在测试集,结果是否有效取决于任务:

  • 预测同一用户的未来行为:实体跨时间出现是正常的,但必须保持时间顺序;
  • 评估新用户泛化:应按实体隔离,否则会高估效果;
  • 设备故障预测:同一设备的连续窗口大量重叠,必须明确评估的是已知设备未来预测,还是新设备迁移能力。

“随机切分后指标很高”不能自动说明模型有问题,但必须说明它评估的到底是哪一种泛化能力。

7. 生成式 AI 中的泄漏

生成式 AI 系统同样受时间泄漏约束:

  • 检索库不能包含预测时点之后发布的文档;
  • 评测集内容不能在提示词模板、缓存或向量库中提前暴露;
  • 未来事件的摘要不能作为当前上下文;
  • 用后验人工标签反复优化提示词后,再在同一评测集上报告结果,会形成评测泄漏;
  • 预训练模型可能已经见过公开历史数据,若任务要求严格的时间外推,应说明模型训练截止时间是否满足要求。

把大语言模型换成时间序列模型,并不会自动消除因果约束。模型规模不能弥补错误的信息边界。


八、用“可用时间”设计特征,而不是只用事件时间

一个可靠的特征表至少要记录:

entity_id
event_time
available_time
feature_name
value
source_version

构造训练样本时,特征 xx 必须满足:

available_time(x)t\text{available\_time}(x)\leq t

目标则应满足:

target_time>t\text{target\_time}>t

如果目标标签在未来一段时间后才成熟,还需满足:

label_available_time训练构造时间\text{label\_available\_time}\leq \text{训练构造时间}

这使得数据连接不再只是:

JOIN ... ON feature.event_time <= prediction_time

而应同时约束可用时间,例如:

SELECT
    p.entity_id,
    p.prediction_time,
    f.value
FROM predictions AS p
JOIN features AS f
  ON f.entity_id = p.entity_id
 AND f.event_time <= p.prediction_time
 AND f.available_time <= p.prediction_time;

如果一个实体在预测时点之前有多条特征记录,还需要选取最新一条,并处理版本:

ROW_NUMBER() OVER (
    PARTITION BY p.entity_id, p.prediction_time
    ORDER BY f.event_time DESC, f.available_time DESC, f.revision DESC
)

具体 SQL 语法会随数据库变化,但因果条件不变:事件发生得早,不代表信息已经可用;最终修订值存在,不代表历史上已经知道。


九、漂移:数据变化不等于模型失效,但必须可检测

漂移是数据生成过程或预测关系随时间变化的现象。至少要区分三类。

1. 协变量漂移

输入分布变化:

Pt(X)Pt+1(X)P_t(X)\neq P_{t+1}(X)

例如用户年龄结构、流量来源、传感器量程发生变化。

模型可能仍然有效,也可能失效,取决于条件关系是否稳定。

2. 标签漂移

目标分布变化:

Pt(Y)Pt+1(Y)P_t(Y)\neq P_{t+1}(Y)

例如异常事件比例从 1% 变为 5%。分类阈值、容量规划和告警量可能因此改变。

3. 概念漂移

输入与目标的关系变化:

Pt(YX)Pt+1(YX)P_t(Y\mid X)\neq P_{t+1}(Y\mid X)

例如同样的交易特征在风控规则调整后对应不同的欺诈概率;同样的广告曝光量在产品改版后带来的转化率不同。

概念漂移通常比单纯的输入分布变化更直接影响模型表现,但标签往往延迟到达,因此检测更困难。

4. 漂移检测的基本思路

对数值特征,可以比较基准窗口和当前窗口的:

  • 均值、方差、分位数;
  • 直方图或分箱占比;
  • PSI、KS 等分布差异统计量。

对类别特征,可以比较类别频率和新类别比例。

对预测系统,还应监控:

  • 预测值分布;
  • 缺失率和延迟;
  • 特征覆盖率;
  • 目标分布;
  • 延迟标签到达后的 MAE、RMSE、校准误差;
  • 分群和预测时距上的性能。

阈值不能脱离业务随意解释。统计显著的漂移可能只是样本量变大,而实际影响很小;反之,小样本下看似不显著的变化也可能是严重故障。

5. 漂移的处理动作

检测到漂移后,不应自动执行“立即重训”。合理动作可能包括:

  1. 检查数据管道、时区、单位、枚举和上游版本;
  2. 判断是正常季节性还是异常变化;
  3. 将新旧时间段分层比较;
  4. 检查模型残差与业务指标;
  5. 选择扩展窗口、滚动窗口或加权训练;
  6. 调整特征、阈值或模型;
  7. 进行影子部署或小流量验证;
  8. 保留旧模型作为回滚版本。

如果漂移来自数据错误,重训会把错误写入模型,反而扩大影响。


十、回测、漂移和上线系统的完整数据流

一个生产系统可以抽象为以下路径:

flowchart LR
    A[原始事件流] --> B[时间对齐与版本化]
    B --> C[可用性约束的特征层]
    C --> D[训练样本构造]
    D --> E[滚动回测]
    E --> F[模型注册与评审]
    F --> G[影子或灰度部署]
    G --> H[在线预测]
    H --> I[预测日志]
    I --> J[延迟标签回流]
    J --> K[性能与漂移监控]
    K --> L{是否异常}
    L -- 否 --> H
    L -- 是 --> M[诊断、降级或回滚]
    M --> F

关键路径不是“训练完成后直接上线”,而是:

  • 原始数据必须保留事件时间、到达时间和版本;
  • 特征计算要能复现某个历史预测时点;
  • 回测要使用与线上相同的特征逻辑;
  • 预测日志要保存模型版本、特征版本、预测时刻和输入摘要;
  • 标签成熟后才能计算真实性能;
  • 异常处理必须有降级和回滚路径。

状态和故障

一个模型版本至少可以经历:

candidate
  -> backtested
  -> approved
  -> shadow
  -> canary
  -> production
  -> retired

状态转换应有证据约束。例如:

  • candidate -> backtested:回测数据、时间范围和指标已保存;
  • backtested -> approved:完成泄漏检查、权限检查和成本评审;
  • shadow -> canary:影子预测与线上基线比较正常;
  • production -> retired:存在替代版本或回滚版本。

常见故障路径包括:

  • 特征迟到:使用上一版本特征、默认值或降级模型;
  • 标签迟到:只监控输入和预测分布,暂不宣称实时准确率;
  • 上游 schema 变化:阻断发布,而不是静默把字符串转成错误数值;
  • 模型服务超时:使用缓存预测、基线模型或规则;
  • 分布异常:暂停自动重训,进入人工诊断;
  • 预测结果被重复写入:使用唯一键,例如 (entity_id, prediction_time, horizon, model_version),保证幂等。

十一、深度学习窗口:张量形状和训练—推理差异

对于多变量序列,窗口通常表示为:

XRN×L×FX\in\mathbb{R}^{N\times L\times F}

其中:

  • NN:样本数;
  • LL:时间窗口长度;
  • FF:每个时间点的特征数。

单步回归的目标可能是:

yRNy\in\mathbb{R}^{N}

多步、多变量预测则可能是:

YRN×H×OY\in\mathbb{R}^{N\times H\times O}

其中 HH 是预测步数,OO 是输出变量数。

RNN、TCN、Transformer 等模型的差别在于如何建模窗口内的依赖关系,但它们都必须遵守同一数据边界。Transformer 的注意力机制可以看到输入窗口中的所有位置,却不能合法地看到预测目标之后的数据;如果进行自回归解码,训练时常用 teacher forcing,即输入真实的前一步目标,推理时却只能输入模型自己的预测,这会产生 exposure bias。

深度模型还有一个特殊风险:对全数据先计算标准化统计量、频域变换、分词或词表,再切分时间数据,仍然可能泄漏未来分布。模型结构复杂不代表数据管道可以放松约束。


十二、生成式 AI 与时间序列的结合边界

生成式 AI 可以用于:

  • 生成预测解释;
  • 将多个时间序列摘要成自然语言;
  • 从事件日志抽取结构化特征;
  • 根据检索到的历史案例生成运维建议;
  • 对预测区间、异常原因和候选动作进行辅助说明。

但它不应被误认为天然具备严格预测能力。

如果语言模型参与预测,仍需明确:

  1. 输入文档和事件的截止时间;
  2. 检索结果的发布时间和版本;
  3. 是否包含未来标签或后验分析;
  4. 预测输出如何与数值模型校准;
  5. 生成内容如何经过规则、权限和人工审批;
  6. token、检索和推理成本如何限制。

一种较稳妥的架构是:

时间序列模型 -> 数值预测与区间
检索系统     -> 截止预测时点的证据
生成式模型   -> 基于预测和证据生成解释
规则/策略层  -> 校验、权限与最终动作

生成式模型可以解释和编排,但关键数值、阈值和自动化动作应有可验证的来源。若模型读取了预测时点之后的事故报告,再生成“当时应该如何预测”的解释,解释本身就已经泄漏。


十三、最小回测实现:明确每一折的边界

下面的代码展示一个简单扩展窗口回测。它假设特征已经按因果方式构造,并且 target 是未来目标。

import numpy as np
import pandas as pd

from sklearn.base import clone
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

def expanding_backtest(
    data: pd.DataFrame,
    feature_cols: list[str],
    target_col: str,
    initial_train_size: int,
    test_size: int,
    step: int,
    gap: int = 0,
):
    """
    data 必须已经按 prediction_time 升序排列。
    initial_train_size: 第一折训练样本数
    test_size: 每折连续验证样本数
    step: 下一折向前移动的样本数
    gap: 训练末尾与验证开头之间留出的样本数
    """
    base_model = Pipeline([
        ("scale", StandardScaler()),
        ("regressor", Ridge(alpha=1.0)),
    ])

    results = []

    train_end = initial_train_size
    while True:
        test_start = train_end + gap
        test_end = test_start + test_size

        if test_end > len(data):
            break

        train = data.iloc[:train_end]
        test = data.iloc[test_start:test_end]

        model = clone(base_model)
        model.fit(train[feature_cols], train[target_col])
        pred = model.predict(test[feature_cols])

        results.append({
            "train_end": train["prediction_time"].iloc[-1],
            "test_start": test["prediction_time"].iloc[0],
            "test_end": test["prediction_time"].iloc[-1],
            "mae": mean_absolute_error(test[target_col], pred),
            "n_test": len(test),
        })

        train_end += step

    return pd.DataFrame(results)

使用前置条件:

  • data 必须按预测时间排序;
  • 特征必须只依赖各自预测时间之前的信息;
  • target 的标签成熟条件应已在数据筛选阶段处理;
  • 如果样本是重叠窗口,gap 应按业务延迟而不是随意填写。

输出会包含每一折训练结束时间、验证区间和 MAE。若后期 MAE 持续上升,应进一步检查:

  • 输入分布是否改变;
  • 标签比例是否改变;
  • 特征是否迟到;
  • 上游单位或时区是否变化;
  • 旧数据是否仍适合训练;
  • 模型是否只在某些预测步长失败。

回测函数本身不会判断这些问题,它只提供时间顺序下的实验骨架。


十四、常见误解与失败表现

误解一:时间序列不能使用随机抽样

更准确的说法是:不能用会破坏评估因果顺序的随机划分来估计未来泛化。

对于某些独立实体、固定时间截面或明确的横截面任务,随机抽样可能合理;但只要任务是“根据过去预测未来”,验证集就必须位于训练信息之后,或采用严格的时间与实体分组设计。

误解二:只要特征时间戳早于标签就没有泄漏

特征的事件时间早于标签,并不代表在预测时刻可用。发布延迟、批处理、数据修订和人工审核都可能使特征晚于预测时点到达。

误解三:滚动平均天然是历史特征

滚动平均是否安全取决于窗口是否包含当前目标,以及预测基准时间是什么。shift(1) 常常是必要的,但如果任务定义为“利用当前已观测值预测下一期”,那么当前值可以进入输入;关键是先明确任务的预测时点。

误解四:回测分数高就能上线

高分可能来自:

  • 标签泄漏;
  • 全数据归一化;
  • 未来实际天气等外生变量;
  • 与线上不同的缺失处理;
  • 只选择表现最好的历史区间;
  • 在测试集上反复调参。

诊断时应保存每一折的样本边界、特征快照和变换器版本,而不是只保存一个平均指标。

误解五:漂移检测报警就应该重训

漂移可能来自节假日、促销、采集故障、季节性或真实业务变化。重训前必须先判断变化的来源。若数据管道错了,重训只会把错误固化。


十五、权限、成本和复现也属于时间序列正确性

生产预测系统处理的不是抽象矩阵,而是有权限和成本边界的数据。

权限

特征服务、训练作业和监控系统可能拥有不同数据权限。训练时可读取的字段不一定允许线上服务读取,反之亦然。应记录:

  • 数据源和字段权限;
  • 训练作业使用的身份;
  • 模型可访问的在线特征范围;
  • 生成式 AI 检索时的租户和文档权限。

否则可能出现离线指标很好,但线上无法取得同样特征;更严重时,模型会把用户无权查看的信息带入预测或解释。

成本

窗口长度、回测折数、模型数量和更新频率都会影响成本:

  • 更长窗口增加特征存储和深度模型显存;
  • 多步直接预测增加模型数量;
  • 高频重训增加计算和版本管理成本;
  • 生成式 AI 增加检索、上下文和推理成本。

成本约束应进入实验设计。例如在相同时间外推条件下比较:

短窗口 + 高频更新
长窗口 + 低频更新
滚动窗口 + 小模型
扩展窗口 + 大模型

不能只优化 MAE,再忽略线上延迟、失败率和预算。

复现

每个预测结果至少应能够追溯到:

model_version
feature_version
training_cutoff
prediction_time
horizon
data_snapshot
code_version

没有这些信息,就很难判断一次指标下降究竟来自模型变化、数据修订、特征延迟还是评估逻辑改变。


十六、交付前的自查方法

时间序列模型交付前,至少应逐项回答:

  1. 预测基准时间 tt 和预测步长 hh 是什么?
  2. 每个输入特征的 available_time 如何定义?
  3. 滞后和滚动特征是否明确排除了未来?
  4. 外生变量在未来是否真实可知?
  5. 标签何时成熟,训练时是否只使用已成熟标签?
  6. 训练、验证和测试是否按时间顺序划分?
  7. 是否模拟了生产中的训练频率、窗口和特征延迟?
  8. 预处理器、编码器和特征选择是否只在训练折拟合?
  9. 回测是否按时间段、预测步长和业务分组报告?
  10. 漂移监控是否区分输入、标签和条件关系变化?
  11. 特征迟到、上游失败和模型超时如何降级?
  12. 模型、特征、数据快照和权限是否可追溯?
  13. 自动重训是否有验证、审批和回滚?
  14. 若使用深度学习或生成式 AI,训练—推理输入边界是否一致?
  15. 指标改善是否来自真实信息,还是来自评估流程泄漏?

时间序列机器学习的核心不是某个特定算法,而是对“信息在什么时候可用”的精确定义。窗口决定模型看到哪段历史,特征决定历史如何被表达,回测决定未来性能如何被模拟,漂移决定系统何时偏离原有假设,而泄漏防护保证这些结论没有建立在不可能获得的信息之上。只有这几部分同时成立,模型分数才具有生产意义。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。