autogluon.timeseries 1.2.1b20250224__py3-none-any.whl → 1.4.1b20251215__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of autogluon.timeseries might be problematic. Click here for more details.

Files changed (108) hide show
  1. autogluon/timeseries/configs/__init__.py +3 -2
  2. autogluon/timeseries/configs/hyperparameter_presets.py +62 -0
  3. autogluon/timeseries/configs/predictor_presets.py +106 -0
  4. autogluon/timeseries/dataset/ts_dataframe.py +256 -141
  5. autogluon/timeseries/learner.py +86 -52
  6. autogluon/timeseries/metrics/__init__.py +42 -8
  7. autogluon/timeseries/metrics/abstract.py +89 -19
  8. autogluon/timeseries/metrics/point.py +142 -53
  9. autogluon/timeseries/metrics/quantile.py +46 -21
  10. autogluon/timeseries/metrics/utils.py +4 -4
  11. autogluon/timeseries/models/__init__.py +8 -2
  12. autogluon/timeseries/models/abstract/__init__.py +2 -2
  13. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +361 -592
  14. autogluon/timeseries/models/abstract/model_trial.py +2 -1
  15. autogluon/timeseries/models/abstract/tunable.py +189 -0
  16. autogluon/timeseries/models/autogluon_tabular/__init__.py +2 -0
  17. autogluon/timeseries/models/autogluon_tabular/mlforecast.py +282 -194
  18. autogluon/timeseries/models/autogluon_tabular/per_step.py +513 -0
  19. autogluon/timeseries/models/autogluon_tabular/transforms.py +25 -18
  20. autogluon/timeseries/models/chronos/__init__.py +2 -1
  21. autogluon/timeseries/models/chronos/chronos2.py +361 -0
  22. autogluon/timeseries/models/chronos/model.py +219 -138
  23. autogluon/timeseries/models/chronos/{pipeline/utils.py → utils.py} +81 -50
  24. autogluon/timeseries/models/ensemble/__init__.py +37 -2
  25. autogluon/timeseries/models/ensemble/abstract.py +107 -0
  26. autogluon/timeseries/models/ensemble/array_based/__init__.py +3 -0
  27. autogluon/timeseries/models/ensemble/array_based/abstract.py +240 -0
  28. autogluon/timeseries/models/ensemble/array_based/models.py +185 -0
  29. autogluon/timeseries/models/ensemble/array_based/regressor/__init__.py +12 -0
  30. autogluon/timeseries/models/ensemble/array_based/regressor/abstract.py +88 -0
  31. autogluon/timeseries/models/ensemble/array_based/regressor/linear_stacker.py +186 -0
  32. autogluon/timeseries/models/ensemble/array_based/regressor/per_quantile_tabular.py +94 -0
  33. autogluon/timeseries/models/ensemble/array_based/regressor/tabular.py +107 -0
  34. autogluon/timeseries/models/ensemble/ensemble_selection.py +167 -0
  35. autogluon/timeseries/models/ensemble/per_item_greedy.py +172 -0
  36. autogluon/timeseries/models/ensemble/weighted/__init__.py +8 -0
  37. autogluon/timeseries/models/ensemble/weighted/abstract.py +45 -0
  38. autogluon/timeseries/models/ensemble/weighted/basic.py +91 -0
  39. autogluon/timeseries/models/ensemble/weighted/greedy.py +62 -0
  40. autogluon/timeseries/models/gluonts/__init__.py +1 -1
  41. autogluon/timeseries/models/gluonts/{abstract_gluonts.py → abstract.py} +148 -208
  42. autogluon/timeseries/models/gluonts/dataset.py +109 -0
  43. autogluon/timeseries/models/gluonts/{torch/models.py → models.py} +38 -22
  44. autogluon/timeseries/models/local/__init__.py +0 -7
  45. autogluon/timeseries/models/local/abstract_local_model.py +71 -74
  46. autogluon/timeseries/models/local/naive.py +13 -9
  47. autogluon/timeseries/models/local/npts.py +9 -2
  48. autogluon/timeseries/models/local/statsforecast.py +52 -36
  49. autogluon/timeseries/models/multi_window/multi_window_model.py +65 -45
  50. autogluon/timeseries/models/registry.py +64 -0
  51. autogluon/timeseries/models/toto/__init__.py +3 -0
  52. autogluon/timeseries/models/toto/_internal/__init__.py +9 -0
  53. autogluon/timeseries/models/toto/_internal/backbone/__init__.py +3 -0
  54. autogluon/timeseries/models/toto/_internal/backbone/attention.py +196 -0
  55. autogluon/timeseries/models/toto/_internal/backbone/backbone.py +262 -0
  56. autogluon/timeseries/models/toto/_internal/backbone/distribution.py +70 -0
  57. autogluon/timeseries/models/toto/_internal/backbone/kvcache.py +136 -0
  58. autogluon/timeseries/models/toto/_internal/backbone/rope.py +89 -0
  59. autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
  60. autogluon/timeseries/models/toto/_internal/backbone/scaler.py +305 -0
  61. autogluon/timeseries/models/toto/_internal/backbone/transformer.py +333 -0
  62. autogluon/timeseries/models/toto/_internal/dataset.py +165 -0
  63. autogluon/timeseries/models/toto/_internal/forecaster.py +423 -0
  64. autogluon/timeseries/models/toto/dataloader.py +108 -0
  65. autogluon/timeseries/models/toto/hf_pretrained_model.py +200 -0
  66. autogluon/timeseries/models/toto/model.py +249 -0
  67. autogluon/timeseries/predictor.py +685 -297
  68. autogluon/timeseries/regressor.py +94 -44
  69. autogluon/timeseries/splitter.py +8 -32
  70. autogluon/timeseries/trainer/__init__.py +3 -0
  71. autogluon/timeseries/trainer/ensemble_composer.py +444 -0
  72. autogluon/timeseries/trainer/model_set_builder.py +256 -0
  73. autogluon/timeseries/trainer/prediction_cache.py +149 -0
  74. autogluon/timeseries/{trainer.py → trainer/trainer.py} +387 -390
  75. autogluon/timeseries/trainer/utils.py +17 -0
  76. autogluon/timeseries/transforms/__init__.py +2 -13
  77. autogluon/timeseries/transforms/covariate_scaler.py +34 -40
  78. autogluon/timeseries/transforms/target_scaler.py +37 -20
  79. autogluon/timeseries/utils/constants.py +10 -0
  80. autogluon/timeseries/utils/datetime/lags.py +3 -5
  81. autogluon/timeseries/utils/datetime/seasonality.py +1 -3
  82. autogluon/timeseries/utils/datetime/time_features.py +2 -2
  83. autogluon/timeseries/utils/features.py +70 -47
  84. autogluon/timeseries/utils/forecast.py +19 -14
  85. autogluon/timeseries/utils/timer.py +173 -0
  86. autogluon/timeseries/utils/warning_filters.py +4 -2
  87. autogluon/timeseries/version.py +1 -1
  88. autogluon.timeseries-1.4.1b20251215-py3.11-nspkg.pth +1 -0
  89. {autogluon.timeseries-1.2.1b20250224.dist-info → autogluon_timeseries-1.4.1b20251215.dist-info}/METADATA +49 -36
  90. autogluon_timeseries-1.4.1b20251215.dist-info/RECORD +103 -0
  91. {autogluon.timeseries-1.2.1b20250224.dist-info → autogluon_timeseries-1.4.1b20251215.dist-info}/WHEEL +1 -1
  92. autogluon/timeseries/configs/presets_configs.py +0 -79
  93. autogluon/timeseries/evaluator.py +0 -6
  94. autogluon/timeseries/models/chronos/pipeline/__init__.py +0 -11
  95. autogluon/timeseries/models/chronos/pipeline/base.py +0 -160
  96. autogluon/timeseries/models/chronos/pipeline/chronos.py +0 -585
  97. autogluon/timeseries/models/chronos/pipeline/chronos_bolt.py +0 -518
  98. autogluon/timeseries/models/ensemble/abstract_timeseries_ensemble.py +0 -78
  99. autogluon/timeseries/models/ensemble/greedy_ensemble.py +0 -170
  100. autogluon/timeseries/models/gluonts/torch/__init__.py +0 -0
  101. autogluon/timeseries/models/presets.py +0 -360
  102. autogluon.timeseries-1.2.1b20250224-py3.9-nspkg.pth +0 -1
  103. autogluon.timeseries-1.2.1b20250224.dist-info/RECORD +0 -68
  104. {autogluon.timeseries-1.2.1b20250224.dist-info → autogluon_timeseries-1.4.1b20251215.dist-info/licenses}/LICENSE +0 -0
  105. {autogluon.timeseries-1.2.1b20250224.dist-info → autogluon_timeseries-1.4.1b20251215.dist-info/licenses}/NOTICE +0 -0
  106. {autogluon.timeseries-1.2.1b20250224.dist-info → autogluon_timeseries-1.4.1b20251215.dist-info}/namespace_packages.txt +0 -0
  107. {autogluon.timeseries-1.2.1b20250224.dist-info → autogluon_timeseries-1.4.1b20251215.dist-info}/top_level.txt +0 -0
  108. {autogluon.timeseries-1.2.1b20250224.dist-info → autogluon_timeseries-1.4.1b20251215.dist-info}/zip-safe +0 -0
