快速入门

在本笔记本中,我们将介绍该库的主要功能

这里只展示一些最简单的“入门”示例。有关更深入的信息,请参考我们的 用户指南示例笔记本

安装 Darts

我们建议使用虚拟环境。主要有两种方式。

使用 pip

pip install darts

使用 conda

conda install -c conda-forge -c pytorch u8darts-all

如果遇到问题或想安装不同版本(避免某些依赖项),请查阅详细安装指南

首先,让我们导入一些东西

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

fix_pythonpath_if_working_locally()

import warnings

warnings.filterwarnings("ignore", category=FutureWarning)
[2]:
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

构建和操作 TimeSeries

TimeSeries 是 Darts 中的主要数据类。一个 TimeSeries 表示一个单变量或多变量时间序列,带有适当的时间索引。时间索引可以是 pandas.DatetimeIndex 类型(包含日期时间),也可以是 pandas.RangeIndex 类型(包含用于表示没有特定时间戳的顺序数据的整数)。在某些情况下,TimeSeries 甚至可以表示*概率*序列,以便获取置信区间。Darts 中的所有模型都接收 TimeSeries 并生成 TimeSeries

读取数据并构建 TimeSeries

可以使用一些工厂方法轻松构建 TimeSeries

  • DataFrame 构建,使用 TimeSeries.from_dataframe() (文档)。我们支持多种框架后端,例如 pandas.DataFramepolars.DataFramepyarrow.table 等等(见 (a))。

  • 从时间索引和对应的数值数组构建,使用 TimeSeries.from_times_and_values() (文档)。

  • 从 NumPy 数值数组构建,使用 TimeSeries.from_values() (文档)。

  • Series 构建,使用 TimeSeries.from_series() (文档)。这里我们也支持多种序列后端,例如 pandas.Seriespolars.Series 等等(见 (a))。

  • 从 Pandas DataFrame 按组提取多个 TimeSeries,使用 TimeSeries.from_group_dataframe() (文档)。

  • xarray.DataArray 构建,使用 TimeSeries.from_xarray() (文档)。

  • 从 CSV 文件构建,使用 TimeSeries.from_csv() (文档)。

  • 从 JSON 文件构建,使用 TimeSeries.from_json() (文档)。

  • 或使用我们内置的 TimeSeries 生成函数 (文档)。

(a) 我们将 narwhals 用作 DataFrame 和 Series 库之间的兼容层。请参阅 narwhals 文档 以了解所有支持的后端。

让我们从一个 DataFrame 创建一个多变量(两列)TimeSeries。该框架包含正弦和余弦序列,并具有每日频率的时间索引。

[3]:
from darts import TimeSeries
from darts.utils.utils import generate_index

# generate a sine and cosine wave
x = np.linspace(0, 2 * np.pi, 100)
sine_vals = np.sin(x)
cosine_vals = np.cos(x)

# generate a DatetimeIndex with daily frequency
dates = generate_index("2020-01-01", length=len(x), freq="D")

# create a DataFrame; here we use pandas but you can use any other backend (e.g. polars, pyarrow, ...).
df = pd.DataFrame({"sine": sine_vals, "cosine": cosine_vals, "time": dates})

series = TimeSeries.from_dataframe(df, time_col="time")
series.plot();
../_images/quickstart_00-quickstart_6_0.png

加载现有数据集

或者,您可以加载我们现有数据集之一 (文档)。

下面,我们通过直接加载航空旅客系列数据集来获取一个 TimeSeries

[4]:
from darts.datasets import AirPassengersDataset

series = AirPassengersDataset().load()
series.plot();
../_images/quickstart_00-quickstart_8_0.png

导出 TimeSeries

TimeSeries 可以通过多种不同的方式导出

  • 导出为 DataFrame,使用 TimeSeries.to_dataframe() (文档)。我们支持多种框架后端,例如 pandaspolarspyarrow 等等(见上面的 (a))。

  • 导出为 NumPy 数值数组,使用 TimeSeries.values() (文档) 或 TimeSeries.all_values() (文档)。

  • 导出为 Series,使用 TimeSeries.to_series() (文档)。这里我们也支持多种序列后端(见上面的 (a))。

  • 导出为 xarray.DataArray,使用 TimeSeries.data_array() (文档)。

  • 导出为 CSV 文件,使用 TimeSeries.to_csv() (文档)。

  • 导出为 JSON 文件,使用 TimeSeries.to_json() (文档)。

  • 还有更多!

这里是一个如何导出到 polars.DataFrame 的示例。Polars 是一个可选依赖项,因此请务必先安装它(请参阅这里的安装指南)。

[5]:
series[-5:].to_dataframe(backend="polars", time_as_index=False)
[5]:
形状:(5, 2)
月份#乘客数
datetime[ns]f64
1960-08-01 00:00:00606.0
1960-09-01 00:00:00508.0
1960-10-01 00:00:00461.0
1960-11-01 00:00:00390.0
1960-12-01 00:00:00432.0

一些 TimeSeries 操作

TimeSeries 支持多种操作 - 这里有一些示例。

分割

我们也可以在序列的某个比例、在 pandas Timestamp 或在整数索引值处进行分割。

[6]:
series1, series2 = series.split_after(0.75)
series1.plot()
series2.plot();
../_images/quickstart_00-quickstart_12_0.png

切片

[7]:
series1, series2 = series[:-36], series[-36:]
series1.plot()
series2.plot();
../_images/quickstart_00-quickstart_14_0.png

算术运算

[8]:
series_noise = TimeSeries.from_times_and_values(
    series.time_index, np.random.randn(len(series))
)
(series / 2 + 20 * series_noise - 10).plot();
../_images/quickstart_00-quickstart_16_0.png

堆叠

连接新维度以生成一个新的单一多变量序列。

[9]:
(series / 50).stack(series_noise).plot();
../_images/quickstart_00-quickstart_18_0.png

映射

[10]:
series.map(np.log).plot();
../_images/quickstart_00-quickstart_20_0.png

在时间戳和值上进行映射

[11]:
series.map(lambda ts, x: x / ts.days_in_month).plot();
../_images/quickstart_00-quickstart_22_0.png

添加日期时间属性作为额外维度(产生一个多变量序列)

[12]:
(series / 20).add_datetime_attribute("month").plot();
../_images/quickstart_00-quickstart_24_0.png

添加二元假期分量

[13]:
(series / 200).add_holidays("US").plot();
../_images/quickstart_00-quickstart_26_0.png

差分

[14]:
series.diff().plot();
../_images/quickstart_00-quickstart_28_0.png

填充缺失值(使用 ``utils`` 函数)。

缺失值由 np.nan 表示。

[15]:
from darts.utils.missing_values import fill_missing_values

values = np.arange(50, step=0.5)
values[10:30] = np.nan
values[60:95] = np.nan
series_ = TimeSeries.from_values(values)

(series_ - 10).plot(label="with missing values (shifted below)")
fill_missing_values(series_).plot(label="without missing values");
../_images/quickstart_00-quickstart_30_0.png

创建训练和验证序列

接下来,我们将把 TimeSeries 分割成训练序列和验证序列。注意:一般来说,将测试序列放在一边,直到流程结束之前都不动它也是一个好习惯。这里,为简单起见,我们只构建训练和验证序列。

训练序列将是一个包含直到 1958 年 1 月(不含)的值的 TimeSeries,而验证序列将是一个包含其余部分的 TimeSeries

