回归模型

以下是对 Darts 中回归模型的深入演示 - 从基本到高级功能,包括:

  • Darts 的回归模型

  • 滞后和滞后数据提取

  • 协变量使用

  • 参数 output_chunk_length 与 multi_models 的关系

  • 一次性预测和自回归预测

  • 多输出支持

  • 概率预测

  • 可解释性

  • 以及更多

[1]:
# fix python path if working locally
from utils import fix_pythonpath_if_working_locally

fix_pythonpath_if_working_locally()

# activate javascript
from shap import initjs

initjs()
Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
[2]:
%load_ext autoreload
%autoreload 2
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.linear_model import BayesianRidge

from darts.models import (
    LinearRegressionModel,
    RegressionModel,
    LightGBMModel,
    XGBModel,
    CatBoostModel,
)
from darts.metrics import mape
from darts.datasets import ElectricityConsumptionZurichDataset
from darts.explainability import ShapExplainer

输入数据集

对于这个笔记本,我们使用了来自瑞士苏黎世家庭用电量的数据集。

该数据集的频率为每刻钟一次(15分钟时间间隔),但我们将其重新采样为每小时一次以保持简单。

目标系列 (我们想要预测的系列): - Value_NE5: 电网级别5的家庭电力消耗 (以千瓦时为单位)。

协变量**(用于帮助改进预测的外部数据):数据集还包含我们可以用作协变量的天气测量数据。为简单起见,我们使用: - **T [°C]:测量的温度 - StrGlo [W/m2]:测量的太阳辐射 - RainDur [min]:测量的降雨持续时间

[3]:
ts_energy = ElectricityConsumptionZurichDataset().load()

# extract values recorded between 2017 and 2019
start_date = pd.Timestamp("2017-01-01")
end_date = pd.Timestamp("2019-01-31")
ts_energy = ts_energy[start_date:end_date]

# resample to hourly frequency
ts_energy = ts_energy.resample(freq="H")

# extract temperature, solar irradiation and rain duration
ts_weather = ts_energy[["T [°C]", "StrGlo [W/m2]", "RainDur [min]"]]

# extract households energy consumption
ts_energy = ts_energy["Value_NE5"]

# create train and validation splits
validation_cutoff = pd.Timestamp("2018-10-31")
ts_energy_train, ts_energy_val = ts_energy.split_after(validation_cutoff)

ts_energy.plot()
plt.show()

ts_weather.plot()
plt.show()
../_images/examples_20-RegressionModel-examples_4_0.png
../_images/examples_20-RegressionModel-examples_4_1.png

Darts 回归模型

回归是一种统计方法,用于数据科学和机器学习中,以建模因变量(目标 y)与一个或多个自变量(特征 X)之间的关系。

为了方便,Darts 核心包自带了一些回归模型: - LinearRegressionModelRandomForest:完全集成的 sklearn 模型 - RegressionModel:将 Darts 模型 API 包装在任何类似 sklearn 的模型周围 - XGBModel:围绕 XGBoost 的 XGBRegressor 的包装器

除了这些,我们还为一些最先进的回归模型提供了一个统一的API,可以按照我们的 安装指南 进行安装:

  • LightGBMModel: 围绕 LightGBM 的 LightGBMRegressor 的包装器

  • CatBoostModel: 围绕 CatBoost 的 CatBoostRegressor 的包装器。

在 Darts 中,预测问题通过将时间序列转换为两个表格数组,转化为回归问题:- X:特征或输入数组,形状为(样本数/观测数,特征数)- 特征数由(特定特征的)目标、过去和未来协变量的滞后总数给出。- y:目标或标签数组,形状为(样本数/观测数,目标数)- 目标数由模型参数 output_chunk_lengthmulti_models 给出(我们稍后会解释这一点)。

目标和协变量滞后

滞后特征是指相对于某个参考点,特征在之前或未来时间步的值。

在Darts中,滞后值指定了特征值相对于每个观察/样本(X``中的一行)的第一个预测目标时间步 ``y_t0 的位置。

  • lag == 0: 第一个预测时间步 t0 的位置,例如 y_t0 的位置

  • lag < 0: 所有位置在第一个预测时间步 t-1, t-2, … 的过去。

  • lag > 0: 所有在第一个预测时间步 t+1, t+2, … 之后的未来位置

滞后选择在实现良好的预测准确性方面至关重要。我们希望模型能够接收到相关信息,以捕捉目标序列的时间特性/依赖关系(模式、季节性、趋势等)。它还对模型的性能/复杂性有重大影响,因为每个额外的滞后都会为 X 添加一个新特征。

在模型创建时,我们可以分别为目标序列和协变量序列设置滞后。

  • lags: 目标序列(我们想要预测的那个)的滞后项

  • lags_past_covariates: 可选地,过去协变量序列的滞后(外部过去观测特征,有助于提高预测)

  • lags_future_covariates: 可选地,未来协变量序列的滞后(外部未来已知特征,有助于提高预测)

定义滞后的方式有多种 (int, List[int], Tuple[int, int], ...)。你可以在 RegressionModel 文档 中找到更多关于这方面的信息。

滞后数据提取

现在,让我们看看如何从下面的场景中提取 Xy 用于训练:

  • lags=[-4,-3,-2,-1]: 使用第一个预测时间步(橙色)之前的最后4个目标值(绿色)作为 X 特征。

  • output_chunk_length=1: 预测下一个 (1) 时间步的目标值 y (橙色)。

  • 我们有一个包含7个时间步的目标序列 t_i, ..., t_i+6 (蓝色)。

注意:此示例仅展示目标 ``lags`` 提取,但同样适用于 ``lags_past/future_covariates``。

forecast_single_model_ocl3

示例

让我们尝试将此应用于我们的电力数据集。假设我们想要预测训练集结束后的接下来几个小时的消耗量。

作为输入特征,我们提供它从一天前和两天前的同一小时的消费数据 -> lags=[-24, -48]

[4]:
model = LinearRegressionModel(lags=[-24, -48])
model.fit(ts_energy_train)
pred = model.predict(12)

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:12].plot(label="validation")
pred.plot(label="forecast")
[4]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_6_1.png

基于协变量的预测

要在目标系列的历史数据旁边使用外部数据,我们指定过去和/或未来的协变量滞后,然后将 past_covariates 和/或 future_covariates 传递给 fit()predict()

让我们假设我们有的不是天气测量数据,而是天气预报。那么我们可以将它们用作 future_covariates。我们在这里仅出于演示目的这样做!

下面是一个使用过去24小时(24)的每小时目标序列(电力消耗)和我们的天气“预测”中预测时间步长([0])的值的示例。

[5]:
model = LinearRegressionModel(lags=24, lags_future_covariates=[0])
model.fit(ts_energy_train, future_covariates=ts_weather)
pred = model.predict(12)

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:12].plot(label="validation")
pred.plot(label="forecast")
[5]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_8_1.png

仅使用协变量

有时,我们可能也对纯粹依赖协变量值的预测模型感兴趣。

为此,至少指定 lags_past_covariateslags_future_covariates 中的一个,并将 lags 设置为 None。Darts 回归模型以监督方式进行训练,因此我们仍然需要提供训练的目标序列。

仅使用天气“预报”作为输入,我们能多好地预测电力消耗?

元组 (24, 1) 表示 (过去滞后的数量, 未来滞后的数量)。

[6]:
model = LinearRegressionModel(lags=None, lags_future_covariates=(24, 1))
model.fit(series=ts_energy_train, future_covariates=ts_weather)
pred = model.predict(12)

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:12].plot(label="validation")
pred.plot(label="forecast")
[6]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_10_1.png

如果我们添加一些日历信息呢?我们可以使用 add_encoders 让模型免费生成日历属性编码。

[7]:
model = LinearRegressionModel(
    lags=None,
    lags_future_covariates=(24, 1),
    add_encoders={
        "cyclic": {"future": ["minute", "hour", "dayofweek", "month"]},
        "tz": "CET",
    },
)
model.fit(series=ts_energy_train, future_covariates=ts_weather)
pred = model.predict(12)

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:12].plot(label="validation")
pred.plot(label="forecast")
plt.show()
../_images/examples_20-RegressionModel-examples_12_0.png

