import os
os.environ['NIXTLA_ID_AS_COL'] = '1' 交叉验证
实施交叉验证以评估历史数据上的模型
时间序列交叉验证是一种评估模型在历史数据上表现的方法。它通过定义一个滑动窗口,跨越过去的观测值,并预测随后的时间段来进行工作。它与标准交叉验证的不同之处在于保持数据的时间顺序,而不是随机划分数据。
这种方法通过考虑多个时间段,可以更好地估计我们模型的预测能力。当只使用一个窗口时,它类似于标准的训练-测试划分,其中测试数据是最后一组观测值,而训练集由早期数据组成。
下图展示了时间序列交叉验证的工作原理。

在本教程中,我们将解释如何在 NeuralForecast 中执行交叉验证。
大纲: 1. 安装 NeuralForecast
加载并绘制数据
使用交叉验证训练多个模型
评估模型并为每个系列选择最佳模型
绘制交叉验证结果
本指南假定您对 neuralforecast 有基本的了解。要访问最小示例,请访问 快速入门
以下代码行用于使交叉验证的输出将系列的ID作为一列而不是索引。
1. 安装 NeuralForecast
%%capture
!pip install neuralforecast2. 加载并绘制数据
我们将使用pandas从M4预测比赛加载每小时数据集,该数据集已存储在parquet文件中以提高效率。
import pandas as pd
Y_df = pd.read_parquet('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.parquet')
Y_df.head()| unique_id | ds | y | |
|---|---|---|---|
| 0 | H1 | 1 | 605.0 |
| 1 | H1 | 2 | 586.0 |
| 2 | H1 | 3 | 586.0 |
| 3 | H1 | 4 | 559.0 |
| 4 | H1 | 5 | 511.0 |
neuralforecast 的输入应该是一个长格式的数据框,包含三列:unique_id、ds 和 y。
unique_id(字符串、整数或类别):每个时间序列的唯一标识符。ds(整数或时间戳):整数索引时间或格式为 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS 的时间戳。y(数值型):要预测的目标变量。
该数据集包含414个独特的时间序列。为了减少总执行时间,我们将只使用前10个。
uids = Y_df['unique_id'].unique()[:10] # 选择10个ID以加快示例运行速度
Y_df = Y_df.query('unique_id in @uids').reset_index(drop=True)为了绘制系列数据,我们将使用 utilsforecast.plotting 中的 plot_series 方法。utilsforecast 是 neuralforecast 的依赖项,因此它应该已经安装。
from utilsforecast.plotting import plot_series
plot_series(Y_df)
3. 使用交叉验证训练多个模型
我们将使用 cross-validation 方法训练来自 neuralforecast 的不同模型,以决定哪个模型在历史数据上表现最佳。为此,我们需要导入 NeuralForecast 类以及我们想要比较的模型。
from neuralforecast import NeuralForecast
from neuralforecast.auto import MLP, NBEATS, NHITS在本教程中,我们将使用neuralforecast的MPL、NBEATS和NHITS模型。
首先,我们需要创建一个模型列表,然后实例化NeuralForecast类。对于每个模型,我们将定义以下超参数:
h:预测范围。这里,我们将使用与M4比赛相同的范围,即提前48步。input_size:模型用于进行预测的历史观察值(滞后)的数量。在这种情况下,它将是预测范围的两倍。loss:要优化的损失函数。在这里,我们将使用来自neuralforecast.losses.pytorch的多量分位损失(MQLoss)。
多分位损失(MQLoss)是每个目标分位的分位损失之和。单个分位的分位损失衡量模型对实际分布特定分位的预测效果,基于分位值对高估和低估进行不对称处罚。更多细节请参见 这里。
虽然对于每个模型可以定义其他超参数,但为了本教程的目的,我们将使用默认值。有关每个模型超参数的更多信息,请查阅相应的文档。
from neuralforecast.losses.pytorch import MQLoss
horizon = 48
models = [MLP(h=horizon, input_size=2*horizon, loss=MQLoss()),
NBEATS(h=horizon, input_size=2*horizon, loss=MQLoss()),
NHITS(h=horizon, input_size=2*horizon, loss=MQLoss()),]
nf = NeuralForecast(models=models, freq=1)Seed set to 1
Seed set to 1
Seed set to 1
cross_validation 方法接受以下参数:
df: 按照第2节中描述格式的数据框。n_windows(int): 要评估的窗口数量。默认值为1,此处我们将使用3。step_size(int): 产生预测的连续窗口之间的步长。在这个例子中,我们将step_size设置为horizon以产生不重叠的预测。以下图示展示了基于step_size参数和模型的预测视野h产生预测的方式。在这个图示中,step_size=2且h=4。

