autogluon.timeseries 1.4.1b20251115__py3-none-any.whl → 1.4.1b20251218__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 (82) hide show
  1. autogluon/timeseries/configs/hyperparameter_presets.py +7 -21
  2. autogluon/timeseries/configs/predictor_presets.py +23 -39
  3. autogluon/timeseries/dataset/ts_dataframe.py +32 -34
  4. autogluon/timeseries/learner.py +67 -33
  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 +4 -4
  9. autogluon/timeseries/models/__init__.py +2 -1
  10. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +52 -39
  11. autogluon/timeseries/models/abstract/model_trial.py +2 -1
  12. autogluon/timeseries/models/abstract/tunable.py +8 -8
  13. autogluon/timeseries/models/autogluon_tabular/mlforecast.py +30 -26
  14. autogluon/timeseries/models/autogluon_tabular/per_step.py +12 -10
  15. autogluon/timeseries/models/autogluon_tabular/transforms.py +2 -2
  16. autogluon/timeseries/models/chronos/__init__.py +2 -1
  17. autogluon/timeseries/models/chronos/chronos2.py +395 -0
  18. autogluon/timeseries/models/chronos/model.py +29 -24
  19. autogluon/timeseries/models/chronos/utils.py +5 -5
  20. autogluon/timeseries/models/ensemble/__init__.py +17 -10
  21. autogluon/timeseries/models/ensemble/abstract.py +13 -9
  22. autogluon/timeseries/models/ensemble/array_based/__init__.py +2 -2
  23. autogluon/timeseries/models/ensemble/array_based/abstract.py +24 -31
  24. autogluon/timeseries/models/ensemble/array_based/models.py +146 -11
  25. autogluon/timeseries/models/ensemble/array_based/regressor/__init__.py +2 -0
  26. autogluon/timeseries/models/ensemble/array_based/regressor/abstract.py +6 -5
  27. autogluon/timeseries/models/ensemble/array_based/regressor/linear_stacker.py +186 -0
  28. autogluon/timeseries/models/ensemble/array_based/regressor/per_quantile_tabular.py +44 -83
  29. autogluon/timeseries/models/ensemble/array_based/regressor/tabular.py +21 -55
  30. autogluon/timeseries/models/ensemble/ensemble_selection.py +167 -0
  31. autogluon/timeseries/models/ensemble/per_item_greedy.py +172 -0
  32. autogluon/timeseries/models/ensemble/weighted/abstract.py +7 -3
  33. autogluon/timeseries/models/ensemble/weighted/basic.py +26 -13
  34. autogluon/timeseries/models/ensemble/weighted/greedy.py +20 -145
  35. autogluon/timeseries/models/gluonts/abstract.py +30 -29
  36. autogluon/timeseries/models/gluonts/dataset.py +9 -9
  37. autogluon/timeseries/models/gluonts/models.py +0 -7
  38. autogluon/timeseries/models/local/__init__.py +0 -7
  39. autogluon/timeseries/models/local/abstract_local_model.py +13 -16
  40. autogluon/timeseries/models/local/naive.py +2 -2
  41. autogluon/timeseries/models/local/npts.py +7 -1
  42. autogluon/timeseries/models/local/statsforecast.py +12 -12
  43. autogluon/timeseries/models/multi_window/multi_window_model.py +38 -23
  44. autogluon/timeseries/models/registry.py +3 -4
  45. autogluon/timeseries/models/toto/_internal/backbone/attention.py +3 -4
  46. autogluon/timeseries/models/toto/_internal/backbone/backbone.py +6 -6
  47. autogluon/timeseries/models/toto/_internal/backbone/rope.py +4 -9
  48. autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
  49. autogluon/timeseries/models/toto/_internal/backbone/scaler.py +2 -3
  50. autogluon/timeseries/models/toto/_internal/backbone/transformer.py +10 -10
  51. autogluon/timeseries/models/toto/_internal/dataset.py +2 -2
  52. autogluon/timeseries/models/toto/_internal/forecaster.py +8 -8
  53. autogluon/timeseries/models/toto/dataloader.py +4 -4
  54. autogluon/timeseries/models/toto/hf_pretrained_model.py +97 -16
  55. autogluon/timeseries/models/toto/model.py +30 -17
  56. autogluon/timeseries/predictor.py +517 -129
  57. autogluon/timeseries/regressor.py +18 -23
  58. autogluon/timeseries/splitter.py +2 -2
  59. autogluon/timeseries/trainer/ensemble_composer.py +323 -129
  60. autogluon/timeseries/trainer/model_set_builder.py +9 -9
  61. autogluon/timeseries/trainer/prediction_cache.py +16 -16
  62. autogluon/timeseries/trainer/trainer.py +235 -144
  63. autogluon/timeseries/trainer/utils.py +3 -4
  64. autogluon/timeseries/transforms/covariate_scaler.py +7 -7
  65. autogluon/timeseries/transforms/target_scaler.py +8 -8
  66. autogluon/timeseries/utils/constants.py +10 -0
  67. autogluon/timeseries/utils/datetime/lags.py +1 -3
  68. autogluon/timeseries/utils/datetime/seasonality.py +1 -3
  69. autogluon/timeseries/utils/features.py +22 -9
  70. autogluon/timeseries/utils/forecast.py +1 -2
  71. autogluon/timeseries/utils/timer.py +173 -0
  72. autogluon/timeseries/version.py +1 -1
  73. {autogluon_timeseries-1.4.1b20251115.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/METADATA +23 -21
  74. autogluon_timeseries-1.4.1b20251218.dist-info/RECORD +103 -0
  75. autogluon_timeseries-1.4.1b20251115.dist-info/RECORD +0 -96
  76. /autogluon.timeseries-1.4.1b20251115-py3.9-nspkg.pth → /autogluon.timeseries-1.4.1b20251218-py3.11-nspkg.pth +0 -0
  77. {autogluon_timeseries-1.4.1b20251115.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/WHEEL +0 -0
  78. {autogluon_timeseries-1.4.1b20251115.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/licenses/LICENSE +0 -0
  79. {autogluon_timeseries-1.4.1b20251115.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/licenses/NOTICE +0 -0
  80. {autogluon_timeseries-1.4.1b20251115.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/namespace_packages.txt +0 -0
  81. {autogluon_timeseries-1.4.1b20251115.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/top_level.txt +0 -0
  82. {autogluon_timeseries-1.4.1b20251115.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/zip-safe +0 -0
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
 
3
3
  import numpy as np
4
4
 
@@ -8,14 +8,20 @@ from .abstract import AbstractWeightedTimeSeriesEnsembleModel
8
8
 
9
9
 
10
10
  class SimpleAverageEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
11
- """Constructs a weighted ensemble using a simple average of the constituent models' predictions."""
11
+ """Simple ensemble that assigns equal weights to all base models for uniform averaging.
12
+
13
+ This ensemble computes predictions as the arithmetic mean of all base model forecasts,
14
+ giving each model equal influence. Simple averaging is robust and often performs well when base
15
+ models have similar accuracy levels or when validation data is insufficient to reliably
16
+ estimate performance differences.
17
+ """
12
18
 
13
19
  def _fit(
14
20
  self,
15
21
  predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
16
22
  data_per_window: list[TimeSeriesDataFrame],
17
- model_scores: Optional[dict[str, float]] = None,
18
- time_limit: Optional[float] = None,
23
+ model_scores: dict[str, float] | None = None,
24
+ time_limit: float | None = None,
19
25
  ):
20
26
  self.model_to_weight = {}
21
27
  num_models = len(predictions_per_window)
@@ -24,16 +30,23 @@ class SimpleAverageEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
24
30
 
25
31
 
26
32
  class PerformanceWeightedEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
27
- """Constructs a weighted ensemble, where the weights are assigned in proportion to the
28
- (inverse) validation scores.
33
+ """Performance-based weighted ensemble that assigns weights proportional to validation scores.
34
+
35
+ This ensemble computes model weights based on their validation performance, giving higher
36
+ weights to better-performing models. The weighting scheme transforms validation scores
37
+ (higher is better) into ensemble weights using configurable transformation functions.
38
+
39
+ .. warning::
40
+ This ensemble method is deprecated and may be removed in a future version.
29
41
 
30
42
  Other Parameters
31
43
  ----------------
32
- weight_scheme: Literal["sq", "inv", "loginv"], default = "loginv"
44
+ weight_scheme : Literal["sq", "inv", "sqrt"], default = "sqrt"
33
45
  Method used to compute the weights as a function of the validation scores.
34
- - "sqrt" computes weights in proportion to `sqrt(1 / S)`. This is the default.
35
- - "inv" computes weights in proportion to `(1 / S)`.
36
- - "sq" computes the weights in proportion to `(1 / S)^2` as outlined in [PC2020]_.
46
+
47
+ - "sqrt" computes weights in proportion to ``sqrt(1 / S)``. This is the default.
48
+ - "inv" computes weights in proportion to ``(1 / S)``.
49
+ - "sq" computes the weights in proportion to ``(1 / S)^2`` as outlined in [PC2020]_.
37
50
 
38
51
  References
39
52
  ----------
@@ -49,12 +62,12 @@ class PerformanceWeightedEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
49
62
  self,
50
63
  predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
51
64
  data_per_window: list[TimeSeriesDataFrame],
52
- model_scores: Optional[dict[str, float]] = None,
53
- time_limit: Optional[float] = None,
65
+ model_scores: dict[str, float] | None = None,
66
+ time_limit: float | None = None,
54
67
  ):
55
68
  assert model_scores is not None
56
69
 
57
- weight_scheme = self.get_hyperparameters()["weight_scheme"]
70
+ weight_scheme = self.get_hyperparameter("weight_scheme")
58
71
 
59
72
  # drop NaNs
60
73
  model_scores = {k: v for k, v in model_scores.items() if np.isfinite(v)}
@@ -1,156 +1,36 @@
1
- import copy
2
1
  import logging
3
2
  import pprint
4
- from typing import Any, Optional
3
+ from typing import Any
5
4
 
6
- import numpy as np
7
-
8
- import autogluon.core as ag
9
- from autogluon.core.models.greedy_ensemble.ensemble_selection import EnsembleSelection
10
5
  from autogluon.timeseries import TimeSeriesDataFrame
11
- from autogluon.timeseries.metrics import TimeSeriesScorer
12
- from autogluon.timeseries.utils.datetime import get_seasonality
13
6
 
7
+ from ..ensemble_selection import fit_time_series_ensemble_selection
14
8
  from .abstract import AbstractWeightedTimeSeriesEnsembleModel
15
9
 
16
10
  logger = logging.getLogger(__name__)
17
11
 
18
12
 
19
- class TimeSeriesEnsembleSelection(EnsembleSelection):
20
- def __init__(
21
- self,
22
- ensemble_size: int,
23
- metric: TimeSeriesScorer,
24
- problem_type: str = ag.constants.QUANTILE,
25
- sorted_initialization: bool = False,
26
- bagging: bool = False,
27
- tie_breaker: str = "random",
28
- random_state: Optional[np.random.RandomState] = None,
29
- prediction_length: int = 1,
30
- target: str = "target",
31
- **kwargs,
32
- ):
33
- super().__init__(
34
- ensemble_size=ensemble_size,
35
- metric=metric, # type: ignore
36
- problem_type=problem_type,
37
- sorted_initialization=sorted_initialization,
38
- bagging=bagging,
39
- tie_breaker=tie_breaker,
40
- random_state=random_state,
41
- **kwargs,
42
- )
43
- self.prediction_length = prediction_length
44
- self.target = target
45
- self.metric: TimeSeriesScorer
46
-
47
- self.dummy_pred_per_window = []
48
- self.scorer_per_window = []
49
-
50
- self.dummy_pred_per_window: Optional[list[TimeSeriesDataFrame]]
51
- self.scorer_per_window: Optional[list[TimeSeriesScorer]]
52
- self.data_future_per_window: Optional[list[TimeSeriesDataFrame]]
53
-
54
- def fit( # type: ignore
55
- self,
56
- predictions: list[list[TimeSeriesDataFrame]],
57
- labels: list[TimeSeriesDataFrame],
58
- time_limit: Optional[float] = None,
59
- ):
60
- return super().fit(
61
- predictions=predictions, # type: ignore
62
- labels=labels, # type: ignore
63
- time_limit=time_limit,
64
- )
65
-
66
- def _fit( # type: ignore
67
- self,
68
- predictions: list[list[TimeSeriesDataFrame]],
69
- labels: list[TimeSeriesDataFrame],
70
- time_limit: Optional[float] = None,
71
- sample_weight: Optional[list[float]] = None,
72
- ):
73
- # Stack predictions for each model into a 3d tensor of shape [num_val_windows, num_rows, num_cols]
74
- stacked_predictions = [np.stack(preds) for preds in predictions]
75
-
76
- self.dummy_pred_per_window = []
77
- self.scorer_per_window = []
78
- self.data_future_per_window = []
79
-
80
- seasonal_period = self.metric.seasonal_period
81
- if seasonal_period is None:
82
- seasonal_period = get_seasonality(labels[0].freq)
83
-
84
- for window_idx, data in enumerate(labels):
85
- dummy_pred = copy.deepcopy(predictions[0][window_idx])
86
- # This should never happen; sanity check to make sure that all predictions have the same index
87
- assert all(dummy_pred.index.equals(pred[window_idx].index) for pred in predictions)
88
- assert all(dummy_pred.columns.equals(pred[window_idx].columns) for pred in predictions)
89
-
90
- self.dummy_pred_per_window.append(dummy_pred)
91
-
92
- scorer = copy.deepcopy(self.metric)
93
- # Split the observed time series once to avoid repeated computations inside the evaluator
94
- data_past = data.slice_by_timestep(None, -self.prediction_length)
95
- data_future = data.slice_by_timestep(-self.prediction_length, None)
96
- scorer.save_past_metrics(data_past, target=self.target, seasonal_period=seasonal_period)
97
- self.scorer_per_window.append(scorer)
98
- self.data_future_per_window.append(data_future)
99
-
100
- super()._fit(
101
- predictions=stacked_predictions,
102
- labels=data_future, # type: ignore
103
- time_limit=time_limit,
104
- )
105
- self.dummy_pred_per_window = None
106
- self.evaluator_per_window = None
107
- self.data_future_per_window = None
108
-
109
- def _calculate_regret( # type: ignore
110
- self,
111
- y_true,
112
- y_pred_proba,
113
- metric: TimeSeriesScorer,
114
- sample_weight=None,
115
- ):
116
- # Compute average score across all validation windows
117
- total_score = 0.0
118
-
119
- assert self.data_future_per_window is not None
120
- assert self.dummy_pred_per_window is not None
121
- assert self.scorer_per_window is not None
122
-
123
- for window_idx, data_future in enumerate(self.data_future_per_window):
124
- dummy_pred = self.dummy_pred_per_window[window_idx]
125
- dummy_pred[list(dummy_pred.columns)] = y_pred_proba[window_idx]
126
- # We use scorer.compute_metric instead of scorer.score to avoid repeated calls to scorer.save_past_metrics
127
- metric_value = self.scorer_per_window[window_idx].compute_metric(
128
- data_future,
129
- dummy_pred,
130
- target=self.target,
131
- )
132
- total_score += metric.sign * metric_value
133
- avg_score = total_score / len(self.data_future_per_window)
134
- # score: higher is better, regret: lower is better, so we flip the sign
135
- return -avg_score
136
-
137
-
138
13
  class GreedyEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
139
- """Constructs a weighted ensemble using the greedy Ensemble Selection algorithm by
140
- Caruana et al. [Car2004]
14
+ """Greedy ensemble selection algorithm that iteratively builds an ensemble by selecting models with
15
+ replacement.
16
+
17
+ This class implements the Ensemble Selection algorithm by Caruana et al. [Car2004]_, which starts
18
+ with an empty ensemble and repeatedly adds the model that most improves the ensemble's validation
19
+ performance. Models can be selected multiple times, allowing the algorithm to assign higher effective
20
+ weights to better-performing models.
141
21
 
142
22
  Other Parameters
143
23
  ----------------
144
- ensemble_size: int, default = 100
24
+ ensemble_size : int, default = 100
145
25
  Number of models (with replacement) to include in the ensemble.
146
26
 
147
27
  References
148
28
  ----------
149
- .. [Car2024] Caruana, Rich, et al. "Ensemble selection from libraries of models."
29
+ .. [Car2004] Caruana, Rich, et al. "Ensemble selection from libraries of models."
150
30
  Proceedings of the twenty-first international conference on Machine learning. 2004.
151
31
  """
152
32
 
153
- def __init__(self, name: Optional[str] = None, **kwargs):
33
+ def __init__(self, name: str | None = None, **kwargs):
154
34
  if name is None:
155
35
  # FIXME: the name here is kept for backward compatibility. it will be called
156
36
  # GreedyEnsemble in v1.4 once ensemble choices are exposed
@@ -164,24 +44,19 @@ class GreedyEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
164
44
  self,
165
45
  predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
166
46
  data_per_window: list[TimeSeriesDataFrame],
167
- model_scores: Optional[dict[str, float]] = None,
168
- time_limit: Optional[float] = None,
47
+ model_scores: dict[str, float] | None = None,
48
+ time_limit: float | None = None,
169
49
  ):
170
- ensemble_selection = TimeSeriesEnsembleSelection(
171
- ensemble_size=self.get_hyperparameters()["ensemble_size"],
172
- metric=self.eval_metric,
50
+ model_to_weight = fit_time_series_ensemble_selection(
51
+ data_per_window=data_per_window,
52
+ predictions_per_window=predictions_per_window,
53
+ ensemble_size=self.get_hyperparameter("ensemble_size"),
54
+ eval_metric=self.eval_metric,
173
55
  prediction_length=self.prediction_length,
174
56
  target=self.target,
175
- )
176
- ensemble_selection.fit(
177
- predictions=list(predictions_per_window.values()),
178
- labels=data_per_window,
179
57
  time_limit=time_limit,
180
58
  )
181
- self.model_to_weight = {}
182
- for model_name, weight in zip(predictions_per_window.keys(), ensemble_selection.weights_):
183
- if weight != 0:
184
- self.model_to_weight[model_name] = weight
59
+ self.model_to_weight = {model: weight for model, weight in model_to_weight.items() if weight > 0}
185
60
 
186
61
  weights_for_printing = {model: round(float(weight), 2) for model, weight in self.model_to_weight.items()}
187
62
  logger.info(f"\tEnsemble weights: {pprint.pformat(weights_for_printing, width=200)}")
@@ -3,7 +3,7 @@ import os
3
3
  import shutil
4
4
  from datetime import timedelta
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Any, Callable, Optional, Type, Union, cast, overload
6
+ from typing import TYPE_CHECKING, Any, Callable, Type, cast, overload
7
7
 
8
8
  import gluonts
9
9
  import gluonts.core.settings
@@ -72,12 +72,12 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
72
72
 
73
73
  def __init__(
74
74
  self,
75
- freq: Optional[str] = None,
75
+ freq: str | None = None,
76
76
  prediction_length: int = 1,
77
- path: Optional[str] = None,
78
- name: Optional[str] = None,
79
- eval_metric: Optional[str] = None,
80
- hyperparameters: Optional[dict[str, Any]] = None,
77
+ path: str | None = None,
78
+ name: str | None = None,
79
+ eval_metric: str | None = None,
80
+ hyperparameters: dict[str, Any] | None = None,
81
81
  **kwargs, # noqa
82
82
  ):
83
83
  super().__init__(
@@ -89,9 +89,9 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
89
89
  hyperparameters=hyperparameters,
90
90
  **kwargs,
91
91
  )
92
- self.gts_predictor: Optional[GluonTSPredictor] = None
93
- self._ohe_generator_known: Optional[OneHotEncoder] = None
94
- self._ohe_generator_past: Optional[OneHotEncoder] = None
92
+ self.gts_predictor: GluonTSPredictor | None = None
93
+ self._ohe_generator_known: OneHotEncoder | None = None
94
+ self._ohe_generator_past: OneHotEncoder | None = None
95
95
  self.callbacks = []
96
96
  # Following attributes may be overridden during fit() based on train_data & model parameters
97
97
  self.num_feat_static_cat = 0
@@ -105,7 +105,7 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
105
105
  self.past_feat_dynamic_cat_cardinality: list[int] = []
106
106
  self.negative_data = True
107
107
 
108
- def save(self, path: Optional[str] = None, verbose: bool = True) -> str:
108
+ def save(self, path: str | None = None, verbose: bool = True) -> str:
109
109
  # we flush callbacks instance variable if it has been set. it can keep weak references which breaks training
110
110
  self.callbacks = []
111
111
  # The GluonTS predictor is serialized using custom logic
@@ -153,18 +153,17 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
153
153
  assert dataset.static_features is not None, (
154
154
  "Static features must be provided if num_feat_static_cat > 0"
155
155
  )
156
- feat_static_cat = dataset.static_features[self.covariate_metadata.static_features_cat]
157
- self.feat_static_cat_cardinality = feat_static_cat.nunique().tolist()
156
+ self.feat_static_cat_cardinality = list(self.covariate_metadata.static_cat_cardinality.values())
158
157
 
159
158
  disable_known_covariates = model_params.get("disable_known_covariates", False)
160
159
  if not disable_known_covariates and self.supports_known_covariates:
161
160
  self.num_feat_dynamic_cat = len(self.covariate_metadata.known_covariates_cat)
162
161
  self.num_feat_dynamic_real = len(self.covariate_metadata.known_covariates_real)
163
162
  if self.num_feat_dynamic_cat > 0:
164
- feat_dynamic_cat = dataset[self.covariate_metadata.known_covariates_cat]
165
163
  if self.supports_cat_covariates:
166
- self.feat_dynamic_cat_cardinality = feat_dynamic_cat.nunique().tolist()
164
+ self.feat_dynamic_cat_cardinality = list(self.covariate_metadata.known_cat_cardinality.values())
167
165
  else:
166
+ feat_dynamic_cat = dataset[self.covariate_metadata.known_covariates_cat]
168
167
  # If model doesn't support categorical covariates, convert them to real via one hot encoding
169
168
  self._ohe_generator_known = OneHotEncoder(
170
169
  max_levels=model_params.get("max_cat_cardinality", 100),
@@ -180,10 +179,12 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
180
179
  self.num_past_feat_dynamic_cat = len(self.covariate_metadata.past_covariates_cat)
181
180
  self.num_past_feat_dynamic_real = len(self.covariate_metadata.past_covariates_real)
182
181
  if self.num_past_feat_dynamic_cat > 0:
183
- past_feat_dynamic_cat = dataset[self.covariate_metadata.past_covariates_cat]
184
182
  if self.supports_cat_covariates:
185
- self.past_feat_dynamic_cat_cardinality = past_feat_dynamic_cat.nunique().tolist()
183
+ self.past_feat_dynamic_cat_cardinality = list(
184
+ self.covariate_metadata.past_cat_cardinality.values()
185
+ )
186
186
  else:
187
+ past_feat_dynamic_cat = dataset[self.covariate_metadata.past_covariates_cat]
187
188
  # If model doesn't support categorical covariates, convert them to real via one hot encoding
188
189
  self._ohe_generator_past = OneHotEncoder(
189
190
  max_levels=model_params.get("max_cat_cardinality", 100),
@@ -277,8 +278,8 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
277
278
 
278
279
  return torch.cuda.is_available()
279
280
 
280
- def get_minimum_resources(self, is_gpu_available: bool = False) -> dict[str, Union[int, float]]:
281
- minimum_resources: dict[str, Union[int, float]] = {"num_cpus": 1}
281
+ def get_minimum_resources(self, is_gpu_available: bool = False) -> dict[str, int | float]:
282
+ minimum_resources: dict[str, int | float] = {"num_cpus": 1}
282
283
  # if GPU is available, we train with 1 GPU per trial
283
284
  if is_gpu_available:
284
285
  minimum_resources["num_gpus"] = 1
@@ -289,8 +290,8 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
289
290
  @overload
290
291
  def _to_gluonts_dataset(self, time_series_df: TimeSeriesDataFrame, known_covariates=None) -> GluonTSDataset: ...
291
292
  def _to_gluonts_dataset(
292
- self, time_series_df: Optional[TimeSeriesDataFrame], known_covariates: Optional[TimeSeriesDataFrame] = None
293
- ) -> Optional[GluonTSDataset]:
293
+ self, time_series_df: TimeSeriesDataFrame | None, known_covariates: TimeSeriesDataFrame | None = None
294
+ ) -> GluonTSDataset | None:
294
295
  if time_series_df is not None:
295
296
  # TODO: Preprocess real-valued features with StdScaler?
296
297
  if self.num_feat_static_cat > 0:
@@ -388,10 +389,10 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
388
389
  def _fit(
389
390
  self,
390
391
  train_data: TimeSeriesDataFrame,
391
- val_data: Optional[TimeSeriesDataFrame] = None,
392
- time_limit: Optional[float] = None,
393
- num_cpus: Optional[int] = None,
394
- num_gpus: Optional[int] = None,
392
+ val_data: TimeSeriesDataFrame | None = None,
393
+ time_limit: float | None = None,
394
+ num_cpus: int | None = None,
395
+ num_gpus: int | None = None,
395
396
  verbosity: int = 2,
396
397
  **kwargs,
397
398
  ) -> None:
@@ -438,8 +439,8 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
438
439
 
439
440
  def _get_callbacks(
440
441
  self,
441
- time_limit: Optional[float],
442
- early_stopping_patience: Optional[int] = None,
442
+ time_limit: float | None,
443
+ early_stopping_patience: int | None = None,
443
444
  ) -> list[Callable]:
444
445
  """Retrieve a list of callback objects for the GluonTS trainer"""
445
446
  from lightning.pytorch.callbacks import EarlyStopping, Timer
@@ -454,7 +455,7 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
454
455
  def _predict(
455
456
  self,
456
457
  data: TimeSeriesDataFrame,
457
- known_covariates: Optional[TimeSeriesDataFrame] = None,
458
+ known_covariates: TimeSeriesDataFrame | None = None,
458
459
  **kwargs,
459
460
  ) -> TimeSeriesDataFrame:
460
461
  if self.gts_predictor is None:
@@ -471,8 +472,8 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
471
472
  def _predict_gluonts_forecasts(
472
473
  self,
473
474
  data: TimeSeriesDataFrame,
474
- known_covariates: Optional[TimeSeriesDataFrame] = None,
475
- num_samples: Optional[int] = None,
475
+ known_covariates: TimeSeriesDataFrame | None = None,
476
+ num_samples: int | None = None,
476
477
  ) -> list[Forecast]:
477
478
  assert self.gts_predictor is not None, "GluonTS models must be fit before predicting."
478
479
  gts_data = self._to_gluonts_dataset(data, known_covariates=known_covariates)
@@ -1,4 +1,4 @@
1
- from typing import Any, Iterator, Optional, Type
1
+ from typing import Any, Iterator, Type
2
2
 
3
3
  import numpy as np
4
4
  import pandas as pd
@@ -17,14 +17,14 @@ class SimpleGluonTSDataset(GluonTSDataset):
17
17
  target_df: TimeSeriesDataFrame,
18
18
  freq: str,
19
19
  target_column: str = "target",
20
- feat_static_cat: Optional[np.ndarray] = None,
21
- feat_static_real: Optional[np.ndarray] = None,
22
- feat_dynamic_cat: Optional[np.ndarray] = None,
23
- feat_dynamic_real: Optional[np.ndarray] = None,
24
- past_feat_dynamic_cat: Optional[np.ndarray] = None,
25
- past_feat_dynamic_real: Optional[np.ndarray] = None,
20
+ feat_static_cat: np.ndarray | None = None,
21
+ feat_static_real: np.ndarray | None = None,
22
+ feat_dynamic_cat: np.ndarray | None = None,
23
+ feat_dynamic_real: np.ndarray | None = None,
24
+ past_feat_dynamic_cat: np.ndarray | None = None,
25
+ past_feat_dynamic_real: np.ndarray | None = None,
26
26
  includes_future: bool = False,
27
- prediction_length: Optional[int] = None,
27
+ prediction_length: int | None = None,
28
28
  ):
29
29
  assert target_df is not None
30
30
  # Convert TimeSeriesDataFrame to pd.Series for faster processing
@@ -48,7 +48,7 @@ class SimpleGluonTSDataset(GluonTSDataset):
48
48
  assert len(self.item_ids) == len(self.start_timestamps)
49
49
 
50
50
  @staticmethod
51
- def _astype(array: Optional[np.ndarray], dtype: Type[np.generic]) -> Optional[np.ndarray]:
51
+ def _astype(array: np.ndarray | None, dtype: Type[np.generic]) -> np.ndarray | None:
52
52
  if array is None:
53
53
  return None
54
54
  else:
@@ -41,10 +41,8 @@ class DeepARModel(AbstractGluonTSModel):
41
41
  Number of steps to unroll the RNN for before computing predictions
42
42
  disable_static_features : bool, default = False
43
43
  If True, static features won't be used by the model even if they are present in the dataset.
44
- If False, static features will be used by the model if they are present in the dataset.
45
44
  disable_known_covariates : bool, default = False
46
45
  If True, known covariates won't be used by the model even if they are present in the dataset.
47
- If False, known covariates will be used by the model if they are present in the dataset.
48
46
  num_layers : int, default = 2
49
47
  Number of RNN layers
50
48
  hidden_size : int, default = 40
@@ -170,13 +168,10 @@ class TemporalFusionTransformerModel(AbstractGluonTSModel):
170
168
  Distribution output object that defines how the model output is converted to a forecast, and how the loss is computed.
171
169
  disable_static_features : bool, default = False
172
170
  If True, static features won't be used by the model even if they are present in the dataset.
173
- If False, static features will be used by the model if they are present in the dataset.
174
171
  disable_known_covariates : bool, default = False
175
172
  If True, known covariates won't be used by the model even if they are present in the dataset.
176
- If False, known covariates will be used by the model if they are present in the dataset.
177
173
  disable_past_covariates : bool, default = False
178
174
  If True, past covariates won't be used by the model even if they are present in the dataset.
179
- If False, past covariates will be used by the model if they are present in the dataset.
180
175
  hidden_dim : int, default = 32
181
176
  Size of the LSTM & transformer hidden states.
182
177
  variable_dim : int, default = 32
@@ -470,10 +465,8 @@ class TiDEModel(AbstractGluonTSModel):
470
465
  Number of past values used for prediction.
471
466
  disable_static_features : bool, default = False
472
467
  If True, static features won't be used by the model even if they are present in the dataset.
473
- If False, static features will be used by the model if they are present in the dataset.
474
468
  disable_known_covariates : bool, default = False
475
469
  If True, known covariates won't be used by the model even if they are present in the dataset.
476
- If False, known covariates will be used by the model if they are present in the dataset.
477
470
  feat_proj_hidden_dim : int, default = 4
478
471
  Size of the feature projection layer.
479
472
  encoder_hidden_dim : int, default = 64
@@ -1,5 +1,3 @@
1
- import joblib.externals.loky
2
-
3
1
  from .naive import AverageModel, NaiveModel, SeasonalAverageModel, SeasonalNaiveModel
4
2
  from .npts import NPTSModel
5
3
  from .statsforecast import (
@@ -15,8 +13,3 @@ from .statsforecast import (
15
13
  ThetaModel,
16
14
  ZeroModel,
17
15
  )
18
-
19
- # By default, joblib w/ loky backend kills processes that take >300MB of RAM assuming that this is caused by a memory
20
- # leak. This leads to problems for some memory-hungry models like AutoARIMA/Theta.
21
- # This monkey patch removes this undesired behavior
22
- joblib.externals.loky.process_executor._MAX_MEMORY_LEAK_SIZE = int(3e10)
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import time
3
3
  from multiprocessing import TimeoutError
4
- from typing import Any, Callable, Optional, Union
4
+ from typing import Any, Callable
5
5
 
6
6
  import numpy as np
7
7
  import pandas as pd
@@ -12,16 +12,13 @@ from autogluon.core.utils.exceptions import TimeLimitExceeded
12
12
  from autogluon.timeseries.dataset import TimeSeriesDataFrame
13
13
  from autogluon.timeseries.metrics import TimeSeriesScorer
14
14
  from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel
15
+ from autogluon.timeseries.utils.constants import AG_DEFAULT_N_JOBS
15
16
  from autogluon.timeseries.utils.datetime import get_seasonality
16
17
  from autogluon.timeseries.utils.warning_filters import warning_filter
17
18
 
18
19
  logger = logging.getLogger(__name__)
19
20
 
20
21
 
21
- # We use the same default n_jobs across AG-TS to ensure that Joblib reuses the process pool
22
- AG_DEFAULT_N_JOBS = max(cpu_count(only_physical_cores=True), 1)
23
-
24
-
25
22
  class AbstractLocalModel(AbstractTimeSeriesModel):
26
23
  """Abstract class for local forecasting models that are trained separately for each time series.
27
24
 
@@ -40,18 +37,18 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
40
37
  """
41
38
 
42
39
  allowed_local_model_args: list[str] = []
43
- default_max_ts_length: Optional[int] = 2500
40
+ default_max_ts_length: int | None = 2500
44
41
  default_max_time_limit_ratio = 1.0
45
42
  init_time_in_seconds: int = 0
46
43
 
47
44
  def __init__(
48
45
  self,
49
- freq: Optional[str] = None,
46
+ freq: str | None = None,
50
47
  prediction_length: int = 1,
51
- path: Optional[str] = None,
52
- name: Optional[str] = None,
53
- eval_metric: Union[str, TimeSeriesScorer, None] = None,
54
- hyperparameters: Optional[dict[str, Any]] = None,
48
+ path: str | None = None,
49
+ name: str | None = None,
50
+ eval_metric: str | TimeSeriesScorer | None = None,
51
+ hyperparameters: dict[str, Any] | None = None,
55
52
  **kwargs, # noqa
56
53
  ):
57
54
  super().__init__(
@@ -79,10 +76,10 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
79
76
  def preprocess(
80
77
  self,
81
78
  data: TimeSeriesDataFrame,
82
- known_covariates: Optional[TimeSeriesDataFrame] = None,
79
+ known_covariates: TimeSeriesDataFrame | None = None,
83
80
  is_train: bool = False,
84
81
  **kwargs,
85
- ) -> tuple[TimeSeriesDataFrame, Optional[TimeSeriesDataFrame]]:
82
+ ) -> tuple[TimeSeriesDataFrame, TimeSeriesDataFrame | None]:
86
83
  if not self._get_tags()["allow_nan"]:
87
84
  data = data.fill_missing_values()
88
85
  return data, known_covariates
@@ -95,7 +92,7 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
95
92
  }
96
93
 
97
94
  @staticmethod
98
- def _compute_n_jobs(n_jobs: Union[int, float]) -> int:
95
+ def _compute_n_jobs(n_jobs: int | float) -> int:
99
96
  if isinstance(n_jobs, float) and 0 < n_jobs <= 1:
100
97
  return max(int(cpu_count() * n_jobs), 1)
101
98
  elif isinstance(n_jobs, int):
@@ -103,7 +100,7 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
103
100
  else:
104
101
  raise ValueError(f"n_jobs must be a float between 0 and 1 or an integer (received n_jobs = {n_jobs})")
105
102
 
106
- def _fit(self, train_data: TimeSeriesDataFrame, time_limit: Optional[int] = None, **kwargs):
103
+ def _fit(self, train_data: TimeSeriesDataFrame, time_limit: int | None = None, **kwargs):
107
104
  self._check_fit_params()
108
105
 
109
106
  if time_limit is not None and time_limit < self.init_time_in_seconds:
@@ -184,7 +181,7 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
184
181
  self,
185
182
  time_series: pd.Series,
186
183
  use_fallback_model: bool,
187
- end_time: Optional[float] = None,
184
+ end_time: float | None = None,
188
185
  ) -> tuple[pd.DataFrame, bool]:
189
186
  if end_time is not None and time.time() >= end_time:
190
187
  raise TimeLimitExceeded
@@ -96,7 +96,7 @@ class AverageModel(AbstractLocalModel):
96
96
  When set to a float between 0.0 and 1.0, that fraction of available CPU cores is used.
97
97
  When set to a positive integer, that many cores are used.
98
98
  When set to -1, all CPU cores are used.
99
- max_ts_length : Optional[int], default = None
99
+ max_ts_length : int | None, default = None
100
100
  If not None, only the last ``max_ts_length`` time steps of each time series will be used to train the model.
101
101
  This significantly speeds up fitting and usually leads to no change in accuracy.
102
102
  """
@@ -136,7 +136,7 @@ class SeasonalAverageModel(AbstractLocalModel):
136
136
  When set to a float between 0.0 and 1.0, that fraction of available CPU cores is used.
137
137
  When set to a positive integer, that many cores are used.
138
138
  When set to -1, all CPU cores are used.
139
- max_ts_length : Optional[int], default = None
139
+ max_ts_length : int | None, default = None
140
140
  If not None, only the last ``max_ts_length`` time steps of each time series will be used to train the model.
141
141
  This significantly speeds up fitting and usually leads to no change in accuracy.
142
142
  """
@@ -31,7 +31,7 @@ class NPTSModel(AbstractLocalModel):
31
31
  When set to a float between 0.0 and 1.0, that fraction of available CPU cores is used.
32
32
  When set to a positive integer, that many cores are used.
33
33
  When set to -1, all CPU cores are used.
34
- max_ts_length : Optional[int], default = 2500
34
+ max_ts_length : int | None, default = 2500
35
35
  If not None, only the last ``max_ts_length`` time steps of each time series will be used to train the model.
36
36
  This significantly speeds up fitting and usually leads to no change in accuracy.
37
37
  """
@@ -59,6 +59,11 @@ class NPTSModel(AbstractLocalModel):
59
59
  ) -> pd.DataFrame:
60
60
  from gluonts.model.npts import NPTSPredictor
61
61
 
62
+ # NPTS model is non-deterministic due to sampling. Set seed for reproducibility in parallel processes
63
+ # and restore original state to avoid side effects when running with n_jobs=1
64
+ original_random_state = np.random.get_state()
65
+ np.random.seed(123)
66
+
62
67
  local_model_args.pop("seasonal_period")
63
68
  num_samples = local_model_args.pop("num_samples")
64
69
  num_default_time_features = local_model_args.pop("num_default_time_features")
@@ -88,6 +93,7 @@ class NPTSModel(AbstractLocalModel):
88
93
  forecast_dict = {"mean": forecast.mean}
89
94
  for q in self.quantile_levels:
90
95
  forecast_dict[str(q)] = forecast.quantile(q)
96
+ np.random.set_state(original_random_state)
91
97
  return pd.DataFrame(forecast_dict)
92
98
 
93
99
  def _more_tags(self) -> dict: