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
|
@@ -5,7 +5,7 @@ import time
|
|
|
5
5
|
import traceback
|
|
6
6
|
from collections import defaultdict
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any, Literal
|
|
8
|
+
from typing import Any, Literal
|
|
9
9
|
|
|
10
10
|
import networkx as nx
|
|
11
11
|
import numpy as np
|
|
@@ -20,18 +20,20 @@ from autogluon.core.utils.savers import save_pkl
|
|
|
20
20
|
from autogluon.timeseries import TimeSeriesDataFrame
|
|
21
21
|
from autogluon.timeseries.metrics import TimeSeriesScorer, check_get_evaluation_metric
|
|
22
22
|
from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel, TimeSeriesModelBase
|
|
23
|
-
from autogluon.timeseries.models.ensemble import AbstractTimeSeriesEnsembleModel
|
|
23
|
+
from autogluon.timeseries.models.ensemble import AbstractTimeSeriesEnsembleModel
|
|
24
24
|
from autogluon.timeseries.models.multi_window import MultiWindowBacktestingModel
|
|
25
25
|
from autogluon.timeseries.splitter import AbstractWindowSplitter, ExpandingWindowSplitter
|
|
26
|
+
from autogluon.timeseries.trainer.ensemble_composer import EnsembleComposer, validate_ensemble_hyperparameters
|
|
26
27
|
from autogluon.timeseries.utils.features import (
|
|
27
28
|
ConstantReplacementFeatureImportanceTransform,
|
|
28
29
|
CovariateMetadata,
|
|
29
30
|
PermutationFeatureImportanceTransform,
|
|
30
31
|
)
|
|
31
|
-
from autogluon.timeseries.utils.warning_filters import disable_tqdm
|
|
32
|
+
from autogluon.timeseries.utils.warning_filters import disable_tqdm
|
|
32
33
|
|
|
33
34
|
from .model_set_builder import TrainableModelSetBuilder, contains_searchspace
|
|
34
35
|
from .prediction_cache import PredictionCache, get_prediction_cache
|
|
36
|
+
from .utils import log_scores_and_times
|
|
35
37
|
|
|
36
38
|
logger = logging.getLogger("autogluon.timeseries.trainer")
|
|
37
39
|
|
|
@@ -45,16 +47,16 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
45
47
|
self,
|
|
46
48
|
path: str,
|
|
47
49
|
prediction_length: int = 1,
|
|
48
|
-
eval_metric:
|
|
50
|
+
eval_metric: str | TimeSeriesScorer | None = None,
|
|
49
51
|
save_data: bool = True,
|
|
50
52
|
skip_model_selection: bool = False,
|
|
51
53
|
enable_ensemble: bool = True,
|
|
52
54
|
verbosity: int = 2,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
num_val_windows: tuple[int, ...] = (1,),
|
|
56
|
+
val_step_size: int | None = None,
|
|
57
|
+
refit_every_n_windows: int | None = 1,
|
|
55
58
|
# TODO: Set cache_predictions=False by default once all models in default presets have a reasonable inference speed
|
|
56
59
|
cache_predictions: bool = True,
|
|
57
|
-
ensemble_model_type: Optional[Type] = None,
|
|
58
60
|
**kwargs,
|
|
59
61
|
):
|
|
60
62
|
super().__init__(
|
|
@@ -71,13 +73,11 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
71
73
|
self.skip_model_selection = skip_model_selection
|
|
72
74
|
# Ensemble cannot be fit if val_scores are not computed
|
|
73
75
|
self.enable_ensemble = enable_ensemble and not skip_model_selection
|
|
74
|
-
if ensemble_model_type is None:
|
|
75
|
-
ensemble_model_type = GreedyEnsemble
|
|
76
|
-
else:
|
|
76
|
+
if kwargs.get("ensemble_model_type") is not None:
|
|
77
77
|
logger.warning(
|
|
78
|
-
"Using a custom `ensemble_model_type` is
|
|
78
|
+
"Using a custom `ensemble_model_type` is no longer supported. Use the `ensemble_hyperparameters` "
|
|
79
|
+
"argument to `fit` instead."
|
|
79
80
|
)
|
|
80
|
-
self.ensemble_model_type: Type[AbstractTimeSeriesEnsembleModel] = ensemble_model_type
|
|
81
81
|
|
|
82
82
|
self.verbosity = verbosity
|
|
83
83
|
|
|
@@ -86,10 +86,16 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
86
86
|
self.model_refit_map = {}
|
|
87
87
|
|
|
88
88
|
self.eval_metric = check_get_evaluation_metric(eval_metric, prediction_length=prediction_length)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
|
|
90
|
+
self.num_val_windows = num_val_windows
|
|
91
|
+
|
|
92
|
+
# Validate num_val_windows
|
|
93
|
+
if len(self.num_val_windows) == 0:
|
|
94
|
+
raise ValueError("num_val_windows cannot be empty")
|
|
95
|
+
if not all(isinstance(w, int) and w > 0 for w in self.num_val_windows):
|
|
96
|
+
raise ValueError(f"num_val_windows must contain only positive integers, got {self.num_val_windows}")
|
|
97
|
+
|
|
98
|
+
self.val_step_size = val_step_size
|
|
93
99
|
self.refit_every_n_windows = refit_every_n_windows
|
|
94
100
|
self.hpo_results = {}
|
|
95
101
|
|
|
@@ -112,14 +118,14 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
112
118
|
path = os.path.join(self.path_data, "train.pkl")
|
|
113
119
|
return load_pkl.load(path=path)
|
|
114
120
|
|
|
115
|
-
def load_val_data(self) ->
|
|
121
|
+
def load_val_data(self) -> TimeSeriesDataFrame | None:
|
|
116
122
|
path = os.path.join(self.path_data, "val.pkl")
|
|
117
123
|
if os.path.exists(path):
|
|
118
124
|
return load_pkl.load(path=path)
|
|
119
125
|
else:
|
|
120
126
|
return None
|
|
121
127
|
|
|
122
|
-
def load_data(self) -> tuple[TimeSeriesDataFrame,
|
|
128
|
+
def load_data(self) -> tuple[TimeSeriesDataFrame, TimeSeriesDataFrame | None]:
|
|
123
129
|
train_data = self.load_train_data()
|
|
124
130
|
val_data = self.load_val_data()
|
|
125
131
|
return train_data, val_data
|
|
@@ -142,7 +148,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
142
148
|
def _add_model(
|
|
143
149
|
self,
|
|
144
150
|
model: TimeSeriesModelBase,
|
|
145
|
-
base_models:
|
|
151
|
+
base_models: list[str] | None = None,
|
|
146
152
|
):
|
|
147
153
|
"""Add a model to the model graph of the trainer. If the model is an ensemble, also add
|
|
148
154
|
information about dependencies to the model graph (list of models specified via ``base_models``).
|
|
@@ -174,8 +180,8 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
174
180
|
for base_model in base_models:
|
|
175
181
|
self.model_graph.add_edge(base_model, model.name)
|
|
176
182
|
|
|
177
|
-
def
|
|
178
|
-
"""Get a dictionary mapping each model to their
|
|
183
|
+
def _get_model_layers(self) -> dict[str, int]:
|
|
184
|
+
"""Get a dictionary mapping each model to their layer in the model graph"""
|
|
179
185
|
|
|
180
186
|
# get nodes without a parent
|
|
181
187
|
rootset = set(self.model_graph.nodes)
|
|
@@ -188,14 +194,14 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
188
194
|
for dest_node in paths_to:
|
|
189
195
|
paths_from[dest_node][source_node] = paths_to[dest_node]
|
|
190
196
|
|
|
191
|
-
# determine
|
|
192
|
-
|
|
197
|
+
# determine layers
|
|
198
|
+
layers = {}
|
|
193
199
|
for n in paths_from:
|
|
194
|
-
|
|
200
|
+
layers[n] = max(paths_from[n].get(src, 0) for src in rootset)
|
|
195
201
|
|
|
196
|
-
return
|
|
202
|
+
return layers
|
|
197
203
|
|
|
198
|
-
def get_models_attribute_dict(self, attribute: str, models:
|
|
204
|
+
def get_models_attribute_dict(self, attribute: str, models: list[str] | None = None) -> dict[str, Any]:
|
|
199
205
|
"""Get an attribute from the `model_graph` for each of the model names
|
|
200
206
|
specified. If `models` is none, the attribute will be returned for all models"""
|
|
201
207
|
results = {}
|
|
@@ -213,25 +219,25 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
213
219
|
if len(models) == 1:
|
|
214
220
|
return models[0]
|
|
215
221
|
model_performances = self.get_models_attribute_dict(attribute="val_score")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
(m, model_performances[m],
|
|
222
|
+
model_layers = self._get_model_layers()
|
|
223
|
+
model_name_score_layer_list = [
|
|
224
|
+
(m, model_performances[m], model_layers.get(m, 0)) for m in models if model_performances[m] is not None
|
|
219
225
|
]
|
|
220
226
|
|
|
221
|
-
if not
|
|
227
|
+
if not model_name_score_layer_list:
|
|
222
228
|
raise ValueError("No fitted models have validation scores computed.")
|
|
223
229
|
|
|
224
230
|
# rank models in terms of validation score. if two models have the same validation score,
|
|
225
|
-
# rank them by their
|
|
231
|
+
# rank them by their layer in the model graph (lower layer models are preferred).
|
|
226
232
|
return max(
|
|
227
|
-
|
|
228
|
-
key=lambda mns: (mns[1], -mns[2]), # (score, -
|
|
233
|
+
model_name_score_layer_list,
|
|
234
|
+
key=lambda mns: (mns[1], -mns[2]), # (score, -layer)
|
|
229
235
|
)[0]
|
|
230
236
|
|
|
231
|
-
def get_model_names(self,
|
|
237
|
+
def get_model_names(self, layer: int | None = None) -> list[str]:
|
|
232
238
|
"""Get model names that are registered in the model graph"""
|
|
233
|
-
if
|
|
234
|
-
return list(node for node, l in self.
|
|
239
|
+
if layer is not None:
|
|
240
|
+
return list(node for node, l in self._get_model_layers().items() if l == layer) # noqa: E741
|
|
235
241
|
return list(self.model_graph.nodes)
|
|
236
242
|
|
|
237
243
|
def get_info(self, include_model_info: bool = False) -> dict[str, Any]:
|
|
@@ -259,32 +265,13 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
259
265
|
|
|
260
266
|
return info
|
|
261
267
|
|
|
262
|
-
def _train_single(
|
|
263
|
-
self,
|
|
264
|
-
train_data: TimeSeriesDataFrame,
|
|
265
|
-
model: AbstractTimeSeriesModel,
|
|
266
|
-
val_data: Optional[TimeSeriesDataFrame] = None,
|
|
267
|
-
time_limit: Optional[float] = None,
|
|
268
|
-
) -> AbstractTimeSeriesModel:
|
|
269
|
-
"""Train the single model and return the model object that was fitted. This method
|
|
270
|
-
does not save the resulting model."""
|
|
271
|
-
model.fit(
|
|
272
|
-
train_data=train_data,
|
|
273
|
-
val_data=val_data,
|
|
274
|
-
time_limit=time_limit,
|
|
275
|
-
verbosity=self.verbosity,
|
|
276
|
-
val_splitter=self.val_splitter,
|
|
277
|
-
refit_every_n_windows=self.refit_every_n_windows,
|
|
278
|
-
)
|
|
279
|
-
return model
|
|
280
|
-
|
|
281
268
|
def tune_model_hyperparameters(
|
|
282
269
|
self,
|
|
283
270
|
model: AbstractTimeSeriesModel,
|
|
284
271
|
train_data: TimeSeriesDataFrame,
|
|
285
|
-
time_limit:
|
|
286
|
-
val_data:
|
|
287
|
-
hyperparameter_tune_kwargs:
|
|
272
|
+
time_limit: float | None = None,
|
|
273
|
+
val_data: TimeSeriesDataFrame | None = None,
|
|
274
|
+
hyperparameter_tune_kwargs: str | dict = "auto",
|
|
288
275
|
):
|
|
289
276
|
default_num_trials = None
|
|
290
277
|
if time_limit is None and (
|
|
@@ -300,7 +287,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
300
287
|
hyperparameter_tune_kwargs=hyperparameter_tune_kwargs,
|
|
301
288
|
time_limit=time_limit,
|
|
302
289
|
default_num_trials=default_num_trials,
|
|
303
|
-
val_splitter=self.
|
|
290
|
+
val_splitter=self._get_val_splitter(use_val_data=val_data is not None),
|
|
304
291
|
refit_every_n_windows=self.refit_every_n_windows,
|
|
305
292
|
)
|
|
306
293
|
total_tuning_time = time.time() - tuning_start_time
|
|
@@ -310,11 +297,21 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
310
297
|
# add each of the trained HPO configurations to the trained models
|
|
311
298
|
for model_hpo_name, model_info in hpo_models.items():
|
|
312
299
|
model_path = os.path.join(self.path, model_info["path"])
|
|
300
|
+
|
|
313
301
|
# Only load model configurations that didn't fail
|
|
314
|
-
if Path(model_path).exists():
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
302
|
+
if not Path(model_path).exists():
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
model_hpo = self.load_model(model_hpo_name, path=model_path, model_type=type(model))
|
|
306
|
+
|
|
307
|
+
# override validation score to align evaluations on the final ensemble layer's window
|
|
308
|
+
if isinstance(model_hpo, MultiWindowBacktestingModel):
|
|
309
|
+
model_hpo.val_score = float(
|
|
310
|
+
np.mean([info["val_score"] for info in model_hpo.info_per_val_window[-self.num_val_windows[-1] :]])
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
self._add_model(model_hpo)
|
|
314
|
+
model_names_trained.append(model_hpo.name)
|
|
318
315
|
|
|
319
316
|
logger.info(f"\tTrained {len(model_names_trained)} models while tuning {model.name}.")
|
|
320
317
|
|
|
@@ -335,8 +332,8 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
335
332
|
self,
|
|
336
333
|
train_data: TimeSeriesDataFrame,
|
|
337
334
|
model: AbstractTimeSeriesModel,
|
|
338
|
-
val_data:
|
|
339
|
-
time_limit:
|
|
335
|
+
val_data: TimeSeriesDataFrame | None = None,
|
|
336
|
+
time_limit: float | None = None,
|
|
340
337
|
) -> list[str]:
|
|
341
338
|
"""Fit and save the given model on given training and validation data and save the trained model.
|
|
342
339
|
|
|
@@ -353,18 +350,39 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
353
350
|
logger.info(f"\tSkipping {model.name} due to lack of time remaining.")
|
|
354
351
|
return model_names_trained
|
|
355
352
|
|
|
356
|
-
model
|
|
353
|
+
model.fit(
|
|
354
|
+
train_data=train_data,
|
|
355
|
+
val_data=None if isinstance(model, MultiWindowBacktestingModel) else val_data,
|
|
356
|
+
time_limit=time_limit,
|
|
357
|
+
verbosity=self.verbosity,
|
|
358
|
+
val_splitter=self._get_val_splitter(use_val_data=val_data is not None),
|
|
359
|
+
refit_every_n_windows=self.refit_every_n_windows,
|
|
360
|
+
)
|
|
361
|
+
|
|
357
362
|
fit_end_time = time.time()
|
|
358
363
|
model.fit_time = model.fit_time or (fit_end_time - fit_start_time)
|
|
359
364
|
|
|
360
365
|
if time_limit is not None:
|
|
361
366
|
time_limit = time_limit - (fit_end_time - fit_start_time)
|
|
362
|
-
if val_data is not None
|
|
367
|
+
if val_data is not None:
|
|
363
368
|
model.score_and_cache_oof(
|
|
364
369
|
val_data, store_val_score=True, store_predict_time=True, time_limit=time_limit
|
|
365
370
|
)
|
|
366
371
|
|
|
367
|
-
|
|
372
|
+
# by default, MultiWindowBacktestingModel computes validation score on all windows. However,
|
|
373
|
+
# when doing multi-layer stacking, the trainer only scores on the windows of the last layer.
|
|
374
|
+
# we override the val_score to align scores.
|
|
375
|
+
if isinstance(model, MultiWindowBacktestingModel):
|
|
376
|
+
model.val_score = float(
|
|
377
|
+
np.mean([info["val_score"] for info in model.info_per_val_window[-self.num_val_windows[-1] :]])
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
log_scores_and_times(
|
|
381
|
+
val_score=model.val_score,
|
|
382
|
+
fit_time=model.fit_time,
|
|
383
|
+
predict_time=model.predict_time,
|
|
384
|
+
eval_metric_name=self.eval_metric.name_with_sign,
|
|
385
|
+
)
|
|
368
386
|
|
|
369
387
|
self.save_model(model=model)
|
|
370
388
|
except TimeLimitExceeded:
|
|
@@ -380,34 +398,64 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
380
398
|
|
|
381
399
|
return model_names_trained
|
|
382
400
|
|
|
383
|
-
def
|
|
384
|
-
self,
|
|
385
|
-
val_score: Optional[float] = None,
|
|
386
|
-
fit_time: Optional[float] = None,
|
|
387
|
-
predict_time: Optional[float] = None,
|
|
388
|
-
):
|
|
389
|
-
if val_score is not None:
|
|
390
|
-
logger.info(f"\t{val_score:<7.4f}".ljust(15) + f"= Validation score ({self.eval_metric.name_with_sign})")
|
|
391
|
-
if fit_time is not None:
|
|
392
|
-
logger.info(f"\t{fit_time:<7.2f} s".ljust(15) + "= Training runtime")
|
|
393
|
-
if predict_time is not None:
|
|
394
|
-
logger.info(f"\t{predict_time:<7.2f} s".ljust(15) + "= Validation (prediction) runtime")
|
|
395
|
-
|
|
396
|
-
def _train_multi(
|
|
401
|
+
def fit(
|
|
397
402
|
self,
|
|
398
403
|
train_data: TimeSeriesDataFrame,
|
|
399
|
-
hyperparameters:
|
|
400
|
-
val_data:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
404
|
+
hyperparameters: str | dict[Any, dict],
|
|
405
|
+
val_data: TimeSeriesDataFrame | None = None,
|
|
406
|
+
ensemble_hyperparameters: dict | list[dict] | None = None,
|
|
407
|
+
hyperparameter_tune_kwargs: str | dict | None = None,
|
|
408
|
+
excluded_model_types: list[str] | None = None,
|
|
409
|
+
time_limit: float | None = None,
|
|
410
|
+
random_seed: int | None = None,
|
|
411
|
+
):
|
|
412
|
+
"""Fit a set of timeseries models specified by the `hyperparameters`
|
|
413
|
+
dictionary that maps model names to their specified hyperparameters.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
train_data
|
|
418
|
+
Training data for fitting time series timeseries models.
|
|
419
|
+
hyperparameters
|
|
420
|
+
A dictionary mapping selected model names, model classes or model factory to hyperparameter
|
|
421
|
+
settings. Model names should be present in `trainer.presets.DEFAULT_MODEL_NAMES`. Optionally,
|
|
422
|
+
the user may provide one of "default", "light" and "very_light" to specify presets.
|
|
423
|
+
val_data
|
|
424
|
+
Optional validation data set to report validation scores on.
|
|
425
|
+
ensemble_hyperparameters
|
|
426
|
+
A dictionary mapping ensemble names to their specified hyperparameters. Ensemble names
|
|
427
|
+
should be defined in the models.ensemble namespace. defaults to `{"GreedyEnsemble": {}}`
|
|
428
|
+
which only fits a greedy weighted ensemble with default hyperparameters. Providing an
|
|
429
|
+
empty dictionary disables ensemble training.
|
|
430
|
+
hyperparameter_tune_kwargs
|
|
431
|
+
Args for hyperparameter tuning
|
|
432
|
+
excluded_model_types
|
|
433
|
+
Names of models that should not be trained, even if listed in `hyperparameters`.
|
|
434
|
+
time_limit
|
|
435
|
+
Time limit for training
|
|
436
|
+
random_seed
|
|
437
|
+
Random seed that will be set to each model during training
|
|
438
|
+
"""
|
|
406
439
|
logger.info(f"\nStarting training. Start time is {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
407
440
|
|
|
441
|
+
# Handle ensemble hyperparameters
|
|
442
|
+
if ensemble_hyperparameters is None:
|
|
443
|
+
ensemble_hyperparameters = [{"GreedyEnsemble": {}}]
|
|
444
|
+
if isinstance(ensemble_hyperparameters, dict):
|
|
445
|
+
ensemble_hyperparameters = [ensemble_hyperparameters]
|
|
446
|
+
validate_ensemble_hyperparameters(ensemble_hyperparameters)
|
|
447
|
+
|
|
408
448
|
time_start = time.time()
|
|
409
449
|
hyperparameters = copy.deepcopy(hyperparameters)
|
|
410
450
|
|
|
451
|
+
if val_data is not None:
|
|
452
|
+
if self.num_val_windows[-1] != 1:
|
|
453
|
+
raise ValueError(
|
|
454
|
+
f"When val_data is provided, the last element of num_val_windows must be 1, "
|
|
455
|
+
f"got {self.num_val_windows[-1]}"
|
|
456
|
+
)
|
|
457
|
+
multi_window = self._get_val_splitter(use_val_data=val_data is not None).num_val_windows > 0
|
|
458
|
+
|
|
411
459
|
if self.save_data and not self.is_data_saved:
|
|
412
460
|
self.save_train_data(train_data)
|
|
413
461
|
if val_data is not None:
|
|
@@ -418,7 +466,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
418
466
|
hyperparameters=hyperparameters,
|
|
419
467
|
hyperparameter_tune=hyperparameter_tune_kwargs is not None, # TODO: remove hyperparameter_tune
|
|
420
468
|
freq=train_data.freq,
|
|
421
|
-
multi_window=
|
|
469
|
+
multi_window=multi_window,
|
|
422
470
|
excluded_model_types=excluded_model_types,
|
|
423
471
|
)
|
|
424
472
|
|
|
@@ -447,7 +495,6 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
447
495
|
time_reserved_for_ensemble = min(
|
|
448
496
|
self.max_ensemble_time_limit, time_left / (num_base_models - i + 1)
|
|
449
497
|
)
|
|
450
|
-
logger.debug(f"Reserving {time_reserved_for_ensemble:.1f}s for ensemble")
|
|
451
498
|
else:
|
|
452
499
|
time_reserved_for_ensemble = 0.0
|
|
453
500
|
time_left_for_model = (time_left - time_reserved_for_ensemble) / (num_base_models - i)
|
|
@@ -487,42 +534,16 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
487
534
|
train_data, model=model, val_data=val_data, time_limit=time_left_for_model
|
|
488
535
|
)
|
|
489
536
|
|
|
490
|
-
if self.enable_ensemble:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
f"Time left: {time_left_for_ensemble:.1f} seconds"
|
|
501
|
-
)
|
|
502
|
-
elif len(models_available_for_ensemble) <= 1:
|
|
503
|
-
logger.info(
|
|
504
|
-
"Not fitting ensemble as "
|
|
505
|
-
+ (
|
|
506
|
-
"no models were successfully trained."
|
|
507
|
-
if not models_available_for_ensemble
|
|
508
|
-
else "only 1 model was trained."
|
|
509
|
-
)
|
|
510
|
-
)
|
|
511
|
-
else:
|
|
512
|
-
try:
|
|
513
|
-
model_names_trained.append(
|
|
514
|
-
self.fit_ensemble(
|
|
515
|
-
data_per_window=self._get_ensemble_oof_data(train_data=train_data, val_data=val_data),
|
|
516
|
-
model_names=models_available_for_ensemble,
|
|
517
|
-
time_limit=time_left_for_ensemble,
|
|
518
|
-
)
|
|
519
|
-
)
|
|
520
|
-
except Exception as err: # noqa
|
|
521
|
-
logger.error(
|
|
522
|
-
"\tWarning: Exception caused ensemble to fail during training... Skipping this model."
|
|
523
|
-
)
|
|
524
|
-
logger.error(f"\t{err}")
|
|
525
|
-
logger.debug(traceback.format_exc())
|
|
537
|
+
if self.enable_ensemble and ensemble_hyperparameters:
|
|
538
|
+
model_names = self.get_model_names(layer=0)
|
|
539
|
+
ensemble_names = self._fit_ensembles(
|
|
540
|
+
data_per_window=self._get_validation_windows(train_data, val_data),
|
|
541
|
+
predictions_per_window=self._get_base_model_predictions(model_names),
|
|
542
|
+
time_limit=None if time_limit is None else time_limit - (time.time() - time_start),
|
|
543
|
+
ensemble_hyperparameters=ensemble_hyperparameters,
|
|
544
|
+
num_windows_per_layer=self.num_val_windows,
|
|
545
|
+
)
|
|
546
|
+
model_names_trained.extend(ensemble_names)
|
|
526
547
|
|
|
527
548
|
logger.info(f"Training complete. Models trained: {model_names_trained}")
|
|
528
549
|
logger.info(f"Total runtime: {time.time() - time_start:.2f} s")
|
|
@@ -536,82 +557,64 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
536
557
|
|
|
537
558
|
return model_names_trained
|
|
538
559
|
|
|
539
|
-
def
|
|
540
|
-
self, train_data: TimeSeriesDataFrame, val_data: Optional[TimeSeriesDataFrame]
|
|
541
|
-
) -> list[TimeSeriesDataFrame]:
|
|
542
|
-
if val_data is None:
|
|
543
|
-
return [val_fold for _, val_fold in self.val_splitter.split(train_data)]
|
|
544
|
-
else:
|
|
545
|
-
return [val_data]
|
|
546
|
-
|
|
547
|
-
def _get_ensemble_model_name(self) -> str:
|
|
548
|
-
"""Ensure we don't have name collisions in the ensemble model name"""
|
|
549
|
-
ensemble_name = "WeightedEnsemble"
|
|
550
|
-
increment = 1
|
|
551
|
-
while ensemble_name in self._get_banned_model_names():
|
|
552
|
-
increment += 1
|
|
553
|
-
ensemble_name = f"WeightedEnsemble_{increment}"
|
|
554
|
-
return ensemble_name
|
|
555
|
-
|
|
556
|
-
def fit_ensemble(
|
|
560
|
+
def _fit_ensembles(
|
|
557
561
|
self,
|
|
562
|
+
*,
|
|
558
563
|
data_per_window: list[TimeSeriesDataFrame],
|
|
559
|
-
|
|
560
|
-
time_limit:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
for model_name in model_names:
|
|
568
|
-
predictions_per_window[model_name] = self._get_model_oof_predictions(model_name=model_name)
|
|
569
|
-
|
|
570
|
-
time_start = time.time()
|
|
571
|
-
ensemble = self.ensemble_model_type(
|
|
572
|
-
name=self._get_ensemble_model_name(),
|
|
564
|
+
predictions_per_window: dict[str, list[TimeSeriesDataFrame]],
|
|
565
|
+
time_limit: float | None,
|
|
566
|
+
ensemble_hyperparameters: list[dict],
|
|
567
|
+
num_windows_per_layer: tuple[int, ...],
|
|
568
|
+
) -> list[str]:
|
|
569
|
+
ensemble_composer = EnsembleComposer(
|
|
570
|
+
path=self.path,
|
|
571
|
+
prediction_length=self.prediction_length,
|
|
573
572
|
eval_metric=self.eval_metric,
|
|
574
573
|
target=self.target,
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
freq=data_per_window[0].freq,
|
|
574
|
+
ensemble_hyperparameters=ensemble_hyperparameters,
|
|
575
|
+
num_windows_per_layer=num_windows_per_layer,
|
|
578
576
|
quantile_levels=self.quantile_levels,
|
|
579
|
-
|
|
577
|
+
model_graph=self.model_graph,
|
|
578
|
+
).fit(
|
|
579
|
+
data_per_window=data_per_window,
|
|
580
|
+
predictions_per_window=predictions_per_window,
|
|
581
|
+
time_limit=time_limit,
|
|
580
582
|
)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
val_score=ensemble.val_score,
|
|
603
|
-
fit_time=ensemble.fit_time,
|
|
604
|
-
predict_time=ensemble.predict_time,
|
|
583
|
+
|
|
584
|
+
ensembles_trained = []
|
|
585
|
+
for _, model, base_models in ensemble_composer.iter_ensembles():
|
|
586
|
+
self._add_model(model=model, base_models=base_models)
|
|
587
|
+
self.save_model(model=model)
|
|
588
|
+
ensembles_trained.append(model.name)
|
|
589
|
+
|
|
590
|
+
return ensembles_trained
|
|
591
|
+
|
|
592
|
+
def _get_validation_windows(self, train_data: TimeSeriesDataFrame, val_data: TimeSeriesDataFrame | None):
|
|
593
|
+
train_splitter = self._get_val_splitter(use_val_data=val_data is not None)
|
|
594
|
+
return [val_fold for _, val_fold in train_splitter.split(train_data)] + (
|
|
595
|
+
[] if val_data is None else [val_data]
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def _get_val_splitter(self, use_val_data: bool = False) -> AbstractWindowSplitter:
|
|
599
|
+
num_windows_from_train = sum(self.num_val_windows[:-1]) if use_val_data else sum(self.num_val_windows)
|
|
600
|
+
return ExpandingWindowSplitter(
|
|
601
|
+
prediction_length=self.prediction_length,
|
|
602
|
+
num_val_windows=num_windows_from_train,
|
|
603
|
+
val_step_size=self.val_step_size,
|
|
605
604
|
)
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
605
|
+
|
|
606
|
+
def _get_base_model_predictions(self, model_names: list[str]) -> dict[str, list[TimeSeriesDataFrame]]:
|
|
607
|
+
"""Get base model predictions for ensemble training / inference."""
|
|
608
|
+
predictions_per_window = {}
|
|
609
|
+
for model_name in model_names:
|
|
610
|
+
predictions_per_window[model_name] = self._get_model_oof_predictions(model_name)
|
|
611
|
+
return predictions_per_window
|
|
609
612
|
|
|
610
613
|
def leaderboard(
|
|
611
614
|
self,
|
|
612
|
-
data:
|
|
615
|
+
data: TimeSeriesDataFrame | None = None,
|
|
613
616
|
extra_info: bool = False,
|
|
614
|
-
extra_metrics:
|
|
617
|
+
extra_metrics: list[str | TimeSeriesScorer] | None = None,
|
|
615
618
|
use_cache: bool = True,
|
|
616
619
|
) -> pd.DataFrame:
|
|
617
620
|
logger.debug("Generating leaderboard for all models trained")
|
|
@@ -701,7 +704,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
701
704
|
return df[explicit_column_order]
|
|
702
705
|
|
|
703
706
|
def persist(
|
|
704
|
-
self, model_names:
|
|
707
|
+
self, model_names: Literal["all", "best"] | list[str] = "all", with_ancestors: bool = False
|
|
705
708
|
) -> list[str]:
|
|
706
709
|
if model_names == "all":
|
|
707
710
|
model_names = self.get_model_names()
|
|
@@ -726,7 +729,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
726
729
|
|
|
727
730
|
return model_names
|
|
728
731
|
|
|
729
|
-
def unpersist(self, model_names:
|
|
732
|
+
def unpersist(self, model_names: Literal["all"] | list[str] = "all") -> list[str]:
|
|
730
733
|
if model_names == "all":
|
|
731
734
|
model_names = list(self.models.keys())
|
|
732
735
|
if not isinstance(model_names, list):
|
|
@@ -738,9 +741,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
738
741
|
unpersisted_models.append(model)
|
|
739
742
|
return unpersisted_models
|
|
740
743
|
|
|
741
|
-
def _get_model_for_prediction(
|
|
742
|
-
self, model: Optional[Union[str, TimeSeriesModelBase]] = None, verbose: bool = True
|
|
743
|
-
) -> str:
|
|
744
|
+
def _get_model_for_prediction(self, model: str | TimeSeriesModelBase | None = None, verbose: bool = True) -> str:
|
|
744
745
|
"""Given an optional identifier or model object, return the name of the model with which to predict.
|
|
745
746
|
|
|
746
747
|
If the model is not provided, this method will default to the best model according to the validation score.
|
|
@@ -766,10 +767,10 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
766
767
|
def predict(
|
|
767
768
|
self,
|
|
768
769
|
data: TimeSeriesDataFrame,
|
|
769
|
-
known_covariates:
|
|
770
|
-
model:
|
|
770
|
+
known_covariates: TimeSeriesDataFrame | None = None,
|
|
771
|
+
model: str | TimeSeriesModelBase | None = None,
|
|
771
772
|
use_cache: bool = True,
|
|
772
|
-
random_seed:
|
|
773
|
+
random_seed: int | None = None,
|
|
773
774
|
) -> TimeSeriesDataFrame:
|
|
774
775
|
model_name = self._get_model_for_prediction(model)
|
|
775
776
|
model_pred_dict, _ = self.get_model_pred_dict(
|
|
@@ -784,7 +785,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
784
785
|
raise ValueError(f"Model {model_name} failed to predict. Please check the model's logs.")
|
|
785
786
|
return predictions
|
|
786
787
|
|
|
787
|
-
def _get_eval_metric(self, metric:
|
|
788
|
+
def _get_eval_metric(self, metric: str | TimeSeriesScorer | None) -> TimeSeriesScorer:
|
|
788
789
|
if metric is None:
|
|
789
790
|
return self.eval_metric
|
|
790
791
|
else:
|
|
@@ -799,7 +800,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
799
800
|
self,
|
|
800
801
|
data: TimeSeriesDataFrame,
|
|
801
802
|
predictions: TimeSeriesDataFrame,
|
|
802
|
-
metric:
|
|
803
|
+
metric: str | TimeSeriesScorer | None = None,
|
|
803
804
|
) -> float:
|
|
804
805
|
"""Compute the score measuring how well the predictions align with the data."""
|
|
805
806
|
return self._get_eval_metric(metric).score(
|
|
@@ -811,8 +812,8 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
811
812
|
def score(
|
|
812
813
|
self,
|
|
813
814
|
data: TimeSeriesDataFrame,
|
|
814
|
-
model:
|
|
815
|
-
metric:
|
|
815
|
+
model: str | TimeSeriesModelBase | None = None,
|
|
816
|
+
metric: str | TimeSeriesScorer | None = None,
|
|
816
817
|
use_cache: bool = True,
|
|
817
818
|
) -> float:
|
|
818
819
|
eval_metric = self._get_eval_metric(metric)
|
|
@@ -822,8 +823,8 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
822
823
|
def evaluate(
|
|
823
824
|
self,
|
|
824
825
|
data: TimeSeriesDataFrame,
|
|
825
|
-
model:
|
|
826
|
-
metrics:
|
|
826
|
+
model: str | TimeSeriesModelBase | None = None,
|
|
827
|
+
metrics: str | TimeSeriesScorer | list[str | TimeSeriesScorer] | None = None,
|
|
827
828
|
use_cache: bool = True,
|
|
828
829
|
) -> dict[str, float]:
|
|
829
830
|
past_data, known_covariates = data.get_model_inputs_for_scoring(
|
|
@@ -844,13 +845,13 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
844
845
|
self,
|
|
845
846
|
data: TimeSeriesDataFrame,
|
|
846
847
|
features: list[str],
|
|
847
|
-
model:
|
|
848
|
-
metric:
|
|
849
|
-
time_limit:
|
|
848
|
+
model: str | TimeSeriesModelBase | None = None,
|
|
849
|
+
metric: str | TimeSeriesScorer | None = None,
|
|
850
|
+
time_limit: float | None = None,
|
|
850
851
|
method: Literal["naive", "permutation"] = "permutation",
|
|
851
852
|
subsample_size: int = 50,
|
|
852
|
-
num_iterations:
|
|
853
|
-
random_seed:
|
|
853
|
+
num_iterations: int | None = None,
|
|
854
|
+
random_seed: int | None = None,
|
|
854
855
|
relative_scores: bool = False,
|
|
855
856
|
include_confidence_band: bool = True,
|
|
856
857
|
confidence_level: float = 0.99,
|
|
@@ -867,9 +868,6 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
867
868
|
# start timer and cap subsample size if it's greater than the number of items in the provided data set
|
|
868
869
|
time_start = time.time()
|
|
869
870
|
if subsample_size > data.num_items:
|
|
870
|
-
logger.info(
|
|
871
|
-
f"Subsample_size {subsample_size} is larger than the number of items in the data and will be ignored"
|
|
872
|
-
)
|
|
873
871
|
subsample_size = data.num_items
|
|
874
872
|
|
|
875
873
|
# set default number of iterations and cap iterations if the number of items in the data is smaller
|
|
@@ -949,7 +947,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
949
947
|
|
|
950
948
|
return importance_df
|
|
951
949
|
|
|
952
|
-
def _model_uses_feature(self, model:
|
|
950
|
+
def _model_uses_feature(self, model: str | TimeSeriesModelBase, feature: str) -> bool:
|
|
953
951
|
"""Check if the given model uses the given feature."""
|
|
954
952
|
models_with_ancestors = set(self.get_minimum_model_set(model))
|
|
955
953
|
|
|
@@ -962,6 +960,72 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
962
960
|
|
|
963
961
|
return False
|
|
964
962
|
|
|
963
|
+
def backtest_predictions(
|
|
964
|
+
self,
|
|
965
|
+
data: TimeSeriesDataFrame | None,
|
|
966
|
+
model_names: list[str],
|
|
967
|
+
num_val_windows: int | None = None,
|
|
968
|
+
val_step_size: int | None = None,
|
|
969
|
+
use_cache: bool = True,
|
|
970
|
+
) -> dict[str, list[TimeSeriesDataFrame]]:
|
|
971
|
+
if data is None:
|
|
972
|
+
assert num_val_windows is None, "num_val_windows must be None when data is None"
|
|
973
|
+
assert val_step_size is None, "val_step_size must be None when data is None"
|
|
974
|
+
return {model_name: self._get_model_oof_predictions(model_name) for model_name in model_names}
|
|
975
|
+
|
|
976
|
+
if val_step_size is None:
|
|
977
|
+
val_step_size = self.prediction_length
|
|
978
|
+
if num_val_windows is None:
|
|
979
|
+
num_val_windows = 1
|
|
980
|
+
|
|
981
|
+
splitter = ExpandingWindowSplitter(
|
|
982
|
+
prediction_length=self.prediction_length,
|
|
983
|
+
num_val_windows=num_val_windows,
|
|
984
|
+
val_step_size=val_step_size,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
result: dict[str, list[TimeSeriesDataFrame]] = {model_name: [] for model_name in model_names}
|
|
988
|
+
for past_data, full_data in splitter.split(data):
|
|
989
|
+
known_covariates = full_data.slice_by_timestep(-self.prediction_length, None)[
|
|
990
|
+
self.covariate_metadata.known_covariates
|
|
991
|
+
]
|
|
992
|
+
pred_dict, _ = self.get_model_pred_dict(
|
|
993
|
+
model_names=model_names,
|
|
994
|
+
data=past_data,
|
|
995
|
+
known_covariates=known_covariates,
|
|
996
|
+
use_cache=use_cache,
|
|
997
|
+
)
|
|
998
|
+
for model_name in model_names:
|
|
999
|
+
result[model_name].append(pred_dict[model_name]) # type: ignore
|
|
1000
|
+
|
|
1001
|
+
return result
|
|
1002
|
+
|
|
1003
|
+
def backtest_targets(
|
|
1004
|
+
self,
|
|
1005
|
+
data: TimeSeriesDataFrame | None,
|
|
1006
|
+
num_val_windows: int | None = None,
|
|
1007
|
+
val_step_size: int | None = None,
|
|
1008
|
+
) -> list[TimeSeriesDataFrame]:
|
|
1009
|
+
if data is None:
|
|
1010
|
+
assert num_val_windows is None, "num_val_windows must be None when data is None"
|
|
1011
|
+
assert val_step_size is None, "val_step_size must be None when data is None"
|
|
1012
|
+
train_data = self.load_train_data()
|
|
1013
|
+
val_data = self.load_val_data()
|
|
1014
|
+
return self._get_validation_windows(train_data=train_data, val_data=val_data)
|
|
1015
|
+
|
|
1016
|
+
if val_step_size is None:
|
|
1017
|
+
val_step_size = self.prediction_length
|
|
1018
|
+
if num_val_windows is None:
|
|
1019
|
+
num_val_windows = 1
|
|
1020
|
+
|
|
1021
|
+
splitter = ExpandingWindowSplitter(
|
|
1022
|
+
prediction_length=self.prediction_length,
|
|
1023
|
+
num_val_windows=num_val_windows,
|
|
1024
|
+
val_step_size=val_step_size,
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
return [val_fold for _, val_fold in splitter.split(data)]
|
|
1028
|
+
|
|
965
1029
|
def _add_ci_to_feature_importance(
|
|
966
1030
|
self, importance_df: pd.DataFrame, confidence_level: float = 0.99
|
|
967
1031
|
) -> pd.DataFrame:
|
|
@@ -991,10 +1055,10 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
991
1055
|
|
|
992
1056
|
def _predict_model(
|
|
993
1057
|
self,
|
|
994
|
-
model:
|
|
1058
|
+
model: str | TimeSeriesModelBase,
|
|
995
1059
|
data: TimeSeriesDataFrame,
|
|
996
|
-
model_pred_dict: dict[str,
|
|
997
|
-
known_covariates:
|
|
1060
|
+
model_pred_dict: dict[str, TimeSeriesDataFrame | None],
|
|
1061
|
+
known_covariates: TimeSeriesDataFrame | None = None,
|
|
998
1062
|
) -> TimeSeriesDataFrame:
|
|
999
1063
|
"""Generate predictions using the given model.
|
|
1000
1064
|
|
|
@@ -1007,10 +1071,10 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1007
1071
|
|
|
1008
1072
|
def _get_inputs_to_model(
|
|
1009
1073
|
self,
|
|
1010
|
-
model:
|
|
1074
|
+
model: str | TimeSeriesModelBase,
|
|
1011
1075
|
data: TimeSeriesDataFrame,
|
|
1012
|
-
model_pred_dict: dict[str,
|
|
1013
|
-
) ->
|
|
1076
|
+
model_pred_dict: dict[str, TimeSeriesDataFrame | None],
|
|
1077
|
+
) -> TimeSeriesDataFrame | dict[str, TimeSeriesDataFrame | None]:
|
|
1014
1078
|
"""Get the first argument that should be passed to model.predict.
|
|
1015
1079
|
|
|
1016
1080
|
This method assumes that model_pred_dict contains the predictions of all base models, if model is an ensemble.
|
|
@@ -1028,11 +1092,11 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1028
1092
|
self,
|
|
1029
1093
|
model_names: list[str],
|
|
1030
1094
|
data: TimeSeriesDataFrame,
|
|
1031
|
-
known_covariates:
|
|
1095
|
+
known_covariates: TimeSeriesDataFrame | None = None,
|
|
1032
1096
|
raise_exception_if_failed: bool = True,
|
|
1033
1097
|
use_cache: bool = True,
|
|
1034
|
-
random_seed:
|
|
1035
|
-
) -> tuple[dict[str,
|
|
1098
|
+
random_seed: int | None = None,
|
|
1099
|
+
) -> tuple[dict[str, TimeSeriesDataFrame | None], dict[str, float]]:
|
|
1036
1100
|
"""Return a dictionary with predictions of all models for the given dataset.
|
|
1037
1101
|
|
|
1038
1102
|
Parameters
|
|
@@ -1064,8 +1128,8 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1064
1128
|
for model_name in model_names:
|
|
1065
1129
|
model_set.update(self.get_minimum_model_set(model_name))
|
|
1066
1130
|
if len(model_set) > 1:
|
|
1067
|
-
|
|
1068
|
-
model_set = sorted(model_set, key=
|
|
1131
|
+
model_to_layer = self._get_model_layers()
|
|
1132
|
+
model_set = sorted(model_set, key=model_to_layer.get) # type: ignore
|
|
1069
1133
|
logger.debug(f"Prediction order: {model_set}")
|
|
1070
1134
|
|
|
1071
1135
|
failed_models = []
|
|
@@ -1115,7 +1179,7 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1115
1179
|
return dict(pred_time_dict_total)
|
|
1116
1180
|
|
|
1117
1181
|
def _merge_refit_full_data(
|
|
1118
|
-
self, train_data: TimeSeriesDataFrame, val_data:
|
|
1182
|
+
self, train_data: TimeSeriesDataFrame, val_data: TimeSeriesDataFrame | None
|
|
1119
1183
|
) -> TimeSeriesDataFrame:
|
|
1120
1184
|
if val_data is None:
|
|
1121
1185
|
return train_data
|
|
@@ -1125,9 +1189,9 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1125
1189
|
|
|
1126
1190
|
def refit_single_full(
|
|
1127
1191
|
self,
|
|
1128
|
-
train_data:
|
|
1129
|
-
val_data:
|
|
1130
|
-
models:
|
|
1192
|
+
train_data: TimeSeriesDataFrame | None = None,
|
|
1193
|
+
val_data: TimeSeriesDataFrame | None = None,
|
|
1194
|
+
models: list[str] | None = None,
|
|
1131
1195
|
) -> list[str]:
|
|
1132
1196
|
train_data = train_data or self.load_train_data()
|
|
1133
1197
|
val_data = val_data or self.load_val_data()
|
|
@@ -1136,12 +1200,12 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1136
1200
|
if models is None:
|
|
1137
1201
|
models = self.get_model_names()
|
|
1138
1202
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1203
|
+
model_to_layer = self._get_model_layers()
|
|
1204
|
+
models_sorted_by_layer = sorted(models, key=model_to_layer.get) # type: ignore
|
|
1141
1205
|
|
|
1142
1206
|
model_refit_map = {}
|
|
1143
1207
|
models_trained_full = []
|
|
1144
|
-
for model in
|
|
1208
|
+
for model in models_sorted_by_layer:
|
|
1145
1209
|
model = self.load_model(model)
|
|
1146
1210
|
model_name = model.name
|
|
1147
1211
|
if model._get_tags()["can_refit_full"]:
|
|
@@ -1206,11 +1270,11 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1206
1270
|
|
|
1207
1271
|
def get_trainable_base_models(
|
|
1208
1272
|
self,
|
|
1209
|
-
hyperparameters:
|
|
1273
|
+
hyperparameters: str | dict[str, Any],
|
|
1210
1274
|
*,
|
|
1211
1275
|
multi_window: bool = False,
|
|
1212
|
-
freq:
|
|
1213
|
-
excluded_model_types:
|
|
1276
|
+
freq: str | None = None,
|
|
1277
|
+
excluded_model_types: list[str] | None = None,
|
|
1214
1278
|
hyperparameter_tune: bool = False,
|
|
1215
1279
|
) -> list[AbstractTimeSeriesModel]:
|
|
1216
1280
|
return TrainableModelSetBuilder(
|
|
@@ -1228,46 +1292,3 @@ class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
|
|
1228
1292
|
excluded_model_types=excluded_model_types,
|
|
1229
1293
|
banned_model_names=self._get_banned_model_names(),
|
|
1230
1294
|
)
|
|
1231
|
-
|
|
1232
|
-
def fit(
|
|
1233
|
-
self,
|
|
1234
|
-
train_data: TimeSeriesDataFrame,
|
|
1235
|
-
hyperparameters: Union[str, dict[Any, dict]],
|
|
1236
|
-
val_data: Optional[TimeSeriesDataFrame] = None,
|
|
1237
|
-
hyperparameter_tune_kwargs: Optional[Union[str, dict]] = None,
|
|
1238
|
-
excluded_model_types: Optional[list[str]] = None,
|
|
1239
|
-
time_limit: Optional[float] = None,
|
|
1240
|
-
random_seed: Optional[int] = None,
|
|
1241
|
-
):
|
|
1242
|
-
"""
|
|
1243
|
-
Fit a set of timeseries models specified by the `hyperparameters`
|
|
1244
|
-
dictionary that maps model names to their specified hyperparameters.
|
|
1245
|
-
|
|
1246
|
-
Parameters
|
|
1247
|
-
----------
|
|
1248
|
-
train_data
|
|
1249
|
-
Training data for fitting time series timeseries models.
|
|
1250
|
-
hyperparameters
|
|
1251
|
-
A dictionary mapping selected model names, model classes or model factory to hyperparameter
|
|
1252
|
-
settings. Model names should be present in `trainer.presets.DEFAULT_MODEL_NAMES`. Optionally,
|
|
1253
|
-
the user may provide one of "default", "light" and "very_light" to specify presets.
|
|
1254
|
-
val_data
|
|
1255
|
-
Optional validation data set to report validation scores on.
|
|
1256
|
-
hyperparameter_tune_kwargs
|
|
1257
|
-
Args for hyperparameter tuning
|
|
1258
|
-
excluded_model_types
|
|
1259
|
-
Names of models that should not be trained, even if listed in `hyperparameters`.
|
|
1260
|
-
time_limit
|
|
1261
|
-
Time limit for training
|
|
1262
|
-
random_seed
|
|
1263
|
-
Random seed that will be set to each model during training
|
|
1264
|
-
"""
|
|
1265
|
-
self._train_multi(
|
|
1266
|
-
train_data,
|
|
1267
|
-
val_data=val_data,
|
|
1268
|
-
hyperparameters=hyperparameters,
|
|
1269
|
-
hyperparameter_tune_kwargs=hyperparameter_tune_kwargs,
|
|
1270
|
-
excluded_model_types=excluded_model_types,
|
|
1271
|
-
time_limit=time_limit,
|
|
1272
|
-
random_seed=random_seed,
|
|
1273
|
-
)
|