sktime.forecasting.compose._reduce 源代码

#!/usr/bin/env python3 -u
# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
"""Composition functionality for reduction approaches to forecasting."""

__author__ = [
    "mloning",
    "AyushmaanSeth",
    "danbartl",
    "kAnand77",
    "LuisZugasti",
    "Lovkush-A",
    "fkiraly",
    "benheid",
]

__all__ = [
    "make_reduction",
    "DirectTimeSeriesRegressionForecaster",
    "RecursiveTimeSeriesRegressionForecaster",
    "MultioutputTimeSeriesRegressionForecaster",
    "DirectTabularRegressionForecaster",
    "RecursiveTabularRegressionForecaster",
    "MultioutputTabularRegressionForecaster",
    "DirRecTabularRegressionForecaster",
    "DirRecTimeSeriesRegressionForecaster",
    "DirectReductionForecaster",
    "YfromX",
]

import numpy as np
import pandas as pd
from sklearn.base import clone
from sklearn.multioutput import MultiOutputRegressor

from sktime.datatypes._utilities import get_time_index
from sktime.forecasting.base import BaseForecaster, ForecastingHorizon
from sktime.forecasting.base._fh import _index_range
from sktime.forecasting.base._sktime import _BaseWindowForecaster
from sktime.registry import is_scitype, scitype
from sktime.transformations.compose import FeatureUnion
from sktime.transformations.series.summarize import WindowSummarizer
from sktime.utils.datetime import _shift
from sktime.utils.estimators.dispatch import construct_dispatch
from sktime.utils.sklearn import is_sklearn_estimator, prep_skl_df, sklearn_scitype
from sktime.utils.validation import check_window_length
from sktime.utils.warnings import warn


def _concat_y_X(y, X):
    """Concatenate y and X prior to sliding-window transform."""
    z = y.to_numpy()
    if z.ndim == 1:
        z = z.reshape(-1, 1)
    if X is not None:
        z = np.column_stack([z, X.to_numpy()])
    return z


def _check_fh(fh):
    """Check fh prior to sliding-window transform."""
    assert fh.is_relative
    assert fh.is_all_out_of_sample()
    return fh.to_indexer().to_numpy()


def _sliding_window_transform(
    y,
    window_length,
    fh,
    X=None,
    transformers=None,
    scitype="tabular-regressor",
    pooling="local",
    windows_identical=True,
):
    """Transform time series data using sliding window.

    See ``test_sliding_window_transform_explicit`` in test_reduce.py for explicit
    example.

    Parameters
    ----------
    y : pd.Series
        Endogenous time series
    window_length : int
        Window length for transformed feature variables
    fh : ForecastingHorizon
        Forecasting horizon for transformed target variable
    X : pd.DataFrame, optional (default=None)
        Exogenous series.
    transformers: list of transformers (default = None)
        A suitable list of transformers that allows for using an en-bloc approach with
        make_reduction. This means that instead of using the raw past observations of
        y across the window length, suitable features will be generated directly from
        the past raw observations. Currently only supports WindowSummarizer (or a list
        of WindowSummarizers) to generate features e.g. the mean of the past 7
        observations.
    pooling: str {"local", "global"}, optional
        Specifies whether separate models will be fit at the level of each instance
        (local) of if you wish to fit a single model to all instances ("global").
    scitype : str {"tabular-regressor", "time-series-regressor"}, optional
        Scitype of estimator to use with transformed data.
        - If "tabular-regressor", returns X as tabular 2d array
        - If "time-series-regressor", returns X as panel 3d array
    windows_identical: bool, (default = True)
        Direct forecasting only.
        Specifies whether all direct models use the same number of observations
        (True: Total observations + 1 - window_length - maximum forecasting horizon)
        or a different number of observations (False: Total observations + 1
        - window_length - forecasting horizon).

    Returns
    -------
    yt : np.ndarray, shape = (n_timepoints - window_length, 1)
        Transformed target variable.
    Xt : np.ndarray, shape = (n_timepoints - window_length, n_variables,
    window_length)
        Transformed lagged values of target variable and exogenous variables,
        excluding contemporaneous values.
    """
    # There are different ways to implement this transform. Pre-allocating an
    # array and filling it by iterating over the window length seems to be the most
    # efficient one.

    ts_index = get_time_index(y)
    n_timepoints = ts_index.shape[0]
    window_length = check_window_length(window_length, n_timepoints)

    if pooling == "global":
        n_cut = -window_length

        if len(transformers) == 1:
            tf_fit = transformers[0].fit(y)
        else:
            feat = [("trafo_" + str(index), i) for index, i in enumerate(transformers)]
            tf_fit = FeatureUnion(feat).fit(y)
        X_from_y = tf_fit.transform(y)

        X_from_y_cut = _cut_df(X_from_y, n_obs=n_cut)
        yt = _cut_df(y, n_obs=n_cut)

        if X is not None:
            X_cut = _cut_df(X, n_obs=n_cut)
            Xt = pd.concat([X_from_y_cut, X_cut], axis=1)
        else:
            Xt = X_from_y_cut
    else:
        z = _concat_y_X(y, X)
        n_timepoints, n_variables = z.shape

        fh = _check_fh(fh)
        fh_max = fh[-1]

        if window_length + fh_max >= n_timepoints:
            raise ValueError(
                "The `window_length` and `fh` are incompatible with the length of `y`"
            )

        # Get the effective window length accounting for the forecasting horizon.
        effective_window_length = window_length + fh_max
        Zt = np.zeros(
            (
                n_timepoints + effective_window_length,
                n_variables,
                effective_window_length + 1,
            )
        )

        # Transform data.
        for k in range(effective_window_length + 1):
            i = effective_window_length - k
            j = n_timepoints + effective_window_length - k
            Zt[i:j, :, k] = z

        # Truncate data, selecting only full windows, discarding incomplete ones.
        if windows_identical is True:
            Zt = Zt[effective_window_length:-effective_window_length]
        else:
            Zt = Zt[effective_window_length:-window_length]
        # Return transformed feature and target variables separately. This
        # excludes contemporaneous values of the exogenous variables. Including them
        # would lead to unequal-length data, with more time points for
        # exogenous series than the target series, which is currently not supported.
        yt = Zt[:, 0, window_length + fh]
        Xt = Zt[:, :, :window_length]
    # Pre-allocate array for sliding windows.
    # If the scitype is tabular regression, we have to convert X into a 2d array.
    if scitype == "tabular-regressor" and transformers is None:
        Xt = Xt.reshape(Xt.shape[0], -1)

    assert Xt.ndim == 2 or Xt.ndim == 3
    assert yt.ndim == 2

    return yt, Xt


class _Reducer(_BaseWindowForecaster):
    """Base class for reducing forecasting to regression."""

    _tags = {
        "authors": [
            "mloning",
            "AyushmaanSeth",
            "danbartl",
            "kAnand77",
            "LuisZugasti",
            "Lovkush-A",
            "fkiraly",
            "benheid",
        ],
        "ignores-exogeneous-X": False,  # reduction uses X in non-trivial way
        "handles-missing-data": True,
        "capability:insample": False,
        "capability:pred_int": True,
        "capability:pred_int:insample": False,
    }

    def __init__(
        self,
        estimator,
        window_length=10,
        transformers=None,
        pooling="local",
    ):
        super().__init__(window_length=window_length)
        self.transformers = transformers
        self.transformers_ = None
        self.estimator = estimator
        self.pooling = pooling
        self._cv = None

        # it seems that the sklearn tags are not fully reliable
        # see discussion in PR #3405 and issue #3402
        # therefore this is commented out until sktime and sklearn are better aligned
        # self.set_tags(**{"handles-missing-data": estimator._get_tags()["allow_nan"]})

        # for dealing with probabilistic regressors:
        # self._est_type encodes information what type of estimator is passed
        if hasattr(estimator, "get_tags"):
            _est_type = estimator.get_tag("object_type", "regressor", False)
        else:
            _est_type = "regressor"

        if _est_type not in ["regressor", "regressor_proba"]:
            raise TypeError(
                f"error in {type(self).__name}, "
                "estimator must be either an sklearn compatible "
                "regressor, or an skpro probabilistic regressor."
            )

        # has probabilistic mode iff the estimator is of type regressor_proba
        self.set_tags(**{"capability:pred_int": _est_type == "regressor_proba"})

        self._est_type = _est_type

    def _is_predictable(self, last_window):
        """Check if we can make predictions from last window."""
        return (
            len(last_window) == self.window_length_
            and np.sum(np.isnan(last_window)) == 0
            and np.sum(np.isinf(last_window)) == 0
        )

    def _predict_quantiles(self, fh, X, alpha):
        """Compute/return prediction quantiles for a forecast.

        Parameters
        ----------
        fh : guaranteed to be ForecastingHorizon
            The forecasting horizon with the steps ahead to to predict.
        X :  sktime time series object, optional (default=None)
            guaranteed to be of an mtype in self.get_tag("X_inner_mtype")
            Exogeneous time series for the forecast
        alpha : list of float (guaranteed not None and floats in [0,1] interval)
            A list of probabilities at which quantile forecasts are computed.
        """
        kwargs = {"X": X, "alpha": alpha, "method": "predict_quantiles"}

        y_pred = self._predict_boilerplate(fh, **kwargs)

        return y_pred

    def _predict_in_sample(self, fh, X=None, **kwargs):
        # Note that we currently only support out-of-sample predictions. For the
        # direct and multioutput strategy, we need to check this already during fit,
        # as the fh is required for fitting.
        pass

    @classmethod
    def get_test_params(cls, parameter_set="default"):
        """Return testing parameter settings for the estimator.

        Parameters
        ----------
        parameter_set : str, default="default"
            Name of the set of test parameters to return, for use in tests. If no
            special parameters are defined for a value, will return ``"default"`` set.

        Returns
        -------
        params : dict or list of dict, default = {}
            Parameters to create testing instances of the class
            Each dict are parameters to construct an "interesting" test instance, i.e.,
            ``MyClass(**params)`` or ``MyClass(**params[i])`` creates a valid test
            instance.
            ``create_test_instance`` uses the first (or only) dictionary in ``params``
        """
        from sklearn.linear_model import LinearRegression
        from sklearn.pipeline import make_pipeline

        from sktime.transformations.panel.reduce import Tabularizer
        from sktime.utils.dependencies import _check_soft_dependencies

        # naming convention is as follows:
        #   reducers with Tabular take an sklearn estimator, e.g., LinearRegressor
        #   reducers with TimeSeries take an sktime supervised estimator
        #       e.g., pipeline of Tabularizer and Linear Regression
        # which of these is the case, we check by checking substring in the class name
        est = LinearRegression()
        if "TimeSeries" in cls.__name__:
            est = make_pipeline(Tabularizer(), est)

        params = [{"estimator": est, "window_length": 3}]

        PROBA_IMPLEMENTED = ["DirectTabularRegressionForecaster"]
        self_supports_proba = cls.__name__ in PROBA_IMPLEMENTED

        if _check_soft_dependencies("skpro", severity="none") and self_supports_proba:
            from skpro.regression.residual import ResidualDouble

            params_proba_local = {
                "estimator": ResidualDouble.create_test_instance(),
                "pooling": "local",
                "window_length": 3,
            }
            params_proba_global = {
                "estimator": ResidualDouble.create_test_instance(),
                "pooling": "global",
                "window_length": 4,
            }
            params = params + [params_proba_local, params_proba_global]

        return params

    def _get_shifted_window(self, shift=0, y_update=None, X_update=None):
        """Get the start and end points of a shifted window.

        In recursive forecasting, the time based features need to be recalculated for
        every time step that is forecast. This is done in an iterative fashion over
        every forecasting horizon step. Shift specifies the timestamp over which the
        iteration is done, i.e. a shift of 0 will get a window between window_length
        steps in the past and t=0, shift = 1 will be window_length - 1 steps in the past
        and t= 1 etc- up to the forecasting horizon.

        Will also apply any transformers passed to the recursive reducer to y. This en
        block approach of directly applying the transformers is more efficient than
        creating all lags first across the window and then applying the transformers
        to the lagged data.

        Please see below a graphical representation of the logic using the following
        symbols:

        ``z`` = first observation to forecast.
        Not part of the window.
        ``*`` = (other) time stamps in the window which is summarized
        ``x`` = observations, past or future, not part of the window

        For``window_length = 7`` and ``fh = [3]`` we get the following windows

        ``shift = 0``
        |--------------------------- |
        | x x x x * * * * * * * z x x|
        |----------------------------|

        ``shift = 1``
        |--------------------------- |
        | x x x x x * * * * * * * z x|
        |----------------------------|

        ``shift = 2``
        |--------------------------- |
        | x x x x x x * * * * * * * z|
        |----------------------------|

        Parameters
        ----------
        shift: int, default=0
            this will be correspond to the shift of the window_length into the future
        y_update : a pandas Series or Dataframe
            y values that were obtained in the recursive fashion.
        X_update : a pandas Series or Dataframe
            X values also need to be cut based on the into windows, see above.

        Returns
        -------
        y, X: A pandas dataframe or series
            contains the y and X data prepared for the respective windows, see above.
        """
        if hasattr(self._timepoints, "freq"):
            if self._timepoints.freq is None:
                freq_inferred = pd.infer_freq(self._timepoints)
                cutoff_with_freq = self._cutoff
                cutoff_with_freq.freq = freq_inferred
            else:
                cutoff_with_freq = self._cutoff
        else:
            cutoff_with_freq = self._cutoff
        cutoff = _shift(cutoff_with_freq, by=shift, return_index=True)

        relative_int = pd.Index(list(map(int, range(-self.window_length_ + 1, 2))))
        # relative _int will give the integer indices of the window. Also contains the
        # first observation after the window (this is what the window is summarized to).

        index_range = _index_range(relative_int, cutoff)
        if isinstance(cutoff, pd.DatetimeIndex):
            if cutoff.tzinfo is not None:
                index_range = index_range.tz_localize(cutoff.tzinfo)
        # index_range will convert the indices to the date format of cutoff

        y_raw = _create_fcst_df(index_range, self._y)
        # y_raw is a dataframe window_length forecasting steps into the past in order to
        # calculate the new X from y features based on the transformer provided

        y_raw.update(self._y)
        # Historical values are passed here for all time steps of y_raw that lie in
        # the past .

        if y_update is not None:
            y_raw.update(y_update)
        # The y_raw dataframe will is updated with recursively forecast values.

        if len(self.transformers_) == 1:
            X_from_y = self.transformers_[0].fit_transform(y_raw)
        else:
            ref = self.transformers_
            feat = [("trafo_" + str(index), i) for index, i in enumerate(ref)]
            X_from_y = FeatureUnion(feat).fit_transform(y_raw)
        # After filling the empty y_raw frame with historic / forecast values
        # X from y features can be calculated based on the passed transformer.

        X_from_y_cut = _cut_df(X_from_y)
        # We are only interested in the last observation, since only that one
        # contains the value the window is summarized to.

        if self._X is not None:
            X = _create_fcst_df([index_range[-1]], self._X)
            X.update(self._X)
            if X_update is not None:
                X.update(X_update)
            X_cut = _cut_df(X)
            X = pd.concat([X_from_y_cut, X_cut], axis=1)
            # X_from_y_cut is added to X dataframe (no features need to be calculated).
        else:
            X = X_from_y_cut

        y = _cut_df(y_raw)
        return y, X


