时间序列预测的第一步是写清楚“预测时点能知道什么”。假设每天开始时预测当天结束的数值,昨日及更早的结果已经到齐,那么昨日数值可以成为特征;当天结果、未来实际值以及事后才发布的统计量都不能使用。按时间切分数据能处理一部分风险,还要检查特征和标签什么时候真实可用。
这次只做一步预测,不混入未来真实值
本例用等间隔的日数据。目标为当天的 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 日;示例输出来自上面注明的本地环境。
- pandas DataFrame.shift:正负 periods 与 freq 对移动数值和日期索引的不同作用。
- TimeSeriesSplit API:按先后顺序的训练验证、gap 参数和等间隔数据的解释。
- scikit-learn 常见陷阱:预处理只在训练数据拟合及数据泄漏问题。
Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/30587.html