用 Python 轻松实现时间序列预测:Darts 静态协变量 Static Covariates

该文章已生成可运行项目,

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Darts

Darts 是一个 Python 库,用于对时间序列进行用户友好型预测和异常检测。它包含多种模型,从 ARIMA 等经典模型到深度神经网络。所有预测模型都能以类似 scikit-learn 的方式使用 fit()predict() 函数。该库还可以轻松地对模型进行回溯测试,将多个模型的预测结果结合起来,并将外部数据考虑在内。Darts 支持单变量和多变量时间序列和模型。基于 ML 的模型可以在包含多个时间序列的潜在大型数据集上进行训练,其中一些模型还为概率预测提供了丰富的支持。

静态协变量

# 如果在本地工作,请修复 Python 路径
from utils import fix_pythonpath_if_working_locally

fix_pythonpath_if_working_locally()
%load_ext autoreload
%autoreload 2
%matplotlib inline
import warnings

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

from darts import TimeSeries

warnings.filterwarnings("ignore")
import logging

logging.disable(logging.CRITICAL)

静态协变量是时间序列的特征/常量,不会随时间变化。当处理多个时间序列时,静态协变量可以帮助特定模型改进预测。Darts 的模型只会考虑嵌入在目标序列(我们想要预测未来值的序列)中的静态协变量,而不会考虑过去和/或未来的协变量(外部数据)。

在本教程中,我们将探讨:

  1. 如何定义静态协变量(数值型和/或分类型)
  2. 如何将静态协变量添加到现有的目标序列
  3. 如何在创建 TimeSeries 时添加静态协变量
  4. 如何使用 TimeSeries.from_group_dataframe() 自动提取带有嵌入静态协变量的 TimeSeries
  5. 如何对嵌入在序列中的静态协变量进行缩放/转换/编码
  6. 如何在 Darts 的模型中使用静态协变量

我们首先生成一个包含三个分量的多元时间序列 ["comp1", "comp2", "comp3"]

np.random.seed(0)
series = TimeSeries.from_times_and_values(
    times=pd.date_range(start="2020-01-01", periods=10, freq="h"),
    values=np.random.random((10, 3)),
    columns=["comp1", "comp2", "comp3"],
)
series.plot()

img

1. 定义静态协变量

将静态协变量定义为 pd.DataFrame,其中列代表静态变量,行代表要添加到的单变量/多变量 TimeSeries 的分量。

  • 行数必须为 1 或与 series 的分量数相等。
  • 在多变量(多分量)series 中使用单行静态协变量 DataFrame 时,静态协变量将全局映射到所有分量。
# 任意的连续和分类静态协变量(单行)
static_covs_single = pd.DataFrame(data={"cont": [0], "cat": ["a"]})
print(static_covs_single)

# 多变量静态协变量(多个分量)。
# 注意行数与 `series` 的分量数匹配
static_covs_multi = pd.DataFrame(data={"cont": [0, 2, 1], "cat": ["a", "c", "b"]})
print(static_covs_multi)
   cont cat
0     0   a
   cont cat
0     0   a
1     2   c
2     1   b

2. 向现有 TimeSeries 添加静态协变量

使用 with_static_covariates() 方法(参见文档)从现有 TimeSeries 创建带有添加静态协变量的新序列。

  • 单行静态协变量与多变量 series 创建“global_components”,映射到所有分量
  • 多行静态协变量与多变量 series 将映射到 series 的分量名称(参见静态协变量索引/行名称)
assert series.static_covariates is None

series_single = series.with_static_covariates(static_covs_single)
print("Single row static covarites with multivariate `series`")
print(series_single.static_covariates)

series_multi = series.with_static_covariates(static_covs_multi)
print("\nMulti row static covarites with multivariate `series`")
print(series_multi.static_covariates)
Single row static covarites with multivariate `series`
static_covariates  cont cat
global_components   0.0   a

Multi row static covarites with multivariate `series`
static_covariates  cont cat
component
comp1               0.0   a
comp2               2.0   c
comp3               1.0   b

3. 在 TimeSeries 构建时添加静态协变量

静态协变量也可以直接在使用大多数 TimeSeries.from_*() 方法创建时间序列时通过参数 static_covariates 添加。

# 添加任意的连续和分类静态协变量
series = TimeSeries.from_values(
    values=np.random.random((10, 3)),
    columns=["comp1", "comp2", "comp3"],
    static_covariates=static_covs_multi,
)
print(series.static_covariates)
static_covariates  cont cat
component
comp1               0.0   a
comp2               2.0   c
comp3               1.0   b

