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

fix_pythonpath_if_working_locally()
[2]:
%load_ext autoreload
%autoreload 2
%matplotlib inline
[3]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from darts import TimeSeries

import warnings

warnings.filterwarnings("ignore")
import logging

logging.disable(logging.CRITICAL)

静态协变量

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

在本教程中,我们探讨以下内容:

  1. 如何定义静态协变量(数值和/或分类)

  2. 如何将静态协变量添加到现有的 目标 序列

  3. 如何在创建时间序列时添加静态协变量

  4. 如何使用 TimeSeries.from_group_dataframe() 自动提取嵌入静态协变量的时间序列

  5. 如何缩放/转换/编码嵌入在序列中的静态协变量

  6. 如何在使用 Darts 模型时使用静态协变量

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

[4]:
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()
../_images/examples_15-static-covariates_4_0.png

1. 定义静态协变量

将您的静态协变量定义为一个 pd.DataFrame,其中列表示静态变量,行表示它们将被添加到的单变量/多变量 TimeSeries 的组成部分。

  • 行数必须为1或等于 series 中的组件数。

  • 使用单行静态协变量 DataFrame 与多变量(多成分)``series``,静态协变量将全局映射到所有成分。

[5]:
# arbitrary continuous and categorical static covariates (single row)
static_covs_single = pd.DataFrame(data={"cont": [0], "cat": ["a"]})
print(static_covs_single)

# multivariate static covariates (multiple components). note that the number of rows matches the number of components of `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. 向现有的时间序列添加静态协变量

使用方法 with_static_covariates() 从现有的 TimeSeries 创建一个新系列,并添加静态协变量(参见文档 这里

  • 单行静态协变量与多变量 series 创建了“global_components”,这些组件被映射到所有组件

  • 多行静态协变量与多元 series 将被映射到 series 的组件名称(参见静态协变量索引/行名称)

[6]:
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.from_*() 方法中通过参数 static_covariates 创建时间序列时直接添加。

[7]:
# add arbitrary continuous and categorical 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

使用多个时间序列的静态协变量

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

[8]:
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() (参见文档 这里)将它们提取为 TimeSeries 实例的列表。这需要一个或多个列,DataFrame 应按这些列分组(参数 group_cols)。group_cols 将自动作为静态协变量添加到各个序列中。可以使用参数 static_cols 将其他列用作静态协变量。

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

[9]:
# generate an DataFrame example
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",  # individual time series are extracted by grouping `df` by `group_cols`
    static_cols=[
        "var1"
    ],  # also extract these additional columns as static covariates (without grouping)
    value_cols=[
        "comp1",
        "comp2",
        "comp3",
    ],  # optionally, specify the time varying columns
)

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
../_images/examples_15-static-covariates_14_1.png

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

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

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

[10]:
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,它支持数值和分类静态协变量。

[11]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pytorch_lightning.callbacks import TQDMProgressBar

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

6.1 实验设置

在我们的实验中,我们生成了两个时间序列:一个完全的正弦波序列(标签 = 平滑)和一个每隔一个周期带有不规则性的正弦波序列(标签 = 不规则,见周期 2 和 4 的斜坡)。

[12]:
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()
../_images/examples_15-static-covariates_20_0.png

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

  1. fit/predict 没有静态协变量

  2. 使用二进制(数值)静态协变量进行拟合/预测

  3. 使用分类静态协变量进行拟合/预测

对于每个设置,我们将在两个系列上训练模型,然后仅使用第3个周期(两个系列的正弦波)来预测第4个周期(“平滑”系列的正弦波和“不规则”系列的斜坡)。

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

现在,这就是静态协变量真正发挥作用的地方。例如,我们可以通过静态协变量将曲线类型的数据嵌入到 目标 序列中。有了这些信息,我们预计模型会生成更准确的预测。

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

[13]:
def test_case(model, train_series, predict_series):
    """helper function which performs model training, prediction and plotting"""
    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():
    """helper function that generates model parameters with a new Progress Bar object"""
    return {
        "input_chunk_length": int(period / 2),
        "output_chunk_length": int(period / 2),
        "add_encoders": {
            "datetime_attribute": {"future": ["hour"]}
        },  # TFTModel requires future input, with this we won't have to supply any future_covariates
        "random_state": 42,
        "n_epochs": 150,
        "pl_trainer_kwargs": {
            "callbacks": [TQDMProgressBar(refresh_rate=4)],
        },
    }

6.2 不使用静态协变量的预测

让我们在没有静态协变量的情况下训练第一个模型

[14]:
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],
)
../_images/examples_15-static-covariates_24_2.png
../_images/examples_15-static-covariates_24_3.png

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

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

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

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

[15]:
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
../_images/examples_15-static-covariates_27_3.png
../_images/examples_15-static-covariates_27_4.png

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

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

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

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

[16]:
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)

# use StaticCovariatesTransformer to encode categorical static covariates into numeric data
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 来实现,这是一个字典:{特征名称: (类别数量, 嵌入大小)}

[17]:
n_categories = 2  # "smooth" and "non_smooth"
embedding_size = 2  # embed the categorical variable into a numeric vector of size 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],
)
../_images/examples_15-static-covariates_32_2.png
../_images/examples_15-static-covariates_32_3.png

很好,这似乎也奏效了!作为最后一步,让我们看看这些模型之间的表现如何。

[18]:
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"],
        )
    )
../_images/examples_15-static-covariates_34_0.png
Metric
        no st    bin st    cat st
RMSE  0.16352  0.042527  0.050242
../_images/examples_15-static-covariates_34_2.png
Metric
         no st    bin st    cat st
RMSE  0.289051  0.138122  0.138631

这些结果非常棒!两种使用静态协变量的方法都将RMSE相对于基线减少了超过一半!

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