时间序列预测怎么避免看见未来?构造滞后特征并按时间验证

按预测时点构造 lag_1、lag_7,使用顺序验证与独立留出段比较模型、上一日及七日前基线,区分一步与多步预测。

时间序列预测的第一步是写清楚“预测时点能知道什么”。假设每天开始时预测当天结束的数值,昨日及更早的结果已经到齐,那么昨日数值可以成为特征;当天结果、未来实际值以及事后才发布的统计量都不能使用。按时间切分数据能处理一部分风险,还要检查特征和标签什么时候真实可用。

这次只做一步预测,不混入未来真实值

本例用等间隔的日数据。目标为当天的 y,输入只包括 lag_1=y(t-1) 和 lag_7=y(t-7)。用正数 shift 移动数值,并保持原日期索引;不给 freq,避免只移动索引而数值对应关系未按目标改变。

时间序列预测怎么避免看见未来?构造滞后特征并按时间验证

先对全序列构造这些纯历史滞后列,再按时间截取,在本例中是可行的:每个特征的来源日期都早于本行目标日期,计算没有学习全体数据的统计量。负数 shift、中心窗口和对全序列拟合缩放器则需要另外检查。真实业务中“前一天”结果若延迟两天公布,即使日期较早也不能用,必须按可用时点继续滞后。

环境与教学数据

下面示例在 Windows、Python 3.11.15、CPU 环境实际运行。先安装 Python 3.11,在新建的空文件夹中打开 PowerShell,把完整代码保存为 09-time-lag-forecast.py。创建独立环境并执行:

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install numpy==2.4.6 pandas==3.0.6 scikit-learn==1.9.1
.\.venv\Scripts\python.exe 09-time-lag-forecast.py

最后一行在保存代码后执行。macOS 或 Linux 把 Python 路径换成 ./.venv/bin/python。示例不需要 GPU、外部模型下载或付费 API;安装依赖需要联网。不同库版本可能带来数值末位差异,先核对数据行数、标签顺序和输出结构。

序列由趋势、七天周期和固定随机种子的噪声人工生成,未使用真实销售、金融或气象数据。最早七行缺少 lag_7,删除后还有 173 行;末尾 30 行作最终留出段,其余 143 行用于顺序验证。LinearRegression 的配置固定,不在最终留出段调参。

构造滞后特征并顺序验证

把完整代码保存为 09-time-lag-forecast.py 并运行。它生成 180 天等间隔的教学序列,构造前一天与前七天特征,只在较早的开发段做时间验证,最后对 30 天留出段与两个简单基线比较。

import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error

# 等间隔日序列,人工生成;每个预测时点只知道上一日及更早的实际值。
rng = np.random.default_rng(42)
t = np.arange(180)
df = pd.DataFrame({'y': 20+0.04*t+4*np.sin(2*np.pi*t/7)+rng.normal(0, 0.4, len(t))},
                  index=pd.date_range('2025-01-01', periods=len(t), freq='D'))
for lag in (1, 7):
    df[f'lag_{lag}'] = df['y'].shift(lag)
data = df.dropna()
features = ['lag_1', 'lag_7']
dev, holdout = data.iloc[:-30], data.iloc[-30:]
assert dev.index.max() < holdout.index.min()
print('development/holdout rows:', len(dev), len(holdout))
print('holdout dates:', holdout.index.min().date(), holdout.index.max().date())

# 只在开发段进行顺序验证。模型参数固定,不根据最终留出段调参。
for fold, (train_i, valid_i) in enumerate(
        TimeSeriesSplit(n_splits=3, test_size=20).split(dev), 1):
    train, valid = dev.iloc[train_i], dev.iloc[valid_i]
    assert train.index.max() < valid.index.min()
    model = LinearRegression().fit(train[features], train['y'])
    pred = model.predict(valid[features])
    mae = mean_absolute_error(valid['y'], pred)
    naive = mean_absolute_error(valid['y'], valid['lag_1'])
    print('fold', fold, 'last_train/first_valid', train.index.max().date(),
          valid.index.min().date(), 'MAE/model,last_value', f'{mae:.3f}', f'{naive:.3f}')

model = LinearRegression().fit(dev[features], dev['y'])
pred = model.predict(holdout[features])
print('holdout MAE/model,last_value,seasonal7:',
      f'{mean_absolute_error(holdout.y, pred):.3f}',
      f'{mean_absolute_error(holdout.y, holdout.lag_1):.3f}',
      f'{mean_absolute_error(holdout.y, holdout.lag_7):.3f}')