[16]:
train, val = series.split_before(pd.Timestamp("19580101"))
train.plot(label="training")
val.plot(label="validation");
../_images/quickstart_00-quickstart_32_0.png

训练预测模型和进行预测

使用玩具模型

Darts 中有一系列“朴素”基准模型,它们对于了解可以预期的最低准确度非常有用。例如,NaiveSeasonal(K) 模型总是“重复”发生在前 K 个时间步的值。

在其最朴素的形式下,当 K=1 时,该模型只是简单地重复训练序列的最后一个值。

[17]:
from darts.models import NaiveSeasonal

naive_model = NaiveSeasonal(K=1)
naive_model.fit(train)
naive_forecast = naive_model.predict(36)

series.plot(label="actual")
naive_forecast.plot(label="naive forecast (K=1)");
../_images/quickstart_00-quickstart_34_0.png

TimeSeries 上拟合模型并生成预测非常容易。所有模型都有一个 fit() 和一个 predict() 函数。这类似于 Scikit-learn,但它是时间序列特有的。fit() 函数接受用于拟合模型的训练时间序列作为参数,而 predict() 函数接受(训练序列结束后的)预测时间步数作为参数。

检查季节性

我们上面这个模型可能有点太朴素了。我们可以通过利用数据的季节性来改进。数据显然具有年度季节性,我们可以通过查看自相关函数 (ACF) 并突出显示滞后 m=12 来证实这一点。

[18]:
from darts.utils.statistics import check_seasonality, plot_acf

plot_acf(train, m=12, alpha=0.05, max_lag=24)
../_images/quickstart_00-quickstart_36_0.png

ACF 在 x = 12 处呈现一个峰值,这表明存在年度季节性趋势(红色突出显示)。蓝色区域确定了置信水平为 \(\alpha = 5\%\) 时统计量的显著性。我们还可以对每个候选周期 m 进行季节性统计检验。

[19]:
for m in range(2, 25):
    is_seasonal, period = check_seasonality(train, m=m, alpha=0.05)
    if is_seasonal:
        print(f"There is seasonality of order {period}.")
There is seasonality of order 12.

一个不那么朴素的模型

让我们再次尝试使用季节性为 12 的 NaiveSeasonal 模型。

[20]:
seasonal_model = NaiveSeasonal(K=12)
seasonal_model.fit(train)
seasonal_forecast = seasonal_model.predict(36)

series.plot(label="actual")
seasonal_forecast.plot(label="naive forecast (K=12)");
../_images/quickstart_00-quickstart_40_0.png

这好一些了,但我们仍然忽略了趋势。幸运的是,还有另一种捕捉趋势的朴素基准模型,称为 NaiveDrift。该模型只是简单地生成线性预测,斜率由训练集的第一个和最后一个值确定。

[21]:
from darts.models import NaiveDrift

drift_model = NaiveDrift()
drift_model.fit(train)
drift_forecast = drift_model.predict(36)

combined_forecast = drift_forecast + seasonal_forecast - train.last_value()

series.plot()
combined_forecast.plot(label="combined")
drift_forecast.plot(label="drift");
../_images/quickstart_00-quickstart_42_0.png

这里发生了什么?我们只是简单地拟合了一个朴素漂移模型,并将其预测添加到我们之前的季节性预测中。我们还从结果中减去训练集的最后一个值,以便最终组合的预测以正确的偏移量开始。

计算误差指标

这看起来已经是一个相当不错的预测了,而且我们还没有使用任何非朴素模型。事实上 - 任何模型都应该能够超越它。

那么我们需要超越的误差是多少?我们将使用 平均绝对百分比误差 (MAPE)(请注意,在实践中通常有充分理由使用 MAPE - 我们在这里使用它因为它非常方便且不受量级影响)。在 Darts 中,它只是一个简单的函数调用。

[22]:
from darts.metrics import mape

print(
    f"Mean absolute percentage error for the combined naive drift + seasonal: {mape(series, combined_forecast):.2f}%."
)
Mean absolute percentage error for the combined naive drift + seasonal: 5.66%.

darts.metrics 包含更多用于比较时间序列的指标。当两个序列未对齐时,这些指标将仅比较序列的公共切片,并在大量序列对上并行计算 - 但我们不要操之过急。

快速尝试多种模型

Darts 的构建旨在方便以统一的方式训练和验证多种模型。让我们再训练几个模型,并在验证集上计算它们各自的 MAPE。

[23]:
from darts.models import AutoARIMA, ExponentialSmoothing, Theta


def eval_model(model):
    model.fit(train)
    forecast = model.predict(len(val))
    print(f"model {model} obtains MAPE: {mape(val, forecast):.2f}%")


eval_model(ExponentialSmoothing())
eval_model(AutoARIMA())
eval_model(Theta())
model ExponentialSmoothing() obtains MAPE: 5.11%
model AutoARIMA() obtains MAPE: 12.70%
model Theta() obtains MAPE: 8.15%

在这里,我们仅使用默认参数创建了这些模型。如果我们针对问题进行微调,可能会做得更好。让我们试试 Theta 方法。

使用 Theta 方法搜索超参数

模型 Theta 包含了 Assimakopoulos 和 Nikolopoulos 的 Theta 方法的实现。该方法取得了一些成功,特别是在 M3 竞赛中。

虽然 Theta 参数的值在应用中通常设置为 0,但我们的实现支持一个可变值用于参数调优目的。让我们尝试找到一个适合 Theta 的值。

[24]:
# Search for the best theta parameter, by trying 50 different values
thetas = 2 - np.linspace(-10, 10, 50)

best_mape = float("inf")
best_theta = 0

for theta in thetas:
    model = Theta(theta)
    model.fit(train)
    pred_theta = model.predict(len(val))
    res = mape(val, pred_theta)

    if res < best_mape:
        best_mape = res
        best_theta = theta
[25]:
best_theta_model = Theta(best_theta)
best_theta_model.fit(train)
pred_best_theta = best_theta_model.predict(len(val))

print(f"Lowest MAPE is: {mape(val, pred_best_theta):.2f}, with theta = {best_theta}.")
Lowest MAPE is: 4.40, with theta = -3.5102040816326543.
[26]:
train.plot(label="train")
val.plot(label="true")
pred_best_theta.plot(label="prediction");
../_images/quickstart_00-quickstart_53_0.png

我们可以观察到,使用 best_theta 的模型是迄今为止我们拥有的最好的模型,就 MAPE 而言。

回测:模拟历史预测

所以此时我们有一个在验证集上表现良好的模型,这很好。但是,如果我们在历史上使用过这个模型,我们又如何知道我们将获得的性能呢?

回测模拟了历史上使用给定模型本应获得的预测。它可能需要一些时间来生成,因为(默认情况下)每当模拟预测时间向前推进时,模型都会重新训练。

这种模拟预测总是相对于一个预测范围来定义的,预测范围是将预测时间与预测未来时间点分隔开的时间步数。在下面的示例中,我们模拟了未来 3 个月(相对于预测时间)的预测。调用 historical_forecasts() 的结果(默认情况下)是一个 TimeSeries,它仅包含每个提前 3 个月的预测的最后一个预测值。

[27]:
hfc_params = {
    "series": series,
    "start": pd.Timestamp(
        "1956-01-01"
    ),  # can also be a float for the fraction of the series to start at
    "forecast_horizon": 3,
    "verbose": True,
}
historical_fcast_theta = best_theta_model.historical_forecasts(
    last_points_only=True, **hfc_params
)

