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.
- autogluon/timeseries/evaluator.py +26 -249
- autogluon/timeseries/learner.py +5 -5
- autogluon/timeseries/metrics/__init__.py +58 -0
- autogluon/timeseries/metrics/abstract.py +201 -0
- autogluon/timeseries/metrics/point.py +156 -0
- autogluon/timeseries/metrics/quantile.py +26 -0
- autogluon/timeseries/metrics/utils.py +18 -0
- autogluon/timeseries/models/abstract/abstract_timeseries_model.py +43 -41
- autogluon/timeseries/models/abstract/model_trial.py +1 -1
- autogluon/timeseries/models/autogluon_tabular/mlforecast.py +28 -55
- autogluon/timeseries/models/ensemble/greedy_ensemble.py +27 -15
- autogluon/timeseries/models/gluonts/abstract_gluonts.py +1 -20
- autogluon/timeseries/models/local/abstract_local_model.py +1 -1
- autogluon/timeseries/models/multi_window/multi_window_model.py +4 -2
- autogluon/timeseries/models/presets.py +2 -1
- autogluon/timeseries/predictor.py +24 -15
- autogluon/timeseries/trainer/abstract_trainer.py +14 -22
- autogluon/timeseries/version.py +1 -1
- {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/METADATA +6 -5
- {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/RECORD +27 -22
- /autogluon.timeseries-0.8.3b20231024-py3.8-nspkg.pth → /autogluon.timeseries-0.8.3b20231027-py3.8-nspkg.pth +0 -0
- {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/LICENSE +0 -0
- {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/NOTICE +0 -0
- {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/WHEEL +0 -0
- {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/namespace_packages.txt +0 -0
- {autogluon.timeseries-0.8.3b20231024.dist-info → autogluon.timeseries-0.8.3b20231027.dist-info}/top_level.txt +0 -0
- {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
|
-
|
|
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.
|
|
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
|
-
"""
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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.
|
|
34
|
+
return self.eval_metric.sign
|
|
148
35
|
|
|
149
36
|
@property
|
|
150
37
|
def higher_is_better(self) -> bool:
|
|
151
|
-
return self.
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
autogluon/timeseries/learner.py
CHANGED
|
@@ -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.
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|