@@ -1,12 +1,10 @@
1
- from __future__ import annotations
2
-
3
1
  import copy
4
2
  import logging
5
3
  import os
6
4
  import re
7
5
  import time
8
- from contextlib import nullcontext
9
- from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Sequence
10
8
 
11
9
  import pandas as pd
12
10
  from typing_extensions import Self
@@ -14,143 +12,51 @@ from typing_extensions import Self
14
12
  from autogluon.common import space
15
13
  from autogluon.common.loaders import load_pkl
16
14
  from autogluon.common.savers import save_pkl
17
- from autogluon.common.utils.distribute_utils import DistributedContext
18
- from autogluon.common.utils.log_utils import DuplicateFilter
19
15
  from autogluon.common.utils.resource_utils import get_resource_manager
20
- from autogluon.common.utils.try_import import try_import_ray
21
16
  from autogluon.common.utils.utils import setup_outputdir
22
- from autogluon.core.constants import AG_ARG_PREFIX, AG_ARGS_FIT, REFIT_FULL_SUFFIX
23
- from autogluon.core.hpo.constants import CUSTOM_BACKEND, RAY_BACKEND
24
- from autogluon.core.hpo.exceptions import EmptySearchSpace
25
- from autogluon.core.hpo.executors import HpoExecutor, HpoExecutorFactory, RayHpoExecutor
17
+ from autogluon.core.constants import AG_ARGS_FIT, REFIT_FULL_SUFFIX
26
18
  from autogluon.core.models import ModelBase
27
19
  from autogluon.core.utils.exceptions import TimeLimitExceeded
28
20
  from autogluon.timeseries.dataset import TimeSeriesDataFrame
29
21
  from autogluon.timeseries.metrics import TimeSeriesScorer, check_get_evaluation_metric
30
- from autogluon.timeseries.regressor import CovariateRegressor
31
- from autogluon.timeseries.transforms import (
32
- CovariateScaler,
33
- LocalTargetScaler,
34
- get_covariate_scaler_from_name,
35
- get_target_scaler_from_name,
36
- )
22
+ from autogluon.timeseries.models.registry import ModelRegistry
23
+ from autogluon.timeseries.regressor import CovariateRegressor, get_covariate_regressor
24
+ from autogluon.timeseries.transforms import CovariateScaler, TargetScaler, get_covariate_scaler, get_target_scaler
37
25
  from autogluon.timeseries.utils.features import CovariateMetadata
38
- from autogluon.timeseries.utils.forecast import get_forecast_horizon_index_ts_dataframe
39
- from autogluon.timeseries.utils.warning_filters import disable_stdout, warning_filter
26
+ from autogluon.timeseries.utils.forecast import make_future_data_frame
40
27
 
41
- from .model_trial import model_trial, skip_hpo
28
+ from .tunable import TimeSeriesTunable
42
29
 
43
30
  logger = logging.getLogger(__name__)
44
- dup_filter = DuplicateFilter()
45
- logger.addFilter(dup_filter)
46
31
 
47
32
 