class _DirectReducer(_Reducer):
    strategy = "direct"
    _tags = {
        "requires-fh-in-fit": True,  # is the forecasting horizon required in fit?
    }

    def __init__(
        self,
        estimator,
        window_length=10,
        transformers=None,
        pooling="local",
        windows_identical=True,
    ):
        self.windows_identical = windows_identical
        super().__init__(
            estimator=estimator,
            window_length=window_length,
            transformers=transformers,
            pooling=pooling,
        )

    def _transform(self, y, X=None):
        fh = self.fh.to_relative(self.cutoff)
        return _sliding_window_transform(
            y,
            window_length=self.window_length_,
            fh=fh,
            X=X,
            transformers=self.transformers_,
            scitype=self._estimator_scitype,
            pooling=self.pooling,
            windows_identical=self.windows_identical,
        )

    def _fit(self, y, X, fh):
        """Fit to training data.

        Parameters
        ----------
        y : pd.Series
            Target time series to which to fit the forecaster.
        X : pd.DataFrame, optional (default=None)
            Exogenous variables are ignored
        fh : int, list or np.array, optional (default=None)
             The forecasters horizon with the steps ahead to to predict.

        Returns
        -------
        self : Estimator
            An fitted instance of self.
        """
        # We currently only support out-of-sample predictions. For the direct
        # strategy, we need to check this at the beginning of fit, as the fh is
        # required for fitting.
        self._timepoints = get_time_index(y)
        n_timepoints = len(self._timepoints)

        if self.pooling is not None and self.pooling not in ["local", "global"]:
            raise ValueError(
                "pooling must be one of local, global" + f" but found {self.pooling}"
            )

        if self.window_length is not None and self.transformers is not None:
            raise ValueError(
                "Transformers provided, suggesting en-bloc approach"
                + " to derive reduction features. Window length will be"
                + " inferred, please set to None"
            )
        if self.transformers is not None and self.pooling == "local":
            raise ValueError(
                "Transformers currently cannot be provided"
                + "for models that run locally"
            )
        pd_format = isinstance(y, pd.Series) or isinstance(y, pd.DataFrame)
        if self.pooling == "local":
            if pd_format is True and isinstance(y, pd.MultiIndex):
                warn(
                    "Pooling is by default 'local', which"
                    + " means that separate models will be fit at the level of"
                    + " each instance. If you wish to fit a single model to"
                    + " all instances, please specify pooling = 'global'.",
                    obj=self,
                )
        self.window_length_ = check_window_length(
            self.window_length, n_timepoints=len(y)
        )
        if self.transformers is not None:
            self.transformers_ = clone(self.transformers)

        if self.transformers is None and self.pooling == "global":
            kwargs = {
                "lag_feature": {
                    "lag": list(range(1, self.window_length + 1)),
                }
            }
            self.transformers_ = [WindowSummarizer(**kwargs, n_jobs=1)]

        if self.window_length is None:
            trafo = self.transformers_
            fit_trafo = [i.fit(y) for i in trafo]
            ts = [i.truncate_start for i in fit_trafo if hasattr(i, "truncate_start")]

            if len(ts) > 0:
                self.window_length_ = max(ts)
            else:
                raise ValueError(
                    "Reduce must either have window length as argument"
                    + "or needs to have it passed by transformer via"
                    + "truncate_start"
                )

            if self.transformers_ is not None and n_timepoints < max(ts):
                raise ValueError(
                    "Not sufficient observations to calculate transformations"
                    + "Please reduce window length / window lagging to match"
                    + "observation size"
                )

        yt, Xt = self._transform(y, X)
        if hasattr(Xt, "columns"):
            Xt.columns = Xt.columns.astype(str)

        # Iterate over forecasting horizon, fitting a separate estimator for each step.
        self.estimators_ = []
        for i in range(len(self.fh)):
            fh_rel = fh.to_relative(self.cutoff)
            estimator = clone(self.estimator)

            if self.transformers_ is not None:
                fh_rel = fh.to_relative(self.cutoff)
                Xt_cut = _cut_df(Xt, n_timepoints - fh_rel[i] + 1, type="head")
                yt_cut = _cut_df(yt, n_timepoints - fh_rel[i] + 1)
            elif self.windows_identical is True or (fh_rel[i] - 1) == 0:
                Xt_cut = Xt
                yt_cut = yt[:, i]
            else:
                Xt_cut = Xt[: -(fh_rel[i] - 1)]
                yt_cut = yt[: -(fh_rel[i] - 1), i]

            # coercion to pandas for skpro proba regressors
            if self._est_type != "regressor" and not isinstance(Xt, pd.DataFrame):
                Xt_cut = pd.DataFrame(Xt_cut)
            if self._est_type != "regressor" and not isinstance(yt, pd.DataFrame):
                yt_cut = pd.DataFrame(yt_cut)

            estimator.fit(Xt_cut, yt_cut)
            self.estimators_.append(estimator)
        return self

    def _predict_last_window(self, fh, X=None, **kwargs):
        """.

        In recursive reduction, iteration must be done over the
        entire forecasting horizon. Specifically, when transformers are
        applied to y that generate features in X, forecasting must be done step by
        step to integrate the latest prediction of for the new set of features in
        X derived from that y.

        Parameters
        ----------
        fh : int, list, np.array or ForecastingHorizon
            Forecasting horizon
        X : pd.DataFrame, optional (default=None)
            Exogenous time series

        Returns
        -------
        y_return = pd.Series or pd.DataFrame
        """
        if "method" in kwargs:
            method = kwargs.pop("method")
        else:
            method = "predict"

        # estimator type for case branches
        est_type = self._est_type
        # "regressor" for sklearn, "regressor_proba" for skpro

        if self._X is not None and X is None:
            raise ValueError(
                "`X` must be passed to `predict` if `X` is given in `fit`."
            )

        if self.pooling == "global":
            y_last, X_last = self._get_shifted_window(X_update=X)
            ys = np.array(y_last)
            if not np.sum(np.isnan(ys)) == 0 and np.sum(np.isinf(ys)) == 0:
                return self._predict_nan(fh, method=method, **kwargs)
        else:
            y_last, X_last = self._get_last_window()
            if not self._is_predictable(y_last):
                return self._predict_nan(fh, method=method, **kwargs)
        # Get last window of available data.
        # If we cannot generate a prediction from the available data, return nan.

        if isinstance(X_last, pd.DataFrame):
            X_last = prep_skl_df(X_last)

        def pool_preds(y_preds):
            """Pool predictions from different estimators.

            Parameters
            ----------
            y_preds : list of pd.DataFrame
                List of predictions from different estimators.
            """
            y_pred = y_preds.pop(0)
            for y_pred_i in y_preds:
                y_pred = y_pred.combine_first(y_pred_i)
            return y_pred

        def _coerce_to_numpy(y_pred):
            """Coerce predictions to numpy array, assumes pd.DataFrame or numpy."""
            if isinstance(y_pred, pd.DataFrame):
                return y_pred.values
            else:
                return y_pred

        if self.pooling == "global":
            fh_abs = fh.to_absolute_index(self.cutoff)
            y_preds = []

            for i, estimator in enumerate(self.estimators_):
                y_pred_est = getattr(estimator, method)(X_last, **kwargs)
                if est_type == "regressor":
                    y_pred_i = _create_fcst_df([fh_abs[i]], self._y, fill=y_pred_est)
                else:  # est_type == "regressor_proba"
                    y_pred_v = _coerce_to_numpy(y_pred_est)
                    y_pred_i = _create_fcst_df([fh_abs[i]], y_pred_est, fill=y_pred_v)
                y_preds.append(y_pred_i)
            y_pred = pool_preds(y_preds)

        else:
            # Pre-allocate arrays.
            if self._X is None:
                n_columns = 1
            else:
                # X is ignored here, since we currently only look at lagged values for
                # exogenous variables and not contemporaneous ones.
                n_columns = self._X.shape[1] + 1

            # Pre-allocate arrays.
            window_length = self.window_length_
            X_pred = np.zeros((1, n_columns, window_length))

            # Fill pre-allocated arrays with available data.
            X_pred[:, 0, :] = y_last
            if self._X is not None:
                X_pred[:, 1:, :] = X_last.T

            # We need to make sure that X has the same order as used in fit.
            if self._estimator_scitype == "tabular-regressor":
                X_pred = X_pred.reshape(1, -1)

            # Allocate array for predictions.
            if est_type == "regressor":
                y_pred = np.zeros(len(fh))
            else:  # est_type == "regressor_proba"
                y_preds = []

            # Iterate over estimators/forecast horizon
            for i, estimator in enumerate(self.estimators_):
                y_pred_est = getattr(estimator, method)(X_pred, **kwargs)
                if est_type == "regressor":
                    y_pred[i] = y_pred_est
                else:  # est_type == "regressor_proba"
                    y_pred_v = _coerce_to_numpy(y_pred_est)
                    y_pred_i = _create_fcst_df([fh[i]], y_pred_est, fill=y_pred_v)
                    y_preds.append(y_pred_i)

            if est_type != "regressor":
                y_pred = pool_preds(y_preds)

        # coerce index and columns to expected
        index = fh.get_expected_pred_idx(y=self._y, cutoff=self.cutoff)
        columns = self._get_columns(method=method, **kwargs)
        if isinstance(y_pred, pd.DataFrame):
            y_pred.index = index
            y_pred.columns = columns
        else:
            y_pred = pd.DataFrame(y_pred, index=index, columns=columns)

        return y_pred


