在本 Notebook 中,我们将演示如何使用 HierarchicalForecast 在时间层级之间生成一致的预测。我们将使用 M3 数据集的月度和季度时间序列。首先,我们将加载 M3 数据,并使用 StatsForecast 中的 AutoETS 模型生成基础预测。然后,根据指定的时间层级,使用 HierarchicalForecast 中的 THIEF(时间序列层级预测)对预测进行调和。

参考文献

Athanasopoulos, G, Hyndman, Rob J., Kourentzes, N., Petropoulos, Fotios (2017). Forecasting with temporal hierarchies. European Journal of Operational Research, 262, 60-74

您可以使用 Google Colab 在 CPU 或 GPU 上运行这些实验。

!pip install hierarchicalforecast statsforecast datasetsforecast

1. 加载和处理数据

import numpy as np
import pandas as pd
from datasetsforecast.m3 import M3
m3_monthly, _, _ = M3.load(directory='data', group='Monthly')
m3_quarterly, _, _ = M3.load(directory='data', group='Quarterly')

我们将进行高达年度级别的时间序列聚合,因此对于月度和季度数据,我们确保每个时间序列都包含底层时间步长的整数倍。

例如,m3_monthly 中的第一个时间序列(unique_id='M1')有 68 个时间步长。这不是 12 的倍数(一年有 12 个月),因此我们无法将所有时间步长聚合成年。因此,我们截断(移除)了前 8 个时间步长,使该序列剩余 60 个时间步长。对于季度数据,我们也做了类似的处理,只是以 4 为倍数(一年有 4 个季度)。

根据您调和问题中最高的时间序列聚合级别,您可能需要以不同的方式截断数据。

