autogluon.timeseries 1.0.1b20240304__py3-none-any.whl → 1.4.1b20251210__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of autogluon.timeseries might be problematic. Click here for more details.

Files changed (108) hide show
  1. autogluon/timeseries/configs/__init__.py +3 -2
  2. autogluon/timeseries/configs/hyperparameter_presets.py +62 -0
  3. autogluon/timeseries/configs/predictor_presets.py +84 -0
  4. autogluon/timeseries/dataset/ts_dataframe.py +339 -186
  5. autogluon/timeseries/learner.py +192 -60
  6. autogluon/timeseries/metrics/__init__.py +55 -11
  7. autogluon/timeseries/metrics/abstract.py +96 -25
  8. autogluon/timeseries/metrics/point.py +186 -39
  9. autogluon/timeseries/metrics/quantile.py +47 -20
  10. autogluon/timeseries/metrics/utils.py +6 -6
  11. autogluon/timeseries/models/__init__.py +13 -7
  12. autogluon/timeseries/models/abstract/__init__.py +2 -2
  13. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +533 -273
  14. autogluon/timeseries/models/abstract/model_trial.py +10 -10
  15. autogluon/timeseries/models/abstract/tunable.py +189 -0
  16. autogluon/timeseries/models/autogluon_tabular/__init__.py +2 -0
  17. autogluon/timeseries/models/autogluon_tabular/mlforecast.py +369 -215
  18. autogluon/timeseries/models/autogluon_tabular/per_step.py +513 -0
  19. autogluon/timeseries/models/autogluon_tabular/transforms.py +67 -0
  20. autogluon/timeseries/models/autogluon_tabular/utils.py +3 -51
  21. autogluon/timeseries/models/chronos/__init__.py +4 -0
  22. autogluon/timeseries/models/chronos/chronos2.py +361 -0
  23. autogluon/timeseries/models/chronos/model.py +738 -0
  24. autogluon/timeseries/models/chronos/utils.py +369 -0
  25. autogluon/timeseries/models/ensemble/__init__.py +35 -2
  26. autogluon/timeseries/models/ensemble/{abstract_timeseries_ensemble.py → abstract.py} +50 -26
  27. autogluon/timeseries/models/ensemble/array_based/__init__.py +3 -0
  28. autogluon/timeseries/models/ensemble/array_based/abstract.py +236 -0
  29. autogluon/timeseries/models/ensemble/array_based/models.py +73 -0
  30. autogluon/timeseries/models/ensemble/array_based/regressor/__init__.py +12 -0
  31. autogluon/timeseries/models/ensemble/array_based/regressor/abstract.py +88 -0
  32. autogluon/timeseries/models/ensemble/array_based/regressor/linear_stacker.py +167 -0
  33. autogluon/timeseries/models/ensemble/array_based/regressor/per_quantile_tabular.py +94 -0
  34. autogluon/timeseries/models/ensemble/array_based/regressor/tabular.py +107 -0
  35. autogluon/timeseries/models/ensemble/ensemble_selection.py +167 -0
  36. autogluon/timeseries/models/ensemble/per_item_greedy.py +162 -0
  37. autogluon/timeseries/models/ensemble/weighted/__init__.py +8 -0
  38. autogluon/timeseries/models/ensemble/weighted/abstract.py +40 -0
  39. autogluon/timeseries/models/ensemble/weighted/basic.py +78 -0
  40. autogluon/timeseries/models/ensemble/weighted/greedy.py +57 -0
  41. autogluon/timeseries/models/gluonts/__init__.py +3 -1
  42. autogluon/timeseries/models/gluonts/abstract.py +583 -0
  43. autogluon/timeseries/models/gluonts/dataset.py +109 -0
  44. autogluon/timeseries/models/gluonts/{torch/models.py → models.py} +185 -44
  45. autogluon/timeseries/models/local/__init__.py +1 -10
  46. autogluon/timeseries/models/local/abstract_local_model.py +150 -97
  47. autogluon/timeseries/models/local/naive.py +31 -23
  48. autogluon/timeseries/models/local/npts.py +6 -2
  49. autogluon/timeseries/models/local/statsforecast.py +99 -112
  50. autogluon/timeseries/models/multi_window/multi_window_model.py +99 -40
  51. autogluon/timeseries/models/registry.py +64 -0
  52. autogluon/timeseries/models/toto/__init__.py +3 -0
  53. autogluon/timeseries/models/toto/_internal/__init__.py +9 -0
  54. autogluon/timeseries/models/toto/_internal/backbone/__init__.py +3 -0
  55. autogluon/timeseries/models/toto/_internal/backbone/attention.py +196 -0
  56. autogluon/timeseries/models/toto/_internal/backbone/backbone.py +262 -0
  57. autogluon/timeseries/models/toto/_internal/backbone/distribution.py +70 -0
  58. autogluon/timeseries/models/toto/_internal/backbone/kvcache.py +136 -0
  59. autogluon/timeseries/models/toto/_internal/backbone/rope.py +89 -0
  60. autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
  61. autogluon/timeseries/models/toto/_internal/backbone/scaler.py +305 -0
  62. autogluon/timeseries/models/toto/_internal/backbone/transformer.py +333 -0
  63. autogluon/timeseries/models/toto/_internal/dataset.py +165 -0
  64. autogluon/timeseries/models/toto/_internal/forecaster.py +423 -0
  65. autogluon/timeseries/models/toto/dataloader.py +108 -0
  66. autogluon/timeseries/models/toto/hf_pretrained_model.py +118 -0
  67. autogluon/timeseries/models/toto/model.py +236 -0
  68. autogluon/timeseries/predictor.py +826 -305
  69. autogluon/timeseries/regressor.py +253 -0
  70. autogluon/timeseries/splitter.py +10 -31
  71. autogluon/timeseries/trainer/__init__.py +2 -3
  72. autogluon/timeseries/trainer/ensemble_composer.py +439 -0
  73. autogluon/timeseries/trainer/model_set_builder.py +256 -0
  74. autogluon/timeseries/trainer/prediction_cache.py +149 -0
  75. autogluon/timeseries/trainer/trainer.py +1298 -0
  76. autogluon/timeseries/trainer/utils.py +17 -0
  77. autogluon/timeseries/transforms/__init__.py +2 -0
  78. autogluon/timeseries/transforms/covariate_scaler.py +164 -0
  79. autogluon/timeseries/transforms/target_scaler.py +149 -0
  80. autogluon/timeseries/utils/constants.py +10 -0
  81. autogluon/timeseries/utils/datetime/base.py +38 -20
  82. autogluon/timeseries/utils/datetime/lags.py +18 -16
  83. autogluon/timeseries/utils/datetime/seasonality.py +14 -14
  84. autogluon/timeseries/utils/datetime/time_features.py +17 -14
  85. autogluon/timeseries/utils/features.py +317 -53
  86. autogluon/timeseries/utils/forecast.py +31 -17
  87. autogluon/timeseries/utils/timer.py +173 -0
  88. autogluon/timeseries/utils/warning_filters.py +44 -6
  89. autogluon/timeseries/version.py +2 -1
  90. autogluon.timeseries-1.4.1b20251210-py3.11-nspkg.pth +1 -0
  91. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/METADATA +71 -47
  92. autogluon_timeseries-1.4.1b20251210.dist-info/RECORD +103 -0
  93. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/WHEEL +1 -1
  94. autogluon/timeseries/configs/presets_configs.py +0 -11
  95. autogluon/timeseries/evaluator.py +0 -6
  96. autogluon/timeseries/models/ensemble/greedy_ensemble.py +0 -170
  97. autogluon/timeseries/models/gluonts/abstract_gluonts.py +0 -550
  98. autogluon/timeseries/models/gluonts/torch/__init__.py +0 -0
  99. autogluon/timeseries/models/presets.py +0 -325
  100. autogluon/timeseries/trainer/abstract_trainer.py +0 -1144
  101. autogluon/timeseries/trainer/auto_trainer.py +0 -74
  102. autogluon.timeseries-1.0.1b20240304-py3.8-nspkg.pth +0 -1
  103. autogluon.timeseries-1.0.1b20240304.dist-info/RECORD +0 -58
  104. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info/licenses}/LICENSE +0 -0
  105. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info/licenses}/NOTICE +0 -0
  106. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/namespace_packages.txt +0 -0
  107. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/top_level.txt +0 -0
  108. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/zip-safe +0 -0
