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.
- autogluon/timeseries/configs/hyperparameter_presets.py +13 -28
- autogluon/timeseries/configs/predictor_presets.py +23 -39
- autogluon/timeseries/dataset/ts_dataframe.py +97 -86
- autogluon/timeseries/learner.py +70 -35
- autogluon/timeseries/metrics/__init__.py +4 -4
- autogluon/timeseries/metrics/abstract.py +8 -8
- autogluon/timeseries/metrics/point.py +9 -9
- autogluon/timeseries/metrics/quantile.py +5 -5
- autogluon/timeseries/metrics/utils.py +4 -4
- autogluon/timeseries/models/__init__.py +4 -1
- autogluon/timeseries/models/abstract/abstract_timeseries_model.py +52 -50
- autogluon/timeseries/models/abstract/model_trial.py +2 -1
- autogluon/timeseries/models/abstract/tunable.py +8 -8
- autogluon/timeseries/models/autogluon_tabular/mlforecast.py +58 -62
- autogluon/timeseries/models/autogluon_tabular/per_step.py +27 -16
- autogluon/timeseries/models/autogluon_tabular/transforms.py +11 -9
- autogluon/timeseries/models/chronos/__init__.py +2 -1
- autogluon/timeseries/models/chronos/chronos2.py +395 -0
- autogluon/timeseries/models/chronos/model.py +127 -89
- autogluon/timeseries/models/chronos/{pipeline/utils.py → utils.py} +69 -37
- autogluon/timeseries/models/ensemble/__init__.py +36 -2
- autogluon/timeseries/models/ensemble/abstract.py +14 -46
- autogluon/timeseries/models/ensemble/array_based/__init__.py +3 -0
- autogluon/timeseries/models/ensemble/array_based/abstract.py +240 -0
- autogluon/timeseries/models/ensemble/array_based/models.py +185 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/__init__.py +12 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/abstract.py +88 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/linear_stacker.py +186 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/per_quantile_tabular.py +94 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/tabular.py +107 -0
- autogluon/timeseries/models/ensemble/{greedy.py → ensemble_selection.py} +41 -61
- autogluon/timeseries/models/ensemble/per_item_greedy.py +172 -0
- autogluon/timeseries/models/ensemble/weighted/__init__.py +8 -0
- autogluon/timeseries/models/ensemble/weighted/abstract.py +45 -0
- autogluon/timeseries/models/ensemble/{basic.py → weighted/basic.py} +25 -22
- autogluon/timeseries/models/ensemble/weighted/greedy.py +64 -0
- autogluon/timeseries/models/gluonts/abstract.py +32 -31
- autogluon/timeseries/models/gluonts/dataset.py +11 -11
- autogluon/timeseries/models/gluonts/models.py +0 -7
- autogluon/timeseries/models/local/__init__.py +0 -7
- autogluon/timeseries/models/local/abstract_local_model.py +15 -18
- autogluon/timeseries/models/local/naive.py +2 -2
- autogluon/timeseries/models/local/npts.py +7 -1
- autogluon/timeseries/models/local/statsforecast.py +13 -13
- autogluon/timeseries/models/multi_window/multi_window_model.py +39 -24
- autogluon/timeseries/models/registry.py +3 -4
- autogluon/timeseries/models/toto/__init__.py +3 -0
- autogluon/timeseries/models/toto/_internal/__init__.py +9 -0
- autogluon/timeseries/models/toto/_internal/backbone/__init__.py +3 -0
- autogluon/timeseries/models/toto/_internal/backbone/attention.py +196 -0
- autogluon/timeseries/models/toto/_internal/backbone/backbone.py +262 -0
- autogluon/timeseries/models/toto/_internal/backbone/distribution.py +70 -0
- autogluon/timeseries/models/toto/_internal/backbone/kvcache.py +136 -0
- autogluon/timeseries/models/toto/_internal/backbone/rope.py +89 -0
- autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
- autogluon/timeseries/models/toto/_internal/backbone/scaler.py +305 -0
- autogluon/timeseries/models/toto/_internal/backbone/transformer.py +333 -0
- autogluon/timeseries/models/toto/_internal/dataset.py +165 -0
- autogluon/timeseries/models/toto/_internal/forecaster.py +423 -0
- autogluon/timeseries/models/toto/dataloader.py +108 -0
- autogluon/timeseries/models/toto/hf_pretrained_model.py +200 -0
- autogluon/timeseries/models/toto/model.py +249 -0
- autogluon/timeseries/predictor.py +541 -162
- autogluon/timeseries/regressor.py +27 -30
- autogluon/timeseries/splitter.py +3 -27
- autogluon/timeseries/trainer/ensemble_composer.py +444 -0
- autogluon/timeseries/trainer/model_set_builder.py +9 -9
- autogluon/timeseries/trainer/prediction_cache.py +16 -16
- autogluon/timeseries/trainer/trainer.py +300 -279
- autogluon/timeseries/trainer/utils.py +17 -0
- autogluon/timeseries/transforms/covariate_scaler.py +8 -8
- autogluon/timeseries/transforms/target_scaler.py +15 -15
- autogluon/timeseries/utils/constants.py +10 -0
- autogluon/timeseries/utils/datetime/lags.py +1 -3
- autogluon/timeseries/utils/datetime/seasonality.py +1 -3
- autogluon/timeseries/utils/features.py +31 -14
- autogluon/timeseries/utils/forecast.py +6 -7
- autogluon/timeseries/utils/timer.py +173 -0
- autogluon/timeseries/version.py +1 -1
- autogluon.timeseries-1.5.1b20260122-py3.11-nspkg.pth +1 -0
- {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/METADATA +39 -22
- autogluon_timeseries-1.5.1b20260122.dist-info/RECORD +103 -0
- {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/WHEEL +1 -1
- autogluon/timeseries/evaluator.py +0 -6
- autogluon/timeseries/models/chronos/pipeline/__init__.py +0 -10
- autogluon/timeseries/models/chronos/pipeline/base.py +0 -160
- autogluon/timeseries/models/chronos/pipeline/chronos.py +0 -544
- autogluon/timeseries/models/chronos/pipeline/chronos_bolt.py +0 -580
- autogluon.timeseries-1.4.1b20250907-py3.9-nspkg.pth +0 -1
- autogluon.timeseries-1.4.1b20250907.dist-info/RECORD +0 -75
- {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info/licenses}/LICENSE +0 -0
- {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info/licenses}/NOTICE +0 -0
- {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/namespace_packages.txt +0 -0
- {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.dist-info}/top_level.txt +0 -0
- {autogluon.timeseries-1.4.1b20250907.dist-info → autogluon_timeseries-1.5.1b20260122.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=1000)}")
|
|
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,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
|
|
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
|
-
"""
|
|
11
|
+
"""Simple ensemble that assigns equal weights to all base models for uniform averaging.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
23
|
-
time_limit:
|
|
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
|
-
"""
|
|
33
|
-
|
|
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", "
|
|
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
|
-
|
|
40
|
-
- "
|
|
41
|
-
- "
|
|
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:
|
|
63
|
-
time_limit:
|
|
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.
|
|
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,64 @@
|
|
|
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
|
+
Also known as ``WeightedEnsemble`` for backward compatibility.
|
|
18
|
+
|
|
19
|
+
This class implements the Ensemble Selection algorithm by Caruana et al. [Car2004]_, which starts
|
|
20
|
+
with an empty ensemble and repeatedly adds the model that most improves the ensemble's validation
|
|
21
|
+
performance. Models can be selected multiple times, allowing the algorithm to assign higher effective
|
|
22
|
+
weights to better-performing models.
|
|
23
|
+
|
|
24
|
+
Other Parameters
|
|
25
|
+
----------------
|
|
26
|
+
ensemble_size : int, default = 100
|
|
27
|
+
Number of models (with replacement) to include in the ensemble.
|
|
28
|
+
|
|
29
|
+
References
|
|
30
|
+
----------
|
|
31
|
+
.. [Car2004] Caruana, Rich, et al. "Ensemble selection from libraries of models."
|
|
32
|
+
Proceedings of the twenty-first international conference on Machine learning. 2004.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, name: str | None = None, **kwargs):
|
|
36
|
+
if name is None:
|
|
37
|
+
# FIXME: the name here is kept for backward compatibility. it will be called
|
|
38
|
+
# GreedyEnsemble in v1.4 once ensemble choices are exposed
|
|
39
|
+
name = "WeightedEnsemble"
|
|
40
|
+
super().__init__(name=name, **kwargs)
|
|
41
|
+
|
|
42
|
+
def _get_default_hyperparameters(self) -> dict[str, Any]:
|
|
43
|
+
return {"ensemble_size": 100}
|
|
44
|
+
|
|
45
|
+
def _fit(
|
|
46
|
+
self,
|
|
47
|
+
predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
|
|
48
|
+
data_per_window: list[TimeSeriesDataFrame],
|
|
49
|
+
model_scores: dict[str, float] | None = None,
|
|
50
|
+
time_limit: float | None = None,
|
|
51
|
+
):
|
|
52
|
+
model_to_weight = fit_time_series_ensemble_selection(
|
|
53
|
+
data_per_window=data_per_window,
|
|
54
|
+
predictions_per_window=predictions_per_window,
|
|
55
|
+
ensemble_size=self.get_hyperparameter("ensemble_size"),
|
|
56
|
+
eval_metric=self.eval_metric,
|
|
57
|
+
prediction_length=self.prediction_length,
|
|
58
|
+
target=self.target,
|
|
59
|
+
time_limit=time_limit,
|
|
60
|
+
)
|
|
61
|
+
self.model_to_weight = {model: weight for model, weight in model_to_weight.items() if weight > 0}
|
|
62
|
+
|
|
63
|
+
weights_for_printing = {model: round(float(weight), 2) for model, weight in self.model_to_weight.items()}
|
|
64
|
+
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,
|
|
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
|
|
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:
|
|
75
|
+
freq: str | None = None,
|
|
76
76
|
prediction_length: int = 1,
|
|
77
|
-
path:
|
|
78
|
-
name:
|
|
79
|
-
eval_metric:
|
|
80
|
-
hyperparameters:
|
|
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:
|
|
93
|
-
self._ohe_generator_known:
|
|
94
|
-
self._ohe_generator_past:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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,
|
|
281
|
-
minimum_resources: dict[str,
|
|
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:
|
|
293
|
-
) ->
|
|
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:
|
|
392
|
-
time_limit:
|
|
393
|
-
num_cpus:
|
|
394
|
-
num_gpus:
|
|
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:
|
|
442
|
-
early_stopping_patience:
|
|
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:
|
|
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:
|
|
475
|
-
num_samples:
|
|
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,
|
|
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
|
|
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:
|
|
21
|
-
feat_static_real:
|
|
22
|
-
feat_dynamic_cat:
|
|
23
|
-
feat_dynamic_real:
|
|
24
|
-
past_feat_dynamic_cat:
|
|
25
|
-
past_feat_dynamic_real:
|
|
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:
|
|
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:
|
|
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)
|