在许多应用中,一组时间序列被组织成层级结构。例子包括地理层级、产品或类别,它们定义了不同类型的聚合。在这种情况下,预测者通常需要为所有分解和聚合的序列提供预测。一个自然的期望是这些预测是“一致的”,也就是说,底层序列的预测值精确地加总等于聚合序列的预测值。

在本笔记本中,我们展示一个如何使用 HierarchicalForecast 来生成地理层级和时间层级之间一致预测的示例。我们将使用经典的澳大利亚国内旅游 (Tourism) 数据集,它包含澳大利亚每个州的月度游客数量时间序列。

我们将首先加载 Tourism 数据,并使用 StatsForecast 中的 AutoETS 模型生成基础预测。然后,我们根据跨部门地理层级,使用 HierarchicalForecast 中的几种协调算法来协调这些预测。最后,我们根据时间层级在时间维度上协调预测。

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

!pip install hierarchicalforecast statsforecast

1. 加载和处理数据

在本例中,我们将使用《预测:原理与实践》书中的 Tourism 数据集。

数据集仅包含最低层级的时间序列,因此我们需要为所有层级创建时间序列。

import numpy as np
import pandas as pd
Y_df = pd.read_csv('https://raw.githubusercontent.com/Nixtla/transfer-learning-time-series/main/datasets/tourism.csv')
Y_df = Y_df.rename({'Trips': 'y', 'Quarter': 'ds'}, axis=1)
Y_df.insert(0, 'Country', 'Australia')
Y_df = Y_df[['Country', 'Region', 'State', 'Purpose', 'ds', 'y']]
Y_df['ds'] = Y_df['ds'].str.replace(r'(\d+) (Q\d)', r'\1-\2', regex=True)
Y_df['ds'] = pd.PeriodIndex(Y_df["ds"], freq='Q').to_timestamp()
Y_df.head()
国家区域目的dsy
0澳大利亚阿德莱德南澳大利亚商务1998-01-01135.077690
1澳大利亚阿德莱德南澳大利亚商务1998-04-01109.987316
2澳大利亚阿德莱德南澳大利亚商务1998-07-01166.034687
3澳大利亚阿德莱德南澳大利亚商务1998-10-01127.160464
4澳大利亚阿德莱德南澳大利亚商务1999-01-01137.448533

2. 跨部门协调

2a. 根据跨部门层级聚合数据集

数据集可以按以下非严格的层级结构进行分组。

spec = [
    ['Country'],
    ['Country', 'State'], 
    ['Country', 'Purpose'], 
    ['Country', 'State', 'Region'], 
    ['Country', 'State', 'Purpose'], 
    ['Country', 'State', 'Region', 'Purpose']
]

使用 HierarchicalForecast 中的 aggregate 函数,我们可以获得完整的时间序列集。

from hierarchicalforecast.utils import aggregate
Y_df_cs, S_df_cs, tags_cs = aggregate(Y_df, spec)
Y_df_cs
unique_iddsy
0澳大利亚1998-01-0123182.197269
1澳大利亚1998-04-0120323.380067
2澳大利亚1998-07-0119826.640511
3澳大利亚1998-10-0120830.129891
4澳大利亚1999-01-0122087.353380
............
33995澳大利亚/西澳大利亚/珀斯体验/V...2016-10-01439.699451
33996澳大利亚/西澳大利亚/珀斯体验/V...2017-01-01356.867038
33997澳大利亚/西澳大利亚/珀斯体验/V...2017-04-01302.296119
33998澳大利亚/西澳大利亚/珀斯体验/V...2017-07-01373.442070
33999澳大利亚/西澳大利亚/珀斯体验/V...2017-10-01455.316702
S_df_cs.iloc[:5, :5]
unique_id澳大利亚/ACT/堪培拉/商务澳大利亚/ACT/堪培拉/度假澳大利亚/ACT/堪培拉/其他澳大利亚/ACT/堪培拉/探亲访友
0澳大利亚1.01.01.01.0
1澳大利亚/ACT1.01.01.01.0
2澳大利亚/新南威尔士0.00.00.00.0
3澳大利亚/北领地0.00.00.00.0
4澳大利亚/昆士兰0.00.00.00.0

2b. 拆分训练/测试集

我们使用最后两年(8个季度)作为测试集。因此,我们的预测 horizon=8。