series.plot(label="data")
historical_fcast_theta.plot(label="backtest 3-months ahead forecast (Theta)")
print(f"MAPE = {mape(series, historical_fcast_theta):.2f}%")
MAPE = 7.99%
../_images/quickstart_00-quickstart_56_2.png

我们还可以通过设置 last_points_only=False 来检索每个历史预测的所有预测值。通过 stride 参数,我们定义了两个连续预测之间移动的步数。我们将其设置为 3 个月,这样我们就可以将预测连接成一个单一的 TimeSeries

[28]:
historical_fcast_theta_all = best_theta_model.historical_forecasts(
    last_points_only=False, stride=3, **hfc_params
)

series.plot(label="data")
for idx, hfc in enumerate(historical_fcast_theta_all):
    hfc.plot(label=f"forecast {idx}")

from darts import concatenate

historical_fcast_theta_all = concatenate(historical_fcast_theta_all, axis=0)
print(f"MAPE = {mape(series, historical_fcast_theta_all):.2f}%")
MAPE = 5.61%
../_images/quickstart_00-quickstart_58_2.png

所以看来我们在验证集上表现最好的模型在回测时不再那么出色了(我是不是听到了过拟合 :D)

为了更仔细地查看误差,我们还可以使用 backtest() 方法来获取我们的模型本应获得的所有原始误差(例如,MAPE 误差)。

[29]:
best_theta_model = Theta(best_theta)

raw_errors = best_theta_model.backtest(
    metric=mape, reduction=None, last_points_only=False, stride=1, **hfc_params
)

from darts.utils.statistics import plot_hist

plot_hist(
    raw_errors,
    bins=np.arange(0, max(raw_errors), 1),
    title="Individual backtest error scores (histogram)",
);
../_images/quickstart_00-quickstart_60_1.png

最后,使用 backtest() 我们还可以更简单地查看历史预测的平均误差。

[30]:
average_error = best_theta_model.backtest(
    metric=mape,
    reduction=np.mean,  # this is actually the default
    **hfc_params,
)

print(f"Average error (MAPE) over all historical forecasts: {average_error:.2f}")
Average error (MAPE) over all historical forecasts: 6.30

例如,我们也可以指定参数 reduction=np.median 来获取中位数 MAPE。

上面,每次我们调用 backtest() 时,它都会重新计算历史预测。我们也可以使用一些预先计算好的预测来更快地获得结果!

[31]:
hfc_precomputed = best_theta_model.historical_forecasts(
    last_points_only=False, stride=1, **hfc_params
)
new_error = best_theta_model.backtest(
    historical_forecasts=hfc_precomputed, last_points_only=False, stride=1, **hfc_params
)

print(f"Average error (MAPE) over all historical forecasts: {new_error:.2f}")
Average error (MAPE) over all historical forecasts: 6.30

让我们看看我们当前 Theta 模型的拟合值残差,即通过在所有先前点上拟合模型获得的每个时间点的 1 步预测与实际观测值之间的差异。

[32]:
from darts.utils.statistics import plot_residuals_analysis

plot_residuals_analysis(best_theta_model.residuals(series))
../_images/quickstart_00-quickstart_66_0.png

我们可以看到分布不以 0 为中心,这意味着我们的 Theta 模型存在偏差。我们还可以看到滞后等于 12 的较大 ACF 值,这表明残差包含模型未使用的信息。

我们的 residuals 方法实际上更强大!它可用于计算 Darts 中的任何按时间步指标(请参阅此处的列表),即使是多步预测。它还支持类似于回测的预先计算的历史预测。

现在让我们检查一下 3 个月预测中每个步骤获得的绝对误差分布。

[33]:
from darts.metrics import ae

residuals = best_theta_model.residuals(
    historical_forecasts=hfc_precomputed,
    metric=ae,  # the absolute error per time step
    last_points_only=False,
    values_only=True,  # return a list of numpy arrays
    **hfc_params,
)
residuals = np.concatenate(residuals, axis=1)[:, :, 0]

fig, ax = plt.subplots()
for forecast_step in range(len(residuals)):
    ax.hist(residuals[forecast_step], label=f"step {forecast_step}", alpha=0.5)
ax.legend()
ax.set_title("Absolute errors per forecast step");
../_images/quickstart_00-quickstart_68_0.png

我们可以清楚地看到,预测未来越远,误差就越大。

使用简单的 ExponentialSmoothing 模型会不会更好呢?

[34]:
model_es = ExponentialSmoothing(seasonal_periods=12)
historical_fcast_es = model_es.historical_forecasts(**hfc_params)

series.plot(label="data")
historical_fcast_es.plot(label="backtest 3-months ahead forecast (Exp. Smoothing)")
print(f"MAPE = {mape(historical_fcast_es, series):.2f}%")
MAPE = 4.42%
../_images/quickstart_00-quickstart_71_2.png

这好多了!在这种情况下,当回测时,使用 3 个月的预测范围,我们得到的平均绝对百分比误差约为 4-5%。

[35]:
plot_residuals_analysis(model_es.residuals(series, verbose=True))
../_images/quickstart_00-quickstart_73_1.png

残差分析也反映了性能的改进,因为我们现在残差的分布以 0 为中心,并且 ACF 值虽然并非不显著,但幅度较低。

机器学习和全局模型

Darts 对机器学习和深度学习预测模型提供了丰富的支持;例如

  • RegressionModel 可以包装任何 sklearn 兼容的回归模型来生成预测(它有自己的下面一节)。

  • RNNModel 是一个灵活的 RNN 实现,可以像 DeepAR 一样使用。

  • NBEATSModel 实现了 N-BEATS 模型。

  • TFTModel 实现了 Temporal Fusion Transformer 模型。

  • TCNModel 实现了时间卷积网络。

除了支持与其他模型相同的基本 fit()/predict() 接口外,这些模型还是*全局模型*,因为它们支持在多个时间序列上进行训练(有时称为*元学习*)。

这是使用基于 ML 的模型进行预测的一个关键点:通常情况下,ML 模型(特别是深度学习模型)需要在大量数据上进行训练,这通常意味着大量独立但相关的时序数据。

在 Darts 中,指定多个 TimeSeries 的基本方法是使用 TimeSeriesSequence(例如,一个简单的 TimeSeries 列表)。

一个包含两个序列的玩具示例

这些模型可以在数千个序列上进行训练。此处为说明目的,我们将加载两个不同的序列 - 航空旅客计数序列和每头奶牛每月产奶磅数的另一个序列。我们还将序列转换为 np.float32,因为这将略微加快训练速度。

[36]:
from darts.datasets import AirPassengersDataset, MonthlyMilkDataset

series_air = AirPassengersDataset().load().astype(np.float32)
series_milk = MonthlyMilkDataset().load().astype(np.float32)

# set aside last 36 months of each series as validation set:
train_air, val_air = series_air[:-36], series_air[-36:]
train_milk, val_milk = series_milk[:-36], series_milk[-36:]

train_air.plot()
val_air.plot()
train_milk.plot()
val_milk.plot();
../_images/quickstart_00-quickstart_76_0.png

首先,让我们将这两个序列缩放到 0 到 1 之间,因为这对大多数 ML 模型都有益。我们将为此使用一个 Scaler

[37]:
from darts.dataprocessing.transformers import Scaler

scaler = Scaler()
train_air_scaled, train_milk_scaled = scaler.fit_transform([train_air, train_milk])

