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.
- autogluon/timeseries/configs/__init__.py +3 -2
- autogluon/timeseries/configs/hyperparameter_presets.py +62 -0
- autogluon/timeseries/configs/predictor_presets.py +84 -0
- autogluon/timeseries/dataset/ts_dataframe.py +339 -186
- autogluon/timeseries/learner.py +192 -60
- autogluon/timeseries/metrics/__init__.py +55 -11
- autogluon/timeseries/metrics/abstract.py +96 -25
- autogluon/timeseries/metrics/point.py +186 -39
- autogluon/timeseries/metrics/quantile.py +47 -20
- autogluon/timeseries/metrics/utils.py +6 -6
- autogluon/timeseries/models/__init__.py +13 -7
- autogluon/timeseries/models/abstract/__init__.py +2 -2
- autogluon/timeseries/models/abstract/abstract_timeseries_model.py +533 -273
- autogluon/timeseries/models/abstract/model_trial.py +10 -10
- autogluon/timeseries/models/abstract/tunable.py +189 -0
- autogluon/timeseries/models/autogluon_tabular/__init__.py +2 -0
- autogluon/timeseries/models/autogluon_tabular/mlforecast.py +369 -215
- autogluon/timeseries/models/autogluon_tabular/per_step.py +513 -0
- autogluon/timeseries/models/autogluon_tabular/transforms.py +67 -0
- autogluon/timeseries/models/autogluon_tabular/utils.py +3 -51
- autogluon/timeseries/models/chronos/__init__.py +4 -0
- autogluon/timeseries/models/chronos/chronos2.py +361 -0
- autogluon/timeseries/models/chronos/model.py +738 -0
- autogluon/timeseries/models/chronos/utils.py +369 -0
- autogluon/timeseries/models/ensemble/__init__.py +35 -2
- autogluon/timeseries/models/ensemble/{abstract_timeseries_ensemble.py → abstract.py} +50 -26
- autogluon/timeseries/models/ensemble/array_based/__init__.py +3 -0
- autogluon/timeseries/models/ensemble/array_based/abstract.py +236 -0
- autogluon/timeseries/models/ensemble/array_based/models.py +73 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/__init__.py +12 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/abstract.py +88 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/linear_stacker.py +167 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/per_quantile_tabular.py +94 -0
- autogluon/timeseries/models/ensemble/array_based/regressor/tabular.py +107 -0
- autogluon/timeseries/models/ensemble/ensemble_selection.py +167 -0
- autogluon/timeseries/models/ensemble/per_item_greedy.py +162 -0
- autogluon/timeseries/models/ensemble/weighted/__init__.py +8 -0
- autogluon/timeseries/models/ensemble/weighted/abstract.py +40 -0
- autogluon/timeseries/models/ensemble/weighted/basic.py +78 -0
- autogluon/timeseries/models/ensemble/weighted/greedy.py +57 -0
- autogluon/timeseries/models/gluonts/__init__.py +3 -1
- autogluon/timeseries/models/gluonts/abstract.py +583 -0
- autogluon/timeseries/models/gluonts/dataset.py +109 -0
- autogluon/timeseries/models/gluonts/{torch/models.py → models.py} +185 -44
- autogluon/timeseries/models/local/__init__.py +1 -10
- autogluon/timeseries/models/local/abstract_local_model.py +150 -97
- autogluon/timeseries/models/local/naive.py +31 -23
- autogluon/timeseries/models/local/npts.py +6 -2
- autogluon/timeseries/models/local/statsforecast.py +99 -112
- autogluon/timeseries/models/multi_window/multi_window_model.py +99 -40
- autogluon/timeseries/models/registry.py +64 -0
- autogluon/timeseries/models/toto/__init__.py +3 -0
- autogluon/timeseries/models/toto/_internal/__init__.py +9 -0
- autogluon/timeseries/models/toto/_internal/backbone/__init__.py +3 -0
- autogluon/timeseries/models/toto/_internal/backbone/attention.py +196 -0
- autogluon/timeseries/models/toto/_internal/backbone/backbone.py +262 -0
- autogluon/timeseries/models/toto/_internal/backbone/distribution.py +70 -0
- autogluon/timeseries/models/toto/_internal/backbone/kvcache.py +136 -0
- autogluon/timeseries/models/toto/_internal/backbone/rope.py +89 -0
- autogluon/timeseries/models/toto/_internal/backbone/rotary_embedding_torch.py +342 -0
- autogluon/timeseries/models/toto/_internal/backbone/scaler.py +305 -0
- autogluon/timeseries/models/toto/_internal/backbone/transformer.py +333 -0
- autogluon/timeseries/models/toto/_internal/dataset.py +165 -0
- autogluon/timeseries/models/toto/_internal/forecaster.py +423 -0
- autogluon/timeseries/models/toto/dataloader.py +108 -0
- autogluon/timeseries/models/toto/hf_pretrained_model.py +118 -0
- autogluon/timeseries/models/toto/model.py +236 -0
- autogluon/timeseries/predictor.py +826 -305
- autogluon/timeseries/regressor.py +253 -0
- autogluon/timeseries/splitter.py +10 -31
- autogluon/timeseries/trainer/__init__.py +2 -3
- autogluon/timeseries/trainer/ensemble_composer.py +439 -0
- autogluon/timeseries/trainer/model_set_builder.py +256 -0
- autogluon/timeseries/trainer/prediction_cache.py +149 -0
- autogluon/timeseries/trainer/trainer.py +1298 -0
- autogluon/timeseries/trainer/utils.py +17 -0
- autogluon/timeseries/transforms/__init__.py +2 -0
- autogluon/timeseries/transforms/covariate_scaler.py +164 -0
- autogluon/timeseries/transforms/target_scaler.py +149 -0
- autogluon/timeseries/utils/constants.py +10 -0
- autogluon/timeseries/utils/datetime/base.py +38 -20
- autogluon/timeseries/utils/datetime/lags.py +18 -16
- autogluon/timeseries/utils/datetime/seasonality.py +14 -14
- autogluon/timeseries/utils/datetime/time_features.py +17 -14
- autogluon/timeseries/utils/features.py +317 -53
- autogluon/timeseries/utils/forecast.py +31 -17
- autogluon/timeseries/utils/timer.py +173 -0
- autogluon/timeseries/utils/warning_filters.py +44 -6
- autogluon/timeseries/version.py +2 -1
- autogluon.timeseries-1.4.1b20251210-py3.11-nspkg.pth +1 -0
- {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/METADATA +71 -47
- autogluon_timeseries-1.4.1b20251210.dist-info/RECORD +103 -0
- {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/WHEEL +1 -1
- autogluon/timeseries/configs/presets_configs.py +0 -11
- autogluon/timeseries/evaluator.py +0 -6
- autogluon/timeseries/models/ensemble/greedy_ensemble.py +0 -170
- autogluon/timeseries/models/gluonts/abstract_gluonts.py +0 -550
- autogluon/timeseries/models/gluonts/torch/__init__.py +0 -0
- autogluon/timeseries/models/presets.py +0 -325
- autogluon/timeseries/trainer/abstract_trainer.py +0 -1144
- autogluon/timeseries/trainer/auto_trainer.py +0 -74
- autogluon.timeseries-1.0.1b20240304-py3.8-nspkg.pth +0 -1
- autogluon.timeseries-1.0.1b20240304.dist-info/RECORD +0 -58
- {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info/licenses}/LICENSE +0 -0
- {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info/licenses}/NOTICE +0 -0
- {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/namespace_packages.txt +0 -0
- {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/top_level.txt +0 -0
- {autogluon.timeseries-1.0.1b20240304.dist-info → autogluon_timeseries-1.4.1b20251210.dist-info}/zip-safe +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
|
34
|
-
train
|
|
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:
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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).
|
|
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
|
|
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(
|
|
162
|
-
"""Compute mean of
|
|
163
|
-
return np.
|
|
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
|
-
) ->
|
|
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
|
|
207
|
+
y_true
|
|
174
208
|
Target time series values during the forecast horizon.
|
|
175
|
-
y_pred
|
|
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
|
-
) ->
|
|
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
|
|
224
|
+
y_true
|
|
191
225
|
Target time series values during the forecast horizon.
|
|
192
|
-
q_pred
|
|
226
|
+
q_pred
|
|
193
227
|
Quantile forecast for each predicted quantile level. Column order corresponds to ``quantile_levels``.
|
|
194
|
-
quantile_levels
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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__(
|
|
230
|
-
self
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
250
|
-
|
|
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
|
|
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
|
|
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__(
|
|
288
|
-
self
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
308
|
-
|
|
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(
|
|
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
|
-
|
|
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)
|