refit(bool或int): 是否为每个交叉验证窗口重新训练模型。如果为False,则在开始时训练模型,然后用于预测每个窗口。如果为正整数,则每refit窗口重新训练模型。默认值为False,但这里我们将refit设置为1,以便在每个窗口后使用截止时间之前的时间戳数据重新训练模型。
%%capture
cv_df = nf.cross_validation(Y_df, n_windows=3, step_size=horizon, refit=1)值得一提的是,neuralforecast中cross_validation方法的默认版本与其他库不同,后者通常在每个窗口开始时重新训练模型。默认情况下,它只训练模型一次,然后在所有窗口上使用这些模型生成预测,从而减少总执行时间。对于需要重新训练模型的场景,可以使用refit参数来指定模型应在多少个窗口之后重新训练。
cv_df.head()| unique_id | ds | cutoff | MLP-median | MLP-lo-90 | MLP-lo-80 | MLP-hi-80 | MLP-hi-90 | NBEATS-median | NBEATS-lo-90 | NBEATS-lo-80 | NBEATS-hi-80 | NBEATS-hi-90 | NHITS-median | NHITS-lo-90 | NHITS-lo-80 | NHITS-hi-80 | NHITS-hi-90 | y | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | H1 | 605 | 604 | 627.988586 | 534.398438 | 557.919495 | 706.230042 | 729.458984 | 622.185120 | 582.681030 | 598.783203 | 648.851318 | 653.490173 | 632.352661 | 579.588623 | 581.992249 | 641.553772 | 663.227234 | 622.0 |
| 1 | H1 | 606 | 604 | 567.843018 | 473.527313 | 503.057220 | 652.319458 | 702.797974 | 544.922485 | 489.482178 | 518.790222 | 572.878967 | 585.352661 | 553.002197 | 495.692871 | 504.136749 | 595.143494 | 616.993591 | 558.0 |
| 2 | H1 | 607 | 604 | 521.304260 | 412.619751 | 446.281494 | 598.887146 | 630.079163 | 496.092041 | 439.469055 | 450.099365 | 524.822876 | 523.129578 | 496.740112 | 449.902313 | 453.967438 | 539.902588 | 560.253723 | 513.0 |
| 3 | H1 | 608 | 604 | 488.616760 | 393.635559 | 403.897003 | 557.288452 | 600.689697 | 464.539612 | 409.801483 | 425.563171 | 483.017029 | 497.362915 | 455.020721 | 416.871094 | 420.002441 | 500.547150 | 507.826721 | 476.0 |
| 4 | H1 | 609 | 604 | 464.543854 | 339.080414 | 367.943787 | 536.813965 | 558.877808 | 444.119019 | 384.093872 | 409.416290 | 470.489075 | 474.619446 | 432.959076 | 389.460663 | 397.191345 | 465.580536 | 491.252747 | 449.0 |
cross-validation 方法的输出是一个数据框,其中包括以下列:
unique_id: 每个时间序列的唯一标识符。ds: 时间戳或时间索引。cutoff: 在该交叉验证窗口中使用的最后时间戳或时间索引。"model": 包含模型点预测(中位数)和预测区间的列。默认情况下,在使用 MQLoss 时包含 80% 和 90% 的预测区间。y: 实际值。
4. 评估模型并为每个序列选择最佳模型
为了评估模型的点预测,我们将使用均方根误差(RMSE),其定义为实际值与预测值之间平方差的均值的平方根。
为了方便,我们将使用 utilsforecast 中的 evaluate 和 rmse 函数。
from utilsforecast.evaluation import evaluate
from utilsforecast.losses import rmse evaluate 函数接受以下参数:
df:要评估的包含预测的数据框。metrics(列表):要计算的指标。models(列表):要评估的模型名称。默认值为None,这将使用在删除id_col、time_col和target_col后的所有列。id_col(字符串):标识序列的唯一 ID 的列。默认值为unique_id。time_col(字符串):带有时间戳或时间索引的列。默认值为ds。target_col(字符串):带有目标变量的列。默认值为y。
请注意,如果我们使用 models 的默认值,则需要从交叉验证数据框中排除 cutoff 列。
evaluation_df = evaluate(cv_df.loc[:, cv_df.columns != 'cutoff'], metrics=[rmse])对于每个唯一的 ID,我们将选择具有最低 RMSE 的模型。
evaluation_df['best_model'] = evaluation_df.drop(columns=['metric', 'unique_id']).idxmin(axis=1)
evaluation_df| unique_id | metric | MLP-median | NBEATS-median | NHITS-median | best_model | |
|---|---|---|---|---|---|---|
| 0 | H1 | rmse | 44.453354 | 48.828117 | 47.718341 | MLP-median |
| 1 | H10 | rmse | 24.375221 | 18.887296 | 17.938162 | NHITS-median |
| 2 | H100 | rmse | 165.888534 | 211.220029 | 200.504549 | MLP-median |
| 3 | H101 | rmse | 375.096472 | 201.191102 | 149.309690 | NHITS-median |
| 4 | H102 | rmse | 475.430266 | 321.459725 | 331.499041 | NBEATS-median |
| 5 | H103 | rmse | 8552.597224 | 9091.500057 | 8169.006459 | NHITS-median |
| 6 | H104 | rmse | 187.017183 | 194.206979 | 148.196734 | NHITS-median |
| 7 | H105 | rmse | 349.070196 | 304.997747 | 312.518832 | NBEATS-median |
| 8 | H106 | rmse | 196.937493 | 361.771205 | 357.442132 | MLP-median |
| 9 | H107 | rmse | 211.585091 | 236.404925 | 241.229147 | MLP-median |
我们可以汇总结果以查看每个模型获胜的次数。
summary_df = evaluation_df.groupby(['metric', 'best_model']).size().sort_values().to_frame()
summary_df = summary_df.reset_index()
summary_df.columns = ['metric', 'model', 'num. of unique_ids']
summary_df| metric | model | num. of unique_ids | |
|---|---|---|---|
| 0 | rmse | NBEATS-median | 2 |
| 1 | rmse | MLP-median | 4 |
| 2 | rmse | NHITS-median | 4 |
有了这些信息,我们现在知道在历史数据中哪个模型对每个序列的表现最好。
5. 绘制交叉验证结果
为了可视化交叉验证结果,我们将再次使用 plot_series 方法。我们需要将交叉验证输出中的 y 列重命名,以避免与原始数据框重复。我们还将排除 cutoff 列,并使用 max_insample_length 参数仅绘制最后 300 个观测值,以便更好地进行可视化。
cv_df.rename(columns = {'y' : 'actual'}, inplace = True) # 重命名实际值
plot_series(Y_df, cv_df.loc[:, cv_df.columns != 'cutoff'], max_insample_length=300)
为了进一步阐明交叉验证的概念,我们将绘制在每个截止点生成的预测,系列的 unique_id='H1'。由于我们设定了 n_windows=3,因此有三个截止点。在这个例子中,我们使用了 refit=1,因此每个模型在每个窗口中都使用截止时间之前及包含截止时间的数据重新训练。此外,由于 step_size 等于预测范围,生成的预测是非重叠的。
cutoff1, cutoff2, cutoff3 = cv_df['cutoff'].unique()
plot_series(Y_df, cv_df[cv_df['cutoff'] == cutoff1].loc[:, cv_df.columns != 'cutoff'], ids=['H1']) # 使用ids参数选择特定系列
plot_series(Y_df, cv_df[cv_df['cutoff'] == cutoff2].loc[:, cv_df.columns != 'cutoff'], ids=['H1'])
plot_series(Y_df, cv_df[cv_df['cutoff'] == cutoff3].loc[:, cv_df.columns != 'cutoff'], ids=['H1'])
Give us a ⭐ on Github