train_air_scaled.plot()
train_milk_scaled.plot();
../_images/quickstart_00-quickstart_78_0.png

注意我们可以一次性缩放多个序列。我们还可以通过指定 n_jobs 参数,在多个处理器上并行化此类操作。

使用深度学习:以 N-BEATS 为例

注意:您可以在此处找到我们神经网络模型 (TorchForecastingModels) 的详细用户指南。

接下来,我们将构建一个 N-BEATS 模型。该模型可以通过许多超参数进行调优(例如堆栈数量、层数等)。为简单起见,我们将使用默认超参数。我们必须提供的仅有的两个超参数是:

  • input_chunk_length:这是模型的“回溯窗口” - 即神经网络在向前传递中接收多少时间步的历史作为输入以产生其输出。

  • output_chunk_length:这是模型的“向前窗口” - 即神经网络在向前传递中输出多少时间步的未来值。

random_state 参数仅用于获取可复现的结果。

Darts 中的大多数神经网络需要这两个参数。这里,我们将使用季节性的倍数。现在我们可以准备在我们的两个序列上拟合模型了(通过将包含这两个序列的列表传递给 fit())。

[38]:
from darts.models import NBEATSModel

model = NBEATSModel(input_chunk_length=24, output_chunk_length=12, random_state=42)

model.fit([train_air_scaled, train_milk_scaled], epochs=50, verbose=True);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

  | Name            | Type             | Params | Mode
-------------------------------------------------------------
0 | criterion       | MSELoss          | 0      | train
1 | train_criterion | MSELoss          | 0      | train
2 | val_criterion   | MSELoss          | 0      | train
3 | train_metrics   | MetricCollection | 0      | train
4 | val_metrics     | MetricCollection | 0      | train
5 | stacks          | ModuleList       | 6.2 M  | train
-------------------------------------------------------------
6.2 M     Trainable params
1.4 K     Non-trainable params
6.2 M     Total params
24.787    Total estimated model params size (MB)
396       Modules in train mode
0         Modules in eval mode
`Trainer.fit` stopped: `max_epochs=50` reached.

现在让我们为我们的两个序列获取未来 36 个月的预测。我们可以直接使用 predict() 函数的 series 参数来告诉模型要预测哪个序列。重要的是,output_chunk_length 并不会直接限制 predict() 的预测范围 n。这里,我们用 output_chunk_length=12 训练模型,并为未来 36 个月生成预测;这只是在幕后以自回归的方式完成的(网络递归地消费其先前的输出)。

[39]:
pred_air, pred_milk = model.predict(series=[train_air_scaled, train_milk_scaled], n=36)

# scale back:
pred_air, pred_milk = scaler.inverse_transform([pred_air, pred_milk])

plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)");
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
../_images/quickstart_00-quickstart_83_2.png

考虑到我们使用一个模型及其默认超参数来同时捕捉航空旅客和牛奶产量,我们的预测实际上还不错!

该模型在捕捉年度季节性方面看起来相当不错,但未能捕捉到航空序列的趋势。在下一节中,我们将尝试使用外部数据(协变量)来解决这个问题。

协变量:使用外部数据

除了目标序列(我们感兴趣预测的序列)之外,Darts 中的许多模型还接受协变量序列作为输入。协变量是我们不想预测的序列,但可以为模型提供有用的额外信息。目标和协变量都可以是多变量或单变量的。

Darts 中有两种类型的协变量时间序列

  • past_covariates 是不一定在预测时间之前已知的数据系列。例如,它们可以代表必须测量且事先未知的事物。模型在进行预测时不会使用 past_covariates 的未来值(仅适用于使用 n > output_chunk_length 进行预测时的全局模型,由于自回归)。有关过去/未来协变量的更多信息,请查看本用户指南

  • future_covariates 是在预测范围之前已知的数据系列。它们可以代表日历信息、假期、天气预报等。接受 future_covariates 的模型在进行预测时将查看未来值(直到预测范围)。

  • static_covariates 是目标序列随时间不变的特征。它们直接嵌入到目标序列中。它们可以代表产品类别、国家信息等。有关静态协变量的更多信息,请查看本用户指南

covariates

每个协变量都可能是多变量的。如果您有多个协变量系列(例如月份和年份值),您应该 stack()concatenate() 它们以获得一个多变量序列。

您提供的协变量可能比实际需要的长。Darts 的模型会自动为您处理相关时间帧的提取! 但是,如果您的协变量没有足够的时间跨度,您将收到错误。

并非所有模型都支持每种协变量类型。您可以在此处找到模型列表,其中说明了它们支持的类型。

现在让我们构建一些外部协变量,其中包含航空和牛奶系列的月度和年度值。在下面的单元格中,我们使用 darts.utils.timeseries_generation.datetime_attribute_timeseries() 函数生成包含月份和年份值的序列,然后我们将这些序列沿 "component" 轴(与 axis=1 相同)进行 concatenate(),以便为每个目标序列获得一个包含两个分量(月份和年份)的协变量序列。为简单起见,我们直接将月份和年份值缩放到 0 到 1 之间。

[40]:
from darts import concatenate
from darts.utils.timeseries_generation import datetime_attribute_timeseries as dt_attr

air_covs = concatenate(
    [
        dt_attr(series_air, "month", dtype=np.float32),
        dt_attr(series_air, "year", dtype=np.float32),
    ],
    axis="component",
)

milk_covs = concatenate(
    [
        dt_attr(series_milk, "month", dtype=np.float32),
        dt_attr(series_milk, "year", dtype=np.float32),
    ],
    axis="component",
)

air_covs_scaled, milk_covs_scaled = Scaler().fit_transform([air_covs, milk_covs])
air_covs_scaled.plot()
milk_covs_scaled.plot()
plt.title(
    "one multivariate time series of 2 dimensions, containing covariates for the air series:"
);
../_images/quickstart_00-quickstart_86_0.png

NBEATSModel 仅支持 past_covariates。因此,尽管我们的协变量表示日历信息并且是提前已知的,我们仍将在 N-BEATS 中将它们用作 past_covariates。要进行训练,我们只需将它们作为 past_covariates 传递给 fit() 函数,顺序与目标相同。

[41]:
model = NBEATSModel(input_chunk_length=24, output_chunk_length=12, random_state=42)

model.fit(
    [train_air_scaled, train_milk_scaled],
    past_covariates=[air_covs_scaled, milk_covs_scaled],
    epochs=50,
    verbose=True,
);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

  | Name            | Type             | Params | Mode
-------------------------------------------------------------
0 | criterion       | MSELoss          | 0      | train
1 | train_criterion | MSELoss          | 0      | train
2 | val_criterion   | MSELoss          | 0      | train
3 | train_metrics   | MetricCollection | 0      | train
4 | val_metrics     | MetricCollection | 0      | train
5 | stacks          | ModuleList       | 6.6 M  | train
-------------------------------------------------------------
6.6 M     Trainable params
1.7 K     Non-trainable params
6.6 M     Total params
26.314    Total estimated model params size (MB)
396       Modules in train mode
0         Modules in eval mode
`Trainer.fit` stopped: `max_epochs=50` reached.

然后要生成预测,我们还需要将我们的协变量作为 past_covariates 提供给 predict() 函数。

[42]:
preds = model.predict(
    series=[train_air_scaled, train_milk_scaled],
    past_covariates=[air_covs_scaled, milk_covs_scaled],
    n=36,
)