在多个 TimeSeries 中使用静态协变量

静态协变量只有在跨多个 TimeSeries 使用时才真正有用。按照惯例,所有序列的静态协变量布局(pd.DataFrame 列、索引)必须相同。

first_series = series.with_static_covariates(
    pd.DataFrame(data={"ID": ["SERIES1"], "var1": [0.5]})
)
second_series = series.with_static_covariates(
    pd.DataFrame(data={"ID": ["SERIES2"], "var1": [0.75]})
)

print("Valid static covariates for multiple series")
print(first_series.static_covariates)
print(second_series.static_covariates)

series_multi = [first_series, second_series]
Valid static covariates for multiple series
static_covariates       ID  var1
global_components  SERIES1   0.5
static_covariates       ID  var1
global_components  SERIES2  0.75

4. 使用 from_group_dataframe()DataFrame 中按组提取时间序列列表

如果您的 DataFrame 包含多个垂直堆叠的时间序列,您可以使用 TimeSeries.from_group_dataframe()(请参阅此处的文档)将它们提取为时间序列实例列表。这需要对 DataFrame 进行分组的一列或一列列表(参数 group_cols )。可以使用参数 static_cols 将其他列作为静态协变量。

在下面的示例中,我们生成了包含两个不同时间序列(日期重叠/重复)"SERIES1 "和 "SERIES2 "数据的 DataFrame,并使用 from_group_dataframe() 提取时间序列。

# 生成 DataFrame 示例
df = pd.DataFrame(
    data={
        "dates": [
            "2020-01-01",
            "2020-01-02",
            "2020-01-03",
            "2020-01-01",
            "2020-01-02",
            "2020-01-03",
        ],
        "comp1": np.random.random((6,)),
        "comp2": np.random.random((6,)),
        "comp3": np.random.random((6,)),
        "ID": ["SERIES1", "SERIES1", "SERIES1", "SERIES2", "SERIES2", "SERIES2"],
        "var1": [0.5, 0.5, 0.5, 0.75, 0.75, 0.75],
    }
)
print("Input DataFrame")
print(df)

series_multi = TimeSeries.from_group_dataframe(
    df,
    time_col="dates",
    group_cols="ID",  # 通过将 `df` 按 `group_cols` 分组,提取单个时间序列
    static_cols=[
        "var1"
    ],  # 也提取这些附加列作为静态协变量(不分组)
    value_cols=[
        "comp1",
        "comp2",
        "comp3",
    ],  # 可选择指定时间变化列
)

print(f"\n{len(series_multi)} series were extracted from the input DataFrame")
for i, ts in enumerate(series_multi):
    print(f"Static covariates of series {i}")
    print(ts.static_covariates)
    ts["comp1"].plot(label=f"comp1_series_{i}")
Input DataFrame
        dates     comp1     comp2     comp3       ID  var1
0  2020-01-01  0.158970  0.820993  0.976761  SERIES1  0.50
1  2020-01-02  0.110375  0.097101  0.604846  SERIES1  0.50
2  2020-01-03  0.656330  0.837945  0.739264  SERIES1  0.50
3  2020-01-01  0.138183  0.096098  0.039188  SERIES2  0.75
4  2020-01-02  0.196582  0.976459  0.282807  SERIES2  0.75
5  2020-01-03  0.368725  0.468651  0.120197  SERIES2  0.75

2 series were extracted from the input DataFrame
Static covariates of series 0
static_covariates       ID  var1
global_components  SERIES1   0.5
Static covariates of series 1
static_covariates       ID  var1
global_components  SERIES2  0.75

img

5. 缩放/编码/转换静态协变量数据

可能需要缩放数值静态协变量或编码分类静态协变量,因为并非所有模型都能处理非数值静态协变量。

使用 StaticCovariatesTransformer(参见文档)来缩放/转换静态协变量。默认情况下,它使用 MinMaxScaler 缩放数值数据,使用 OrdinalEncoder 编码分类数据。数值和分类转换器都将全局拟合在传递给 StaticCovariatesTransformer.fit() 的所有时间序列的静态协变量数据上。

from darts.dataprocessing.transformers import StaticCovariatesTransformer

transformer = StaticCovariatesTransformer()
series_transformed = transformer.fit_transform(series_multi)