特定组件的延迟

如果目标或任何协变量是多元的(具有多个分量/列),我们可以为每个分量使用专门的滞后。只需将字典传递给 lags* 参数,其中分量名称作为键,滞后作为值。

在下面的示例中,我们将默认滞后设置为 (24,1)``(用于协变量组件 ``'T [°C]''StrGlo [W/ms]'),并为特征 'RainDur [min]' 指定一个专用滞后 [0]

[8]:
model = LinearRegressionModel(
    lags=None, lags_future_covariates={"default_lags": (24, 1), "RainDur [min]": [0]}
)
model.fit(series=ts_energy_train, future_covariates=ts_weather)
pred = model.predict(12)

ts_energy_train[-48:].plot(label="training data")
ts_energy_val[:12].plot(label="validation data")
pred.plot(label="forecast")
[8]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_14_1.png

模型的输出块长度

此关键参数设置了 内部回归模型可以一次预测的时间步数

这与 predict() 中的预测范围 n 不同,后者是 期望 生成的预测点数量,可以通过以下方式实现:- 单次预测(如果 n <= output_chunk_length),或 - 自回归预测,消耗其自身的预测(以及协变量的未来值)作为额外预测的输入(否则)

例如,如果我们希望模型根据前一天的消费情况预测接下来24小时的电力消耗,设置 output_chunk_length=24 可以确保模型不会使用其预测值或未来协变量的值来预测整个一天。

[9]:
model_auto_regression = LinearRegressionModel(lags=24, output_chunk_length=1)
model_single_shot = LinearRegressionModel(lags=24, output_chunk_length=24)

model_auto_regression.fit(ts_energy_train)
model_single_shot.fit(ts_energy_train)

pred_auto_regression = model_auto_regression.predict(24)
pred_single_shot = model_single_shot.predict(24)

ts_energy_train[-48:].plot(label="training data")
ts_energy_val[:24].plot(label="validation data")
pred_auto_regression.plot(label="forecast (auto-regression)")
pred_single_shot.plot(label="forecast (single-shot)")
[9]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_16_1.png

多模型预测

output_chunk_length>1 时,可以通过修改 multi_models 参数来进一步参数化模型行为。

multi_models=True 是 Darts 中的默认行为,如上所示。我们创建了 output_chunk_length 个模型的副本,并训练每个模型来预测 output_chunk_length 个时间步长中的一个(使用相同的输入)。这种方法在计算和内存上更为密集,但往往能产生更好的结果。

单模型预测

multi_model=False 时,我们使用单一模型,并训练它仅预测 output_chunk_length 中的最后一个点。这降低了模型复杂性,因为只会训练和存储一组系数。

我们仍然能够通过在表格化过程中移动滞后来预测从1到``output_chunk_length``的所有点。这意味着每个预测值都使用了一组新的输入值。由于移动,训练序列的最小长度要求也将增加。

[10]:
multi_models = LinearRegressionModel(lags=24, output_chunk_length=24, multi_models=True)
single_model = LinearRegressionModel(
    lags=24, output_chunk_length=24, multi_models=False
)

multi_models.fit(ts_energy_train)
single_model.fit(ts_energy_train)

pred_multi_models = multi_models.predict(24)
pred_single_model = single_model.predict(24)

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:24].plot(label="validation")
pred_multi_models.plot(label="forecast (multi models)")
pred_single_model.plot(label="forecast (single model)")
[10]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_18_1.png

可视化

为了可视化内部发生的事情,让我们简化模型并展示不同参数的过程。

模型设置:LinearRegressionModel(lags=[-4, -3, -2, -1], output_chunk_length=2, multi_models=True)

当调用 predict(n=4) 时,输入序列的处理如下:

forecast_multi_model_ocl2

由于 n>output_chunk_length,模型必须使用自回归,预测期由两个大小为 output_chunk_length 的块组成。每个输出块没有滞后偏移,因为 multi_models=True