class _MultioutputReducer(_Reducer):
    strategy = "multioutput"
    _tags = {
        "requires-fh-in-fit": True,  # is the forecasting horizon required in fit?
    }

    def _transform(self, y, X=None):
        fh = self.fh.to_relative(self.cutoff)
        return _sliding_window_transform(
            y,
            window_length=self.window_length,
            fh=fh,
            X=X,
            scitype=self._estimator_scitype,
        )

    def _fit(self, y, X, fh):
        """Fit to training data.

        Parameters
        ----------
        y : pd.Series
            Target time series to which to fit the forecaster.
        X : pd.DataFrame, optional (default=None)
            Exogenous variables are ignored
        fh : int, list or np.array, optional (default=None)
             The forecasters horizon with the steps ahead to to predict.

        Returns
        -------
        self : returns an instance of self.
        """
        # We currently only support out-of-sample predictions. For the direct
        # strategy, we need to check this at the beginning of fit, as the fh is
        # required for fitting.
        self.window_length_ = check_window_length(
            self.window_length, n_timepoints=len(y)
        )

        yt, Xt = self._transform(y, X)

        # Fit a multi-output estimator to the transformed data.
        self.estimator_ = clone(self.estimator)
        self.estimator_.fit(Xt, yt)
        return self

    def _predict_last_window(self, fh, X=None, **kwargs):
        """Predict to training data.

        Parameters
        ----------
        fh : int, list, np.array or ForecastingHorizon
            Forecasting horizon
        X : pd.DataFrame, optional (default=None)
            Exogenous time series

        Returns
        -------
        y_pred = pd.Series or pd.DataFrame
        """
        # Get last window of available data.
        y_last, X_last = self._get_last_window()

        # If we cannot generate a prediction from the available data, return nan.
        if not self._is_predictable(y_last):
            return self._predict_nan(fh)

        if self._X is None:
            n_columns = 1
        else:
            # X is ignored here, since we currently only look at lagged values for
            # exogenous variables and not contemporaneous ones.
            n_columns = self._X.shape[1] + 1

        # Pre-allocate arrays.
        window_length = self.window_length_
        X_pred = np.zeros((1, n_columns, window_length))

        # Fill pre-allocated arrays with available data.
        X_pred[:, 0, :] = y_last
        if self._X is not None:
            X_pred[:, 1:, :] = X_last.T

        # We need to make sure that X has the same order as used in fit.
        if self._estimator_scitype == "tabular-regressor":
            X_pred = X_pred.reshape(1, -1)

        # Iterate over estimators/forecast horizon
        y_pred = self.estimator_.predict(X_pred)
        return y_pred.ravel()


class _RecursiveReducer(_Reducer):
    strategy = "recursive"

    def _transform(self, y, X=None):
        # For the recursive strategy, the forecasting horizon for the sliding-window
        # transform is simply a one-step ahead horizon, regardless of the horizon
        # used during prediction.
        fh = ForecastingHorizon([1])
        return _sliding_window_transform(
            y,
            self.window_length_,
            fh,
            X=X,
            transformers=self.transformers_,
            scitype=self._estimator_scitype,
            pooling=self.pooling,
        )

    def _fit(self, y, X, fh):
        """Fit to training data.

        Parameters
        ----------
        y : pd.Series
            Target time series to which to fit the forecaster.
        X : pd.DataFrame, optional (default=None)
            Exogenous variables are ignored
        fh : int, list or np.array, optional (default=None)
             The forecasters horizon with the steps ahead to to predict.

        Returns
        -------
        self : returns an instance of self.
        """
        if self.pooling is not None and self.pooling not in ["local", "global"]:
            raise ValueError(
                "pooling must be one of local, global" + f" but found {self.pooling}"
            )

        if self.window_length is not None and self.transformers is not None:
            raise ValueError(
                "Transformers provided, suggesting en-bloc approach"
                + " to derive reduction features. Window length will be"
                + " inferred, please set to None"
            )
        if self.transformers is not None and self.pooling == "local":
            raise ValueError(
                "Transformers currently cannot be provided"
                + "for models that run locally"
            )

        pd_format = isinstance(y, pd.Series) or isinstance(y, pd.DataFrame)

        self._timepoints = get_time_index(y)
        n_timepoints = len(self._timepoints)

        self.window_length_ = check_window_length(
            self.window_length, n_timepoints=n_timepoints
        )

        if self.pooling == "local":
            if pd_format is True and isinstance(y, pd.MultiIndex):
                warn(
                    "Pooling is by default 'local', which"
                    + " means that separate models will be fit at the level of"
                    + " each instance. If you wish to fit a single model to"
                    + " all instances, please specify pooling = 'global'.",
                    obj=self,
                )
        if self.transformers is not None:
            self.transformers_ = clone(self.transformers)

        if self.transformers is None and self.pooling == "global":
            kwargs = {
                "lag_feature": {
                    "lag": list(range(1, self.window_length + 1)),
                }
            }
            self.transformers_ = [WindowSummarizer(**kwargs, n_jobs=1)]

        if self.window_length is None:
            trafo = self.transformers_
            fit_trafo = [i.fit(y) for i in trafo]
            ts = [i.truncate_start for i in fit_trafo if hasattr(i, "truncate_start")]

            if len(ts) > 0:
                self.window_length_ = max(ts)
            else:
                raise ValueError(
                    "Reduce must either have window length as argument"
                    + "or needs to have it passed by transformer via"
                    + "truncate_start"
                )

            if self.transformers_ is not None and n_timepoints < max(ts):
                raise ValueError(
                    "Not sufficient observations to calculate transformations"
                    + "Please reduce window length / window lagging to match"
                    + "observation size"
                )

        yt, Xt = self._transform(y, X)

        # Make sure yt is 1d array to avoid DataConversion warning from scikit-learn.
        if self.transformers_ is not None:
            yt = yt.to_numpy().ravel()
        else:
            yt = yt.ravel()

        self.estimator_ = clone(self.estimator)
        self.estimator_.fit(Xt, yt)
        return self

    def _predict_last_window(self, fh, X=None, **kwargs):
        """.

        In recursive reduction, iteration must be done over the
        entire forecasting horizon. Specifically, when transformers are
        applied to y that generate features in X, forecasting must be done step by
        step to integrate the latest prediction of for the new set of features in
        X derived from that y.

        Parameters
        ----------
        fh : int, list, np.array or ForecastingHorizon
            Forecasting horizon
        X : pd.DataFrame, optional (default=None)
            Exogenous time series
        return_pred_int : bool
        alpha : float or array-like

        Returns
        -------
        y_return = pd.Series or pd.DataFrame
        """
        if self._X is not None and X is None:
            raise ValueError(
                "`X` must be passed to `predict` if `X` is given in `fit`."
            )

        # Get last window of available data.
        # If we cannot generate a prediction from the available data, return nan.

        if self.pooling == "global":
            y_last, X_last = self._get_shifted_window(X_update=X)
            ys = np.array(y_last)
            if not np.sum(np.isnan(ys)) == 0 and np.sum(np.isinf(ys)) == 0:
                return self._predict_nan(fh)
        else:
            y_last, X_last = self._get_last_window()
            if not self._is_predictable(y_last):
                return self._predict_nan(fh)

        if self.pooling == "global":
            fh_max = fh.to_relative(self.cutoff)[-1]
            relative = pd.Index(list(map(int, range(1, fh_max + 1))))
            index_range = _index_range(relative, self.cutoff)
            if isinstance(self.cutoff, pd.DatetimeIndex):
                if self.cutoff.tzinfo is not None:
                    index_range = index_range.tz_localize(self.cutoff.tzinfo)

            y_pred = _create_fcst_df(index_range, self._y)

            for i in range(fh_max):
                # Generate predictions.
                y_pred_vector = self.estimator_.predict(X_last)
                y_pred_curr = _create_fcst_df(
                    [index_range[i]], self._y, fill=y_pred_vector
                )
                y_pred.update(y_pred_curr)

                # # Update last window with previous prediction.
                if i + 1 != fh_max:
                    y_last, X_last = self._get_shifted_window(
                        y_update=y_pred, X_update=X, shift=i + 1
                    )
        else:
            # Pre-allocate arrays.
            if X is None:
                n_columns = 1
            else:
                n_columns = X.shape[1] + 1
            window_length = self.window_length_
            fh_max = fh.to_relative(self.cutoff)[-1]

            y_pred = np.zeros(fh_max)

            # Array with input data for prediction.
            last = np.zeros((1, n_columns, window_length + fh_max))

            # Fill pre-allocated arrays with available data.
            last[:, 0, :window_length] = y_last
            if X is not None:
                X_to_use = np.concatenate(
                    [X_last.T, X.iloc[-(last.shape[2] - window_length) :, :].T], axis=1
                )
                if X_to_use.shape[1] < window_length + fh_max:
                    X_to_use = np.pad(
                        X_to_use,
                        ((0, 0), (0, window_length + fh_max - X_to_use.shape[1])),
                        "edge",
                    )
                elif X_to_use.shape[1] > window_length + fh_max:
                    X_to_use = X_to_use[:, : window_length + fh_max]
                # else X_to_use.shape[1] == window_length + fh_max
                # and there are no additional steps to take
                last[:, 1:] = X_to_use

            # Recursively generate predictions by iterating over forecasting horizon.
            for i in range(fh_max):
                # Slice prediction window.
                X_pred = last[:, :, i : window_length + i]

                # Reshape data into tabular array.
                if self._estimator_scitype == "tabular-regressor":
                    X_pred = X_pred.reshape(1, -1)

                # Generate predictions.
                y_pred[i] = self.estimator_.predict(X_pred)[0]

                # Update last window with previous prediction.
                last[:, 0, window_length + i] = y_pred[i]

        # While the recursive strategy requires to generate predictions for all steps
        # until the furthest step in the forecasting horizon, we only return the
        # requested ones.
        fh_idx = fh.to_indexer(self.cutoff)

        if isinstance(self._y.index, pd.MultiIndex):
            yi_grp = self._y.index.names[0:-1]
            y_return = y_pred.groupby(yi_grp, as_index=False).nth(fh_idx.to_list())
        elif isinstance(y_pred, pd.Series) or isinstance(y_pred, pd.DataFrame):
            y_return = y_pred.iloc[fh_idx]
            if hasattr(y_return.index, "freq"):
                if y_return.index.freq != y_pred.index.freq:
                    y_return.index.freq = None
        else:
            y_return = y_pred[fh_idx]

        return y_return