for i, (ts, ts_scaled) in enumerate(zip(series_multi, series_transformed)):
    print(f"Original series {i}")
    print(ts.static_covariates)
    print(f"Transformed series {i}")
    print(ts_scaled.static_covariates)
    print("")
Original series 0
static_covariates       ID  var1
global_components  SERIES1   0.5
Transformed series 0
static_covariates   ID  var1
global_components  0.0   0.0

Original series 1
static_covariates       ID  var1
global_components  SERIES2  0.75
Transformed series 1
static_covariates   ID  var1
global_components  1.0   1.0

6. 使用 TFTModel 和静态协变量进行预测示例

现在让我们看看在预测问题中添加静态协变量是否可以提高预测准确性。我们将使用支持数值和分类静态协变量的 TFTModel

import numpy as np
import pandas as pd
from pytorch_lightning.callbacks import TQDMProgressBar

from darts import TimeSeries
from darts.dataprocessing.transformers import StaticCovariatesTransformer
from darts.metrics import rmse
from darts.models import TFTModel
from darts.utils import timeseries_generation as tg

6.1 实验设置

在我们的实验中,我们生成两个时间序列:一个完整的正弦波序列(标签 = smooth)和一个每隔一个周期就有一些不规则的正弦波序列(标签 = irregular,参见第 2 和第 4 个周期的斜坡)。

period = 20
sine_series = tg.sine_timeseries(
    length=4 * period, value_frequency=1 / period, column_name="smooth", freq="h"
)

sine_vals = sine_series.values()
linear_vals = np.expand_dims(np.linspace(1, -1, num=19), -1)

sine_vals[21:40] = linear_vals
sine_vals[61:80] = linear_vals
irregular_series = TimeSeries.from_times_and_values(
    values=sine_vals, times=sine_series.time_index, columns=["irregular"]
)
sine_series.plot()
irregular_series.plot()

img

我们将使用三种不同的设置进行训练和评估:

  1. 无静态协变量的拟合/预测
  2. 带二进制(数值)静态协变量的拟合/预测
  3. 带分类静态协变量的拟合/预测

对于每种设置,我们将在两个序列上训练模型,然后仅使用第 3 个周期(两个序列的正弦波)来预测第 4 个周期(“smooth”为正弦,“irregular”为斜坡)。

我们希望的是,没有静态协变量的模型表现比其他模型差。非静态模型应该无法识别 predict() 中使用的底层序列是 smooth 还是 irregular 序列,因为它只获得正弦波曲线作为输入(第 3 个周期)。这应导致预测介于平滑和不规则序列之间(通过最小化训练期间的全局损失来学习)。

这正是静态协变量真正有用的地方。例如,我们可以通过静态协变量将曲线类型的数据嵌入到目标序列中。有了这些信息,我们期望模型能够生成改进的预测。

首先,我们创建一些辅助函数,以将相同的实验条件应用于所有模型。

def test_case(model, train_series, predict_series):
    """执行模型训练、预测和绘图的辅助函数"""
    model.fit(train_series)
    preds = model.predict(n=int(period / 2), num_samples=250, series=predict_series)
    for ts, ps in zip(train_series, preds):
        ts.plot()
        ps.plot()
        plt.show()
    return preds

def get_model_params():
    """生成带有新进度条对象的模型参数的辅助函数"""
    return {
        "input_chunk_length": int(period / 2),
        "output_chunk_length": int(period / 2),
        "add_encoders": {
            "datetime_attribute": {"future": ["hour"]}
        },  # TFTModel 需要未来输入,这样我们就不需要提供任何 future_covariates
        "random_state": 42,
        "n_epochs": 150,
        "pl_trainer_kwargs": {
            "callbacks": [TQDMProgressBar(refresh_rate=4)],
        },
    }

6.2 无静态协变量的预测

让我们首先训练没有任何静态协变量的模型

train_series = [sine_series, irregular_series]
for series in train_series:
    assert not series.has_static_covariates

model = TFTModel(**get_model_params())
preds = test_case(
    model,
    train_series,
    predict_series=[series[:60] for series in train_series],
)

img

img

从图中可以看出,预测在第 3 个周期后开始(~01-03-2022 - 12:00)。预测输入是最后 input_chunk_length=10 个值——两个序列相同(正弦波)。

正如预期的那样,模型无法确定底层预测序列的类型(平滑或不规则),并为两者生成了类似正弦波的预测。

6.3 使用 0/1 二进制静态协变量(数值)进行预测