模型设置: LinearRegressionModel(lags=[-4, -3, -2, -1], output_chunk_length=2, multi_models=False).

forecast_single_model_ocl2

除了自回归预测外,我们可以看到滞后随着每个预测时间步的变化而变化,因为 multi_models=False

在模型训练的表格化过程中,同样的过程也会发生:每个绿色块与一个橙色值配对,构成训练数据集。

概率预测

要将模型设为概率性模型,在创建 RegressionModel 时将参数 likelihood 设置为 quantilepoisson。在预测时,概率性模型可以: - 当 num_samples > 1 时,使用蒙特卡罗采样基于拟合的分布参数生成样本 - 当 predict_likelihood_parameters=True 时,返回拟合的分布参数

请注意,当使用 quantile 回归器时,每个分位数将由不同的模型拟合。

概率模型在每次调用 predict() 时,如果 num_samples > 1,将会生成不同的预测结果。为了获得可重复的结果,请在创建模型时设置随机种子,并按完全相同的顺序调用方法。

[11]:
model = XGBModel(
    lags=24, output_chunk_length=1, likelihood="quantile", quantiles=[0.05, 0.5, 0.95]
)

model.fit(ts_energy_train)

pred_samples = model.predict(n=24, num_samples=200)
pred_params = model.predict(n=1, num_samples=1, predict_likelihood_parameters=True)

for val, comp in zip(pred_params.values()[0], pred_params.components):
    print(f"{comp} : {round(val, 3)}")

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:24].plot(label="validation")
pred_samples.plot(label="forecasts")
plt.tight_layout()
Value_NE5_q0.05 : 19580.123046875
Value_NE5_q0.50 : 19623.302734375
Value_NE5_q0.95 : 20418.7421875
../_images/examples_20-RegressionModel-examples_21_1.png

MultiOutputRegressor 包装器

一些回归模型支持原生的多输出支持。这对于拟合和预测是必需的:- 多个输出/多个目标步骤(使用 output_chunk_length>1multi_models=True)- 使用分位数回归的概率模型 - 多变量序列

对于不支持此功能的模型,Darts 会在其周围包装 sklearn 的 MultiOutputRegressor 来处理底层的逻辑。

可解释性

我们通过 Darts 的 ShapExplainer 为回归模型提供可解释性。解释器使用基于博弈论的 shap 库,为我们预测的时间范围提供特征重要性的见解。

[12]:
model = LinearRegressionModel(lags=24, lags_future_covariates=(24, 24))
model.fit(ts_energy_train, future_covariates=ts_weather)
shap_explainer = ShapExplainer(model=model)
shap_values = shap_explainer.summary_plot()
No data for colormapping provided via 'c'. Parameters 'vmin', 'vmax' will be ignored
../_images/examples_20-RegressionModel-examples_24_1.png
[13]:
# extracting the end of each series to reduce computation time
foreground_target = ts_energy_train[-24 * 2 :]
foreground_future_cov = ts_weather[foreground_target.start_time() :]

shap_explainer.force_plot_from_ts(
    foreground_series=foreground_target,
    foreground_future_covariates=foreground_future_cov,
)

# the plot cannot be rendered on GitHub or the Documentation page. Run it locally to see it.
[13]:
Visualization omitted, Javascript library not loaded!
Have you run `initjs()` in this notebook? If this notebook was from another user you must also trust this notebook (File -> Trust notebook). If you are viewing this notebook on github the Javascript has been stripped for security. If you are using JupyterLab this error is because a JupyterLab extension has not yet been written.

结论

通过表格化数据并在库之间统一API,Darts 弥合了传统回归问题与时间序列预测之间的差距。

RegressionModel 及其子类提供了广泛的功能,只需几行代码就可以构建强大的模型。

附录

回归模型

RegressionModel 将 Darts API 包装在任何 sklearn 回归模型周围。通过这个,我们可以像使用任何其他 Darts 预测模型一样使用该模型。

作为一个例子,在示例数据集上拟合贝叶斯岭回归只需要几行代码:

[14]:
model = RegressionModel(
    lags=24,
    lags_future_covariates=(48, 24),
    model=BayesianRidge(),
    output_chunk_length=24,
)

model.fit(ts_energy_train, future_covariates=ts_weather)

pred = model.predict(n=24)

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:24].plot(label="validation")
pred.plot(label="forecast")
[14]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_29_1.png

底层模型方法仍然可以访问,通过将 BasesianRidge.coef_ 属性与 RegressionModel.lagged_feature_names 属性结合,可以轻松解释回归模型的系数:

[15]:
# extract the coefficients of the first timestemp estimator
coef_values = model.model.estimators_[0].coef_
# get the lagged features name
coef_names = model.lagged_feature_names
# combine them in a dict
coefficients = {name: val for name, val in zip(coef_names, coef_values)}
# see the coefficient of the target value at last timestep before the forecasted period
{c_name: c_val for idx, (c_name, c_val) in enumerate(coefficients.items()) if idx < 5}
[15]:
{'Value_NE5_target_lag-24': -0.3195560563281926,
 'Value_NE5_target_lag-23': 0.37621876784175745,
 'Value_NE5_target_lag-22': 0.005325414282057739,
 'Value_NE5_target_lag-21': -0.11885377506043901,
 'Value_NE5_target_lag-20': 0.12892167797527437}

RegressionModel 类的一个限制是它没有提供开箱即用的概率预测,但可以通过创建一个继承自 RegressionModel_LikelihoodMixin 的新类并实现缺失的方法来实现(LinearRegressionModel 类可以用作模板)。

自定义模型

只要模型能处理表格数据并提供 fit()predict() 方法,你甚至可以实现自己的模型:

[16]:
class CustomRegressionModel:
    def __init__(self, weights: np.ndarray):
        """Barebone weighted average"""
        self.weights = weights
        self.norm_coef = sum(weights)

    def fit(self, X: np.ndarray, y: np.ndarray, *args, **kwargs):
        return self

    def predict(self, X: np.ndarray):
        """Apply weights on each sample"""
        return (
            np.stack([np.correlate(x, self.weights, mode="valid") for x in X])
            / self.norm_coef
        )

    def get_params(self, deep: bool):
        return {"weights": self.weights}


window_weights = np.arange(1, 25, 1) ** 2
model = RegressionModel(
    lags=24,
    output_chunk_length=24,
    model=CustomRegressionModel(window_weights),
    multi_models=False,
)
model.fit(ts_energy_train)
pred = model.predict(n=24)

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:24].plot(label="validation")
pred.plot(label="forecast")
[16]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_34_1.png

提升树模型的示例

确保已安装 lightgbm 和 catboost 的依赖项。您可以查看我们的安装指南 这里

[17]:
lgbm_model = LightGBMModel(lags=24, output_chunk_length=24, verbose=0)
xgboost_model = XGBModel(lags=24, output_chunk_length=24)
catboost_model = CatBoostModel(lags=24, output_chunk_length=24)

lgbm_model.fit(ts_energy_train)
xgboost_model.fit(ts_energy_train)
catboost_model.fit(ts_energy_train)

pred_lgbm = lgbm_model.predict(n=24)
pred_xgboost = xgboost_model.predict(n=24)
pred_catboost = catboost_model.predict(n=24)

print(f"LightGBMModel MAPE: {mape(ts_energy_val, pred_lgbm)}")
print(f"XGBoostModel MAPE: {mape(ts_energy_val, pred_xgboost)}")
print(f"CatboostModel MAPE: {mape(ts_energy_val, pred_catboost)}")

ts_energy_train[-48:].plot(label="training")
ts_energy_val[:24].plot(label="validation")
pred_lgbm.plot(label="lgbm")
pred_xgboost.plot(label="xgboost")
pred_catboost.plot(label="catboost")
LightGBMModel MAPE: 2.2682070257112743
XGBoostModel MAPE: 3.5364068635935895
CatboostModel MAPE: 2.373275454432286
[17]:
<Axes: xlabel='Timestamp'>
../_images/examples_20-RegressionModel-examples_36_2.png