horizon = 8
Y_test_df_cs = Y_df_cs.groupby("unique_id", as_index=False).tail(horizon)
Y_train_df_cs = Y_df_cs.drop(Y_test_df_cs.index)

2c. 计算基础预测

以下单元格使用 AutoETS 模型为 Y_df 中的每个时间序列计算基础预测。请注意,Y_hat_df 包含预测值,但它们并不一致。

from statsforecast.models import AutoETS
from statsforecast.core import StatsForecast
fcst = StatsForecast(models=[AutoETS(season_length=4, model='ZZA')], 
                     freq='QS', n_jobs=-1)
Y_hat_df_cs = fcst.forecast(df=Y_train_df_cs, h=horizon, fitted=True)
Y_fitted_df_cs = fcst.forecast_fitted_values()

2d. 协调预测

以下单元格使用 HierarchicalReconciliation 类使先前的预测保持一致。由于层级结构不是严格的,我们无法使用 TopDownMiddleOut 等方法。在本例中,我们使用 BottomUpMinTrace

from hierarchicalforecast.methods import BottomUp, MinTrace
from hierarchicalforecast.core import HierarchicalReconciliation
reconcilers = [
    BottomUp(),
    MinTrace(method='mint_shrink'),
    MinTrace(method='ols')
]
hrec = HierarchicalReconciliation(reconcilers=reconcilers)
Y_rec_df_cs = hrec.reconcile(Y_hat_df=Y_hat_df_cs, Y_df=Y_fitted_df_cs, S=S_df_cs, tags=tags_cs)

数据框 Y_rec_df 包含协调后的预测值。

Y_rec_df_cs.head()
unique_iddsAutoETSAutoETS/BottomUpAutoETS/MinTrace_method-mint_shrinkAutoETS/MinTrace_method-ols
0澳大利亚2016-01-0125990.06800424381.91173725428.08978325894.399067
1澳大利亚2016-04-0124458.49028222903.89596423914.27140024357.301898
2澳大利亚2016-07-0123974.05598422412.26573923428.46239423865.910647
3澳大利亚2016-10-0124563.45449523127.34957824089.84595524470.782393
4澳大利亚2017-01-0125990.06800424518.11800625545.35867825901.362283

3. 时间协调

接下来,我们旨在在时间域上也协调我们的预测。

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

我们首先定义时间聚合规范。规范是一个字典,其中键是聚合的名称,值是该聚合中应聚合的底层时间步长的数量。例如,year 包含 12 个月,因此我们定义一个键值对 "yearly":12。我们可以对我们感兴趣的其他聚合进行类似操作。

在此示例中,我们选择 yearsemiannualquarter 的时间聚合。底层时间步长是季度频率的。

spec_temporal = {"year": 4, "semiannual": 2, "quarter": 1}

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

from hierarchicalforecast.utils import aggregate_temporal
Y_train_df_te, S_train_df_te, tags_te_train = aggregate_temporal(df=Y_train_df_cs, spec=spec_temporal)
Y_test_df_te, S_test_df_te, tags_te_test = aggregate_temporal(df=Y_test_df_cs, spec=spec_temporal)
S_train_df_te.iloc[:5, :5]
temporal_idquarter-1quarter-2quarter-3quarter-4
0year-11.01.01.01.0
1year-20.00.00.00.0
2year-30.00.00.00.0
3year-40.00.00.00.0
4year-50.00.00.00.0
S_test_df_te.iloc[:5, :5]
temporal_idquarter-1quarter-2quarter-3quarter-4
0year-11.01.01.01.0
1year-20.00.00.00.0
2semiannual-11.01.00.00.0
3semiannual-20.00.01.01.0
4semiannual-30.00.00.00.0

如果您没有可用的测试集,通常是进行预测时的情况,则需要创建一个未来数据框,其中包含正确的底层 unique_ids 和时间戳,以便可以进行时间聚合。我们可以使用 make_future_dataframe 辅助函数来完成此操作。

from hierarchicalforecast.utils import make_future_dataframe
Y_test_df_te_new = make_future_dataframe(Y_train_df_te, freq="QS", h=horizon)

Y_test_df_te_new 然后可以在 aggregate_temporal 中使用,以构建时间聚合结构。

Y_test_df_te_new, S_test_df_te_new, tags_te_test_new = aggregate_temporal(df=Y_test_df_te_new, spec=spec_temporal)

我们可以验证我们是否拥有相同的时间聚合测试集,只是 Y_test_df_te_new 不包含真实值 y

Y_test_df_te
temporal_idunique_iddsy
0year-1澳大利亚2016-10-01101484.586551
1year-2澳大利亚2017-10-01107709.864650
2year-1澳大利亚/ACT2016-10-012457.401367
3year-2澳大利亚/ACT2017-10-012734.748452
4year-1澳大利亚/ACT/商务2016-10-01754.139245
...............
5945quarter-4澳大利亚/西澳大利亚/探亲访友2016-10-01787.030391
5946quarter-5澳大利亚/西澳大利亚/探亲访友2017-01-01702.777251
5947quarter-6澳大利亚/西澳大利亚/探亲访友2017-04-01642.516090
5948quarter-7澳大利亚/西澳大利亚/探亲访友2017-07-01646.521395
5949quarter-8澳大利亚/西澳大利亚/探亲访友2017-10-01813.184778
Y_test_df_te_new
temporal_idunique_idds
0year-1澳大利亚2016-10-01
1year-2澳大利亚2017-10-01
2year-1澳大利亚/ACT2016-10-01
3year-2澳大利亚/ACT2017-10-01
4year-1澳大利亚/ACT/商务2016-10-01
............
5945quarter-4澳大利亚/西澳大利亚/探亲访友2016-10-01
5946quarter-5澳大利亚/西澳大利亚/探亲访友2017-01-01
5947quarter-6澳大利亚/西澳大利亚/探亲访友2017-04-01
5948quarter-7澳大利亚/西澳大利亚/探亲访友2017-07-01
5949quarter-8澳大利亚/西澳大利亚/探亲访友2017-10-01

3b. 计算基础预测

现在,我们需要为每个时间聚合计算基础预测。以下单元格使用 AutoETS 模型为 Y_train_df_te 中的每个时间聚合计算基础预测。请注意,Y_hat_df_te 包含预测值,但它们并不一致。

另请注意,每个时间聚合的频率和 horizon 都不同。在此示例中,最低层级具有季度频率,horizon 为 8(构成 2 年)。因此,year 聚合具有年度频率,horizon 为 2

当然,可以为时间聚合中的每个层级选择不同的模型 - 您可以随心所欲地创新!

Y_hat_dfs_te = []
id_cols = ["unique_id", "temporal_id", "ds", "y"]
# We will train a model for each temporal level
for level, temporal_ids_train in tags_te_train.items():
    # Filter the data for the level
    Y_level_train = Y_train_df_te.query("temporal_id in @temporal_ids_train")
    temporal_ids_test = tags_te_test[level]
    Y_level_test = Y_test_df_te.query("temporal_id in @temporal_ids_test")
    # For each temporal level we have a different frequency and forecast horizon
    freq_level = pd.infer_freq(Y_level_train["ds"].unique())
    horizon_level = Y_level_test["ds"].nunique()
    # Train a model and create forecasts
    fcst = StatsForecast(models=[AutoETS(model='ZZZ')], freq=freq_level, n_jobs=-1)
    Y_hat_df_te_level = fcst.forecast(df=Y_level_train[["ds", "unique_id", "y"]], h=horizon_level)
    # Add the test set to the forecast
    Y_hat_df_te_level = Y_hat_df_te_level.merge(Y_level_test, on=["ds", "unique_id"], how="left")
    # Put cols in the right order (for readability)
    Y_hat_cols = id_cols + [col for col in Y_hat_df_te_level.columns if col not in id_cols]
    Y_hat_df_te_level = Y_hat_df_te_level[Y_hat_cols]
    # Append the forecast to the list
    Y_hat_dfs_te.append(Y_hat_df_te_level)

Y_hat_df_te = pd.concat(Y_hat_dfs_te, ignore_index=True)

3c. 协调预测

我们可以再次使用 HierarchicalReconciliation 类来协调预测。在此示例中,我们使用 BottomUpMinTrace。请注意,我们必须在 reconcile 函数中设置 temporal=True

请注意,时间协调目前不支持样本内(insample)协调方法,例如 MinTrace(method='mint_shrink')

reconcilers = [
    BottomUp(),
    MinTrace(method='ols')
]
hrec = HierarchicalReconciliation(reconcilers=reconcilers)
Y_rec_df_te = hrec.reconcile(Y_hat_df=Y_hat_df_te, S=S_test_df_te, tags=tags_te_test, temporal=True)

4. 评估

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

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

4a. 跨部门评估

