由于 mlforecast 使用单个全局模型,对目标应用一些变换可能有助于确保所有时间序列具有相似的分布。它们还可以帮助去除那些无法直接处理趋势的模型的趋势。

数据准备

在本例中,我们将使用 M4 数据集中的单个时间序列。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from datasetsforecast.m4 import M4
from sklearn.base import BaseEstimator

from mlforecast import MLForecast
from mlforecast.target_transforms import Differences, LocalStandardScaler
data_path = 'data'
await M4.async_download(data_path, group='Hourly')
df, *_ = M4.load(data_path, 'Hourly')
df['ds'] = df['ds'].astype('int32')
serie = df[df['unique_id'].eq('H196')]

本地变换

每个时间序列应用的变换

差分

我们将查看我们的时间序列,看看哪些差分可能有助于我们的模型。

def plot(series, fname):
    n_series = len(series)
    fig, ax = plt.subplots(ncols=n_series, figsize=(7 * n_series, 6), squeeze=False)
    for (title, serie), axi in zip(series.items(), ax.flat):
        serie.set_index('ds')['y'].plot(title=title, ax=axi)
    fig.savefig(f'../../figs/{fname}', bbox_inches='tight')
    plt.close()
plot({'original': serie}, 'target_transforms__eda.png')

我们可以看到我们的数据具有趋势以及明显的季节性。我们可以先尝试去除趋势。

fcst = MLForecast(
    models=[],
    freq=1,
    target_transforms=[Differences([1])],
)
without_trend = fcst.preprocess(serie)
plot({'original': serie, 'without trend': without_trend}, 'target_transforms__diff1.png')

趋势已经去除,我们现在可以尝试进行 24 阶差分(减去前一天同一小时的值)。

fcst = MLForecast(
    models=[],
    freq=1,
    target_transforms=[Differences([1, 24])],
)
without_trend_and_seasonality = fcst.preprocess(serie)
plot({'original': serie, 'without trend and seasonality': without_trend_and_seasonality}, 'target_transforms__diff2.png')

LocalStandardScaler

我们看到现在我们的时间序列变成了随机噪声。假设我们还想对其进行标准化,即 使其均值为 0、方差为 1。我们可以在进行差分后添加 LocalStandardScaler 变换。

fcst = MLForecast(
    models=[],
    freq=1,
    target_transforms=[Differences([1, 24]), LocalStandardScaler()],
)
standardized = fcst.preprocess(serie)
plot({'original': serie, 'standardized': standardized}, 'target_transforms__standardized.png')
standardized['y'].agg(['mean', 'var']).round(2)
mean   -0.0
var     1.0
Name: y, dtype: float64

既然我们已经捕捉到了时间序列的组成部分(趋势 + 季节性),我们可以尝试使用一个总是预测 0 的模型进行预测,这基本上会投射出趋势和季节性。

class Zeros(BaseEstimator):
    def fit(self, X, y=None):
        return self

    def predict(self, X, y=None):
        return np.zeros(X.shape[0])

fcst = MLForecast(
    models={'zeros_model': Zeros()},
    freq=1,
    target_transforms=[Differences([1, 24]), LocalStandardScaler()],
)
preds = fcst.fit(serie).predict(48)
fig, ax = plt.subplots()
pd.concat([serie.tail(24 * 10), preds]).set_index('ds').plot(ax=ax)
plt.close()

全局变换

应用于所有时间序列的变换

GlobalSklearnTransformer

有些变换不需要学习任何参数,例如应用对数变换。使用 GlobalSklearnTransformer 可以轻松定义这些变换,它接受一个 scikit-learn 兼容的变换器并将其应用于所有时间序列。这是一个定义将时间序列每个值 + 1 后应用对数变换的示例,这有助于避免计算 0 的对数。

import numpy as np
from sklearn.preprocessing import FunctionTransformer

from mlforecast.target_transforms import GlobalSklearnTransformer

sk_log1p = FunctionTransformer(func=np.log1p, inverse_func=np.expm1)
fcst = MLForecast(
    models={'zeros_model': Zeros()},
    freq=1,
    target_transforms=[GlobalSklearnTransformer(sk_log1p)],
)
log1p_transformed = fcst.preprocess(serie)
plot({'original': serie, 'Log transformed': log1p_transformed}, 'target_transforms__log.png')

我们也可以将其与本地变换结合使用。例如,我们可以先应用对数变换,然后进行差分。

fcst = MLForecast(
    models=[],
    freq=1,
    target_transforms=[GlobalSklearnTransformer(sk_log1p), Differences([1, 24])],
)
log_diffs = fcst.preprocess(serie)
plot({'original': serie, 'Log + Differences': log_diffs}, 'target_transforms__log_diffs.png')

自定义变换

实现您自己的目标变换

为了实现您自己的目标变换,您必须定义一个继承自 mlforecast.target_transforms.BaseTargetTransform 的类(这负责将列名设置为 id_coltime_coltarget_col 属性),并实现 fit_transforminverse_transform 方法。这是一个定义最小-最大缩放器的示例。

from mlforecast.target_transforms import BaseTargetTransform
class LocalMinMaxScaler(BaseTargetTransform):
    """Scales each serie to be in the [0, 1] interval."""
    def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:
        self.stats_ = df.groupby(self.id_col)[self.target_col].agg(['min', 'max'])
        df = df.merge(self.stats_, on=self.id_col)
        df[self.target_col] = (df[self.target_col] - df['min']) / (df['max'] - df['min'])
        df = df.drop(columns=['min', 'max'])
        return df

    def inverse_transform(self, df: pd.DataFrame) -> pd.DataFrame:
        df = df.merge(self.stats_, on=self.id_col)
        for col in df.columns.drop([self.id_col, self.time_col, 'min', 'max']):
            df[col] = df[col] * (df['max'] - df['min']) + df['min']
        df = df.drop(columns=['min', 'max'])
        return df

现在您可以将这个类的实例传递给 target_transforms 参数。

fcst = MLForecast(
    models=[],
    freq=1,
    target_transforms=[LocalMinMaxScaler()],
)
minmax_scaled = fcst.preprocess(serie)
plot({'original': serie, 'min-max scaled': minmax_scaled}, 'target_transforms__minmax.png')