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
@@ -0,0 +1,186 @@
1
+ from typing import Literal
2
+
3
+ import numpy as np
4
+ from typing_extensions import Self
5
+
6
+ from autogluon.timeseries.utils.timer import Timer
7
+
8
+ from .abstract import EnsembleRegressor
9
+
10
+
11
+ class LinearStackerEnsembleRegressor(EnsembleRegressor):
12
+ """Linear stacker ensemble regressor using PyTorch optimization with softmax weights.
13
+
14
+ Implements weighted averaging of base model predictions with learnable weights optimized
15
+ via gradient descent. Uses PyTorch during training for optimization, then stores weights
16
+ as numpy arrays for efficient prediction.
17
+
18
+ Parameters
19
+ ----------
20
+ quantile_levels
21
+ List of quantile levels for quantile predictions (e.g., [0.1, 0.5, 0.9]).
22
+ weights_per
23
+ Weight configuration specifying which dimensions to learn weights for:
24
+
25
+ - "m": Per-model weights (shape: num_models), defaults to "m"
26
+ - "mt": Per-model and per-time weights (shape: prediction_length, num_models)
27
+ - "mq": Per-model and per-model-output (quantiles and mean) weights
28
+ (shape: num_quantiles+1, num_models)
29
+ - "mtq": Per-model, per-time, and per-quantile weights
30
+ (shape: prediction_length, num_quantiles+1, num_models)
31
+ lr
32
+ Learning rate for Adam optimizer. Defaults to 0.1.
33
+ max_epochs
34
+ Maximum number of training epochs. Defaults to 10000.
35
+ relative_tolerance
36
+ Convergence tolerance for relative loss change between epochs. Defaults to 1e-7.
37
+ prune_below
38
+ Importance threshold for model sparsification. Models with importance below this
39
+ threshold are dropped after weight optimization. Set to 0.0 to disable sparsification.
40
+ Defaults to 0.0.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ quantile_levels: list[float],
46
+ weights_per: Literal["m", "mt", "mq", "mtq"] = "m",
47
+ lr: float = 0.1,
48
+ max_epochs: int = 10_000,
49
+ relative_tolerance: float = 1e-7,
50
+ prune_below: float = 0.0,
51
+ ):
52
+ super().__init__()
53
+ self.quantile_levels = quantile_levels
54
+ self.weights_per = weights_per
55
+ self.lr = lr
56
+ self.max_epochs = max_epochs
57
+ self.relative_tolerance = relative_tolerance
58
+ self.prune_below = prune_below
59
+
60
+ self.weights: np.ndarray | None = None
61
+ self.kept_indices: list[int] | None = None
62
+
63
+ def _compute_weight_shape(self, base_model_predictions_shape: tuple) -> tuple:
64
+ """Compute weight tensor shape based on weights_per configuration."""
65
+ _, _, prediction_length, num_outputs, num_models = base_model_predictions_shape
66
+
67
+ shapes = {
68
+ "m": (1, 1, num_models),
69
+ "mt": (prediction_length, 1, num_models),
70
+ "mq": (1, num_outputs, num_models),
71
+ "mtq": (prediction_length, num_outputs, num_models),
72
+ }
73
+ try:
74
+ return (1, 1) + shapes[self.weights_per]
75
+ except KeyError:
76
+ raise ValueError(f"Unsupported weights_per: {self.weights_per}")
77
+
78
+ def make_weighted_average_module(self, base_model_predictions_shape: tuple):
79
+ import torch
80
+
81
+ class WeightedAverage(torch.nn.Module):
82
+ def __init__(self, shape):
83
+ super().__init__()
84
+ self.raw_weights = torch.nn.Parameter(torch.zeros(*shape, dtype=torch.float32))
85
+
86
+ def get_normalized_weights(self):
87
+ return torch.softmax(self.raw_weights, dim=-1) # softmax over models
88
+
89
+ def forward(self, base_model_predictions: torch.Tensor):
90
+ return torch.sum(self.get_normalized_weights() * base_model_predictions, dim=-1)
91
+
92
+ return WeightedAverage(self._compute_weight_shape(base_model_predictions_shape))
93
+
94
+ def fit(
95
+ self,
96
+ base_model_mean_predictions: np.ndarray,
97
+ base_model_quantile_predictions: np.ndarray,
98
+ labels: np.ndarray,
99
+ time_limit: float | None = None,
100
+ ) -> Self:
101
+ import torch
102
+
103
+ def _ql(
104
+ labels_tensor: torch.Tensor,
105
+ ensemble_predictions: torch.Tensor,
106
+ ) -> torch.Tensor:
107
+ """Compute the weighted quantile loss on predictions and ground truth (labels).
108
+ Considering that the first dimension of predictions is the mean, we treat
109
+ mean predictions on the same footing as median (0.5) predictions as contribution
110
+ to the overall weighted quantile loss.
111
+ """
112
+ quantile_levels = torch.tensor([0.5] + self.quantile_levels, dtype=torch.float32)
113
+ error = labels_tensor - ensemble_predictions # (num_windows, num_items, num_time, num_outputs)
114
+ quantile_loss = torch.maximum(quantile_levels * error, (quantile_levels - 1) * error)
115
+ return torch.mean(quantile_loss)
116
+
117
+ timer = Timer(time_limit).start()
118
+
119
+ base_model_predictions = torch.tensor(
120
+ np.concatenate(
121
+ [base_model_mean_predictions, base_model_quantile_predictions],
122
+ axis=3,
123
+ ),
124
+ dtype=torch.float32,
125
+ )
126
+ labels_tensor = torch.tensor(labels, dtype=torch.float32)
127
+
128
+ weighted_average = self.make_weighted_average_module(base_model_predictions.shape)
129
+
130
+ optimizer = torch.optim.Adam(weighted_average.parameters(), lr=self.lr)
131
+
132
+ prev_loss = float("inf")
133
+ for _ in range(self.max_epochs):
134
+ optimizer.zero_grad()
135
+
136
+ ensemble_predictions = weighted_average(base_model_predictions)
137
+
138
+ loss = _ql(labels_tensor, ensemble_predictions)
139
+ loss.backward()
140
+ optimizer.step()
141
+
142
+ loss_change = abs(prev_loss - loss.item()) / (loss.item() + 1e-8)
143
+ if loss_change < self.relative_tolerance:
144
+ break
145
+ prev_loss = loss.item()
146
+
147
+ if timer.timed_out():
148
+ break
149
+
150
+ with torch.no_grad():
151
+ self.weights = weighted_average.get_normalized_weights().detach().numpy()
152
+
153
+ assert self.weights is not None
154
+ if self.prune_below > 0.0:
155
+ importances = self.weights.mean(axis=tuple(range(self.weights.ndim - 1))) # shape (num_models,)
156
+
157
+ mask = importances >= self.prune_below
158
+ if not mask.any():
159
+ mask[importances.argmax()] = True
160
+
161
+ if not mask.all():
162
+ self.kept_indices = np.where(mask)[0].tolist()
163
+ self.weights = self.weights[..., mask]
164
+ self.weights = self.weights / self.weights.sum(axis=-1, keepdims=True)
165
+
166
+ return self
167
+
168
+ def predict(
169
+ self,
170
+ base_model_mean_predictions: np.ndarray,
171
+ base_model_quantile_predictions: np.ndarray,
172
+ ) -> tuple[np.ndarray, np.ndarray]:
173
+ if self.weights is None:
174
+ raise ValueError("Model must be fitted before prediction")
175
+
176
+ all_predictions = np.concatenate([base_model_mean_predictions, base_model_quantile_predictions], axis=3)
177
+
178
+ if self.kept_indices is not None:
179
+ assert all_predictions.shape[-1] == len(self.kept_indices)
180
+
181
+ ensemble_pred = np.sum(self.weights * all_predictions, axis=-1)
182
+
183
+ mean_predictions = ensemble_pred[:, :, :, :1]
184
+ quantile_predictions = ensemble_pred[:, :, :, 1:]
185
+
186
+ return mean_predictions, quantile_predictions
@@ -0,0 +1,94 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from typing_extensions import Self
6
+
7
+ from autogluon.tabular.registry import ag_model_registry as tabular_ag_model_registry
8
+ from autogluon.timeseries.utils.timer import SplitTimer
9
+
10
+ from .abstract import EnsembleRegressor
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class PerQuantileTabularEnsembleRegressor(EnsembleRegressor):
16
+ """Ensemble regressor using separate models per quantile plus dedicated mean model."""
17
+
18
+ def __init__(
19
+ self,
20
+ quantile_levels: list[float],
21
+ model_name: str,
22
+ model_hyperparameters: dict | None = None,
23
+ ):
24
+ super().__init__()
25
+ self.quantile_levels = quantile_levels
26
+ model_type = tabular_ag_model_registry.key_to_cls(model_name)
27
+ model_hyperparameters = model_hyperparameters or {}
28
+ self.mean_model = model_type(
29
+ problem_type="regression",
30
+ hyperparameters=model_hyperparameters,
31
+ path="",
32
+ name=f"{model_name}_mean",
33
+ )
34
+ self.quantile_models = [
35
+ model_type(
36
+ problem_type="quantile",
37
+ hyperparameters=model_hyperparameters | {"ag.quantile_levels": [quantile]},
38
+ path="",
39
+ name=f"{model_name}_q{quantile}",
40
+ )
41
+ for quantile in quantile_levels
42
+ ]
43
+
44
+ def fit(
45
+ self,
46
+ base_model_mean_predictions: np.ndarray,
47
+ base_model_quantile_predictions: np.ndarray,
48
+ labels: np.ndarray,
49
+ time_limit: float | None = None,
50
+ ) -> Self:
51
+ num_windows, num_items, prediction_length = base_model_mean_predictions.shape[:3]
52
+ y = pd.Series(labels.reshape(num_windows * num_items * prediction_length))
53
+
54
+ total_rounds = 1 + len(self.quantile_levels)
55
+ timer = SplitTimer(time_limit, rounds=total_rounds).start()
56
+
57
+ # Fit mean model
58
+ X_mean = self._get_feature_df(base_model_mean_predictions, 0)
59
+ self.mean_model.fit(X=X_mean, y=y, time_limit=timer.round_time_remaining())
60
+ timer.next_round()
61
+
62
+ # Fit quantile models
63
+ for i, model in enumerate(self.quantile_models):
64
+ X_q = self._get_feature_df(base_model_quantile_predictions, i)
65
+ model.fit(X=X_q, y=y, time_limit=timer.round_time_remaining())
66
+ timer.next_round()
67
+
68
+ return self
69
+
70
+ def _get_feature_df(self, predictions: np.ndarray, index: int) -> pd.DataFrame:
71
+ num_windows, num_items, prediction_length, _, num_models = predictions.shape
72
+ num_tabular_items = num_windows * num_items * prediction_length
73
+ return pd.DataFrame(
74
+ predictions[:, :, :, index].reshape(num_tabular_items, num_models),
75
+ columns=[f"model_{mi}" for mi in range(num_models)],
76
+ )
77
+
78
+ def predict(
79
+ self, base_model_mean_predictions: np.ndarray, base_model_quantile_predictions: np.ndarray
80
+ ) -> tuple[np.ndarray, np.ndarray]:
81
+ assert self.mean_model.is_fit()
82
+ num_windows, num_items, prediction_length = base_model_mean_predictions.shape[:3]
83
+ assert num_windows == 1, "Prediction expects a single window to be provided"
84
+
85
+ X_mean = self._get_feature_df(base_model_mean_predictions, 0)
86
+ mean_predictions = self.mean_model.predict(X_mean).reshape(num_windows, num_items, prediction_length, 1)
87
+
88
+ quantile_predictions_list = []
89
+ for i, model in enumerate(self.quantile_models):
90
+ X_q = self._get_feature_df(base_model_quantile_predictions, i)
91
+ quantile_predictions_list.append(model.predict(X_q).reshape(num_windows, num_items, prediction_length))
92
+ quantile_predictions = np.stack(quantile_predictions_list, axis=-1)
93
+
94
+ return mean_predictions, quantile_predictions
@@ -0,0 +1,107 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from typing_extensions import Self
6
+
7
+ from autogluon.tabular.registry import ag_model_registry as tabular_ag_model_registry
8
+
9
+ from .abstract import EnsembleRegressor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TabularEnsembleRegressor(EnsembleRegressor):
15
+ """Ensemble regressor based on a single model from AutoGluon-Tabular that predicts all quantiles simultaneously."""
16
+
17
+ def __init__(
18
+ self,
19
+ quantile_levels: list[float],
20
+ model_name: str,
21
+ model_hyperparameters: dict | None = None,
22
+ ):
23
+ super().__init__()
24
+ self.quantile_levels = quantile_levels
25
+ model_type = tabular_ag_model_registry.key_to_cls(model_name)
26
+ model_hyperparameters = model_hyperparameters or {}
27
+ self.model = model_type(
28
+ problem_type="quantile",
29
+ hyperparameters=model_hyperparameters | {"ag.quantile_levels": quantile_levels},
30
+ path="",
31
+ name=model_name,
32
+ )
33
+
34
+ def fit(
35
+ self,
36
+ base_model_mean_predictions: np.ndarray,
37
+ base_model_quantile_predictions: np.ndarray,
38
+ labels: np.ndarray,
39
+ time_limit: float | None = None,
40
+ ) -> Self:
41
+ X = self._get_feature_df(base_model_mean_predictions, base_model_quantile_predictions)
42
+ num_windows, num_items, prediction_length = base_model_mean_predictions.shape[:3]
43
+ y = pd.Series(labels.reshape(num_windows * num_items * prediction_length))
44
+ self.model.fit(X=X, y=y, time_limit=time_limit)
45
+ return self
46
+
47
+ def predict(
48
+ self,
49
+ base_model_mean_predictions: np.ndarray,
50
+ base_model_quantile_predictions: np.ndarray,
51
+ ) -> tuple[np.ndarray, np.ndarray]:
52
+ assert self.model.is_fit()
53
+ num_windows, num_items, prediction_length = base_model_mean_predictions.shape[:3]
54
+ assert num_windows == 1, "Prediction expects a single window to be provided"
55
+
56
+ X = self._get_feature_df(base_model_mean_predictions, base_model_quantile_predictions)
57
+
58
+ pred = self.model.predict(X)
59
+
60
+ # Reshape back to (num_windows, num_items, prediction_length, num_quantiles)
61
+ pred = pred.reshape(num_windows, num_items, prediction_length, len(self.quantile_levels))
62
+
63
+ # Use median quantile as mean prediction
64
+ median_idx = self._get_median_quantile_index()
65
+ mean_pred = pred[:, :, :, median_idx : median_idx + 1]
66
+ quantile_pred = pred
67
+
68
+ return mean_pred, quantile_pred
69
+
70
+ def _get_feature_df(
71
+ self,
72
+ base_model_mean_predictions: np.ndarray,
73
+ base_model_quantile_predictions: np.ndarray,
74
+ ) -> pd.DataFrame:
75
+ num_windows, num_items, prediction_length, _, num_models = base_model_mean_predictions.shape
76
+ num_tabular_items = num_windows * num_items * prediction_length
77
+ features_array = np.hstack(
78
+ [
79
+ base_model_mean_predictions.reshape(num_tabular_items, -1),
80
+ base_model_quantile_predictions.reshape(num_tabular_items, -1),
81
+ ]
82
+ )
83
+ return pd.DataFrame(features_array, columns=self._get_feature_names(num_models))
84
+
85
+ def _get_feature_names(self, num_models: int) -> list[str]:
86
+ feature_names = []
87
+ for mi in range(num_models):
88
+ feature_names.append(f"model_{mi}_mean")
89
+ for quantile in self.quantile_levels:
90
+ for mi in range(num_models):
91
+ feature_names.append(f"model_{mi}_q{quantile}")
92
+
93
+ return feature_names
94
+
95
+ def _get_median_quantile_index(self):
96
+ """Get quantile index closest to 0.5"""
97
+ quantile_array = np.array(self.quantile_levels)
98
+ median_idx = int(np.argmin(np.abs(quantile_array - 0.5)))
99
+ selected_quantile = quantile_array[median_idx]
100
+
101
+ if selected_quantile != 0.5:
102
+ logger.warning(
103
+ f"Selected quantile {selected_quantile} is not exactly 0.5. "
104
+ f"Using closest available quantile for median prediction."
105
+ )
106
+
107
+ return median_idx
@@ -1,7 +1,4 @@
1
1
  import copy
2
- import logging
3
- import pprint
4
- from typing import Any, Optional
5
2
 
6
3
  import numpy as np
7
4
 
@@ -11,10 +8,6 @@ from autogluon.timeseries import TimeSeriesDataFrame
11
8
  from autogluon.timeseries.metrics import TimeSeriesScorer
12
9
  from autogluon.timeseries.utils.datetime import get_seasonality
13
10
 
14
- from .abstract import AbstractWeightedTimeSeriesEnsembleModel
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
11
 
19
12
  class TimeSeriesEnsembleSelection(EnsembleSelection):
20
13
  def __init__(
@@ -25,7 +18,7 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
25
18
  sorted_initialization: bool = False,
26
19
  bagging: bool = False,
27
20
  tie_breaker: str = "random",
28
- random_state: Optional[np.random.RandomState] = None,
21
+ random_state: np.random.RandomState | None = None,
29
22
  prediction_length: int = 1,
30
23
  target: str = "target",
31
24
  **kwargs,
@@ -47,15 +40,15 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
47
40
  self.dummy_pred_per_window = []
48
41
  self.scorer_per_window = []
49
42
 
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]]
43
+ self.dummy_pred_per_window: list[TimeSeriesDataFrame] | None
44
+ self.scorer_per_window: list[TimeSeriesScorer] | None
45
+ self.data_future_per_window: list[TimeSeriesDataFrame] | None
53
46
 
54
47
  def fit( # type: ignore
55
48
  self,
56
49
  predictions: list[list[TimeSeriesDataFrame]],
57
50
  labels: list[TimeSeriesDataFrame],
58
- time_limit: Optional[float] = None,
51
+ time_limit: float | None = None,
59
52
  ):
60
53
  return super().fit(
61
54
  predictions=predictions, # type: ignore
@@ -67,8 +60,8 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
67
60
  self,
68
61
  predictions: list[list[TimeSeriesDataFrame]],
69
62
  labels: list[TimeSeriesDataFrame],
70
- time_limit: Optional[float] = None,
71
- sample_weight: Optional[list[float]] = None,
63
+ time_limit: float | None = None,
64
+ sample_weight: list[float] | None = None,
72
65
  ):
73
66
  # Stack predictions for each model into a 3d tensor of shape [num_val_windows, num_rows, num_cols]
74
67
  stacked_predictions = [np.stack(preds) for preds in predictions]
@@ -135,53 +128,40 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
135
128
  return -avg_score
136
129
 
137
130
 
138
- class GreedyEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
139
- """Constructs a weighted ensemble using the greedy Ensemble Selection algorithm by
140
- Caruana et al. [Car2004]
141
-
142
- Other Parameters
143
- ----------------
144
- ensemble_size: int, default = 100
145
- Number of models (with replacement) to include in the ensemble.
131
+ def fit_time_series_ensemble_selection(
132
+ data_per_window: list[TimeSeriesDataFrame],
133
+ predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
134
+ ensemble_size: int,
135
+ eval_metric: TimeSeriesScorer,
136
+ prediction_length: int = 1,
137
+ target: str = "target",
138
+ time_limit: float | None = None,
139
+ ) -> dict[str, float]:
140
+ """Fit ensemble selection for time series forecasting and return ensemble weights.
146
141
 
147
- References
142
+ Parameters
148
143
  ----------
149
- .. [Car2024] Caruana, Rich, et al. "Ensemble selection from libraries of models."
150
- Proceedings of the twenty-first international conference on Machine learning. 2004.
144
+ data_per_window:
145
+ List of ground truth time series data for each validation window.
146
+ predictions_per_window:
147
+ Dictionary mapping model names to their predictions for each validation window.
148
+ ensemble_size:
149
+ Number of iterations of the ensemble selection algorithm.
150
+
151
+ Returns
152
+ -------
153
+ weights:
154
+ Dictionary mapping the model name to its weight in the ensemble.
151
155
  """