@@ -1,16 +1,21 @@
1
+ import copy
1
2
  import logging
2
3
  import math
3
- import os
4
4
  import time
5
- from typing import Any, Dict, List, Optional, Tuple
5
+ import warnings
6
+ from typing import Any, Callable, Collection, Type
6
7
 
7
8
  import numpy as np
8
9
  import pandas as pd
9
10
  from sklearn.base import BaseEstimator
10
11
 
11
12
  import autogluon.core as ag
12
- from autogluon.tabular import TabularPredictor
13
- from autogluon.timeseries.dataset.ts_dataframe import ITEMID, TIMESTAMP, TimeSeriesDataFrame
13
+ from autogluon.core.models import AbstractModel as AbstractTabularModel
14
+ from autogluon.features import AutoMLPipelineFeatureGenerator
15
+ from autogluon.tabular.registry import ag_model_registry
16
+ from autogluon.timeseries.dataset import TimeSeriesDataFrame
17
+ from autogluon.timeseries.metrics.abstract import TimeSeriesScorer
18
+ from autogluon.timeseries.metrics.utils import in_sample_squared_seasonal_error
14
19
  from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel
15
20
  from autogluon.timeseries.models.local import SeasonalNaiveModel
16
21
  from autogluon.timeseries.utils.datetime import (
@@ -18,52 +23,53 @@ from autogluon.timeseries.utils.datetime import (
18
23
  get_seasonality,
19
24
  get_time_features_for_frequency,
20
25
  )
21
- from autogluon.timeseries.utils.forecast import get_forecast_horizon_index_ts_dataframe
22
- from autogluon.timeseries.utils.warning_filters import warning_filter
26
+ from autogluon.timeseries.utils.warning_filters import set_loggers_level, warning_filter
23
27
 
24
- logger = logging.getLogger(__name__)
25
-
26
- MLF_TARGET = "y"
27
- MLF_ITEMID = "unique_id"
28
- MLF_TIMESTAMP = "ds"
28
+ from .utils import MLF_ITEMID, MLF_TARGET, MLF_TIMESTAMP
29
29
 
30
+ logger = logging.getLogger(__name__)
30
31
 
31
- class TabularEstimator(BaseEstimator):
32
- """Scikit-learn compatible interface for TabularPredictor."""
33
32
 
34
- def __init__(self, predictor_init_kwargs: Optional[dict] = None, predictor_fit_kwargs: Optional[dict] = None):
35
- self.predictor_init_kwargs = predictor_init_kwargs if predictor_init_kwargs is not None else {}
36
- self.predictor_fit_kwargs = predictor_fit_kwargs if predictor_fit_kwargs is not None else {}
33
+ class TabularModel(BaseEstimator):
34
+ """A scikit-learn compatible wrapper for arbitrary autogluon.tabular models"""
37
35
 
38
- def get_params(self, deep: bool = True) -> dict:
39
- return {
40
- "predictor_init_kwargs": self.predictor_init_kwargs,
41
- "predictor_fit_kwargs": self.predictor_fit_kwargs,
42
- }
36
+ def __init__(self, model_class: Type[AbstractTabularModel], model_kwargs: dict | None = None):
37
+ self.model_class = model_class
38
+ self.model_kwargs = {} if model_kwargs is None else model_kwargs
39
+ self.feature_pipeline = AutoMLPipelineFeatureGenerator(verbosity=0)
43
40
 
44
- def fit(self, X: pd.DataFrame, y: pd.Series) -> "TabularEstimator":
45
- assert isinstance(X, pd.DataFrame) and isinstance(y, pd.Series)
46
- df = pd.concat([X, y.rename(MLF_TARGET).to_frame()], axis=1)
47
- self.predictor = TabularPredictor(**self.predictor_init_kwargs)
48
- with warning_filter():
49
- self.predictor.fit(df, **self.predictor_fit_kwargs)
41
+ def fit(self, X: pd.DataFrame, y: pd.Series, X_val: pd.DataFrame, y_val: pd.Series, **kwargs):
42
+ self.model = self.model_class(**self.model_kwargs)
43
+ X = self.feature_pipeline.fit_transform(X=X)
44
+ X_val = self.feature_pipeline.transform(X=X_val)
45
+ self.model.fit(X=X, y=y, X_val=X_val, y_val=y_val, **kwargs)
50
46
  return self
51
47
 
52
- def predict(self, X: pd.DataFrame) -> np.ndarray:
53
- assert isinstance(X, pd.DataFrame)
54
- return self.predictor.predict(X).values
48
+ def predict(self, X: pd.DataFrame, **kwargs):
49
+ X = self.feature_pipeline.transform(X=X)
50
+ return self.model.predict(X=X, **kwargs)
51
+
52
+ def get_params(self, deep=True):
53
+ params = {"model_class": self.model_class, "model_kwargs": self.model_kwargs}
54
+ if deep:
55
+ return copy.deepcopy(params)
56
+ else:
57
+ return params
55
58
 
56
59
 
57
60
  class AbstractMLForecastModel(AbstractTimeSeriesModel):
61
+ _supports_known_covariates = True
62
+ _supports_static_features = True
63
+
58
64
  def __init__(
59
65
  self,
60
- freq: Optional[str] = None,
66
+ freq: str | None = None,
61
67
  prediction_length: int = 1,
62
- path: Optional[str] = None,
63
- name: Optional[str] = None,
64
- eval_metric: str = None,
65
- hyperparameters: Dict[str, Any] = None,
66
- **kwargs, # noqa
68
+ path: str | None = None,
69
+ name: str | None = None,
70
+ eval_metric: str | TimeSeriesScorer | None = None,
71
+ hyperparameters: dict[str, Any] | None = None,
72
+ **kwargs,
67
73
  ):
68
74
  super().__init__(
69
75
  path=path,
@@ -78,44 +84,90 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
78
84
  from mlforecast.target_transforms import BaseTargetTransform
79
85
 
80
86
  self._sum_of_differences: int = 0 # number of time steps removed from each series by differencing
81
- self._max_ts_length: Optional[int] = None
82
- self._target_lags: Optional[List[int]] = None
83
- self._date_features: Optional[List[str]] = None
84
- self._mlf: Optional[MLForecast] = None
85
- self._scaler: Optional[BaseTargetTransform] = None
86
- self._residuals_std_per_item: Optional[pd.Series] = None
87
- self._avg_residuals_std: Optional[float] = None
88
-
89
- def _get_extra_tabular_init_kwargs(self) -> dict:
90
- raise NotImplementedError
87
+ self._max_ts_length: int | None = None
88
+ self._target_lags: np.ndarray
89
+ self._date_features: list[Callable]
90
+ self._mlf: MLForecast
91
+ self._scaler: BaseTargetTransform | None = None
92
+ self._residuals_std_per_item: pd.Series
93
+ self._train_target_median: float | None = None
94
+ self._non_boolean_real_covariates: list[str] = []
95
+
96
+ def _initialize_transforms_and_regressor(self):
97
+ super()._initialize_transforms_and_regressor()
98
+ # Do not create a scaler in the model, scaler will be passed to MLForecast
99
+ self.target_scaler = None
91
100
 
92
- def _get_model_params(self) -> dict:
93
- model_params = super()._get_model_params().copy()
94
- model_params.setdefault("max_num_items", 20_000)
95
- model_params.setdefault("max_num_samples", 1_000_000)
96
- model_params.setdefault("tabular_hyperparameters", {"GBM": {}})
97
- model_params.setdefault("tabular_fit_kwargs", {})
98
- return model_params
101
+ @property
102
+ def allowed_hyperparameters(self) -> list[str]:
103
+ return super().allowed_hyperparameters + [
104
+ "lags",
105
+ "date_features",
106
+ "differences",
107
+ "model_name",
108
+ "model_hyperparameters",
109
+ "max_num_items",
110
+ "max_num_samples",
111
+ "lag_transforms",
112
+ ]
113
+
114
+ def preprocess(
115
+ self,
116
+ data: TimeSeriesDataFrame,
117
+ known_covariates: TimeSeriesDataFrame | None = None,
118
+ is_train: bool = False,
119
+ **kwargs,
120
+ ) -> tuple[TimeSeriesDataFrame, TimeSeriesDataFrame | None]:
121
+ if is_train:
122
+ # All-NaN series are removed; partially-NaN series in train_data are handled inside _generate_train_val_dfs
123
+ all_nan_items = data.item_ids[
124
+ data[self.target].isna().groupby(TimeSeriesDataFrame.ITEMID, sort=False).all()
125
+ ]
126
+ if len(all_nan_items):
127
+ data = data.query("item_id not in @all_nan_items")
128
+ else:
129
+ data = data.fill_missing_values()
130
+ # Fill time series consisting of all NaNs with the median of target in train_data
131
+ if data.isna().any(axis=None):
132
+ data[self.target] = data[self.target].fillna(value=self._train_target_median)
133
+ return data, known_covariates
99
134
 
100
- def _get_mlforecast_init_args(self, train_data: TimeSeriesDataFrame, model_params: dict) -> dict:
101
- # TODO: Support lag generation for all pandas frequencies
102
- # TODO: Support date_feature generation for all pandas frequencies
135
+ def _get_default_hyperparameters(self) -> dict[str, Any]:
136
+ return {
137
+ "max_num_items": 20_000,
138
+ "max_num_samples": 1_000_000,
139
+ "model_name": "GBM",
140
+ "model_hyperparameters": {},
141
+ }
142
+
143
+ def _create_tabular_model(self, model_name: str, model_hyperparameters: dict[str, Any]) -> TabularModel:
144
+ raise NotImplementedError
145
+
146
+ def _get_mlforecast_init_args(
147
+ self, train_data: TimeSeriesDataFrame, model_params: dict[str, Any]
148
+ ) -> dict[str, Any]:
103
149
  from mlforecast.target_transforms import Differences
104
150
 
105
- from .utils import MeanAbsScaler, StandardScaler
151
+ from .transforms import MLForecastScaler
106
152
 
107
153
  lags = model_params.get("lags")
108
154
  if lags is None:
155
+ assert self.freq is not None
109
156
  lags = get_lags_for_frequency(self.freq)
110
157
  self._target_lags = np.array(sorted(set(lags)), dtype=np.int64)
111
158
 
112
159
  date_features = model_params.get("date_features")
113
160
  if date_features is None:
114
161
  date_features = get_time_features_for_frequency(self.freq)
115
- self._date_features = date_features
162
+ known_covariates = self.covariate_metadata.known_covariates
163
+ conflicting = [f.__name__ for f in date_features if f.__name__ in known_covariates]
164
+ if conflicting:
165
+ logger.info(f"\tRemoved automatic date_features {conflicting} since they clash with known_covariates")
166
+ self._date_features = [f for f in date_features if f.__name__ not in known_covariates]
116
167
 
117
168
  target_transforms = []
118
169
  differences = model_params.get("differences")
170
+ assert isinstance(differences, Collection)
119
171
 
120
172
  ts_lengths = train_data.num_timesteps_per_item()
121
173
  required_ts_length = sum(differences) + 1
@@ -132,25 +184,22 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
132
184
  target_transforms.append(Differences(differences))
133
185
  self._sum_of_differences = sum(differences)
134
186
 
135
- scaler_name = model_params.get("scaler")
136
- if scaler_name is None:
137
- pass
138
- elif scaler_name == "standard":
139
- self._scaler = StandardScaler()
140
- elif scaler_name == "mean_abs":
141
- self._scaler = MeanAbsScaler()
142
- else:
143
- logger.warning(
144
- f"Unrecognized `scaler` {scaler_name} (supported options: ['standard', 'mean_abs', None]). Scaling disabled."
187
+ if "target_scaler" in model_params and "scaler" in model_params:
188
+ warnings.warn(
189
+ f"Both 'target_scaler' and 'scaler' hyperparameters are provided to {self.__class__.__name__}. "
190
+ "Please only set the 'target_scaler' parameter."
145
191
  )
146
-
147
- if self._scaler is not None:
192
+ # Support "scaler" for backward compatibility
193
+ scaler_type = model_params.get("target_scaler", model_params.get("scaler"))
194
+ if scaler_type is not None:
195
+ self._scaler = MLForecastScaler(scaler_type=scaler_type)
148
196
  target_transforms.append(self._scaler)
149
197
 
150
198
  return {
151
199
  "lags": self._target_lags.tolist(),
152
200
  "date_features": self._date_features,
153
201
  "target_transforms": target_transforms,
202
+ "lag_transforms": model_params.get("lag_transforms"),
154
203
  }
155
204
 
156
205
  def _mask_df(self, df: pd.DataFrame) -> pd.DataFrame:
@@ -162,13 +211,13 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
162
211
  return df
163
212
 
164
213
  @staticmethod
165
- def _shorten_all_series(mlforecast_df: pd.DataFrame, max_length: int):
214
+ def _shorten_all_series(mlforecast_df: pd.DataFrame, max_length: int) -> pd.DataFrame:
166
215
  logger.debug(f"Shortening all series to at most {max_length}")
167
216
  return mlforecast_df.groupby(MLF_ITEMID, as_index=False, sort=False).tail(max_length)
168
217
 
169
218
  def _generate_train_val_dfs(
170
- self, data: TimeSeriesDataFrame, max_num_items: Optional[int] = None, max_num_samples: Optional[int] = None
171
- ) -> Tuple[pd.DataFrame, pd.DataFrame]:
219
+ self, data: TimeSeriesDataFrame, max_num_items: int | None = None, max_num_samples: int | None = None
220
+ ) -> tuple[pd.DataFrame, pd.DataFrame]:
172
221
  # Exclude items that are too short for chosen differences - otherwise exception will be raised
173
222
  if self._sum_of_differences > 0:
174
223
  ts_lengths = data.num_timesteps_per_item()
@@ -181,6 +230,10 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
181
230
  items_to_keep = data.item_ids.to_series().sample(n=int(max_num_items)) # noqa: F841
182
231
  data = data.query("item_id in @items_to_keep")
183
232
 
233
+ # MLForecast.preprocess does not support missing values, but we will exclude them later from the training set
234
+ missing_entries = data.index[data[self.target].isna()]
235
+ data = data.fill_missing_values()
236
+
184
237
  num_items = data.num_items
185
238
  mlforecast_df = self._to_mlforecast_df(data, data.static_features)
186
239
 
@@ -193,10 +246,14 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
193
246
  # Unless we set static_features=[], MLForecast interprets all known covariates as static features
194
247
  df = self._mlf.preprocess(mlforecast_df, dropna=False, static_features=[])
195
248
  # df.query results in 2x memory saving compared to df.dropna(subset="y")
196
- df = df.query("y.notnull()")
249
+ df = df.query("y.notnull()") # type: ignore
197
250
 
198
251
  df = self._mask_df(df)
199
252
 
253
+ # We remove originally missing values filled via imputation from the training set
254
+ if len(missing_entries):
255
+ df = df.set_index(["unique_id", "ds"]).drop(missing_entries, errors="ignore").reset_index()
256
+
200
257
  if max_num_samples is not None and len(df) > max_num_samples:
201
258
  df = df.sample(n=max_num_samples)
202
259
 
@@ -208,12 +265,12 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
208
265
  val_df = grouped_df.tail(val_rows_per_item)
209
266
  logger.debug(f"train_df shape: {train_df.shape}, val_df shape: {val_df.shape}")
210
267
 
211
- return train_df.drop(columns=[MLF_TIMESTAMP]), val_df.drop(columns=[MLF_TIMESTAMP])
268
+ return train_df.drop(columns=[MLF_TIMESTAMP]), val_df.drop(columns=[MLF_TIMESTAMP]) # type: ignore
212
269
 
213
270
  def _to_mlforecast_df(
214
271
  self,
215
272
  data: TimeSeriesDataFrame,
216
- static_features: pd.DataFrame,
273
+ static_features: pd.DataFrame | None,
217
274
  include_target: bool = True,
218
275
  ) -> pd.DataFrame:
219
276
  """Convert TimeSeriesDataFrame to a format expected by MLForecast methods `predict` and `preprocess`.
@@ -221,15 +278,33 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
221
278
  Each row contains unique_id, ds, y, and (optionally) known covariates & static features.
222
279
  """
223
280
  # TODO: Add support for past_covariates
224
- selected_columns = self.metadata.known_covariates_real.copy()
225
- column_name_mapping = {ITEMID: MLF_ITEMID, TIMESTAMP: MLF_TIMESTAMP}
281
+ selected_columns = self.covariate_metadata.known_covariates.copy()
282
+ column_name_mapping = {TimeSeriesDataFrame.ITEMID: MLF_ITEMID, TimeSeriesDataFrame.TIMESTAMP: MLF_TIMESTAMP}
226
283
  if include_target:
227
284
  selected_columns += [self.target]
228
285
  column_name_mapping[self.target] = MLF_TARGET
229
286
 
230
287
  df = pd.DataFrame(data)[selected_columns].reset_index()
231
288
  if static_features is not None:
232
- df = pd.merge(df, static_features, how="left", on=ITEMID, suffixes=(None, "_static_feat"))
289
+ df = pd.merge(
290
+ df, static_features, how="left", on=TimeSeriesDataFrame.ITEMID, suffixes=(None, "_static_feat")
291
+ )
292
+
293
+ for col in self._non_boolean_real_covariates:
294
+ # Normalize non-boolean features using mean_abs scaling
295
+ df[f"__scaled_{col}"] = (
296
+ df[col]
297
+ / df[col]
298
+ .abs()
299
+ .groupby(df[TimeSeriesDataFrame.ITEMID])
300
+ .mean()
301
+ .reindex(df[TimeSeriesDataFrame.ITEMID])
302
+ .values
303
+ )
304
+
305
+ # Convert float64 to float32 to reduce memory usage
306
+ float64_cols = list(df.select_dtypes(include="float64"))
307
+ df[float64_cols] = df[float64_cols].astype("float32")
233
308
 
234
309
  # We assume that df is sorted by 'unique_id' inside `TimeSeriesPredictor._check_and_prepare_data_frame`
235
310
  return df.rename(columns=column_name_mapping)
@@ -237,19 +312,26 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
237
312
  def _fit(
238
313
  self,
239
314
  train_data: TimeSeriesDataFrame,
240
- val_data: Optional[TimeSeriesDataFrame] = None,
241
- time_limit: Optional[int] = None,
315
+ val_data: TimeSeriesDataFrame | None = None,
316
+ time_limit: float | None = None,
317
+ num_cpus: int | None = None,
318
+ num_gpus: int | None = None,
242
319
  verbosity: int = 2,
243
320
  **kwargs,
244
321
  ) -> None:
245
322
  from mlforecast import MLForecast
246
323
 
247
324
  self._check_fit_params()
325
+ self._log_unused_hyperparameters()
248
326
  fit_start_time = time.time()
249
- # TabularEstimator is passed to MLForecast later to include tuning_data
250
- model_params = self._get_model_params()
327
+ self._train_target_median = train_data[self.target].median()
328
+ for col in self.covariate_metadata.known_covariates_real:
329
+ if not set(train_data[col].unique()) == set([0, 1]):
330
+ self._non_boolean_real_covariates.append(col)
331
+ model_params = self.get_hyperparameters()
251
332
 
252
333
  mlforecast_init_args = self._get_mlforecast_init_args(train_data, model_params)
334
+ assert self.freq is not None
253
335
  self._mlf = MLForecast(models={}, freq=self.freq, **mlforecast_init_args)
254
336
 
255
337
  # We generate train/val splits from train_data and ignore val_data to avoid overfitting
@@ -259,54 +341,65 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
259
341
  max_num_samples=model_params["max_num_samples"],
260
342
  )
261
343
 
262
- estimator = TabularEstimator(
263
- predictor_init_kwargs={
264
- "path": os.path.join(self.path, "tabular_predictor"),
265
- "verbosity": verbosity - 2,
266
- "label": MLF_TARGET,
267
- **self._get_extra_tabular_init_kwargs(),
268
- },
269
- predictor_fit_kwargs={
270
- "tuning_data": val_df.drop(columns=[MLF_ITEMID]),
271
- "time_limit": None if time_limit is None else time_limit - (time.time() - fit_start_time),
272
- "hyperparameters": model_params["tabular_hyperparameters"],
273
- **model_params["tabular_fit_kwargs"],
274
- },
275
- )
276
- self._mlf.models = {"mean": estimator}
344
+ with set_loggers_level(regex=r"^autogluon\.(tabular|features).*", level=logging.ERROR):
345
+ tabular_model = self._create_tabular_model(
346
+ model_name=model_params["model_name"], model_hyperparameters=model_params["model_hyperparameters"]
347
+ )
348
+ tabular_model.fit(
349
+ X=train_df.drop(columns=[MLF_TARGET, MLF_ITEMID]),
350
+ y=train_df[MLF_TARGET],
351
+ X_val=val_df.drop(columns=[MLF_TARGET, MLF_ITEMID]),
352
+ y_val=val_df[MLF_TARGET],
353
+ time_limit=(None if time_limit is None else time_limit - (time.time() - fit_start_time)),
354
+ verbosity=verbosity - 1,
355
+ )
277
356
 
278
- with warning_filter():
279
- self._mlf.fit_models(X=train_df.drop(columns=[MLF_TARGET, MLF_ITEMID]), y=train_df[MLF_TARGET])
357
+ # We directly insert the trained model into models_ since calling _mlf.fit_models does not support X_val, y_val
358
+ self._mlf.models_ = {"mean": tabular_model}
280
359
 
281
360
  self._save_residuals_std(val_df)
282
361
 
362
+ def get_tabular_model(self) -> TabularModel:
363
+ """Get the underlying tabular regression model."""
364
+ assert "mean" in self._mlf.models_, "Call `fit` before calling `get_tabular_model`"
365
+ mean_estimator = self._mlf.models_["mean"]
366
+ assert isinstance(mean_estimator, TabularModel)
367
+ return mean_estimator
368
+
283
369
  def _save_residuals_std(self, val_df: pd.DataFrame) -> None:
284
370
  """Compute standard deviation of residuals for each item using the validation set.
285
371
 
286
- Saves per-item residuals to `self.residuals_std_per_item` and average std to `self._avg_residuals_std`.
372
+ Saves per-item residuals to `self.residuals_std_per_item`.
287
373
  """
288
- residuals = val_df[MLF_TARGET] - self._mlf.models_["mean"].predict(val_df)
289
- self._residuals_std_per_item = residuals.pow(2.0).groupby(val_df[MLF_ITEMID], sort=False).mean().pow(0.5)
290
- self._avg_residuals_std = np.sqrt(residuals.pow(2.0).mean())
374
+ residuals_df = val_df[[MLF_ITEMID, MLF_TARGET]]
375
+ mean_estimator = self.get_tabular_model()
291
376
 
292
- def _get_scale_per_item(self, item_ids: pd.Index) -> pd.Series:
293
- """Extract the '_scale' values from the scaler object, if available."""
294
- raise NotImplementedError
377
+ residuals_df = residuals_df.assign(y_pred=mean_estimator.predict(val_df))
378
+ if self._scaler is not None:
379
+ # Scaler expects to find column MLF_TIMESTAMP even though it's not used - fill with dummy
380
+ residuals_df = residuals_df.assign(**{MLF_TIMESTAMP: np.datetime64("2010-01-01")})
381
+ residuals_df = self._scaler.inverse_transform(residuals_df)
382
+
383
+ assert isinstance(residuals_df, pd.DataFrame)
384
+ residuals = residuals_df[MLF_TARGET] - residuals_df["y_pred"]
385
+ self._residuals_std_per_item = (
386
+ residuals.pow(2.0).groupby(val_df[MLF_ITEMID].values, sort=False).mean().pow(0.5) # type: ignore
387
+ )
295
388
 
296
389
  def _remove_short_ts_and_generate_fallback_forecast(
297
390
  self,
298
391
  data: TimeSeriesDataFrame,
299
- known_covariates: Optional[TimeSeriesDataFrame] = None,
300
- ) -> Tuple[TimeSeriesDataFrame, Optional[TimeSeriesDataFrame], Optional[TimeSeriesDataFrame]]:
392
+ known_covariates: TimeSeriesDataFrame | None = None,
393
+ ) -> tuple[TimeSeriesDataFrame, TimeSeriesDataFrame, TimeSeriesDataFrame | None]:
301
394
  """Remove series that are too short for chosen differencing from data and generate naive forecast for them.
302
395
 
303
396
  Returns
304
397
  -------
305
- data_long : TimeSeriesDataFrame
398
+ data_long
306
399
  Data containing only time series that are long enough for the model to predict.
307
- known_covariates_long : TimeSeriesDataFrame or None
400
+ known_covariates_long
308
401
  Future known covariates containing only time series that are long enough for the model to predict.
309
- forecast_for_short_series : TimeSeriesDataFrame or None
402
+ forecast_for_short_series
310
403
  Seasonal naive forecast for short series, if there are any in the dataset.
311
404
  """