现在让我们重复实验,但这次我们将曲线类型的信息作为二进制(数值)静态协变量 "curve_type" 添加。

sine_series_st_bin = sine_series.with_static_covariates(
    pd.DataFrame(data={"curve_type": [1]})
)
irregular_series_st_bin = irregular_series.with_static_covariates(
    pd.DataFrame(data={"curve_type": [0]})
)

train_series = [sine_series_st_bin, irregular_series_st_bin]
for series in train_series:
    print(series.static_covariates)

model = TFTModel(**get_model_params())
preds_st_bin = test_case(
    model,
    train_series,
    predict_series=[series[:60] for series in train_series],
)
static_covariates  curve_type
component
smooth                    1.0
static_covariates  curve_type
component
irregular                 0.0

img

img

这已经看起来好多了!模型能够从二进制静态协变量特征中识别曲线类型/类别。

6.4 使用分类静态协变量进行预测

最后一个实验已经显示出有希望的结果。那么为什么不仅仅使用二进制特征来表示分类数据呢?虽然对于我们的两个时间序列来说可能效果很好,但如果我们有更多的曲线类型,我们需要将特征一次性编码为每个类别的二进制变量。当类别很多时,这会导致大量特征/预测变量和多重共线性,从而降低模型的预测准确性。

作为最后一个实验,让我们将曲线类型用作分类特征。TFTModel 学习分类特征的嵌入。Darts 的 TorchForecastingModels(如 TFTModel)仅支持数值数据。在训练之前,我们需要使用 StaticCovariatesTransformer(参见第 5 节)将 "curve_type" 转换为整数值特征。

sine_series_st_cat = sine_series.with_static_covariates(
    pd.DataFrame(data={"curve_type": ["smooth"]})
)
irregular_series_st_cat = irregular_series.with_static_covariates(
    pd.DataFrame(data={"curve_type": ["non_smooth"]})
)

train_series = [sine_series_st_cat, irregular_series_st_cat]
print("Static covariates before encoding:")
print(train_series[0].static_covariates)

# 使用静态协变量转换器将分类静态协变量编码为数值数据
scaler = StaticCovariatesTransformer()
train_series = scaler.fit_transform(train_series)
print("\nStatic covariates after encoding:")
print(train_series[0].static_covariates)
Static covariates before encoding:
static_covariates curve_type
component
smooth                smooth

Static covariates after encoding:
static_covariates  curve_type
component
smooth                    1.0

现在我们需要做的只是告诉 TFTModel "curve_type" 是一个需要嵌入的分类变量。我们可以通过模型参数 categorical_embedding_sizes 来实现,这是一个字典:{特征名称: (类别数, 嵌入大小)}

n_categories = 2  # "smooth" 和 "non_smooth"
embedding_size = 2  # 将分类变量嵌入到大小为 2 的数值向量中
categorical_embedding_sizes = {"curve_type": (n_categories, embedding_size)}

model = TFTModel(
    categorical_embedding_sizes=categorical_embedding_sizes, **get_model_params()
)
preds_st_cat = test_case(
    model,
    train_series,
    predict_series=[series[:60] for series in train_series],
)

img

img

很好,这似乎也行得通!作为最后一步,让我们看看模型之间的比较表现如何。

for series, ps_no_st, ps_st_bin, ps_st_cat in zip(
    train_series, preds, preds_st_bin, preds_st_cat
):
    series[-40:].plot(label="target")
    ps_no_st.quantile(0.5).plot(label="no static covs")
    ps_st_bin.quantile(0.5).plot(label="binary static covs")
    ps_st_cat.quantile(0.5).plot(label="categorical static covs")
    plt.show()
    print("Metric")
    print(
        pd.DataFrame(
            {
                name: [rmse(series, ps)]
                for name, ps in zip(
                    ["no st", "bin st", "cat st"], [ps_no_st, ps_st_bin, ps_st_cat]
                )
            },
            index=["RMSE"],
        )
    )

img

Metric
        no st    bin st    cat st
RMSE  0.16352  0.042527  0.050242

img

Metric
         no st    bin st    cat st
RMSE  0.289051  0.138122  0.138631

这些结果很棒!与基线相比,使用静态协变量的两种方法都将两个序列的 RMSE 降低了一半以上!

请注意,我们只使用了一个静态协变量特征,但您可以根据需要使用任意数量的特征,包括混合数据类型(数值和分类)。

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

该文章已生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值