目标变换

%load_ext autoreload
%autoreload 2

无缝转换目标值

由于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')

本地标准化器

我们看到我们的序列现在是随机噪声。假设我们还想对其进行标准化,即使其均值为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()
fig.savefig('../../figs/target_transforms__zeros.png')

全局变换

应用于所有序列的变换

全局Sklearn转换器

有一些变换不需要学习任何参数,例如应用对数。这些变换可以使用 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):
    """将每个序列缩放到 [0, 1] 区间内。"""
    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')

Give us a ⭐ on Github