312
405
  ts_lengths = data.num_timesteps_per_item()
@@ -318,7 +411,12 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
318
411
  "Fallback model SeasonalNaive is used for these time series."
319
412
  )
320
413
  data_short = data.query("item_id in @short_series")
321
- seasonal_naive = SeasonalNaiveModel(freq=self.freq, prediction_length=self.prediction_length)
414
+ seasonal_naive = SeasonalNaiveModel(
415
+ freq=self.freq,
416
+ prediction_length=self.prediction_length,
417
+ target=self.target,
418
+ quantile_levels=self.quantile_levels,
419
+ )
322
420
  seasonal_naive.fit(train_data=data_short)
323
421
  forecast_for_short_series = seasonal_naive.predict(data_short)
324
422
 
@@ -333,35 +431,41 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
333
431
  forecast_for_short_series = None
334
432
  return data_long, known_covariates_long, forecast_for_short_series
335
433
 
336
- def _add_gaussian_quantiles(self, predictions: pd.DataFrame, repeated_item_ids: pd.Series):
434
+ def _add_gaussian_quantiles(
435
+ self, predictions: pd.DataFrame, repeated_item_ids: pd.Series, past_target: pd.Series
436
+ ) -> pd.DataFrame:
337
437
  """
338
438
  Add quantile levels assuming that residuals follow normal distribution
339
439
  """
340
440
  from scipy.stats import norm
341
441
 
342
- scale_per_item = self._get_scale_per_item(repeated_item_ids.unique())
343
442
  num_items = int(len(predictions) / self.prediction_length)
344
443
  sqrt_h = np.sqrt(np.arange(1, self.prediction_length + 1))
345
444
  # Series where normal_scale_per_timestep.loc[item_id].loc[N] = sqrt(1 + N) for N in range(prediction_length)
346
445
  normal_scale_per_timestep = pd.Series(np.tile(sqrt_h, num_items), index=repeated_item_ids)
347
446
 
348
447
  residuals_std_per_timestep = self._residuals_std_per_item.reindex(repeated_item_ids)
349
- # Use avg_residuals_std in case unseen item received for prediction
350
- if residuals_std_per_timestep.isna().any():
351
- residuals_std_per_timestep = residuals_std_per_timestep.fillna(value=self._avg_residuals_std)
352
- std_per_timestep = residuals_std_per_timestep * scale_per_item * normal_scale_per_timestep
448
+ # Use in-sample seasonal error in for items not seen during fit
449
+ items_not_seen_during_fit = residuals_std_per_timestep.index[residuals_std_per_timestep.isna()].unique()
450
+ if len(items_not_seen_during_fit) > 0:
451
+ scale_for_new_items: pd.Series = in_sample_squared_seasonal_error(
452
+ y_past=past_target.loc[items_not_seen_during_fit]
453
+ ).pow(0.5)
454
+ residuals_std_per_timestep = residuals_std_per_timestep.fillna(scale_for_new_items)
455
+
456
+ std_per_timestep = residuals_std_per_timestep * normal_scale_per_timestep
353
457
  for q in self.quantile_levels:
354
458
  predictions[str(q)] = predictions["mean"] + norm.ppf(q) * std_per_timestep.to_numpy()
355
459
  return predictions
356
460
 
357
- def _more_tags(self) -> dict:
358
- return {"can_refit_full": True}
461
+ def _more_tags(self) -> dict[str, Any]:
462
+ return {"allow_nan": True, "can_refit_full": True}
359
463
 
360
464
 
361
465
  class DirectTabularModel(AbstractMLForecastModel):
362
- """Predict all future time series values simultaneously using TabularPredictor from AutoGluon-Tabular.
466
+ """Predict all future time series values simultaneously using a regression model from AutoGluon-Tabular.
363
467
 
364
- A single TabularPredictor is used to forecast all future time series values using the following features:
468
+ A single tabular model is used to forecast all future time series values using the following features:
365
469
 
366
470
  - lag features (observed time series values) based on ``freq`` of the data
367
471
  - time features (e.g., day of the week) based on the timestamp of the measurement
@@ -370,8 +474,8 @@ class DirectTabularModel(AbstractMLForecastModel):
370
474
 
371
475
  Features not known during the forecast horizon (e.g., future target values) are replaced by NaNs.
372
476
 
373
- If ``eval_metric.needs_quantile``, the TabularPredictor will be trained with ``"quantile"`` problem type.
374
- Otherwise, TabularPredictor will be trained with ``"regression"`` problem type, and dummy quantiles will be
477
+ If ``eval_metric.needs_quantile``, the tabular regression model will be trained with ``"quantile"`` problem type.
478
+ Otherwise, the model will be trained with ``"regression"`` problem type, and dummy quantiles will be
375
479
  obtained by assuming that the residuals follow zero-mean normal distribution.
376
480
 
377
481
  Based on the `mlforecast <https://github.com/Nixtla/mlforecast>`_ library.
@@ -379,45 +483,55 @@ class DirectTabularModel(AbstractMLForecastModel):
379
483
 
380
484
  Other Parameters
381
485
  ----------------
382
- lags : List[int], default = None
486
+ lags : list[int], default = None
383
487
  Lags of the target that will be used as features for predictions. If None, will be determined automatically
384
488
  based on the frequency of the data.
385
- date_features : List[Union[str, Callable]], default = None
489
+ date_features : list[str | Callable], default = None
386
490
  Features computed from the dates. Can be pandas date attributes or functions that will take the dates as input.
387
491
  If None, will be determined automatically based on the frequency of the data.
388
- differences : List[int], default = []
492
+ differences : list[int], default = []
389
493
  Differences to take of the target before computing the features. These are restored at the forecasting step.
390
- If None, will be set to ``[seasonal_period]``, where seasonal_period is determined based on the data frequency.
391
494
  Defaults to no differencing.
392
- scaler : {"standard", "mean_abs", None}, default = "mean_abs"
393
- Scaling applied to each time series.
394
- tabular_hyperparameters : Dict[Dict[str, Any]], optional
395
- Hyperparameters dictionary passed to ``TabularPredictor.fit``. Contains the names of models that should be fit.
396
- Defaults to ``{"GBM": {}}``.
397
- tabular_fit_kwargs : Dict[str, Any], optional
398
- Additional keyword arguments passed to ``TabularPredictor.fit``. Defaults to an empty dict.
495
+ target_scaler : {"standard", "mean_abs", "min_max", "robust", None}, default = "mean_abs"
496
+ Scaling applied to each time series. Scaling is applied after differencing.
497
+ model_name : str, default = "GBM"
498
+ Name of the tabular regression model. See ``autogluon.tabular.registry.ag_model_registry`` or
499
+ `the documentation <https://auto.gluon.ai/stable/api/autogluon.tabular.models.html>`_ for the list of available
500
+ tabular models.
501
+ model_hyperparameters : dict[str, Any], optional
502
+ Hyperparameters passed to the tabular regression model.
399
503
  max_num_items : int or None, default = 20_000
400
504
  If not None, the model will randomly select this many time series for training and validation.
401
505
  max_num_samples : int or None, default = 1_000_000
402
- If not None, training dataset passed to TabularPredictor will contain at most this many rows (starting from the
403
- end of each time series).
506
+ If not None, training dataset passed to the tabular regression model will contain at most this many rows
507
+ (starting from the end of each time series).
404
508
  """
405
509
 
510
+ ag_priority = 85
511
+
406
512
  @property
407
513
  def is_quantile_model(self) -> bool:
408
514
  return self.eval_metric.needs_quantile
409
515
 
410
- def _get_model_params(self) -> dict:
411
- model_params = super()._get_model_params()
412
- model_params.setdefault("scaler", "mean_abs")
413
- model_params.setdefault("differences", [])
516
+ def get_hyperparameters(self) -> dict[str, Any]:
517
+ model_params = super().get_hyperparameters()
518
+ # We don't set 'target_scaler' if user already provided 'scaler' to avoid overriding the user-provided value
519
+ if "scaler" not in model_params:
520
+ model_params.setdefault("target_scaler", "mean_abs")
521
+ if "differences" not in model_params or model_params["differences"] is None:
522
+ model_params["differences"] = []
523
+ if "lag_transforms" in model_params:
524
+ model_params.pop("lag_transforms")
525
+ logger.warning(f"{self.name} does not support the 'lag_transforms' hyperparameter.")
414
526
  return model_params
415
527
 
416
528
  def _mask_df(self, df: pd.DataFrame) -> pd.DataFrame:
417
529
  """Apply a mask that mimics the situation at prediction time when target/covariates are unknown during the
418
530
  forecast horizon.
419
531
  """
420
- num_hidden = np.random.randint(0, self.prediction_length, size=len(df))
532
+ # Fix seed to make the model deterministic
533
+ rng = np.random.default_rng(seed=123)
534
+ num_hidden = rng.integers(0, self.prediction_length, size=len(df))
421
535
  lag_cols = [f"lag{lag}" for lag in self._target_lags]
422
536
  mask = num_hidden[:, None] < self._target_lags[None] # shape [len(num_hidden), len(_target_lags)]
423
537
  # use df.loc[:, lag_cols] instead of df[lag_cols] to avoid SettingWithCopyWarning
@@ -428,41 +542,46 @@ class DirectTabularModel(AbstractMLForecastModel):
428
542
  if self.is_quantile_model:
429
543
  # Quantile model does not require residuals to produce prediction intervals
430
544
  self._residuals_std_per_item = pd.Series(1.0, index=val_df[MLF_ITEMID].unique())
431
- self._avg_residuals_std = 1.0
432
545
  else:
433
546
  super()._save_residuals_std(val_df=val_df)
434
547
 
435
548
  def _predict(
436
549
  self,
437
550
  data: TimeSeriesDataFrame,
438
- known_covariates: Optional[TimeSeriesDataFrame] = None,
551
+ known_covariates: TimeSeriesDataFrame | None = None,
439
552
  **kwargs,
440
553
  ) -> TimeSeriesDataFrame:
554
+ from .transforms import apply_inverse_transform
555
+
441
556
  original_item_id_order = data.item_ids
442
557
  data, known_covariates, forecast_for_short_series = self._remove_short_ts_and_generate_fallback_forecast(
443
558
  data=data, known_covariates=known_covariates
444
559
  )
445
560
  if len(data) == 0:
446
561
  # All time series are too short for chosen differences
562
+ assert forecast_for_short_series is not None
447
563
  return forecast_for_short_series
448
564
 
449
565
  if known_covariates is not None:
450
566
  data_future = known_covariates.copy()
451
567
  else:
452
- future_index = get_forecast_horizon_index_ts_dataframe(data, self.prediction_length)
568
+ future_index = self.get_forecast_horizon_index(data)
453
569
  data_future = pd.DataFrame(columns=[self.target], index=future_index, dtype="float32")
454
570
  # MLForecast raises exception of target contains NaN. We use inf as placeholder, replace them by NaN afterwards
455
571
  data_future[self.target] = float("inf")
456
572
  data_extended = pd.concat([data, data_future])
457
- mlforecast_df = self._to_mlforecast_df(data_extended, data.static_features)
573
+ mlforecast_df = self._to_mlforecast_df(data_extended, data.static_features) # type: ignore
458
574
  if self._max_ts_length is not None:
459
575
  # We appended `prediction_length` time steps to each series, so increase length
460
576
  mlforecast_df = self._shorten_all_series(mlforecast_df, self._max_ts_length + self.prediction_length)
461
577
  df = self._mlf.preprocess(mlforecast_df, dropna=False, static_features=[])
578
+ assert isinstance(df, pd.DataFrame)
579
+
462
580
  df = df.groupby(MLF_ITEMID, sort=False).tail(self.prediction_length)
463
581
  df = df.replace(float("inf"), float("nan"))
464
582
 
465
- raw_predictions = self._mlf.models_["mean"].predict(df)
583
+ mean_estimator = self.get_tabular_model()
584
+ raw_predictions = mean_estimator.predict(df)
466
585
  predictions = self._postprocess_predictions(raw_predictions, repeated_item_ids=df[MLF_ITEMID])
467
586
  # Paste columns one by one to preserve dtypes
468
587
  predictions[MLF_ITEMID] = df[MLF_ITEMID].values
@@ -473,57 +592,72 @@ class DirectTabularModel(AbstractMLForecastModel):
473
592
  mlforecast_df_past = self._to_mlforecast_df(data, None)
474
593
  if self._max_ts_length is not None:
475
594
  mlforecast_df_past = self._shorten_all_series(mlforecast_df_past, self._max_ts_length)
476
- self._mlf.preprocess(mlforecast_df_past, static_features=[])
595
+ self._mlf.preprocess(mlforecast_df_past, static_features=[], dropna=False)
596
+ assert self._mlf.ts.target_transforms is not None
477
597
  for tfm in self._mlf.ts.target_transforms[::-1]:
478
- predictions = tfm.inverse_transform(predictions)
479
- predictions = TimeSeriesDataFrame(predictions.rename(columns={MLF_ITEMID: ITEMID, MLF_TIMESTAMP: TIMESTAMP}))
598
+ predictions = apply_inverse_transform(predictions, transform=tfm)
599
+
600
+ if not self.is_quantile_model:
601
+ predictions = self._add_gaussian_quantiles(
602
+ predictions, repeated_item_ids=predictions[MLF_ITEMID], past_target=data[self.target]
603
+ )
604
+ predictions_tsdf: TimeSeriesDataFrame = TimeSeriesDataFrame(
605
+ predictions.rename(
606
+ columns={MLF_ITEMID: TimeSeriesDataFrame.ITEMID, MLF_TIMESTAMP: TimeSeriesDataFrame.TIMESTAMP}
607
+ )
608
+ )
480
609
 
481
610
  if forecast_for_short_series is not None:
482
- predictions = pd.concat([predictions, forecast_for_short_series])
483
- predictions = predictions.reindex(original_item_id_order, level=ITEMID)
484
- return predictions
611
+ predictions_tsdf = pd.concat([predictions_tsdf, forecast_for_short_series]) # type: ignore
612
+ predictions_tsdf = predictions_tsdf.reindex(original_item_id_order, level=TimeSeriesDataFrame.ITEMID)
613
+
614
+ return predictions_tsdf
485
615
 
486
- def _postprocess_predictions(self, predictions: np.ndarray, repeated_item_ids: pd.Series) -> pd.DataFrame:
616
+ def _postprocess_predictions(
617
+ self, predictions: np.ndarray | pd.Series, repeated_item_ids: pd.Series
618
+ ) -> pd.DataFrame:
487
619
  if self.is_quantile_model:
488
- predictions = pd.DataFrame(predictions, columns=[str(q) for q in self.quantile_levels])
489
- predictions.values.sort(axis=1)
490
- predictions["mean"] = predictions["0.5"]
620
+ predictions_df = pd.DataFrame(predictions, columns=[str(q) for q in self.quantile_levels])
621
+ predictions_df.values.sort(axis=1)
622
+ predictions_df["mean"] = predictions_df["0.5"]
491
623
  else:
492
- predictions = pd.DataFrame(predictions, columns=["mean"])
493
- predictions = self._add_gaussian_quantiles(predictions, repeated_item_ids=repeated_item_ids)
494
-
495
- column_order = ["mean"] + [col for col in predictions.columns if col != "mean"]
496
- return predictions[column_order]
624
+ predictions_df = pd.DataFrame(predictions, columns=["mean"])
497
625
 
498
- def _get_scale_per_item(self, item_ids: pd.Index) -> pd.Series:
499
- # Rescaling is applied in the inverse_transform step, no need to scale predictions
500
- return pd.Series(1.0, index=item_ids)
626
+ column_order = ["mean"] + [col for col in predictions_df.columns if col != "mean"]
627
+ return predictions_df[column_order]
501
628
 
502
- def _get_extra_tabular_init_kwargs(self) -> dict:
629
+ def _create_tabular_model(self, model_name: str, model_hyperparameters: dict[str, Any]) -> TabularModel:
630
+ model_class = ag_model_registry.key_to_cls(model_name)
503
631
  if self.is_quantile_model:
504
- return {
505
- "problem_type": ag.constants.QUANTILE,
506
- "quantile_levels": self.quantile_levels,
507
- "eval_metric": "pinball_loss",
508
- }
632
+ problem_type = ag.constants.QUANTILE
633
+ eval_metric = "pinball_loss"
634
+ model_hyperparameters["ag.quantile_levels"] = self.quantile_levels
509
635
  else:
510
- return {
511
- "problem_type": ag.constants.REGRESSION,
512
- "eval_metric": self.eval_metric.equivalent_tabular_regression_metric or "mean_absolute_error",
513
- }
636
+ problem_type = ag.constants.REGRESSION
637
+ eval_metric = self.eval_metric.equivalent_tabular_regression_metric or "mean_absolute_error"
638
+ return TabularModel(
639
+ model_class=model_class,
640
+ model_kwargs={
641
+ "path": "",
642
+ "name": model_class.__name__,
643
+ "hyperparameters": model_hyperparameters,
644
+ "problem_type": problem_type,
645
+ "eval_metric": eval_metric,
646
+ },
647
+ )
514
648
 
515
649
 
516
650
  class RecursiveTabularModel(AbstractMLForecastModel):
517
- """Predict future time series values one by one using TabularPredictor from AutoGluon-Tabular.
651
+ """Predict future time series values one by one using a regression model from AutoGluon-Tabular.
518
652
 
519
- A single TabularPredictor is used to forecast the future time series values using the following features:
653
+ A single tabular regression model is used to forecast the future time series values using the following features:
520
654
 
521
655
  - lag features (observed time series values) based on ``freq`` of the data
522
656
  - time features (e.g., day of the week) based on the timestamp of the measurement
523
657
  - known covariates (if available)
524
658
  - static features of each item (if available)
525
659
 
526
- TabularPredictor will always be trained with ``"regression"`` problem type, and dummy quantiles will be
660
+ The tabular model will always be trained with ``"regression"`` problem type, and dummy quantiles will be
527
661
  obtained by assuming that the residuals follow zero-mean normal distribution.
528
662
 
529
663
  Based on the `mlforecast <https://github.com/Nixtla/mlforecast>`_ library.
@@ -531,39 +665,48 @@ class RecursiveTabularModel(AbstractMLForecastModel):
531
665
 
532
666
  Other Parameters
533
667
  ----------------
534
- lags : List[int], default = None
668
+ lags : list[int], default = None
535
669
  Lags of the target that will be used as features for predictions. If None, will be determined automatically
536
670
  based on the frequency of the data.
537
- date_features : List[Union[str, Callable]], default = None
671
+ date_features : list[str | Callable], default = None
538
672
  Features computed from the dates. Can be pandas date attributes or functions that will take the dates as input.
539
673
  If None, will be determined automatically based on the frequency of the data.
540
- differences : List[int], default = None
674
+ differences : list[int], default = None
541
675
  Differences to take of the target before computing the features. These are restored at the forecasting step.
542
676
  If None, will be set to ``[seasonal_period]``, where seasonal_period is determined based on the data frequency.
543
- scaler : {"standard", "mean_abs", None}, default = "standard"
544
- Scaling applied to each time series.
545
- tabular_hyperparameters : Dict[Dict[str, Any]], optional
546
- Hyperparameters dictionary passed to ``TabularPredictor.fit``. Contains the names of models that should be fit.
547
- Defaults to ``{"GBM": {}}``.
548
- tabular_fit_kwargs : Dict[str, Any], optional
549
- Additional keyword arguments passed to ``TabularPredictor.fit``. Defaults to an empty dict.
677
+ target_scaler : {"standard", "mean_abs", "min_max", "robust", None}, default = "standard"
678
+ Scaling applied to each time series. Scaling is applied after differencing.
679
+ lag_transforms : dict[int, list[Callable]], default = None
680
+ Dictionary mapping lag periods to transformation functions applied to lagged target values (e.g., rolling mean).
681
+ See `MLForecast documentation <https://nixtlaverse.nixtla.io/mlforecast/lag_transforms.html>`_ for more details.
682
+ model_name : str, default = "GBM"
683
+ Name of the tabular regression model. See ``autogluon.tabular.registry.ag_model_registry`` or
684
+ `the documentation <https://auto.gluon.ai/stable/api/autogluon.tabular.models.html>`_ for the list of available
685
+ tabular models.
686
+ model_hyperparameters : dict[str, Any], optional
687
+ Hyperparameters passed to the tabular regression model.
550
688
  max_num_items : int or None, default = 20_000
551
689
  If not None, the model will randomly select this many time series for training and validation.
552
690
  max_num_samples : int or None, default = 1_000_000
553
- If not None, training dataset passed to TabularPredictor will contain at most this many rows (starting from the
554
- end of each time series).
691
+ If not None, training dataset passed to the tabular regression model will contain at most this many rows
692
+ (starting from the end of each time series).
555
693
  """
556
694
 
557
- def _get_model_params(self) -> dict:
558
- model_params = super()._get_model_params()
559
- model_params.setdefault("scaler", "standard")
560
- model_params.setdefault("differences", [get_seasonality(self.freq)])
695
+ ag_priority = 90
696
+
697
+ def get_hyperparameters(self) -> dict[str, Any]:
698
+ model_params = super().get_hyperparameters()
699
+ # We don't set 'target_scaler' if user already provided 'scaler' to avoid overriding the user-provided value
700
+ if "scaler" not in model_params:
701
+ model_params.setdefault("target_scaler", "standard")
702
+ if "differences" not in model_params or model_params["differences"] is None:
703
+ model_params["differences"] = [get_seasonality(self.freq)]
561
704
  return model_params
562
705
 
563
706
  def _predict(
564
707
  self,
565
708
  data: TimeSeriesDataFrame,
566
- known_covariates: Optional[TimeSeriesDataFrame] = None,
709
+ known_covariates: TimeSeriesDataFrame | None = None,
567
710
  **kwargs,
568
711
  ) -> TimeSeriesDataFrame:
569
712
  original_item_id_order = data.item_ids
@@ -572,14 +715,17 @@ class RecursiveTabularModel(AbstractMLForecastModel):
572
715
  )
573
716
  if len(data) == 0:
574
717
  # All time series are too short for chosen differences
718
+ assert forecast_for_short_series is not None
575
719
  return forecast_for_short_series
576
720
 
577
721
  new_df = self._to_mlforecast_df(data, data.static_features)
578
722
  if self._max_ts_length is not None:
579
723
  new_df = self._shorten_all_series(new_df, self._max_ts_length)
580
724
  if known_covariates is None:
581
- future_index = get_forecast_horizon_index_ts_dataframe(data, self.prediction_length)
582
- known_covariates = pd.DataFrame(columns=[self.target], index=future_index, dtype="float32")
725
+ future_index = self.get_forecast_horizon_index(data)
726
+ known_covariates = TimeSeriesDataFrame(
727
+ pd.DataFrame(columns=[self.target], index=future_index, dtype="float32")
728
+ )
583
729
  X_df = self._to_mlforecast_df(known_covariates, data.static_features, include_target=False)
584
730
  # If both covariates & static features are missing, set X_df = None to avoid exception from MLForecast
585
731
  if len(X_df.columns.difference([MLF_ITEMID, MLF_TIMESTAMP])) == 0:
@@ -590,23 +736,31 @@ class RecursiveTabularModel(AbstractMLForecastModel):
590
736
  new_df=new_df,
591
737
  X_df=X_df,
592
738
  )
593
- predictions = raw_predictions.rename(columns={MLF_ITEMID: ITEMID, MLF_TIMESTAMP: TIMESTAMP})
594
- predictions = TimeSeriesDataFrame(
595
- self._add_gaussian_quantiles(predictions, repeated_item_ids=predictions[ITEMID])
739
+ assert isinstance(raw_predictions, pd.DataFrame)
740
+ raw_predictions = raw_predictions.rename(
741
+ columns={MLF_ITEMID: TimeSeriesDataFrame.ITEMID, MLF_TIMESTAMP: TimeSeriesDataFrame.TIMESTAMP}
596
742
  )
597
743
 
744
+ predictions: TimeSeriesDataFrame = TimeSeriesDataFrame(
745
+ self._add_gaussian_quantiles(
746
+ raw_predictions,
747
+ repeated_item_ids=raw_predictions[TimeSeriesDataFrame.ITEMID],
748
+ past_target=data[self.target],
749
+ )
750
+ )
598
751
  if forecast_for_short_series is not None:
599
- predictions = pd.concat([predictions, forecast_for_short_series])
600
- return predictions.reindex(original_item_id_order, level=ITEMID)
601
-
602
- def _get_extra_tabular_init_kwargs(self) -> dict:
603
- return {
604
- "problem_type": ag.constants.REGRESSION,
605
- "eval_metric": self.eval_metric.equivalent_tabular_regression_metric or "mean_absolute_error",
606
- }
607
-
608
- def _get_scale_per_item(self, item_ids: pd.Index) -> pd.Series:
609
- if self._scaler is not None:
610
- return self._scaler.stats_["_scale"].copy().reindex(item_ids)
611
- else:
612
- return pd.Series(1.0, index=item_ids)
752
+ predictions = pd.concat([predictions, forecast_for_short_series]) # type: ignore
753
+ return predictions.reindex(original_item_id_order, level=TimeSeriesDataFrame.ITEMID)
754
+
755
+ def _create_tabular_model(self, model_name: str, model_hyperparameters: dict[str, Any]) -> TabularModel:
756
+ model_class = ag_model_registry.key_to_cls(model_name)
757
+ return TabularModel(
758
+ model_class=model_class,
759
+ model_kwargs={
760
+ "path": "",
761
+ "name": model_class.__name__,
762
+ "hyperparameters": model_hyperparameters,
763
+ "problem_type": ag.constants.REGRESSION,
764
+ "eval_metric": self.eval_metric.equivalent_tabular_regression_metric or "mean_absolute_error",
765
+ },
766
+ )