# scale back:
pred_air, pred_milk = scaler.inverse_transform(preds)

plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)");
`predict()` was called with `n > output_chunk_length`: using auto-regression to forecast the values after `output_chunk_length` points. The model will access `(n - output_chunk_length)` future values of your `past_covariates` (relative to the first predicted time step). To hide this warning, set `show_warnings=False`.
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
../_images/quickstart_00-quickstart_90_2.png

现在看来模型更好地捕捉了航空序列的趋势(这也稍微扰动了牛奶序列的预测)。

模型警告非常重要。它告诉我们,由于我们选择的预测范围 n > output_chunk_length 激活了自回归,模型使用了我们 past_covariates 的未来值进行预测。然后模型将使用自身的预测作为下一次预测的输入。由于预测点向前移动,模型需要过去协变量的新值。然而,这些值只从相对于预测点的过去中提取(绝不会从预测范围本身提取)!

在我们的案例中,这样做是可以的,因为我们只使用了我们事先知道的日历信息。如果改用我们只在过去知道的一些特征,那么我们应该只使用 n <= output_chunk_length 进行预测。

编码器:免费使用协变量

使用与日历或时间轴相关的协变量(例如我们上面例子中的月份和年份)非常常见,因此所有支持协变量的 Darts 模型都内置了开箱即用的生成它们的功能。

为了轻松地将这些协变量集成到您的模型中,您只需在模型创建时指定 add_encoders 参数。此参数必须是一个字典,包含应作为额外协变量进行编码的信息。对于支持过去和未来协变量的模型,这样一个字典的示例如下所示:

[43]:
def extract_year(idx):
    """Extract the year each time index entry and normalized it."""
    return (idx.year - 1950) / 50


encoders = {
    "cyclic": {"future": ["month"]},
    "datetime_attribute": {"future": ["hour", "dayofweek"]},
    "position": {"past": ["absolute"], "future": ["relative"]},
    "custom": {"past": [extract_year]},
    "transformer": Scaler(),
}

在上面的字典中,指定了以下内容:

  • 月份应作为未来协变量,采用周期性(sin/cos)编码。

  • 小时和星期几应作为未来协变量使用。

  • 绝对位置(序列中的时间步)应作为过去协变量使用。

  • 相对位置(相对于预测时间)应作为未来协变量使用。

  • 应将年份的附加自定义函数用作过去协变量。

  • 所有上述协变量都应使用 Scaler 进行缩放,Scaler 将在调用模型 fit() 函数时拟合,然后用于转换协变量。

有关如何使用编码器的更多信息,请参阅API 文档。请注意,lambda 函数不能使用,因为它们不可被序列化 (pickable)。

为了重现我们在 N-BEATS 中将月份和年份用作过去协变量的示例,我们可以使用一些编码器,如下所示:

[44]:
encoders = {"datetime_attribute": {"past": ["month", "year"]}, "transformer": Scaler()}

现在,使用这些协变量训练 N-BEATS 模型的完整过程如下所示:

[45]:
model = NBEATSModel(
    input_chunk_length=24,
    output_chunk_length=12,
    add_encoders=encoders,
    random_state=42,
)

model.fit([train_air_scaled, train_milk_scaled], epochs=50, verbose=True);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

  | Name            | Type             | Params | Mode
-------------------------------------------------------------
0 | criterion       | MSELoss          | 0      | train
1 | train_criterion | MSELoss          | 0      | train
2 | val_criterion   | MSELoss          | 0      | train
3 | train_metrics   | MetricCollection | 0      | train
4 | val_metrics     | MetricCollection | 0      | train
5 | stacks          | ModuleList       | 6.6 M  | train
-------------------------------------------------------------
6.6 M     Trainable params
1.7 K     Non-trainable params
6.6 M     Total params
26.314    Total estimated model params size (MB)
396       Modules in train mode
0         Modules in eval mode
`Trainer.fit` stopped: `max_epochs=50` reached.

并获取航空旅客序列的一些预测

[46]:
preds = model.predict(
    series=[train_air_scaled, train_milk_scaled], n=36, show_warnings=False
)

# scale back:
pred_air, pred_milk = scaler.inverse_transform(preds)

plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)");
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
../_images/quickstart_00-quickstart_99_2.png

回归预测模型

注意:您可以在此处找到我们的 RegressionModel 的详细示例笔记本。

RegressionModels 是围绕 sklearn 兼容回归模型进行包装的预测模型。内部回归模型用于预测目标序列的未来值,作为目标、过去和未来协变量特定滞后期的函数。在幕后,时间序列被表格化以构建正确格式的训练数据集。

默认情况下,RegressionModel 将执行线性回归。通过指定 model 参数,使用任何所需的 sklearn 兼容回归模型都非常容易,但为了方便起见,Darts 也提供了一些开箱即用的现成模型:

例如,这就是对我们的玩具双序列问题拟合贝叶斯岭回归的样子:

[47]:
from sklearn.linear_model import BayesianRidge

from darts.models import RegressionModel

model = RegressionModel(lags=72, lags_future_covariates=[-6, 0], model=BayesianRidge())

model.fit(
    [train_air_scaled, train_milk_scaled],
    future_covariates=[air_covs_scaled, milk_covs_scaled],
);

上面发生了几件事:

  • lags=72 告诉 RegressionModel 查看目标过去 72 个滞后/月的数据。

  • 此外,lags_future_covariates=[-6, 0] 意味着模型还将查看我们提供的 future_covariates 的滞后。这里我们指定模型需要考虑的确切滞后;“-6th” 和 “0th” 滞后。“0th” 滞后是指“当前”滞后(即,在预测的时间步);显然,了解此滞后需要提前知道数据(因此我们使用了 future_covariates)。类似地,-6 意味着我们还查看预测时间步之前 6 个月的协变量值(如果预测范围超过 6 步,这也需要提前知道协变量)。

  • model=BayesianRidge() 提供了实际的内部回归模型。

现在让我们获取一些预测

[48]:
preds = model.predict(
    series=[train_air_scaled, train_milk_scaled],
    future_covariates=[air_covs_scaled, milk_covs_scaled],
    n=36,
)

# scale back:
pred_air, pred_milk = scaler.inverse_transform(preds)

plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)");
../_images/quickstart_00-quickstart_103_0.png

请注意上面我们如何一次性获得了两个时间序列的预测。类似地,我们也可以获取序列集合的一些指标。

[49]:
mape([series_air, series_milk], [pred_air, pred_milk])
[49]:
[3.435389, 5.129032]

或者所有序列的平均指标

[50]:
mape([series_air, series_milk], [pred_air, pred_milk], series_reduction=np.mean)
[50]:
4.2822104

看来这个模型在航空交通序列上表现良好,当我们仅回测这个序列时,它的表现如何呢?

[51]:
bayes_ridge_model = RegressionModel(
    lags=72, lags_future_covariates=[0], model=BayesianRidge()
)

backtest = bayes_ridge_model.historical_forecasts(
    future_covariates=[air_covs_scaled, milk_covs_scaled],
    **hfc_params,
)

print(f"MAPE = {mape(series_air, backtest):.2f}%")
series_air.plot()
backtest.plot();
`enable_optimization=True` is ignored because `retrain` is not `False` or `0`. To hide this warning, set `show_warnings=False` or `enable_optimization=False`.
`enable_optimization=True` is ignored because `forecast_horizon > model.output_chunk_length`. To hide this warning, set `show_warnings=False` or `enable_optimization=False`.
MAPE = 3.76%
../_images/quickstart_00-quickstart_109_3.png

迄今为止我们最好的模型!

为了避免通过目标和未来协变量缩放器造成数据泄露,我们将将其传递给 historical_forecasts,以便在每次迭代中将缩放器拟合并应用于目标和未来协变量。

[52]:
backtest_auto_scaling = bayes_ridge_model.historical_forecasts(
    future_covariates=air_covs,
    data_transformers={"series": Scaler(), "future_covariates": Scaler()},
    retrain=True,  # ensure that the scalers are fitted at each iterations
    **hfc_params,
)

print(f"MAPE = {mape(series_air, backtest_auto_scaling):.2f}%")
series_air.plot()
backtest_auto_scaling.plot();
`enable_optimization=True` is ignored because `retrain` is not `False` or `0`. To hide this warning, set `show_warnings=False` or `enable_optimization=False`.
`enable_optimization=True` is ignored because `forecast_horizon > model.output_chunk_length`. To hide this warning, set `show_warnings=False` or `enable_optimization=False`.
MAPE = 3.92%
../_images/quickstart_00-quickstart_111_3.png

MAPE 分数略有下降,这是预期的,因为信息无法再从未来泄露,并且回测条件更可靠。

样本权重

我们所有的全局模型(回归、集成和神经网络模型)都支持训练样本权重。它们按观测、标签(output_chunk_length 中的每个步骤)和分量应用。这对于以下情况非常有用:

  • 对某些时间段赋予更高的权重(例如,过去点越远权重衰减,具有更高商业价值的时间赋予更高权重,…)

  • 减少/消除异常值或缺失值对模型的影响

让我们给航空旅客系列引入一些异常值,看看具有 12 个月回溯窗口(lags=12)的线性回归模型在该数据上的表现如何。

[53]:
# convert to pandas DataFrame and add some outliers
air_df = series_air.to_dataframe()
outlier_mask = (air_df.index.year >= 1950) & (air_df.index.year <= 1951)
air_df.loc[outlier_mask] = 600.0
air_outlier = TimeSeries.from_dataframe(air_df)

from darts.models import LinearRegressionModel

model_lin = LinearRegressionModel(lags=12, lags_future_covariates=[0])
hist_fc_kwargs = {
    "series": air_outlier,
    "future_covariates": air_covs_scaled,
    "start": 0.6,
    "forecast_horizon": 3,
    "verbose": True,
    "show_warnings": False,
}
backtest = model_lin.historical_forecasts(**hist_fc_kwargs)

print(f"MAPE = {mape(air_outlier, backtest):.2f}%")
air_outlier.plot(label="air series with outliers")
backtest.plot(label="non-weighted predictions");
MAPE = 26.40%
../_images/quickstart_00-quickstart_114_2.png

这看起来不太好。如果能以某种方式忽略异常值就好了。这就是样本权重的作用!

在 Darts 中,样本权重被视为协变量 - 它们本身被定义为 TimeSeries。因此,我们的模型可以自动为我们提取相关的时间帧!

让我们创建一个权重序列,其中我们将所有异常值的时间设置为 0.,其余时间设置为 1.。我们还将最后一个异常值之后的 12 个月设置为 0.,因为这些时间落在我们模型的回溯窗口内。否则,模型输入中仍然会有一些异常值。

[54]:
weight_mask = (air_df.index.year >= 1950) & (air_df.index.year <= 1952)
sample_weight = np.ones((len(air_df), 1))
sample_weight[weight_mask, 0] = 0.0
sample_weight = air_outlier.with_values(sample_weight)

# and plot the results
air_outlier.plot(label="air series with outliers")
(sample_weight * 300).plot(label="sample weight * 300");
../_images/quickstart_00-quickstart_116_0.png

现在我们可以使用 fit()historical_forecastsbacktest() 等方法,使用样本权重训练模型…

[55]:
model_lin = LinearRegressionModel(lags=12, lags_future_covariates=[0])
backtest_weighted = model_lin.historical_forecasts(
    sample_weight=sample_weight, **hist_fc_kwargs
)

print(f"MAPE = {mape(series_air, backtest_weighted):.2f}%")
air_outlier.plot(label="air series with outliers")
backtest_weighted.plot(label="weighted predictions");
MAPE = 5.19%
../_images/quickstart_00-quickstart_118_2.png

哇,这看起来很棒!我们成功地完全消除了异常值对我们模型的负面影响!

注意:我们的样本权重还支持多步预测、多个时间序列、多变量序列以及评估集的权重(如果模型支持的话)

预测开始偏移

我们可能还对目标序列结束后带偏移量的预测感兴趣。例如,这可能很有用:

  • 在日前市场(或日内市场)中,(每天)我们需要为一个未来的时间点(第二天)进行一些报价 - 我们并不真正关心从现在到那个未来时间点之间发生的事情。通过仅关注感兴趣的时间点,我们可以降低模型复杂度。

  • 当协变量(或目标序列)延迟报告时

我们所有的全局模型都支持通过模型创建参数 output_chunk_shift 进行带偏移量的预测 - 这是将第一个预测步骤向前偏移的步数。

有了输出偏移,模型就无法再进行自回归了。所以让我们创建一个线性模型,直接预测未来 12 个月,偏移量为 12 个月。

[56]:
model_shifted = LinearRegressionModel(
    lags=12,
    lags_future_covariates=(0, 12),
    output_chunk_length=12,
    output_chunk_shift=12,
)

model_shifted.fit(series_air[:-24], future_covariates=air_covs)
preds = model_shifted.predict(n=12)

series_air[:-24].plot(label="train series")
series_air[-24:].plot(label="val_series")
preds.plot(label="shifted prediction");
../_images/quickstart_00-quickstart_121_0.png

我们看到预测从训练序列结束后的 12 个月开始。

概率预测

Darts 中的大多数模型支持概率预测(一些局部模型以及所有回归、集成和神经网络模型)。完整的支持列表可在Darts README 页面上找到

使用局部模型

对于局部模型(ARIMAExponentialSmoothing 等),我们只需在调用 predict() 时指定 num_samples 参数。返回的 TimeSeries 将包含描述时间序列值分布的 num_samples 个蒙特卡罗样本。依赖蒙特卡罗样本(与显式置信区间不同)的优势在于,它们可以用于描述任何参数或非参数联合分布,并计算任意分位数。

[57]:
model_es = ExponentialSmoothing()
model_es.fit(train)
probabilistic_forecast = model_es.predict(len(val), num_samples=500)

series.plot(label="actual")
probabilistic_forecast.plot(label="probabilistic forecast")
plt.legend()
plt.show()
../_images/quickstart_00-quickstart_124_0.png

使用神经网络模型 (TorchForecastingModel)

使用神经网络时,我们必须在创建模型时使用 Likelihood 对象。似然函数指定模型将尝试拟合的分布,以及分布参数的潜在先验值。可用似然函数的完整列表可在文档中找到

除了分布之外,我们还支持将 QuantileRegression 作为似然函数,它估计目标序列的未来分位数。

使用似然函数很简单。例如,训练一个 TCNModel 以拟合拉普拉斯似然函数的过程如下所示:

[58]:
from darts.models import TCNModel
from darts.utils.likelihood_models.torch import LaplaceLikelihood

model = TCNModel(
    input_chunk_length=24,
    output_chunk_length=12,
    random_state=42,
    likelihood=LaplaceLikelihood(),
    pl_trainer_kwargs={"accelerator": "cpu"},
)

model.fit(train_air_scaled, epochs=400, verbose=True);
GPU available: True (mps), used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
/Users/dennisbader/miniconda3/envs/darts/lib/python3.10/site-packages/pytorch_lightning/trainer/setup.py:177: GPU available but not used. You can set it by doing `Trainer(accelerator='gpu')`.

  | Name            | Type             | Params | Mode
-------------------------------------------------------------
0 | criterion       | MSELoss          | 0      | train
1 | train_criterion | MSELoss          | 0      | train
2 | val_criterion   | MSELoss          | 0      | train
3 | train_metrics   | MetricCollection | 0      | train
4 | val_metrics     | MetricCollection | 0      | train
5 | res_blocks      | ModuleList       | 166    | train
-------------------------------------------------------------
166       Trainable params
0         Non-trainable params
166       Total params
0.001     Total estimated model params size (MB)
23        Modules in train mode
0         Modules in eval mode
IOPub message rate exceeded.
The Jupyter server will temporarily stop sending output
to the client in order to avoid crashing it.
To change this limit, set the config variable
`--ServerApp.iopub_msg_rate_limit`.

Current values:
ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)
ServerApp.rate_limit_window=3.0 (secs)

概率样本预测

然后要获取概率预测,我们同样只需要指定一些 num_samples >> 1。这将从预测分布中进行采样。

[59]:
pred = model.predict(n=36, num_samples=500)

# scale back:
pred = scaler.inverse_transform(pred, series_idx=0)

series_air.plot()
pred.plot();
GPU available: True (mps), used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
../_images/quickstart_00-quickstart_128_2.png

直接参数预测

我们的似然函数的优点在于,我们无需从分布/分位数中采样,而是可以直接预测分布/分位数参数。为此,我们只需要设置 predict_likelihood_parameters=True

下面我们获取拉普拉斯分布的预测位置 (mu) 和尺度 (b)(在 0 到 1 之间的缩放空间中)。

[60]:
pred = model.predict(n=12, predict_likelihood_parameters=True)

train_air_scaled.plot()
pred.plot(label="laplace_dist");
GPU available: True (mps), used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
/Users/dennisbader/miniconda3/envs/darts/lib/python3.10/site-packages/pytorch_lightning/trainer/setup.py:177: GPU available but not used. You can set it by doing `Trainer(accelerator='gpu')`.
../_images/quickstart_00-quickstart_130_2.png

此外,例如,我们还可以指定我们有一些先验信念,即分布的尺度约为 \(0.1\)(在变换后的域中),同时通过指定 prior_b=.1 来捕捉分布的一些时间依赖性。

在幕后,这将通过 Kullback-Leibler 散度项来正则化训练损失。

[61]:
model = TCNModel(
    input_chunk_length=24,
    output_chunk_length=12,
    random_state=42,
    likelihood=LaplaceLikelihood(prior_b=0.1),
    pl_trainer_kwargs={"accelerator": "cpu"},
)

model.fit(train_air_scaled, epochs=400, verbose=True);
GPU available: True (mps), used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
/Users/dennisbader/miniconda3/envs/darts/lib/python3.10/site-packages/pytorch_lightning/trainer/setup.py:177: GPU available but not used. You can set it by doing `Trainer(accelerator='gpu')`.

  | Name            | Type             | Params | Mode
-------------------------------------------------------------
0 | criterion       | MSELoss          | 0      | train
1 | train_criterion | MSELoss          | 0      | train
2 | val_criterion   | MSELoss          | 0      | train
3 | train_metrics   | MetricCollection | 0      | train
4 | val_metrics     | MetricCollection | 0      | train
5 | res_blocks      | ModuleList       | 166    | train
-------------------------------------------------------------
166       Trainable params
0         Non-trainable params
166       Total params
0.001     Total estimated model params size (MB)
23        Modules in train mode
0         Modules in eval mode
`Trainer.fit` stopped: `max_epochs=400` reached.
[62]:
pred = model.predict(n=36, num_samples=500)

# scale back:
pred = scaler.inverse_transform(pred, series_idx=0)

series_air.plot()
pred.plot();
GPU available: True (mps), used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
../_images/quickstart_00-quickstart_133_2.png

默认情况下,TimeSeries.plot() 显示中位数以及第 5 和第 95 个百分位数(如果是多变量 TimeSeries,则是边缘分布的)。可以控制这一点。

[63]:
pred.plot(low_quantile=0.01, high_quantile=0.99, label="1-99th percentiles")
pred.plot(low_quantile=0.2, high_quantile=0.8, label="20-80th percentiles");
../_images/quickstart_00-quickstart_135_0.png

分布类型

似然函数必须与您的时间序列值的域兼容。例如,PoissonLikelihood 可用于离散正值,ExponentialLikelihood 可用于实数正值,BetaLikelihood 可用于 \((0,1)\) 范围内的实数值。

也可以使用 QuantileRegression 应用分位数损失并直接拟合所需的某些分位数。

评估概率预测

如何评估概率预测的质量?默认情况下,大多数指标函数(例如 mape())将继续工作,但只关注中位数预测。也可以使用平均分位数损失指标 mql(),它量化每个预测分位数的误差。对于分位数 = 0.5(中位数),它与平均绝对误差 (MAE) 相同。

[64]:
from darts.metrics import mae, mql

print(f"MAPE of median forecast: {mape(series_air, pred):.2f}")
print(f"MAE of median forecast: {mae(series_air, pred):.2f}")
for q in [0.05, 0.1, 0.5, 0.9, 0.95]:
    q_loss = mql(series_air, pred, q=q)
    print(f"quantile loss at quantile {q:.2f}: {q_loss:.2f}")
MAPE of median forecast: 12.14
MAE of median forecast: 51.64
quantile loss at quantile 0.05: 5.33
quantile loss at quantile 0.10: 13.14
quantile loss at quantile 0.50: 51.64
quantile loss at quantile 0.90: 20.74
quantile loss at quantile 0.95: 12.20

使用分位数损失

通过直接拟合这些分位数,我们可以做得更好吗?我们可以直接使用 QuantileRegression 似然函数。

[65]:
from darts.utils.likelihood_models.torch import QuantileRegression

model = TCNModel(
    input_chunk_length=24,
    output_chunk_length=12,
    random_state=42,
    likelihood=QuantileRegression([0.05, 0.1, 0.5, 0.9, 0.95]),
)

model.fit(train_air_scaled, epochs=400, verbose=True);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

  | Name            | Type             | Params | Mode
-------------------------------------------------------------
0 | criterion       | MSELoss          | 0      | train
1 | train_criterion | MSELoss          | 0      | train
2 | val_criterion   | MSELoss          | 0      | train
3 | train_metrics   | MetricCollection | 0      | train
4 | val_metrics     | MetricCollection | 0      | train
5 | res_blocks      | ModuleList       | 208    | train
-------------------------------------------------------------
208       Trainable params
0         Non-trainable params
208       Total params
0.001     Total estimated model params size (MB)
23        Modules in train mode
0         Modules in eval mode
`Trainer.fit` stopped: `max_epochs=400` reached.
[66]:
pred = model.predict(n=36, num_samples=500)

# scale back:
pred = scaler.inverse_transform(pred, series_idx=0)

series_air.plot()
pred.plot()

