autogluon.timeseries 1.0.1b20240304__py3-none-any.whl → 1.4.1b20251210__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 +84 -0
  4. autogluon/timeseries/dataset/ts_dataframe.py +339 -186
  5. autogluon/timeseries/learner.py +192 -60
  6. autogluon/timeseries/metrics/__init__.py +55 -11
  7. autogluon/timeseries/metrics/abstract.py +96 -25
  8. autogluon/timeseries/metrics/point.py +186 -39
  9. autogluon/timeseries/metrics/quantile.py +47 -20
  10. autogluon/timeseries/metrics/utils.py +6 -6
  11. autogluon/timeseries/models/__init__.py +13 -7
  12. autogluon/timeseries/models/abstract/__init__.py +2 -2
  13. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +533 -273
  14. autogluon/timeseries/models/abstract/model_trial.py +10 -10
  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 +369 -215
  18. autogluon/timeseries/models/autogluon_tabular/per_step.py +513 -0
  19. autogluon/timeseries/models/autogluon_tabular/transforms.py +67 -0
  20. autogluon/timeseries/models/autogluon_tabular/utils.py +3 -51
  21. autogluon/timeseries/models/chronos/__init__.py +4 -0
  22. autogluon/timeseries/models/chronos/chronos2.py +361 -0
  23. autogluon/timeseries/models/chronos/model.py +738 -0
  24. autogluon/timeseries/models/chronos/utils.py +369 -0
  25. autogluon/timeseries/models/ensemble/__init__.py +35 -2
  26. autogluon/timeseries/models/ensemble/{abstract_timeseries_ensemble.py → abstract.py} +50 -26
  27. autogluon/timeseries/models/ensemble/array_based/__init__.py +3 -0
  28. autogluon/timeseries/models/ensemble/array_based/abstract.py +236 -0
  29. autogluon/timeseries/models/ensemble/array_based/models.py +73 -0
  30. autogluon/timeseries/models/ensemble/array_based/regressor/__init__.py +12 -0
  31. autogluon/timeseries/models/ensemble/array_based/regressor/abstract.py +88 -0
  32. autogluon/timeseries/models/ensemble/array_based/regressor/linear_stacker.py +167 -0
  33. autogluon/timeseries/models/ensemble/array_based/regressor/per_quantile_tabular.py +94 -0
  34. autogluon/timeseries/models/ensemble/array_based/regressor/tabular.py +107 -0
  35. autogluon/timeseries/models/ensemble/ensemble_selection.py +167 -0
  36. autogluon/timeseries/models/ensemble/per_item_greedy.py +162 -0
  37. autogluon/timeseries/models/ensemble/weighted/__init__.py +8 -0
  38. autogluon/timeseries/models/ensemble/weighted/abstract.py +40 -0
  39. autogluon/timeseries/models/ensemble/weighted/basic.py +78 -0
  40. autogluon/timeseries/models/ensemble/weighted/greedy.py +57 -0
  41. autogluon/timeseries/models/gluonts/__init__.py +3 -1
  42. autogluon/timeseries/models/gluonts/abstract.py +583 -0
  43. autogluon/timeseries/models/gluonts/dataset.py +109 -0
  44. autogluon/timeseries/models/gluonts/{torch/models.py → models.py} +185 -44
  45. autogluon/timeseries/models/local/__init__.py +1 -10
  46. autogluon/timeseries/models/local/abstract_local_model.py +150 -97
  47. autogluon/timeseries/models/local/naive.py +31 -23
  48. autogluon/timeseries/models/local/npts.py +6 -2
  49. autogluon/timeseries/models/local/statsforecast.py +99 -112
  50. autogluon/timeseries/models/multi_window/multi_window_model.py +99 -40
  51. autogluon/timeseries/models/registry.py +64 -0
  52. autogluon/timeseries/models/toto/__init__.py +3 -0
  53. autogluon/timeseries/models/toto/_internal/__init__.py +9 -0
  54. autogluon/timeseries/models/toto/_internal/backbone/__init__.py +3 -0
  55. autogluon/timeseries/models/toto/_internal/backbone/attention.py +196 -0
  56. autogluon/timeseries/models/toto/_internal/backbone/backbone.py +262 -0
  57. autogluon/timeseries/models/toto/_internal/backbone/distribution.py +70 -0
  58. autogluon/timeseries/models/toto/_internal/backbone/kvcache.py +136 -0
  59. autogluon/timeseries/models/toto/_internal/backbone/rope.py +89 -0
  60. autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
  61. autogluon/timeseries/models/toto/_internal/backbone/scaler.py +305 -0
  62. autogluon/timeseries/models/toto/_internal/backbone/transformer.py +333 -0
  63. autogluon/timeseries/models/toto/_internal/dataset.py +165 -0
  64. autogluon/timeseries/models/toto/_internal/forecaster.py +423 -0
  65. autogluon/timeseries/models/toto/dataloader.py +108 -0
  66. autogluon/timeseries/models/toto/hf_pretrained_model.py +118 -0
  67. autogluon/timeseries/models/toto/model.py +236 -0
  68. autogluon/timeseries/predictor.py +826 -305
  69. autogluon/timeseries/regressor.py +253 -0
  70. autogluon/timeseries/splitter.py +10 -31
  71. autogluon/timeseries/trainer/__init__.py +2 -3
  72. autogluon/timeseries/trainer/ensemble_composer.py +439 -0
  73. autogluon/timeseries/trainer/model_set_builder.py +256 -0
  74. autogluon/timeseries/trainer/prediction_cache.py +149 -0
  75. autogluon/timeseries/trainer/trainer.py +1298 -0
  76. autogluon/timeseries/trainer/utils.py +17 -0
  77. autogluon/timeseries/transforms/__init__.py +2 -0
  78. autogluon/timeseries/transforms/covariate_scaler.py +164 -0
  79. autogluon/timeseries/transforms/target_scaler.py +149 -0
  80. autogluon/timeseries/utils/constants.py +10 -0
  81. autogluon/timeseries/utils/datetime/base.py +38 -20
  82. autogluon/timeseries/utils/datetime/lags.py +18 -16
  83. autogluon/timeseries/utils/datetime/seasonality.py +14 -14
  84. autogluon/timeseries/utils/datetime/time_features.py +17 -14
  85. autogluon/timeseries/utils/features.py +317 -53
  86. autogluon/timeseries/utils/forecast.py +31 -17
  87. autogluon/timeseries/utils/timer.py +173 -0
  88. autogluon/timeseries/utils/warning_filters.py +44 -6
  89. autogluon/timeseries/version.py +2 -1
  90. autogluon.timeseries-1.4.1b20251210-py3.11-nspkg.pth +1 -0
  91. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/METADATA +71 -47
  92. autogluon_timeseries-1.4.1b20251210.dist-info/RECORD +103 -0
  93. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/WHEEL +1 -1
  94. autogluon/timeseries/configs/presets_configs.py +0 -11
  95. autogluon/timeseries/evaluator.py +0 -6
  96. autogluon/timeseries/models/ensemble/greedy_ensemble.py +0 -170
  97. autogluon/timeseries/models/gluonts/abstract_gluonts.py +0 -550
  98. autogluon/timeseries/models/gluonts/torch/__init__.py +0 -0
  99. autogluon/timeseries/models/presets.py +0 -325
  100. autogluon/timeseries/trainer/abstract_trainer.py +0 -1144
  101. autogluon/timeseries/trainer/auto_trainer.py +0 -74
  102. autogluon.timeseries-1.0.1b20240304-py3.8-nspkg.pth +0 -1
  103. autogluon.timeseries-1.0.1b20240304.dist-info/RECORD +0 -58
  104. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info/licenses}/LICENSE +0 -0
  105. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info/licenses}/NOTICE +0 -0
  106. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/namespace_packages.txt +0 -0
  107. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/top_level.txt +0 -0
  108. {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/zip-safe +0 -0
@@ -1,4 +1,5 @@
1
- from typing import Optional, Tuple
1
+ import warnings
2
+ from typing import Sequence, overload
2
3
 
3
4
  import numpy as np
4
5
  import pandas as pd
@@ -15,6 +16,18 @@ class TimeSeriesScorer:
15
16
 
16
17
  Follows the design of ``autogluon.core.metrics.Scorer``.
17
18
 
19
+ Parameters
20
+ ----------
21
+ prediction_length : int, default = 1
22
+ The length of the forecast horizon. The predictions provided to the ``TimeSeriesScorer`` are expected to contain
23
+ a forecast for this many time steps for each time series.
24
+ seasonal_period : int or None, default = None
25
+ Seasonal period used to compute some evaluation metrics such as mean absolute scaled error (MASE). Defaults to
26
+ ``None``, in which case the seasonal period is computed based on the data frequency.
27
+ horizon_weight : Sequence[float], np.ndarray or None, default = None
28
+ Weight assigned to each time step in the forecast horizon when computing the metric. If provided, the
29
+ ``horizon_weight`` will be stored as a numpy array of shape ``[1, prediction_length]``.
30
+
18
31
  Attributes
19
32
  ----------
20
33
  greater_is_better_internal : bool, default = False
@@ -30,15 +43,28 @@ class TimeSeriesScorer:
30
43
  Whether the given metric uses the quantile predictions. Some models will modify the training procedure if they
31
44
  are trained to optimize a quantile metric.
32
45
  equivalent_tabular_regression_metric : str
33
- Name of an equivalent metric used by AutoGluon-Tabular with ``problem_type="regression"``. Used by models that
34
- train a TabularPredictor under the hood. This attribute should only be specified by point forecast metrics.
46
+ Name of an equivalent metric used by AutoGluon-Tabular with ``problem_type="regression"``. Used by forecasting
47
+ models that train tabular regression models under the hood. This attribute should only be specified by point
48
+ forecast metrics.
35
49
  """
36
50
 
37
51
  greater_is_better_internal: bool = False
38
52
  optimum: float = 0.0
39
53
  optimized_by_median: bool = False
40
54
  needs_quantile: bool = False
41
- equivalent_tabular_regression_metric: Optional[str] = None
55
+ equivalent_tabular_regression_metric: str | None = None
56
+
57
+ def __init__(
58
+ self,
59
+ prediction_length: int = 1,
60
+ seasonal_period: int | None = None,
61
+ horizon_weight: Sequence[float] | None = None,
62
+ ):
63
+ self.prediction_length = int(prediction_length)
64
+ if self.prediction_length < 1:
65
+ raise ValueError(f"prediction_length must be >= 1 (received {prediction_length})")
66
+ self.seasonal_period = seasonal_period
67
+ self.horizon_weight = self.check_get_horizon_weight(horizon_weight, prediction_length=prediction_length)
42
68
 
43
69
  @property
44
70
  def sign(self) -> int:
@@ -66,17 +92,25 @@ class TimeSeriesScorer:
66
92
  self,
67
93
  data: TimeSeriesDataFrame,
68
94
  predictions: TimeSeriesDataFrame,
69
- prediction_length: int = 1,
70
95
  target: str = "target",
71
- seasonal_period: Optional[int] = None,
72
96
  **kwargs,
73
97
  ) -> float:
74
- seasonal_period = get_seasonality(data.freq) if seasonal_period is None else seasonal_period
75
-
76
- data_past = data.slice_by_timestep(None, -prediction_length)
77
- data_future = data.slice_by_timestep(-prediction_length, None)
78
-
79
- assert (predictions.num_timesteps_per_item() == prediction_length).all()
98
+ seasonal_period = get_seasonality(data.freq) if self.seasonal_period is None else self.seasonal_period
99
+
100
+ if "prediction_length" in kwargs:
101
+ warnings.warn(
102
+ "Passing `prediction_length` to `TimeSeriesScorer.__call__` is deprecated and will be removed in v2.0. "
103
+ "Please set the `eval_metric.prediction_length` attribute instead.",
104
+ category=FutureWarning,
105
+ )
106
+ self.prediction_length = kwargs["prediction_length"]
107
+ self.horizon_weight = self.check_get_horizon_weight(self.horizon_weight, self.prediction_length)
108
+
109
+ data_past = data.slice_by_timestep(None, -self.prediction_length)
110
+ data_future = data.slice_by_timestep(-self.prediction_length, None)
111
+
112
+ assert not predictions.isna().any().any(), "Predictions contain NaN values."
113
+ assert (predictions.num_timesteps_per_item() == self.prediction_length).all()
80
114
  assert data_future.index.equals(predictions.index), "Prediction and data indices do not match."
81
115
 
82
116
  try:
@@ -114,10 +148,10 @@ class TimeSeriesScorer:
114
148
  ----------
115
149
  data_future : TimeSeriesDataFrame
116
150
  Actual values of the time series during the forecast horizon (``prediction_length`` values for each time
117
- series in the dataset). This data frame is guaranteed to have the same index as ``predictions``.
151
+ series in the dataset). Must have the same index as ``predictions``.
118
152
  predictions : TimeSeriesDataFrame
119
153
  Data frame with predictions for the forecast horizon. Contain columns "mean" (point forecast) and the
120
- columns corresponding to each of the quantile levels.
154
+ columns corresponding to each of the quantile levels. Must have the same index as ``data_future``.
121
155
  target : str, default = "target"
122
156
  Name of the column in ``data_future`` that contains the target time series.
123
157
 
@@ -139,7 +173,7 @@ class TimeSeriesScorer:
139
173
  ) -> None:
140
174
  """Compute auxiliary metrics on past data (before forecast horizon), if the chosen metric requires it.
141
175
 
142
- This method should only be implemented by metrics that rely on historic (in-sample) data, such as Mean Absolute
176
+ This method should only be implemented by metrics that rely on historical (in-sample) data, such as Mean Absolute
143
177
  Scaled Error (MASE) https://en.wikipedia.org/wiki/Mean_absolute_scaled_error.
144
178
 
145
179
  We keep this method separate from :meth:`compute_metric` to avoid redundant computations when fitting ensemble.
@@ -158,21 +192,21 @@ class TimeSeriesScorer:
158
192
  return self.optimum - self.score(*args, **kwargs)
159
193
 
160
194
  @staticmethod
161
- def _safemean(series: pd.Series) -> float:
162
- """Compute mean of an pd.Series, ignoring inf, -inf and nan values."""
163
- return np.nanmean(series.replace([np.inf, -np.inf], np.nan).values)
195
+ def _safemean(array: np.ndarray | pd.Series) -> float:
196
+ """Compute mean of a numpy array-like object, ignoring inf, -inf and nan values."""
197
+ return float(np.mean(array[np.isfinite(array)]))
164
198
 
165
199
  @staticmethod
166
200
  def _get_point_forecast_score_inputs(
167
201
  data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target"
168
- ) -> Tuple[pd.Series, pd.Series]:
202
+ ) -> tuple[pd.Series, pd.Series]:
169
203
  """Get inputs necessary to compute point forecast metrics.
170
204
 
171
205
  Returns
172
206
  -------
173
- y_true : pd.Series, shape [num_items * prediction_length]
207
+ y_true
174
208
  Target time series values during the forecast horizon.
175
- y_pred : pd.Series, shape [num_items * prediction_length]
209
+ y_pred
176
210
  Predicted time series values during the forecast horizon.
177
211
  """
