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.
Files changed (20) hide show
  1. autogluon/timeseries/dataset/ts_dataframe.py +1 -1
  2. autogluon/timeseries/models/abstract/__init__.py +2 -2
  3. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +178 -122
  4. autogluon/timeseries/models/chronos/model.py +3 -2
  5. autogluon/timeseries/models/ensemble/__init__.py +3 -2
  6. autogluon/timeseries/models/ensemble/abstract.py +139 -0
  7. autogluon/timeseries/models/ensemble/basic.py +88 -0
  8. autogluon/timeseries/models/ensemble/{greedy_ensemble.py → greedy.py} +66 -53
  9. autogluon/timeseries/trainer.py +35 -22
  10. autogluon/timeseries/version.py +1 -1
  11. {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/METADATA +4 -4
  12. {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/RECORD +19 -18
  13. autogluon/timeseries/models/ensemble/abstract_timeseries_ensemble.py +0 -86
  14. /autogluon.timeseries-1.2.1b20250421-py3.9-nspkg.pth → /autogluon.timeseries-1.2.1b20250423-py3.9-nspkg.pth +0 -0
  15. {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/LICENSE +0 -0
  16. {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/NOTICE +0 -0
  17. {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/WHEEL +0 -0
  18. {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/namespace_packages.txt +0 -0
  19. {autogluon.timeseries-1.2.1b20250421.dist-info → autogluon.timeseries-1.2.1b20250423.dist-info}/top_level.txt +0 -0
  20. {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( # type: ignore
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
- model_path_safe = str(model_path_input).replace("/", "__").replace(os.path.sep, "__")[-50:]
200
- name += f"[{model_path_safe}]"
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 .abstract_timeseries_ensemble import AbstractTimeSeriesEnsembleModel
2
- from .greedy_ensemble import TimeSeriesGreedyEnsemble
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: Optional[int] = None,
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
- def _fit(
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]], # first dim: model, second dim: val window index
58
+ predictions: List[List[TimeSeriesDataFrame]],
50
59
  labels: List[TimeSeriesDataFrame],
51
- time_limit: Optional[int] = None,
52
- sample_weight=None,
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(self, y_true, y_pred_proba, metric=None, sample_weight=None): # noqa
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 TimeSeriesGreedyEnsemble(AbstractTimeSeriesEnsembleModel):
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, ensemble_size: int = 100, **kwargs):
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 _fit_ensemble(
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
- time_limit: Optional[int] = None,
116
- **kwargs,
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
@@ -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, TimeSeriesGreedyEnsemble
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[AbstractTimeSeriesModel]):
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 = TimeSeriesGreedyEnsemble
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: AbstractTimeSeriesModel,
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 : AbstractTimeSeriesModel
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, data_per_window: List[TimeSeriesDataFrame], model_names: List[str], time_limit: Optional[float] = None
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
- model_preds: Dict[str, List[TimeSeriesDataFrame]] = {}
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
- model_preds[model_name] = self._get_model_oof_predictions(model_name=model_name)
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.fit_ensemble(model_preds, data_per_window=data_per_window, time_limit=time_limit)
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: model_preds[n][window_idx] for n in ensemble.model_names})
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, AbstractTimeSeriesModel]] = None, verbose: bool = True
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, AbstractTimeSeriesModel):
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, AbstractTimeSeriesModel]] = None,
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, AbstractTimeSeriesModel]] = None,
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, AbstractTimeSeriesModel]] = None,
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, AbstractTimeSeriesModel]] = None,
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, AbstractTimeSeriesModel], feature: str) -> bool:
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, AbstractTimeSeriesModel],
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, AbstractTimeSeriesModel],
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[AbstractTimeSeriesModel]:
1265
+ ) -> List[TimeSeriesModelBase]:
1253
1266
  return get_preset_models(
1254
1267
  path=self.path,
1255
1268
  eval_metric=self.eval_metric,
@@ -1,4 +1,4 @@
1
1
  """This is the autogluon version file."""
2
2
 
3
- __version__ = "1.2.1b20250421"
3
+ __version__ = "1.2.1b20250423"
4
4
  __lite__ = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: autogluon.timeseries
3
- Version: 1.2.1b20250421
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.1b20250421
59
- Requires-Dist: autogluon.common==1.2.1b20250421
60
- Requires-Dist: autogluon.tabular[catboost,lightgbm,xgboost]==1.2.1b20250421
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.1b20250421-py3.9-nspkg.pth,sha256=cQGwpuGPqg1GXscIwt-7PmME1OnSpD-7ixkikJ31WAY,554
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=LHLaLvzOLjjwFHfKifydp5NOCLLv2nv2BJLerbeNWuU,57700
9
- autogluon/timeseries/version.py,sha256=w-cB2fz5-mm5dxzYRX6litgefPHSxeBnNxZVL1Yml6E,91
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=SodnGhEA2V-hnfYHuAkH8rK4hQlLH8K5Tb6dsGapvPM,47161
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=wvDsQAZIV0N3AwBeMaGItoQ82trEfnT-nol2AAOIxBg,102
22
- autogluon/timeseries/models/abstract/abstract_timeseries_model.py,sha256=gGairH3JX5rMEWhSj6VYy6zu7isZ04IaIj4lDXaTc1E,30814
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=9kKVUBCEdgQ176YM33tvcn3pQsEpv0_OLw7VK-Scxw8,31590
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=kFr11Gmt7lQJu9Rr8HuIPphQN5l1TsoorfbJm_O3a_s,128
37
- autogluon/timeseries/models/ensemble/abstract_timeseries_ensemble.py,sha256=LzL64JASiwkLsuFxGToXJGRItcMxq5_Ig2QP5Zm7SHw,3537
38
- autogluon/timeseries/models/ensemble/greedy_ensemble.py,sha256=v5A2xv4d_QynA1GWD7iqmn-VVEFpD88Oiswyp72yBCc,7321
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.1b20250421.dist-info/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
63
- autogluon.timeseries-1.2.1b20250421.dist-info/METADATA,sha256=xGnEawYe3fyJ9cqSQ1j5EqCdjY5G3G7gNsdWMad9QUw,12687
64
- autogluon.timeseries-1.2.1b20250421.dist-info/NOTICE,sha256=7nPQuj8Kp-uXsU0S5so3-2dNU5EctS5hDXvvzzehd7E,114
65
- autogluon.timeseries-1.2.1b20250421.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
66
- autogluon.timeseries-1.2.1b20250421.dist-info/namespace_packages.txt,sha256=giERA4R78OkJf2ijn5slgjURlhRPzfLr7waIcGkzYAo,10
67
- autogluon.timeseries-1.2.1b20250421.dist-info/top_level.txt,sha256=giERA4R78OkJf2ijn5slgjURlhRPzfLr7waIcGkzYAo,10
68
- autogluon.timeseries-1.2.1b20250421.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
69
- autogluon.timeseries-1.2.1b20250421.dist-info/RECORD,,
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