autogluon.timeseries 1.4.1b20250907__py3-none-any.whl → 1.5.1b20260122__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 (95) hide show
  1. autogluon/timeseries/configs/hyperparameter_presets.py +13 -28
  2. autogluon/timeseries/configs/predictor_presets.py +23 -39
  3. autogluon/timeseries/dataset/ts_dataframe.py +97 -86
  4. autogluon/timeseries/learner.py +70 -35
  5. autogluon/timeseries/metrics/__init__.py +4 -4
  6. autogluon/timeseries/metrics/abstract.py +8 -8
  7. autogluon/timeseries/metrics/point.py +9 -9
  8. autogluon/timeseries/metrics/quantile.py +5 -5
  9. autogluon/timeseries/metrics/utils.py +4 -4
  10. autogluon/timeseries/models/__init__.py +4 -1
  11. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +52 -50
  12. autogluon/timeseries/models/abstract/model_trial.py +2 -1
  13. autogluon/timeseries/models/abstract/tunable.py +8 -8
  14. autogluon/timeseries/models/autogluon_tabular/mlforecast.py +58 -62
  15. autogluon/timeseries/models/autogluon_tabular/per_step.py +27 -16
  16. autogluon/timeseries/models/autogluon_tabular/transforms.py +11 -9
  17. autogluon/timeseries/models/chronos/__init__.py +2 -1
  18. autogluon/timeseries/models/chronos/chronos2.py +395 -0
  19. autogluon/timeseries/models/chronos/model.py +127 -89
  20. autogluon/timeseries/models/chronos/{pipeline/utils.py → utils.py} +69 -37
  21. autogluon/timeseries/models/ensemble/__init__.py +36 -2
  22. autogluon/timeseries/models/ensemble/abstract.py +14 -46
  23. autogluon/timeseries/models/ensemble/array_based/__init__.py +3 -0
  24. autogluon/timeseries/models/ensemble/array_based/abstract.py +240 -0
  25. autogluon/timeseries/models/ensemble/array_based/models.py +185 -0
  26. autogluon/timeseries/models/ensemble/array_based/regressor/__init__.py +12 -0
  27. autogluon/timeseries/models/ensemble/array_based/regressor/abstract.py +88 -0
  28. autogluon/timeseries/models/ensemble/array_based/regressor/linear_stacker.py +186 -0
  29. autogluon/timeseries/models/ensemble/array_based/regressor/per_quantile_tabular.py +94 -0
  30. autogluon/timeseries/models/ensemble/array_based/regressor/tabular.py +107 -0
  31. autogluon/timeseries/models/ensemble/{greedy.py → ensemble_selection.py} +41 -61
  32. autogluon/timeseries/models/ensemble/per_item_greedy.py +172 -0
  33. autogluon/timeseries/models/ensemble/weighted/__init__.py +8 -0
  34. autogluon/timeseries/models/ensemble/weighted/abstract.py +45 -0
  35. autogluon/timeseries/models/ensemble/{basic.py → weighted/basic.py} +25 -22
  36. autogluon/timeseries/models/ensemble/weighted/greedy.py +64 -0
  37. autogluon/timeseries/models/gluonts/abstract.py +32 -31
  38. autogluon/timeseries/models/gluonts/dataset.py +11 -11
  39. autogluon/timeseries/models/gluonts/models.py +0 -7
  40. autogluon/timeseries/models/local/__init__.py +0 -7
  41. autogluon/timeseries/models/local/abstract_local_model.py +15 -18
  42. autogluon/timeseries/models/local/naive.py +2 -2
  43. autogluon/timeseries/models/local/npts.py +7 -1
  44. autogluon/timeseries/models/local/statsforecast.py +13 -13
  45. autogluon/timeseries/models/multi_window/multi_window_model.py +39 -24
  46. autogluon/timeseries/models/registry.py +3 -4
  47. autogluon/timeseries/models/toto/__init__.py +3 -0
  48. autogluon/timeseries/models/toto/_internal/__init__.py +9 -0
  49. autogluon/timeseries/models/toto/_internal/backbone/__init__.py +3 -0
  50. autogluon/timeseries/models/toto/_internal/backbone/attention.py +196 -0
  51. autogluon/timeseries/models/toto/_internal/backbone/backbone.py +262 -0
  52. autogluon/timeseries/models/toto/_internal/backbone/distribution.py +70 -0
  53. autogluon/timeseries/models/toto/_internal/backbone/kvcache.py +136 -0
  54. autogluon/timeseries/models/toto/_internal/backbone/rope.py +89 -0
  55. autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
  56. autogluon/timeseries/models/toto/_internal/backbone/scaler.py +305 -0
  57. autogluon/timeseries/models/toto/_internal/backbone/transformer.py +333 -0
  58. autogluon/timeseries/models/toto/_internal/dataset.py +165 -0
  59. autogluon/timeseries/models/toto/_internal/forecaster.py +423 -0
  60. autogluon/timeseries/models/toto/dataloader.py +108 -0
  61. autogluon/timeseries/models/toto/hf_pretrained_model.py +200 -0
  62. autogluon/timeseries/models/toto/model.py +249 -0
  63. autogluon/timeseries/predictor.py +541 -162
  64. autogluon/timeseries/regressor.py +27 -30
  65. autogluon/timeseries/splitter.py +3 -27
  66. autogluon/timeseries/trainer/ensemble_composer.py +444 -0
  67. autogluon/timeseries/trainer/model_set_builder.py +9 -9
  68. autogluon/timeseries/trainer/prediction_cache.py +16 -16
  69. autogluon/timeseries/trainer/trainer.py +300 -279
  70. autogluon/timeseries/trainer/utils.py +17 -0
  71. autogluon/timeseries/transforms/covariate_scaler.py +8 -8
  72. autogluon/timeseries/transforms/target_scaler.py +15 -15
  73. autogluon/timeseries/utils/constants.py +10 -0
  74. autogluon/timeseries/utils/datetime/lags.py +1 -3
  75. autogluon/timeseries/utils/datetime/seasonality.py +1 -3
  76. autogluon/timeseries/utils/features.py +31 -14
  77. autogluon/timeseries/utils/forecast.py +6 -7
  78. autogluon/timeseries/utils/timer.py +173 -0
  79. autogluon/timeseries/version.py +1 -1
  80. autogluon.timeseries-1.5.1b20260122-py3.11-nspkg.pth +1 -0
  81. {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/METADATA +39 -22
  82. autogluon_timeseries-1.5.1b20260122.dist-info/RECORD +103 -0
  83. {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/WHEEL +1 -1
  84. autogluon/timeseries/evaluator.py +0 -6
  85. autogluon/timeseries/models/chronos/pipeline/__init__.py +0 -10
  86. autogluon/timeseries/models/chronos/pipeline/base.py +0 -160
  87. autogluon/timeseries/models/chronos/pipeline/chronos.py +0 -544
  88. autogluon/timeseries/models/chronos/pipeline/chronos_bolt.py +0 -580
  89. autogluon.timeseries-1.4.1b20250907-py3.9-nspkg.pth +0 -1
  90. autogluon.timeseries-1.4.1b20250907.dist-info/RECORD +0 -75
  91. {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info/licenses}/LICENSE +0 -0
  92. {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info/licenses}/NOTICE +0 -0
  93. {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/namespace_packages.txt +0 -0
  94. {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/top_level.txt +0 -0
  95. {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/zip-safe +0 -0
@@ -1,3 +1,37 @@
1
1
  from .abstract import AbstractTimeSeriesEnsembleModel
2
- from .basic import PerformanceWeightedEnsemble, SimpleAverageEnsemble
3
- from .greedy import GreedyEnsemble
2
+ from .array_based import LinearStackerEnsemble, MedianEnsemble, PerQuantileTabularEnsemble, TabularEnsemble
3
+ from .per_item_greedy import PerItemGreedyEnsemble
4
+ from .weighted import GreedyEnsemble, PerformanceWeightedEnsemble, SimpleAverageEnsemble
5
+
6
+
7
+ def get_ensemble_class(name: str):
8
+ mapping = {
9
+ "Greedy": GreedyEnsemble,
10
+ "PerItemGreedy": PerItemGreedyEnsemble,
11
+ "PerformanceWeighted": PerformanceWeightedEnsemble,
12
+ "SimpleAverage": SimpleAverageEnsemble,
13
+ "Weighted": GreedyEnsemble, # old alias for this model
14
+ "Median": MedianEnsemble,
15
+ "Tabular": TabularEnsemble,
16
+ "PerQuantileTabular": PerQuantileTabularEnsemble,
17
+ "LinearStacker": LinearStackerEnsemble,
18
+ }
19
+
20
+ name_clean = name.removesuffix("Ensemble")
21
+ if name_clean not in mapping:
22
+ raise ValueError(f"Unknown ensemble type: {name}. Available: {list(mapping.keys())}")
23
+ return mapping[name_clean]
24
+
25
+
26
+ __all__ = [
27
+ "AbstractTimeSeriesEnsembleModel",
28
+ "GreedyEnsemble",
29
+ "LinearStackerEnsemble",
30
+ "MedianEnsemble",
31
+ "PerformanceWeightedEnsemble",
32
+ "PerItemGreedyEnsemble",
33
+ "PerQuantileTabularEnsemble",
34
+ "SimpleAverageEnsemble",
35
+ "TabularEnsemble",
36
+ "get_ensemble_class",
37
+ ]
@@ -1,9 +1,6 @@
1
- import functools
2
1
  import logging
3
2
  from abc import ABC, abstractmethod
4
- from typing import Optional
5
3
 
6
- import numpy as np
7
4
  from typing_extensions import final
8
5
 
9
6
  from autogluon.core.utils.exceptions import TimeLimitExceeded
@@ -14,7 +11,12 @@ logger = logging.getLogger(__name__)
14
11
 
15
12
 
16
13
  class AbstractTimeSeriesEnsembleModel(TimeSeriesModelBase, ABC):
17
- """Abstract class for time series ensemble models."""
14
+ """Abstract base class for time series ensemble models that combine predictions from multiple base models.
15
+
16
+ Ensemble training process operates on validation predictions from base models rather than raw time series
17
+ data. This allows the ensemble to learn optimal combination strategies based on each model's performance
18
+ across different validation windows and time series patterns.
19
+ """
18
20
 
19
21
  @property
20
22
  @abstractmethod
@@ -27,8 +29,8 @@ class AbstractTimeSeriesEnsembleModel(TimeSeriesModelBase, ABC):
27
29
  self,
28
30
  predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
29
31
  data_per_window: list[TimeSeriesDataFrame],
30
- model_scores: Optional[dict[str, float]] = None,
31
- time_limit: Optional[float] = None,
32
+ model_scores: dict[str, float] | None = None,
33
+ time_limit: float | None = None,
32
34
  ):
33
35
  """Fit ensemble model given predictions of candidate base models and the true data.
34
36
 
@@ -52,7 +54,7 @@ class AbstractTimeSeriesEnsembleModel(TimeSeriesModelBase, ABC):
52
54
  )
53
55
  raise TimeLimitExceeded
54
56
  if isinstance(data_per_window, TimeSeriesDataFrame):
55
- raise ValueError("When fitting ensemble, `data` should contain ground truth for each validation window")
57
+ raise ValueError("When fitting ensemble, ``data`` should contain ground truth for each validation window")
56
58
  num_val_windows = len(data_per_window)
57
59
  for model, preds in predictions_per_window.items():
58
60
  if len(preds) != num_val_windows:
@@ -69,11 +71,11 @@ class AbstractTimeSeriesEnsembleModel(TimeSeriesModelBase, ABC):
69
71
  self,
70
72
  predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
71
73
  data_per_window: list[TimeSeriesDataFrame],
72
- model_scores: Optional[dict[str, float]] = None,
73
- time_limit: Optional[float] = None,
74
- ):
75
- """Private method for `fit`. See `fit` for documentation of arguments. Apart from the model
76
- training logic, `fit` additionally implements other logic such as keeping track of the time limit.
74
+ model_scores: dict[str, float] | None = None,
75
+ time_limit: float | None = None,
76
+ ) -> None:
77
+ """Private method for ``fit``. See ``fit`` for documentation of arguments. Apart from the model
78
+ training logic, ``fit`` additionally implements other logic such as keeping track of the time limit.
77
79
  """
78
80
  raise NotImplementedError
79
81
 
@@ -103,37 +105,3 @@ class AbstractTimeSeriesEnsembleModel(TimeSeriesModelBase, ABC):
103
105
  This method should be called after performing refit_full to point to the refitted base models, if necessary.
104
106
  """
105
107
  pass
106
-
107
-
108
- class AbstractWeightedTimeSeriesEnsembleModel(AbstractTimeSeriesEnsembleModel, ABC):
109
- """Abstract class for weighted ensembles which assign one (global) weight per model."""
110
-
111
- def __init__(self, name: Optional[str] = None, **kwargs):
112
- if name is None:
113
- name = "WeightedEnsemble"
114
- super().__init__(name=name, **kwargs)
115
- self.model_to_weight: dict[str, float] = {}
116
-
117
- @property
118
- def model_names(self) -> list[str]:
119
- return list(self.model_to_weight.keys())
120
-
121
- @property
122
- def model_weights(self) -> np.ndarray:
123
- return np.array(list(self.model_to_weight.values()), dtype=np.float64)
124
-
125
- def _predict(self, data: dict[str, TimeSeriesDataFrame], **kwargs) -> TimeSeriesDataFrame:
126
- weighted_predictions = [data[model_name] * weight for model_name, weight in self.model_to_weight.items()]
127
- return functools.reduce(lambda x, y: x + y, weighted_predictions)
128
-
129
- def get_info(self) -> dict:
130
- info = super().get_info()
131
- info["model_weights"] = self.model_to_weight.copy()
132
- return info
133
-
134
- def remap_base_models(self, model_refit_map: dict[str, str]) -> None:
135
- updated_weights = {}
136
- for model, weight in self.model_to_weight.items():
137
- model_full_name = model_refit_map.get(model, model)
138
- updated_weights[model_full_name] = weight
139
- self.model_to_weight = updated_weights
@@ -0,0 +1,3 @@
1
+ from .models import LinearStackerEnsemble, MedianEnsemble, PerQuantileTabularEnsemble, TabularEnsemble
2
+
3
+ __all__ = ["LinearStackerEnsemble", "MedianEnsemble", "PerQuantileTabularEnsemble", "TabularEnsemble"]
@@ -0,0 +1,240 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Sequence
3
+
4
+ import numpy as np
5
+
6
+ from autogluon.timeseries.dataset import TimeSeriesDataFrame
7
+ from autogluon.timeseries.metrics.abstract import TimeSeriesScorer
8
+ from autogluon.timeseries.utils.features import CovariateMetadata
9
+
10
+ from ..abstract import AbstractTimeSeriesEnsembleModel
11
+ from .regressor import EnsembleRegressor
12
+
13
+
14
+ class ArrayBasedTimeSeriesEnsembleModel(AbstractTimeSeriesEnsembleModel, ABC):
15
+ """Abstract base class for ensemble models that operate on multi-dimensional arrays of base model predictions.
16
+
17
+ Array-based ensembles convert time series predictions into structured numpy arrays for efficient processing
18
+ and enable sophisticated combination strategies beyond simple weighted averaging. Array-based ensembles also
19
+ support isotonization in quantile forecasts--ensuring quantile crossing does not occur. They also have built-in
20
+ failed model detection and filtering capabilities.
21
+
22
+ Other Parameters
23
+ ----------------
24
+ isotonization : str, default = "sort"
25
+ The isotonization method to use (i.e. the algorithm to prevent quantile non-crossing).
26
+ Currently only "sort" is supported.
27
+ detect_and_ignore_failures : bool, default = True
28
+ Whether to detect and ignore "failed models", defined as models which have a loss that is larger
29
+ than 10x the median loss of all the models. This can be very important for the regression-based
30
+ ensembles, as moving the weight from such a "failed model" to zero can require a long training
31
+ time.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ path: str | None = None,
37
+ name: str | None = None,
38
+ hyperparameters: dict[str, Any] | None = None,
39
+ freq: str | None = None,
40
+ prediction_length: int = 1,
41
+ covariate_metadata: CovariateMetadata | None = None,
42
+ target: str = "target",
43
+ quantile_levels: Sequence[float] = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
44
+ eval_metric: str | TimeSeriesScorer | None = None,
45
+ ):
46
+ super().__init__(
47
+ path=path,
48
+ name=name,
49
+ hyperparameters=hyperparameters,
50
+ freq=freq,
51
+ prediction_length=prediction_length,
52
+ covariate_metadata=covariate_metadata,
53
+ target=target,
54
+ quantile_levels=quantile_levels,
55
+ eval_metric=eval_metric,
56
+ )
57
+ self.ensemble_regressor: EnsembleRegressor | None = None
58
+ self._model_names: list[str] = []
59
+
60
+ def _get_default_hyperparameters(self) -> dict[str, Any]:
61
+ return {
62
+ "isotonization": "sort",
63
+ "detect_and_ignore_failures": True,
64
+ }
65
+
66
+ @staticmethod
67
+ def to_array(df: TimeSeriesDataFrame) -> np.ndarray:
68
+ """Given a TimeSeriesDataFrame object, return a single array composing the values contained
69
+ in the data frame.
70
+
71
+ Parameters
72
+ ----------
73
+ df
74
+ TimeSeriesDataFrame to convert to an array. Must contain exactly ``prediction_length``
75
+ values for each item. The columns of ``df`` can correspond to ground truth values
76
+ or predictions (in which case, these will be the mean or quantile forecasts).
77
+
78
+ Returns
79
+ -------
80
+ array
81
+ of shape (num_items, prediction_length, num_outputs).
82
+ """
83
+ assert df.index.is_monotonic_increasing
84
+ array = df.to_numpy()
85
+ num_items = df.num_items
86
+ shape = (
87
+ num_items,
88
+ df.shape[0] // num_items, # timesteps per item
89
+ df.shape[1], # num_outputs
90
+ )
91
+ return array.reshape(shape)
92
+
93
+ def _get_base_model_predictions(
94
+ self,
95
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]] | dict[str, TimeSeriesDataFrame],
96
+ ) -> tuple[np.ndarray, np.ndarray]:
97
+ """Given a mapping from model names to a list of data frames representing
98
+ their predictions per window, return a multidimensional array representation.
99
+
100
+ Parameters
101
+ ----------
102
+ predictions_per_window
103
+ A dictionary with list[TimeSeriesDataFrame] values, where each TimeSeriesDataFrame
104
+ contains predictions for the window in question. If the dictionary values are
105
+ TimeSeriesDataFrame, they will be treated like a single window.
106
+
107
+ Returns
108
+ -------
109
+ base_model_mean_predictions
110
+ Array of shape (num_windows, num_items, prediction_length, 1, num_models)
111
+ base_model_quantile_predictions
112
+ Array of shape (num_windows, num_items, prediction_length, num_quantiles, num_models)
113
+ """
114
+
115
+ if not predictions_per_window:
116
+ raise ValueError("No base model predictions are provided.")
117
+
118
+ first_prediction = list(predictions_per_window.values())[0]
119
+ if isinstance(first_prediction, TimeSeriesDataFrame):
120
+ predictions_per_window = {k: [v] for k, v in predictions_per_window.items()} # type: ignore
121
+
122
+ predictions = {
123
+ model_name: [self.to_array(window) for window in windows] # type: ignore
124
+ for model_name, windows in predictions_per_window.items()
125
+ }
126
+ base_model_predictions = np.stack([x for x in predictions.values()], axis=-1)
127
+
128
+ return base_model_predictions[:, :, :, :1, :], base_model_predictions[:, :, :, 1:, :]
129
+
130
+ def _isotonize(self, prediction_array: np.ndarray) -> np.ndarray:
131
+ """Apply isotonization to ensure quantile non-crossing.
132
+
133
+ Parameters
134
+ ----------
135
+ prediction_array
136
+ Array of shape (num_windows, num_items, prediction_length, num_quantiles)
137
+
138
+ Returns
139
+ -------
140
+ isotonized_array
141
+ Array with same shape but quantiles sorted along last dimension
142
+ """
143
+ isotonization = self.get_hyperparameter("isotonization")
144
+ if isotonization == "sort":
145
+ return np.sort(prediction_array, axis=-1)
146
+ return prediction_array
147
+
148
+ def _fit(
149
+ self,
150
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
151
+ data_per_window: list[TimeSeriesDataFrame],
152
+ model_scores: dict[str, float] | None = None,
153
+ time_limit: float | None = None,
154
+ ) -> None:
155
+ # process inputs
156
+ filtered_predictions = self._filter_failed_models(predictions_per_window, model_scores)
157
+ base_model_mean_predictions, base_model_quantile_predictions = self._get_base_model_predictions(
158
+ filtered_predictions
159
+ )
160
+
161
+ # process labels
162
+ ground_truth_per_window = [y.slice_by_timestep(-self.prediction_length, None) for y in data_per_window]
163
+ labels = np.stack(
164
+ [self.to_array(gt) for gt in ground_truth_per_window], axis=0
165
+ ) # (num_windows, num_items, prediction_length, 1)
166
+
167
+ self._model_names = list(filtered_predictions.keys())
168
+ self.ensemble_regressor = self._get_ensemble_regressor()
169
+ self.ensemble_regressor.fit(
170
+ base_model_mean_predictions=base_model_mean_predictions,
171
+ base_model_quantile_predictions=base_model_quantile_predictions,
172
+ labels=labels,
173
+ time_limit=time_limit,
174
+ )
175
+
176
+ @abstractmethod
177
+ def _get_ensemble_regressor(self) -> EnsembleRegressor:
178
+ pass
179
+
180
+ def _predict(self, data: dict[str, TimeSeriesDataFrame], **kwargs) -> TimeSeriesDataFrame:
181
+ if self.ensemble_regressor is None:
182
+ if not self._model_names:
183
+ raise ValueError("Ensemble model has not been fitted yet.")
184
+ # Try to recreate the regressor (for loaded models)
185
+ self.ensemble_regressor = self._get_ensemble_regressor()
186
+
187
+ input_data = {}
188
+ for m in self.model_names:
189
+ assert m in data, f"Predictions for model {m} not provided during ensemble prediction."
190
+ input_data[m] = data[m]
191
+
192
+ base_model_mean_predictions, base_model_quantile_predictions = self._get_base_model_predictions(input_data)
193
+
194
+ mean_predictions, quantile_predictions = self.ensemble_regressor.predict(
195
+ base_model_mean_predictions=base_model_mean_predictions,
196
+ base_model_quantile_predictions=base_model_quantile_predictions,
197
+ )
198
+
199
+ quantile_predictions = self._isotonize(quantile_predictions)
200
+ prediction_array = np.concatenate([mean_predictions, quantile_predictions], axis=-1)
201
+
202
+ output = list(input_data.values())[0].copy()
203
+ num_folds, num_items, num_timesteps, num_outputs = prediction_array.shape
204
+ assert (num_folds, num_timesteps) == (1, self.prediction_length)
205
+ assert len(output.columns) == num_outputs
206
+
207
+ output[output.columns] = prediction_array.reshape((num_items * num_timesteps, num_outputs))
208
+
209
+ return output
210
+
211
+ @property
212
+ def model_names(self) -> list[str]:
213
+ return self._model_names
214
+
215
+ def remap_base_models(self, model_refit_map: dict[str, str]) -> None:
216
+ """Update names of the base models based on the mapping in model_refit_map."""
217
+ self._model_names = [model_refit_map.get(name, name) for name in self._model_names]
218
+
219
+ def _filter_failed_models(
220
+ self,
221
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
222
+ model_scores: dict[str, float] | None,
223
+ ) -> dict[str, list[TimeSeriesDataFrame]]:
224
+ """Filter out failed models based on detect_and_ignore_failures setting."""
225
+ if not self.get_hyperparameter("detect_and_ignore_failures"):
226
+ return predictions_per_window
227
+
228
+ if model_scores is None or len(model_scores) == 0:
229
+ return predictions_per_window
230
+
231
+ valid_scores = {k: v for k, v in model_scores.items() if np.isfinite(v)}
232
+ if len(valid_scores) == 0:
233
+ raise ValueError("All models have NaN scores. At least one model must run successfully to fit an ensemble")
234
+
235
+ losses = {k: -v for k, v in valid_scores.items()}
236
+ median_loss = np.nanmedian(list(losses.values()))
237
+ threshold = 10 * median_loss
238
+ good_models = {k for k, loss in losses.items() if loss <= threshold}
239
+
240
+ return {k: v for k, v in predictions_per_window.items() if k in good_models}
@@ -0,0 +1,185 @@
1
+ from abc import ABC
2
+ from typing import Any, Type
3
+
4
+ from autogluon.timeseries.dataset import TimeSeriesDataFrame
5
+
6
+ from .abstract import ArrayBasedTimeSeriesEnsembleModel
7
+ from .regressor import (
8
+ EnsembleRegressor,
9
+ LinearStackerEnsembleRegressor,
10
+ MedianEnsembleRegressor,
11
+ PerQuantileTabularEnsembleRegressor,
12
+ TabularEnsembleRegressor,
13
+ )
14
+
15
+
16
+ class MedianEnsemble(ArrayBasedTimeSeriesEnsembleModel):
17
+ """Robust ensemble that computes predictions as the element-wise median of base model mean
18
+ and quantile forecasts, providing robustness to outlier predictions.
19
+
20
+ Other Parameters
21
+ ----------------
22
+ isotonization : str, default = "sort"
23
+ The isotonization method to use (i.e. the algorithm to prevent quantile non-crossing).
24
+ Currently only "sort" is supported.
25
+ detect_and_ignore_failures : bool, default = True
26
+ Whether to detect and ignore "failed models", defined as models which have a loss that is larger
27
+ than 10x the median loss of all the models. This can be very important for the regression-based
28
+ ensembles, as moving the weight from such a "failed model" to zero can require a long training
29
+ time.
30
+ """
31
+
32
+ def _get_ensemble_regressor(self) -> MedianEnsembleRegressor:
33
+ return MedianEnsembleRegressor()
34
+
35
+
36
+ class BaseTabularEnsemble(ArrayBasedTimeSeriesEnsembleModel, ABC):
37
+ ensemble_regressor_type: Type[EnsembleRegressor]
38
+
39
+ def _get_default_hyperparameters(self) -> dict[str, Any]:
40
+ default_hps = super()._get_default_hyperparameters()
41
+ default_hps.update({"model_name": "CAT", "model_hyperparameters": {}})
42
+ return default_hps
43
+
44
+ def _get_ensemble_regressor(self):
45
+ hyperparameters = self.get_hyperparameters()
46
+ return self.ensemble_regressor_type(
47
+ quantile_levels=list(self.quantile_levels),
48
+ model_name=hyperparameters["model_name"],
49
+ model_hyperparameters=hyperparameters["model_hyperparameters"],
50
+ )
51
+
52
+
53
+ class TabularEnsemble(BaseTabularEnsemble):
54
+ """Tabular ensemble that uses a single AutoGluon-Tabular model to learn ensemble combinations.
55
+
56
+ This ensemble trains a single tabular model (such as gradient boosting machines) to predict all
57
+ quantiles simultaneously from base model predictions. The tabular model learns complex non-linear
58
+ patterns in how base models should be combined, potentially capturing interactions and conditional
59
+ dependencies that simple weighted averages cannot represent.
60
+
61
+ Other Parameters
62
+ ----------------
63
+ model_name : str, default = "CAT"
64
+ Name of the AutoGluon-Tabular model to use for ensemble learning. Model name should be registered
65
+ in AutoGluon-Tabular model registry.
66
+ model_hyperparameters : dict, default = {}
67
+ Hyperparameters to pass to the underlying AutoGluon-Tabular model.
68
+ isotonization : str, default = "sort"
69
+ The isotonization method to use (i.e. the algorithm to prevent quantile non-crossing).
70
+ Currently only "sort" is supported.
71
+ detect_and_ignore_failures : bool, default = True
72
+ Whether to detect and ignore "failed models", defined as models which have a loss that is larger
73
+ than 10x the median loss of all the models. This can be very important for the regression-based
74
+ ensembles, as moving the weight from such a "failed model" to zero can require a long training
75
+ time.
76
+ """
77
+
78
+ ensemble_regressor_type = TabularEnsembleRegressor
79
+
80
+
81
+ class PerQuantileTabularEnsemble(BaseTabularEnsemble):
82
+ """Tabular ensemble using separate AutoGluon-Tabular models for each quantile and mean forecast.
83
+
84
+ This ensemble trains dedicated tabular models for each quantile level plus a separate model
85
+ for the mean prediction. Each model specializes in learning optimal combinations for its
86
+ specific target, allowing for quantile-specific ensemble strategies that can capture different
87
+ model behaviors across the prediction distribution.
88
+
89
+ Other Parameters
90
+ ----------------
91
+ model_name : str, default = "GBM"
92
+ Name of the AutoGluon-Tabular model to use for ensemble learning. Model name should be registered
93
+ in AutoGluon-Tabular model registry.
94
+ model_hyperparameters : dict, default = {}
95
+ Hyperparameters to pass to the underlying AutoGluon-Tabular model.
96
+ isotonization : str, default = "sort"
97
+ The isotonization method to use (i.e. the algorithm to prevent quantile non-crossing).
98
+ Currently only "sort" is supported.
99
+ detect_and_ignore_failures : bool, default = True
100
+ Whether to detect and ignore "failed models", defined as models which have a loss that is larger
101
+ than 10x the median loss of all the models. This can be very important for the regression-based
102
+ ensembles, as moving the weight from such a "failed model" to zero can require a long training
103
+ time.
104
+ """
105
+
106
+ ensemble_regressor_type = PerQuantileTabularEnsembleRegressor
107
+
108
+
109
+ class LinearStackerEnsemble(ArrayBasedTimeSeriesEnsembleModel):
110
+ """Linear stacking ensemble that learns optimal linear combination weights through gradient-based
111
+ optimization.
112
+
113
+ Weighted combinations can be per model or per model-quantile, model-horizon, model-quantile-horizon
114
+ combinations. These choices are controlled by the ``weights_per`` hyperparameter.
115
+
116
+ The optimization process uses gradient descent with configurable learning rates and convergence
117
+ criteria, allowing for flexible training dynamics. Weight pruning can be applied to remove
118
+ models with negligible contributions, resulting in sparse and interpretable ensembles.
119
+
120
+ Other Parameters
121
+ ----------------
122
+ weights_per : str, default = "m"
123
+ Granularity of weight learning.
124
+
125
+ - "m": single weight per model
126
+ - "mq": single weight for each model-quantile combination
127
+ - "mt": single weight for each model-time step where time steps run across the prediction horizon
128
+ - "mtq": single weight for each model-quantile-time step combination
129
+ lr : float, default = 0.1
130
+ Learning rate for PyTorch optimizer during weight training.
131
+ max_epochs : int, default = 10000
132
+ Maximum number of training epochs for weight optimization.
133
+ relative_tolerance : float, default = 1e-7
134
+ Relative tolerance for convergence detection during training.
135
+ prune_below : float, default = 0.0
136
+ Threshold below which weights are pruned to zero for sparsity. The weights are redistributed across
137
+ remaining models after pruning.
138
+ isotonization : str, default = "sort"
139
+ The isotonization method to use (i.e. the algorithm to prevent quantile non-crossing).
140
+ Currently only "sort" is supported.
141
+ detect_and_ignore_failures : bool, default = True
142
+ Whether to detect and ignore "failed models", defined as models which have a loss that is larger
143
+ than 10x the median loss of all the models. This can be very important for the regression-based
144
+ ensembles, as moving the weight from such a "failed model" to zero can require a long training
145
+ time.
146
+ """
147
+
148
+ def _get_default_hyperparameters(self) -> dict[str, Any]:
149
+ default_hps = super()._get_default_hyperparameters()
150
+ default_hps.update(
151
+ {
152
+ "weights_per": "m",
153
+ "lr": 0.1,
154
+ "max_epochs": 10000,
155
+ "relative_tolerance": 1e-7,
156
+ "prune_below": 0.0,
157
+ }
158
+ )
159
+ return default_hps
160
+
161
+ def _get_ensemble_regressor(self) -> LinearStackerEnsembleRegressor:
162
+ hps = self.get_hyperparameters()
163
+ return LinearStackerEnsembleRegressor(
164
+ quantile_levels=list(self.quantile_levels),
165
+ weights_per=hps["weights_per"],
166
+ lr=hps["lr"],
167
+ max_epochs=hps["max_epochs"],
168
+ relative_tolerance=hps["relative_tolerance"],
169
+ prune_below=hps["prune_below"],
170
+ )
171
+
172
+ def _fit(
173
+ self,
174
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
175
+ data_per_window: list[TimeSeriesDataFrame],
176
+ model_scores: dict[str, float] | None = None,
177
+ time_limit: float | None = None,
178
+ ) -> None:
179
+ super()._fit(predictions_per_window, data_per_window, model_scores, time_limit)
180
+
181
+ assert isinstance(self.ensemble_regressor, LinearStackerEnsembleRegressor)
182
+
183
+ if self.ensemble_regressor.kept_indices is not None:
184
+ original_names = self._model_names
185
+ self._model_names = [original_names[i] for i in self.ensemble_regressor.kept_indices]
@@ -0,0 +1,12 @@
1
+ from .abstract import EnsembleRegressor, MedianEnsembleRegressor
2
+ from .linear_stacker import LinearStackerEnsembleRegressor
3
+ from .per_quantile_tabular import PerQuantileTabularEnsembleRegressor
4
+ from .tabular import TabularEnsembleRegressor
5
+
6
+ __all__ = [
7
+ "EnsembleRegressor",
8
+ "LinearStackerEnsembleRegressor",
9
+ "MedianEnsembleRegressor",
10
+ "PerQuantileTabularEnsembleRegressor",
11
+ "TabularEnsembleRegressor",
12
+ ]
@@ -0,0 +1,88 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ import numpy as np
4
+ from typing_extensions import Self
5
+
6
+
7
+ class EnsembleRegressor(ABC):
8
+ def __init__(self, *args, **kwargs):
9
+ pass
10
+
11
+ @abstractmethod
12
+ def fit(
13
+ self,
14
+ base_model_mean_predictions: np.ndarray,
15
+ base_model_quantile_predictions: np.ndarray,
16
+ labels: np.ndarray,
17
+ time_limit: float | None = None,
18
+ ) -> Self:
19
+ """
20
+ Parameters
21
+ ----------
22
+ base_model_mean_predictions
23
+ Mean (point) predictions of base models. Array of shape
24
+ (num_windows, num_items, prediction_length, 1, num_models)
25
+
26
+ base_model_quantile_predictions
27
+ Quantile predictions of base models. Array of shape
28
+ (num_windows, num_items, prediction_length, num_quantiles, num_models)
29
+
30
+ labels
31
+ Ground truth array of shape
32
+ (num_windows, num_items, prediction_length, 1)
33
+
34
+ time_limit
35
+ Approximately how long ``fit`` will run (wall-clock time in seconds). If
36
+ not specified, training time will not be limited.
37
+ """
38
+ pass
39
+
40
+ @abstractmethod
41
+ def predict(
42
+ self,
43
+ base_model_mean_predictions: np.ndarray,
44
+ base_model_quantile_predictions: np.ndarray,
45
+ ) -> tuple[np.ndarray, np.ndarray]:
46
+ """Predict with the fitted ensemble regressor for a single window.
47
+ The items do not have to refer to the same item indices used when fitting
48
+ the model.
49
+
50
+ Parameters
51
+ ----------
52
+ base_model_mean_predictions
53
+ Mean (point) predictions of base models. Array of shape
54
+ (1, num_items, prediction_length, 1, num_models)
55
+
56
+ base_model_quantile_predictions
57
+ Quantile predictions of base models. Array of shape
58
+ (1, num_items, prediction_length, num_quantiles, num_models)
59
+
60
+ Returns
61
+ -------
62
+ ensemble_mean_predictions
63
+ Array of shape (1, num_items, prediction_length, 1)
64
+ ensemble_quantile_predictions
65
+ Array of shape (1, num_items, prediction_length, num_quantiles)
66
+ """
67
+ pass
68
+
69
+
70
+ class MedianEnsembleRegressor(EnsembleRegressor):
71
+ def fit(
72
+ self,
73
+ base_model_mean_predictions: np.ndarray,
74
+ base_model_quantile_predictions: np.ndarray,
75
+ labels: np.ndarray,
76
+ time_limit: float | None = None,
77
+ ) -> Self:
78
+ return self
79
+
80
+ def predict(
81
+ self,
82
+ base_model_mean_predictions: np.ndarray,
83
+ base_model_quantile_predictions: np.ndarray,
84
+ ) -> tuple[np.ndarray, np.ndarray]:
85
+ return (
86
+ np.nanmedian(base_model_mean_predictions, axis=-1),
87
+ np.nanmedian(base_model_quantile_predictions, axis=-1),
88
+ )