178
212
  y_true = data_future[target]
@@ -182,16 +216,16 @@ class TimeSeriesScorer:
182
216
  @staticmethod
183
217
  def _get_quantile_forecast_score_inputs(
184
218
  data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target"
185
- ) -> Tuple[pd.Series, pd.DataFrame, np.ndarray]:
219
+ ) -> tuple[pd.Series, pd.DataFrame, np.ndarray]:
186
220
  """Get inputs necessary to compute quantile forecast metrics.
187
221
 
188
222
  Returns
189
223
  -------
190
- y_true : pd.Series, shape [num_items * prediction_length]
224
+ y_true
191
225
  Target time series values during the forecast horizon.
192
- q_pred : pd.DataFrame, shape [num_items * prediction_length, num_quantiles]
226
+ q_pred
193
227
  Quantile forecast for each predicted quantile level. Column order corresponds to ``quantile_levels``.
194
- quantile_levels : np.ndarray, shape [num_quantiles]
228
+ quantile_levels
195
229
  Quantile levels for which the forecasts are generated (as floats).
196
230
  """
197
231
  quantile_columns = [col for col in predictions.columns if col != "mean"]
@@ -199,3 +233,40 @@ class TimeSeriesScorer:
199
233
  q_pred = pd.DataFrame(predictions[quantile_columns])
200
234
  quantile_levels = np.array(quantile_columns, dtype=float)
201
235
  return y_true, q_pred, quantile_levels
236
+
237
+ @overload
238
+ @staticmethod
239
+ def check_get_horizon_weight(horizon_weight: None, prediction_length: int) -> None: ...
240
+ @overload
241
+ @staticmethod
242
+ def check_get_horizon_weight(
243
+ horizon_weight: Sequence[float] | np.ndarray, prediction_length: int
244
+ ) -> np.ndarray: ...
245
+
246
+ @staticmethod
247
+ def check_get_horizon_weight(
248
+ horizon_weight: Sequence[float] | np.ndarray | None, prediction_length: int
249
+ ) -> np.ndarray | None:
250
+ """Convert horizon_weight to a non-negative numpy array that sums up to prediction_length.
251
+ Raises an exception if horizon_weight has an invalid shape or contains invalid values.
252
+
253
+ Returns
254
+ -------
255
+ horizon_weight
256
+ None if the input is None, otherwise a numpy array of shape [1, prediction_length].
257
+ """
258
+ if horizon_weight is None:
259
+ return None
260
+ horizon_weight_np = np.ravel(horizon_weight).astype(np.float64)
261
+ if horizon_weight_np.shape != (prediction_length,):
262
+ raise ValueError(
263
+ f"horizon_weight must have length equal to {prediction_length=} (got {len(horizon_weight)=})"
264
+ )
265
+ if not (horizon_weight_np >= 0).all():
266
+ raise ValueError(f"All values in horizon_weight must be >= 0 (got {horizon_weight})")
267
+ if not horizon_weight_np.sum() > 0:
268
+ raise ValueError(f"At least some values in horizon_weight must be > 0 (got {horizon_weight})")
269
+ if not np.isfinite(horizon_weight_np).all():
270
+ raise ValueError(f"All horizon_weight values must be finite (got {horizon_weight})")
271
+ horizon_weight_np = horizon_weight_np * prediction_length / horizon_weight_np.sum()
272
+ return horizon_weight_np.reshape([1, prediction_length])
@@ -1,14 +1,14 @@
1
1
  import logging
2
- from typing import Optional
2
+ import warnings
3
+ from typing import Sequence
3
4
 
4
5
  import numpy as np
5
6
  import pandas as pd
6
7
 
7
8
  from autogluon.timeseries import TimeSeriesDataFrame
8
- from autogluon.timeseries.dataset.ts_dataframe import ITEMID
9
9
 
10
10
  from .abstract import TimeSeriesScorer
11
- from .utils import _in_sample_abs_seasonal_error, _in_sample_squared_seasonal_error
11
+ from .utils import in_sample_abs_seasonal_error, in_sample_squared_seasonal_error
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -38,10 +38,18 @@ class RMSE(TimeSeriesScorer):
38
38
  equivalent_tabular_regression_metric = "root_mean_squared_error"
39
39
 
40
40
  def compute_metric(
41
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
41
+ self,
42
+ data_future: TimeSeriesDataFrame,
43
+ predictions: TimeSeriesDataFrame,
44
+ target: str = "target",
45
+ **kwargs,
42
46
  ) -> float:
43
47
  y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
44
- return np.sqrt(self._safemean((y_true - y_pred) ** 2))
48
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
49
+ errors = ((y_true - y_pred) ** 2).reshape([-1, self.prediction_length])
50
+ if self.horizon_weight is not None:
51
+ errors *= self.horizon_weight
52
+ return np.sqrt(self._safemean(errors))
45
53
 
46
54
 
47
55
  class MSE(TimeSeriesScorer):
@@ -69,10 +77,18 @@ class MSE(TimeSeriesScorer):
69
77
  equivalent_tabular_regression_metric = "mean_squared_error"
70
78
 
71
79
  def compute_metric(
72
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
80
+ self,
81
+ data_future: TimeSeriesDataFrame,
82
+ predictions: TimeSeriesDataFrame,
83
+ target: str = "target",
84
+ **kwargs,
73
85
  ) -> float:
74
86
  y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
75
- return self._safemean((y_true - y_pred) ** 2)
87
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
88
+ errors = ((y_true - y_pred) ** 2).reshape([-1, self.prediction_length])
89
+ if self.horizon_weight is not None:
90
+ errors *= self.horizon_weight
91
+ return self._safemean(errors)
76
92
 
77
93
 
78
94
  class MAE(TimeSeriesScorer):
@@ -98,10 +114,18 @@ class MAE(TimeSeriesScorer):
98
114
  equivalent_tabular_regression_metric = "mean_absolute_error"
99
115
 
100
116
  def compute_metric(
101
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
117
+ self,
118
+ data_future: TimeSeriesDataFrame,
119
+ predictions: TimeSeriesDataFrame,
120
+ target: str = "target",
121
+ **kwargs,
102
122
  ) -> float:
103
123
  y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
104
- return self._safemean((y_true - y_pred).abs())
124
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
125
+ errors = np.abs(y_true - y_pred).reshape([-1, self.prediction_length])
126
+ if self.horizon_weight is not None:
127
+ errors *= self.horizon_weight
128
+ return self._safemean(errors)
105
129
 
106
130
 
107
131
  class WAPE(TimeSeriesScorer):
@@ -119,6 +143,7 @@ class WAPE(TimeSeriesScorer):
119
143
  - not sensitive to outliers
120
144
  - prefers models that accurately estimate the median
121
145
 
146
+ If ``self.horizon_weight`` is provided, both the errors and the target time series in the denominator will be re-weighted.
122
147
 
123
148
  References
124
149
  ----------
@@ -129,10 +154,19 @@ class WAPE(TimeSeriesScorer):
129
154
  equivalent_tabular_regression_metric = "mean_absolute_error"
130
155
 
131
156
  def compute_metric(
132
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
157
+ self,
158
+ data_future: TimeSeriesDataFrame,
159
+ predictions: TimeSeriesDataFrame,
160
+ target: str = "target",
161
+ **kwargs,
133
162
  ) -> float:
134
163
  y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
135
- return (y_true - y_pred).abs().sum() / y_true.abs().sum()
164
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
165
+ errors = np.abs(y_true - y_pred).reshape([-1, self.prediction_length])
166
+ if self.horizon_weight is not None:
167
+ errors *= self.horizon_weight
168
+ y_true = y_true.reshape([-1, self.prediction_length]) * self.horizon_weight
169
+ return np.nansum(errors) / np.nansum(np.abs(y_true))
136
170
 
137
171
 
138
172
  class SMAPE(TimeSeriesScorer):
@@ -158,10 +192,18 @@ class SMAPE(TimeSeriesScorer):
158
192
  equivalent_tabular_regression_metric = "symmetric_mean_absolute_percentage_error"
159
193
 
160
194
  def compute_metric(
161
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
195
+ self,
196
+ data_future: TimeSeriesDataFrame,
197
+ predictions: TimeSeriesDataFrame,
198
+ target: str = "target",
199
+ **kwargs,
162
200
  ) -> float:
163
201
  y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
164
- return self._safemean(2 * ((y_true - y_pred).abs() / (y_true.abs() + y_pred.abs())))
202
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
203
+ errors = (np.abs(y_true - y_pred) / (np.abs(y_true) + np.abs(y_pred))).reshape([-1, self.prediction_length])
204
+ if self.horizon_weight is not None:
205
+ errors *= self.horizon_weight
206
+ return 2 * self._safemean(errors)
165
207
 
166
208
 
167
209
  class MAPE(TimeSeriesScorer):
@@ -187,22 +229,30 @@ class MAPE(TimeSeriesScorer):
187
229
  equivalent_tabular_regression_metric = "mean_absolute_percentage_error"
188
230
 
189
231
  def compute_metric(
190
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
232
+ self,
233
+ data_future: TimeSeriesDataFrame,
234
+ predictions: TimeSeriesDataFrame,
235
+ target: str = "target",
236
+ **kwargs,
191
237
  ) -> float:
192
238
  y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
193
- return self._safemean((y_true - y_pred).abs() / y_true.abs())
239
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
240
+ errors = (np.abs(y_true - y_pred) / np.abs(y_true)).reshape([-1, self.prediction_length])
241
+ if self.horizon_weight is not None:
242
+ errors *= self.horizon_weight
243
+ return self._safemean(errors)
194
244
 
195
245
 
196
246
  class MASE(TimeSeriesScorer):
197
247
  r"""Mean absolute scaled error.
198
248
 
199
- Normalizes the absolute error for each time series by the historic seasonal error of this time series.
249
+ Normalizes the absolute error for each time series by the historical seasonal error of this time series.
200
250
 
201
251
  .. math::
202
252
 
203
253
  \operatorname{MASE} = \frac{1}{N} \frac{1}{H} \sum_{i=1}^{N} \frac{1}{a_i} \sum_{t=T+1}^{T+H} |y_{i,t} - f_{i,t}|
204
254
 
205
- where :math:`a_i` is the historic absolute seasonal error defined as
255
+ where :math:`a_i` is the historical absolute seasonal error defined as
206
256
 
207
257
  .. math::
208
258
 
@@ -226,13 +276,21 @@ class MASE(TimeSeriesScorer):
226
276
  optimized_by_median = True
227
277
  equivalent_tabular_regression_metric = "mean_absolute_error"
228
278
 
229
- def __init__(self):
230
- self._past_abs_seasonal_error: Optional[pd.Series] = None
279
+ def __init__(
280
+ self,
281
+ prediction_length: int = 1,
282
+ seasonal_period: int | None = None,
283
+ horizon_weight: Sequence[float] | None = None,
284
+ ):
285
+ super().__init__(
286
+ prediction_length=prediction_length, seasonal_period=seasonal_period, horizon_weight=horizon_weight
287
+ )
288
+ self._past_abs_seasonal_error: pd.Series | None = None
231
289
 
232
290
  def save_past_metrics(
233
291
  self, data_past: TimeSeriesDataFrame, target: str = "target", seasonal_period: int = 1, **kwargs
234
292
  ) -> None:
235
- self._past_abs_seasonal_error = _in_sample_abs_seasonal_error(
293
+ self._past_abs_seasonal_error = in_sample_abs_seasonal_error(
236
294
  y_past=data_past[target], seasonal_period=seasonal_period
237
295
  )
238
296
 
@@ -240,26 +298,34 @@ class MASE(TimeSeriesScorer):
240
298
  self._past_abs_seasonal_error = None
241
299
 
242
300
  def compute_metric(
243
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
301
+ self,
302
+ data_future: TimeSeriesDataFrame,
303
+ predictions: TimeSeriesDataFrame,
304
+ target: str = "target",
305
+ **kwargs,
244
306
  ) -> float:
245
- y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
246
307
  if self._past_abs_seasonal_error is None:
247
308
  raise AssertionError("Call `save_past_metrics` before `compute_metric`")
248
309
 
249
- mae_per_item = (y_true - y_pred).abs().groupby(level=ITEMID, sort=False).mean()
250
- return self._safemean(mae_per_item / self._past_abs_seasonal_error)
310
+ y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
311
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
312
+
313
+ errors = np.abs(y_true - y_pred).reshape([-1, self.prediction_length])
314
+ if self.horizon_weight is not None:
315
+ errors *= self.horizon_weight
316
+ return self._safemean(errors / self._past_abs_seasonal_error.to_numpy()[:, None])
251
317
 
252
318
 
253
319
  class RMSSE(TimeSeriesScorer):
254
320
  r"""Root mean squared scaled error.
255
321
 
256
- Normalizes the absolute error for each time series by the historic seasonal error of this time series.
322
+ Normalizes the absolute error for each time series by the historical seasonal error of this time series.
257
323
 
258
324
  .. math::
259
325
 
260
326
  \operatorname{RMSSE} = \sqrt{\frac{1}{N} \frac{1}{H} \sum_{i=1}^{N} \frac{1}{s_i} \sum_{t=T+1}^{T+H} (y_{i,t} - f_{i,t})^2}
261
327
 
262
- where :math:`s_i` is the historic squared seasonal error defined as
328
+ where :math:`s_i` is the historical squared seasonal error defined as
263
329
 
264
330
  .. math::
265
331
 
@@ -284,13 +350,21 @@ class RMSSE(TimeSeriesScorer):
284
350
 
285
351
  equivalent_tabular_regression_metric = "root_mean_squared_error"
286
352
 
287
- def __init__(self):
288
- self._past_squared_seasonal_error: Optional[pd.Series] = None
353
+ def __init__(
354
+ self,
355
+ prediction_length: int = 1,
356
+ seasonal_period: int | None = None,
357
+ horizon_weight: Sequence[float] | None = None,
358
+ ):
359
+ super().__init__(
360
+ prediction_length=prediction_length, seasonal_period=seasonal_period, horizon_weight=horizon_weight
361
+ )
362
+ self._past_squared_seasonal_error: pd.Series | None = None
289
363
 
290
364
  def save_past_metrics(
291
365
  self, data_past: TimeSeriesDataFrame, target: str = "target", seasonal_period: int = 1, **kwargs
292
366
  ) -> None:
293
- self._past_squared_seasonal_error = _in_sample_squared_seasonal_error(
367
+ self._past_squared_seasonal_error = in_sample_squared_seasonal_error(
294
368
  y_past=data_past[target], seasonal_period=seasonal_period
295
369
  )
296
370
 
@@ -298,14 +372,21 @@ class RMSSE(TimeSeriesScorer):
298
372
  self._past_squared_seasonal_error = None
299
373
 
300
374
  def compute_metric(
301
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target", **kwargs
375
+ self,
376
+ data_future: TimeSeriesDataFrame,
377
+ predictions: TimeSeriesDataFrame,
378
+ target: str = "target",
379
+ **kwargs,
302
380
  ) -> float:
303
- y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
304
381
  if self._past_squared_seasonal_error is None:
305
382
  raise AssertionError("Call `save_past_metrics` before `compute_metric`")
306
383
 
307
- mse_per_item = (y_true - y_pred).pow(2.0).groupby(level=ITEMID, sort=False).mean()
308
- return np.sqrt(self._safemean(mse_per_item / self._past_squared_seasonal_error))
384
+ y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
385
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
386
+ errors = ((y_true - y_pred) ** 2).reshape([-1, self.prediction_length])
387
+ if self.horizon_weight is not None:
388
+ errors *= self.horizon_weight
389
+ return np.sqrt(self._safemean(errors / self._past_squared_seasonal_error.to_numpy()[:, None]))
309
390
 
310
391
 
311
392
  class RMSLE(TimeSeriesScorer):
@@ -331,19 +412,27 @@ class RMSLE(TimeSeriesScorer):
331
412
  - `Scikit-learn: <https://scikit-learn.org/stable/modules/model_evaluation.html#mean-squared-log-error>`_
332
413
  """
333
414
 
334
- def compute_metric(self, data_future, predictions, target, **kwargs):
415
+ def compute_metric(
416
+ self,
417
+ data_future: TimeSeriesDataFrame,
418
+ predictions: TimeSeriesDataFrame,
419
+ target: str = "target",
420
+ **kwargs,
421
+ ) -> float:
335
422
  y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
423
+ y_true, y_pred = y_true.to_numpy(), y_pred.to_numpy()
336
424
  y_pred = np.clip(y_pred, a_min=0.0, a_max=None)
337
425
 
338
- return np.sqrt(np.power(np.log1p(y_pred) - np.log1p(y_true), 2).mean())
426
+ errors = np.power(np.log1p(y_pred) - np.log1p(y_true), 2).reshape([-1, self.prediction_length])
427
+ if self.horizon_weight is not None:
428
+ errors *= self.horizon_weight
429
+ return np.sqrt(self._safemean(errors))
339
430
 
340
431
  def __call__(
341
432
  self,
342
433
  data: TimeSeriesDataFrame,
343
434
  predictions: TimeSeriesDataFrame,
344
- prediction_length: int = 1,
345
435
  target: str = "target",
346
- seasonal_period: Optional[int] = None,
347
436
  **kwargs,
348
437
  ) -> float:
349
438
  if (data[target] < 0).any():
@@ -351,8 +440,66 @@ class RMSLE(TimeSeriesScorer):
351
440
  return super().__call__(
352
441
  data=data,
353
442
  predictions=predictions,
354
- prediction_length=prediction_length,
355
443
  target=target,
356
- seasonal_period=seasonal_period,
357
444
  **kwargs,
358
445
  )
446
+
447
+
448
+ class WCD(TimeSeriesScorer):
449
+ r"""Weighted cumulative discrepancy.
450
+
451
+ Measures the discrepancy between the cumulative sum of the forecast and the cumulative sum of the actual values.
452
+
453
+ .. math::
454
+
455
+ \operatorname{WCD} = 2 \cdot \frac{1}{N} \frac{1}{H} \sum_{i=1}^{N} \sum_{t=T+1}^{T+H} \alpha \cdot \max(0, -d_{i, t}) + (1 - \alpha) \cdot \max(0, d_{i, t})
456
+
457
+ where :math:`d_{i, t}` is the difference between the cumulative predicted value and the cumulative actual value
458
+
459
+ .. math::
460
+
461
+ d_{i, t} = \left(\sum_{s=T+1}^t f_{i, s}) - \left(\sum_{s=T+1}^t y_{i, s})
462
+
463
+ Parameters
464
+ ----------
465
+ alpha : float, default = 0.5
466
+ Values > 0.5 put a stronger penalty on underpredictions (when cumulative forecast is below the
467
+ cumulative actual value). Values < 0.5 put a stronger penalty on overpredictions.
468
+ """
469
+
470
+ def __init__(
471
+ self,
472
+ alpha: float = 0.5,
473
+ prediction_length: int = 1,
474
+ seasonal_period: int | None = None,
475
+ horizon_weight: Sequence[float] | None = None,
476
+ ):
477
+ super().__init__(
478
+ prediction_length=prediction_length, seasonal_period=seasonal_period, horizon_weight=horizon_weight
479
+ )
480
+ assert 0 < alpha < 1, "alpha must be in (0, 1)"
481
+ self.alpha = alpha
482
+ warnings.warn(
483
+ f"{self.name} is an experimental metric. Its behavior may change in the future version of AutoGluon."
484
+ )
485
+
486
+ def _fast_cumsum(self, y: np.ndarray) -> np.ndarray:
487
+ """Compute the cumulative sum for each consecutive `self.prediction_length` items in the array."""
488
+ y = y.reshape(-1, self.prediction_length)
489
+ return np.nancumsum(y, axis=1).ravel()
490
+
491
+ def compute_metric(
492
+ self,
493
+ data_future: TimeSeriesDataFrame,
494
+ predictions: TimeSeriesDataFrame,
495
+ target: str = "target",
496
+ **kwargs,
497
+ ) -> float:
498
+ y_true, y_pred = self._get_point_forecast_score_inputs(data_future, predictions, target=target)
499
+ cumsum_true = self._fast_cumsum(y_true.to_numpy())
500
+ cumsum_pred = self._fast_cumsum(y_pred.to_numpy())
501
+ diffs = cumsum_pred - cumsum_true
502
+ errors = (diffs * np.where(diffs < 0, -self.alpha, (1 - self.alpha))).reshape([-1, self.prediction_length])
503
+ if self.horizon_weight is not None:
504
+ errors *= self.horizon_weight
505
+ return 2 * self._safemean(errors)