152
-
153
- def __init__(self, name: Optional[str] = None, **kwargs):
154
- if name is None:
155
- # FIXME: the name here is kept for backward compatibility. it will be called
156
- # GreedyEnsemble in v1.4 once ensemble choices are exposed
157
- name = "WeightedEnsemble"
158
- super().__init__(name=name, **kwargs)
159
-
160
- def _get_default_hyperparameters(self) -> dict[str, Any]:
161
- return {"ensemble_size": 100}
162
-
163
- def _fit(
164
- self,
165
- predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
166
- data_per_window: list[TimeSeriesDataFrame],
167
- model_scores: Optional[dict[str, float]] = None,
168
- time_limit: Optional[float] = None,
169
- ):
170
- ensemble_selection = TimeSeriesEnsembleSelection(
171
- ensemble_size=self.get_hyperparameters()["ensemble_size"],
172
- metric=self.eval_metric,
173
- prediction_length=self.prediction_length,
174
- target=self.target,
175
- )
176
- ensemble_selection.fit(
177
- predictions=list(predictions_per_window.values()),
178
- labels=data_per_window,
179
- time_limit=time_limit,
180
- )
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
185
-
186
- weights_for_printing = {model: round(float(weight), 2) for model, weight in self.model_to_weight.items()}
187
- logger.info(f"\tEnsemble weights: {pprint.pformat(weights_for_printing, width=200)}")
156
+ ensemble_selection = TimeSeriesEnsembleSelection(
157
+ ensemble_size=ensemble_size,
158
+ metric=eval_metric,
159
+ prediction_length=prediction_length,
160
+ target=target,
161
+ )
162
+ ensemble_selection.fit(
163
+ predictions=list(predictions_per_window.values()),
164
+ labels=data_per_window,
165
+ time_limit=time_limit,
166
+ )
167
+ return {model: float(weight) for model, weight in zip(predictions_per_window.keys(), ensemble_selection.weights_)}