我们首先评估所有跨部门聚合的预测。

eval_tags = {}
eval_tags['Total'] = tags_cs['Country']
eval_tags['Purpose'] = tags_cs['Country/Purpose']
eval_tags['State'] = tags_cs['Country/State']
eval_tags['Regions'] = tags_cs['Country/State/Region']
eval_tags['Bottom'] = tags_cs['Country/State/Region/Purpose']

evaluation = evaluate(df = Y_rec_df_te.drop(columns = 'temporal_id'),
                      tags = eval_tags,
                      metrics = [rmse])

evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(ols)']
numeric_cols = evaluation.select_dtypes(include="number").columns
evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64)
evaluation
levelmetricBaseBottomUpMinTrace(ols)
0Totalrmse4249.254461.954234.55
1目的rmse1222.571273.481137.57
2rmse635.78546.02611.32
3Regionsrmse103.67107.0099.23
4Bottomrmse33.1533.9832.30
5Overallrmse81.8982.4178.97

可以看出,MinTrace(ols) 似乎是每个跨部门聚合的最佳预测方法。

4b. 时间评估

然后,我们评估所有时间聚合的时间聚合预测。

evaluation = evaluate(df = Y_rec_df_te.drop(columns = 'unique_id'),
                      tags = tags_te_test,
                      metrics = [rmse],
                      id_col="temporal_id")

evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(ols)']
numeric_cols = evaluation.select_dtypes(include="number").columns
evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64)
evaluation
levelmetricBaseBottomUpMinTrace(ols)
0yearrmse480.85581.18515.32
1semiannualrmse312.33304.98275.30
2quarterrmse168.02168.02155.61
3Overallrmse253.94266.17241.19

同样,MinTrace(ols) 是总体最佳方法,在 quarter 聚合预测上得分最低的 rmse,而在 year 聚合预测上略差于 Base 预测。

4c. 跨时间评估

最后,我们进行跨时间评估。为此,我们首先需要获取跨部门层级和时间层级的组合,我们可以使用 get_cross_temporal_tags 辅助函数来完成。

from hierarchicalforecast.utils import get_cross_temporal_tags
Y_rec_df_te, tags_ct = get_cross_temporal_tags(Y_rec_df_te, tags_cs=tags_cs, tags_te=tags_te_test)

正如我们所见,我们现在有一个标签 Country//year,其中包含 Australia//year-1Australia//year-2,表示跨部门层级 Australia 在时间层级 20162017 下的聚合。

tags_ct["Country//year"]
['Australia//year-1', 'Australia//year-2']

我们现在的数据集和跨时间标签已准备好进行评估。

我们定义一组 eval_tags,现在我们将每个跨部门聚合也按每个时间聚合进行拆分。请注意,我们在下面的概述中跳过了 semiannual 时间聚合。

eval_tags = {}
eval_tags['TotalByYear'] = tags_ct['Country//year']
eval_tags['RegionsByYear'] = tags_ct['Country/State/Region//year']
eval_tags['BottomByYear'] = tags_ct['Country/State/Region/Purpose//year']
eval_tags['TotalByQuarter'] = tags_ct['Country//quarter']
eval_tags['RegionsByQuarter'] = tags_ct['Country/State/Region//quarter']
eval_tags['BottomByQuarter'] = tags_ct['Country/State/Region/Purpose//quarter']


evaluation = evaluate(df = Y_rec_df_te.drop(columns=['unique_id', 'temporal_id']),
                      tags = eval_tags,
                      id_col = 'cross_temporal_id',
                      metrics = [rmse])

evaluation.columns = ['level', 'metric', 'Base', 'BottomUp', 'MinTrace(ols)']
numeric_cols = evaluation.select_dtypes(include="number").columns
evaluation[numeric_cols] = evaluation[numeric_cols].map('{:.2f}'.format).astype(np.float64)
evaluation
levelmetricBaseBottomUpMinTrace(ols)
0TotalByYearrmse7148.998243.067748.40
1RegionsByYearrmse151.96175.69158.48
2BottomByYearrmse46.9850.7846.72
3TotalByQuarterrmse2060.772060.771942.32
4RegionsByQuarterrmse57.0757.0754.12
5BottomByQuarterrmse19.4219.4218.69
6Overallrmse43.1445.2742.49

我们发现最佳方法是跨时间协调方法 AutoETS/MinTrace_method-ols,它实现了总体最低的 RMSE。

参考文献