From fbbe7b8894413886ea239b3000dfa7185af4a0bb Mon Sep 17 00:00:00 2001 From: authierj Date: Fri, 10 Oct 2025 11:50:03 +0200 Subject: [PATCH 1/3] added DirRec parameter to linear regression --- darts/models/forecasting/linear_regression_model.py | 5 +++++ darts/models/forecasting/sklearn_model.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/darts/models/forecasting/linear_regression_model.py b/darts/models/forecasting/linear_regression_model.py index 98fc24b69b..b63bb26e8e 100644 --- a/darts/models/forecasting/linear_regression_model.py +++ b/darts/models/forecasting/linear_regression_model.py @@ -43,6 +43,7 @@ def __init__( random_state: Optional[int] = None, multi_models: Optional[bool] = True, use_static_covariates: bool = True, + dir_rec: Optional[bool] = False, **kwargs, ): """Linear regression model. @@ -136,6 +137,9 @@ def encode_year(idx): Whether the model should use static covariate information in case the input `series` passed to ``fit()`` contain static covariates. If ``True``, and static covariates are available at fitting time, will enforce that all target `series` have the same static covariate dimensionality in ``fit()`` and ``predict()``. + dir_rec + Whether the model should use DirRec prediction strategy. + **kwargs Additional keyword arguments passed to `sklearn.linear_model.LinearRegression` (by default), to `sklearn.linear_model.PoissonRegressor` (if `likelihood="poisson"`), or to @@ -200,6 +204,7 @@ def encode_year(idx): multi_models=multi_models, use_static_covariates=use_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) def fit( diff --git a/darts/models/forecasting/sklearn_model.py b/darts/models/forecasting/sklearn_model.py index 02e3dd0300..a1abeb7634 100644 --- a/darts/models/forecasting/sklearn_model.py +++ b/darts/models/forecasting/sklearn_model.py @@ -118,6 +118,7 @@ def __init__( multi_models: Optional[bool] = True, use_static_covariates: bool = True, random_state: Optional[int] = None, + dir_rec: Optional[bool] = False, ): """Regression Model Can be used to fit any scikit-learn-like regressor class to predict the target time series from lagged values. @@ -210,6 +211,8 @@ def encode_year(idx): that all target `series` have the same static covariate dimensionality in ``fit()`` and ``predict()``. random_state Controls the randomness for reproducible forecasting. + dir_rec + Whether the model should use DirRec prediction strategy. Examples -------- From 31bc52bd2bf6e8cef9f16359cf89a001135207a0 Mon Sep 17 00:00:00 2001 From: authierj Date: Tue, 4 Nov 2025 14:56:35 +0100 Subject: [PATCH 2/3] first attempt --- darts/models/forecasting/sklearn_model.py | 46 ++++- darts/utils/multioutput.py | 230 ++++++++++++++++++++-- 2 files changed, 249 insertions(+), 27 deletions(-) diff --git a/darts/models/forecasting/sklearn_model.py b/darts/models/forecasting/sklearn_model.py index a1abeb7634..ea339c822f 100644 --- a/darts/models/forecasting/sklearn_model.py +++ b/darts/models/forecasting/sklearn_model.py @@ -92,7 +92,12 @@ SKLearnLikelihood, _get_likelihood, ) -from darts.utils.multioutput import MultiOutputMixin, get_multioutput_estimator_cls +from darts.utils.multioutput import ( + MultiOutputMixin, + RecurrentMultiOutputMixin, + get_multioutput_estimator_cls, + get_recurrent_multioutput_estimator_cls, +) from darts.utils.ts_utils import get_single_series, seq2series, series2seq from darts.utils.utils import ModelType, random_method @@ -256,6 +261,7 @@ def encode_year(idx): self._static_covariates_shape: Optional[tuple[int, int]] = None self._lagged_feature_names: Optional[list[str]] = None self._lagged_label_names: Optional[list[str]] = None + self.dir_rec = dir_rec # optionally, the model can be wrapped in a likelihood model self._likelihood: Optional[SKLearnLikelihood] = getattr( @@ -728,8 +734,10 @@ def _add_val_set_to_kwargs( last_static_covariates_shape=self._static_covariates_shape, stride=stride, ) - # create validation sets for MultiOutputMixin - if val_labels.ndim == 2 and isinstance(self.model, MultiOutputMixin): + # create validation sets for MultiOutputMixin and RecurrentMultiOutputMixin + if val_labels.ndim == 2 and isinstance( + self.model, (MultiOutputMixin, RecurrentMultiOutputMixin) + ): val_sets, val_weights = [], [] for i in range(val_labels.shape[1]): val_sets.append((val_samples, val_labels[:, i])) @@ -1018,8 +1026,32 @@ def fit( self.output_chunk_length > 1 and self.multi_models ) - # If multi-output required and model doesn't support it natively, wrap it in a MultiOutputMixin - if ( + # If multi-output required and direct-recursive prediction is enabled, wrap it in RecurrentMultiOutputMixin + # Note: dir_rec ALWAYS requires wrapping (even if model supports native multi-output) + if self.dir_rec: + val_set_name, val_weight_name = self.val_set_params + mor_kwargs = { + "eval_set_name": val_set_name, + "eval_weight_name": val_weight_name, + } + mor_kwargs["output_chunk_length"] = self.output_chunk_length + + if ( + n_jobs_multioutput_wrapper is not None + and n_jobs_multioutput_wrapper != 1 + ): + logger.warning( + "Direct-recursive multi-output prediction is sequential by design. " + "`n_jobs_multioutput_wrapper` parameter will be ignored." + ) + + # do I need to worry about regressor vs classifier here? + self.model = get_recurrent_multioutput_estimator_cls(self._model_type)( + estimator=self.model, **mor_kwargs + ) + + # Elif multi-output required and model doesn't support it natively, wrap it in a MultiOutputMixin + elif ( requires_multioutput and not isinstance(self.model, MultiOutputMixin) and ( @@ -1041,7 +1073,7 @@ def fit( ) if ( - not isinstance(self.model, MultiOutputMixin) + not isinstance(self.model, (MultiOutputMixin, RecurrentMultiOutputMixin)) and n_jobs_multioutput_wrapper is not None ): logger.warning("Provided `n_jobs_multioutput_wrapper` wasn't used.") @@ -1474,7 +1506,7 @@ def supports_sample_weight(self) -> bool: """Whether the model supports a validation set during training.""" return ( self.model.supports_sample_weight - if isinstance(self.model, MultiOutputMixin) + if isinstance(self.model, (MultiOutputMixin, RecurrentMultiOutputMixin)) else has_fit_parameter(self.model, "sample_weight") ) diff --git a/darts/utils/multioutput.py b/darts/utils/multioutput.py index 42a855300b..6a21397a94 100644 --- a/darts/utils/multioutput.py +++ b/darts/utils/multioutput.py @@ -1,6 +1,7 @@ from typing import Optional -from sklearn.base import is_classifier +import numpy as np +from sklearn.base import clone, is_classifier from sklearn.multioutput import MultiOutputClassifier as sk_MultiOutputClassifier from sklearn.multioutput import MultiOutputRegressor as sk_MultiOutputRegressor from sklearn.multioutput import _fit_estimator @@ -39,34 +40,20 @@ def __init__( self.eval_weight_name = eval_weight_name self.output_chunk_length = output_chunk_length - def fit(self, X, y, sample_weight=None, **fit_params): - """Fit the model to data, separately for each output variable. + def _validate_fit_inputs(self, y, sample_weight): + """Validate inputs for fitting. Shared by MultiOutputMixin and RecurrentMultiOutputMixin. Parameters ---------- - X : {array-like, sparse matrix} of shape (n_samples, n_features) - The input data. - y : {array-like, sparse matrix} of shape (n_samples, n_outputs) - Multi-output targets. An indicator matrix turns on multilabel - estimation. - + Multi-output targets. sample_weight : array-like of shape (n_samples, n_outputs), default=None - Sample weights. If `None`, then samples are equally weighted. - Only supported if the underlying regressor supports sample - weights. - - **fit_params : dict of string -> object - Parameters passed to the ``estimator.fit`` method of each step. - - .. versionadded:: 0.23 + Sample weights. Returns ------- - self : object - Returns a fitted instance. + y : validated y array """ - if not hasattr(self.estimator, "fit"): raise_log( ValueError("The base estimator should implement a fit method"), @@ -98,6 +85,37 @@ def fit(self, X, y, sample_weight=None, **fit_params): logger=logger, ) + return y + + def fit(self, X, y, sample_weight=None, **fit_params): + """Fit the model to data, separately for each output variable. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The input data. + + y : {array-like, sparse matrix} of shape (n_samples, n_outputs) + Multi-output targets. An indicator matrix turns on multilabel + estimation. + + sample_weight : array-like of shape (n_samples, n_outputs), default=None + Sample weights. If `None`, then samples are equally weighted. + Only supported if the underlying regressor supports sample + weights. + + **fit_params : dict of string -> object + Parameters passed to the ``estimator.fit`` method of each step. + + .. versionadded:: 0.23 + + Returns + ------- + self : object + Returns a fitted instance. + """ + y = self._validate_fit_inputs(y, sample_weight) + fit_params_validated = _check_method_params(X, fit_params) eval_set = fit_params_validated.pop(self.eval_set_name, None) eval_weight = fit_params_validated.pop(self.eval_weight_name, None) @@ -155,6 +173,162 @@ def fit(self, X, y, sample_weight=None, **fit_params): return self +class RecurrentMultiOutputMixin(MultiOutputMixin): + """ + Mixin for :class:`sklearn.utils.multioutput._MultiOutputEstimator` that implements direct-recursive multi-output + prediction. Each output estimator is trained sequentially, using predictions from all previous estimators as + additional features. + + This approach differs from :class:`MultiOutputRegressor` where all estimators are trained independently + in parallel. Here, estimator i uses predictions from estimators 0, 1, ..., i-1 as additional input features, + creating a chain of dependencies. + """ + + def fit(self, X, y, sample_weight=None, **fit_params): + """Fit the model to data sequentially for each output variable, where each estimator + uses predictions from previous estimators as additional features. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The input data. + + y : {array-like, sparse matrix} of shape (n_samples, n_outputs) + Multi-output targets. An indicator matrix turns on multilabel + estimation. + + sample_weight : array-like of shape (n_samples, n_outputs), default=None + Sample weights. If `None`, then samples are equally weighted. + Only supported if the underlying regressor supports sample + weights. + + **fit_params : dict of string -> object + Parameters passed to the ``estimator.fit`` method of each step. + + .. versionadded:: 0.23 + + Returns + ------- + self : object + Returns a fitted instance. + """ + y = self._validate_fit_inputs(y, sample_weight) + + fit_params_validated = _check_method_params(X, fit_params) + eval_set = fit_params_validated.pop(self.eval_set_name, None) + eval_weight = fit_params_validated.pop(self.eval_weight_name, None) + + self.estimators_ = [] + predictions = [] # Store predictions from previous estimators + + # Sequential training - each estimator depends on predictions from previous ones + for i in range(y.shape[1]): + # Clone the base estimator for this output + estimator = clone(self.estimator) + + # Augment features with predictions from all previous estimators + if predictions: + X_augmented = np.concatenate([X] + predictions, axis=1) + else: + X_augmented = X + + # Prepare evaluation set with augmented features if provided + # not 100% sure about this + eval_set_augmented = None + if eval_set is not None: + eval_X, eval_y = eval_set[i] + if predictions: + # Augment eval_X with predictions from already-trained estimators + eval_predictions = [ + est.predict(eval_X).reshape(-1, 1) for est in self.estimators_ + ] + eval_X_augmented = np.concatenate( + [eval_X] + eval_predictions, axis=1 + ) + else: + eval_X_augmented = eval_X + eval_set_augmented = [(eval_X_augmented, eval_y)] + + # Fit the estimator for this output + estimator.fit( + X_augmented, + y[:, i], + sample_weight=sample_weight[:, i] + if sample_weight is not None + else None, + **( + {self.eval_set_name: eval_set_augmented} + if eval_set_augmented is not None + else {} + ), + **( + {self.eval_weight_name: [eval_weight[i]]} + if eval_weight is not None + else {} + ), + **fit_params_validated, + ) + + self.estimators_.append(estimator) + + # Generate predictions to use as features for the next estimator + y_pred = estimator.predict(X_augmented).reshape(-1, 1) + predictions.append(y_pred) + + # Set sklearn-standard attributes from first estimator + if hasattr(self.estimators_[0], "n_features_in_"): + self.n_features_in_ = self.estimators_[0].n_features_in_ + if hasattr(self.estimators_[0], "feature_names_in_"): + self.feature_names_in_ = self.estimators_[0].feature_names_in_ + + return self + + def predict(self, X): + """Predict multi-output variable sequentially, where each estimator uses predictions + from previous estimators as additional features. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The input data. + + Returns + ------- + y : array of shape (n_samples, n_outputs) + Multi-output targets predicted across multiple predictors. + """ + predictions = [] + + for estimator in self.estimators_: + # Augment features with predictions from previous estimators + if predictions: + X_augmented = np.concatenate([X] + predictions, axis=1) + else: + X_augmented = X + + # Predict for this output + y_pred = estimator.predict(X_augmented).reshape(-1, 1) + predictions.append(y_pred) + + # Stack all predictions horizontally to form (n_samples, n_outputs) + return np.concatenate(predictions, axis=1) + + +class RecurrentMultiOutputRegressor(RecurrentMultiOutputMixin, sk_MultiOutputRegressor): + "wrapper class for recurrent multioutput regressor" + + +class RecurrentMultiOutputClassifier( + RecurrentMultiOutputMixin, sk_MultiOutputRegressor +): + "wrapper class for recurrent multioutput classifier" + + def fit(self, X, y, sample_weight=None, **fit_params): + super().fit(X=X, y=y, sample_weight=sample_weight, **fit_params) + self.classes_ = [estimator.classes_ for estimator in self.estimators_] + return self + + def get_multioutput_estimator_cls(model_type: ModelType) -> type[MultiOutputMixin]: if model_type == ModelType.FORECASTING_REGRESSOR: return MultiOutputRegressor @@ -167,3 +341,19 @@ def get_multioutput_estimator_cls(model_type: ModelType) -> type[MultiOutputMixi f"Received: `{model_type}`." ) ) + + +def get_recurrent_multioutput_estimator_cls( + model_type: ModelType, +) -> type[RecurrentMultiOutputMixin]: + if model_type == ModelType.FORECASTING_REGRESSOR: + return RecurrentMultiOutputRegressor + elif model_type == ModelType.FORECASTING_CLASSIFIER: + return RecurrentMultiOutputClassifier + else: + raise_log( + ValueError( + "Model type must be one of `[ModelType.FORECASTING_REGRESSOR, ModelType.FORECASTING_CLASSIFIER]`. " + f"Received: `{model_type}`." + ) + ) From 4e288a2c0fb9bdd74714b7bb9bda4267af1f7edd Mon Sep 17 00:00:00 2001 From: authierj Date: Tue, 4 Nov 2025 17:27:41 +0100 Subject: [PATCH 3/3] added dir_rec to most models --- darts/models/forecasting/catboost_model.py | 12 +++++++++++ darts/models/forecasting/lgbm.py | 12 +++++++++++ .../forecasting/linear_regression_model.py | 5 +++-- darts/models/forecasting/random_forest.py | 6 ++++++ darts/models/forecasting/sklearn_model.py | 21 ++++++++++++++++--- darts/models/forecasting/xgboost.py | 12 +++++++++++ 6 files changed, 63 insertions(+), 5 deletions(-) diff --git a/darts/models/forecasting/catboost_model.py b/darts/models/forecasting/catboost_model.py index 7fd328edf1..90ee3f5162 100644 --- a/darts/models/forecasting/catboost_model.py +++ b/darts/models/forecasting/catboost_model.py @@ -58,6 +58,7 @@ def __init__( categorical_past_covariates: Optional[Union[str, list[str]]] = None, categorical_future_covariates: Optional[Union[str, list[str]]] = None, categorical_static_covariates: Optional[Union[str, list[str]]] = None, + dir_rec: Optional[bool] = False, **kwargs, ): """CatBoost Model @@ -167,6 +168,10 @@ def encode_year(idx): Optionally, string or list of strings specifying the static covariates that should be treated as categorical by the underlying `CatBoostRegressor`. The components that are specified as categorical must be integer-encoded. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `catboost.CatBoostRegressor`. Native multi-output support can be achieved by using an appropriate `loss_function` ('MultiRMSE', @@ -230,6 +235,7 @@ def encode_year(idx): categorical_future_covariates=categorical_future_covariates, categorical_static_covariates=categorical_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) # if no loss provided, get the default loss from the model @@ -470,6 +476,7 @@ def __init__( categorical_past_covariates: Optional[Union[str, list[str]]] = None, categorical_future_covariates: Optional[Union[str, list[str]]] = None, categorical_static_covariates: Optional[Union[str, list[str]]] = None, + dir_rec: Optional[bool] = False, **kwargs, ): """CatBoost Model for classification forecasting @@ -576,6 +583,10 @@ def encode_year(idx): Optionally, string or list of strings specifying the static covariates that should be treated as categorical by the underlying `CatBoostRegressor`. The components that are specified as categorical must be integer-encoded. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `catboost.CatBoostClassifier`. @@ -626,6 +637,7 @@ def encode_year(idx): categorical_past_covariates=categorical_past_covariates, categorical_future_covariates=categorical_future_covariates, categorical_static_covariates=categorical_static_covariates, + dir_rec=dir_rec, **kwargs, ) diff --git a/darts/models/forecasting/lgbm.py b/darts/models/forecasting/lgbm.py index 048c457310..97b8b8a4d4 100644 --- a/darts/models/forecasting/lgbm.py +++ b/darts/models/forecasting/lgbm.py @@ -55,6 +55,7 @@ def __init__( categorical_past_covariates: Optional[Union[str, list[str]]] = None, categorical_future_covariates: Optional[Union[str, list[str]]] = None, categorical_static_covariates: Optional[Union[str, list[str]]] = None, + dir_rec: Optional[bool] = False, **kwargs, ): """LGBM Model @@ -161,6 +162,10 @@ def encode_year(idx): Optionally, string or list of strings specifying the static covariates that should be treated as categorical by the underlying `lightgbm.LightGBMRegressor`. The components that are specified as categorical must be integer-encoded. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `lightgbm.LGBRegressor`. @@ -218,6 +223,7 @@ def encode_year(idx): categorical_future_covariates=categorical_future_covariates, categorical_static_covariates=categorical_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) @staticmethod @@ -374,6 +380,7 @@ def __init__( categorical_past_covariates: Optional[Union[str, list[str]]] = None, categorical_future_covariates: Optional[Union[str, list[str]]] = None, categorical_static_covariates: Optional[Union[str, list[str]]] = None, + dir_rec: Optional[bool] = False, **kwargs, ): """LGBM Model for classification forecasting @@ -480,6 +487,10 @@ def encode_year(idx): Optionally, string or list of strings specifying the static covariates that should be treated as categorical by the underlying `lightgbm.LightGBMRegressor`. The components that are specified as categorical must be integer-encoded. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `lightgbm.LGBClassifier`. @@ -531,6 +542,7 @@ def encode_year(idx): categorical_past_covariates=categorical_past_covariates, categorical_future_covariates=categorical_future_covariates, categorical_static_covariates=categorical_static_covariates, + dir_rec=dir_rec, **kwargs, ) diff --git a/darts/models/forecasting/linear_regression_model.py b/darts/models/forecasting/linear_regression_model.py index b63bb26e8e..4f87f9be9d 100644 --- a/darts/models/forecasting/linear_regression_model.py +++ b/darts/models/forecasting/linear_regression_model.py @@ -138,8 +138,9 @@ def encode_year(idx): contain static covariates. If ``True``, and static covariates are available at fitting time, will enforce that all target `series` have the same static covariate dimensionality in ``fit()`` and ``predict()``. dir_rec - Whether the model should use DirRec prediction strategy. - + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `sklearn.linear_model.LinearRegression` (by default), to `sklearn.linear_model.PoissonRegressor` (if `likelihood="poisson"`), or to diff --git a/darts/models/forecasting/random_forest.py b/darts/models/forecasting/random_forest.py index 0374e5f813..39f64c3326 100644 --- a/darts/models/forecasting/random_forest.py +++ b/darts/models/forecasting/random_forest.py @@ -43,6 +43,7 @@ def __init__( multi_models: Optional[bool] = True, use_static_covariates: bool = True, random_state: Optional[int] = None, + dir_rec: Optional[bool] = False, **kwargs, ): """Random Forest Model @@ -135,6 +136,10 @@ def encode_year(idx): that all target `series` have the same static covariate dimensionality in ``fit()`` and ``predict()``. random_state Controls the randomness for reproducible forecasting. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `sklearn.ensemble.RandomForestRegressor`. @@ -186,6 +191,7 @@ def encode_year(idx): model=RandomForestRegressor(**kwargs), use_static_covariates=use_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) diff --git a/darts/models/forecasting/sklearn_model.py b/darts/models/forecasting/sklearn_model.py index ea339c822f..f7ca49888e 100644 --- a/darts/models/forecasting/sklearn_model.py +++ b/darts/models/forecasting/sklearn_model.py @@ -217,7 +217,9 @@ def encode_year(idx): random_state Controls the randomness for reproducible forecasting. dir_rec - Whether the model should use DirRec prediction strategy. + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. Examples -------- @@ -1026,7 +1028,7 @@ def fit( self.output_chunk_length > 1 and self.multi_models ) - # If multi-output required and direct-recursive prediction is enabled, wrap it in RecurrentMultiOutputMixin + # If direct-recursive prediction is enabled, wrap it in RecurrentMultiOutputMixin # Note: dir_rec ALWAYS requires wrapping (even if model supports native multi-output) if self.dir_rec: val_set_name, val_weight_name = self.val_set_params @@ -1045,7 +1047,6 @@ def fit( "`n_jobs_multioutput_wrapper` parameter will be ignored." ) - # do I need to worry about regressor vs classifier here? self.model = get_recurrent_multioutput_estimator_cls(self._model_type)( estimator=self.model, **mor_kwargs ) @@ -1648,6 +1649,7 @@ def __init__( categorical_future_covariates: Optional[Union[str, list[str]]] = None, categorical_static_covariates: Optional[Union[str, list[str]]] = None, random_state: Optional[int] = None, + dir_rec: Optional[bool] = False, ): """ Extension of `SKLearnModel` for regression models that support categorical features. @@ -1747,6 +1749,10 @@ def encode_year(idx): categorical. random_state Controls the randomness for reproducible forecasting. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. """ super().__init__( lags=lags, @@ -1759,6 +1765,7 @@ def encode_year(idx): multi_models=multi_models, use_static_covariates=use_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) if categorical_static_covariates is not None and not use_static_covariates: @@ -1960,6 +1967,7 @@ def __init__( multi_models: Optional[bool] = True, use_static_covariates: bool = True, random_state: Optional[int] = None, + dir_rec: Optional[bool] = False, ): """Regression Model Can be used to fit any scikit-learn-like regressor class to predict the target time series from lagged values. @@ -2055,6 +2063,10 @@ def encode_year(idx): that all target `series` have the same static covariate dimensionality in ``fit()`` and ``predict()``. random_state Controls the randomness for reproducible forecasting. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. Examples -------- @@ -2103,6 +2115,7 @@ def encode_year(idx): multi_models=multi_models, use_static_covariates=use_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) @@ -2199,6 +2212,7 @@ def __init__( multi_models: Optional[bool] = True, use_static_covariates: bool = True, random_state: Optional[int] = None, + dir_rec: Optional[bool] = False, ): """SKLearn Classifier Model @@ -2367,4 +2381,5 @@ def encode_year(idx): multi_models=multi_models, use_static_covariates=use_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) diff --git a/darts/models/forecasting/xgboost.py b/darts/models/forecasting/xgboost.py index 3ad4b01be9..2dbd5cf526 100644 --- a/darts/models/forecasting/xgboost.py +++ b/darts/models/forecasting/xgboost.py @@ -72,6 +72,7 @@ def __init__( random_state: Optional[int] = None, multi_models: Optional[bool] = True, use_static_covariates: bool = True, + dir_rec: Optional[bool] = False, **kwargs, ): """XGBoost Model @@ -164,6 +165,10 @@ def encode_year(idx): Whether the model should use static covariate information in case the input `series` passed to ``fit()`` contain static covariates. If ``True``, and static covariates are available at fitting time, will enforce that all target `series` have the same static covariate dimensionality in ``fit()`` and ``predict()``. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `xgb.XGBRegressor`. @@ -219,6 +224,7 @@ def encode_year(idx): model=self._create_model(**self.kwargs), use_static_covariates=use_static_covariates, random_state=random_state, + dir_rec=dir_rec, ) @staticmethod @@ -374,6 +380,7 @@ def __init__( random_state: Optional[int] = None, multi_models: Optional[bool] = True, use_static_covariates: bool = True, + dir_rec: Optional[bool] = False, **kwargs, ): """XGBoost Model for classification forecasting @@ -467,6 +474,10 @@ def encode_year(idx): Whether the model should use static covariate information in case the input `series` passed to ``fit()`` contain static covariates. If ``True``, and static covariates are available at fitting time, will enforce that all target `series` have the same static covariate dimensionality in ``fit()`` and ``predict()``. + dir_rec + Whether to use direct-recursive strategy for multi-step forecasting. When True, each forecast + horizon uses predictions from previous horizons as additional input features. This creates a + chained prediction where step t+2 uses the prediction for step t+1 as a feature. Default: False. **kwargs Additional keyword arguments passed to `xgb.XGBClassifier`. @@ -513,6 +524,7 @@ def encode_year(idx): random_state=random_state, multi_models=multi_models, use_static_covariates=use_static_covariates, + dir_rec=dir_rec, **kwargs, )