autogluon.timeseries 1.2.1b20250421__py3-none-any.whl → 1.2.1b20250423__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.
- autogluon/timeseries/dataset/ts_dataframe.py +1 -1
- autogluon/timeseries/models/abstract/__init__.py +2 -2
- autogluon/timeseries/models/abstract/abstract_timeseries_model.py +178 -122
- autogluon/timeseries/models/chronos/model.py +3 -2
- autogluon/timeseries/models/ensemble/__init__.py +3 -2
- autogluon/timeseries/models/ensemble/abstract.py +139 -0
- autogluon/timeseries/models/ensemble/basic.py +88 -0
- autogluon/timeseries/models/ensemble/{greedy_ensemble.py → greedy.py} +66 -53
- autogluon/timeseries/trainer.py +35 -22
- autogluon/timeseries/version.py +1 -1
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/METADATA +4 -4
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/RECORD +19 -18
- autogluon/timeseries/models/ensemble/abstract_timeseries_ensemble.py +0 -86
- /autogluon.timeseries-1.2.1b20250421-py3.9-nspkg.pth → /autogluon.timeseries-1.2.1b20250423-py3.9-nspkg.pth +0 -0
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/LICENSE +0 -0
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/NOTICE +0 -0
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/WHEEL +0 -0
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/namespace_packages.txt +0 -0
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/top_level.txt +0 -0
- {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/zip-safe +0 -0
@@ -490,7 +490,7 @@ class TimeSeriesDataFrame(pd.DataFrame):
|
|
490
490
|
except ValueError:
|
491
491
|
inferred_freq = None
|
492
492
|
else:
|
493
|
-
inferred_freq = candidate_freq
|
493
|
+
inferred_freq = candidate_freq.freqstr
|
494
494
|
return inferred_freq
|
495
495
|
|
496
496
|
freq_for_each_item = index_df.groupby(ITEMID, sort=False).agg(get_freq)[TIMESTAMP]
|
@@ -1,3 +1,3 @@
|
|
1
|
-
from .abstract_timeseries_model import AbstractTimeSeriesModel
|
1
|
+
from .abstract_timeseries_model import AbstractTimeSeriesModel, TimeSeriesModelBase
|
2
2
|
|
3
|
-
__all__ = ["AbstractTimeSeriesModel"]
|
3
|
+
__all__ = ["AbstractTimeSeriesModel", "TimeSeriesModelBase"]
|
@@ -32,7 +32,8 @@ logger = logging.getLogger(__name__)
|
|
32
32
|
|
33
33
|
|
34
34
|
class TimeSeriesModelBase(ModelBase, ABC):
|
35
|
-
"""Abstract class for all `Model` objects in autogluon.timeseries
|
35
|
+
"""Abstract base class for all `Model` objects in autogluon.timeseries, including both
|
36
|
+
forecasting models and forecast combination/ensemble models.
|
36
37
|
|
37
38
|
Parameters
|
38
39
|
----------
|
@@ -134,11 +135,6 @@ class TimeSeriesModelBase(ModelBase, ABC):
|
|
134
135
|
)
|
135
136
|
self.val_score: Optional[float] = None # Score with eval_metric (Validation data)
|
136
137
|
|
137
|
-
self.target_scaler: Optional[TargetScaler]
|
138
|
-
self.covariate_scaler: Optional[CovariateScaler]
|
139
|
-
self.covariate_regressor: Optional[CovariateRegressor]
|
140
|
-
self._initialize_transforms_and_regressor()
|
141
|
-
|
142
138
|
def __repr__(self) -> str:
|
143
139
|
return self.name
|
144
140
|
|
@@ -249,21 +245,6 @@ class TimeSeriesModelBase(ModelBase, ABC):
|
|
249
245
|
self._oof_predictions = self.load_oof_predictions(self.path)
|
250
246
|
return self._oof_predictions
|
251
247
|
|
252
|
-
def _initialize_transforms_and_regressor(self) -> None:
|
253
|
-
self.target_scaler = get_target_scaler(self.get_hyperparameters().get("target_scaler"), target=self.target)
|
254
|
-
self.covariate_scaler = get_covariate_scaler(
|
255
|
-
self.get_hyperparameters().get("covariate_scaler"),
|
256
|
-
covariate_metadata=self.covariate_metadata,
|
257
|
-
use_static_features=self.supports_static_features,
|
258
|
-
use_known_covariates=self.supports_known_covariates,
|
259
|
-
use_past_covariates=self.supports_past_covariates,
|
260
|
-
)
|
261
|
-
self.covariate_regressor = get_covariate_regressor(
|
262
|
-
self.get_hyperparameters().get("covariate_regressor"),
|
263
|
-
target=self.target,
|
264
|
-
covariate_metadata=self.covariate_metadata,
|
265
|
-
)
|
266
|
-
|
267
248
|
def _get_default_hyperparameters(self) -> dict:
|
268
249
|
return {}
|
269
250
|
|
@@ -303,78 +284,6 @@ class TimeSeriesModelBase(ModelBase, ABC):
|
|
303
284
|
else:
|
304
285
|
raise
|
305
286
|
|
306
|
-
@property
|
307
|
-
def allowed_hyperparameters(self) -> List[str]:
|
308
|
-
"""List of hyperparameters allowed by the model."""
|
309
|
-
return ["target_scaler", "covariate_regressor"]
|
310
|
-
|
311
|
-
def _score_with_predictions(
|
312
|
-
self,
|
313
|
-
data: TimeSeriesDataFrame,
|
314
|
-
predictions: TimeSeriesDataFrame,
|
315
|
-
metric: Optional[str] = None,
|
316
|
-
) -> float:
|
317
|
-
"""Compute the score measuring how well the predictions align with the data."""
|
318
|
-
eval_metric = self.eval_metric if metric is None else check_get_evaluation_metric(metric)
|
319
|
-
return eval_metric.score(
|
320
|
-
data=data,
|
321
|
-
predictions=predictions,
|
322
|
-
prediction_length=self.prediction_length,
|
323
|
-
target=self.target,
|
324
|
-
seasonal_period=self.eval_metric_seasonal_period,
|
325
|
-
)
|
326
|
-
|
327
|
-
def score(self, data: TimeSeriesDataFrame, metric: Optional[str] = None) -> float: # type: ignore
|
328
|
-
"""Return the evaluation scores for given metric and dataset. The last
|
329
|
-
`self.prediction_length` time steps of each time series in the input data set
|
330
|
-
will be held out and used for computing the evaluation score. Time series
|
331
|
-
models always return higher-is-better type scores.
|
332
|
-
|
333
|
-
Parameters
|
334
|
-
----------
|
335
|
-
data: TimeSeriesDataFrame
|
336
|
-
Dataset used for scoring.
|
337
|
-
metric: str
|
338
|
-
String identifier of evaluation metric to use, from one of
|
339
|
-
`autogluon.timeseries.utils.metric_utils.AVAILABLE_METRICS`.
|
340
|
-
|
341
|
-
Other Parameters
|
342
|
-
----------------
|
343
|
-
num_samples: int
|
344
|
-
Number of samples to use for making evaluation predictions if the probabilistic
|
345
|
-
forecasts are generated by forward sampling from the fitted model.
|
346
|
-
|
347
|
-
Returns
|
348
|
-
-------
|
349
|
-
score: float
|
350
|
-
The computed forecast evaluation score on the last `self.prediction_length`
|
351
|
-
time steps of each time series.
|
352
|
-
"""
|
353
|
-
past_data, known_covariates = data.get_model_inputs_for_scoring(
|
354
|
-
prediction_length=self.prediction_length, known_covariates_names=self.covariate_metadata.known_covariates
|
355
|
-
)
|
356
|
-
predictions = self.predict(past_data, known_covariates=known_covariates)
|
357
|
-
return self._score_with_predictions(data=data, predictions=predictions, metric=metric)
|
358
|
-
|
359
|
-
def score_and_cache_oof(
|
360
|
-
self,
|
361
|
-
val_data: TimeSeriesDataFrame,
|
362
|
-
store_val_score: bool = False,
|
363
|
-
store_predict_time: bool = False,
|
364
|
-
**predict_kwargs,
|
365
|
-
) -> None:
|
366
|
-
"""Compute val_score, predict_time and cache out-of-fold (OOF) predictions."""
|
367
|
-
past_data, known_covariates = val_data.get_model_inputs_for_scoring(
|
368
|
-
prediction_length=self.prediction_length, known_covariates_names=self.covariate_metadata.known_covariates
|
369
|
-
)
|
370
|
-
predict_start_time = time.time()
|
371
|
-
oof_predictions = self.predict(past_data, known_covariates=known_covariates, **predict_kwargs)
|
372
|
-
self._oof_predictions = [oof_predictions]
|
373
|
-
if store_predict_time:
|
374
|
-
self.predict_time = time.time() - predict_start_time
|
375
|
-
if store_val_score:
|
376
|
-
self.val_score = self._score_with_predictions(val_data, oof_predictions)
|
377
|
-
|
378
287
|
def _is_gpu_available(self) -> bool:
|
379
288
|
return False
|
380
289
|
|
@@ -391,16 +300,6 @@ class TimeSeriesModelBase(ModelBase, ABC):
|
|
391
300
|
def _get_model_base(self) -> Self:
|
392
301
|
return self
|
393
302
|
|
394
|
-
def preprocess( # type: ignore
|
395
|
-
self,
|
396
|
-
data: TimeSeriesDataFrame,
|
397
|
-
known_covariates: Optional[TimeSeriesDataFrame] = None,
|
398
|
-
is_train: bool = False,
|
399
|
-
**kwargs,
|
400
|
-
) -> Tuple[TimeSeriesDataFrame, Optional[TimeSeriesDataFrame]]:
|
401
|
-
"""Method that implements model-specific preprocessing logic."""
|
402
|
-
return data, known_covariates
|
403
|
-
|
404
303
|
def persist(self) -> Self:
|
405
304
|
"""Ask the model to persist its assets in memory, i.e., to predict with low latency. In practice
|
406
305
|
this is used for pretrained models that have to lazy-load model parameters to device memory at
|
@@ -427,8 +326,112 @@ class TimeSeriesModelBase(ModelBase, ABC):
|
|
427
326
|
"can_use_val_data": False,
|
428
327
|
}
|
429
328
|
|
329
|
+
def get_params(self) -> dict:
|
330
|
+
"""Get the constructor parameters required for cloning this model object"""
|
331
|
+
# We only use the user-provided hyperparameters for cloning. We cannot use the output of get_hyperparameters()
|
332
|
+
# since it may contain search spaces that won't be converted to concrete values during HPO
|
333
|
+
hyperparameters = self._hyperparameters.copy()
|
334
|
+
if self._extra_ag_args:
|
335
|
+
hyperparameters[AG_ARGS_FIT] = self._extra_ag_args.copy()
|
336
|
+
|
337
|
+
return dict(
|
338
|
+
path=self.path_root,
|
339
|
+
name=self.name,
|
340
|
+
eval_metric=self.eval_metric,
|
341
|
+
hyperparameters=hyperparameters,
|
342
|
+
freq=self.freq,
|
343
|
+
prediction_length=self.prediction_length,
|
344
|
+
quantile_levels=self.quantile_levels,
|
345
|
+
covariate_metadata=self.covariate_metadata,
|
346
|
+
target=self.target,
|
347
|
+
)
|
348
|
+
|
349
|
+
def convert_to_refit_full_via_copy(self) -> Self:
|
350
|
+
# save the model as a new model on disk
|
351
|
+
previous_name = self.name
|
352
|
+
self.rename(self.name + REFIT_FULL_SUFFIX)
|
353
|
+
refit_model_path = self.path
|
354
|
+
self.save(path=self.path, verbose=False)
|
355
|
+
|
356
|
+
self.rename(previous_name)
|
357
|
+
|
358
|
+
refit_model = self.load(path=refit_model_path, verbose=False)
|
359
|
+
refit_model.val_score = None
|
360
|
+
refit_model.predict_time = None
|
361
|
+
|
362
|
+
return refit_model
|
363
|
+
|
364
|
+
def convert_to_refit_full_template(self) -> Self:
|
365
|
+
"""After calling this function, returned model should be able to be fit without `val_data`."""
|
366
|
+
params = copy.deepcopy(self.get_params())
|
367
|
+
|
368
|
+
if "hyperparameters" not in params:
|
369
|
+
params["hyperparameters"] = dict()
|
370
|
+
|
371
|
+
if AG_ARGS_FIT not in params["hyperparameters"]:
|
372
|
+
params["hyperparameters"][AG_ARGS_FIT] = dict()
|
373
|
+
|
374
|
+
params["name"] = params["name"] + REFIT_FULL_SUFFIX
|
375
|
+
template = self.__class__(**params)
|
376
|
+
|
377
|
+
return template
|
378
|
+
|
430
379
|
|
431
380
|
class AbstractTimeSeriesModel(TimeSeriesModelBase, TimeSeriesTunable, ABC):
|
381
|
+
"""Abstract base class for all time series models that take historical data as input and
|
382
|
+
make predictions for the forecast horizon.
|
383
|
+
"""
|
384
|
+
|
385
|
+
def __init__(
|
386
|
+
self,
|
387
|
+
path: Optional[str] = None,
|
388
|
+
name: Optional[str] = None,
|
389
|
+
hyperparameters: Optional[Dict[str, Any]] = None,
|
390
|
+
freq: Optional[str] = None,
|
391
|
+
prediction_length: int = 1,
|
392
|
+
covariate_metadata: Optional[CovariateMetadata] = None,
|
393
|
+
target: str = "target",
|
394
|
+
quantile_levels: Sequence[float] = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
|
395
|
+
eval_metric: Union[str, TimeSeriesScorer, None] = None,
|
396
|
+
eval_metric_seasonal_period: Optional[int] = None,
|
397
|
+
):
|
398
|
+
super().__init__(
|
399
|
+
path=path,
|
400
|
+
name=name,
|
401
|
+
hyperparameters=hyperparameters,
|
402
|
+
freq=freq,
|
403
|
+
prediction_length=prediction_length,
|
404
|
+
covariate_metadata=covariate_metadata,
|
405
|
+
target=target,
|
406
|
+
quantile_levels=quantile_levels,
|
407
|
+
eval_metric=eval_metric,
|
408
|
+
eval_metric_seasonal_period=eval_metric_seasonal_period,
|
409
|
+
)
|
410
|
+
self.target_scaler: Optional[TargetScaler]
|
411
|
+
self.covariate_scaler: Optional[CovariateScaler]
|
412
|
+
self.covariate_regressor: Optional[CovariateRegressor]
|
413
|
+
self._initialize_transforms_and_regressor()
|
414
|
+
|
415
|
+
def _initialize_transforms_and_regressor(self) -> None:
|
416
|
+
self.target_scaler = get_target_scaler(self.get_hyperparameters().get("target_scaler"), target=self.target)
|
417
|
+
self.covariate_scaler = get_covariate_scaler(
|
418
|
+
self.get_hyperparameters().get("covariate_scaler"),
|
419
|
+
covariate_metadata=self.covariate_metadata,
|
420
|
+
use_static_features=self.supports_static_features,
|
421
|
+
use_known_covariates=self.supports_known_covariates,
|
422
|
+
use_past_covariates=self.supports_past_covariates,
|
423
|
+
)
|
424
|
+
self.covariate_regressor = get_covariate_regressor(
|
425
|
+
self.get_hyperparameters().get("covariate_regressor"),
|
426
|
+
target=self.target,
|
427
|
+
covariate_metadata=self.covariate_metadata,
|
428
|
+
)
|
429
|
+
|
430
|
+
@property
|
431
|
+
def allowed_hyperparameters(self) -> List[str]:
|
432
|
+
"""List of hyperparameters allowed by the model."""
|
433
|
+
return ["target_scaler", "covariate_regressor"]
|
434
|
+
|
432
435
|
def fit(
|
433
436
|
self,
|
434
437
|
train_data: TimeSeriesDataFrame,
|
@@ -549,7 +552,7 @@ class AbstractTimeSeriesModel(TimeSeriesModelBase, TimeSeriesTunable, ABC):
|
|
549
552
|
"as hyperparameters when initializing or use `hyperparameter_tune` instead."
|
550
553
|
)
|
551
554
|
|
552
|
-
def predict(
|
555
|
+
def predict(
|
553
556
|
self,
|
554
557
|
data: TimeSeriesDataFrame,
|
555
558
|
known_covariates: Optional[TimeSeriesDataFrame] = None,
|
@@ -647,24 +650,6 @@ class AbstractTimeSeriesModel(TimeSeriesModelBase, TimeSeriesTunable, ABC):
|
|
647
650
|
|
648
651
|
return template
|
649
652
|
|
650
|
-
def get_params(self) -> dict:
|
651
|
-
"""Get the constructor parameters required for cloning this model object"""
|
652
|
-
hyperparameters = self.get_hyperparameters().copy()
|
653
|
-
if self._extra_ag_args:
|
654
|
-
hyperparameters[AG_ARGS_FIT] = self._extra_ag_args.copy()
|
655
|
-
|
656
|
-
return dict(
|
657
|
-
path=self.path_root,
|
658
|
-
name=self.name,
|
659
|
-
eval_metric=self.eval_metric,
|
660
|
-
hyperparameters=hyperparameters,
|
661
|
-
freq=self.freq,
|
662
|
-
prediction_length=self.prediction_length,
|
663
|
-
quantile_levels=self.quantile_levels,
|
664
|
-
covariate_metadata=self.covariate_metadata,
|
665
|
-
target=self.target,
|
666
|
-
)
|
667
|
-
|
668
653
|
def get_forecast_horizon_index(self, data: TimeSeriesDataFrame) -> pd.MultiIndex:
|
669
654
|
"""For each item in the dataframe, get timestamps for the next `prediction_length` time steps into the future."""
|
670
655
|
return pd.MultiIndex.from_frame(
|
@@ -709,3 +694,74 @@ class AbstractTimeSeriesModel(TimeSeriesModelBase, TimeSeriesTunable, ABC):
|
|
709
694
|
"""
|
710
695
|
params = self._hyperparameters.copy()
|
711
696
|
return params
|
697
|
+
|
698
|
+
def _score_with_predictions(
|
699
|
+
self,
|
700
|
+
data: TimeSeriesDataFrame,
|
701
|
+
predictions: TimeSeriesDataFrame,
|
702
|
+
metric: Optional[str] = None,
|
703
|
+
) -> float:
|
704
|
+
"""Compute the score measuring how well the predictions align with the data."""
|
705
|
+
eval_metric = self.eval_metric if metric is None else check_get_evaluation_metric(metric)
|
706
|
+
return eval_metric.score(
|
707
|
+
data=data,
|
708
|
+
predictions=predictions,
|
709
|
+
prediction_length=self.prediction_length,
|
710
|
+
target=self.target,
|
711
|
+
seasonal_period=self.eval_metric_seasonal_period,
|
712
|
+
)
|
713
|
+
|
714
|
+
def score(self, data: TimeSeriesDataFrame, metric: Optional[str] = None) -> float:
|
715
|
+
"""Return the evaluation scores for given metric and dataset. The last
|
716
|
+
`self.prediction_length` time steps of each time series in the input data set
|
717
|
+
will be held out and used for computing the evaluation score. Time series
|
718
|
+
models always return higher-is-better type scores.
|
719
|
+
|
720
|
+
Parameters
|
721
|
+
----------
|
722
|
+
data: TimeSeriesDataFrame
|
723
|
+
Dataset used for scoring.
|
724
|
+
metric: str
|
725
|
+
String identifier of evaluation metric to use, from one of
|
726
|
+
`autogluon.timeseries.utils.metric_utils.AVAILABLE_METRICS`.
|
727
|
+
|
728
|
+
Returns
|
729
|
+
-------
|
730
|
+
score: float
|
731
|
+
The computed forecast evaluation score on the last `self.prediction_length`
|
732
|
+
time steps of each time series.
|
733
|
+
"""
|
734
|
+
past_data, known_covariates = data.get_model_inputs_for_scoring(
|
735
|
+
prediction_length=self.prediction_length, known_covariates_names=self.covariate_metadata.known_covariates
|
736
|
+
)
|
737
|
+
predictions = self.predict(past_data, known_covariates=known_covariates)
|
738
|
+
return self._score_with_predictions(data=data, predictions=predictions, metric=metric)
|
739
|
+
|
740
|
+
def score_and_cache_oof(
|
741
|
+
self,
|
742
|
+
val_data: TimeSeriesDataFrame,
|
743
|
+
store_val_score: bool = False,
|
744
|
+
store_predict_time: bool = False,
|
745
|
+
**predict_kwargs,
|
746
|
+
) -> None:
|
747
|
+
"""Compute val_score, predict_time and cache out-of-fold (OOF) predictions."""
|
748
|
+
past_data, known_covariates = val_data.get_model_inputs_for_scoring(
|
749
|
+
prediction_length=self.prediction_length, known_covariates_names=self.covariate_metadata.known_covariates
|
750
|
+
)
|
751
|
+
predict_start_time = time.time()
|
752
|
+
oof_predictions = self.predict(past_data, known_covariates=known_covariates, **predict_kwargs)
|
753
|
+
self._oof_predictions = [oof_predictions]
|
754
|
+
if store_predict_time:
|
755
|
+
self.predict_time = time.time() - predict_start_time
|
756
|
+
if store_val_score:
|
757
|
+
self.val_score = self._score_with_predictions(val_data, oof_predictions)
|
758
|
+
|
759
|
+
def preprocess(
|
760
|
+
self,
|
761
|
+
data: TimeSeriesDataFrame,
|
762
|
+
known_covariates: Optional[TimeSeriesDataFrame] = None,
|
763
|
+
is_train: bool = False,
|
764
|
+
**kwargs,
|
765
|
+
) -> Tuple[TimeSeriesDataFrame, Optional[TimeSeriesDataFrame]]:
|
766
|
+
"""Method that implements model-specific preprocessing logic."""
|
767
|
+
return data, known_covariates
|
@@ -196,8 +196,9 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
196
196
|
name = name if name is not None else "Chronos"
|
197
197
|
if not isinstance(model_path_input, Space):
|
198
198
|
# we truncate the name to avoid long path errors on Windows
|
199
|
-
|
200
|
-
|
199
|
+
model_path_suffix = "[" + str(model_path_input).replace("/", "__").replace(os.path.sep, "__")[-50:] + "]"
|
200
|
+
if model_path_suffix not in name:
|
201
|
+
name += model_path_suffix
|
201
202
|
|
202
203
|
super().__init__(
|
203
204
|
path=path,
|
@@ -1,2 +1,3 @@
|
|
1
|
-
from .
|
2
|
-
from .
|
1
|
+
from .abstract import AbstractTimeSeriesEnsembleModel
|
2
|
+
from .greedy import GreedyEnsemble
|
3
|
+
from .basic import SimpleAverageEnsemble, PerformanceWeightedEnsemble
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import functools
|
2
|
+
import logging
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from typing import Dict, List, Optional
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
from typing_extensions import final
|
8
|
+
|
9
|
+
from autogluon.core.utils.exceptions import TimeLimitExceeded
|
10
|
+
from autogluon.timeseries.dataset import TimeSeriesDataFrame
|
11
|
+
from autogluon.timeseries.models.abstract import TimeSeriesModelBase
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class AbstractTimeSeriesEnsembleModel(TimeSeriesModelBase, ABC):
|
17
|
+
"""Abstract class for time series ensemble models."""
|
18
|
+
|
19
|
+
@property
|
20
|
+
@abstractmethod
|
21
|
+
def model_names(self) -> List[str]:
|
22
|
+
"""Names of base models included in the ensemble."""
|
23
|
+
pass
|
24
|
+
|
25
|
+
@final
|
26
|
+
def fit(
|
27
|
+
self,
|
28
|
+
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]],
|
29
|
+
data_per_window: List[TimeSeriesDataFrame],
|
30
|
+
model_scores: Optional[Dict[str, float]] = None,
|
31
|
+
time_limit: Optional[float] = None,
|
32
|
+
):
|
33
|
+
"""Fit ensemble model given predictions of candidate base models and the true data.
|
34
|
+
|
35
|
+
Parameters
|
36
|
+
----------
|
37
|
+
predictions_per_window : Dict[str, List[TimeSeriesDataFrame]]
|
38
|
+
Dictionary that maps the names of component models to their respective predictions for each validation
|
39
|
+
window.
|
40
|
+
data_per_window : List[TimeSeriesDataFrame]
|
41
|
+
Observed ground truth data used to train the ensemble for each validation window. Each entry in the list
|
42
|
+
includes both the forecast horizon (for which the predictions are given in ``predictions``), as well as the
|
43
|
+
"history".
|
44
|
+
model_scores : Optional[Dict[str, float]]
|
45
|
+
Scores (higher is better) for the models that will constitute the ensemble.
|
46
|
+
time_limit : Optional[float]
|
47
|
+
Maximum allowed time for training in seconds.
|
48
|
+
"""
|
49
|
+
if time_limit is not None and time_limit <= 0:
|
50
|
+
logger.warning(
|
51
|
+
f"\tWarning: Model has no time left to train, skipping model... (Time Left = {round(time_limit, 1)}s)"
|
52
|
+
)
|
53
|
+
raise TimeLimitExceeded
|
54
|
+
if isinstance(data_per_window, TimeSeriesDataFrame):
|
55
|
+
raise ValueError("When fitting ensemble, `data` should contain ground truth for each validation window")
|
56
|
+
num_val_windows = len(data_per_window)
|
57
|
+
for model, preds in predictions_per_window.items():
|
58
|
+
if len(preds) != num_val_windows:
|
59
|
+
raise ValueError(f"For model {model} predictions are unavailable for some validation windows")
|
60
|
+
self._fit(
|
61
|
+
predictions_per_window=predictions_per_window,
|
62
|
+
data_per_window=data_per_window,
|
63
|
+
model_scores=model_scores,
|
64
|
+
time_limit=time_limit,
|
65
|
+
)
|
66
|
+
return self
|
67
|
+
|
68
|
+
def _fit(
|
69
|
+
self,
|
70
|
+
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]],
|
71
|
+
data_per_window: List[TimeSeriesDataFrame],
|
72
|
+
model_scores: Optional[Dict[str, float]] = None,
|
73
|
+
time_limit: Optional[float] = None,
|
74
|
+
):
|
75
|
+
"""Private method for `fit`. See `fit` for documentation of arguments. Apart from the model
|
76
|
+
training logic, `fit` additionally implements other logic such as keeping track of the time limit.
|
77
|
+
"""
|
78
|
+
raise NotImplementedError
|
79
|
+
|
80
|
+
@final
|
81
|
+
def predict(self, data: Dict[str, TimeSeriesDataFrame], **kwargs) -> TimeSeriesDataFrame:
|
82
|
+
if not set(self.model_names).issubset(set(data.keys())):
|
83
|
+
raise ValueError(
|
84
|
+
f"Set of models given for prediction in {self.name} differ from those provided during initialization."
|
85
|
+
)
|
86
|
+
for model_name, model_pred in data.items():
|
87
|
+
if model_pred is None:
|
88
|
+
raise RuntimeError(f"{self.name} cannot predict because base model {model_name} failed.")
|
89
|
+
|
90
|
+
# Make sure that all predictions have same shape
|
91
|
+
assert len(set(pred.shape for pred in data.values())) == 1
|
92
|
+
|
93
|
+
return self._predict(data=data, **kwargs)
|
94
|
+
|
95
|
+
@abstractmethod
|
96
|
+
def _predict(self, data: Dict[str, TimeSeriesDataFrame], **kwargs) -> TimeSeriesDataFrame:
|
97
|
+
pass
|
98
|
+
|
99
|
+
@abstractmethod
|
100
|
+
def remap_base_models(self, model_refit_map: Dict[str, str]) -> None:
|
101
|
+
"""Update names of the base models based on the mapping in model_refit_map.
|
102
|
+
|
103
|
+
This method should be called after performing refit_full to point to the refitted base models, if necessary.
|
104
|
+
"""
|
105
|
+
pass
|
106
|
+
|
107
|
+
|
108
|
+
class AbstractWeightedTimeSeriesEnsembleModel(AbstractTimeSeriesEnsembleModel, ABC):
|
109
|
+
"""Abstract class for weighted ensembles which assign one (global) weight per model."""
|
110
|
+
|
111
|
+
def __init__(self, name: Optional[str] = None, **kwargs):
|
112
|
+
if name is None:
|
113
|
+
name = "WeightedEnsemble"
|
114
|
+
super().__init__(name=name, **kwargs)
|
115
|
+
self.model_to_weight: Dict[str, float] = {}
|
116
|
+
|
117
|
+
@property
|
118
|
+
def model_names(self) -> List[str]:
|
119
|
+
return list(self.model_to_weight.keys())
|
120
|
+
|
121
|
+
@property
|
122
|
+
def model_weights(self) -> np.ndarray:
|
123
|
+
return np.array(list(self.model_to_weight.values()), dtype=np.float64)
|
124
|
+
|
125
|
+
def _predict(self, data: Dict[str, TimeSeriesDataFrame], **kwargs) -> TimeSeriesDataFrame:
|
126
|
+
weighted_predictions = [data[model_name] * weight for model_name, weight in self.model_to_weight.items()]
|
127
|
+
return functools.reduce(lambda x, y: x + y, weighted_predictions)
|
128
|
+
|
129
|
+
def get_info(self) -> dict:
|
130
|
+
info = super().get_info()
|
131
|
+
info["model_weights"] = self.model_to_weight.copy()
|
132
|
+
return info
|
133
|
+
|
134
|
+
def remap_base_models(self, model_refit_map: Dict[str, str]) -> None:
|
135
|
+
updated_weights = {}
|
136
|
+
for model, weight in self.model_to_weight.items():
|
137
|
+
model_full_name = model_refit_map.get(model, model)
|
138
|
+
updated_weights[model_full_name] = weight
|
139
|
+
self.model_to_weight = updated_weights
|
@@ -0,0 +1,88 @@
|
|
1
|
+
from typing import Dict, List, Optional
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
from autogluon.timeseries.dataset import TimeSeriesDataFrame
|
6
|
+
|
7
|
+
from .abstract import AbstractWeightedTimeSeriesEnsembleModel
|
8
|
+
|
9
|
+
|
10
|
+
class SimpleAverageEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
|
11
|
+
"""Constructs a weighted ensemble using a simple average of the constituent models' predictions."""
|
12
|
+
|
13
|
+
def __init__(self, name: Optional[str] = None, **kwargs):
|
14
|
+
if name is None:
|
15
|
+
name = "SimpleAverageEnsemble"
|
16
|
+
super().__init__(name=name, **kwargs)
|
17
|
+
|
18
|
+
def _fit(
|
19
|
+
self,
|
20
|
+
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]],
|
21
|
+
data_per_window: List[TimeSeriesDataFrame],
|
22
|
+
model_scores: Optional[Dict[str, float]] = None,
|
23
|
+
time_limit: Optional[float] = None,
|
24
|
+
):
|
25
|
+
self.model_to_weight = {}
|
26
|
+
num_models = len(predictions_per_window)
|
27
|
+
for model_name in predictions_per_window.keys():
|
28
|
+
self.model_to_weight[model_name] = 1.0 / num_models
|
29
|
+
|
30
|
+
|
31
|
+
class PerformanceWeightedEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
|
32
|
+
"""Constructs a weighted ensemble, where the weights are assigned in proportion to the
|
33
|
+
(inverse) validation scores.
|
34
|
+
|
35
|
+
Other Parameters
|
36
|
+
----------------
|
37
|
+
weight_scheme: Literal["sq", "inv", "loginv"], default = "loginv"
|
38
|
+
Method used to compute the weights as a function of the validation scores.
|
39
|
+
- "sqrt" computes weights in proportion to `sqrt(1 / S)`. This is the default.
|
40
|
+
- "inv" computes weights in proportion to `(1 / S)`.
|
41
|
+
- "sq" computes the weights in proportion to `(1 / S)^2` as outlined in [PC2020]_.
|
42
|
+
|
43
|
+
References
|
44
|
+
----------
|
45
|
+
.. [PC2020] Pawlikowski, Maciej, and Agata Chorowska.
|
46
|
+
"Weighted ensemble of statistical models." International Journal of Forecasting
|
47
|
+
36.1 (2020): 93-97.
|
48
|
+
"""
|
49
|
+
|
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
|
+
def _get_default_hyperparameters(self) -> Dict:
|
56
|
+
return {"weight_scheme": "sqrt"}
|
57
|
+
|
58
|
+
def _fit(
|
59
|
+
self,
|
60
|
+
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]],
|
61
|
+
data_per_window: List[TimeSeriesDataFrame],
|
62
|
+
model_scores: Optional[Dict[str, float]] = None,
|
63
|
+
time_limit: Optional[float] = None,
|
64
|
+
):
|
65
|
+
assert model_scores is not None
|
66
|
+
|
67
|
+
weight_scheme = self.get_hyperparameters()["weight_scheme"]
|
68
|
+
|
69
|
+
# drop NaNs
|
70
|
+
model_scores = {k: v for k, v in model_scores.items() if np.isfinite(v)}
|
71
|
+
assert len(model_scores) > 0, (
|
72
|
+
"All models have NaN scores. At least one model must score successfully to fit an ensemble"
|
73
|
+
)
|
74
|
+
assert all(s <= 0 for s in model_scores.values()), (
|
75
|
+
"All model scores must be negative, in higher-is-better format."
|
76
|
+
)
|
77
|
+
|
78
|
+
score_transform = {
|
79
|
+
"sq": lambda x: np.square(np.reciprocal(x)),
|
80
|
+
"inv": lambda x: np.reciprocal(x),
|
81
|
+
"sqrt": lambda x: np.sqrt(np.reciprocal(x)),
|
82
|
+
}[weight_scheme]
|
83
|
+
|
84
|
+
self.model_to_weight = {
|
85
|
+
model_name: score_transform(-model_scores[model_name] + 1e-5) for model_name in model_scores.keys()
|
86
|
+
}
|
87
|
+
total_weight = sum(self.model_to_weight.values())
|
88
|
+
self.model_to_weight = {k: v / total_weight for k, v in self.model_to_weight.items()}
|
@@ -9,9 +9,10 @@ import autogluon.core as ag
|
|
9
9
|
from autogluon.core.models.greedy_ensemble.ensemble_selection import EnsembleSelection
|
10
10
|
from autogluon.timeseries import TimeSeriesDataFrame
|
11
11
|
from autogluon.timeseries.metrics import TimeSeriesScorer
|
12
|
-
from autogluon.timeseries.models.ensemble import AbstractTimeSeriesEnsembleModel
|
13
12
|
from autogluon.timeseries.utils.datetime import get_seasonality
|
14
13
|
|
14
|
+
from .abstract import AbstractWeightedTimeSeriesEnsembleModel
|
15
|
+
|
15
16
|
logger = logging.getLogger(__name__)
|
16
17
|
|
17
18
|
|
@@ -24,15 +25,15 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
|
|
24
25
|
sorted_initialization: bool = False,
|
25
26
|
bagging: bool = False,
|
26
27
|
tie_breaker: str = "random",
|
27
|
-
random_state: np.random.RandomState = None,
|
28
|
+
random_state: Optional[np.random.RandomState] = None,
|
28
29
|
prediction_length: int = 1,
|
29
30
|
target: str = "target",
|
30
|
-
eval_metric_seasonal_period:
|
31
|
+
eval_metric_seasonal_period: int = 1,
|
31
32
|
**kwargs,
|
32
33
|
):
|
33
34
|
super().__init__(
|
34
35
|
ensemble_size=ensemble_size,
|
35
|
-
metric=metric,
|
36
|
+
metric=metric, # type: ignore
|
36
37
|
problem_type=problem_type,
|
37
38
|
sorted_initialization=sorted_initialization,
|
38
39
|
bagging=bagging,
|
@@ -43,13 +44,33 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
|
|
43
44
|
self.prediction_length = prediction_length
|
44
45
|
self.target = target
|
45
46
|
self.eval_metric_seasonal_period = eval_metric_seasonal_period
|
47
|
+
self.metric: TimeSeriesScorer
|
46
48
|
|
47
|
-
|
49
|
+
self.dummy_pred_per_window = []
|
50
|
+
self.scorer_per_window = []
|
51
|
+
|
52
|
+
self.dummy_pred_per_window: Optional[List[TimeSeriesDataFrame]]
|
53
|
+
self.scorer_per_window: Optional[List[TimeSeriesScorer]]
|
54
|
+
self.data_future_per_window: Optional[List[TimeSeriesDataFrame]]
|
55
|
+
|
56
|
+
def fit( # type: ignore
|
48
57
|
self,
|
49
|
-
predictions: List[List[TimeSeriesDataFrame]],
|
58
|
+
predictions: List[List[TimeSeriesDataFrame]],
|
50
59
|
labels: List[TimeSeriesDataFrame],
|
51
|
-
time_limit: Optional[
|
52
|
-
|
60
|
+
time_limit: Optional[float] = None,
|
61
|
+
):
|
62
|
+
return super().fit(
|
63
|
+
predictions=predictions, # type: ignore
|
64
|
+
labels=labels, # type: ignore
|
65
|
+
time_limit=time_limit,
|
66
|
+
)
|
67
|
+
|
68
|
+
def _fit( # type: ignore
|
69
|
+
self,
|
70
|
+
predictions: List[List[TimeSeriesDataFrame]],
|
71
|
+
labels: List[TimeSeriesDataFrame],
|
72
|
+
time_limit: Optional[float] = None,
|
73
|
+
sample_weight: Optional[List[float]] = None,
|
53
74
|
):
|
54
75
|
# Stack predictions for each model into a 3d tensor of shape [num_val_windows, num_rows, num_cols]
|
55
76
|
stacked_predictions = [np.stack(preds) for preds in predictions]
|
@@ -75,16 +96,27 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
|
|
75
96
|
|
76
97
|
super()._fit(
|
77
98
|
predictions=stacked_predictions,
|
78
|
-
labels=data_future,
|
99
|
+
labels=data_future, # type: ignore
|
79
100
|
time_limit=time_limit,
|
80
101
|
)
|
81
102
|
self.dummy_pred_per_window = None
|
82
103
|
self.evaluator_per_window = None
|
83
104
|
self.data_future_per_window = None
|
84
105
|
|
85
|
-
def _calculate_regret(
|
106
|
+
def _calculate_regret( # type: ignore
|
107
|
+
self,
|
108
|
+
y_true,
|
109
|
+
y_pred_proba,
|
110
|
+
metric: TimeSeriesScorer,
|
111
|
+
sample_weight=None,
|
112
|
+
):
|
86
113
|
# Compute average score across all validation windows
|
87
114
|
total_score = 0.0
|
115
|
+
|
116
|
+
assert self.data_future_per_window is not None
|
117
|
+
assert self.dummy_pred_per_window is not None
|
118
|
+
assert self.scorer_per_window is not None
|
119
|
+
|
88
120
|
for window_idx, data_future in enumerate(self.data_future_per_window):
|
89
121
|
dummy_pred = self.dummy_pred_per_window[window_idx]
|
90
122
|
dummy_pred[list(dummy_pred.columns)] = y_pred_proba[window_idx]
|
@@ -98,27 +130,42 @@ class TimeSeriesEnsembleSelection(EnsembleSelection):
|
|
98
130
|
return -avg_score
|
99
131
|
|
100
132
|
|
101
|
-
class
|
102
|
-
"""Constructs a weighted ensemble using the greedy Ensemble Selection algorithm
|
133
|
+
class GreedyEnsemble(AbstractWeightedTimeSeriesEnsembleModel):
|
134
|
+
"""Constructs a weighted ensemble using the greedy Ensemble Selection algorithm by
|
135
|
+
Caruana et al. [Car2004]
|
136
|
+
|
137
|
+
Other Parameters
|
138
|
+
----------------
|
139
|
+
ensemble_size: int, default = 100
|
140
|
+
Number of models (with replacement) to include in the ensemble.
|
141
|
+
|
142
|
+
References
|
143
|
+
----------
|
144
|
+
.. [Car2024] Caruana, Rich, et al. "Ensemble selection from libraries of models."
|
145
|
+
Proceedings of the twenty-first international conference on Machine learning. 2004.
|
146
|
+
"""
|
103
147
|
|
104
|
-
def __init__(self, name: Optional[str] = None,
|
148
|
+
def __init__(self, name: Optional[str] = None, **kwargs):
|
105
149
|
if name is None:
|
150
|
+
# FIXME: the name here is kept for backward compatibility. it will be called
|
151
|
+
# GreedyEnsemble in v1.4 once ensemble choices are exposed
|
106
152
|
name = "WeightedEnsemble"
|
107
153
|
super().__init__(name=name, **kwargs)
|
108
|
-
self.ensemble_size = ensemble_size
|
109
|
-
self.model_to_weight: Dict[str, float] = {}
|
110
154
|
|
111
|
-
def
|
155
|
+
def _get_default_hyperparameters(self) -> Dict:
|
156
|
+
return {"ensemble_size": 100}
|
157
|
+
|
158
|
+
def _fit(
|
112
159
|
self,
|
113
160
|
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]],
|
114
161
|
data_per_window: List[TimeSeriesDataFrame],
|
115
|
-
|
116
|
-
|
162
|
+
model_scores: Optional[Dict[str, float]] = None,
|
163
|
+
time_limit: Optional[float] = None,
|
117
164
|
):
|
118
165
|
if self.eval_metric_seasonal_period is None:
|
119
166
|
self.eval_metric_seasonal_period = get_seasonality(self.freq)
|
120
167
|
ensemble_selection = TimeSeriesEnsembleSelection(
|
121
|
-
ensemble_size=self.ensemble_size,
|
168
|
+
ensemble_size=self.get_hyperparameters()["ensemble_size"],
|
122
169
|
metric=self.eval_metric,
|
123
170
|
prediction_length=self.prediction_length,
|
124
171
|
target=self.target,
|
@@ -136,37 +183,3 @@ class TimeSeriesGreedyEnsemble(AbstractTimeSeriesEnsembleModel):
|
|
136
183
|
|
137
184
|
weights_for_printing = {model: round(weight, 2) for model, weight in self.model_to_weight.items()}
|
138
185
|
logger.info(f"\tEnsemble weights: {pprint.pformat(weights_for_printing, width=200)}")
|
139
|
-
|
140
|
-
@property
|
141
|
-
def model_names(self) -> List[str]:
|
142
|
-
return list(self.model_to_weight.keys())
|
143
|
-
|
144
|
-
@property
|
145
|
-
def model_weights(self) -> np.ndarray:
|
146
|
-
return np.array(list(self.model_to_weight.values()), dtype=np.float64)
|
147
|
-
|
148
|
-
def predict(self, data: Dict[str, Optional[TimeSeriesDataFrame]], **kwargs) -> TimeSeriesDataFrame:
|
149
|
-
if not set(self.model_names).issubset(set(data.keys())):
|
150
|
-
raise ValueError(
|
151
|
-
f"Set of models given for prediction in {self.name} differ from those provided during initialization."
|
152
|
-
)
|
153
|
-
for model_name, model_pred in data.items():
|
154
|
-
if model_pred is None:
|
155
|
-
raise RuntimeError(f"{self.name} cannot predict because base model {model_name} failed.")
|
156
|
-
|
157
|
-
# Make sure that all predictions have same shape
|
158
|
-
assert len(set(pred.shape for pred in data.values())) == 1
|
159
|
-
|
160
|
-
return sum(data[model_name] * weight for model_name, weight in self.model_to_weight.items())
|
161
|
-
|
162
|
-
def get_info(self) -> dict:
|
163
|
-
info = super().get_info()
|
164
|
-
info["model_weights"] = self.model_to_weight
|
165
|
-
return info
|
166
|
-
|
167
|
-
def remap_base_models(self, model_refit_map: Dict[str, str]) -> None:
|
168
|
-
updated_weights = {}
|
169
|
-
for model, weight in self.model_to_weight.items():
|
170
|
-
model_full_name = model_refit_map.get(model, model)
|
171
|
-
updated_weights[model_full_name] = weight
|
172
|
-
self.model_to_weight = updated_weights
|
autogluon/timeseries/trainer.py
CHANGED
@@ -19,8 +19,8 @@ from autogluon.core.utils.loaders import load_pkl
|
|
19
19
|
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
|
-
from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel
|
23
|
-
from autogluon.timeseries.models.ensemble import AbstractTimeSeriesEnsembleModel,
|
22
|
+
from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel, TimeSeriesModelBase
|
23
|
+
from autogluon.timeseries.models.ensemble import AbstractTimeSeriesEnsembleModel, GreedyEnsemble
|
24
24
|
from autogluon.timeseries.models.multi_window import MultiWindowBacktestingModel
|
25
25
|
from autogluon.timeseries.models.presets import contains_searchspace, get_preset_models
|
26
26
|
from autogluon.timeseries.splitter import AbstractWindowSplitter, ExpandingWindowSplitter
|
@@ -34,7 +34,7 @@ from autogluon.timeseries.utils.warning_filters import disable_tqdm, warning_fil
|
|
34
34
|
logger = logging.getLogger("autogluon.timeseries.trainer")
|
35
35
|
|
36
36
|
|
37
|
-
class TimeSeriesTrainer(AbstractTrainer[
|
37
|
+
class TimeSeriesTrainer(AbstractTrainer[TimeSeriesModelBase]):
|
38
38
|
_cached_predictions_filename = "cached_predictions.pkl"
|
39
39
|
|
40
40
|
max_rel_importance_score: float = 1e5
|
@@ -73,12 +73,12 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
73
73
|
# Ensemble cannot be fit if val_scores are not computed
|
74
74
|
self.enable_ensemble = enable_ensemble and not skip_model_selection
|
75
75
|
if ensemble_model_type is None:
|
76
|
-
ensemble_model_type =
|
76
|
+
ensemble_model_type = GreedyEnsemble
|
77
77
|
else:
|
78
78
|
logger.warning(
|
79
79
|
"Using a custom `ensemble_model_type` is experimental functionality that may break in future versions."
|
80
80
|
)
|
81
|
-
self.ensemble_model_type = ensemble_model_type
|
81
|
+
self.ensemble_model_type: Type[AbstractTimeSeriesEnsembleModel] = ensemble_model_type
|
82
82
|
|
83
83
|
self.verbosity = verbosity
|
84
84
|
|
@@ -145,7 +145,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
145
145
|
|
146
146
|
def _add_model(
|
147
147
|
self,
|
148
|
-
model:
|
148
|
+
model: TimeSeriesModelBase,
|
149
149
|
base_models: Optional[List[str]] = None,
|
150
150
|
):
|
151
151
|
"""Add a model to the model graph of the trainer. If the model is an ensemble, also add
|
@@ -153,7 +153,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
153
153
|
|
154
154
|
Parameters
|
155
155
|
----------
|
156
|
-
model :
|
156
|
+
model : TimeSeriesModelBase
|
157
157
|
The model to be added to the model graph.
|
158
158
|
base_models : List[str], optional, default None
|
159
159
|
If the model is an ensemble, the list of base model names that are included in the ensemble.
|
@@ -442,6 +442,8 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
442
442
|
num_base_models = len(models)
|
443
443
|
model_names_trained = []
|
444
444
|
for i, model in enumerate(models):
|
445
|
+
assert isinstance(model, AbstractTimeSeriesModel)
|
446
|
+
|
445
447
|
if time_limit is None:
|
446
448
|
time_left = None
|
447
449
|
time_left_for_model = None
|
@@ -558,13 +560,18 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
558
560
|
return ensemble_name
|
559
561
|
|
560
562
|
def fit_ensemble(
|
561
|
-
self,
|
563
|
+
self,
|
564
|
+
data_per_window: List[TimeSeriesDataFrame],
|
565
|
+
model_names: List[str],
|
566
|
+
time_limit: Optional[float] = None,
|
562
567
|
) -> str:
|
563
568
|
logger.info("Fitting simple weighted ensemble.")
|
564
569
|
|
565
|
-
|
570
|
+
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]] = {}
|
571
|
+
base_model_scores = self.get_models_attribute_dict(attribute="val_score", models=self.get_model_names(0))
|
572
|
+
|
566
573
|
for model_name in model_names:
|
567
|
-
|
574
|
+
predictions_per_window[model_name] = self._get_model_oof_predictions(model_name=model_name)
|
568
575
|
|
569
576
|
time_start = time.time()
|
570
577
|
ensemble = self.ensemble_model_type(
|
@@ -579,7 +586,12 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
579
586
|
covariate_metadata=self.covariate_metadata,
|
580
587
|
)
|
581
588
|
with warning_filter():
|
582
|
-
ensemble.
|
589
|
+
ensemble.fit(
|
590
|
+
predictions_per_window=predictions_per_window,
|
591
|
+
data_per_window=data_per_window,
|
592
|
+
model_scores=base_model_scores,
|
593
|
+
time_limit=time_limit,
|
594
|
+
)
|
583
595
|
ensemble.fit_time = time.time() - time_start
|
584
596
|
|
585
597
|
predict_time = 0
|
@@ -589,7 +601,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
589
601
|
|
590
602
|
score_per_fold = []
|
591
603
|
for window_idx, data in enumerate(data_per_window):
|
592
|
-
predictions = ensemble.predict({n:
|
604
|
+
predictions = ensemble.predict({n: predictions_per_window[n][window_idx] for n in ensemble.model_names})
|
593
605
|
score_per_fold.append(self._score_with_predictions(data, predictions))
|
594
606
|
ensemble.val_score = float(np.mean(score_per_fold, dtype=np.float64))
|
595
607
|
|
@@ -734,7 +746,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
734
746
|
return unpersisted_models
|
735
747
|
|
736
748
|
def _get_model_for_prediction(
|
737
|
-
self, model: Optional[Union[str,
|
749
|
+
self, model: Optional[Union[str, TimeSeriesModelBase]] = None, verbose: bool = True
|
738
750
|
) -> str:
|
739
751
|
"""Given an optional identifier or model object, return the name of the model with which to predict.
|
740
752
|
|
@@ -751,7 +763,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
751
763
|
)
|
752
764
|
return self.model_best
|
753
765
|
else:
|
754
|
-
if isinstance(model,
|
766
|
+
if isinstance(model, TimeSeriesModelBase):
|
755
767
|
return model.name
|
756
768
|
else:
|
757
769
|
if model not in self.get_model_names():
|
@@ -762,7 +774,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
762
774
|
self,
|
763
775
|
data: TimeSeriesDataFrame,
|
764
776
|
known_covariates: Optional[TimeSeriesDataFrame] = None,
|
765
|
-
model: Optional[Union[str,
|
777
|
+
model: Optional[Union[str, TimeSeriesModelBase]] = None,
|
766
778
|
use_cache: bool = True,
|
767
779
|
random_seed: Optional[int] = None,
|
768
780
|
) -> TimeSeriesDataFrame:
|
@@ -798,7 +810,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
798
810
|
def score(
|
799
811
|
self,
|
800
812
|
data: TimeSeriesDataFrame,
|
801
|
-
model: Optional[Union[str,
|
813
|
+
model: Optional[Union[str, TimeSeriesModelBase]] = None,
|
802
814
|
metric: Union[str, TimeSeriesScorer, None] = None,
|
803
815
|
use_cache: bool = True,
|
804
816
|
) -> float:
|
@@ -809,7 +821,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
809
821
|
def evaluate(
|
810
822
|
self,
|
811
823
|
data: TimeSeriesDataFrame,
|
812
|
-
model: Optional[Union[str,
|
824
|
+
model: Optional[Union[str, TimeSeriesModelBase]] = None,
|
813
825
|
metrics: Optional[Union[str, TimeSeriesScorer, List[Union[str, TimeSeriesScorer]]]] = None,
|
814
826
|
use_cache: bool = True,
|
815
827
|
) -> Dict[str, float]:
|
@@ -831,7 +843,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
831
843
|
self,
|
832
844
|
data: TimeSeriesDataFrame,
|
833
845
|
features: List[str],
|
834
|
-
model: Optional[Union[str,
|
846
|
+
model: Optional[Union[str, TimeSeriesModelBase]] = None,
|
835
847
|
metric: Optional[Union[str, TimeSeriesScorer]] = None,
|
836
848
|
time_limit: Optional[float] = None,
|
837
849
|
method: Literal["naive", "permutation"] = "permutation",
|
@@ -934,7 +946,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
934
946
|
|
935
947
|
return importance_df
|
936
948
|
|
937
|
-
def _model_uses_feature(self, model: Union[str,
|
949
|
+
def _model_uses_feature(self, model: Union[str, TimeSeriesModelBase], feature: str) -> bool:
|
938
950
|
"""Check if the given model uses the given feature."""
|
939
951
|
models_with_ancestors = set(self.get_minimum_model_set(model))
|
940
952
|
|
@@ -976,7 +988,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
976
988
|
|
977
989
|
def _predict_model(
|
978
990
|
self,
|
979
|
-
model: Union[str,
|
991
|
+
model: Union[str, TimeSeriesModelBase],
|
980
992
|
data: TimeSeriesDataFrame,
|
981
993
|
model_pred_dict: Dict[str, Optional[TimeSeriesDataFrame]],
|
982
994
|
known_covariates: Optional[TimeSeriesDataFrame] = None,
|
@@ -992,7 +1004,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
992
1004
|
|
993
1005
|
def _get_inputs_to_model(
|
994
1006
|
self,
|
995
|
-
model: Union[str,
|
1007
|
+
model: Union[str, TimeSeriesModelBase],
|
996
1008
|
data: TimeSeriesDataFrame,
|
997
1009
|
model_pred_dict: Dict[str, Optional[TimeSeriesDataFrame]],
|
998
1010
|
) -> Union[TimeSeriesDataFrame, Dict[str, Optional[TimeSeriesDataFrame]]]:
|
@@ -1184,6 +1196,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
1184
1196
|
model_name = model.name
|
1185
1197
|
if model._get_tags()["can_refit_full"]:
|
1186
1198
|
model_full = model.convert_to_refit_full_template()
|
1199
|
+
assert isinstance(model_full, AbstractTimeSeriesModel)
|
1187
1200
|
logger.info(f"Fitting model: {model_full.name}")
|
1188
1201
|
models_trained = self._train_and_save(
|
1189
1202
|
train_data=refit_full_data,
|
@@ -1249,7 +1262,7 @@ class TimeSeriesTrainer(AbstractTrainer[AbstractTimeSeriesModel]):
|
|
1249
1262
|
freq: Optional[str] = None,
|
1250
1263
|
excluded_model_types: Optional[List[str]] = None,
|
1251
1264
|
hyperparameter_tune: bool = False,
|
1252
|
-
) -> List[
|
1265
|
+
) -> List[TimeSeriesModelBase]:
|
1253
1266
|
return get_preset_models(
|
1254
1267
|
path=self.path,
|
1255
1268
|
eval_metric=self.eval_metric,
|
autogluon/timeseries/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: autogluon.timeseries
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.1b20250423
|
4
4
|
Summary: Fast and Accurate ML in 3 Lines of Code
|
5
5
|
Home-page: https://github.com/autogluon/autogluon
|
6
6
|
Author: AutoGluon Community
|
@@ -55,9 +55,9 @@ Requires-Dist: fugue>=0.9.0
|
|
55
55
|
Requires-Dist: tqdm<5,>=4.38
|
56
56
|
Requires-Dist: orjson~=3.9
|
57
57
|
Requires-Dist: tensorboard<3,>=2.9
|
58
|
-
Requires-Dist: autogluon.core[raytune]==1.2.
|
59
|
-
Requires-Dist: autogluon.common==1.2.
|
60
|
-
Requires-Dist: autogluon.tabular[catboost,lightgbm,xgboost]==1.2.
|
58
|
+
Requires-Dist: autogluon.core[raytune]==1.2.1b20250423
|
59
|
+
Requires-Dist: autogluon.common==1.2.1b20250423
|
60
|
+
Requires-Dist: autogluon.tabular[catboost,lightgbm,xgboost]==1.2.1b20250423
|
61
61
|
Provides-Extra: all
|
62
62
|
Provides-Extra: chronos-onnx
|
63
63
|
Requires-Dist: optimum[onnxruntime]<1.23,>=1.17; extra == "chronos-onnx"
|
@@ -1,16 +1,16 @@
|
|
1
|
-
autogluon.timeseries-1.2.
|
1
|
+
autogluon.timeseries-1.2.1b20250423-py3.9-nspkg.pth,sha256=cQGwpuGPqg1GXscIwt-7PmME1OnSpD-7ixkikJ31WAY,554
|
2
2
|
autogluon/timeseries/__init__.py,sha256=_CrLLc1fkjen7UzWoO0Os8WZoHOgvZbHKy46I8v_4k4,304
|
3
3
|
autogluon/timeseries/evaluator.py,sha256=l642tYfTHsl8WVIq_vV6qhgAFVFr9UuZD7gLra3A_Kc,250
|
4
4
|
autogluon/timeseries/learner.py,sha256=7dqSHKCIX2osjv9cmWWLwaGvdrPvla0HTnsR75bdenY,14112
|
5
5
|
autogluon/timeseries/predictor.py,sha256=eklp1Qils6f4vIex8KhLD6nVsUQwZ6Jt9UKkTsSyErM,85739
|
6
6
|
autogluon/timeseries/regressor.py,sha256=xw5VPrXS-NQ_Ts4ppDjoNV0TdqUYjW4VINUtb_BZdiI,11868
|
7
7
|
autogluon/timeseries/splitter.py,sha256=yzPca9p2bWV-_VJAptUyyzQsxu-uixAdpMoGQtDzMD4,3205
|
8
|
-
autogluon/timeseries/trainer.py,sha256
|
9
|
-
autogluon/timeseries/version.py,sha256=
|
8
|
+
autogluon/timeseries/trainer.py,sha256=-8UBCe_uzwOoMk8wHgVEEhN7mN2biumUzGrf5YY6n4w,58131
|
9
|
+
autogluon/timeseries/version.py,sha256=5cHaVBGEbXKlEyQMJ8g3TzPJ5O8xRFYaKM8gVl6YgvE,91
|
10
10
|
autogluon/timeseries/configs/__init__.py,sha256=BTtHIPCYeGjqgOcvqb8qPD4VNX-ICKOg6wnkew1cPOE,98
|
11
11
|
autogluon/timeseries/configs/presets_configs.py,sha256=cLat8ecLlWrI-SC5KLBDCX2SbVXaucemy2pjxJAtSY0,2543
|
12
12
|
autogluon/timeseries/dataset/__init__.py,sha256=UvnhAN5tjgxXTHoZMQDy64YMDj4Xxa68yY7NP4vAw0o,81
|
13
|
-
autogluon/timeseries/dataset/ts_dataframe.py,sha256=
|
13
|
+
autogluon/timeseries/dataset/ts_dataframe.py,sha256=uX639T0zD6DDmsO4PbCY_X2gI_4M95Gf4oYCy4etGhk,47169
|
14
14
|
autogluon/timeseries/metrics/__init__.py,sha256=dJCrZ2cHwqhqNctwQjwG-FHgGUmzIFT-D0z72f4RAVM,2104
|
15
15
|
autogluon/timeseries/metrics/abstract.py,sha256=CHUZB6xt9oF9yijSOjgGtjLuKo2X0mT6dQDuwg4ZzpU,8192
|
16
16
|
autogluon/timeseries/metrics/point.py,sha256=2nlieQcPBCI9hXMT3v0Oe802ykZDuzvEtDpunzt0IVA,15785
|
@@ -18,8 +18,8 @@ autogluon/timeseries/metrics/quantile.py,sha256=wvFeDMvRf1mFurhvVr_7g13Kg-hKIRoW
|
|
18
18
|
autogluon/timeseries/metrics/utils.py,sha256=HuDe1BNe8yJU4f_DKM913nNrUueoRaw6zhxm1-S20s0,910
|
19
19
|
autogluon/timeseries/models/__init__.py,sha256=MYD9JJ-wUDE5B6jW6E6LU2eXQ6vflfQBvqQJkdzJa3A,1189
|
20
20
|
autogluon/timeseries/models/presets.py,sha256=BdSTW91-flgqhVNuZIvqEf7wUj1iB6BPger4tJaoAZQ,12322
|
21
|
-
autogluon/timeseries/models/abstract/__init__.py,sha256=
|
22
|
-
autogluon/timeseries/models/abstract/abstract_timeseries_model.py,sha256=
|
21
|
+
autogluon/timeseries/models/abstract/__init__.py,sha256=Htfkjjc3vo92RvyM8rIlQ0PLWt3jcrCKZES07UvCMV0,146
|
22
|
+
autogluon/timeseries/models/abstract/abstract_timeseries_model.py,sha256=ZaMkFUgr3YTxGAjm3k3xZCp0bIbgelTzZ2kwWqQ1IQ4,32978
|
23
23
|
autogluon/timeseries/models/abstract/model_trial.py,sha256=ENPg_7nsdxIvaNM0o0UShZ3x8jFlRmwRc5m0fGPC0TM,3720
|
24
24
|
autogluon/timeseries/models/abstract/tunable.py,sha256=SFl4vjkb6BfFFaRPVdftnnLYlIyCThutLHxiiAlV6tY,7168
|
25
25
|
autogluon/timeseries/models/autogluon_tabular/__init__.py,sha256=r9i6jWcyeLHYClkcMSKRVsfrkBUMxpDrTATNTBc_qgQ,136
|
@@ -27,15 +27,16 @@ autogluon/timeseries/models/autogluon_tabular/mlforecast.py,sha256=QaQcImTXJpzl-
|
|
27
27
|
autogluon/timeseries/models/autogluon_tabular/transforms.py,sha256=CVvNun8DKH7UQGyXU-iO2xmvBIHRQElw72gIrZ7QjkU,2504
|
28
28
|
autogluon/timeseries/models/autogluon_tabular/utils.py,sha256=Fn3Vu_Q0PCtEUbtNgLp1xIblg7dOdpFlF3W5kLHgruI,63
|
29
29
|
autogluon/timeseries/models/chronos/__init__.py,sha256=wT77HzTtmQxW3sw2k0mA5Ot6PSHivX-Uvn5fjM05EU4,60
|
30
|
-
autogluon/timeseries/models/chronos/model.py,sha256=
|
30
|
+
autogluon/timeseries/models/chronos/model.py,sha256=dYc3nZE6BqpunwI2IyuOm1LGW1RJJEzxYCB5ZW0585E,31649
|
31
31
|
autogluon/timeseries/models/chronos/pipeline/__init__.py,sha256=N-YZH9BGBoi99r5cznJe1zEEjwjIg7cOYIHZkKuJq44,247
|
32
32
|
autogluon/timeseries/models/chronos/pipeline/base.py,sha256=14OAKHmio6LmO4mVom2mPGB0CvIrOjMGJzb-MVSAq-s,5596
|
33
33
|
autogluon/timeseries/models/chronos/pipeline/chronos.py,sha256=uFJLsSb2WQiSrmDZ0g2mO-lhTFUlq7vplGRBXZ9_VBk,22591
|
34
34
|
autogluon/timeseries/models/chronos/pipeline/chronos_bolt.py,sha256=kNIDesojKB3rbEK9jM8st4k7ZeaT6tz1znf4PsRDv2Q,20066
|
35
35
|
autogluon/timeseries/models/chronos/pipeline/utils.py,sha256=dtDX5Pyu95bGv7qmqgfUc1iYowWPY84dnGN0uyqyHyQ,13131
|
36
|
-
autogluon/timeseries/models/ensemble/__init__.py,sha256=
|
37
|
-
autogluon/timeseries/models/ensemble/
|
38
|
-
autogluon/timeseries/models/ensemble/
|
36
|
+
autogluon/timeseries/models/ensemble/__init__.py,sha256=_BivnZaOWJiIvu93IQy0mrLdCZKT2NHHSqkf31hwF2s,158
|
37
|
+
autogluon/timeseries/models/ensemble/abstract.py,sha256=ie-BKD4JIkQQoKqtf6sYI5Aix7dSgywFsSdeGPxoElk,5821
|
38
|
+
autogluon/timeseries/models/ensemble/basic.py,sha256=BRPWg_Wgfb87iInFSoTRE75BRHaovRR5HFRvzxET_wU,3423
|
39
|
+
autogluon/timeseries/models/ensemble/greedy.py,sha256=2MVLTPvJ9Khuqri1gwQlo0RmKFeWK4qFkEcLH1Dh41E,7362
|
39
40
|
autogluon/timeseries/models/gluonts/__init__.py,sha256=asC1PTj4j9xMbilvk1IT1julnpeoKbv5ZNuAR6-DFgA,361
|
40
41
|
autogluon/timeseries/models/gluonts/abstract_gluonts.py,sha256=35T8rty6sPGiaSFNpiVNmeseo1_qpn664UcWo92W5eI,32906
|
41
42
|
autogluon/timeseries/models/gluonts/torch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -59,11 +60,11 @@ autogluon/timeseries/utils/datetime/base.py,sha256=3NdsH3NDq4cVAOSoy3XpaNixyNlbj
|
|
59
60
|
autogluon/timeseries/utils/datetime/lags.py,sha256=gQDk5_zmsY5DUWDUpSaCKYkQ9nHKKY-LsywJQRAoYSk,5988
|
60
61
|
autogluon/timeseries/utils/datetime/seasonality.py,sha256=YK_2k8hvYIMW-sJPnjGWRtCnvIOthwA2hATB3nwVoD4,834
|
61
62
|
autogluon/timeseries/utils/datetime/time_features.py,sha256=MjLi3zQ00uWWJtXH9oGX2GJkTbvjdSiuabSa4kcVuxE,2672
|
62
|
-
autogluon.timeseries-1.2.
|
63
|
-
autogluon.timeseries-1.2.
|
64
|
-
autogluon.timeseries-1.2.
|
65
|
-
autogluon.timeseries-1.2.
|
66
|
-
autogluon.timeseries-1.2.
|
67
|
-
autogluon.timeseries-1.2.
|
68
|
-
autogluon.timeseries-1.2.
|
69
|
-
autogluon.timeseries-1.2.
|
63
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
|
64
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/METADATA,sha256=vs0Qvp4NFtgEq_tw4OkGPvAwv8EPGJWseJ7StCv_cU4,12687
|
65
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/NOTICE,sha256=7nPQuj8Kp-uXsU0S5so3-2dNU5EctS5hDXvvzzehd7E,114
|
66
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
67
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/namespace_packages.txt,sha256=giERA4R78OkJf2ijn5slgjURlhRPzfLr7waIcGkzYAo,10
|
68
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/top_level.txt,sha256=giERA4R78OkJf2ijn5slgjURlhRPzfLr7waIcGkzYAo,10
|
69
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
70
|
+
autogluon.timeseries-1.2.1b20250423.dist-info/RECORD,,
|
@@ -1,86 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
from typing import Dict, List, Optional
|
3
|
-
|
4
|
-
from autogluon.core.utils.exceptions import TimeLimitExceeded
|
5
|
-
from autogluon.timeseries.dataset import TimeSeriesDataFrame
|
6
|
-
from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel
|
7
|
-
|
8
|
-
logger = logging.getLogger(__name__)
|
9
|
-
|
10
|
-
|
11
|
-
class AbstractTimeSeriesEnsembleModel(AbstractTimeSeriesModel):
|
12
|
-
"""Abstract class for time series ensemble models."""
|
13
|
-
|
14
|
-
@property
|
15
|
-
def model_names(self) -> List[str]:
|
16
|
-
"""Names of base models included in the ensemble."""
|
17
|
-
raise NotImplementedError
|
18
|
-
|
19
|
-
def fit_ensemble(
|
20
|
-
self,
|
21
|
-
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]],
|
22
|
-
data_per_window: List[TimeSeriesDataFrame],
|
23
|
-
time_limit: Optional[float] = None,
|
24
|
-
**kwargs,
|
25
|
-
):
|
26
|
-
"""Fit ensemble model given predictions of candidate base models and the true data.
|
27
|
-
|
28
|
-
Parameters
|
29
|
-
----------
|
30
|
-
predictions_per_window : Dict[str, List[TimeSeriesDataFrame]]
|
31
|
-
Dictionary that maps the names of component models to their respective predictions for each validation
|
32
|
-
window.
|
33
|
-
data_per_window : List[TimeSeriesDataFrame]
|
34
|
-
Observed ground truth data used to train the ensemble for each validation window. Each entry in the list
|
35
|
-
includes both the forecast horizon (for which the predictions are given in ``predictions``), as well as the
|
36
|
-
"history".
|
37
|
-
time_limit : Optional[int]
|
38
|
-
Maximum allowed time for training in seconds.
|
39
|
-
"""
|
40
|
-
if time_limit is not None and time_limit <= 0:
|
41
|
-
logger.warning(
|
42
|
-
f"\tWarning: Model has no time left to train, skipping model... (Time Left = {round(time_limit, 1)}s)"
|
43
|
-
)
|
44
|
-
raise TimeLimitExceeded
|
45
|
-
if isinstance(data_per_window, TimeSeriesDataFrame):
|
46
|
-
raise ValueError("When fitting ensemble, `data` should contain ground truth for each validation window")
|
47
|
-
num_val_windows = len(data_per_window)
|
48
|
-
for model, preds in predictions_per_window.items():
|
49
|
-
if len(preds) != num_val_windows:
|
50
|
-
raise ValueError(f"For model {model} predictions are unavailable for some validation windows")
|
51
|
-
self._fit_ensemble(
|
52
|
-
predictions_per_window=predictions_per_window,
|
53
|
-
data_per_window=data_per_window,
|
54
|
-
time_limit=time_limit,
|
55
|
-
)
|
56
|
-
return self
|
57
|
-
|
58
|
-
def _fit_ensemble(
|
59
|
-
self,
|
60
|
-
predictions_per_window: Dict[str, List[TimeSeriesDataFrame]],
|
61
|
-
data_per_window: List[TimeSeriesDataFrame],
|
62
|
-
time_limit: Optional[int] = None,
|
63
|
-
**kwargs,
|
64
|
-
):
|
65
|
-
"""Private method for `fit_ensemble`. See `fit_ensemble` for documentation of arguments. Apart from the model
|
66
|
-
training logic, `fit_ensemble` additionally implements other logic such as keeping track of the time limit.
|
67
|
-
"""
|
68
|
-
raise NotImplementedError
|
69
|
-
|
70
|
-
def predict(self, data: Dict[str, Optional[TimeSeriesDataFrame]], **kwargs) -> TimeSeriesDataFrame:
|
71
|
-
raise NotImplementedError
|
72
|
-
|
73
|
-
def remap_base_models(self, model_refit_map: Dict[str, str]) -> None:
|
74
|
-
"""Update names of the base models based on the mapping in model_refit_map.
|
75
|
-
|
76
|
-
This method should be called after performing refit_full to point to the refitted base models, if necessary.
|
77
|
-
"""
|
78
|
-
raise NotImplementedError
|
79
|
-
|
80
|
-
# TODO: remove
|
81
|
-
def _fit(*args, **kwargs):
|
82
|
-
pass
|
83
|
-
|
84
|
-
# TODO: remove
|
85
|
-
def _predict(*args, **kwargs):
|
86
|
-
pass
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|