class _DirRecReducer(_Reducer):
    strategy = "dirrec"
    _tags = {
        "requires-fh-in-fit": True,  # is the forecasting horizon required in fit?
        "ignores-exogeneous-X": True,
    }

    def _transform(self, y, X=None):
        # Note that the transform for dirrec is the same as in the direct
        # strategy.
        fh = self.fh.to_relative(self.cutoff)
        return _sliding_window_transform(
            y,
            window_length=self.window_length,
            fh=fh,
            X=X,
            scitype=self._estimator_scitype,
        )

    def _fit(self, y, X, fh):
        """Fit to training data.

        Parameters
        ----------
        y : pd.Series
            Target time series to which to fit the forecaster.
        X : pd.DataFrame, optional (default=None)
            Exogenous variables are ignored
        fh : int, list or np.array, optional (default=None)
             The forecasters horizon with the steps ahead to to predict.

        Returns
        -------
        self : Estimator
            An fitted instance of self.
        """
        # todo: logic for X below is broken. Escape X until fixed.
        if X is not None:
            X = None

        self.window_length_ = check_window_length(
            self.window_length, n_timepoints=len(y)
        )

        # Transform the data using sliding-window.
        yt, Xt = self._transform(y, X)

        # We cast the 2d tabular array into a 3d panel array to handle the data
        # consistently for the reduction to tabular and time-series regression.
        if self._estimator_scitype == "tabular-regressor":
            Xt = np.expand_dims(Xt, axis=1)

        # This only works without exogenous variables. To support exogenous
        # variables, we need additional values for X to fill the array
        # appropriately.
        X_full = np.concatenate([Xt, np.expand_dims(yt, axis=1)], axis=2)

        self.estimators_ = []
        n_timepoints = Xt.shape[2]

        for i in range(len(self.fh)):
            estimator = clone(self.estimator)

            # Slice data using expanding window.
            X_fit = X_full[:, :, : n_timepoints + i]

            # Convert to 2d tabular array for reduction to tabular regression.
            if self._estimator_scitype == "tabular-regressor":
                X_fit = X_fit.reshape(X_fit.shape[0], -1)

            estimator.fit(X_fit, yt[:, i])
            self.estimators_.append(estimator)
        return self

    def _predict_last_window(self, fh, X=None, **kwargs):
        """Fit to training data.

        Parameters
        ----------
        fh : int, list, np.array or ForecastingHorizon
            Forecasting horizon
        X : pd.DataFrame, optional (default=None)
            Exogenous time series

        Returns
        -------
        y_pred = pd.Series or pd.DataFrame
        """
        # Exogenous variables are not yet support for the dirrec strategy.
        # todo: implement this. For now, we escape.
        if X is not None:
            X = None

        # Get last window of available data.
        y_last, X_last = self._get_last_window()
        if not self._is_predictable(y_last):
            return self._predict_nan(fh)

        window_length = self.window_length_

        # Pre-allocated arrays.
        # We set `n_columns` here to 1, because exogenous variables
        # are not yet supported.
        n_columns = 1
        X_full = np.zeros((1, n_columns, window_length + len(self.fh)))
        X_full[:, 0, :window_length] = y_last

        y_pred = np.zeros(len(fh))

        for i in range(len(self.fh)):
            # Slice data using expanding window.
            X_pred = X_full[:, :, : window_length + i]

            if self._estimator_scitype == "tabular-regressor":
                X_pred = X_pred.reshape(1, -1)

            y_pred[i] = self.estimators_[i].predict(X_pred)[0]

            # Update the last window with previously predicted value.
            X_full[:, :, window_length + i] = y_pred[i]

        return y_pred