m3_monthly = m3_monthly.groupby("unique_id", group_keys=False)\
                       .apply(lambda x: x.tail(len(x) //  12 * 12))\
                       .reset_index(drop=True)

m3_quarterly = m3_quarterly.groupby("unique_id", group_keys=False)\
                           .apply(lambda x: x.tail(len(x) //  4 * 4))\
                           .reset_index(drop=True)

2. 时间序列调和

2a. 划分训练/测试集

按照原始 THIEF 论文的做法,我们将月度序列的最后 24 个观测值和每个季度序列的最后 8 个观测值用作测试样本。

horizon_monthly = 24
horizon_quarterly = 8
m3_monthly_test = m3_monthly.groupby("unique_id", as_index=False).tail(horizon_monthly)
m3_monthly_train = m3_monthly.drop(m3_monthly_test.index)

m3_quarterly_test = m3_quarterly.groupby("unique_id", as_index=False).tail(horizon_quarterly)
m3_quarterly_train = m3_quarterly.drop(m3_quarterly_test.index)

2a. 根据时间层级聚合数据集

我们首先定义时间序列聚合规范。规范是一个字典,其中键是聚合的名称,值是在该聚合中应该聚合的底层时间步长数量。例如,year12 个月组成,因此我们定义了一个键值对 "yearly":12。对于我们感兴趣的其他聚合,我们可以做类似的事情。

spec_temporal_monthly = {"yearly": 12, "semiannually": 6, "fourmonthly": 4, "quarterly": 3, "bimonthly": 2, "monthly": 1}
spec_temporal_quarterly = {"yearly": 4, "semiannually": 2, "quarterly": 1}

接下来,我们使用 aggregate_temporal 函数计算时间序列聚合的训练集和测试集。请注意,由于测试集包含训练集中未包含的时间层级,因此训练集和测试集的聚合矩阵 S 是不同的。

from hierarchicalforecast.utils import aggregate_temporal
# Monthly
Y_monthly_train, S_monthly_train, tags_monthly_train = aggregate_temporal(df=m3_monthly_train, spec=spec_temporal_monthly)
Y_monthly_test, S_monthly_test, tags_monthly_test = aggregate_temporal(df=m3_monthly_test, spec=spec_temporal_monthly)

# Quarterly
Y_quarterly_train, S_quarterly_train, tags_quarterly_train = aggregate_temporal(df=m3_quarterly_train, spec=spec_temporal_quarterly)
Y_quarterly_test, S_quarterly_test, tags_quarterly_test = aggregate_temporal(df=m3_quarterly_test,  spec=spec_temporal_quarterly)

我们的聚合矩阵将最低时间粒度(季度)聚合至年份,用于训练集和测试集。

S_monthly_train.iloc[:5, :5]
temporal_idmonthly-1monthly-2monthly-3monthly-4
0yearly-10.00.00.00.0
1yearly-20.00.00.00.0
2yearly-30.00.00.00.0
3yearly-40.00.00.00.0
4yearly-50.00.00.00.0
S_monthly_test.iloc[:5, :5]
temporal_idmonthly-1monthly-2monthly-3monthly-4
0yearly-11.01.01.01.0
1yearly-20.00.00.00.0
2semiannually-11.01.01.01.0
3semiannually-20.00.00.00.0
4semiannually-30.00.00.00.0

2b. 计算基础预测

现在,我们需要计算每个时间序列聚合的基础预测。以下单元格使用 AutoARIMA 模型计算 Y_monthly_trainY_quarterly_train 中每个时间序列聚合的基础预测。请注意,Y_hats 包含预测结果,但它们不一致。

另请注意,每个时间序列聚合的频率和预测范围都不同。对于月度数据,最低级别是月度频率,预测范围为 24(即 2 年)。然而,例如,year 聚合的频率是年度,预测范围为 2。

当然,您可以为时间序列聚合中的每个级别选择不同的模型——您可以尽情发挥创造力!

from statsforecast.models import AutoARIMA
from statsforecast.core import StatsForecast
c:\Users\ospra\miniconda3\envs\hierarchicalforecast\lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
Y_hats = []
id_cols = ["unique_id", "temporal_id", "ds", "y"]

# We loop over the monthly and quarterly data
for tags_train, tags_test, Y_train, Y_test in zip([tags_monthly_train, tags_quarterly_train], 
                                                  [tags_monthly_test, tags_quarterly_test],
                                                  [Y_monthly_train, Y_quarterly_train], 
                                                  [Y_monthly_test, Y_quarterly_test]):
    # We will train a model for each temporal level
    Y_hats_tags = []
    for level, temporal_ids_train in tags_train.items():
        # Filter the data for the level
        Y_level_train = Y_train.query("temporal_id in @temporal_ids_train")
        temporal_ids_test = tags_test[level]
        Y_level_test = Y_test.query("temporal_id in @temporal_ids_test")
        # For each temporal level we have a different frequency and forecast horizon. We use the timestamps of the first timeseries to automatically derive the frequency & horizon of the temporally aggregated series.
        unique_id = Y_level_train["unique_id"].iloc[0]
        freq_level = pd.infer_freq(Y_level_train.query("unique_id == @unique_id")["ds"])
        horizon_level = Y_level_test.query("unique_id == @unique_id")["ds"].nunique()
        # Train a model and create forecasts
        fcst = StatsForecast(models=[AutoARIMA()], freq=freq_level, n_jobs=-1)
        Y_hat_level = fcst.forecast(df=Y_level_train[["ds", "unique_id", "y"]], h=horizon_level)
        # Add the test set to the forecast
        Y_hat_level = pd.concat([Y_level_test.reset_index(drop=True), Y_hat_level.drop(columns=["unique_id", "ds"])], axis=1)
        # Put cols in the right order (for readability)
        Y_hat_cols = id_cols + [col for col in Y_hat_level.columns if col not in id_cols]
        Y_hat_level = Y_hat_level[Y_hat_cols]
        # Append the forecast to the list
        Y_hats_tags.append(Y_hat_level)

    Y_hat_tag = pd.concat(Y_hats_tags, ignore_index=True)
    Y_hats.append(Y_hat_tag)

2c. 调和预测

我们可以使用 HierarchicalReconciliation 类来调和预测。在本例中,我们使用 BottomUpMinTrace(wls_struct)。后者是 Forecasting with temporal hierarchies 中介绍的“结构缩放”方法。

请注意,在 reconcile 函数中,我们必须设置 temporal=True

from hierarchicalforecast.methods import BottomUp, MinTrace
from hierarchicalforecast.core import HierarchicalReconciliation
reconcilers = [
    BottomUp(),
    MinTrace(method="wls_struct"),
]
hrec = HierarchicalReconciliation(reconcilers=reconcilers)
Y_recs = []
# We loop over the monthly and quarterly data
for Y_hat, S, tags in zip(Y_hats, 
                          [S_monthly_test, S_quarterly_test], 
                          [tags_monthly_test, tags_quarterly_test]):
    Y_rec = hrec.reconcile(Y_hat_df=Y_hat, S=S, tags=tags, temporal=True)
    Y_recs.append(Y_rec)

3. 评估

HierarchicalForecast 包包含 evaluate 函数,用于评估不同的层级。

我们评估所有时间序列聚合上的时间序列聚合预测结果。

from hierarchicalforecast.evaluation import evaluate
from utilsforecast.losses import mae

3a. 月度

Y_rec_monthly = Y_recs[0]
evaluation = evaluate(df = Y_rec_monthly.drop(columns = 'unique_id'),
                      tags = tags_monthly_test,
                      metrics = [mae],
                      id_col='temporal_id',
                      benchmark="AutoARIMA")

evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(wls_struct)']
numeric_cols = evaluation.select_dtypes(include="number").columns
evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64)

evaluation
层级指标基础自下而上MinTrace(wls_struct)
0年度mae-scaled1.00.780.75
1半年度mae-scaled1.00.990.95
2四月度mae-scaled1.00.960.93
3季度mae-scaled1.00.950.93
4双月度mae-scaled1.00.960.94
5月度mae-scaled1.01.000.99
6总体mae-scaled1.00.940.92

MinTrace(wls_struct) 是总体表现最好的方法,在所有层级上的 mae 得分最低。

3b. 季度

Y_rec_quarterly = Y_recs[1]
evaluation = evaluate(df = Y_rec_quarterly.drop(columns = 'unique_id'),
                      tags = tags_quarterly_test,
                      metrics = [mae],
                      id_col='temporal_id',
                      benchmark="AutoARIMA")

evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(wls_struct)']
numeric_cols = evaluation.select_dtypes(include="number").columns
evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64)

evaluation
层级指标基础自下而上MinTrace(wls_struct)
0年度mae-scaled1.00.870.85
1半年度mae-scaled1.01.031.00
2季度mae-scaled1.01.000.97
3总体mae-scaled1.00.970.94

同样,MinTrace(wls_struct) 是总体表现最好的方法,在所有层级上的 mae 得分最低。