first_date = holdout.index[0]
assert holdout.loc[first_date, 'lag_1'] == df.loc[first_date-pd.Timedelta(days=1), 'y']
print('first prediction date/lag1_source:', first_date.date(),
      (first_date-pd.Timedelta(days=1)).date())
report = holdout.assign(prediction=pred)[['y', 'lag_1', 'lag_7', 'prediction']]
print(report.head(3).round(3).to_string())
report.to_csv('forecast_report.csv', index_label='date')

TimeSeriesSplit 的每一折都用较早数据拟合,再在较晚数据验证;本例没有随机 train_test_split,也没有验证段拟合。两个比较基线分别是“预测等于昨天实际值”和“预测等于七天前实际值”。MAE 是预测与真值绝对差的均值,和 y 使用同一单位,越小越好。

实际输出与每个预测时点的检查

development/holdout rows: 143 30
holdout dates: 2025-05-31 2025-06-29
fold 1 last_train/first_valid 2025-03-31 2025-04-01 MAE/model,last_value 0.403 2.257
fold 2 last_train/first_valid 2025-04-20 2025-04-21 MAE/model,last_value 0.330 2.091
fold 3 last_train/first_valid 2025-05-10 2025-05-11 MAE/model,last_value 0.582 2.090
holdout MAE/model,last_value,seasonal7: 0.320 2.346 0.406
first prediction date/lag1_source: 2025-05-31 2025-05-30
                 y   lag_1   lag_7  prediction
2025-05-31  27.672  29.923  27.563      27.864
2025-06-01  23.890  27.672  23.699      24.066
2025-06-02  21.510  23.890  21.734      22.072

代码检查每折最后一个训练日期早于第一个验证日期,并确认 2025-05-31 的 lag_1 来自 2025-05-30;最终留出段从 2025-05-31 到 2025-06-29,模型、上一日基线和前七日基线的 MAE 分别约为 0.320、2.346 和 0.406。

本例的三折开发验证和最终留出段均显示模型优于上一日基线;最终留出段也优于七日前基线。这来自人为设置的周期、趋势和噪声,只说明此演示中的比较结果。真实序列可能发生节假日、制度变化、缺测与结构突变,模型完全可能不如简单基线。

forecast_report.csv 保存每天的 y、lag_1、lag_7 与 prediction。它是结果核对表;若日级误差异常,可从日期回溯到特征来源日。不要只保留一个平均误差,掩盖某个时间段持续偏差。

容易造成看见未来的四种做法

  • 把当天 y 本身或 shift(-1) 放进特征:输入包含预测时点未知的结果。
  • 先随机打散日期再交叉验证:较晚数据可能进入模型,较早数据成为验证对象。
  • 用完整数据拟合标准化或目标相关特征选择:较晚分布或标签信息参与了训练。
  • 把一步预测的滞后列用于固定起点的多步预测:未来前一天真实值实际上还不可得。

本例评估的是逐日得到上一日真实结果后,继续预测下一日的一步预测;一次性在 5 月 30 日预测未来 30 天时,不能使用留出段中尚未发生的真实 lag_1。

如果业务需要固定起点的多步预测,可在每一步用之前预测值递归更新,或直接训练指定预测跨度的模型;两种方法都要按相同信息可用规则重新验证。本文没有给出多步预测效果,不能把上面的 0.320 当成未来 30 天一次性预测误差。

数据不整齐或标签延迟时怎么改

TimeSeriesSplit 按行号划分。官方说明,比较各折时间跨度时需要样本等间隔;缺失日期或不等间隔记录应先核对时间索引,按业务含义重建时间网格或自行设计按日期切分。补齐时间格并不代表可以用未来值插值填补历史。

gap 可以在训练与验证之间留出若干行,但它的行数要根据实际信息延迟或标签跨度决定。例如标签代表未来七天累计结果时,训练段末尾标签可能覆盖验证时段;应按标签结束日期剔除跨界样本,并核对特征可用时点。机械设 gap=1 无法保证所有任务无泄漏。

若 lag 列出现缺失,先确认排序、重复时间和历史长度。本例直接删除前七行,生产场景应说明哪些样本被排除;不要用后面的真实数值回填,再把分数当成可部署结果。完成这些检查后,再决定是否更换模型或增加已知的日历特征。

相关官方资料

本文读取官方资料的日期为 2026 年 10 月 1 日;示例输出来自上面注明的本地环境。

Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/30587.html

赞 (0)
AI小管家的头像AI小管家
文档分类数据放不进内存怎么办?用 HashingVectorizer 分批训练
上一篇 1天前
推荐系统怎么入门?用用户评分建立物品协同过滤示例
下一篇 1天前

相关推荐

联系我们

联系我们

1

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
关注微信
分享本页
返回顶部