[文档]class DirectTabularRegressionForecaster(_DirectReducer): """Direct reduction from forecasting to tabular regression. For the direct reduction strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon. Parameters ---------- estimator : Estimator A tabular regression estimator as provided by scikit-learn. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix. """ def __init__( self, estimator, window_length=10, transformers=None, pooling="local", windows_identical=True, ): super(_DirectReducer, self).__init__( estimator=estimator, window_length=window_length, transformers=transformers ) self.pooling = pooling self.windows_identical = windows_identical if pooling == "local": mtypes_y = "pd.Series" mtypes_x = "pd.DataFrame" elif pooling == "global": mtypes_y = ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"] mtypes_x = mtypes_y elif pooling == "panel": mtypes_y = ["pd.DataFrame", "pd-multiindex"] mtypes_x = mtypes_y else: raise ValueError( "pooling in DirectReductionForecaster must be one of" ' "local", "global", "panel", ' f"but found {pooling}" ) self.set_tags(**{"X_inner_mtype": mtypes_x}) self.set_tags(**{"y_inner_mtype": mtypes_y}) _estimator_scitype = "tabular-regressor"
[文档]class MultioutputTabularRegressionForecaster(_MultioutputReducer): """Multioutput reduction from forecasting to tabular regression. For the multioutput strategy, a single estimator capable of handling multioutput targets is fitted to all the future steps in the forecasting horizon. Parameters ---------- estimator : Estimator A tabular regression estimator as provided by scikit-learn. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix. """ _estimator_scitype = "tabular-regressor"
[文档]class RecursiveTabularRegressionForecaster(_RecursiveReducer): """Recursive reduction from forecasting to tabular regression. For the recursive strategy, a single estimator is fit for a one-step-ahead forecasting horizon and then called iteratively to predict multiple steps ahead. Parameters ---------- estimator : Estimator A tabular regression estimator as provided by scikit-learn. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix. transformers: list of transformers (default = None) A suitable list of transformers that allows for using an en-bloc approach with make_reduction. This means that instead of using the raw past observations of y across the window length, suitable features will be generated directly from the past raw observations. Currently only supports WindowSummarizer (or a list of WindowSummarizers) to generate features e.g. the mean of the past 7 observations. pooling: str {"local", "global"}, optional Specifies whether separate models will be fit at the level of each instance (local) of if you wish to fit a single model to all instances ("global"). """ _tags = { "requires-fh-in-fit": False, # is the forecasting horizon required in fit? } def __init__( self, estimator, window_length=10, transformers=None, pooling="local", ): super(_RecursiveReducer, self).__init__( estimator=estimator, window_length=window_length, transformers=transformers ) self.pooling = pooling if pooling == "local": mtypes_y = "pd.Series" mtypes_x = "pd.DataFrame" elif pooling == "global": mtypes_y = ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"] mtypes_x = mtypes_y elif pooling == "panel": mtypes_y = ["pd.DataFrame", "pd-multiindex"] mtypes_x = mtypes_y else: raise ValueError( "pooling in DirectReductionForecaster must be one of" ' "local", "global", "panel", ' f"but found {pooling}" ) self.set_tags(**{"X_inner_mtype": mtypes_x}) self.set_tags(**{"y_inner_mtype": mtypes_y}) _estimator_scitype = "tabular-regressor"
[文档]class DirRecTabularRegressionForecaster(_DirRecReducer): """Dir-rec reduction from forecasting to tabular regression. For the hybrid dir-rec strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon and then the previous forecasting horizon is added as an input for training the next forecaster, following the recursive strategy. Parameters ---------- estimator : sklearn estimator object Tabular regressor. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix """ _estimator_scitype = "tabular-regressor"
[文档]class DirectTimeSeriesRegressionForecaster(_DirectReducer): """Direct reduction from forecasting to time-series regression. For the direct reduction strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon. Parameters ---------- estimator : Estimator A time-series regression estimator as provided by sktime. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix. """ _estimator_scitype = "time-series-regressor"
[文档]class MultioutputTimeSeriesRegressionForecaster(_MultioutputReducer): """Multioutput reduction from forecasting to time series regression. For the multioutput strategy, a single estimator capable of handling multioutput targets is fitted to all the future steps in the forecasting horizon. Parameters ---------- estimator : Estimator A time-series regression estimator as provided by sktime. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix. """ _estimator_scitype = "time-series-regressor"
[文档]class RecursiveTimeSeriesRegressionForecaster(_RecursiveReducer): """Recursive reduction from forecasting to time series regression. For the recursive strategy, a single estimator is fit for a one-step-ahead forecasting horizon and then called iteratively to predict multiple steps ahead. Parameters ---------- estimator : Estimator A time-series regression estimator as provided by sktime. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix. """ _tags = { "requires-fh-in-fit": False, # is the forecasting horizon required in fit? } _estimator_scitype = "time-series-regressor"
[文档]class DirRecTimeSeriesRegressionForecaster(_DirRecReducer): """Dir-rec reduction from forecasting to time-series regression. For the hybrid dir-rec strategy, a separate forecaster is fitted for each step ahead of the forecasting horizon and then the previous forecasting horizon is added as an input for training the next forecaster, following the recursive strategy. Parameters ---------- estimator : sktime estimator object Time-series regressor. window_length : int, optional (default=10) The length of the sliding window used to transform the series into a tabular matrix """ _estimator_scitype = "time-series-regressor"
[文档]def make_reduction( estimator, strategy="recursive", window_length=10, scitype="infer", transformers=None, pooling="local", windows_identical=True, ): r"""Make forecaster based on reduction to tabular or time-series regression. During fitting, a sliding-window approach is used to first transform the time series into tabular or panel data, which is then used to fit a tabular or time-series regression estimator. During prediction, the last available data is used as input to the fitted regression estimator to generate forecasts. Please see below a graphical representation of the make_reduction logic using the following symbols: - ``y`` = forecast target. - ``x`` = past values of y that are used as features (X) to forecast y - ``*`` = observations, past or future, neither part of window nor forecast. Assume we have the following training data (14 observations):: |----------------------------| | * * * * * * * * * * * * * *| |----------------------------| And want to forecast with ``window_length = 9`` and ``fh = [2, 4]``. By construction, a recursive reducer always targets the first data point after the window, irrespective of the forecasting horizons requested. In the example the following 5 windows are created:: |----------------------------| | x x x x x x x x x y * * * *| | * x x x x x x x x x y * * *| | * * x x x x x x x x x y * *| | * * * x x x x x x x x x y *| | * * * * x x x x x x x x x y| |----------------------------| Direct Reducers will create multiple models, one for each forecasting horizon. With the argument ``windows_identical = True`` (default) the windows used to train the model are defined by the maximum forecasting horizon. Only two complete windows can be defined in this example ``fh = 4`` (maximum of ``fh = [2, 4]``):: |----------------------------| | x x x x x x x x x * * * y *| | * x x x x x x x x x * * * y| |----------------------------| All other forecasting horizons will also use those two (maximal) windows. ``fh = 2``:: |----------------------------| | x x x x x x x x x * y * * *| | * x x x x x x x x x * y * *| |----------------------------| With ``windows_identical = False`` we drop the requirement to use the same windows for each of the direct models, so more windows can be created for horizons other than the maximum forecasting horizon. ``fh = 2``:: |----------------------------| | x x x x x x x x x * y * * *| | * x x x x x x x x x * y * *| | * * x x x x x x x x x * y *| | * * * x x x x x x x x x * y| |----------------------------| ``fh = 4``:: |----------------------------| | x x x x x x x x x * * * y *| | * x x x x x x x x x * * * y| |----------------------------| Use ``windows_identical = True`` if you want to compare the forecasting performance across different horizons, since all models trained will use the same windows. Use ``windows_identical = False`` if you want to have the highest forecasting accuracy for each forecasting horizon. Parameters ---------- estimator : an estimator instance, can be: * scikit-learn regressor or interface compatible * sktime time series regressor * skpro tabular probabilistic supervised regressor, only for direct reduction this will result in a probabilistic forecaster strategy : str, optional (default="recursive") The strategy to generate forecasts. Must be one of "direct", "recursive" or "multioutput". window_length : int, optional (default=10) Window length used in sliding window transformation. scitype : str, optional (default="infer") Legacy argument for downwards compatibility, should not be used. ``make_reduction`` will automatically infer the correct type of ``estimator``. This internal inference can be force-overridden by the ``scitype`` argument. Must be one of "infer", "tabular-regressor" or "time-series-regressor". If the scitype cannot be inferred, this is a bug and should be reported. transformers: list of transformers (default = None) A suitable list of transformers that allows for using an en-bloc approach with make_reduction. This means that instead of using the raw past observations of y across the window length, suitable features will be generated directly from the past raw observations. Currently only supports WindowSummarizer (or a list of WindowSummarizers) to generate features e.g. the mean of the past 7 observations. Currently only works for RecursiveTimeSeriesRegressionForecaster. pooling: str {"local", "global"}, optional Specifies whether separate models will be fit at the level of each instance (local) of if you wish to fit a single model to all instances ("global"). Currently only works for RecursiveTimeSeriesRegressionForecaster. windows_identical: bool, (default = True) Direct forecasting only. Specifies whether all direct models use the same X windows from y (True: Number of windows = total observations + 1 - window_length - maximum forecasting horizon) or a different number of X windows depending on the forecasting horizon (False: Number of windows = total observations + 1 - window_length - forecasting horizon). See pictionary below for more information. Returns ------- forecaster : an sktime forecaster object the reduction forecaster, wrapping ``estimator`` class is determined by the ``strategy`` argument and type of ``estimator``. Examples -------- >>> from sktime.forecasting.compose import make_reduction >>> from sktime.datasets import load_airline >>> from sklearn.ensemble import GradientBoostingRegressor >>> y = load_airline() >>> regressor = GradientBoostingRegressor() >>> forecaster = make_reduction(regressor, window_length=15, strategy="recursive") >>> forecaster.fit(y) RecursiveTabularRegressionForecaster(...) >>> y_pred = forecaster.predict(fh=[1,2,3]) References ---------- .. [1] Bontempi, Gianluca & Ben Taieb, Souhaib & Le Borgne, Yann-Aël. (2013). Machine Learning Strategies for Time Series Forecasting. """ # We provide this function as a factory method for user convenience. strategy = _check_strategy(strategy) scitype = _check_scitype(scitype) if scitype == "infer": scitype = _infer_scitype(estimator) Forecaster = _get_forecaster(scitype, strategy) dispatch_params = { "estimator": estimator, "window_length": window_length, "transformers": transformers, "pooling": pooling, "windows_identical": windows_identical, } return construct_dispatch(Forecaster, dispatch_params)
def _check_scitype(scitype): valid_scitypes = ("infer", "tabular-regressor", "time-series-regressor") if scitype not in valid_scitypes: raise ValueError( f"Invalid `scitype`. `scitype` must be one of:" f" {valid_scitypes}, but found: {scitype}." ) return scitype def _infer_scitype(estimator): """Infer scitype from estimator. Returns ------- scitype : str The inferred scitype of the estimator. * if sklearn estimator, returns tabular-regressor etc, one of the returns of sklearn_scitype prefixed with "tabular-". * if sktime/skpro or skbase estimator, returns the scitype of the estimator as found in the object_type tag. * if none of the above applies, returns "tabular-regressor" as fallback default. """ if is_sklearn_estimator(estimator): return f"tabular-{sklearn_scitype(estimator)}" else: if is_scitype(estimator, ["object", "estimator"]): return "tabular-regressor" if is_scitype(estimator, "regressor"): return "time-series-regressor" else: return scitype(estimator, raise_on_unknown=False) def _check_strategy(strategy): valid_strategies = ("direct", "recursive", "multioutput", "dirrec") if strategy not in valid_strategies: raise ValueError( f"Invalid `strategy`. `strategy` must be one of :" f" {valid_strategies}, but found: {strategy}." ) return strategy def _get_forecaster(scitype, strategy): """Select forecaster for a given scientific type and reduction strategy.""" registry = { "tabular-regressor": { "direct": DirectTabularRegressionForecaster, "recursive": RecursiveTabularRegressionForecaster, "multioutput": MultioutputTabularRegressionForecaster, "dirrec": DirRecTabularRegressionForecaster, }, "time-series-regressor": { "direct": DirectTimeSeriesRegressionForecaster, "recursive": RecursiveTimeSeriesRegressionForecaster, "multioutput": MultioutputTimeSeriesRegressionForecaster, "dirrec": DirRecTimeSeriesRegressionForecaster, }, "regressor_proba": {"direct": DirectTabularRegressionForecaster}, } if scitype not in registry: raise ValueError( "Error in make_reduction, no reduction strategies defined for " f"specified or inferred scitype of estimator: {scitype}. " f"Valid scitypes are: {list(registry.keys())}." ) if strategy not in registry[scitype]: raise ValueError( f"Error in make_reduction, strategy {strategy} not defined for " f"specified or inferred scitype {scitype}. " f"Valid strategies are: {list(registry[scitype].keys())}." ) return registry[scitype][strategy] def _cut_df(X, n_obs=1, type="tail"): """Cut input at tail or head, supports grouping.""" if n_obs == 0: return X.copy() if isinstance(X.index, pd.MultiIndex): levels = list(range(X.index.nlevels - 1)) if type == "tail": X = X.groupby(level=levels, as_index=False).tail(n_obs) elif type == "head": X = X.groupby(level=levels, as_index=False).head(n_obs) else: if type == "tail": X = X.tail(n_obs) elif type == "head": X = X.head(n_obs) return X def _create_fcst_df(target_date, origin_df, fill=None): """Create an empty multiindex dataframe from origin dataframe. In recursive forecasting, a new dataframe needs to be created that collects all forecasting steps (even for forecasting horizons other than those of interests). For example for fh =[1,2,12] we need the whole forecasting horizons from 1 to 12. Parameters ---------- target_date : a list of dates this will be correspond to the new timepoints index to be created in the forecasting dataframe origin_df : a pandas Series or Dataframe the origin_df corresponds to the dataframe with the historic data. Useful information inferred from that dataframe is the index of the historic dataframe as well as the names of the original columns and the type of the object (dataframe or series) fill : a numpy.ndarray (optional) Corresponds to a numpy array of values that is used to fill up the dataframe. Useful when forecasts are returned from a forecasting models that discards the hierarchical structure of the input pandas dataframe Returns ------- A pandas dataframe or series """ if not isinstance(target_date, ForecastingHorizon): ix = pd.Index(target_date) fh = ForecastingHorizon(ix, is_relative=False) else: fh = target_date.to_absolute() index = fh.get_expected_pred_idx(origin_df) if isinstance(origin_df, pd.Series): columns = [origin_df.name] else: columns = origin_df.columns.to_list() if fill is None: values = 0 else: values = fill res = pd.DataFrame(values, index=index, columns=columns, dtype="float64") if isinstance(origin_df, pd.Series) and not isinstance(index, pd.MultiIndex): res = res.iloc[:, 0] res.name = origin_df.name return res def slice_at_ix(df, ix): """Slice pd.DataFrame at one index value, valid for simple Index and MultiIndex. Parameters ---------- df : pd.DataFrame ix : pandas compatible index value, or iterable of index values (incl pd.Index) Returns ------- pd.DataFrame, row(s) of df, sliced at last (-1 st) level of df being equal to ix all index levels are retained in the return, none are dropped CAVEAT: index is sorted by last (-1 st) level if ix is iterable """ if isinstance(ix, (list, pd.Index, ForecastingHorizon)): return pd.concat([slice_at_ix(df, x) for x in ix]) if isinstance(df.index, pd.MultiIndex): return df.xs(ix, level=-1, axis=0, drop_level=False) else: return df.loc[[ix]] def _get_notna_idx(df): """Get sub-index of df that contains rows without nans. Parameters ---------- df : pd.DataFrame Returns ------- df_notna_idx : pd.Index sub-set of df.index that contains rows of df without nans index is in same order as of df """ df_notna_bool = df.notnull().all(axis=1) df_notna_idx = df.index[df_notna_bool] return df_notna_idx class _ReducerMixin: """Common utilities for reducers.""" def _get_expected_pred_idx(self, fh): """Construct DataFrame Index expected in y_pred, return of _predict. Parameters ---------- fh : ForecastingHorizon, fh of self; or, iterable coercible to pd.Index Returns ------- fh_idx : pd.Index, expected index of y_pred returned by _predict CAVEAT: sorted by index level -1, since reduction is applied by fh """ if isinstance(fh, ForecastingHorizon): fh_idx = pd.Index(fh.to_absolute_index(self.cutoff)) else: fh_idx = pd.Index(fh) y_index = self._y.index if isinstance(y_index, pd.MultiIndex): y_inst_idx = y_index.droplevel(-1).unique() if isinstance(y_inst_idx, pd.MultiIndex): fh_idx = pd.Index([x + (y,) for x in y_inst_idx for y in fh_idx]) else: fh_idx = pd.Index([(x, y) for x in y_inst_idx for y in fh_idx]) if hasattr(y_index, "names") and y_index.names is not None: fh_idx.names = y_index.names return fh_idx
[文档]class DirectReductionForecaster(BaseForecaster, _ReducerMixin): """Direct reduction forecaster, incl single-output, multi-output, exogeneous Dir. Implements direct reduction, of forecasting to tabular regression. For no ``X``, defaults to DirMO (direct multioutput) for ``X_treatment = "concurrent"``, and simple direct (direct single-output) for ``X_treatment = "shifted"``. Direct single-output with concurrent ``X`` behaviour can be configured by passing a single-output ``scikit-learn`` compatible transformer. Algorithm details: In ``fit``, given endogeneous time series ``y`` and possibly exogeneous ``X``: fits ``estimator`` to feature-label pairs as defined as follows. if `X_treatment = "concurrent": features = ``y(t)``, ``y(t-1)``, ..., ``y(t-window_size)``, if provided: ``X(t+h)`` labels = ``y(t+h)`` for ``h`` in the forecasting horizon ranging over all ``t`` where the above have been observed (are in the index) for each ``h`` in the forecasting horizon (separate estimator fitted per ``h``) if `X_treatment = "shifted": features = ``y(t)``, ``y(t-1)``, ..., ``y(t-window_size)``, if provided: ``X(t)`` labels = ``y(t+h_1)``, ..., ``y(t+h_k)`` for ``h_j`` in the forecasting horizon ranging over all ``t`` where the above have been observed (are in the index) estimator is fitted as a multi-output estimator (for all ``h_j`` simultaneously) In ``predict``, given possibly exogeneous ``X``, at cutoff time ``c``, if `X_treatment = "concurrent": applies fitted estimators' predict to feature = ``y(c)``, ``y(c-1)``, ..., ``y(c-window_size)``, if provided: ``X(c+h)`` to obtain a prediction for ``y(c+h)``, for each ``h`` in the forecasting horizon if `X_treatment = "shifted": applies fitted estimator's predict to features = ``y(c)``, ``y(c-1)``, ..., ``y(c-window_size)``, if provided: ``X(c)`` to obtain prediction for ``y(c+h_1)``, ..., ``y(c+h_k)`` for ``h_j`` in forec. horizon Parameters ---------- estimator : sklearn regressor, must be compatible with sklearn interface tabular regression algorithm used in reduction algorithm window_length : int, optional, default=10 window length used in the reduction algorithm transformers : currently not used X_treatment : str, optional, one of "concurrent" (default) or "shifted" determines the timestamps of X from which y(t+h) is predicted, for horizon h "concurrent": y(t+h) is predicted from lagged y, and X(t+h), for all h in fh in particular, if no y-lags are specified, y(t+h) is predicted from X(t) "shifted": y(t+h) is predicted from lagged y, and X(t), for all h in fh in particular, if no y-lags are specified, y(t+h) is predicted from X(t+h) impute : str or None, optional, method string passed to Imputer default="bfill", admissible strings are of Imputer.method parameter, see there if None, no imputation is done when applying Lag transformer to obtain inner X pooling : str, one of ["local", "global", "panel"], optional, default="local" level on which data are pooled to fit the supervised regression model "local" = unit/instance level, one reduced model per lowest hierarchy level "global" = top level, one reduced model overall, on pooled data ignoring levels "panel" = second lowest level, one reduced model per panel level (-2) if there are 2 or less levels, "global" and "panel" result in the same if there is only 1 level (single time series), all three settings agree windows_identical : bool, optional, default=False Specifies whether all direct models use the same number of observations or a different number of observations. * `True` : Uniform window of length (total observations - maximum forecasting horizon). Note: Currently, there are no missings arising from window length due to backwards imputation in `ReductionTransformer`. Without imputation, the window size corresponds to (total observations + 1 - window_length + maximum forecasting horizon). * `False` : Window size differs for each forecasting horizon. Window length corresponds to (total observations + 1 - window_length + forecasting horizon). """ _tags = { "authors": "fkiraly", "maintainers": "hliebert", "requires-fh-in-fit": True, # is the forecasting horizon required in fit? "ignores-exogeneous-X": False, "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"], "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"], } def __init__( self, estimator, window_length=10, transformers=None, X_treatment="concurrent", impute_method="bfill", pooling="local", windows_identical=False, ): self.window_length = window_length self.transformers = transformers self.transformers_ = None self.estimator = estimator self.X_treatment = X_treatment self.impute_method = impute_method self.pooling = pooling self.windows_identical = windows_identical self._lags = list(range(window_length)) super().__init__() if pooling == "local": mtypes = "pd.DataFrame" elif pooling == "global": mtypes = ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"] elif pooling == "panel": mtypes = ["pd.DataFrame", "pd-multiindex"] else: raise ValueError( "pooling in DirectReductionForecaster must be one of" ' "local", "global", "panel", ' f"but found {pooling}" ) self.set_tags(**{"X_inner_mtype": mtypes}) self.set_tags(**{"y_inner_mtype": mtypes}) # it seems that the sklearn tags are not fully reliable # see discussion in PR #3405 and issue #3402 # therefore this is commented out until sktime and sklearn are better aligned # self.set_tags(**{"handles-missing-data": estimator._get_tags()["allow_nan"]}) def _fit(self, y, X, fh): """Fit dispatcher based on X_treatment and windows_identical.""" # shifted X (future X unknown) and identical windows reduce to # multioutput regression, o/w fit multiple individual estimators if (self.X_treatment == "shifted") and (self.windows_identical is True): return self._fit_multioutput(y=y, X=X, fh=fh) else: return self._fit_multiple(y=y, X=X, fh=fh) def _predict(self, X=None, fh=None): """Predict dispatcher based on X_treatment and windows_identical.""" if self.X_treatment == "shifted": if self.windows_identical is True: return self._predict_multioutput(X=X, fh=fh) else: return self._predict_multiple(X=self._X, fh=fh) else: return self._predict_multiple(X=X, fh=fh) def _fit_multioutput(self, y, X=None, fh=None): """Fit to training data.""" from sktime.transformations.series.lag import Lag, ReducerTransform impute_method = self.impute_method lags = self._lags trafos = self.transformers # lagger_y_to_X_ will lag y to obtain the sklearn X lagger_y_to_X = ReducerTransform( lags=lags, transformers=trafos, impute_method=impute_method ) self.lagger_y_to_X_ = lagger_y_to_X # lagger_y_to_y_ will lag y to obtain the sklearn y fh_rel = fh.to_relative(self.cutoff) y_lags = list(fh_rel) y_lags = [-x for x in y_lags] lagger_y_to_y = Lag(lags=y_lags, index_out="original", keep_column_names=True) self.lagger_y_to_y_ = lagger_y_to_y yt = lagger_y_to_y.fit_transform(X=y) y_notna_idx = _get_notna_idx(yt) # we now check whether the set of full lags is empty # if yes, we set a flag, since we cannot fit the reducer # instead, later, we return a dummy prediction if len(y_notna_idx) == 0: self.empty_lags_ = True self.dummy_value_ = y.mean() return self else: self.empty_lags_ = False yt = yt.loc[y_notna_idx] Xt = lagger_y_to_X.fit_transform(X=y, y=X) Xt = Xt.loc[y_notna_idx] Xt = prep_skl_df(Xt) yt = prep_skl_df(yt) estimator = clone(self.estimator) if not estimator._get_tags()["multioutput"]: estimator = MultiOutputRegressor(estimator) estimator.fit(Xt, yt) self.estimator_ = estimator return self def _predict_multioutput(self, fh=None, X=None): """Predict core logic.""" y_cols = self._y.columns fh_idx = self._get_expected_pred_idx(fh=fh) if self.empty_lags_: ret = pd.DataFrame(index=fh_idx, columns=y_cols) for i in ret.index: ret.loc[i] = self.dummy_value_ return ret lagger_y_to_X = self.lagger_y_to_X_ Xt = lagger_y_to_X.transform(X=self._y, y=self._X) Xt_lastrow = slice_at_ix(Xt, self.cutoff) Xt_lastrow = prep_skl_df(Xt_lastrow) estimator = self.estimator_ # 2D numpy array with col index = (fh, var) and 1 row y_pred = estimator.predict(Xt_lastrow) y_pred = y_pred.reshape((len(fh_idx), len(y_cols))) y_pred = pd.DataFrame(y_pred, columns=y_cols, index=fh_idx) if isinstance(y_pred.index, pd.MultiIndex): y_pred = y_pred.sort_index() return y_pred def _fit_multiple(self, y, X=None, fh=None): """Fit to training data.""" from sktime.transformations.series.lag import Lag, ReducerTransform impute_method = self.impute_method X_treatment = self.X_treatment windows_identical = self.windows_identical # lagger_y_to_X_ will lag y to obtain the sklearn X lags = self._lags # lagger_y_to_y_ will lag y to obtain the sklearn y fh_rel = fh.to_relative(self.cutoff) y_lags = list(fh_rel) y_lags = [-x for x in y_lags] # lagging behaviour is per fh, so w initialize dicts # copied to self.lagger_y_to_X/y_, by reference lagger_y_to_y = dict() lagger_y_to_X = dict() self.lagger_y_to_y_ = lagger_y_to_y self.lagger_y_to_X_ = lagger_y_to_X self.estimators_ = [] for lag in y_lags: t = Lag(lags=lag, index_out="original", keep_column_names=True) lagger_y_to_y[lag] = t yt = lagger_y_to_y[lag].fit_transform(X=y) impute_method = self.impute_method lags = self._lags trafos = self.transformers # determine whether to use concurrent X (lead them) or shifted (0) X_lag = lag if X_treatment == "concurrent" else 0 # lagger_y_to_X_ will lag y to obtain the sklearn X # also updates self.lagger_y_to_X_ by reference lagger_y_to_X[lag] = ReducerTransform( lags=lags, shifted_vars_lag=X_lag, transformers=trafos, impute_method=impute_method, ) Xtt = lagger_y_to_X[lag].fit_transform(X=y, y=X) Xtt_notna_idx = _get_notna_idx(Xtt) yt_notna_idx = _get_notna_idx(yt) notna_idx = Xtt_notna_idx.intersection(yt_notna_idx) yt = yt.loc[notna_idx] Xtt = Xtt.loc[notna_idx] if windows_identical: # determine offset for uniform window length # convert to abs values to account for in-sample prediction offset = np.abs(fh_rel.to_numpy()).max() - abs(lag) yt = yt[offset:] Xtt = Xtt[offset:] Xtt = prep_skl_df(Xtt) yt = prep_skl_df(yt) # we now check whether the set of full lags is empty # if yes, we set a flag, since we cannot fit the reducer # instead, later, we return a dummy prediction if len(notna_idx) == 0: self.estimators_.append(y.mean()) else: estimator = clone(self.estimator) estimator.fit(Xtt, yt) self.estimators_.append(estimator) return self def _predict_multiple(self, X=None, fh=None): """Fit to training data.""" from sktime.transformations.series.lag import Lag if X is not None and self._X is not None: X_pool = X.combine_first(self._X) elif X is None and self._X is not None: X_pool = self._X else: X_pool = X fh_idx = self._get_expected_pred_idx(fh=fh) y_cols = self._y.columns lagger_y_to_X = self.lagger_y_to_X_ fh_rel = fh.to_relative(self.cutoff) fh_abs = fh.to_absolute(self.cutoff) y_lags = list(fh_rel) y_abs = list(fh_abs) y_pred_list = [] for i, lag in enumerate(y_lags): predict_idx = y_abs[i] lag_plus = Lag(lag, index_out="extend", keep_column_names=True) Xt = lagger_y_to_X[-lag].transform(X=self._y, y=X_pool) Xtt = lag_plus.fit_transform(Xt) Xtt_predrow = slice_at_ix(Xtt, predict_idx) Xtt_predrow = prep_skl_df(Xtt_predrow) estimator = self.estimators_[i] # if = no training indices in _fit, fill in y training mean if isinstance(estimator, pd.Series): y_pred_i = pd.DataFrame(index=[0], columns=y_cols) y_pred_i.iloc[0] = estimator # otherwise proceed as per direct reduction algorithm else: y_pred_i = estimator.predict(Xtt_predrow) # 2D numpy array with col index = (var) and 1 row y_pred_list.append(y_pred_i) y_pred = np.concatenate(y_pred_list) y_pred = pd.DataFrame(y_pred, columns=y_cols, index=fh_idx) if isinstance(y_pred.index, pd.MultiIndex): y_pred = y_pred.sort_index() return y_pred
[文档] @classmethod def get_test_params(cls, parameter_set="default"): """Return testing parameter settings for the estimator. Parameters ---------- parameter_set : str, default="default" Name of the set of test parameters to return, for use in tests. If no special parameters are defined for a value, will return ``"default"`` set. Returns ------- params : dict or list of dict, default = {} Parameters to create testing instances of the class Each dict are parameters to construct an "interesting" test instance, i.e., ``MyClass(**params)`` or ``MyClass(**params[i])`` creates a valid test instance. ``create_test_instance`` uses the first (or only) dictionary in ``params`` """ from sklearn.linear_model import LinearRegression est = LinearRegression() params1 = { "estimator": est, "window_length": 3, "X_treatment": "shifted", "pooling": "global", # all internal mtypes are tested across scenarios "windows_identical": True, } params2 = { "estimator": est, "window_length": 3, "X_treatment": "concurrent", "pooling": "global", "windows_identical": True, } params3 = { "estimator": est, "window_length": 3, "X_treatment": "shifted", "pooling": "global", # all internal mtypes are tested across scenarios "windows_identical": False, } params4 = { "estimator": est, "window_length": 3, "X_treatment": "concurrent", "pooling": "global", "windows_identical": False, } params5 = {"estimator": est, "window_length": 0} params = [params1, params2, params3, params4, params5] # this fails because catboost is not sklearn compatible # and fails set_params contracts already in sklearn; # so it also fails them in sktime... # left here for future reference, e.g., test for non-compliant estimators # # if _check_soft_dependencies("catboost", severity="none"): # from catboost import CatBoostRegressor # # est = CatBoostRegressor(learning_rate=1, depth=6, loss_function="RMSE") # params6 = {"estimator": est, "window_length": 3} # params.append(params6) return params
class RecursiveReductionForecaster(BaseForecaster, _ReducerMixin): """Recursive reduction forecaster, incl exogeneous Rec. Implements recursive reduction, of forecasting to tabular regression. Algorithm details: In ``fit``, given endogeneous time series ``y`` and possibly exogeneous ``X``: fits ``estimator`` to feature-label pairs as defined as follows. features = ``y(t)``, ``y(t-1)``, ..., ``y(t-window_size)``, if provided: ``X(t+1)`` labels = ``y(t+1)`` ranging over all ``t`` where the above have been observed (are in the index) In ``predict``, given possibly exogeneous ``X``, at cutoff time ``c``, applies fitted estimators' predict to feature = ``y(c)``, ``y(c-1)``, ..., ``y(c-window_size)``, if provided: ``X(c+1)`` to obtain a prediction for ``y(c+1)``. If a given ``y(t)`` has not been observed, it is replaced by a prediction obtained in the same way - done repeatedly until all predictions are obtained. Out-of-sample, this results in the "recursive" behaviour, where predictions at time points c+1, c+2, etc, are obtained iteratively. In-sample, predictions are obtained in a single step, with potential missing values obtained via the ``impute`` strategy chosen. Parameters ---------- estimator : sklearn regressor, must be compatible with sklearn interface tabular regression algorithm used in reduction algorithm window_length : int, optional, default=10 window length used in the reduction algorithm impute : str or None, optional, method string passed to Imputer default="bfill", admissible strings are of Imputer.method parameter, see there if None, no imputation is done when applying Lag transformer to obtain inner X pooling : str, one of ["local", "global", "panel"], optional, default="local" level on which data are pooled to fit the supervised regression model "local" = unit/instance level, one reduced model per lowest hierarchy level "global" = top level, one reduced model overall, on pooled data ignoring levels "panel" = second lowest level, one reduced model per panel level (-2) if there are 2 or less levels, "global" and "panel" result in the same if there is only 1 level (single time series), all three settings agree """ _tags = { "authors": "fkiraly", "requires-fh-in-fit": False, # is the forecasting horizon required in fit? "ignores-exogeneous-X": False, "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"], "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"], } def __init__( self, estimator, window_length=10, impute_method="bfill", pooling="local", ): self.window_length = window_length self.estimator = estimator self.impute_method = impute_method self.pooling = pooling self._lags = list(range(window_length)) super().__init__() warn( "RecursiveReductionForecaster is experimental, and interfaces may change. " "user feedback is appreciated in issue #3224 here: " "https://github.com/alan-turing-institute/sktime/issues/3224" ) if pooling == "local": mtypes = "pd.DataFrame" elif pooling == "global": mtypes = ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"] elif pooling == "panel": mtypes = ["pd.DataFrame", "pd-multiindex"] else: raise ValueError( "pooling in DirectReductionForecaster must be one of" ' "local", "global", "panel", ' f"but found {pooling}" ) self.set_tags(**{"X_inner_mtype": mtypes}) self.set_tags(**{"y_inner_mtype": mtypes}) def _fit(self, y, X, fh): """Fit forecaster to training data. private _fit containing the core logic, called from fit Parameters ---------- y : pd.DataFrame mtype is pd.DataFrame, pd-multiindex, or pd_multiindex_hier Time series to which to fit the forecaster. fh : guaranteed to be ForecastingHorizon or None, optional (default=None) The forecasting horizon with the steps ahead to to predict. Required (non-optional) here if self.get_tag("requires-fh-in-fit")==True Otherwise, if not passed in _fit, guaranteed to be passed in _predict X : pd.DataFrame optional (default=None) mtype is pd.DataFrame, pd-multiindex, or pd_multiindex_hier Exogeneous time series to fit to. Returns ------- self : reference to self """ # todo: very similar to _fit_concurrent of DirectReductionForecaster - refactor? from sktime.transformations.series.impute import Imputer from sktime.transformations.series.lag import Lag impute_method = self.impute_method # lagger_y_to_X_ will lag y to obtain the sklearn X lags = self._lags lagger_y_to_X = Lag(lags=lags, index_out="extend") if impute_method is not None: lagger_y_to_X = lagger_y_to_X * Imputer(method=impute_method) self.lagger_y_to_X_ = lagger_y_to_X Xt = lagger_y_to_X.fit_transform(y) # lag is 1, since we want to do recursive forecasting with 1 step ahead lag_plus = Lag(lags=1, index_out="extend") Xtt = lag_plus.fit_transform(Xt) Xtt_notna_idx = _get_notna_idx(Xtt) notna_idx = Xtt_notna_idx.intersection(y.index) yt = y.loc[notna_idx] Xtt = Xtt.loc[notna_idx] # we now check whether the set of full lags is empty # if yes, we set a flag, since we cannot fit the reducer # instead, later, we return a dummy prediction if len(notna_idx) == 0: self.estimator_ = y.mean() else: if X is not None: Xtt = pd.concat([X.loc[notna_idx], Xtt], axis=1) Xtt = prep_skl_df(Xtt) yt = prep_skl_df(yt) estimator = clone(self.estimator) estimator.fit(Xtt, yt) self.estimator_ = estimator return self def _predict(self, X=None, fh=None): """Forecast time series at future horizon. private _predict containing the core logic, called from predict Parameters ---------- fh : guaranteed to be ForecastingHorizon or None, optional (default=None) The forecasting horizon with the steps ahead to to predict. If not passed in _fit, guaranteed to be passed here X : pd.DataFrame, optional (default=None) mtype is pd.DataFrame, pd-multiindex, or pd_multiindex_hier Exogeneous time series for the forecast Returns ------- y_pred : pd.DataFrame, same type as y in _fit Point predictions """ if X is not None and self._X is not None: X_pool = X.combine_first(self._X) elif X is None and self._X is not None: X_pool = self._X else: X_pool = X fh_oos = fh.to_out_of_sample(self.cutoff) fh_ins = fh.to_in_sample(self.cutoff) if len(fh_oos) == 0: y_pred = self._predict_in_sample(X_pool, fh_ins) elif len(fh_ins) == 0: y_pred = self._predict_out_of_sample(X_pool, fh_oos) else: y_pred_ins = self._predict_in_sample(X_pool, fh_ins) y_pred_oos = self._predict_out_of_sample(X_pool, fh_oos) y_pred = pd.concat([y_pred_ins, y_pred_oos], axis=0) if isinstance(y_pred.index, pd.MultiIndex): y_pred = y_pred.sort_index() return y_pred def _predict_out_of_sample(self, X_pool, fh): """Recursive reducer: predict out of sample (ahead of cutoff).""" # very similar to _predict_concurrent of DirectReductionForecaster - refactor? from sktime.transformations.series.impute import Imputer from sktime.transformations.series.lag import Lag fh_idx = self._get_expected_pred_idx(fh=fh) y_cols = self._y.columns lagger_y_to_X = self.lagger_y_to_X_ fh_rel = fh.to_relative(self.cutoff) y_lags = list(fh_rel) # for all positive fh y_lags_no_gaps = range(1, y_lags[-1] + 1) y_abs_no_gaps = ForecastingHorizon( list(y_lags_no_gaps), is_relative=True, freq=self._cutoff ) y_abs_no_gaps = y_abs_no_gaps.to_absolute_index(self._cutoff) # we will keep growing y_plus_preds recursively y_plus_preds = self._y y_pred_list = [] for _ in y_lags_no_gaps: if hasattr(self.fh, "freq") and self.fh.freq is not None: y_plus_preds = y_plus_preds.asfreq(self.fh.freq) Xt = lagger_y_to_X.transform(y_plus_preds) lag_plus = Lag(lags=1, index_out="extend") if self.impute_method is not None: lag_plus = lag_plus * Imputer(method=self.impute_method) Xtt = lag_plus.fit_transform(Xt) y_plus_one = lag_plus.fit_transform(y_plus_preds) predict_idx = y_plus_one.iloc[[-1]].index.get_level_values(-1)[0] Xtt_predrow = slice_at_ix(Xtt, predict_idx) if X_pool is not None: Xtt_predrow = pd.concat( [slice_at_ix(X_pool, predict_idx), Xtt_predrow], axis=1 ) Xtt_predrow = prep_skl_df(Xtt_predrow) estimator = self.estimator_ # if = no training indices in _fit, fill in y training mean if isinstance(estimator, pd.Series): y_pred_i = pd.DataFrame(index=[0], columns=y_cols) y_pred_i.iloc[0] = estimator # otherwise proceed as per direct reduction algorithm else: y_pred_i = estimator.predict(Xtt_predrow) # 2D numpy array with col index = (var) and 1 row y_pred_list.append(y_pred_i) y_pred_new_idx = self._get_expected_pred_idx(fh=[predict_idx]) y_pred_new = pd.DataFrame(y_pred_i, columns=y_cols, index=y_pred_new_idx) y_plus_preds = y_plus_preds.combine_first(y_pred_new) y_pred = np.concatenate(y_pred_list) y_pred = pd.DataFrame(y_pred, columns=y_cols, index=y_abs_no_gaps) y_pred = slice_at_ix(y_pred, fh_idx) return y_pred def _predict_in_sample(self, X_pool, fh): """Recursive reducer: predict out of sample (in past of of cutoff).""" from sktime.transformations.series.impute import Imputer from sktime.transformations.series.lag import Lag fh_idx = self._get_expected_pred_idx(fh=fh) y_cols = self._y.columns lagger_y_to_X = self.lagger_y_to_X_ fh_abs = fh.to_absolute(self.cutoff) y = self._y Xt = lagger_y_to_X.transform(y) lag_plus = Lag(lags=1, index_out="extend") if self.impute_method is not None: lag_plus = lag_plus * Imputer(method=self.impute_method) Xtt = lag_plus.fit_transform(Xt) Xtt_predrows = slice_at_ix(Xtt, fh_abs) if X_pool is not None: Xtt_predrows = pd.concat( [slice_at_ix(X_pool, fh_abs), Xtt_predrows], axis=1 ) Xtt_predrows = prep_skl_df(Xtt_predrows) estimator = self.estimator_ # if = no training indices in _fit, fill in y training mean if isinstance(estimator, pd.Series): y_pred = pd.DataFrame(index=fh_idx, columns=y_cols) y_pred = y_pred.fillna(self.estimator) # otherwise proceed as per direct reduction algorithm else: y_pred = estimator.predict(Xtt_predrows) # 2D numpy array with col index = (var) and 1 row y_pred = pd.DataFrame(y_pred, columns=y_cols, index=fh_idx) return y_pred @classmethod def get_test_params(cls, parameter_set="default"): """Return testing parameter settings for the estimator. Parameters ---------- parameter_set : str, default="default" Name of the set of test parameters to return, for use in tests. If no special parameters are defined for a value, will return ``"default"`` set. Returns ------- params : dict or list of dict, default = {} Parameters to create testing instances of the class Each dict are parameters to construct an "interesting" test instance, i.e., ``MyClass(**params)`` or ``MyClass(**params[i])`` creates a valid test instance. ``create_test_instance`` uses the first (or only) dictionary in ``params`` """ from sklearn.linear_model import LinearRegression est = LinearRegression() params1 = { "estimator": est, "window_length": 3, "pooling": "global", # all internal mtypes are tested across scenarios } return params1
[文档]class YfromX(BaseForecaster, _ReducerMixin): """Simple reduction predicting endogeneous from concurrent exogeneous variables. Tabulates all seen ``X`` and ``y`` by time index and applies tabular supervised regression. In ``fit``, given endogeneous time series ``y`` and exogeneous ``X``: fits ``estimator`` to feature-label pairs as defined as follows. features = :math:`y(t)`, labels: :math:`X(t)` ranging over all :math:`t` where the above have been observed (are in the index) In ``predict``, at a time :math:`t` in the forecasting horizon, uses ``estimator`` to predict :math:`y(t)`, from labels: :math:`X(t)` If regressor is ``skpro`` probabilistic regressor, and has ``predict_interval`` etc, uses ``estimator`` to predict :math:`y(t)`, from labels: :math:`X(t)`, passing on the ``predict_interval`` etc arguments. If no exogeneous data is provided, will predict the mean of ``y`` seen in ``fit``. In order to use a fit not on the entire historical data and update periodically, combine this with ``UpdateRefitsEvery``. In order to deal with missing data, combine this with ``Imputer``. To construct an custom direct reducer, combine with ``YtoX``, ``Lag``, or ``ReducerTransform``. Parameters ---------- estimator : sklearn regressor or skpro probabilistic regressor, must be compatible with sklearn or skpro interface tabular regression algorithm used in reduction algorithm if skpro regressor, resulting forecaster will have probabilistic capability pooling : str, one of ["local", "global", "panel"], optional, default="local" level on which data are pooled to fit the supervised regression model "local" = unit/instance level, one reduced model per lowest hierarchy level "global" = top level, one reduced model overall, on pooled data ignoring levels "panel" = second lowest level, one reduced model per panel level (-2) if there are 2 or less levels, "global" and "panel" result in the same if there is only 1 level (single time series), all three settings agree Example ------- >>> from sktime.datasets import load_longley >>> from sktime.split import temporal_train_test_split >>> from sktime.forecasting.compose import YfromX >>> from sklearn.linear_model import LinearRegression >>> >>> y, X = load_longley() >>> y_train, y_test, X_train, X_test = temporal_train_test_split(y, X) >>> fh = y_test.index >>> >>> f = YfromX(LinearRegression()) >>> f.fit(y=y_train, X=X_train, fh=fh) YfromX(...) >>> y_pred = f.predict(X=X_test) YfromX can also be used with skpro probabilistic regressors, in this case the resulting forecaster will be capable of probabilistic forecasts: >>> from skpro.regression.residual import ResidualDouble # doctest: +SKIP >>> reg_proba = ResidualDouble(LinearRegression()) # doctest: +SKIP >>> f = YfromX(reg_proba) # doctest: +SKIP >>> f.fit(y=y_train, X=X_train, fh=fh) # doctest: +SKIP YfromX(...) >>> y_pred = f.predict_interval(X=X_test) # doctest: +SKIP """ _tags = { "requires-fh-in-fit": False, # is the forecasting horizon required in fit? "ignores-exogeneous-X": False, "handles-missing-data": True, "X_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"], "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"], "capability:pred_int": True, "capability:categorical_in_X": True, } def __init__(self, estimator, pooling="local"): self.estimator = estimator self.pooling = pooling super().__init__() # self._est_type encodes information what type of estimator is passed if hasattr(estimator, "get_tags"): _est_type = estimator.get_tag("object_type", "regressor", False) else: _est_type = "regressor" if _est_type not in ["regressor", "regressor_proba"]: raise TypeError( "error in YfromX, estimator must be either an sklearn compatible " "regressor, or an skpro probabilistic regressor." ) # has probabilistic mode iff the estimator is of type regressor_proba self.set_tags(**{"capability:pred_int": _est_type == "regressor_proba"}) self._est_type = _est_type if pooling == "local": mtypes = "pd.DataFrame" elif pooling == "global": mtypes = ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"] elif pooling == "panel": mtypes = ["pd.DataFrame", "pd-multiindex"] else: raise ValueError( "pooling in DirectReductionForecaster must be one of" ' "local", "global", "panel", ' f"but found {pooling}" ) self.set_tags(**{"X_inner_mtype": mtypes}) self.set_tags(**{"y_inner_mtype": mtypes}) def _fit(self, y, X, fh): """Fit forecaster to training data. private _fit containing the core logic, called from fit Parameters ---------- y : pd.DataFrame mtype is pd.DataFrame, pd-multiindex, or pd_multiindex_hier Time series to which to fit the forecaster. fh : guaranteed to be ForecastingHorizon or None, optional (default=None) The forecasting horizon with the steps ahead to to predict. Required (non-optional) here if self.get_tag("requires-fh-in-fit")==True Otherwise, if not passed in _fit, guaranteed to be passed in _predict X : pd.DataFrame optional (default=None) mtype is pd.DataFrame, pd-multiindex, or pd_multiindex_hier Exogeneous time series to fit to. Returns ------- self : reference to self """ _est_type = self._est_type if X is None: from sklearn.dummy import DummyRegressor if _est_type == "regressor": estimator = DummyRegressor() else: # "proba_regressor" from skpro.regression.residual import ResidualDouble dummy = DummyRegressor() estimator = ResidualDouble(dummy) X = prep_skl_df(y, copy_df=True) else: X = prep_skl_df(X, copy_df=True) estimator = clone(self.estimator) if _est_type == "regressor": y = prep_skl_df(y, copy_df=True) y = y.values.flatten() estimator.fit(X, y) self.estimator_ = estimator return self def _predict(self, X=None, fh=None): """Forecast time series at future horizon. private _predict containing the core logic, called from predict Parameters ---------- fh : guaranteed to be ForecastingHorizon or None, optional (default=None) The forecasting horizon with the steps ahead to to predict. If not passed in _fit, guaranteed to be passed here X : pd.DataFrame, optional (default=None) mtype is pd.DataFrame, pd-multiindex, or pd_multiindex_hier Exogeneous time series for the forecast Returns ------- y_pred : pd.DataFrame, same type as y in _fit Point predictions """ _est_type = self._est_type fh_idx = self._get_expected_pred_idx(fh=fh) X_idx = self._get_pred_X(X=X, fh_idx=fh_idx) y_pred = self.estimator_.predict(X_idx) if _est_type == "regressor": y_cols = self._y.columns y_pred = pd.DataFrame(y_pred, index=fh_idx, columns=y_cols) return y_pred def _predict_quantiles(self, fh, X, alpha): """Compute/return prediction quantiles for a forecast. private _predict_quantiles containing the core logic, called from predict_quantiles and possibly predict_interval State required: Requires state to be "fitted". Accesses in self: Fitted model attributes ending in "_" self.cutoff Parameters ---------- fh : guaranteed to be ForecastingHorizon The forecasting horizon with the steps ahead to to predict. X : sktime time series object, optional (default=None) guaranteed to be of an mtype in self.get_tag("X_inner_mtype") Exogeneous time series for the forecast alpha : list of float (guaranteed not None and floats in [0,1] interval) A list of probabilities at which quantile forecasts are computed. Returns ------- quantiles : pd.DataFrame Column has multi-index: first level is variable name from y in fit, second level being the values of alpha passed to the function. Row index is fh, with additional (upper) levels equal to instance levels, from y seen in fit, if y_inner_mtype is Panel or Hierarchical. Entries are quantile forecasts, for var in col index, at quantile probability in second col index, for the row index. """ fh_idx = self._get_expected_pred_idx(fh=fh) X_idx = self._get_pred_X(X=X, fh_idx=fh_idx) y_pred = self.estimator_.predict_quantiles(X_idx, alpha=alpha) return y_pred def _predict_interval(self, fh, X, coverage): """Compute/return prediction quantiles for a forecast. private _predict_interval containing the core logic, called from predict_interval and possibly predict_quantiles State required: Requires state to be "fitted". Accesses in self: Fitted model attributes ending in "_" self.cutoff Parameters ---------- fh : guaranteed to be ForecastingHorizon The forecasting horizon with the steps ahead to to predict. X : sktime time series object, optional (default=None) guaranteed to be of an mtype in self.get_tag("X_inner_mtype") Exogeneous time series for the forecast coverage : list of float (guaranteed not None and floats in [0,1] interval) nominal coverage(s) of predictive interval(s) Returns ------- pred_int : pd.DataFrame Column has multi-index: first level is variable name from y in fit, second level coverage fractions for which intervals were computed. in the same order as in input ``coverage``. Third level is string "lower" or "upper", for lower/upper interval end. Row index is fh, with additional (upper) levels equal to instance levels, from y seen in fit, if y_inner_mtype is Panel or Hierarchical. Entries are forecasts of lower/upper interval end, for var in col index, at nominal coverage in second col index, lower/upper depending on third col index, for the row index. Upper/lower interval end forecasts are equivalent to quantile forecasts at alpha = 0.5 - c/2, 0.5 + c/2 for c in coverage. """ fh_idx = self._get_expected_pred_idx(fh=fh) X_idx = self._get_pred_X(X=X, fh_idx=fh_idx) y_pred = self.estimator_.predict_interval(X_idx, coverage=coverage) return y_pred def _predict_var(self, fh, X=None, cov=False): """Forecast variance at future horizon. private _predict_var containing the core logic, called from predict_var Parameters ---------- fh : guaranteed to be ForecastingHorizon or None, optional (default=None) The forecasting horizon with the steps ahead to to predict. If not passed in _fit, guaranteed to be passed here X : sktime time series object, optional (default=None) guaranteed to be of an mtype in self.get_tag("X_inner_mtype") Exogeneous time series for the forecast cov : bool, optional (default=False) if True, computes covariance matrix forecast. if False, computes marginal variance forecasts. Returns ------- pred_var : pd.DataFrame, format dependent on ``cov`` variable If cov=False: Column names are exactly those of ``y`` passed in ``fit``/``update``. For nameless formats, column index will be a RangeIndex. Row index is fh, with additional levels equal to instance levels, from y seen in fit, if y_inner_mtype is Panel or Hierarchical. Entries are variance forecasts, for var in col index. A variance forecast for given variable and fh index is a predicted variance for that variable and index, given observed data. If cov=True: Column index is a multiindex: 1st level is variable names (as above) 2nd level is fh. Row index is fh, with additional levels equal to instance levels, from y seen in fit, if y_inner_mtype is Panel or Hierarchical. Entries are (co-)variance forecasts, for var in col index, and covariance between time index in row and col. Note: no covariance forecasts are returned between different variables. """ fh_idx = self._get_expected_pred_idx(fh=fh) X_idx = self._get_pred_X(X=X, fh_idx=fh_idx) y_pred = self.estimator_.predict_var(X_idx) return y_pred def _predict_proba(self, fh, X, marginal=True): """Compute/return fully probabilistic forecasts. private _predict_proba containing the core logic, called from predict_proba Parameters ---------- fh : int, list, np.array or ForecastingHorizon (not optional) The forecasting horizon encoding the time stamps to forecast at. if has not been passed in fit, must be passed, not optional X : sktime time series object, optional (default=None) Exogeneous time series for the forecast Should be of same scitype (Series, Panel, or Hierarchical) as y in fit if self.get_tag("X-y-must-have-same-index"), X.index must contain fh.index and y.index both marginal : bool, optional (default=True) whether returned distribution is marginal by time index Returns ------- pred_dist : sktime BaseDistribution predictive distribution if marginal=True, will be marginal distribution by time point if marginal=False and implemented by method, will be joint """ fh_idx = self._get_expected_pred_idx(fh=fh) X_idx = self._get_pred_X(X=X, fh_idx=fh_idx) y_pred = self.estimator_.predict_proba(X_idx) return y_pred def _get_pred_X(self, X, fh_idx): y_cols = self._y.columns if X is not None and self._X is not None: X_pool = X.combine_first(self._X) elif X is None and self._X is not None: X_pool = self._X elif X is not None: X_pool = X else: X_pool = pd.DataFrame(0, index=fh_idx, columns=y_cols) X_pool = prep_skl_df(X_pool, copy_df=True) X_idx = X_pool.loc[fh_idx] return X_idx @classmethod def get_test_params(cls, parameter_set="default"): """Return testing parameter settings for the estimator. Parameters ---------- parameter_set : str, default="default" Name of the set of test parameters to return, for use in tests. If no special parameters are defined for a value, will return ``"default"`` set. Returns ------- params : dict or list of dict, default = {} Parameters to create testing instances of the class Each dict are parameters to construct an "interesting" test instance, i.e., ``MyClass(**params)`` or ``MyClass(**params[i])`` creates a valid test instance. ``create_test_instance`` uses the first (or only) dictionary in ``params`` """ from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import LinearRegression from sktime.utils.dependencies import _check_soft_dependencies params1 = { "estimator": LinearRegression(), "pooling": "local", } params2 = { "estimator": RandomForestRegressor(), "pooling": "global", # all internal mtypes are tested across scenarios } params = [params1, params2] if _check_soft_dependencies("skpro", severity="none"): from skpro.regression.residual import ResidualDouble params3 = { "estimator": ResidualDouble.create_test_instance(), "pooling": "global", } params = params + [params3] return params