print(f"MAPE of median forecast: {mape(series_air, pred):.2f}")
for q in [0.05, 0.1, 0.5, 0.9, 0.95]:
    q_loss = mql(series_air, pred, q=q)
    print(f"quantile loss at quantile {q:.2f}: {q_loss:.2f}")
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
MAPE of median forecast: 4.91
quantile loss at quantile 0.05: 9.05
quantile loss at quantile 0.10: 13.50
quantile loss at quantile 0.50: 20.07
quantile loss at quantile 0.90: 11.99
quantile loss at quantile 0.95: 7.39
../_images/quickstart_00-quickstart_140_3.png

使用回归模型

我们的 RegressionModels 的概率支持类似于神经网络。我们必须在模型创建时指定一个 likelihood。我们可以不提供似然对象,而是简单地选择 "quantile"(带有一些 quantiles)和 "poisson" 之一。

[67]:
model = LinearRegressionModel(
    lags=24,
    lags_future_covariates=[0],
    likelihood="quantile",
    quantiles=[0.05, 0.1, 0.5, 0.9, 0.95],
)
model.fit(train_air, future_covariates=air_covs)
pred = model.predict(n=36, num_samples=500)

series_air.plot()
pred.plot()

print(f"MAPE of median forecast: {mape(series_air, pred):.2f}")
for q in [0.05, 0.1, 0.5, 0.9, 0.95]:
    q_loss = mql(series_air, pred, q=q)
    print(f"quantile loss at quantile {q:.2f}: {q_loss:.2f}")
MAPE of median forecast: 8.52
quantile loss at quantile 0.05: 20.76
quantile loss at quantile 0.10: 25.44
quantile loss at quantile 0.50: 35.33
quantile loss at quantile 0.90: 11.15
quantile loss at quantile 0.95: 6.19
../_images/quickstart_00-quickstart_142_1.png

模型集成

集成是将多个模型产生的预测进行组合,以获得最终的——并且希望更好的预测。

例如,在我们上面不那么朴素的模型示例中,我们手动将一个朴素季节模型与一个朴素漂移模型组合在一起。在这里,我们将展示如何自动组合模型预测——使用 NaiveEnsembleModel 进行朴素组合,或使用 RegressionEnsembleModel 进行学习组合。

当然,也可以在集成模型中使用 past 和/或 future_covariates,但它们只会被传递给支持它们的预测模型。

朴素集成

朴素集成只是取多个模型的预测的平均值。Darts NaiveEnsembleModel 正是如此,并且使用与预测模型相同的 API(fit, predict, historical forecasts, backtesting 等)。

[68]:
from darts.models import NaiveEnsembleModel

models = [NaiveDrift(), NaiveSeasonal(12)]

ensemble_model = NaiveEnsembleModel(forecasting_models=models)

backtest = ensemble_model.historical_forecasts(**hfc_params)

print(f"MAPE = {mape(backtest, series_air):.2f}%")
series_air.plot()
backtest.plot();
MAPE = 11.93%
../_images/quickstart_00-quickstart_145_2.png

学习集成

在这种情况下,正如预期,朴素集成并未给出很好的结果(尽管在某些情况下可能!)

如果我们将集成视为一个监督回归问题,有时可以做得更好:给定一组预测(特征),找到一个模型来组合它们以最小化目标上的误差。这就是 RegressionEnsembleModel 的作用。它接受以下参数:

  • forecasting_models 是我们想要集成其预测的预测模型列表。

  • regression_train_n_points 是用于拟合“集成回归”模型(即组合预测的内部模型)的时间步数。

  • regression_model 是可选的,可以是 sklearn 兼容的回归模型或 Darts RegressionModel,用于集成回归。如果未指定,则使用线性回归。使用 sklearn 模型开箱即用非常容易,但使用 RegressionModel 可以潜在地将各个预测的任意滞后作为回归模型的输入。

  • 更多内容,请阅读我们的集成模型用户指南

这些要素就位后,RegressionEnsembleModel 就可以像常规预测模型一样使用了。

[69]:
from darts.models import RegressionEnsembleModel

ensemble_model = RegressionEnsembleModel(
    forecasting_models=models, regression_train_n_points=12
)

backtest = ensemble_model.historical_forecasts(**hfc_params)

print(f"MAPE = {mape(backtest, series_air):.2f}")
series_air.plot()
backtest.plot();
MAPE = 4.77
../_images/quickstart_00-quickstart_147_2.png

我们还可以检查在线性组合中用于衡量两个内部模型的系数。

[70]:
ensemble_model.fit(series_air)
ensemble_model.regression_model.model.coef_
[70]:
array([0.01368814, 1.0980107 ], dtype=float32)

集成模型本身也可以是概率性的!您可以在我们的集成模型指南中阅读相关内容。

RegressionEnsembleModel 使用堆叠技术来训练和组合 forecasting_models:每个模型独立训练,然后使用它们的预测作为 future_covariates 来训练 regression_model

滤波模型

除了能够预测序列未来值的预测模型外,Darts 还包含一些有用的滤波模型,它们可以模拟“样本内”序列的值分布。

拟合卡尔曼滤波

KalmanFilter 实现了一个卡尔曼滤波。该实现依赖于 nfoursid,因此例如可以提供一个包含转移矩阵、过程噪声协方差、观测噪声协方差等的 nfoursid.kalman.Kalman 对象。

也可以通过调用 fit() 使用 N4SID 系统辨识算法来“训练”卡尔曼滤波,从而进行系统辨识。

[71]:
from darts.models import KalmanFilter

kf = KalmanFilter(dim_x=3)
kf.fit(train_air_scaled)
filtered_series = kf.filter(train_air_scaled, num_samples=100)

train_air_scaled.plot()
filtered_series.plot();
../_images/quickstart_00-quickstart_153_0.png

使用高斯过程推断缺失值

Darts 还包含一个 GaussianProcessFilter,可用于序列的概率建模。

[72]:
from sklearn.gaussian_process.kernels import RBF

from darts.models import GaussianProcessFilter

# create a series with holes:
values = train_air_scaled.values()
values[20:22] = np.nan
values[28:32] = np.nan
values[55:59] = np.nan
values[72:80] = np.nan
series_holes = TimeSeries.from_times_and_values(train_air_scaled.time_index, values)
series_holes.plot()

kernel = RBF()

gpf = GaussianProcessFilter(kernel=kernel, alpha=0.1, normalize_y=True)
filtered_series = gpf.filter(series_holes, num_samples=100)

filtered_series.plot();
../_images/quickstart_00-quickstart_155_0.png

注意事项

那么,N-BEATS、指数平滑,还是在牛奶产量上训练的贝叶斯岭回归,哪个是预测未来航空旅客数量的最佳方法呢?嗯,在这一点上很难确定哪种方法最好。我们的时间序列很小,验证集甚至更小。在这种情况下,很容易对整个预测练习进行过拟合到如此小的验证集上。如果可用模型的数量及其自由度很高(例如对于深度学习模型),或者我们在单个测试集上尝试了许多模型(如本笔记本所示),情况尤其如此。

作为数据科学家,我们有责任理解我们的模型在多大程度上值得信赖。因此,务必对结果持保留态度,尤其是在小型数据集上,并在进行任何预测之前应用科学方法 :) 祝您建模愉快!

异常检测

Darts 还有一个关于时间序列异常检测的完整模块。更多信息,请阅读我们的异常检测用户指南

[ ]: