autogluon.timeseries 1.4.1b20251016__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 (90) 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 +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 +2 -1
  11. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +52 -39
  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 +26 -15
  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 +126 -88
  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 +62 -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 +12 -12
  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/_internal/backbone/attention.py +3 -4
  48. autogluon/timeseries/models/toto/_internal/backbone/backbone.py +6 -6
  49. autogluon/timeseries/models/toto/_internal/backbone/rope.py +4 -9
  50. autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
  51. autogluon/timeseries/models/toto/_internal/backbone/scaler.py +2 -3
  52. autogluon/timeseries/models/toto/_internal/backbone/transformer.py +10 -10
  53. autogluon/timeseries/models/toto/_internal/dataset.py +2 -2
  54. autogluon/timeseries/models/toto/_internal/forecaster.py +8 -8
  55. autogluon/timeseries/models/toto/dataloader.py +4 -4
  56. autogluon/timeseries/models/toto/hf_pretrained_model.py +97 -16
  57. autogluon/timeseries/models/toto/model.py +35 -20
  58. autogluon/timeseries/predictor.py +527 -155
  59. autogluon/timeseries/regressor.py +27 -30
  60. autogluon/timeseries/splitter.py +3 -27
  61. autogluon/timeseries/trainer/ensemble_composer.py +444 -0
  62. autogluon/timeseries/trainer/model_set_builder.py +9 -9
  63. autogluon/timeseries/trainer/prediction_cache.py +16 -16
  64. autogluon/timeseries/trainer/trainer.py +300 -278
  65. autogluon/timeseries/trainer/utils.py +17 -0
  66. autogluon/timeseries/transforms/covariate_scaler.py +8 -8
  67. autogluon/timeseries/transforms/target_scaler.py +15 -15
  68. autogluon/timeseries/utils/constants.py +10 -0
  69. autogluon/timeseries/utils/datetime/lags.py +1 -3
  70. autogluon/timeseries/utils/datetime/seasonality.py +1 -3
  71. autogluon/timeseries/utils/features.py +31 -14
  72. autogluon/timeseries/utils/forecast.py +6 -7
  73. autogluon/timeseries/utils/timer.py +173 -0
  74. autogluon/timeseries/version.py +1 -1
  75. autogluon.timeseries-1.4.1b20251218-py3.11-nspkg.pth +1 -0
  76. {autogluon.timeseries-1.4.1b20251016.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/METADATA +39 -27
  77. autogluon_timeseries-1.4.1b20251218.dist-info/RECORD +103 -0
  78. {autogluon.timeseries-1.4.1b20251016.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/WHEEL +1 -1
  79. autogluon/timeseries/evaluator.py +0 -6
  80. autogluon/timeseries/models/chronos/pipeline/__init__.py +0 -10
  81. autogluon/timeseries/models/chronos/pipeline/base.py +0 -160
  82. autogluon/timeseries/models/chronos/pipeline/chronos.py +0 -544
  83. autogluon/timeseries/models/chronos/pipeline/chronos_bolt.py +0 -580
  84. autogluon.timeseries-1.4.1b20251016-py3.9-nspkg.pth +0 -1
  85. autogluon.timeseries-1.4.1b20251016.dist-info/RECORD +0 -90
  86. {autogluon.timeseries-1.4.1b20251016.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info/licenses}/LICENSE +0 -0
  87. {autogluon.timeseries-1.4.1b20251016.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info/licenses}/NOTICE +0 -0
  88. {autogluon.timeseries-1.4.1b20251016.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/namespace_packages.txt +0 -0
  89. {autogluon.timeseries-1.4.1b20251016.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/top_level.txt +0 -0
  90. {autogluon.timeseries-1.4.1b20251016.dist-info → autogluon_timeseries-1.4.1b20251218.dist-info}/zip-safe +0 -0
@@ -0,0 +1,172 @@
1
+ import logging
2
+ import pprint
3
+ import time
4
+ from typing import Any
5
+
6
+ import pandas as pd
7
+ from joblib import Parallel, delayed
8
+
9
+ from autogluon.timeseries import TimeSeriesDataFrame
10
+ from autogluon.timeseries.utils.constants import AG_DEFAULT_N_JOBS
11
+
12
+ from .abstract import AbstractTimeSeriesEnsembleModel
13
+ from .ensemble_selection import fit_time_series_ensemble_selection
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class PerItemGreedyEnsemble(AbstractTimeSeriesEnsembleModel):
19
+ """Per-item greedy ensemble that fits separate weighted ensembles for each individual time series.
20
+
21
+ This ensemble applies the greedy Ensemble Selection algorithm by Caruana et al. [Car2004]_ independently
22
+ to each time series in the dataset, allowing for customized model combinations that adapt to the
23
+ specific characteristics of individual series. Each time series gets its own optimal ensemble weights
24
+ based on predictions for that particular series. If items not seen during training are provided at prediction
25
+ time, average model weight across the training items will be used for their predictions.
26
+
27
+ The per-item approach is particularly effective for datasets with heterogeneous time series that
28
+ exhibit different patterns, seasonalities, or noise characteristics.
29
+
30
+ The algorithm uses parallel processing to efficiently fit ensembles across all time series.
31
+
32
+ Other Parameters
33
+ ----------------
34
+ ensemble_size : int, default = 100
35
+ Number of models (with replacement) to include in the ensemble.
36
+ n_jobs : int or float, default = joblib.cpu_count(only_physical_cores=True)
37
+ Number of CPU cores used to fit the ensembles in parallel.
38
+
39
+ References
40
+ ----------
41
+ .. [Car2004] Caruana, Rich, et al. "Ensemble selection from libraries of models."
42
+ Proceedings of the twenty-first international conference on Machine learning. 2004.
43
+ """
44
+
45
+ def __init__(self, name: str | None = None, **kwargs):
46
+ if name is None:
47
+ name = "PerItemWeightedEnsemble"
48
+ super().__init__(name=name, **kwargs)
49
+ self.weights_df: pd.DataFrame
50
+ self.average_weight: pd.Series
51
+
52
+ @property
53
+ def model_names(self) -> list[str]:
54
+ return list(self.weights_df.columns)
55
+
56
+ def _get_default_hyperparameters(self) -> dict[str, Any]:
57
+ return {"ensemble_size": 100, "n_jobs": AG_DEFAULT_N_JOBS}
58
+
59
+ def _fit(
60
+ self,
61
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
62
+ data_per_window: list[TimeSeriesDataFrame],
63
+ model_scores: dict[str, float] | None = None,
64
+ time_limit: float | None = None,
65
+ ) -> None:
66
+ model_names = list(predictions_per_window.keys())
67
+ item_ids = data_per_window[0].item_ids
68
+ n_jobs = min(self.get_hyperparameter("n_jobs"), len(item_ids))
69
+
70
+ predictions_per_item = self._split_predictions_per_item(predictions_per_window)
71
+ data_per_item = self._split_data_per_item(data_per_window)
72
+
73
+ ensemble_selection_kwargs = dict(
74
+ ensemble_size=self.get_hyperparameter("ensemble_size"),
75
+ eval_metric=self.eval_metric,
76
+ prediction_length=self.prediction_length,
77
+ target=self.target,
78
+ )
79
+
80
+ time_limit_per_item = None if time_limit is None else time_limit * n_jobs / len(item_ids)
81
+ end_time = None if time_limit is None else time.time() + time_limit
82
+
83
+ # Fit ensemble for each item in parallel
84
+ executor = Parallel(n_jobs=n_jobs)
85
+ weights_per_item = executor(
86
+ delayed(self._fit_item_ensemble)(
87
+ data_per_item[item_id],
88
+ predictions_per_item[item_id],
89
+ time_limit_per_item=time_limit_per_item,
90
+ end_time=end_time,
91
+ **ensemble_selection_kwargs,
92
+ )
93
+ for item_id in item_ids
94
+ )
95
+ self.weights_df = pd.DataFrame(weights_per_item, index=item_ids, columns=model_names) # type: ignore
96
+ self.average_weight = self.weights_df.mean(axis=0)
97
+
98
+ # Drop models with zero average weight
99
+ if (self.average_weight == 0).any():
100
+ models_to_keep = self.average_weight[self.average_weight > 0].index
101
+ self.weights_df = self.weights_df[models_to_keep]
102
+ self.average_weight = self.average_weight[models_to_keep]
103
+
104
+ weights_for_printing = {model: round(float(weight), 2) for model, weight in self.average_weight.items()}
105
+ logger.info(f"\tAverage ensemble weights: {pprint.pformat(weights_for_printing, width=200)}")
106
+
107
+ def _split_predictions_per_item(
108
+ self, predictions_per_window: dict[str, list[TimeSeriesDataFrame]]
109
+ ) -> dict[str, dict[str, list[TimeSeriesDataFrame]]]:
110
+ """Build a dictionary mapping item_id -> dict[model_name, list[TimeSeriesDataFrame]]."""
111
+ item_ids = list(predictions_per_window.values())[0][0].item_ids
112
+
113
+ predictions_per_item = {}
114
+ for i, item_id in enumerate(item_ids):
115
+ item_predictions = {}
116
+ for model_name, preds_per_window in predictions_per_window.items():
117
+ item_preds_per_window = [
118
+ pred.iloc[i * self.prediction_length : (i + 1) * self.prediction_length]
119
+ for pred in preds_per_window
120
+ ]
121
+ item_predictions[model_name] = item_preds_per_window
122
+ predictions_per_item[item_id] = item_predictions
123
+ return predictions_per_item
124
+
125
+ def _split_data_per_item(self, data_per_window: list[TimeSeriesDataFrame]) -> dict[str, list[TimeSeriesDataFrame]]:
126
+ """Build a dictionary mapping item_id -> ground truth values across all windows."""
127
+ item_ids = data_per_window[0].item_ids
128
+ data_per_item = {item_id: [] for item_id in item_ids}
129
+
130
+ for data in data_per_window:
131
+ indptr = data.get_indptr()
132
+ for item_idx, item_id in enumerate(item_ids):
133
+ new_slice = data.iloc[indptr[item_idx] : indptr[item_idx + 1]]
134
+ data_per_item[item_id].append(new_slice)
135
+ return data_per_item
136
+
137
+ @staticmethod
138
+ def _fit_item_ensemble(
139
+ data_per_window: list[TimeSeriesDataFrame],
140
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
141
+ time_limit_per_item: float | None = None,
142
+ end_time: float | None = None,
143
+ **ensemble_selection_kwargs,
144
+ ) -> dict[str, float]:
145
+ """Fit ensemble for a single item."""
146
+ if end_time is not None:
147
+ assert time_limit_per_item is not None
148
+ time_left = end_time - time.time()
149
+ time_limit_per_item = min(time_limit_per_item, time_left)
150
+ return fit_time_series_ensemble_selection(
151
+ data_per_window, predictions_per_window, time_limit=time_limit_per_item, **ensemble_selection_kwargs
152
+ )
153
+
154
+ def _predict(self, data: dict[str, TimeSeriesDataFrame], **kwargs) -> TimeSeriesDataFrame:
155
+ assert all(model in data for model in self.weights_df.columns)
156
+ item_ids = list(data.values())[0].item_ids
157
+ unseen_item_ids = set(item_ids) - set(self.weights_df.index)
158
+ if unseen_item_ids:
159
+ logger.debug(f"Using average weights for {len(unseen_item_ids)} unseen items")
160
+ weights = self.weights_df.reindex(item_ids).fillna(self.average_weight)
161
+
162
+ result = None
163
+ for model_name in self.weights_df.columns:
164
+ model_pred = data[model_name]
165
+ model_weights = weights[model_name].to_numpy().repeat(self.prediction_length)
166
+ weighted_pred = model_pred.to_data_frame().multiply(model_weights, axis=0)
167
+ result = weighted_pred if result is None else result + weighted_pred
168
+
169
+ return TimeSeriesDataFrame(result) # type: ignore
170
+
171
+ def remap_base_models(self, model_refit_map: dict[str, str]) -> None:
172
+ self.weights_df.rename(columns=model_refit_map, inplace=True)
@@ -0,0 +1,8 @@
1
+ from .basic import PerformanceWeightedEnsemble, SimpleAverageEnsemble
2
+ from .greedy import GreedyEnsemble
3
+
4
+ __all__ = [
5
+ "SimpleAverageEnsemble",
6
+ "PerformanceWeightedEnsemble",
7
+ "GreedyEnsemble",
8
+ ]
@@ -0,0 +1,45 @@
1
+ import functools
2
+ from abc import ABC
3
+
4
+ import numpy as np
5
+
6
+ from autogluon.timeseries.dataset import TimeSeriesDataFrame
7
+
8
+ from ..abstract import AbstractTimeSeriesEnsembleModel
9
+
10
+
11
+ class AbstractWeightedTimeSeriesEnsembleModel(AbstractTimeSeriesEnsembleModel, ABC):
12
+ """Abstract base class for weighted ensemble models that assign global weights to base models.
13
+
14
+ Weighted ensembles combine predictions from multiple base models using learned or computed weights,
15
+ where each base model receives a single global weight applied across all time series and forecast
16
+ horizons. The final prediction is computed as a weighted linear combination of base model forecasts.
17
+ """
18
+
19
+ def __init__(self, name: str | None = None, **kwargs):
20
+ super().__init__(name=name, **kwargs)
21
+ self.model_to_weight: dict[str, float] = {}
22
+
23
+ @property
24
+ def model_names(self) -> list[str]:
25
+ return list(self.model_to_weight.keys())
26
+
27
+ @property
28
+ def model_weights(self) -> np.ndarray:
29
+ return np.array(list(self.model_to_weight.values()), dtype=np.float64)
30
+
31
+ def _predict(self, data: dict[str, TimeSeriesDataFrame], **kwargs) -> TimeSeriesDataFrame:
32
+ weighted_predictions = [data[model_name] * weight for model_name, weight in self.model_to_weight.items()]
33
+ return functools.reduce(lambda x, y: x + y, weighted_predictions)
34
+
35
+ def get_info(self) -> dict:
36
+ info = super().get_info()
37
+ info["model_weights"] = self.model_to_weight.copy()
38
+ return info
39
+
40
+ def remap_base_models(self, model_refit_map: dict[str, str]) -> None:
41
+ updated_weights = {}
42
+ for model, weight in self.model_to_weight.items():
43
+ model_full_name = model_refit_map.get(model, model)
44
+ updated_weights[model_full_name] = weight
45
+ self.model_to_weight = updated_weights
@@ -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,19 +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
12
 
13
- def __init__(self, name: Optional[str] = None, **kwargs):
14
- if name is None:
15
- name = "SimpleAverageEnsemble"
16
- super().__init__(name=name, **kwargs)
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
+ """
17
18
 
18
19
  def _fit(
19
20
  self,
20
21
  predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
21
22
  data_per_window: list[TimeSeriesDataFrame],
22
- model_scores: Optional[dict[str, float]] = None,
23
- time_limit: Optional[float] = None,
23
+ model_scores: dict[str, float] | None = None,
24
+ time_limit: float | None = None,
24
25
  ):
25
26
  self.model_to_weight = {}
26
27
  num_models = len(predictions_per_window)
@@ -29,16 +30,23 @@ class SimpleAverageEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
29
30
 
30
31
 
31
32
  class PerformanceWeightedEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
32
- """Constructs a weighted ensemble, where the weights are assigned in proportion to the
33
- (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.
34
41
 
35
42
  Other Parameters
36
43
  ----------------
37
- weight_scheme: Literal["sq", "inv", "loginv"], default = "loginv"
44
+ weight_scheme : Literal["sq", "inv", "sqrt"], default = "sqrt"
38
45
  Method used to compute the weights as a function of the validation scores.
39
- - "sqrt" computes weights in proportion to `sqrt(1 / S)`. This is the default.
40
- - "inv" computes weights in proportion to `(1 / S)`.
41
- - "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]_.
42
50
 
43
51
  References
44
52
  ----------
@@ -47,11 +55,6 @@ class PerformanceWeightedEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
47
55
  36.1 (2020): 93-97.
48
56
  """
49
57
 
50
- def __init__(self, name: Optional[str] = None, **kwargs):
51
- if name is None:
52
- name = "PerformanceWeightedEnsemble"
53
- super().__init__(name=name, **kwargs)
54
-
55
58
  def _get_default_hyperparameters(self) -> dict[str, Any]:
56
59
  return {"weight_scheme": "sqrt"}
57
60
 
@@ -59,12 +62,12 @@ class PerformanceWeightedEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
59
62
  self,
60
63
  predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
61
64
  data_per_window: list[TimeSeriesDataFrame],
62
- model_scores: Optional[dict[str, float]] = None,
63
- time_limit: Optional[float] = None,
65
+ model_scores: dict[str, float] | None = None,
66
+ time_limit: float | None = None,
64
67
  ):
65
68
  assert model_scores is not None
66
69
 
67
- weight_scheme = self.get_hyperparameters()["weight_scheme"]
70
+ weight_scheme = self.get_hyperparameter("weight_scheme")
68
71
 
69
72
  # drop NaNs
70
73
  model_scores = {k: v for k, v in model_scores.items() if np.isfinite(v)}
@@ -0,0 +1,62 @@
1
+ import logging
2
+ import pprint
3
+ from typing import Any
4
+
5
+ from autogluon.timeseries import TimeSeriesDataFrame
6
+
7
+ from ..ensemble_selection import fit_time_series_ensemble_selection
8
+ from .abstract import AbstractWeightedTimeSeriesEnsembleModel
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class GreedyEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
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.
21
+
22
+ Other Parameters
23
+ ----------------
24
+ ensemble_size : int, default = 100
25
+ Number of models (with replacement) to include in the ensemble.
26
+
27
+ References
28
+ ----------
29
+ .. [Car2004] Caruana, Rich, et al. "Ensemble selection from libraries of models."
30
+ Proceedings of the twenty-first international conference on Machine learning. 2004.
31
+ """
32
+
33
+ def __init__(self, name: str | None = None, **kwargs):
34
+ if name is None:
35
+ # FIXME: the name here is kept for backward compatibility. it will be called
36
+ # GreedyEnsemble in v1.4 once ensemble choices are exposed
37
+ name = "WeightedEnsemble"
38
+ super().__init__(name=name, **kwargs)
39
+
40
+ def _get_default_hyperparameters(self) -> dict[str, Any]:
41
+ return {"ensemble_size": 100}
42
+
43
+ def _fit(
44
+ self,
45
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
46
+ data_per_window: list[TimeSeriesDataFrame],
47
+ model_scores: dict[str, float] | None = None,
48
+ time_limit: float | None = None,
49
+ ):
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,
55
+ prediction_length=self.prediction_length,
56
+ target=self.target,
57
+ time_limit=time_limit,
58
+ )
59
+ self.model_to_weight = {model: weight for model, weight in model_to_weight.items() if weight > 0}
60
+
61
+ weights_for_printing = {model: round(float(weight), 2) for model, weight in self.model_to_weight.items()}
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
@@ -21,7 +21,7 @@ from autogluon.core.hpo.constants import RAY_BACKEND
21
21
  from autogluon.tabular.models.tabular_nn.utils.categorical_encoders import (
22
22
  OneHotMergeRaresHandleUnknownEncoder as OneHotEncoder,
23
23
  )
24
- from autogluon.timeseries.dataset.ts_dataframe import ITEMID, TimeSeriesDataFrame
24
+ from autogluon.timeseries.dataset import TimeSeriesDataFrame
25
25
  from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel
26
26
  from autogluon.timeseries.utils.warning_filters import disable_root_logger, warning_filter
27
27
 
@@ -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)
@@ -566,7 +567,7 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
566
567
  ) -> TimeSeriesDataFrame:
567
568
  from gluonts.torch.model.forecast import DistributionForecast
568
569
 
569
- item_ids = forecast_index.unique(level=ITEMID)
570
+ item_ids = forecast_index.unique(level=TimeSeriesDataFrame.ITEMID)
570
571
  if isinstance(forecasts[0], SampleForecast):
571
572
  forecast_df = self._stack_sample_forecasts(cast(list[SampleForecast], forecasts), item_ids)
572
573
  elif isinstance(forecasts[0], QuantileForecast):
@@ -1,11 +1,11 @@
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
5
5
  from gluonts.dataset.common import Dataset as GluonTSDataset
6
6
  from gluonts.dataset.field_names import FieldName
7
7
 
8
- from autogluon.timeseries.dataset.ts_dataframe import TIMESTAMP, TimeSeriesDataFrame
8
+ from autogluon.timeseries.dataset import TimeSeriesDataFrame
9
9
  from autogluon.timeseries.utils.datetime import norm_freq_str
10
10
 
11
11
 
@@ -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
@@ -44,11 +44,11 @@ class SimpleGluonTSDataset(GluonTSDataset):
44
44
  # Replace inefficient groupby ITEMID with indptr that stores start:end of each time series
45
45
  self.item_ids = target_df.item_ids
46
46
  self.indptr = target_df.get_indptr()
47
- self.start_timestamps = target_df.index[self.indptr[:-1]].to_frame(index=False)[TIMESTAMP]
47
+ self.start_timestamps = target_df.index[self.indptr[:-1]].to_frame(index=False)[TimeSeriesDataFrame.TIMESTAMP]
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)