autogluon.timeseries 0.8.3b20231024__py3-none-any.whl → 0.8.3b20231027__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 (27) hide show
  1. autogluon/timeseries/evaluator.py +26 -249
  2. autogluon/timeseries/learner.py +5 -5
  3. autogluon/timeseries/metrics/__init__.py +58 -0
  4. autogluon/timeseries/metrics/abstract.py +201 -0
  5. autogluon/timeseries/metrics/point.py +156 -0
  6. autogluon/timeseries/metrics/quantile.py +26 -0
  7. autogluon/timeseries/metrics/utils.py +18 -0
  8. autogluon/timeseries/models/abstract/abstract_timeseries_model.py +43 -41
  9. autogluon/timeseries/models/abstract/model_trial.py +1 -1
  10. autogluon/timeseries/models/autogluon_tabular/mlforecast.py +28 -55
  11. autogluon/timeseries/models/ensemble/greedy_ensemble.py +27 -15
  12. autogluon/timeseries/models/gluonts/abstract_gluonts.py +1 -20
  13. autogluon/timeseries/models/local/abstract_local_model.py +1 -1
  14. autogluon/timeseries/models/multi_window/multi_window_model.py +4 -2
  15. autogluon/timeseries/models/presets.py +2 -1
  16. autogluon/timeseries/predictor.py +24 -15
  17. autogluon/timeseries/trainer/abstract_trainer.py +14 -22
  18. autogluon/timeseries/version.py +1 -1
  19. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/METADATA +6 -5
  20. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/RECORD +27 -22
  21. /autogluon.timeseries-0.8.3b20231024-py3.8-nspkg.pth → /autogluon.timeseries-0.8.3b20231027-py3.8-nspkg.pth +0 -0
  22. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/LICENSE +0 -0
  23. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/NOTICE +0 -0
  24. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/WHEEL +0 -0
  25. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/namespace_packages.txt +0 -0
  26. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/top_level.txt +0 -0
  27. {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/zip-safe +0 -0
@@ -1,128 +1,21 @@
1
- """Functions and objects for evaluating forecasts. Adapted from gluonts.evaluation.
2
- See also, https://ts.gluon.ai/api/gluonts/gluonts.evaluation.html
3
- """
4
- import logging
5
1
  from typing import Optional
6
2
 
7
- import numpy as np
8
- import pandas as pd
9
-
3
+ from autogluon.common.utils.deprecated_utils import Deprecated
10
4
  from autogluon.timeseries import TimeSeriesDataFrame
11
- from autogluon.timeseries.dataset.ts_dataframe import ITEMID
12
- from autogluon.timeseries.utils.datetime import get_seasonality
13
- from autogluon.timeseries.utils.warning_filters import warning_filter
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- def get_seasonal_diffs(*, y_past: pd.Series, seasonal_period: int = 1) -> pd.Series:
19
- return y_past.groupby(level=ITEMID, sort=False).diff(seasonal_period).abs()
20
-
21
-
22
- def in_sample_abs_seasonal_error(*, y_past: pd.Series, seasonal_period: int = 1) -> pd.Series:
23
- """Compute seasonal naive forecast error (predict value from seasonal_period steps ago) for each time series."""
24
- seasonal_diffs = get_seasonal_diffs(y_past=y_past, seasonal_period=seasonal_period)
25
- return seasonal_diffs.groupby(level=ITEMID, sort=False).mean().fillna(1.0)
26
-
27
-
28
- def in_sample_squared_seasonal_error(*, y_past: pd.Series, seasonal_period: int = 1) -> pd.Series:
29
- seasonal_diffs = get_seasonal_diffs(y_past=y_past, seasonal_period=seasonal_period)
30
- return seasonal_diffs.dropna().pow(2.0).groupby(level=ITEMID, sort=False).mean()
31
-
32
-
33
- def mse_per_item(*, y_true: pd.Series, y_pred: pd.Series) -> pd.Series:
34
- """Compute Mean Squared Error for each item (time series)."""
35
- return (y_true - y_pred).pow(2.0).groupby(level=ITEMID, sort=False).mean()
36
-
37
-
38
- def mae_per_item(*, y_true: pd.Series, y_pred: pd.Series) -> pd.Series:
39
- """Compute Mean Absolute Error for each item (time series)."""
40
- return (y_true - y_pred).abs().groupby(level=ITEMID, sort=False).mean()
41
-
42
-
43
- def mape_per_item(*, y_true: pd.Series, y_pred: pd.Series) -> pd.Series:
44
- """Compute Mean Absolute Percentage Error for each item (time series)."""
45
- return ((y_true - y_pred) / y_true).abs().groupby(level=ITEMID, sort=False).mean()
46
-
47
-
48
- def rmsse_per_item(*, y_true: pd.Series, y_pred: pd.Series, past_squared_seasonal_error: pd.Series) -> pd.Series:
49
- mse = mse_per_item(y_true=y_true, y_pred=y_pred)
50
- return mse / past_squared_seasonal_error
51
-
52
-
53
- def symmetric_mape_per_item(*, y_true: pd.Series, y_pred: pd.Series) -> pd.Series:
54
- """Compute symmetric Mean Absolute Percentage Error for each item (time series)."""
55
- return (2 * (y_true - y_pred).abs() / (y_true.abs() + y_pred.abs())).groupby(level=ITEMID, sort=False).mean()
5
+ from autogluon.timeseries.metrics import AVAILABLE_METRICS, check_get_evaluation_metric
56
6
 
57
7
 
8
+ @Deprecated(
9
+ min_version_to_warn="1.0",
10
+ min_version_to_error="1.1",
11
+ custom_warning_msg="Please use the metrics defined in autogluon.timeseries.metrics instead.",
12
+ )
58
13
  class TimeSeriesEvaluator:
59
- """Contains functions for computing forecast accuracy metrics.
60
-
61
- Forecast accuracy metrics measure discrepancy between forecasts and ground-truth time
62
- series. After being initialized, AutoGluon ``TimeSeriesEvaluator`` expects two time
63
- series data sets (``TimeSeriesDataFrame``) as input: the first of which contains the
64
- ground truth time series including both the "history" and the forecast horizon. The
65
- second input is the data frame of predictions corresponding only to the forecast
66
- horizon.
67
-
68
- .. warning::
69
- ``TimeSeriesEvaluator`` always computes metrics by their original definition, while
70
- AutoGluon-TimeSeries predictor and model objects always report their scores in
71
- higher-is-better fashion. The coefficients used to "flip" the signs of metrics to
72
- obey this convention are given in ``TimeSeriesEvaluator.METRIC_COEFFICIENTS``.
14
+ """This class has been deprecated in AutoGluon v1.0 and is only provided for backward compatibility!"""
73
15
 
74
- .. warning::
75
- Definitions of forecast accuracy metrics may differ from package to package.
76
- For example, the definition of MASE is different between GluonTS and autogluon.
77
-
78
- Parameters
79
- ----------
80
- eval_metric : str
81
- Name of the metric to be computed. Available metrics are
82
-
83
- * ``MASE``: mean absolute scaled error. See https://en.wikipedia.org/wiki/Mean_absolute_scaled_error
84
- * ``MAPE``: mean absolute percentage error. See https://en.wikipedia.org/wiki/Mean_absolute_percentage_error
85
- * ``sMAPE``: "symmetric" mean absolute percentage error. See https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error
86
- * ``WQL``: mean weighted quantile loss, i.e., average quantile loss scaled
87
- by the total absolute values of the time series. See https://docs.aws.amazon.com/forecast/latest/dg/metrics.html#metrics-wQL
88
- * ``MSE``: mean squared error
89
- * ``RMSE``: root mean squared error
90
- * ``WAPE``: weighted absolute percentage error. See https://docs.aws.amazon.com/forecast/latest/dg/metrics.html#metrics-WAPE
91
- * ``RMSSE``: Root Mean Squared Scaled Error . See https://otexts.com/fpp3/accuracy.html#scaled-errors
92
-
93
- prediction_length : int
94
- Length of the forecast horizon
95
- target_column : str, default = "target"
96
- Name of the target column to be forecasting.
97
- eval_metric_seasonal_period : int, optional
98
- Seasonal period used to compute the mean absolute scaled error (MASE) evaluation metric. This parameter is only
99
- used if ``eval_metric="MASE"`. See https://en.wikipedia.org/wiki/Mean_absolute_scaled_error for more details.
100
- Defaults to ``None``, in which case the seasonal period is computed based on the data frequency.
101
-
102
- Class Attributes
103
- ----------------
104
- AVAILABLE_METRICS
105
- list of names of available metrics
106
- METRIC_COEFFICIENTS
107
- coefficients by which each metric should be multiplied with to obey the higher-is-better
108
- convention
109
- DEFAULT_METRIC
110
- name of default metric returned by
111
- :meth:``~autogluon.timeseries.TimeSeriesEvaluator.check_get_evaluation_metric``.
112
- """
113
-
114
- AVAILABLE_METRICS = ["MASE", "MAPE", "sMAPE", "WQL", "MSE", "RMSE", "WAPE", "RMSSE"]
115
- METRIC_COEFFICIENTS = {
116
- "MASE": -1,
117
- "MAPE": -1,
118
- "sMAPE": -1,
119
- "WQL": -1,
120
- "MSE": -1,
121
- "RMSE": -1,
122
- "WAPE": -1,
123
- "RMSSE": -1,
124
- }
125
- DEFAULT_METRIC = "WQL"
16
+ METRIC_COEFFICIENTS = {metric_name: metric_cls().sign for metric_name, metric_cls in AVAILABLE_METRICS.items()}
17
+ AVAILABLE_METRICS = list(AVAILABLE_METRICS.keys())
18
+ DEFAULT_METRIC = check_get_evaluation_metric(None).name
126
19
 
127
20
  def __init__(
128
21
  self,
@@ -131,151 +24,35 @@ class TimeSeriesEvaluator:
131
24
  target_column: str = "target",
132
25
  eval_metric_seasonal_period: Optional[int] = None,
133
26
  ):
134
- assert eval_metric in self.AVAILABLE_METRICS, f"Metric {eval_metric} not available"
135
-
27
+ self.eval_metric = check_get_evaluation_metric(eval_metric)
136
28
  self.prediction_length = prediction_length
137
- self.eval_metric = eval_metric
138
29
  self.target_column = target_column
139
30
  self.seasonal_period = eval_metric_seasonal_period
140
31
 
141
- self.metric_method = self.__getattribute__("_" + self.eval_metric.lower())
142
- self._past_abs_seasonal_error: Optional[pd.Series] = None
143
- self._past_squared_seasonal_error: Optional[pd.Series] = None
144
-
145
32
  @property
146
33
  def coefficient(self) -> int:
147
- return self.METRIC_COEFFICIENTS[self.eval_metric]
34
+ return self.eval_metric.sign
148
35
 
149
36
  @property
150
37
  def higher_is_better(self) -> bool:
151
- return self.coefficient > 0
152
-
153
- def _safemean(self, data: pd.Series) -> float:
154
- return data.replace([np.inf, -np.inf], np.nan).dropna().mean()
155
-
156
- def _mse(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
157
- y_pred = predictions["mean"]
158
- return self._safemean(mse_per_item(y_true=y_true, y_pred=y_pred))
159
-
160
- def _rmse(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
161
- return np.sqrt(self._mse(y_true=y_true, predictions=predictions))
162
-
163
- def _mase(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
164
- y_pred = self._get_median_forecast(predictions)
165
- mae = mae_per_item(y_true=y_true, y_pred=y_pred)
166
- return self._safemean(mae / self._past_abs_seasonal_error)
167
-
168
- def _mape(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
169
- y_pred = self._get_median_forecast(predictions)
170
- return self._safemean(mape_per_item(y_true=y_true, y_pred=y_pred))
171
-
172
- def _smape(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
173
- y_pred = self._get_median_forecast(predictions)
174
- return self._safemean(symmetric_mape_per_item(y_true=y_true, y_pred=y_pred))
175
-
176
- def _wql(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
177
- values_true = y_true.values[:, None] # shape [N, 1]
178
- quantile_pred_columns = [col for col in predictions.columns if col != "mean"]
179
- values_pred = predictions[quantile_pred_columns].values # shape [N, len(quantile_levels)]
180
- quantile_levels = np.array([float(q) for q in quantile_pred_columns], dtype=float)
181
-
182
- return 2 * np.mean(
183
- np.abs((values_true - values_pred) * ((values_true <= values_pred) - quantile_levels)).sum(axis=0)
184
- / np.abs(values_true).sum()
185
- )
186
-
187
- def _wape(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
188
- y_pred = self._get_median_forecast(predictions)
189
- abs_error_sum = (mae_per_item(y_true=y_true, y_pred=y_pred) * self.prediction_length).sum()
190
- abs_target_sum = y_true.abs().sum()
191
- return abs_error_sum / abs_target_sum
192
-
193
- def _rmsse(self, y_true: pd.Series, predictions: TimeSeriesDataFrame) -> float:
194
- y_pred = predictions["mean"]
195
- return np.sqrt(
196
- rmsse_per_item(
197
- y_true=y_true, y_pred=y_pred, past_squared_seasonal_error=self._past_squared_seasonal_error
198
- ).mean()
199
- )
200
-
201
- def _get_median_forecast(self, predictions: TimeSeriesDataFrame) -> pd.Series:
202
- # TODO: Median forecast doesn't actually minimize the MAPE / sMAPE losses
203
- if "0.5" in predictions.columns:
204
- return predictions["0.5"]
205
- else:
206
- logger.warning("Median forecast not found. Defaulting to mean forecasts.")
207
- return predictions["mean"]
38
+ return self.eval_metric.greater_is_better_internal
208
39
 
209
40
  @staticmethod
210
41
  def check_get_evaluation_metric(
211
42
  metric_name: Optional[str] = None,
212
43
  raise_if_not_available: bool = True,
213
44
  ):
214
- """A utility function that checks if a given evaluation metric
215
- name is available in autogluon.timeseries, and optionally raises
216
- a ValueError otherwise.
217
-
218
- Parameters
219
- ----------
220
- metric_name: str
221
- The requested metric name, currently one of the evaluation metrics available
222
- in GluonTS.
223
- raise_if_not_available: bool
224
- if True, a ValueError will be raised if the requested metric is not yet
225
- available in autogluon.timeseries. Otherwise, the default metric name will be
226
- returned instead of the requested metric.
227
-
228
- Returns
229
- -------
230
- checked_metric_name: str
231
- The requested metric name if it is available in autogluon.timeseries.
232
- """
233
- metric = metric_name or TimeSeriesEvaluator.DEFAULT_METRIC
234
- if metric not in TimeSeriesEvaluator.AVAILABLE_METRICS:
235
- if raise_if_not_available:
236
- raise ValueError(f"metric {metric} is not available yet.")
237
- return TimeSeriesEvaluator.DEFAULT_METRIC
238
- return metric
239
-
240
- def save_past_metrics(self, data_past: TimeSeriesDataFrame):
241
- seasonal_period = get_seasonality(data_past.freq) if self.seasonal_period is None else self.seasonal_period
242
- if self.eval_metric == "MASE":
243
- self._past_abs_seasonal_error = in_sample_abs_seasonal_error(
244
- y_past=data_past[self.target_column], seasonal_period=seasonal_period
245
- )
246
-
247
- if self.eval_metric == "RMSSE":
248
- self._past_squared_seasonal_error = in_sample_squared_seasonal_error(
249
- y_past=data_past[self.target_column], seasonal_period=seasonal_period
250
- )
251
-
252
- def score_with_saved_past_metrics(
253
- self, data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame
254
- ) -> float:
255
- """Compute the metric assuming that the historic metrics have already been computed.
256
-
257
- This method should be preferred to TimeSeriesEvaluator.__call__ if the metrics are computed multiple times, as
258
- it doesn't require splitting the test data into past/future portions each time (e.g., when fitting ensembles).
259
- """
260
- assert (predictions.num_timesteps_per_item() == self.prediction_length).all()
261
-
262
- if self.eval_metric == "MASE" and self._past_abs_seasonal_error is None:
263
- raise AssertionError("Call save_past_metrics before score_with_saved_past_metrics")
264
-
265
- if self.eval_metric == "RMSSE" and self._past_squared_seasonal_error is None:
266
- raise AssertionError("Call save_past_metrics before score_with_saved_past_metrics")
267
-
268
- assert data_future.index.equals(predictions.index), "Prediction and data indices do not match."
269
-
270
- with warning_filter():
271
- return self.metric_method(
272
- y_true=data_future[self.target_column],
273
- predictions=predictions,
274
- )
45
+ return check_get_evaluation_metric(metric_name)
275
46
 
276
47
  def __call__(self, data: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame) -> float:
277
- # Select entries in `data` that correspond to the forecast horizon
278
- data_past = data.slice_by_timestep(None, -self.prediction_length)
279
- data_future = data.slice_by_timestep(-self.prediction_length, None)
280
- self.save_past_metrics(data_past=data_past)
281
- return self.score_with_saved_past_metrics(data_future=data_future, predictions=predictions)
48
+ quantile_levels = [float(col) for col in predictions.columns if col != "mean"]
49
+ score = self.eval_metric(
50
+ data=data,
51
+ predictions=predictions,
52
+ prediction_length=self.prediction_length,
53
+ target=self.target_column,
54
+ seasonal_period=self.seasonal_period,
55
+ quantile_levels=quantile_levels,
56
+ )
57
+ # Return raw metric in lower-is-better format to match the old Evaluator API
58
+ return score * self.eval_metric.sign
@@ -7,7 +7,7 @@ import pandas as pd
7
7
 
8
8
  from autogluon.core.learner import AbstractLearner
9
9
  from autogluon.timeseries.dataset.ts_dataframe import TimeSeriesDataFrame
10
- from autogluon.timeseries.evaluator import TimeSeriesEvaluator
10
+ from autogluon.timeseries.metrics import TimeSeriesScorer, check_get_evaluation_metric
11
11
  from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel
12
12
  from autogluon.timeseries.splitter import AbstractWindowSplitter
13
13
  from autogluon.timeseries.trainer import AbstractTimeSeriesTrainer, AutoTimeSeriesTrainer
@@ -28,14 +28,14 @@ class TimeSeriesLearner(AbstractLearner):
28
28
  target: str = "target",
29
29
  known_covariates_names: Optional[List[str]] = None,
30
30
  trainer_type: Type[AbstractTimeSeriesTrainer] = AutoTimeSeriesTrainer,
31
- eval_metric: Optional[str] = None,
31
+ eval_metric: Union[str, TimeSeriesScorer, None] = None,
32
32
  eval_metric_seasonal_period: Optional[int] = None,
33
33
  prediction_length: int = 1,
34
34
  cache_predictions: bool = True,
35
35
  **kwargs,
36
36
  ):
37
37
  super().__init__(path_context=path_context)
38
- self.eval_metric: str = TimeSeriesEvaluator.check_get_evaluation_metric(eval_metric)
38
+ self.eval_metric: TimeSeriesScorer = check_get_evaluation_metric(eval_metric)
39
39
  self.eval_metric_seasonal_period = eval_metric_seasonal_period
40
40
  self.trainer_type = trainer_type
41
41
  self.target = target
@@ -89,7 +89,7 @@ class TimeSeriesLearner(AbstractLearner):
89
89
  logger.info(f"AutoGluon will save models to {self.path}")
90
90
 
91
91
  logger.info(f"AutoGluon will gauge predictive performance using evaluation metric: '{self.eval_metric}'")
92
- if TimeSeriesEvaluator.METRIC_COEFFICIENTS[self.eval_metric] == -1:
92
+ if not self.eval_metric.greater_is_better_internal:
93
93
  logger.info(
94
94
  "\tThis metric's sign has been flipped to adhere to being 'higher is better'. "
95
95
  "The reported score can be multiplied by -1 to get the metric value.",
@@ -185,7 +185,7 @@ class TimeSeriesLearner(AbstractLearner):
185
185
  self,
186
186
  data: TimeSeriesDataFrame,
187
187
  model: AbstractTimeSeriesModel = None,
188
- metric: Optional[str] = None,
188
+ metric: Union[str, TimeSeriesScorer, None] = None,
189
189
  use_cache: bool = True,
190
190
  ) -> float:
191
191
  data = self.feature_generator.transform(data)
@@ -0,0 +1,58 @@
1
+ import json
2
+ from typing import Type, Union
3
+
4
+ from .abstract import TimeSeriesScorer
5
+ from .point import MAE, MAPE, MASE, MSE, RMSE, RMSSE, WAPE, sMAPE
6
+ from .quantile import WQL
7
+
8
+ __all__ = [
9
+ "MAE",
10
+ "MAPE",
11
+ "MASE",
12
+ "sMAPE",
13
+ "MSE",
14
+ "RMSE",
15
+ "RMSSE",
16
+ "WAPE",
17
+ "WQL",
18
+ ]
19
+
20
+ DEFAULT_METRIC_NAME = "WQL"
21
+
22
+ AVAILABLE_METRICS = {
23
+ "MASE": MASE,
24
+ "MAPE": MAPE,
25
+ "SMAPE": sMAPE,
26
+ "RMSE": RMSE,
27
+ "RMSSE": RMSSE,
28
+ "WAPE": WAPE,
29
+ "WQL": WQL,
30
+ # Exist for compatibility
31
+ "MSE": MSE,
32
+ "MAE": MAE,
33
+ }
34
+
35
+
36
+ def check_get_evaluation_metric(
37
+ eval_metric: Union[str, TimeSeriesScorer, Type[TimeSeriesScorer], None] = None
38
+ ) -> TimeSeriesScorer:
39
+ if isinstance(eval_metric, TimeSeriesScorer):
40
+ eval_metric = eval_metric
41
+ elif isinstance(eval_metric, type) and issubclass(eval_metric, TimeSeriesScorer):
42
+ # e.g., user passed `eval_metric=CustomMetric` instead of `eval_metric=CustomMetric()`
43
+ eval_metric = eval_metric()
44
+ elif isinstance(eval_metric, str):
45
+ if eval_metric.upper() not in AVAILABLE_METRICS:
46
+ raise ValueError(
47
+ f"Time series metric {eval_metric} not supported. Available metrics are:\n"
48
+ f"{json.dumps(list(AVAILABLE_METRICS.keys()), indent=2)}"
49
+ )
50
+ eval_metric = AVAILABLE_METRICS[eval_metric.upper()]()
51
+ elif eval_metric is None:
52
+ eval_metric = AVAILABLE_METRICS[DEFAULT_METRIC_NAME]()
53
+ else:
54
+ raise ValueError(
55
+ f"eval_metric must be of type str, TimeSeriesScorer or None "
56
+ f"(received eval_metric = {eval_metric} of type {type(eval_metric)})"
57
+ )
58
+ return eval_metric
@@ -0,0 +1,201 @@
1
+ from typing import Optional, Tuple
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from autogluon.timeseries import TimeSeriesDataFrame
7
+ from autogluon.timeseries.utils.datetime import get_seasonality
8
+ from autogluon.timeseries.utils.warning_filters import warning_filter
9
+
10
+
11
+ class TimeSeriesScorer:
12
+ """Base class for all evaluation metrics used in AutoGluon-TimeSeries.
13
+
14
+ This object always returns the metric in greater-is-better format.
15
+
16
+ Follows the design of ``autogluon.core.metrics.Scorer``.
17
+
18
+ Attributes
19
+ ----------
20
+ greater_is_better_internal : bool, default = False
21
+ Whether internal method :meth:`~autogluon.timeseries.metrics.TimeSeriesScorer.compute_metric` is
22
+ a loss function (default), meaning low is good, or a score function, meaning high is good.
23
+ optimum : float, default = 0.0
24
+ The best score achievable by the score function, i.e. maximum in case of scorer function and minimum in case of
25
+ loss function.
26
+ optimized_by_median : bool, default = False
27
+ Whether given point forecast metric is optimized by the median (if True) or expected value (if False). If True,
28
+ all models in AutoGluon-TimeSeries will attempt to paste median forecast into the "mean" column.
29
+ needs_quantile : bool, default = False
30
+ Whether the given metric uses the quantile predictions. Some models will modify the training procedure if they
31
+ are trained to optimize a quantile metric.
32
+ 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.
35
+ """
36
+
37
+ greater_is_better_internal: bool = False
38
+ optimum: float = 0.0
39
+ optimized_by_median: bool = False
40
+ needs_quantile: bool = False
41
+ equivalent_tabular_regression_metric: Optional[str] = None
42
+
43
+ @property
44
+ def sign(self) -> int:
45
+ return 1 if self.greater_is_better_internal else -1
46
+
47
+ @property
48
+ def name(self) -> str:
49
+ return f"{self.__class__.__name__}"
50
+
51
+ def __repr__(self) -> str:
52
+ return self.name
53
+
54
+ def __str__(self) -> str:
55
+ return self.name
56
+
57
+ @property
58
+ def name_with_sign(self) -> str:
59
+ if self.greater_is_better_internal:
60
+ prefix = ""
61
+ else:
62
+ prefix = "-"
63
+ return f"{prefix}{self.name}"
64
+
65
+ def __call__(
66
+ self,
67
+ data: TimeSeriesDataFrame,
68
+ predictions: TimeSeriesDataFrame,
69
+ prediction_length: int = 1,
70
+ target: str = "target",
71
+ seasonal_period: Optional[int] = None,
72
+ **kwargs,
73
+ ) -> 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()
80
+ assert data_future.index.equals(predictions.index), "Prediction and data indices do not match."
81
+
82
+ try:
83
+ with warning_filter():
84
+ self.save_past_metrics(
85
+ data_past=data_past,
86
+ target=target,
87
+ seasonal_period=seasonal_period,
88
+ **kwargs,
89
+ )
90
+ metric_value = self.compute_metric(
91
+ data_future=data_future,
92
+ predictions=predictions,
93
+ target=target,
94
+ **kwargs,
95
+ )
96
+ finally:
97
+ self.clear_past_metrics()
98
+ return metric_value * self.sign
99
+
100
+ score = __call__
101
+
102
+ def compute_metric(
103
+ self,
104
+ data_future: TimeSeriesDataFrame,
105
+ predictions: TimeSeriesDataFrame,
106
+ target: str = "target",
107
+ **kwargs,
108
+ ) -> float:
109
+ """Internal method that computes the metric for given forecast & actual data.
110
+
111
+ This method should be implemented by all custom metrics.
112
+
113
+ Parameters
114
+ ----------
115
+ data_future : TimeSeriesDataFrame
116
+ 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``.
118
+ predictions : TimeSeriesDataFrame
119
+ Data frame with predictions for the forecast horizon. Contain columns "mean" (point forecast) and the
120
+ columns corresponding to each of the quantile levels.
121
+ target : str, default = "target"
122
+ Name of the column in ``data_future`` that contains the target time series.
123
+
124
+ Returns
125
+ -------
126
+ score : float
127
+ Value of the metric for given forecast and data. If self.greater_is_better_internal is True, returns score
128
+ in greater-is-better format, otherwise in lower-is-better format.
129
+
130
+ """
131
+ raise NotImplementedError
132
+
133
+ def save_past_metrics(
134
+ self,
135
+ data_past: TimeSeriesDataFrame,
136
+ target: str = "target",
137
+ seasonal_period: int = 1,
138
+ **kwargs,
139
+ ) -> None:
140
+ """Compute auxiliary metrics on past data (before forecast horizon), if the chosen metric requires it.
141
+
142
+ This method should only be implemented by metrics that rely on historic (in-sample) data, such as Mean Absolute
143
+ Scaled Error (MASE) https://en.wikipedia.org/wiki/Mean_absolute_scaled_error.
144
+
145
+ We keep this method separate from :meth:`compute_metric` to avoid redundant computations when fitting ensemble.
146
+ """
147
+ pass
148
+
149
+ def clear_past_metrics(self) -> None:
150
+ """Clear auxiliary metrics saved in :meth:`save_past_metrics`.
151
+
152
+ This method should only be implemented if :meth:`save_past_metrics` has been implemented.
153
+ """
154
+ pass
155
+
156
+ def error(self, *args, **kwargs):
157
+ """Return error in lower-is-better format."""
158
+ return self.optimum - self.score(*args, **kwargs)
159
+
160
+ @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)
164
+
165
+ @staticmethod
166
+ def _get_point_forecast_score_inputs(
167
+ data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target"
168
+ ) -> Tuple[pd.Series, pd.Series]:
169
+ """Get inputs necessary to compute point forecast metrics.
170
+
171
+ Returns
172
+ -------
173
+ y_true : pd.Series, shape [num_items * prediction_length]
174
+ Target time series values during the forecast horizon.
175
+ y_pred : pd.Series, shape [num_items * prediction_length]
176
+ Predicted time series values during the forecast horizon.
177
+ """
178
+ y_true = data_future[target]
179
+ y_pred = predictions["mean"]
180
+ return y_true, y_pred
181
+
182
+ @staticmethod
183
+ def _get_quantile_forecast_score_inputs(
184
+ data_future: TimeSeriesDataFrame, predictions: TimeSeriesDataFrame, target: str = "target"
185
+ ) -> Tuple[pd.Series, pd.DataFrame, np.ndarray]:
186
+ """Get inputs necessary to compute quantile forecast metrics.
187
+
188
+ Returns
189
+ -------
190
+ y_true : pd.Series, shape [num_items * prediction_length]
191
+ Target time series values during the forecast horizon.
192
+ q_pred : pd.DataFrame, shape [num_items * prediction_length, num_quantiles]
193
+ Quantile forecast for each predicted quantile level. Column order corresponds to ``quantile_levels``.
194
+ quantile_levels : np.ndarray, shape [num_quantiles]
195
+ Quantile levels for which the forecasts are generated (as floats).
196
+ """
197
+ quantile_columns = [col for col in predictions.columns if col != "mean"]
198
+ y_true = data_future[target]
199
+ q_pred = predictions[quantile_columns]
200
+ quantile_levels = np.array(quantile_columns, dtype=float)
201
+ return y_true, q_pred, quantile_levels