48
- # TODO: refactor and move to util. We do not need to use "params_aux" in time series
49
- def check_and_split_hyperparameters(
50
- params: Optional[Dict[str, Any]] = None,
51
- ag_args_fit: str = AG_ARGS_FIT,
52
- ag_arg_prefix: str = AG_ARG_PREFIX,
53
- ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
54
- """
55
- Given the user-specified hyperparameters, split into `params` and `params_aux`.
33
+ class TimeSeriesModelBase(ModelBase, ABC):
34
+ """Abstract base class for all `Model` objects in autogluon.timeseries, including both
35
+ forecasting models and forecast combination/ensemble models.
56
36
 
57
37
  Parameters
58
38
  ----------
59
- params : Optional[Dict[str, Any]], default = None
60
- The model hyperparameters dictionary
61
- ag_args_fit : str, default = "ag_args_fit"
62
- The params key to look for that contains params_aux.
63
- If the key is present, the value is used for params_aux and popped from params.
64
- If no such key is found, then initialize params_aux as an empty dictionary.
65
- ag_arg_prefix : str, default = "ag."
66
- The key prefix to look for that indicates a parameter is intended for params_aux.
67
- If None, this logic is skipped.
68
- If a key starts with this prefix, it is popped from params and added to params_aux with the prefix removed.
69
- For example:
70
- input: params={'ag.foo': 2, 'abc': 7}, params_aux={'bar': 3}, and ag_arg_prefix='.ag',
71
- output: params={'abc': 7}, params_aux={'bar': 3, 'foo': 2}
72
- In cases where the key is specified multiple times, the value of the key with the prefix will always take priority.
73
- A warning will be logged if a key is present multiple times.
74
- For example, given the most complex scenario:
75
- input: params={'ag.foo': 1, 'foo': 2, 'ag_args_fit': {'ag.foo': 3, 'foo': 4}}
76
- output: params={'foo': 2}, params_aux={'foo': 1}
77
-
78
- Returns
79
- -------
80
- params, params_aux : (Dict[str, Any], Dict[str, Any])
81
- params will contain the native model hyperparameters
82
- params_aux will contain special auxiliary hyperparameters
83
- """
84
- params = copy.deepcopy(params) if params is not None else dict()
85
- assert isinstance(params, dict), f"Invalid dtype of params! Expected dict, but got {type(params)}"
86
- for k in params.keys():
87
- if not isinstance(k, str):
88
- logger.warning(
89
- f"Warning: Specified hyperparameter key is not of type str: {k} (type={type(k)}). "
90
- f"There might be a bug in your configuration."
91
- )
92
-
93
- params_aux = params.pop(ag_args_fit, dict())
94
- if params_aux is None:
95
- params_aux = dict()
96
- assert isinstance(params_aux, dict), f"Invalid dtype of params_aux! Expected dict, but got {type(params_aux)}"
97
- if ag_arg_prefix is not None:
98
- param_aux_keys = list(params_aux.keys())
99
- for k in param_aux_keys:
100
- if isinstance(k, str) and k.startswith(ag_arg_prefix):
101
- k_no_prefix = k[len(ag_arg_prefix) :]
102
- if k_no_prefix in params_aux:
103
- logger.warning(
104
- f'Warning: hyperparameter "{k}" is present '
105
- f'in `ag_args_fit` as both "{k}" and "{k_no_prefix}". '
106
- f'Will use "{k}" and ignore "{k_no_prefix}".'
107
- )
108
- params_aux[k_no_prefix] = params_aux.pop(k)
109
- param_keys = list(params.keys())
110
- for k in param_keys:
111
- if isinstance(k, str) and k.startswith(ag_arg_prefix):
112
- k_no_prefix = k[len(ag_arg_prefix) :]
113
- if k_no_prefix in params_aux:
114
- logger.warning(
115
- f'Warning: hyperparameter "{k}" is present '
116
- f"in both `ag_args_fit` and `hyperparameters`. "
117
- f"Will use `hyperparameters` value."
118
- )
119
- params_aux[k_no_prefix] = params.pop(k)
120
- return params, params_aux
121
-
122
-
123
- # TODO: refactor. remove params_aux, etc. make class inherit from ABC, make overrides and abstract
124
- # methods clear, change name to TimeSeriesModel, et al.
125
- class AbstractTimeSeriesModel(ModelBase):
126
- """Abstract class for all `Model` objects in autogluon.timeseries.
127
-
128
- Parameters
129
- ----------
130
- path : str, default = None
39
+ path
131
40
  Directory location to store all outputs.
132
41
  If None, a new unique time-stamped directory is chosen.
133
- freq: str
42
+ freq
134
43
  Frequency string (cf. gluonts frequency strings) describing the frequency
135
44
  of the time series data. For example, "h" for hourly or "D" for daily data.
136
- prediction_length: int
45
+ prediction_length
137
46
  Length of the prediction horizon, i.e., the number of time steps the model
138
47
  is fit to forecast.
139
- name : str, default = None
48
+ name
140
49
  Name of the subdirectory inside path where model will be saved.
141
50
  The final model directory will be os.path.join(path, name)
142
51
  If None, defaults to the model's class name: self.__class__.__name__
143
- metadata: CovariateMetadata
52
+ covariate_metadata
144
53
  A mapping of different covariate types known to autogluon.timeseries to column names
145
54
  in the data set.
146
- eval_metric : Union[str, TimeSeriesScorer], default = "WQL"
55
+ eval_metric
147
56
  Metric by which predictions will be ultimately evaluated on future test data. This only impacts
148
57
  ``model.score()``, as eval_metric is not used during training. Available metrics can be found in
149
58
  ``autogluon.timeseries.metrics``.
150
- eval_metric_seasonal_period : int, optional
151
- Seasonal period used to compute some evaluation metrics such as mean absolute scaled error (MASE). Defaults to
152
- ``None``, in which case the seasonal period is computed based on the data frequency.
153
- hyperparameters : dict, default = None
59
+ hyperparameters
154
60
  Hyperparameters that will be used by the model (can be search spaces instead of fixed values).
155
61
  If None, model defaults are used. This is identical to passing an empty dictionary.
156
62
  """
@@ -169,35 +75,34 @@ class AbstractTimeSeriesModel(ModelBase):
169
75
 
170
76
  def __init__(
171
77
  self,
172
- path: Optional[str] = None,
173
- name: Optional[str] = None,
174
- hyperparameters: Optional[Dict[str, Any]] = None,
175
- freq: Optional[str] = None,
78
+ path: str | None = None,
79
+ name: str | None = None,
80
+ hyperparameters: dict[str, Any] | None = None,
81
+ freq: str | None = None,
176
82
  prediction_length: int = 1,
177
- metadata: Optional[CovariateMetadata] = None,
83
+ covariate_metadata: CovariateMetadata | None = None,
178
84
  target: str = "target",
179
85
  quantile_levels: Sequence[float] = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
180
- eval_metric: Union[str, TimeSeriesScorer, None] = None,
181
- eval_metric_seasonal_period: Optional[int] = None,
86
+ eval_metric: str | TimeSeriesScorer | None = None,
182
87
  ):
183
88
  self.name = name or re.sub(r"Model$", "", self.__class__.__name__)
184
89
 
185
90
  self.path_root = path
186
91
  if self.path_root is None:
187
92
  path_suffix = self.name
188
- # TODO: Would be ideal to not create dir, but still track that it is unique. However, this isn't possible to do without a global list of used dirs or using UUID.
93
+ # TODO: Would be ideal to not create dir, but still track that it is unique. However, this isn't possible
94
+ # to do without a global list of used dirs or using UUID.
189
95
  path_cur = setup_outputdir(path=None, create_dir=True, path_suffix=path_suffix)
190
96
  self.path_root = path_cur.rsplit(self.name, 1)[0]
191
97
  logger.log(20, f"Warning: No path was specified for model, defaulting to: {self.path_root}")
192
98
 
193
99
  self.path = os.path.join(self.path_root, self.name)
194
100
 
195
- self.eval_metric: TimeSeriesScorer = check_get_evaluation_metric(eval_metric)
196
- self.eval_metric_seasonal_period = eval_metric_seasonal_period
101
+ self.eval_metric = check_get_evaluation_metric(eval_metric, prediction_length=prediction_length)
197
102
  self.target: str = target
198
- self.metadata = metadata or CovariateMetadata()
103
+ self.covariate_metadata = covariate_metadata or CovariateMetadata()
199
104
 
200
- self.freq: Optional[str] = freq
105
+ self.freq: str | None = freq
201
106
  self.prediction_length: int = prediction_length
202
107
  self.quantile_levels: list[float] = list(quantile_levels)
203
108
 
@@ -212,27 +117,21 @@ class AbstractTimeSeriesModel(ModelBase):
212
117
  else:
213
118
  self.must_drop_median = False
214
119
 
215
- self._oof_predictions: Optional[List[TimeSeriesDataFrame]] = None
216
- self.target_scaler: Optional[LocalTargetScaler] = None
217
- self.covariate_scaler: Optional[CovariateScaler] = None
218
- self.covariate_regressor: Optional[CovariateRegressor] = None
219
-
220
- # TODO: remove the variables below
221
- self.model = None
222
-
223
- self._is_initialized = False
224
- self._user_params, self._user_params_aux = check_and_split_hyperparameters(hyperparameters)
120
+ self._oof_predictions: list[TimeSeriesDataFrame] | None = None
225
121
 
226
- self.params = {}
227
- self.params_aux = {}
228
- self.nondefault_params: List[str] = []
122
+ # user provided hyperparameters and extra arguments that are used during model training
123
+ self._hyperparameters, self._extra_ag_args = self._check_and_split_hyperparameters(hyperparameters)
229
124
 
230
- self.fit_time: Optional[float] = None # Time taken to fit in seconds (Training data)
231
- self.predict_time: Optional[float] = None # Time taken to predict in seconds (Validation data)
232
- self.predict_1_time: Optional[float] = (
233
- None # Time taken to predict 1 row of data in seconds (with batch size `predict_1_batch_size` in params_aux)
234
- )
235
- self.val_score: Optional[float] = None # Score with eval_metric (Validation data)
125
+ # Time taken to fit in seconds (Training data)
126
+ self.fit_time: float | None = None
127
+ # Time taken to predict in seconds, for a single prediction horizon on validation data
128
+ self.predict_time: float | None = None
129
+ # Time taken to predict 1 row of data in seconds (with batch size `predict_1_batch_size`)
130
+ self.predict_1_time: float | None = None
131
+ # Useful for ensembles, additional prediction time excluding base models. None for base models.
132
+ self.predict_time_marginal: float | None = None
133
+ # Score with eval_metric on validation data
134
+ self.val_score: float | None = None
236
135
 
237
136
  def __repr__(self) -> str:
238
137
  return self.name
@@ -248,7 +147,49 @@ class AbstractTimeSeriesModel(ModelBase):
248
147
  self.path = path_context
249
148
  self.path_root = self.path.rsplit(self.name, 1)[0]
250
149
 
251
- def save(self, path: Optional[str] = None, verbose=True) -> str:
150
+ def cache_oof_predictions(self, predictions: TimeSeriesDataFrame | list[TimeSeriesDataFrame]) -> None:
151
+ if isinstance(predictions, TimeSeriesDataFrame):
152
+ predictions = [predictions]
153
+ self._oof_predictions = predictions
154
+
155
+ @classmethod
156
+ def _check_and_split_hyperparameters(
157
+ cls, hyperparameters: dict[str, Any] | None = None
158
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
159
+ """Given the user-specified hyperparameters, split into `hyperparameters` and `extra_ag_args`, intended
160
+ to be used during model initialization.
161
+
162
+ Parameters
163
+ ----------
164
+ hyperparameters
165
+ The model hyperparameters dictionary provided to the model constructor.
166
+
167
+ Returns
168
+ -------
169
+ hyperparameters
170
+ Native model hyperparameters that are passed into the "inner model" AutoGluon wraps
171
+ extra_ag_args
172
+ Special auxiliary parameters that modify the model training process used by AutoGluon
173
+ """
174
+ hyperparameters = copy.deepcopy(hyperparameters) if hyperparameters is not None else dict()
175
+ assert isinstance(hyperparameters, dict), (
176
+ f"Invalid dtype for hyperparameters. Expected dict, but got {type(hyperparameters)}"
177
+ )
178
+ for k in hyperparameters.keys():
179
+ if not isinstance(k, str):
180
+ logger.warning(
181
+ f"Warning: Specified hyperparameter key is not of type str: {k} (type={type(k)}). "
182
+ f"There might be a bug in your configuration."
183
+ )
184
+
185
+ extra_ag_args = hyperparameters.pop(AG_ARGS_FIT, {})
186
+ if not isinstance(extra_ag_args, dict):
187
+ raise ValueError(
188
+ f"Invalid hyperparameter type for `{AG_ARGS_FIT}`. Expected dict, but got {type(extra_ag_args)}"
189
+ )
190
+ return hyperparameters, extra_ag_args
191
+
192
+ def save(self, path: str | None = None, verbose: bool = True) -> str:
252
193
  if path is None:
253
194
  path = self.path
254
195
 
@@ -263,35 +204,30 @@ class AbstractTimeSeriesModel(ModelBase):
263
204
  self._oof_predictions = None
264
205
 
265
206
  file_path = os.path.join(path, self.model_file_name)
266
- _model = self.model
267
207
  save_pkl.save(path=file_path, object=self, verbose=verbose)
268
- self.model = _model
269
208
 
270
209
  self._oof_predictions = oof_predictions
271
210
  return path
272
211
 
273
212
  @classmethod
274
- def load(cls, path: str, reset_paths: bool = True, load_oof: bool = False, verbose: bool = True) -> Self: # type: ignore
213
+ def load(cls, path: str, reset_paths: bool = True, load_oof: bool = False, verbose: bool = True) -> Self:
275
214
  file_path = os.path.join(path, cls.model_file_name)
276
215
  model = load_pkl.load(path=file_path, verbose=verbose)
277
216
  if reset_paths:
278
217
  model.set_contexts(path)
279
- if hasattr(model, "_compiler"):
280
- if model._compiler is not None and not model._compiler.save_in_pkl:
281
- model.model = model._compiler.load(path=path)
282
218
  if load_oof and model._oof_predictions is None:
283
219
  model._oof_predictions = cls.load_oof_predictions(path=path, verbose=verbose)
284
220
  return model
285
221
 
286
222
  @classmethod
287
- def load_oof_predictions(cls, path: str, verbose: bool = True) -> List[TimeSeriesDataFrame]:
223
+ def load_oof_predictions(cls, path: str, verbose: bool = True) -> list[TimeSeriesDataFrame]:
288
224
  """Load the cached OOF predictions from disk."""
289
225
  return load_pkl.load(path=os.path.join(path, "utils", cls._oof_filename), verbose=verbose)
290
226
 
291
227
  @property
292
228
  def supports_known_covariates(self) -> bool:
293
229
  return (
294
- self._get_model_params().get("covariate_regressor") is not None
230
+ self.get_hyperparameters().get("covariate_regressor") is not None
295
231
  or self.__class__._supports_known_covariates
296
232
  )
297
233
 
@@ -302,7 +238,8 @@ class AbstractTimeSeriesModel(ModelBase):
302
238
  @property
303
239
  def supports_static_features(self) -> bool:
304
240
  return (
305
- self._get_model_params().get("covariate_regressor") is not None or self.__class__._supports_static_features
241
+ self.get_hyperparameters().get("covariate_regressor") is not None
242
+ or self.__class__._supports_static_features
306
243
  )
307
244
 
308
245
  def get_oof_predictions(self):
@@ -310,61 +247,99 @@ class AbstractTimeSeriesModel(ModelBase):
310
247
  self._oof_predictions = self.load_oof_predictions(self.path)
311
248
  return self._oof_predictions
312
249
 
313
- def _get_default_auxiliary_params(self) -> dict:
314
- return dict(
315
- # ratio of given time_limit to use during fit(). If time_limit == 10 and max_time_limit_ratio=0.3,
316
- # time_limit would be changed to 3.
317
- max_time_limit_ratio=self.default_max_time_limit_ratio,
318
- # max time_limit value during fit(). If the provided time_limit is greater than this value, it will be
319
- # replaced by max_time_limit. Occurs after max_time_limit_ratio is applied.
320
- max_time_limit=None,
321
- )
322
-
323
- # TODO: remove
324
- @classmethod
325
- def _get_default_ag_args(cls) -> dict:
250
+ def _get_default_hyperparameters(self) -> dict:
326
251
  return {}
327
252
 
328
- def _init_params(self):
329
- """Initializes model hyperparameters"""
330
- hyperparameters = self._user_params
331
- self.nondefault_params = []
332
- if hyperparameters is not None:
333
- self.params.update(hyperparameters)
334
- # These are hyperparameters that user has specified.
335
- self.nondefault_params = list(hyperparameters.keys())[:]
336
- self.params_trained = {}
337
-
338
- def _init_params_aux(self):
253
+ def get_hyperparameters(self) -> dict:
254
+ """Get dictionary of hyperparameters that will be passed to the "inner model" that AutoGluon wraps."""
255
+ return {**self._get_default_hyperparameters(), **self._hyperparameters}
256
+
257
+ def get_hyperparameter(self, key: str) -> Any:
258
+ """Get a single hyperparameter value for the "inner model"."""
259
+ return self.get_hyperparameters()[key]
260
+
261
+ def get_info(self) -> dict:
339
262
  """
340
- Initializes auxiliary hyperparameters.
341
- These parameters are generally not model specific and can have a wide variety of effects.
342
- For documentation on some of the available options and their defaults, refer to `self._get_default_auxiliary_params`.
263
+ Returns a dictionary of numerous fields describing the model.
343
264
  """
344
- hyperparameters_aux = self._user_params_aux or {}
345
- self.params_aux = {**self._get_default_auxiliary_params(), **hyperparameters_aux}
265
+ info = {
266
+ "name": self.name,
267
+ "model_type": type(self).__name__,
268
+ "eval_metric": self.eval_metric,
269
+ "fit_time": self.fit_time,
270
+ "predict_time": self.predict_time,
271
+ "freq": self.freq,
272
+ "prediction_length": self.prediction_length,
273
+ "quantile_levels": self.quantile_levels,
274
+ "val_score": self.val_score,
275
+ "hyperparameters": self.get_hyperparameters(),
276
+ "covariate_metadata": self.covariate_metadata.to_dict(),
277
+ }
278
+ return info
346
279
 
347
- def initialize(self) -> None:
348
- if not self._is_initialized:
349
- self._init_params_aux()
350
- self._init_params()
351
- self._initialize_transforms()
352
- self._is_initialized = True
280
+ @classmethod
281
+ def load_info(cls, path: str, load_model_if_required: bool = True) -> dict:
282
+ # TODO: remove?
283
+ load_path = os.path.join(path, cls.model_info_name)
284
+ try:
285
+ return load_pkl.load(path=load_path)
286
+ except:
287
+ if load_model_if_required:
288
+ model = cls.load(path=path, reset_paths=True)
289
+ return model.get_info()
290
+ else:
291
+ raise
353
292
 
354
- def _initialize_transforms(self) -> None:
355
- self.target_scaler = self._create_target_scaler()
356
- self.covariate_scaler = self._create_covariate_scaler()
357
- self.covariate_regressor = self._create_covariate_regressor()
293
+ def _is_gpu_available(self) -> bool:
294
+ return False
358
295
 
359
- def _get_model_params(self) -> dict:
360
- return self.params.copy()
296
+ @staticmethod
297
+ def _get_system_resources() -> dict[str, Any]:
298
+ resource_manager = get_resource_manager()
299
+ system_num_cpus = resource_manager.get_cpu_count()
300
+ system_num_gpus = resource_manager.get_gpu_count()
301
+ return {
302
+ "num_cpus": system_num_cpus,
303
+ "num_gpus": system_num_gpus,
304
+ }
305
+
306
+ def _get_model_base(self) -> Self:
307
+ return self
308
+
309
+ def persist(self) -> Self:
310
+ """Ask the model to persist its assets in memory, i.e., to predict with low latency. In practice
311
+ this is used for pretrained models that have to lazy-load model parameters to device memory at
312
+ prediction time.
313
+ """
314
+ return self
315
+
316
+ def _more_tags(self) -> dict:
317
+ """Encode model properties using tags, similar to sklearn & autogluon.tabular.
318
+
319
+ For more details, see `autogluon.core.models.abstract.AbstractModel._get_tags()` and
320
+ https://scikit-learn.org/stable/_sources/developers/develop.rst.txt.
321
+
322
+ List of currently supported tags:
323
+ - allow_nan: Can the model handle data with missing values represented by np.nan?
324
+ - can_refit_full: Does it make sense to retrain the model without validation data?
325
+ See `autogluon.core.models.abstract._tags._DEFAULT_TAGS` for more details.
326
+ - can_use_train_data: Can the model use train_data if it's provided to model.fit()?
327
+ - can_use_val_data: Can the model use val_data if it's provided to model.fit()?
328
+ """
329
+ return {
330
+ "allow_nan": False,
331
+ "can_refit_full": False,
332
+ "can_use_train_data": True,
333
+ "can_use_val_data": False,
334
+ }
361
335
 
362
336
  def get_params(self) -> dict:
363
- # TODO: do not extract to AbstractModel if this is only used for getting a
364
- # prototype of the object for HPO.
365
- hyperparameters = self._user_params.copy()
366
- if self._user_params_aux:
367
- hyperparameters[AG_ARGS_FIT] = self._user_params_aux.copy()
337
+ """Get the constructor parameters required for cloning this model object"""
338
+ # We only use the user-provided hyperparameters for cloning. We cannot use the output of get_hyperparameters()
339
+ # since it may contain search spaces that won't be converted to concrete values during HPO
340
+ hyperparameters = self._hyperparameters.copy()
341
+ if self._extra_ag_args:
342
+ hyperparameters[AG_ARGS_FIT] = self._extra_ag_args.copy()
368
343
 
369
344
  return dict(
370
345
  path=self.path_root,
@@ -374,47 +349,105 @@ class AbstractTimeSeriesModel(ModelBase):
374
349
  freq=self.freq,
375
350
  prediction_length=self.prediction_length,
376
351
  quantile_levels=self.quantile_levels,
377
- metadata=self.metadata,
352
+ covariate_metadata=self.covariate_metadata,
378
353
  target=self.target,
379
354
  )
380
355
 
381
- @classmethod
382
- def load_info(cls, path: str, load_model_if_required: bool = True) -> dict:
383
- # TODO: remove?
384
- load_path = os.path.join(path, cls.model_info_name)
385
- try:
386
- return load_pkl.load(path=load_path)
387
- except:
388
- if load_model_if_required:
389
- model = cls.load(path=path, reset_paths=True)
390
- return model.get_info()
391
- else:
392
- raise
356
+ def convert_to_refit_full_via_copy(self) -> Self:
357
+ # save the model as a new model on disk
358
+ previous_name = self.name
359
+ self.rename(self.name + REFIT_FULL_SUFFIX)
360
+ refit_model_path = self.path
361
+ self.save(path=self.path, verbose=False)
393
362
 
394
- def get_info(self) -> dict:
395
- """
396
- Returns a dictionary of numerous fields describing the model.
397
- """
398
- # TODO: Include self.metadata
399
- info = {
400
- "name": self.name,
401
- "model_type": type(self).__name__,
402
- "eval_metric": self.eval_metric,
403
- "fit_time": self.fit_time,
404
- "predict_time": self.predict_time,
405
- "freq": self.freq,
406
- "prediction_length": self.prediction_length,
407
- "quantile_levels": self.quantile_levels,
408
- "val_score": self.val_score,
409
- "hyperparameters": self.params,
410
- }
411
- return info
363
+ self.rename(previous_name)
364
+
365
+ refit_model = self.load(path=refit_model_path, verbose=False)
366
+ refit_model.val_score = None
367
+ refit_model.predict_time = None
368
+
369
+ return refit_model
370
+
371
+ def convert_to_refit_full_template(self) -> Self:
372
+ """After calling this function, returned model should be able to be fit without `val_data`."""
373
+ params = copy.deepcopy(self.get_params())
374
+
375
+ # Remove 0.5 from quantile_levels so that the cloned model sets its must_drop_median correctly
376
+ if self.must_drop_median:
377
+ params["quantile_levels"].remove(0.5)
378
+
379
+ if "hyperparameters" not in params:
380
+ params["hyperparameters"] = dict()
381
+
382
+ if AG_ARGS_FIT not in params["hyperparameters"]:
383
+ params["hyperparameters"][AG_ARGS_FIT] = dict()
384
+
385
+ params["name"] = params["name"] + REFIT_FULL_SUFFIX
386
+ template = self.__class__(**params)
387
+
388
+ return template
389
+
390
+
391
+ class AbstractTimeSeriesModel(TimeSeriesModelBase, TimeSeriesTunable, metaclass=ModelRegistry):
392
+ """Abstract base class for all time series models that take historical data as input and
393
+ make predictions for the forecast horizon.
394
+ """
395
+
396
+ ag_priority: int = 0
397
+
398
+ def __init__(
399
+ self,
400
+ path: str | None = None,
401
+ name: str | None = None,
402
+ hyperparameters: dict[str, Any] | None = None,
403
+ freq: str | None = None,
404
+ prediction_length: int = 1,
405
+ covariate_metadata: CovariateMetadata | None = None,
406
+ target: str = "target",
407
+ quantile_levels: Sequence[float] = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
408
+ eval_metric: str | TimeSeriesScorer | None = None,
409
+ ):
410
+ # TODO: make freq a required argument in AbstractTimeSeriesModel
411
+ super().__init__(
412
+ path=path,
413
+ name=name,
414
+ hyperparameters=hyperparameters,
415
+ freq=freq,
416
+ prediction_length=prediction_length,
417
+ covariate_metadata=covariate_metadata,
418
+ target=target,
419
+ quantile_levels=quantile_levels,
420
+ eval_metric=eval_metric,
421
+ )
422
+ self.target_scaler: TargetScaler | None
423
+ self.covariate_scaler: CovariateScaler | None
424
+ self.covariate_regressor: CovariateRegressor | None
425
+
426
+ def _initialize_transforms_and_regressor(self) -> None:
427
+ self.target_scaler = get_target_scaler(self.get_hyperparameters().get("target_scaler"), target=self.target)
428
+ self.covariate_scaler = get_covariate_scaler(
429
+ self.get_hyperparameters().get("covariate_scaler"),
430
+ covariate_metadata=self.covariate_metadata,
431
+ use_static_features=self.supports_static_features,
432
+ use_known_covariates=self.supports_known_covariates,
433
+ use_past_covariates=self.supports_past_covariates,
434
+ )
435
+ self.covariate_regressor = get_covariate_regressor(
436
+ self.get_hyperparameters().get("covariate_regressor"),
437
+ target=self.target,
438
+ covariate_metadata=self.covariate_metadata,
439
+ )
440
+
441
+ @property
442
+ def allowed_hyperparameters(self) -> list[str]:
443
+ """List of hyperparameters allowed by the model."""
444
+ return ["target_scaler", "covariate_regressor", "covariate_scaler"]
412
445
 
413
- def fit( # type: ignore
446
+ def fit(
414
447
  self,
415
448
  train_data: TimeSeriesDataFrame,
416
- val_data: Optional[TimeSeriesDataFrame] = None,
417
- time_limit: Optional[float] = None,
449
+ val_data: TimeSeriesDataFrame | None = None,
450
+ time_limit: float | None = None,
418
451
  verbosity: int = 2,
419
452
  **kwargs,
420
453
  ) -> Self:
@@ -423,36 +456,36 @@ class AbstractTimeSeriesModel(ModelBase):
423
456
  Models should not override the `fit` method, but instead override the `_fit` method which
424
457
  has the same arguments.
425
458
 
426
- Other Parameters
427
- ----------------
428
- train_data : TimeSeriesDataFrame
459
+ Parameters
460
+ ----------
461
+ train_data
429
462
  The training data provided in the library's `autogluon.timeseries.dataset.TimeSeriesDataFrame`
430
463
  format.
431
- val_data : TimeSeriesDataFrame, optional
464
+ val_data
432
465
  The validation data set in the same format as training data.
433
- time_limit : float, default = None
466
+ time_limit
434
467
  Time limit in seconds to adhere to when fitting model.
435
468
  Ideally, model should early stop during fit to avoid going over the time limit if specified.
436
- num_cpus : int, default = 'auto'
469
+ num_cpus
437
470
  How many CPUs to use during fit.
438
471
  This is counted in virtual cores, not in physical cores.
439
472
  If 'auto', model decides.
440
- num_gpus : int, default = 'auto'
473
+ num_gpus
441
474
  How many GPUs to use during fit.
442
475
  If 'auto', model decides.
443
- verbosity : int, default = 2
476
+ verbosity
444
477
  Verbosity levels range from 0 to 4 and control how much information is printed.
445
478
  Higher levels correspond to more detailed print statements (you can set verbosity = 0 to suppress warnings).
446
- **kwargs :
479
+ **kwargs
447
480
  Any additional fit arguments a model supports.
448
481
 
449
482
  Returns
450
483
  -------
451
- model: AbstractTimeSeriesModel
484
+ model
452
485
  The fitted model object
453
486
  """
454
487
  start_time = time.monotonic()
455
- self.initialize()
488
+ self._initialize_transforms_and_regressor()
456
489
 
457
490
  if self.target_scaler is not None:
458
491
  train_data = self.target_scaler.fit_transform(train_data)
@@ -467,7 +500,7 @@ class AbstractTimeSeriesModel(ModelBase):
467
500
  self.covariate_regressor.fit(
468
501
  train_data,
469
502
  time_limit=covariate_regressor_time_limit,
470
- verbosity=verbosity,
503
+ verbosity=verbosity - 1,
471
504
  )
472
505
 
473
506
  if self._get_tags()["can_use_train_data"]:
@@ -503,35 +536,14 @@ class AbstractTimeSeriesModel(ModelBase):
503
536
 
504
537
  return self
505
538
 
506
- def _preprocess_time_limit(self, time_limit: float) -> float:
507
- original_time_limit = time_limit
508
- max_time_limit_ratio = self.params_aux["max_time_limit_ratio"]
509
- max_time_limit = self.params_aux["max_time_limit"]
510
-
511
- time_limit *= max_time_limit_ratio
512
-
513
- if max_time_limit is not None:
514
- time_limit = min(time_limit, max_time_limit)
515
-
516
- if original_time_limit != time_limit:
517
- time_limit_og_str = f"{original_time_limit:.2f}s" if original_time_limit is not None else "None"
518
- time_limit_str = f"{time_limit:.2f}s" if time_limit is not None else "None"
519
- logger.debug(
520
- f"\tTime limit adjusted due to model hyperparameters: "
521
- f"{time_limit_og_str} -> {time_limit_str} "
522
- f"(ag.max_time_limit={max_time_limit}, "
523
- f"ag.max_time_limit_ratio={max_time_limit_ratio}"
524
- )
525
-
526
- return time_limit
527
-
528
- def _fit( # type: ignore
539
+ @abstractmethod
540
+ def _fit(
529
541
  self,
530
542
  train_data: TimeSeriesDataFrame,
531
- val_data: Optional[TimeSeriesDataFrame] = None,
532
- time_limit: Optional[float] = None,
533
- num_cpus: Optional[int] = None,
534
- num_gpus: Optional[int] = None,
543
+ val_data: TimeSeriesDataFrame | None = None,
544
+ time_limit: float | None = None,
545
+ num_cpus: int | None = None,
546
+ num_gpus: int | None = None,
535
547
  verbosity: int = 2,
536
548
  **kwargs,
537
549
  ) -> None:
@@ -539,78 +551,36 @@ class AbstractTimeSeriesModel(ModelBase):
539
551
  the model training logic, `fit` additionally implements other logic such as keeping
540
552
  track of the time limit, etc.
541
553
  """
542
- # TODO: Make the models respect `num_cpus` and `num_gpus` parameters
543
- raise NotImplementedError
554
+ pass
544
555
 
545
- # TODO: perform this check inside fit() ?
556
+ # TODO: this check cannot be moved inside fit because of the complex way in which
557
+ # MultiWindowBacktestingModel handles hyperparameter spaces during initialization.
558
+ # Move inside fit() after refactoring MultiWindowBacktestingModel
546
559
  def _check_fit_params(self):
547
560
  # gracefully handle hyperparameter specifications if they are provided to fit instead
548
- if any(isinstance(v, space.Space) for v in self.params.values()):
561
+ if any(isinstance(v, space.Space) for v in self.get_hyperparameters().values()):
549
562
  raise ValueError(
550
563
  "Hyperparameter spaces provided to `fit`. Please provide concrete values "
551
564
  "as hyperparameters when initializing or use `hyperparameter_tune` instead."
552
565
  )
553
566
 
554
- @property
555
- def allowed_hyperparameters(self) -> List[str]:
556
- """List of hyperparameters allowed by the model."""
557
- return ["target_scaler", "covariate_regressor"]
558
-
559
- def _create_target_scaler(self) -> Optional[LocalTargetScaler]:
560
- """Create a LocalTargetScaler object based on the value of the `target_scaler` hyperparameter."""
561
- # TODO: Add support for custom target transforms (e.g., Box-Cox, log1p, ...)
562
- target_scaler_type = self._get_model_params().get("target_scaler")
563
- if target_scaler_type is not None:
564
- return get_target_scaler_from_name(target_scaler_type, target=self.target)
565
- else:
566
- return None
567
-
568
- def _create_covariate_scaler(self) -> Optional[CovariateScaler]:
569
- """Create a CovariateScaler object based on the value of the `covariate_scaler` hyperparameter."""
570
- covariate_scaler_type = self._get_model_params().get("covariate_scaler")
571
- if covariate_scaler_type is not None:
572
- return get_covariate_scaler_from_name(
573
- covariate_scaler_type,
574
- metadata=self.metadata,
575
- use_static_features=self.supports_static_features,
576
- use_known_covariates=self.supports_known_covariates,
577
- use_past_covariates=self.supports_past_covariates,
567
+ def _log_unused_hyperparameters(self, extra_allowed_hyperparameters: list[str] | None = None) -> None:
568
+ """Log a warning if unused hyperparameters were provided to the model."""
569
+ allowed_hyperparameters = self.allowed_hyperparameters
570
+ if extra_allowed_hyperparameters is not None:
571
+ allowed_hyperparameters = allowed_hyperparameters + extra_allowed_hyperparameters
572
+
573
+ unused_hyperparameters = [key for key in self.get_hyperparameters() if key not in allowed_hyperparameters]
574
+ if len(unused_hyperparameters) > 0:
575
+ logger.warning(
576
+ f"{self.name} ignores following hyperparameters: {unused_hyperparameters}. "
577
+ f"See the documentation for {self.name} for the list of supported hyperparameters."
578
578
  )
579
- else:
580
- return None
581
-
582
- def _create_covariate_regressor(self) -> Optional[CovariateRegressor]:
583
- """Create a CovariateRegressor object based on the value of the `covariate_regressor` hyperparameter."""
584
- covariate_regressor = self._get_model_params().get("covariate_regressor")
585
- if covariate_regressor is not None:
586
- if len(self.metadata.known_covariates + self.metadata.static_features) == 0:
587
- logger.info(
588
- "\tSkipping covariate_regressor since the dataset contains no covariates or static features."
589
- )
590
- return None
591
- else:
592
- if isinstance(covariate_regressor, str):
593
- return CovariateRegressor(covariate_regressor, target=self.target, metadata=self.metadata)
594
- elif isinstance(covariate_regressor, dict):
595
- return CovariateRegressor(**covariate_regressor, target=self.target, metadata=self.metadata)
596
- elif isinstance(covariate_regressor, CovariateRegressor):
597
- logger.warning(
598
- "\tUsing a custom covariate_regressor is experimental functionality that may break in the future!"
599
- )
600
- covariate_regressor.target = self.target
601
- covariate_regressor.metadata = self.metadata
602
- return covariate_regressor
603
- else:
604
- raise ValueError(
605
- f"Invalid value for covariate_regressor {covariate_regressor} of type {type(covariate_regressor)}"
606
- )
607
- else:
608
- return None
609
579
 
610
- def predict( # type: ignore
580
+ def predict(
611
581
  self,
612
- data: Union[TimeSeriesDataFrame, Dict[str, Optional[TimeSeriesDataFrame]]],
613
- known_covariates: Optional[TimeSeriesDataFrame] = None,
582
+ data: TimeSeriesDataFrame,
583
+ known_covariates: TimeSeriesDataFrame | None = None,
614
584
  **kwargs,
615
585
  ) -> TimeSeriesDataFrame:
616
586
  """Given a dataset, predict the next `self.prediction_length` time steps.
@@ -622,22 +592,19 @@ class AbstractTimeSeriesModel(ModelBase):
622
592
 
623
593
  Parameters
624
594
  ----------
625
- data: Union[TimeSeriesDataFrame, Dict[str, Optional[TimeSeriesDataFrame]]]
595
+ data
626
596
  The dataset where each time series is the "context" for predictions. For ensemble models that depend on
627
597
  the predictions of other models, this method may accept a dictionary of previous models' predictions.
628
- known_covariates : Optional[TimeSeriesDataFrame]
598
+ known_covariates
629
599
  A TimeSeriesDataFrame containing the values of the known covariates during the forecast horizon.
630
600
 
631
601
  Returns
632
602
  -------
633
- predictions: TimeSeriesDataFrame
634
- pandas data frames with a timestamp index, where each input item from the input
603
+ predictions
604
+ pandas dataframes with a timestamp index, where each input item from the input
635
605
  data is given as a separate forecast item in the dictionary, keyed by the `item_id`s
636
606
  of input items.
637
607
  """
638
- # TODO: the method signature is not aligned with the model interface in general as it allows dict
639
- assert isinstance(data, TimeSeriesDataFrame)
640
-
641
608
  if self.target_scaler is not None:
642
609
  data = self.target_scaler.fit_transform(data)
643
610
  if self.covariate_scaler is not None:
@@ -655,7 +622,13 @@ class AbstractTimeSeriesModel(ModelBase):
655
622
  predictions = self._predict(data=data, known_covariates=known_covariates, **kwargs)
656
623
  self.covariate_regressor = covariate_regressor
657
624
 
658
- # "0.5" might be missing from the quantiles if self is a wrapper (MultiWindowBacktestingModel or ensemble)
625
+ # Ensure that 'mean' is the leading column. Trailing columns might not match quantile_levels if self is
626
+ # a MultiWindowBacktestingModel and base_model.must_drop_median=True
627
+ column_order = pd.Index(["mean"] + [col for col in predictions.columns if col != "mean"])
628
+ if not predictions.columns.equals(column_order):
629
+ predictions = predictions.reindex(columns=column_order)
630
+
631
+ # "0.5" might be missing from the quantiles if self is a MultiWindowBacktestingModel
659
632
  if "0.5" in predictions.columns:
660
633
  if self.eval_metric.optimized_by_median:
661
634
  predictions["mean"] = predictions["0.5"]
@@ -680,34 +653,62 @@ class AbstractTimeSeriesModel(ModelBase):
680
653
 
681
654
  def get_forecast_horizon_index(self, data: TimeSeriesDataFrame) -> pd.MultiIndex:
682
655
  """For each item in the dataframe, get timestamps for the next `prediction_length` time steps into the future."""
683
- return get_forecast_horizon_index_ts_dataframe(data, prediction_length=self.prediction_length, freq=self.freq)
656
+ return pd.MultiIndex.from_frame(
657
+ make_future_data_frame(data, prediction_length=self.prediction_length, freq=self.freq)
658
+ )
684
659
 
660
+ @abstractmethod
685
661
  def _predict(
686
662
  self,
687
- data: Union[TimeSeriesDataFrame, Dict[str, TimeSeriesDataFrame]],
688
- known_covariates: Optional[TimeSeriesDataFrame] = None,
663
+ data: TimeSeriesDataFrame,
664
+ known_covariates: TimeSeriesDataFrame | None = None,
689
665
  **kwargs,
690
666
  ) -> TimeSeriesDataFrame:
691
667
  """Private method for `predict`. See `predict` for documentation of arguments."""
692
- raise NotImplementedError
668
+ pass
669
+
670
+ def _preprocess_time_limit(self, time_limit: float) -> float:
671
+ original_time_limit = time_limit
672
+ max_time_limit_ratio = self._extra_ag_args.get("max_time_limit_ratio", self.default_max_time_limit_ratio)
673
+ max_time_limit = self._extra_ag_args.get("max_time_limit")
674
+
675
+ time_limit *= max_time_limit_ratio
676
+
677
+ if max_time_limit is not None:
678
+ time_limit = min(time_limit, max_time_limit)
679
+
680
+ if original_time_limit != time_limit:
681
+ time_limit_og_str = f"{original_time_limit:.2f}s" if original_time_limit is not None else "None"
682
+ time_limit_str = f"{time_limit:.2f}s" if time_limit is not None else "None"
683
+ logger.debug(
684
+ f"\tTime limit adjusted due to model hyperparameters: "
685
+ f"{time_limit_og_str} -> {time_limit_str} "
686
+ f"(ag.max_time_limit={max_time_limit}, "
687
+ f"ag.max_time_limit_ratio={max_time_limit_ratio}"
688
+ )
689
+
690
+ return time_limit
691
+
692
+ def _get_search_space(self):
693
+ """Sets up default search space for HPO. Each hyperparameter which user did not specify is converted from
694
+ default fixed value to default search space.
695
+ """
696
+ params = self._hyperparameters.copy()
697
+ return params
693
698
 
694
699
  def _score_with_predictions(
695
700
  self,
696
701
  data: TimeSeriesDataFrame,
697
702
  predictions: TimeSeriesDataFrame,
698
- metric: Optional[str] = None,
699
703
  ) -> float:
700
704
  """Compute the score measuring how well the predictions align with the data."""
701
- eval_metric = self.eval_metric if metric is None else check_get_evaluation_metric(metric)
702
- return eval_metric.score(
705
+ return self.eval_metric.score(
703
706
  data=data,
704
707
  predictions=predictions,
705
- prediction_length=self.prediction_length,
706
708
  target=self.target,
707
- seasonal_period=self.eval_metric_seasonal_period,
708
709
  )
709
710
 
710
- def score(self, data: TimeSeriesDataFrame, metric: Optional[str] = None) -> float: # type: ignore
711
+ def score(self, data: TimeSeriesDataFrame) -> float:
711
712
  """Return the evaluation scores for given metric and dataset. The last
712
713
  `self.prediction_length` time steps of each time series in the input data set
713
714
  will be held out and used for computing the evaluation score. Time series
@@ -715,31 +716,20 @@ class AbstractTimeSeriesModel(ModelBase):
715
716
 
716
717
  Parameters
717
718
  ----------
718
- data: TimeSeriesDataFrame
719
+ data
719
720
  Dataset used for scoring.
720
- metric: str
721
- String identifier of evaluation metric to use, from one of
722
- `autogluon.timeseries.utils.metric_utils.AVAILABLE_METRICS`.
723
-
724
- Other Parameters
725
- ----------------
726
- num_samples: int
727
- Number of samples to use for making evaluation predictions if the probabilistic
728
- forecasts are generated by forward sampling from the fitted model.
729
721
 
730
722
  Returns
731
723
  -------
732
- score: float
724
+ score
733
725
  The computed forecast evaluation score on the last `self.prediction_length`
734
726
  time steps of each time series.
735
727
  """
736
- # TODO: align method signature in the new AbstractModel
737
-
738
728
  past_data, known_covariates = data.get_model_inputs_for_scoring(
739
- prediction_length=self.prediction_length, known_covariates_names=self.metadata.known_covariates
729
+ prediction_length=self.prediction_length, known_covariates_names=self.covariate_metadata.known_covariates
740
730
  )
741
731
  predictions = self.predict(past_data, known_covariates=known_covariates)
742
- return self._score_with_predictions(data=data, predictions=predictions, metric=metric)
732
+ return self._score_with_predictions(data=data, predictions=predictions)
743
733
 
744
734
  def score_and_cache_oof(
745
735
  self,
@@ -750,243 +740,22 @@ class AbstractTimeSeriesModel(ModelBase):
750
740
  ) -> None:
751
741
  """Compute val_score, predict_time and cache out-of-fold (OOF) predictions."""
752
742
  past_data, known_covariates = val_data.get_model_inputs_for_scoring(
753
- prediction_length=self.prediction_length, known_covariates_names=self.metadata.known_covariates
743
+ prediction_length=self.prediction_length, known_covariates_names=self.covariate_metadata.known_covariates
754
744
  )
755
745
  predict_start_time = time.time()
756
746
  oof_predictions = self.predict(past_data, known_covariates=known_covariates, **predict_kwargs)
757
- self._oof_predictions = [oof_predictions]
747
+ self.cache_oof_predictions(oof_predictions)
758
748
  if store_predict_time:
759
749
  self.predict_time = time.time() - predict_start_time
760
750
  if store_val_score:
761
751
  self.val_score = self._score_with_predictions(val_data, oof_predictions)
762
752
 
763
- def _get_hpo_train_fn_kwargs(self, **train_fn_kwargs) -> dict:
764
- """Update kwargs passed to model_trial depending on the model configuration.
765
-
766
- These kwargs need to be updated, for example, by MultiWindowBacktestingModel.
767
- """
768
- return train_fn_kwargs
769
-
770
- def _is_gpu_available(self) -> bool:
771
- return False
772
-
773
- @staticmethod
774
- def _get_system_resources() -> Dict[str, Any]:
775
- resource_manager = get_resource_manager()
776
- system_num_cpus = resource_manager.get_cpu_count()
777
- system_num_gpus = resource_manager.get_gpu_count()
778
- return {
779
- "num_cpus": system_num_cpus,
780
- "num_gpus": system_num_gpus,
781
- }
782
-
783
- def hyperparameter_tune(
784
- self,
785
- train_data: TimeSeriesDataFrame,
786
- val_data: Optional[TimeSeriesDataFrame],
787
- val_splitter: Any = None,
788
- default_num_trials: Optional[int] = 1,
789
- refit_every_n_windows: Optional[int] = 1,
790
- hyperparameter_tune_kwargs: Union[str, dict] = "auto",
791
- time_limit: Optional[float] = None,
792
- ) -> Tuple[Dict[str, Any], Any]:
793
- hpo_executor = self._get_default_hpo_executor()
794
- hpo_executor.initialize(
795
- hyperparameter_tune_kwargs, default_num_trials=default_num_trials, time_limit=time_limit
796
- )
797
-
798
- self.initialize()
799
-
800
- # we use k_fold=1 to circumvent autogluon.core logic to manage resources during parallelization
801
- # of different folds
802
- # FIXME: we pass in self which currently does not inherit from AbstractModel
803
- hpo_executor.register_resources(self, k_fold=1, **self._get_system_resources()) # type: ignore
804
-
805
- time_start = time.time()
806
- logger.debug(f"\tStarting hyperparameter tuning for {self.name}")
807
- search_space = self._get_search_space()
808
-
809
- try:
810
- hpo_executor.validate_search_space(search_space, self.name)
811
- except EmptySearchSpace:
812
- return skip_hpo(self, train_data, val_data, time_limit=hpo_executor.time_limit)
813
-
814
- train_path, val_path = self._save_with_data(train_data, val_data)
815
-
816
- train_fn_kwargs = self._get_hpo_train_fn_kwargs(
817
- model_cls=self.__class__,
818
- init_params=self.get_params(),
819
- time_start=time_start,
820
- time_limit=hpo_executor.time_limit,
821
- fit_kwargs=dict(
822
- val_splitter=val_splitter,
823
- refit_every_n_windows=refit_every_n_windows,
824
- ),
825
- train_path=train_path,
826
- val_path=val_path,
827
- hpo_executor=hpo_executor,
828
- )
829
-
830
- minimum_resources = self.get_minimum_resources(is_gpu_available=self._is_gpu_available())
831
- hpo_context = disable_stdout if isinstance(hpo_executor, RayHpoExecutor) else nullcontext
832
-
833
- minimum_cpu_per_trial = minimum_resources.get("num_cpus", 1)
834
- if not isinstance(minimum_cpu_per_trial, int):
835
- logger.warning(
836
- f"Minimum number of CPUs per trial for {self.name} is not an integer. "
837
- f"Setting to 1. Minimum number of CPUs per trial: {minimum_cpu_per_trial}"
838
- )
839
- minimum_cpu_per_trial = 1
840
-
841
- with hpo_context(), warning_filter(): # prevent Ray from outputting its results to stdout with print
842
- hpo_executor.execute(
843
- model_trial=model_trial,
844
- train_fn_kwargs=train_fn_kwargs,
845
- directory=self.path,
846
- minimum_cpu_per_trial=minimum_cpu_per_trial,
847
- minimum_gpu_per_trial=minimum_resources.get("num_gpus", 0),
848
- model_estimate_memory_usage=None,
849
- adapter_type="timeseries",
850
- )
851
-
852
- assert self.path_root is not None
853
- hpo_models, analysis = hpo_executor.get_hpo_results(
854
- model_name=self.name,
855
- model_path_root=self.path_root,
856
- time_start=time_start,
857
- )
858
-
859
- return hpo_models, analysis
860
-
861
- @property
862
- def is_ensemble(self) -> bool:
863
- """Return True if the model is an ensemble model or a container of multiple models."""
864
- return self._get_model_base() is self
865
-
866
- def _get_default_hpo_executor(self) -> HpoExecutor:
867
- backend = (
868
- self._get_model_base()._get_hpo_backend()
869
- ) # If ensemble, will use the base model to determine backend
870
- if backend == RAY_BACKEND:
871
- try:
872
- try_import_ray()
873
- except Exception as e:
874
- warning_msg = f"Will use custom hpo logic because ray import failed. Reason: {str(e)}"
875
- dup_filter.attach_filter_targets(warning_msg)
876
- logger.warning(warning_msg)
877
- backend = CUSTOM_BACKEND
878
- hpo_executor = HpoExecutorFactory.get_hpo_executor(backend)() # type: ignore
879
- return hpo_executor
880
-
881
- def _get_model_base(self) -> AbstractTimeSeriesModel:
882
- return self
883
-
884
- def _get_hpo_backend(self) -> str:
885
- """Choose which backend("ray" or "custom") to use for hpo"""
886
- if DistributedContext.is_distributed_mode():
887
- return RAY_BACKEND
888
- return CUSTOM_BACKEND
889
-
890
- def _get_search_space(self):
891
- """Sets up default search space for HPO. Each hyperparameter which user did not specify is converted from
892
- default fixed value to default search space.
893
- """
894
- params = self.params.copy()
895
- return params
896
-
897
- def _save_with_data(self, train_data, val_data):
898
- self.set_contexts(os.path.abspath(self.path))
899
- dataset_train_filename = "dataset_train.pkl"
900
- train_path = os.path.join(self.path, dataset_train_filename)
901
- save_pkl.save(path=train_path, object=train_data)
902
-
903
- dataset_val_filename = "dataset_val.pkl"
904
- val_path = os.path.join(self.path, dataset_val_filename)
905
- save_pkl.save(path=val_path, object=val_data)
906
- return train_path, val_path
907
-
908
- def preprocess( # type: ignore
753
+ def preprocess(
909
754
  self,
910
755
  data: TimeSeriesDataFrame,
911
- known_covariates: Optional[TimeSeriesDataFrame] = None,
756
+ known_covariates: TimeSeriesDataFrame | None = None,
912
757
  is_train: bool = False,
913
758
  **kwargs,
914
- ) -> Tuple[TimeSeriesDataFrame, Optional[TimeSeriesDataFrame]]:
759
+ ) -> tuple[TimeSeriesDataFrame, TimeSeriesDataFrame | None]:
915
760
  """Method that implements model-specific preprocessing logic."""
916
- # TODO: move to new AbstractModel
917
761
  return data, known_covariates
918
-
919
- def persist(self) -> Self:
920
- """Ask the model to persist its assets in memory, i.e., to predict with low latency. In practice
921
- this is used for pretrained models that have to lazy-load model parameters to device memory at
922
- prediction time.
923
- """
924
- return self
925
-
926
- def convert_to_refit_full_via_copy(self) -> Self:
927
- # save the model as a new model on disk
928
- previous_name = self.name
929
- self.rename(self.name + REFIT_FULL_SUFFIX)
930
- refit_model_path = self.path
931
- self.save(path=self.path, verbose=False)
932
-
933
- self.rename(previous_name)
934
-
935
- refit_model = self.load(path=refit_model_path, verbose=False)
936
- refit_model.val_score = None
937
- refit_model.predict_time = None
938
-
939
- return refit_model
940
-
941
- def convert_to_refit_full_template(self):
942
- """
943
- After calling this function, returned model should be able to be fit without X_val, y_val using the iterations trained by the original model.
944
-
945
- Increase max_memory_usage_ratio by 25% to reduce the chance that the refit model will trigger NotEnoughMemoryError and skip training.
946
- This can happen without the 25% increase since the refit model generally will use more training data and thus require more memory.
947
- """
948
- params = copy.deepcopy(self.get_params())
949
-
950
- if "hyperparameters" not in params:
951
- params["hyperparameters"] = dict()
952
-
953
- if AG_ARGS_FIT not in params["hyperparameters"]:
954
- params["hyperparameters"][AG_ARGS_FIT] = dict()
955
-
956
- # TODO: remove
957
- # Increase memory limit by 25% to avoid memory restrictions during fit
958
- params["hyperparameters"][AG_ARGS_FIT]["max_memory_usage_ratio"] = (
959
- params["hyperparameters"][AG_ARGS_FIT].get("max_memory_usage_ratio", 1.0) * 1.25
960
- )
961
-
962
- params["hyperparameters"].update(self.params_trained)
963
- params["name"] = params["name"] + REFIT_FULL_SUFFIX
964
- template = self.__class__(**params)
965
-
966
- return template
967
-
968
- def get_user_params(self) -> dict:
969
- """Used to access user-specified parameters for the model before initialization."""
970
- if self._user_params is None:
971
- return {}
972
- else:
973
- return self._user_params.copy()
974
-
975
- def _more_tags(self) -> dict:
976
- """Encode model properties using tags, similar to sklearn & autogluon.tabular.
977
-
978
- For more details, see `autogluon.core.models.abstract.AbstractModel._get_tags()` and https://scikit-learn.org/stable/_sources/developers/develop.rst.txt.
979
-
980
- List of currently supported tags:
981
- - allow_nan: Can the model handle data with missing values represented by np.nan?
982
- - can_refit_full: Does it make sense to retrain the model without validation data?
983
- See `autogluon.core.models.abstract._tags._DEFAULT_TAGS` for more details.
984
- - can_use_train_data: Can the model use train_data if it's provided to model.fit()?
985
- - can_use_val_data: Can the model use val_data if it's provided to model.fit()?
986
- """
987
- return {
988
- "allow_nan": False,
989
- "can_refit_full": False,
990
- "can_use_train_data": True,
991
- "can_use_val_data": False,
992
- }