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,16 +1,21 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import logging
|
|
2
3
|
import math
|
|
3
|
-
import os
|
|
4
4
|
import time
|
|
5
|
-
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any, Callable, Collection, Type
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
import pandas as pd
|
|
9
10
|
from sklearn.base import BaseEstimator
|
|
10
11
|
|
|
11
12
|
import autogluon.core as ag
|
|
12
|
-
from autogluon.
|
|
13
|
-
from autogluon.
|
|
13
|
+
from autogluon.core.models import AbstractModel as AbstractTabularModel
|
|
14
|
+
from autogluon.features import AutoMLPipelineFeatureGenerator
|
|
15
|
+
from autogluon.tabular.registry import ag_model_registry
|
|
16
|
+
from autogluon.timeseries.dataset import TimeSeriesDataFrame
|
|
17
|
+
from autogluon.timeseries.metrics.abstract import TimeSeriesScorer
|
|
18
|
+
from autogluon.timeseries.metrics.utils import in_sample_squared_seasonal_error
|
|
14
19
|
from autogluon.timeseries.models.abstract import AbstractTimeSeriesModel
|
|
15
20
|
from autogluon.timeseries.models.local import SeasonalNaiveModel
|
|
16
21
|
from autogluon.timeseries.utils.datetime import (
|
|
@@ -18,52 +23,53 @@ from autogluon.timeseries.utils.datetime import (
|
|
|
18
23
|
get_seasonality,
|
|
19
24
|
get_time_features_for_frequency,
|
|
20
25
|
)
|
|
21
|
-
from autogluon.timeseries.utils.
|
|
22
|
-
from autogluon.timeseries.utils.warning_filters import warning_filter
|
|
26
|
+
from autogluon.timeseries.utils.warning_filters import set_loggers_level, warning_filter
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
MLF_TARGET = "y"
|
|
27
|
-
MLF_ITEMID = "unique_id"
|
|
28
|
-
MLF_TIMESTAMP = "ds"
|
|
28
|
+
from .utils import MLF_ITEMID, MLF_TARGET, MLF_TIMESTAMP
|
|
29
29
|
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
30
31
|
|
|
31
|
-
class TabularEstimator(BaseEstimator):
|
|
32
|
-
"""Scikit-learn compatible interface for TabularPredictor."""
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.predictor_fit_kwargs = predictor_fit_kwargs if predictor_fit_kwargs is not None else {}
|
|
33
|
+
class TabularModel(BaseEstimator):
|
|
34
|
+
"""A scikit-learn compatible wrapper for arbitrary autogluon.tabular models"""
|
|
37
35
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
36
|
+
def __init__(self, model_class: Type[AbstractTabularModel], model_kwargs: dict | None = None):
|
|
37
|
+
self.model_class = model_class
|
|
38
|
+
self.model_kwargs = {} if model_kwargs is None else model_kwargs
|
|
39
|
+
self.feature_pipeline = AutoMLPipelineFeatureGenerator(verbosity=0)
|
|
43
40
|
|
|
44
|
-
def fit(self, X: pd.DataFrame, y: pd.Series
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
self.predictor.fit(df, **self.predictor_fit_kwargs)
|
|
41
|
+
def fit(self, X: pd.DataFrame, y: pd.Series, X_val: pd.DataFrame, y_val: pd.Series, **kwargs):
|
|
42
|
+
self.model = self.model_class(**self.model_kwargs)
|
|
43
|
+
X = self.feature_pipeline.fit_transform(X=X)
|
|
44
|
+
X_val = self.feature_pipeline.transform(X=X_val)
|
|
45
|
+
self.model.fit(X=X, y=y, X_val=X_val, y_val=y_val, **kwargs)
|
|
50
46
|
return self
|
|
51
47
|
|
|
52
|
-
def predict(self, X: pd.DataFrame)
|
|
53
|
-
|
|
54
|
-
return self.
|
|
48
|
+
def predict(self, X: pd.DataFrame, **kwargs):
|
|
49
|
+
X = self.feature_pipeline.transform(X=X)
|
|
50
|
+
return self.model.predict(X=X, **kwargs)
|
|
51
|
+
|
|
52
|
+
def get_params(self, deep=True):
|
|
53
|
+
params = {"model_class": self.model_class, "model_kwargs": self.model_kwargs}
|
|
54
|
+
if deep:
|
|
55
|
+
return copy.deepcopy(params)
|
|
56
|
+
else:
|
|
57
|
+
return params
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
61
|
+
_supports_known_covariates = True
|
|
62
|
+
_supports_static_features = True
|
|
63
|
+
|
|
58
64
|
def __init__(
|
|
59
65
|
self,
|
|
60
|
-
freq:
|
|
66
|
+
freq: str | None = None,
|
|
61
67
|
prediction_length: int = 1,
|
|
62
|
-
path:
|
|
63
|
-
name:
|
|
64
|
-
eval_metric: str = None,
|
|
65
|
-
hyperparameters:
|
|
66
|
-
**kwargs,
|
|
68
|
+
path: str | None = None,
|
|
69
|
+
name: str | None = None,
|
|
70
|
+
eval_metric: str | TimeSeriesScorer | None = None,
|
|
71
|
+
hyperparameters: dict[str, Any] | None = None,
|
|
72
|
+
**kwargs,
|
|
67
73
|
):
|
|
68
74
|
super().__init__(
|
|
69
75
|
path=path,
|
|
@@ -78,44 +84,90 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
78
84
|
from mlforecast.target_transforms import BaseTargetTransform
|
|
79
85
|
|
|
80
86
|
self._sum_of_differences: int = 0 # number of time steps removed from each series by differencing
|
|
81
|
-
self._max_ts_length:
|
|
82
|
-
self._target_lags:
|
|
83
|
-
self._date_features:
|
|
84
|
-
self._mlf:
|
|
85
|
-
self._scaler:
|
|
86
|
-
self._residuals_std_per_item:
|
|
87
|
-
self.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
self._max_ts_length: int | None = None
|
|
88
|
+
self._target_lags: np.ndarray
|
|
89
|
+
self._date_features: list[Callable]
|
|
90
|
+
self._mlf: MLForecast
|
|
91
|
+
self._scaler: BaseTargetTransform | None = None
|
|
92
|
+
self._residuals_std_per_item: pd.Series
|
|
93
|
+
self._train_target_median: float | None = None
|
|
94
|
+
self._non_boolean_real_covariates: list[str] = []
|
|
95
|
+
|
|
96
|
+
def _initialize_transforms_and_regressor(self):
|
|
97
|
+
super()._initialize_transforms_and_regressor()
|
|
98
|
+
# Do not create a scaler in the model, scaler will be passed to MLForecast
|
|
99
|
+
self.target_scaler = None
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
@property
|
|
102
|
+
def allowed_hyperparameters(self) -> list[str]:
|
|
103
|
+
return super().allowed_hyperparameters + [
|
|
104
|
+
"lags",
|
|
105
|
+
"date_features",
|
|
106
|
+
"differences",
|
|
107
|
+
"model_name",
|
|
108
|
+
"model_hyperparameters",
|
|
109
|
+
"max_num_items",
|
|
110
|
+
"max_num_samples",
|
|
111
|
+
"lag_transforms",
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
def preprocess(
|
|
115
|
+
self,
|
|
116
|
+
data: TimeSeriesDataFrame,
|
|
117
|
+
known_covariates: TimeSeriesDataFrame | None = None,
|
|
118
|
+
is_train: bool = False,
|
|
119
|
+
**kwargs,
|
|
120
|
+
) -> tuple[TimeSeriesDataFrame, TimeSeriesDataFrame | None]:
|
|
121
|
+
if is_train:
|
|
122
|
+
# All-NaN series are removed; partially-NaN series in train_data are handled inside _generate_train_val_dfs
|
|
123
|
+
all_nan_items = data.item_ids[
|
|
124
|
+
data[self.target].isna().groupby(TimeSeriesDataFrame.ITEMID, sort=False).all()
|
|
125
|
+
]
|
|
126
|
+
if len(all_nan_items):
|
|
127
|
+
data = data.query("item_id not in @all_nan_items")
|
|
128
|
+
else:
|
|
129
|
+
data = data.fill_missing_values()
|
|
130
|
+
# Fill time series consisting of all NaNs with the median of target in train_data
|
|
131
|
+
if data.isna().any(axis=None):
|
|
132
|
+
data[self.target] = data[self.target].fillna(value=self._train_target_median)
|
|
133
|
+
return data, known_covariates
|
|
99
134
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
def _get_default_hyperparameters(self) -> dict[str, Any]:
|
|
136
|
+
return {
|
|
137
|
+
"max_num_items": 20_000,
|
|
138
|
+
"max_num_samples": 1_000_000,
|
|
139
|
+
"model_name": "GBM",
|
|
140
|
+
"model_hyperparameters": {},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def _create_tabular_model(self, model_name: str, model_hyperparameters: dict[str, Any]) -> TabularModel:
|
|
144
|
+
raise NotImplementedError
|
|
145
|
+
|
|
146
|
+
def _get_mlforecast_init_args(
|
|
147
|
+
self, train_data: TimeSeriesDataFrame, model_params: dict[str, Any]
|
|
148
|
+
) -> dict[str, Any]:
|
|
103
149
|
from mlforecast.target_transforms import Differences
|
|
104
150
|
|
|
105
|
-
from .
|
|
151
|
+
from .transforms import MLForecastScaler
|
|
106
152
|
|
|
107
153
|
lags = model_params.get("lags")
|
|
108
154
|
if lags is None:
|
|
155
|
+
assert self.freq is not None
|
|
109
156
|
lags = get_lags_for_frequency(self.freq)
|
|
110
157
|
self._target_lags = np.array(sorted(set(lags)), dtype=np.int64)
|
|
111
158
|
|
|
112
159
|
date_features = model_params.get("date_features")
|
|
113
160
|
if date_features is None:
|
|
114
161
|
date_features = get_time_features_for_frequency(self.freq)
|
|
115
|
-
|
|
162
|
+
known_covariates = self.covariate_metadata.known_covariates
|
|
163
|
+
conflicting = [f.__name__ for f in date_features if f.__name__ in known_covariates]
|
|
164
|
+
if conflicting:
|
|
165
|
+
logger.info(f"\tRemoved automatic date_features {conflicting} since they clash with known_covariates")
|
|
166
|
+
self._date_features = [f for f in date_features if f.__name__ not in known_covariates]
|
|
116
167
|
|
|
117
168
|
target_transforms = []
|
|
118
169
|
differences = model_params.get("differences")
|
|
170
|
+
assert isinstance(differences, Collection)
|
|
119
171
|
|
|
120
172
|
ts_lengths = train_data.num_timesteps_per_item()
|
|
121
173
|
required_ts_length = sum(differences) + 1
|
|
@@ -132,25 +184,22 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
132
184
|
target_transforms.append(Differences(differences))
|
|
133
185
|
self._sum_of_differences = sum(differences)
|
|
134
186
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
self._scaler = StandardScaler()
|
|
140
|
-
elif scaler_name == "mean_abs":
|
|
141
|
-
self._scaler = MeanAbsScaler()
|
|
142
|
-
else:
|
|
143
|
-
logger.warning(
|
|
144
|
-
f"Unrecognized `scaler` {scaler_name} (supported options: ['standard', 'mean_abs', None]). Scaling disabled."
|
|
187
|
+
if "target_scaler" in model_params and "scaler" in model_params:
|
|
188
|
+
warnings.warn(
|
|
189
|
+
f"Both 'target_scaler' and 'scaler' hyperparameters are provided to {self.__class__.__name__}. "
|
|
190
|
+
"Please only set the 'target_scaler' parameter."
|
|
145
191
|
)
|
|
146
|
-
|
|
147
|
-
|
|
192
|
+
# Support "scaler" for backward compatibility
|
|
193
|
+
scaler_type = model_params.get("target_scaler", model_params.get("scaler"))
|
|
194
|
+
if scaler_type is not None:
|
|
195
|
+
self._scaler = MLForecastScaler(scaler_type=scaler_type)
|
|
148
196
|
target_transforms.append(self._scaler)
|
|
149
197
|
|
|
150
198
|
return {
|
|
151
199
|
"lags": self._target_lags.tolist(),
|
|
152
200
|
"date_features": self._date_features,
|
|
153
201
|
"target_transforms": target_transforms,
|
|
202
|
+
"lag_transforms": model_params.get("lag_transforms"),
|
|
154
203
|
}
|
|
155
204
|
|
|
156
205
|
def _mask_df(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -162,13 +211,13 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
162
211
|
return df
|
|
163
212
|
|
|
164
213
|
@staticmethod
|
|
165
|
-
def _shorten_all_series(mlforecast_df: pd.DataFrame, max_length: int):
|
|
214
|
+
def _shorten_all_series(mlforecast_df: pd.DataFrame, max_length: int) -> pd.DataFrame:
|
|
166
215
|
logger.debug(f"Shortening all series to at most {max_length}")
|
|
167
216
|
return mlforecast_df.groupby(MLF_ITEMID, as_index=False, sort=False).tail(max_length)
|
|
168
217
|
|
|
169
218
|
def _generate_train_val_dfs(
|
|
170
|
-
self, data: TimeSeriesDataFrame, max_num_items:
|
|
171
|
-
) ->
|
|
219
|
+
self, data: TimeSeriesDataFrame, max_num_items: int | None = None, max_num_samples: int | None = None
|
|
220
|
+
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
172
221
|
# Exclude items that are too short for chosen differences - otherwise exception will be raised
|
|
173
222
|
if self._sum_of_differences > 0:
|
|
174
223
|
ts_lengths = data.num_timesteps_per_item()
|
|
@@ -181,6 +230,10 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
181
230
|
items_to_keep = data.item_ids.to_series().sample(n=int(max_num_items)) # noqa: F841
|
|
182
231
|
data = data.query("item_id in @items_to_keep")
|
|
183
232
|
|
|
233
|
+
# MLForecast.preprocess does not support missing values, but we will exclude them later from the training set
|
|
234
|
+
missing_entries = data.index[data[self.target].isna()]
|
|
235
|
+
data = data.fill_missing_values()
|
|
236
|
+
|
|
184
237
|
num_items = data.num_items
|
|
185
238
|
mlforecast_df = self._to_mlforecast_df(data, data.static_features)
|
|
186
239
|
|
|
@@ -193,10 +246,14 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
193
246
|
# Unless we set static_features=[], MLForecast interprets all known covariates as static features
|
|
194
247
|
df = self._mlf.preprocess(mlforecast_df, dropna=False, static_features=[])
|
|
195
248
|
# df.query results in 2x memory saving compared to df.dropna(subset="y")
|
|
196
|
-
df = df.query("y.notnull()")
|
|
249
|
+
df = df.query("y.notnull()") # type: ignore
|
|
197
250
|
|
|
198
251
|
df = self._mask_df(df)
|
|
199
252
|
|
|
253
|
+
# We remove originally missing values filled via imputation from the training set
|
|
254
|
+
if len(missing_entries):
|
|
255
|
+
df = df.set_index(["unique_id", "ds"]).drop(missing_entries, errors="ignore").reset_index()
|
|
256
|
+
|
|
200
257
|
if max_num_samples is not None and len(df) > max_num_samples:
|
|
201
258
|
df = df.sample(n=max_num_samples)
|
|
202
259
|
|
|
@@ -208,12 +265,12 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
208
265
|
val_df = grouped_df.tail(val_rows_per_item)
|
|
209
266
|
logger.debug(f"train_df shape: {train_df.shape}, val_df shape: {val_df.shape}")
|
|
210
267
|
|
|
211
|
-
return train_df.drop(columns=[MLF_TIMESTAMP]), val_df.drop(columns=[MLF_TIMESTAMP])
|
|
268
|
+
return train_df.drop(columns=[MLF_TIMESTAMP]), val_df.drop(columns=[MLF_TIMESTAMP]) # type: ignore
|
|
212
269
|
|
|
213
270
|
def _to_mlforecast_df(
|
|
214
271
|
self,
|
|
215
272
|
data: TimeSeriesDataFrame,
|
|
216
|
-
static_features: pd.DataFrame,
|
|
273
|
+
static_features: pd.DataFrame | None,
|
|
217
274
|
include_target: bool = True,
|
|
218
275
|
) -> pd.DataFrame:
|
|
219
276
|
"""Convert TimeSeriesDataFrame to a format expected by MLForecast methods `predict` and `preprocess`.
|
|
@@ -221,15 +278,33 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
221
278
|
Each row contains unique_id, ds, y, and (optionally) known covariates & static features.
|
|
222
279
|
"""
|
|
223
280
|
# TODO: Add support for past_covariates
|
|
224
|
-
selected_columns = self.
|
|
225
|
-
column_name_mapping = {ITEMID: MLF_ITEMID, TIMESTAMP: MLF_TIMESTAMP}
|
|
281
|
+
selected_columns = self.covariate_metadata.known_covariates.copy()
|
|
282
|
+
column_name_mapping = {TimeSeriesDataFrame.ITEMID: MLF_ITEMID, TimeSeriesDataFrame.TIMESTAMP: MLF_TIMESTAMP}
|
|
226
283
|
if include_target:
|
|
227
284
|
selected_columns += [self.target]
|
|
228
285
|
column_name_mapping[self.target] = MLF_TARGET
|
|
229
286
|
|
|
230
287
|
df = pd.DataFrame(data)[selected_columns].reset_index()
|
|
231
288
|
if static_features is not None:
|
|
232
|
-
df = pd.merge(
|
|
289
|
+
df = pd.merge(
|
|
290
|
+
df, static_features, how="left", on=TimeSeriesDataFrame.ITEMID, suffixes=(None, "_static_feat")
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
for col in self._non_boolean_real_covariates:
|
|
294
|
+
# Normalize non-boolean features using mean_abs scaling
|
|
295
|
+
df[f"__scaled_{col}"] = (
|
|
296
|
+
df[col]
|
|
297
|
+
/ df[col]
|
|
298
|
+
.abs()
|
|
299
|
+
.groupby(df[TimeSeriesDataFrame.ITEMID])
|
|
300
|
+
.mean()
|
|
301
|
+
.reindex(df[TimeSeriesDataFrame.ITEMID])
|
|
302
|
+
.values
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Convert float64 to float32 to reduce memory usage
|
|
306
|
+
float64_cols = list(df.select_dtypes(include="float64"))
|
|
307
|
+
df[float64_cols] = df[float64_cols].astype("float32")
|
|
233
308
|
|
|
234
309
|
# We assume that df is sorted by 'unique_id' inside `TimeSeriesPredictor._check_and_prepare_data_frame`
|
|
235
310
|
return df.rename(columns=column_name_mapping)
|
|
@@ -237,19 +312,26 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
237
312
|
def _fit(
|
|
238
313
|
self,
|
|
239
314
|
train_data: TimeSeriesDataFrame,
|
|
240
|
-
val_data:
|
|
241
|
-
time_limit:
|
|
315
|
+
val_data: TimeSeriesDataFrame | None = None,
|
|
316
|
+
time_limit: float | None = None,
|
|
317
|
+
num_cpus: int | None = None,
|
|
318
|
+
num_gpus: int | None = None,
|
|
242
319
|
verbosity: int = 2,
|
|
243
320
|
**kwargs,
|
|
244
321
|
) -> None:
|
|
245
322
|
from mlforecast import MLForecast
|
|
246
323
|
|
|
247
324
|
self._check_fit_params()
|
|
325
|
+
self._log_unused_hyperparameters()
|
|
248
326
|
fit_start_time = time.time()
|
|
249
|
-
|
|
250
|
-
|
|
327
|
+
self._train_target_median = train_data[self.target].median()
|
|
328
|
+
for col in self.covariate_metadata.known_covariates_real:
|
|
329
|
+
if not set(train_data[col].unique()) == set([0, 1]):
|
|
330
|
+
self._non_boolean_real_covariates.append(col)
|
|
331
|
+
model_params = self.get_hyperparameters()
|
|
251
332
|
|
|
252
333
|
mlforecast_init_args = self._get_mlforecast_init_args(train_data, model_params)
|
|
334
|
+
assert self.freq is not None
|
|
253
335
|
self._mlf = MLForecast(models={}, freq=self.freq, **mlforecast_init_args)
|
|
254
336
|
|
|
255
337
|
# We generate train/val splits from train_data and ignore val_data to avoid overfitting
|
|
@@ -259,54 +341,65 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
259
341
|
max_num_samples=model_params["max_num_samples"],
|
|
260
342
|
)
|
|
261
343
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
},
|
|
275
|
-
)
|
|
276
|
-
self._mlf.models = {"mean": estimator}
|
|
344
|
+
with set_loggers_level(regex=r"^autogluon\.(tabular|features).*", level=logging.ERROR):
|
|
345
|
+
tabular_model = self._create_tabular_model(
|
|
346
|
+
model_name=model_params["model_name"], model_hyperparameters=model_params["model_hyperparameters"]
|
|
347
|
+
)
|
|
348
|
+
tabular_model.fit(
|
|
349
|
+
X=train_df.drop(columns=[MLF_TARGET, MLF_ITEMID]),
|
|
350
|
+
y=train_df[MLF_TARGET],
|
|
351
|
+
X_val=val_df.drop(columns=[MLF_TARGET, MLF_ITEMID]),
|
|
352
|
+
y_val=val_df[MLF_TARGET],
|
|
353
|
+
time_limit=(None if time_limit is None else time_limit - (time.time() - fit_start_time)),
|
|
354
|
+
verbosity=verbosity - 1,
|
|
355
|
+
)
|
|
277
356
|
|
|
278
|
-
|
|
279
|
-
|
|
357
|
+
# We directly insert the trained model into models_ since calling _mlf.fit_models does not support X_val, y_val
|
|
358
|
+
self._mlf.models_ = {"mean": tabular_model}
|
|
280
359
|
|
|
281
360
|
self._save_residuals_std(val_df)
|
|
282
361
|
|
|
362
|
+
def get_tabular_model(self) -> TabularModel:
|
|
363
|
+
"""Get the underlying tabular regression model."""
|
|
364
|
+
assert "mean" in self._mlf.models_, "Call `fit` before calling `get_tabular_model`"
|
|
365
|
+
mean_estimator = self._mlf.models_["mean"]
|
|
366
|
+
assert isinstance(mean_estimator, TabularModel)
|
|
367
|
+
return mean_estimator
|
|
368
|
+
|
|
283
369
|
def _save_residuals_std(self, val_df: pd.DataFrame) -> None:
|
|
284
370
|
"""Compute standard deviation of residuals for each item using the validation set.
|
|
285
371
|
|
|
286
|
-
Saves per-item residuals to `self.residuals_std_per_item
|
|
372
|
+
Saves per-item residuals to `self.residuals_std_per_item`.
|
|
287
373
|
"""
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
self._avg_residuals_std = np.sqrt(residuals.pow(2.0).mean())
|
|
374
|
+
residuals_df = val_df[[MLF_ITEMID, MLF_TARGET]]
|
|
375
|
+
mean_estimator = self.get_tabular_model()
|
|
291
376
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
377
|
+
residuals_df = residuals_df.assign(y_pred=mean_estimator.predict(val_df))
|
|
378
|
+
if self._scaler is not None:
|
|
379
|
+
# Scaler expects to find column MLF_TIMESTAMP even though it's not used - fill with dummy
|
|
380
|
+
residuals_df = residuals_df.assign(**{MLF_TIMESTAMP: np.datetime64("2010-01-01")})
|
|
381
|
+
residuals_df = self._scaler.inverse_transform(residuals_df)
|
|
382
|
+
|
|
383
|
+
assert isinstance(residuals_df, pd.DataFrame)
|
|
384
|
+
residuals = residuals_df[MLF_TARGET] - residuals_df["y_pred"]
|
|
385
|
+
self._residuals_std_per_item = (
|
|
386
|
+
residuals.pow(2.0).groupby(val_df[MLF_ITEMID].values, sort=False).mean().pow(0.5) # type: ignore
|
|
387
|
+
)
|
|
295
388
|
|
|
296
389
|
def _remove_short_ts_and_generate_fallback_forecast(
|
|
297
390
|
self,
|
|
298
391
|
data: TimeSeriesDataFrame,
|
|
299
|
-
known_covariates:
|
|
300
|
-
) ->
|
|
392
|
+
known_covariates: TimeSeriesDataFrame | None = None,
|
|
393
|
+
) -> tuple[TimeSeriesDataFrame, TimeSeriesDataFrame, TimeSeriesDataFrame | None]:
|
|
301
394
|
"""Remove series that are too short for chosen differencing from data and generate naive forecast for them.
|
|
302
395
|
|
|
303
396
|
Returns
|
|
304
397
|
-------
|
|
305
|
-
data_long
|
|
398
|
+
data_long
|
|
306
399
|
Data containing only time series that are long enough for the model to predict.
|
|
307
|
-
known_covariates_long
|
|
400
|
+
known_covariates_long
|
|
308
401
|
Future known covariates containing only time series that are long enough for the model to predict.
|
|
309
|
-
forecast_for_short_series
|
|
402
|
+
forecast_for_short_series
|
|
310
403
|
Seasonal naive forecast for short series, if there are any in the dataset.
|
|
311
404
|
"""
|
|
312
405
|
ts_lengths = data.num_timesteps_per_item()
|
|
@@ -318,7 +411,12 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
318
411
|
"Fallback model SeasonalNaive is used for these time series."
|
|
319
412
|
)
|
|
320
413
|
data_short = data.query("item_id in @short_series")
|
|
321
|
-
seasonal_naive = SeasonalNaiveModel(
|
|
414
|
+
seasonal_naive = SeasonalNaiveModel(
|
|
415
|
+
freq=self.freq,
|
|
416
|
+
prediction_length=self.prediction_length,
|
|
417
|
+
target=self.target,
|
|
418
|
+
quantile_levels=self.quantile_levels,
|
|
419
|
+
)
|
|
322
420
|
seasonal_naive.fit(train_data=data_short)
|
|
323
421
|
forecast_for_short_series = seasonal_naive.predict(data_short)
|
|
324
422
|
|
|
@@ -333,35 +431,41 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
333
431
|
forecast_for_short_series = None
|
|
334
432
|
return data_long, known_covariates_long, forecast_for_short_series
|
|
335
433
|
|
|
336
|
-
def _add_gaussian_quantiles(
|
|
434
|
+
def _add_gaussian_quantiles(
|
|
435
|
+
self, predictions: pd.DataFrame, repeated_item_ids: pd.Series, past_target: pd.Series
|
|
436
|
+
) -> pd.DataFrame:
|
|
337
437
|
"""
|
|
338
438
|
Add quantile levels assuming that residuals follow normal distribution
|
|
339
439
|
"""
|
|
340
440
|
from scipy.stats import norm
|
|
341
441
|
|
|
342
|
-
scale_per_item = self._get_scale_per_item(repeated_item_ids.unique())
|
|
343
442
|
num_items = int(len(predictions) / self.prediction_length)
|
|
344
443
|
sqrt_h = np.sqrt(np.arange(1, self.prediction_length + 1))
|
|
345
444
|
# Series where normal_scale_per_timestep.loc[item_id].loc[N] = sqrt(1 + N) for N in range(prediction_length)
|
|
346
445
|
normal_scale_per_timestep = pd.Series(np.tile(sqrt_h, num_items), index=repeated_item_ids)
|
|
347
446
|
|
|
348
447
|
residuals_std_per_timestep = self._residuals_std_per_item.reindex(repeated_item_ids)
|
|
349
|
-
# Use
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
448
|
+
# Use in-sample seasonal error in for items not seen during fit
|
|
449
|
+
items_not_seen_during_fit = residuals_std_per_timestep.index[residuals_std_per_timestep.isna()].unique()
|
|
450
|
+
if len(items_not_seen_during_fit) > 0:
|
|
451
|
+
scale_for_new_items: pd.Series = in_sample_squared_seasonal_error(
|
|
452
|
+
y_past=past_target.loc[items_not_seen_during_fit]
|
|
453
|
+
).pow(0.5)
|
|
454
|
+
residuals_std_per_timestep = residuals_std_per_timestep.fillna(scale_for_new_items)
|
|
455
|
+
|
|
456
|
+
std_per_timestep = residuals_std_per_timestep * normal_scale_per_timestep
|
|
353
457
|
for q in self.quantile_levels:
|
|
354
458
|
predictions[str(q)] = predictions["mean"] + norm.ppf(q) * std_per_timestep.to_numpy()
|
|
355
459
|
return predictions
|
|
356
460
|
|
|
357
|
-
def _more_tags(self) -> dict:
|
|
358
|
-
return {"can_refit_full": True}
|
|
461
|
+
def _more_tags(self) -> dict[str, Any]:
|
|
462
|
+
return {"allow_nan": True, "can_refit_full": True}
|
|
359
463
|
|
|
360
464
|
|
|
361
465
|
class DirectTabularModel(AbstractMLForecastModel):
|
|
362
|
-
"""Predict all future time series values simultaneously using
|
|
466
|
+
"""Predict all future time series values simultaneously using a regression model from AutoGluon-Tabular.
|
|
363
467
|
|
|
364
|
-
A single
|
|
468
|
+
A single tabular model is used to forecast all future time series values using the following features:
|
|
365
469
|
|
|
366
470
|
- lag features (observed time series values) based on ``freq`` of the data
|
|
367
471
|
- time features (e.g., day of the week) based on the timestamp of the measurement
|
|
@@ -370,8 +474,8 @@ class DirectTabularModel(AbstractMLForecastModel):
|
|
|
370
474
|
|
|
371
475
|
Features not known during the forecast horizon (e.g., future target values) are replaced by NaNs.
|
|
372
476
|
|
|
373
|
-
If ``eval_metric.needs_quantile``, the
|
|
374
|
-
Otherwise,
|
|
477
|
+
If ``eval_metric.needs_quantile``, the tabular regression model will be trained with ``"quantile"`` problem type.
|
|
478
|
+
Otherwise, the model will be trained with ``"regression"`` problem type, and dummy quantiles will be
|
|
375
479
|
obtained by assuming that the residuals follow zero-mean normal distribution.
|
|
376
480
|
|
|
377
481
|
Based on the `mlforecast <https://github.com/Nixtla/mlforecast>`_ library.
|
|
@@ -379,45 +483,55 @@ class DirectTabularModel(AbstractMLForecastModel):
|
|
|
379
483
|
|
|
380
484
|
Other Parameters
|
|
381
485
|
----------------
|
|
382
|
-
lags :
|
|
486
|
+
lags : list[int], default = None
|
|
383
487
|
Lags of the target that will be used as features for predictions. If None, will be determined automatically
|
|
384
488
|
based on the frequency of the data.
|
|
385
|
-
date_features :
|
|
489
|
+
date_features : list[str | Callable], default = None
|
|
386
490
|
Features computed from the dates. Can be pandas date attributes or functions that will take the dates as input.
|
|
387
491
|
If None, will be determined automatically based on the frequency of the data.
|
|
388
|
-
differences :
|
|
492
|
+
differences : list[int], default = []
|
|
389
493
|
Differences to take of the target before computing the features. These are restored at the forecasting step.
|
|
390
|
-
If None, will be set to ``[seasonal_period]``, where seasonal_period is determined based on the data frequency.
|
|
391
494
|
Defaults to no differencing.
|
|
392
|
-
|
|
393
|
-
Scaling applied to each time series.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
495
|
+
target_scaler : {"standard", "mean_abs", "min_max", "robust", None}, default = "mean_abs"
|
|
496
|
+
Scaling applied to each time series. Scaling is applied after differencing.
|
|
497
|
+
model_name : str, default = "GBM"
|
|
498
|
+
Name of the tabular regression model. See ``autogluon.tabular.registry.ag_model_registry`` or
|
|
499
|
+
`the documentation <https://auto.gluon.ai/stable/api/autogluon.tabular.models.html>`_ for the list of available
|
|
500
|
+
tabular models.
|
|
501
|
+
model_hyperparameters : dict[str, Any], optional
|
|
502
|
+
Hyperparameters passed to the tabular regression model.
|
|
399
503
|
max_num_items : int or None, default = 20_000
|
|
400
504
|
If not None, the model will randomly select this many time series for training and validation.
|
|
401
505
|
max_num_samples : int or None, default = 1_000_000
|
|
402
|
-
If not None, training dataset passed to
|
|
403
|
-
end of each time series).
|
|
506
|
+
If not None, training dataset passed to the tabular regression model will contain at most this many rows
|
|
507
|
+
(starting from the end of each time series).
|
|
404
508
|
"""
|
|
405
509
|
|
|
510
|
+
ag_priority = 85
|
|
511
|
+
|
|
406
512
|
@property
|
|
407
513
|
def is_quantile_model(self) -> bool:
|
|
408
514
|
return self.eval_metric.needs_quantile
|
|
409
515
|
|
|
410
|
-
def
|
|
411
|
-
model_params = super().
|
|
412
|
-
|
|
413
|
-
|
|
516
|
+
def get_hyperparameters(self) -> dict[str, Any]:
|
|
517
|
+
model_params = super().get_hyperparameters()
|
|
518
|
+
# We don't set 'target_scaler' if user already provided 'scaler' to avoid overriding the user-provided value
|
|
519
|
+
if "scaler" not in model_params:
|
|
520
|
+
model_params.setdefault("target_scaler", "mean_abs")
|
|
521
|
+
if "differences" not in model_params or model_params["differences"] is None:
|
|
522
|
+
model_params["differences"] = []
|
|
523
|
+
if "lag_transforms" in model_params:
|
|
524
|
+
model_params.pop("lag_transforms")
|
|
525
|
+
logger.warning(f"{self.name} does not support the 'lag_transforms' hyperparameter.")
|
|
414
526
|
return model_params
|
|
415
527
|
|
|
416
528
|
def _mask_df(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
417
529
|
"""Apply a mask that mimics the situation at prediction time when target/covariates are unknown during the
|
|
418
530
|
forecast horizon.
|
|
419
531
|
"""
|
|
420
|
-
|
|
532
|
+
# Fix seed to make the model deterministic
|
|
533
|
+
rng = np.random.default_rng(seed=123)
|
|
534
|
+
num_hidden = rng.integers(0, self.prediction_length, size=len(df))
|
|
421
535
|
lag_cols = [f"lag{lag}" for lag in self._target_lags]
|
|
422
536
|
mask = num_hidden[:, None] < self._target_lags[None] # shape [len(num_hidden), len(_target_lags)]
|
|
423
537
|
# use df.loc[:, lag_cols] instead of df[lag_cols] to avoid SettingWithCopyWarning
|
|
@@ -428,41 +542,46 @@ class DirectTabularModel(AbstractMLForecastModel):
|
|
|
428
542
|
if self.is_quantile_model:
|
|
429
543
|
# Quantile model does not require residuals to produce prediction intervals
|
|
430
544
|
self._residuals_std_per_item = pd.Series(1.0, index=val_df[MLF_ITEMID].unique())
|
|
431
|
-
self._avg_residuals_std = 1.0
|
|
432
545
|
else:
|
|
433
546
|
super()._save_residuals_std(val_df=val_df)
|
|
434
547
|
|
|
435
548
|
def _predict(
|
|
436
549
|
self,
|
|
437
550
|
data: TimeSeriesDataFrame,
|
|
438
|
-
known_covariates:
|
|
551
|
+
known_covariates: TimeSeriesDataFrame | None = None,
|
|
439
552
|
**kwargs,
|
|
440
553
|
) -> TimeSeriesDataFrame:
|
|
554
|
+
from .transforms import apply_inverse_transform
|
|
555
|
+
|
|
441
556
|
original_item_id_order = data.item_ids
|
|
442
557
|
data, known_covariates, forecast_for_short_series = self._remove_short_ts_and_generate_fallback_forecast(
|
|
443
558
|
data=data, known_covariates=known_covariates
|
|
444
559
|
)
|
|
445
560
|
if len(data) == 0:
|
|
446
561
|
# All time series are too short for chosen differences
|
|
562
|
+
assert forecast_for_short_series is not None
|
|
447
563
|
return forecast_for_short_series
|
|
448
564
|
|
|
449
565
|
if known_covariates is not None:
|
|
450
566
|
data_future = known_covariates.copy()
|
|
451
567
|
else:
|
|
452
|
-
future_index =
|
|
568
|
+
future_index = self.get_forecast_horizon_index(data)
|
|
453
569
|
data_future = pd.DataFrame(columns=[self.target], index=future_index, dtype="float32")
|
|
454
570
|
# MLForecast raises exception of target contains NaN. We use inf as placeholder, replace them by NaN afterwards
|
|
455
571
|
data_future[self.target] = float("inf")
|
|
456
572
|
data_extended = pd.concat([data, data_future])
|
|
457
|
-
mlforecast_df = self._to_mlforecast_df(data_extended, data.static_features)
|
|
573
|
+
mlforecast_df = self._to_mlforecast_df(data_extended, data.static_features) # type: ignore
|
|
458
574
|
if self._max_ts_length is not None:
|
|
459
575
|
# We appended `prediction_length` time steps to each series, so increase length
|
|
460
576
|
mlforecast_df = self._shorten_all_series(mlforecast_df, self._max_ts_length + self.prediction_length)
|
|
461
577
|
df = self._mlf.preprocess(mlforecast_df, dropna=False, static_features=[])
|
|
578
|
+
assert isinstance(df, pd.DataFrame)
|
|
579
|
+
|
|
462
580
|
df = df.groupby(MLF_ITEMID, sort=False).tail(self.prediction_length)
|
|
463
581
|
df = df.replace(float("inf"), float("nan"))
|
|
464
582
|
|
|
465
|
-
|
|
583
|
+
mean_estimator = self.get_tabular_model()
|
|
584
|
+
raw_predictions = mean_estimator.predict(df)
|
|
466
585
|
predictions = self._postprocess_predictions(raw_predictions, repeated_item_ids=df[MLF_ITEMID])
|
|
467
586
|
# Paste columns one by one to preserve dtypes
|
|
468
587
|
predictions[MLF_ITEMID] = df[MLF_ITEMID].values
|
|
@@ -473,57 +592,72 @@ class DirectTabularModel(AbstractMLForecastModel):
|
|
|
473
592
|
mlforecast_df_past = self._to_mlforecast_df(data, None)
|
|
474
593
|
if self._max_ts_length is not None:
|
|
475
594
|
mlforecast_df_past = self._shorten_all_series(mlforecast_df_past, self._max_ts_length)
|
|
476
|
-
self._mlf.preprocess(mlforecast_df_past, static_features=[])
|
|
595
|
+
self._mlf.preprocess(mlforecast_df_past, static_features=[], dropna=False)
|
|
596
|
+
assert self._mlf.ts.target_transforms is not None
|
|
477
597
|
for tfm in self._mlf.ts.target_transforms[::-1]:
|
|
478
|
-
predictions =
|
|
479
|
-
|
|
598
|
+
predictions = apply_inverse_transform(predictions, transform=tfm)
|
|
599
|
+
|
|
600
|
+
if not self.is_quantile_model:
|
|
601
|
+
predictions = self._add_gaussian_quantiles(
|
|
602
|
+
predictions, repeated_item_ids=predictions[MLF_ITEMID], past_target=data[self.target]
|
|
603
|
+
)
|
|
604
|
+
predictions_tsdf: TimeSeriesDataFrame = TimeSeriesDataFrame(
|
|
605
|
+
predictions.rename(
|
|
606
|
+
columns={MLF_ITEMID: TimeSeriesDataFrame.ITEMID, MLF_TIMESTAMP: TimeSeriesDataFrame.TIMESTAMP}
|
|
607
|
+
)
|
|
608
|
+
)
|
|
480
609
|
|
|
481
610
|
if forecast_for_short_series is not None:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
611
|
+
predictions_tsdf = pd.concat([predictions_tsdf, forecast_for_short_series]) # type: ignore
|
|
612
|
+
predictions_tsdf = predictions_tsdf.reindex(original_item_id_order, level=TimeSeriesDataFrame.ITEMID)
|
|
613
|
+
|
|
614
|
+
return predictions_tsdf
|
|
485
615
|
|
|
486
|
-
def _postprocess_predictions(
|
|
616
|
+
def _postprocess_predictions(
|
|
617
|
+
self, predictions: np.ndarray | pd.Series, repeated_item_ids: pd.Series
|
|
618
|
+
) -> pd.DataFrame:
|
|
487
619
|
if self.is_quantile_model:
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
620
|
+
predictions_df = pd.DataFrame(predictions, columns=[str(q) for q in self.quantile_levels])
|
|
621
|
+
predictions_df.values.sort(axis=1)
|
|
622
|
+
predictions_df["mean"] = predictions_df["0.5"]
|
|
491
623
|
else:
|
|
492
|
-
|
|
493
|
-
predictions = self._add_gaussian_quantiles(predictions, repeated_item_ids=repeated_item_ids)
|
|
494
|
-
|
|
495
|
-
column_order = ["mean"] + [col for col in predictions.columns if col != "mean"]
|
|
496
|
-
return predictions[column_order]
|
|
624
|
+
predictions_df = pd.DataFrame(predictions, columns=["mean"])
|
|
497
625
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return pd.Series(1.0, index=item_ids)
|
|
626
|
+
column_order = ["mean"] + [col for col in predictions_df.columns if col != "mean"]
|
|
627
|
+
return predictions_df[column_order]
|
|
501
628
|
|
|
502
|
-
def
|
|
629
|
+
def _create_tabular_model(self, model_name: str, model_hyperparameters: dict[str, Any]) -> TabularModel:
|
|
630
|
+
model_class = ag_model_registry.key_to_cls(model_name)
|
|
503
631
|
if self.is_quantile_model:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
"eval_metric": "pinball_loss",
|
|
508
|
-
}
|
|
632
|
+
problem_type = ag.constants.QUANTILE
|
|
633
|
+
eval_metric = "pinball_loss"
|
|
634
|
+
model_hyperparameters["ag.quantile_levels"] = self.quantile_levels
|
|
509
635
|
else:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
636
|
+
problem_type = ag.constants.REGRESSION
|
|
637
|
+
eval_metric = self.eval_metric.equivalent_tabular_regression_metric or "mean_absolute_error"
|
|
638
|
+
return TabularModel(
|
|
639
|
+
model_class=model_class,
|
|
640
|
+
model_kwargs={
|
|
641
|
+
"path": "",
|
|
642
|
+
"name": model_class.__name__,
|
|
643
|
+
"hyperparameters": model_hyperparameters,
|
|
644
|
+
"problem_type": problem_type,
|
|
645
|
+
"eval_metric": eval_metric,
|
|
646
|
+
},
|
|
647
|
+
)
|
|
514
648
|
|
|
515
649
|
|
|
516
650
|
class RecursiveTabularModel(AbstractMLForecastModel):
|
|
517
|
-
"""Predict future time series values one by one using
|
|
651
|
+
"""Predict future time series values one by one using a regression model from AutoGluon-Tabular.
|
|
518
652
|
|
|
519
|
-
A single
|
|
653
|
+
A single tabular regression model is used to forecast the future time series values using the following features:
|
|
520
654
|
|
|
521
655
|
- lag features (observed time series values) based on ``freq`` of the data
|
|
522
656
|
- time features (e.g., day of the week) based on the timestamp of the measurement
|
|
523
657
|
- known covariates (if available)
|
|
524
658
|
- static features of each item (if available)
|
|
525
659
|
|
|
526
|
-
|
|
660
|
+
The tabular model will always be trained with ``"regression"`` problem type, and dummy quantiles will be
|
|
527
661
|
obtained by assuming that the residuals follow zero-mean normal distribution.
|
|
528
662
|
|
|
529
663
|
Based on the `mlforecast <https://github.com/Nixtla/mlforecast>`_ library.
|
|
@@ -531,39 +665,48 @@ class RecursiveTabularModel(AbstractMLForecastModel):
|
|
|
531
665
|
|
|
532
666
|
Other Parameters
|
|
533
667
|
----------------
|
|
534
|
-
lags :
|
|
668
|
+
lags : list[int], default = None
|
|
535
669
|
Lags of the target that will be used as features for predictions. If None, will be determined automatically
|
|
536
670
|
based on the frequency of the data.
|
|
537
|
-
date_features :
|
|
671
|
+
date_features : list[str | Callable], default = None
|
|
538
672
|
Features computed from the dates. Can be pandas date attributes or functions that will take the dates as input.
|
|
539
673
|
If None, will be determined automatically based on the frequency of the data.
|
|
540
|
-
differences :
|
|
674
|
+
differences : list[int], default = None
|
|
541
675
|
Differences to take of the target before computing the features. These are restored at the forecasting step.
|
|
542
676
|
If None, will be set to ``[seasonal_period]``, where seasonal_period is determined based on the data frequency.
|
|
543
|
-
|
|
544
|
-
Scaling applied to each time series.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
677
|
+
target_scaler : {"standard", "mean_abs", "min_max", "robust", None}, default = "standard"
|
|
678
|
+
Scaling applied to each time series. Scaling is applied after differencing.
|
|
679
|
+
lag_transforms : dict[int, list[Callable]], default = None
|
|
680
|
+
Dictionary mapping lag periods to transformation functions applied to lagged target values (e.g., rolling mean).
|
|
681
|
+
See `MLForecast documentation <https://nixtlaverse.nixtla.io/mlforecast/lag_transforms.html>`_ for more details.
|
|
682
|
+
model_name : str, default = "GBM"
|
|
683
|
+
Name of the tabular regression model. See ``autogluon.tabular.registry.ag_model_registry`` or
|
|
684
|
+
`the documentation <https://auto.gluon.ai/stable/api/autogluon.tabular.models.html>`_ for the list of available
|
|
685
|
+
tabular models.
|
|
686
|
+
model_hyperparameters : dict[str, Any], optional
|
|
687
|
+
Hyperparameters passed to the tabular regression model.
|
|
550
688
|
max_num_items : int or None, default = 20_000
|
|
551
689
|
If not None, the model will randomly select this many time series for training and validation.
|
|
552
690
|
max_num_samples : int or None, default = 1_000_000
|
|
553
|
-
If not None, training dataset passed to
|
|
554
|
-
end of each time series).
|
|
691
|
+
If not None, training dataset passed to the tabular regression model will contain at most this many rows
|
|
692
|
+
(starting from the end of each time series).
|
|
555
693
|
"""
|
|
556
694
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
model_params
|
|
695
|
+
ag_priority = 90
|
|
696
|
+
|
|
697
|
+
def get_hyperparameters(self) -> dict[str, Any]:
|
|
698
|
+
model_params = super().get_hyperparameters()
|
|
699
|
+
# We don't set 'target_scaler' if user already provided 'scaler' to avoid overriding the user-provided value
|
|
700
|
+
if "scaler" not in model_params:
|
|
701
|
+
model_params.setdefault("target_scaler", "standard")
|
|
702
|
+
if "differences" not in model_params or model_params["differences"] is None:
|
|
703
|
+
model_params["differences"] = [get_seasonality(self.freq)]
|
|
561
704
|
return model_params
|
|
562
705
|
|
|
563
706
|
def _predict(
|
|
564
707
|
self,
|
|
565
708
|
data: TimeSeriesDataFrame,
|
|
566
|
-
known_covariates:
|
|
709
|
+
known_covariates: TimeSeriesDataFrame | None = None,
|
|
567
710
|
**kwargs,
|
|
568
711
|
) -> TimeSeriesDataFrame:
|
|
569
712
|
original_item_id_order = data.item_ids
|
|
@@ -572,14 +715,17 @@ class RecursiveTabularModel(AbstractMLForecastModel):
|
|
|
572
715
|
)
|
|
573
716
|
if len(data) == 0:
|
|
574
717
|
# All time series are too short for chosen differences
|
|
718
|
+
assert forecast_for_short_series is not None
|
|
575
719
|
return forecast_for_short_series
|
|
576
720
|
|
|
577
721
|
new_df = self._to_mlforecast_df(data, data.static_features)
|
|
578
722
|
if self._max_ts_length is not None:
|
|
579
723
|
new_df = self._shorten_all_series(new_df, self._max_ts_length)
|
|
580
724
|
if known_covariates is None:
|
|
581
|
-
future_index =
|
|
582
|
-
known_covariates =
|
|
725
|
+
future_index = self.get_forecast_horizon_index(data)
|
|
726
|
+
known_covariates = TimeSeriesDataFrame(
|
|
727
|
+
pd.DataFrame(columns=[self.target], index=future_index, dtype="float32")
|
|
728
|
+
)
|
|
583
729
|
X_df = self._to_mlforecast_df(known_covariates, data.static_features, include_target=False)
|
|
584
730
|
# If both covariates & static features are missing, set X_df = None to avoid exception from MLForecast
|
|
585
731
|
if len(X_df.columns.difference([MLF_ITEMID, MLF_TIMESTAMP])) == 0:
|
|
@@ -590,23 +736,31 @@ class RecursiveTabularModel(AbstractMLForecastModel):
|
|
|
590
736
|
new_df=new_df,
|
|
591
737
|
X_df=X_df,
|
|
592
738
|
)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
739
|
+
assert isinstance(raw_predictions, pd.DataFrame)
|
|
740
|
+
raw_predictions = raw_predictions.rename(
|
|
741
|
+
columns={MLF_ITEMID: TimeSeriesDataFrame.ITEMID, MLF_TIMESTAMP: TimeSeriesDataFrame.TIMESTAMP}
|
|
596
742
|
)
|
|
597
743
|
|
|
744
|
+
predictions: TimeSeriesDataFrame = TimeSeriesDataFrame(
|
|
745
|
+
self._add_gaussian_quantiles(
|
|
746
|
+
raw_predictions,
|
|
747
|
+
repeated_item_ids=raw_predictions[TimeSeriesDataFrame.ITEMID],
|
|
748
|
+
past_target=data[self.target],
|
|
749
|
+
)
|
|
750
|
+
)
|
|
598
751
|
if forecast_for_short_series is not None:
|
|
599
|
-
predictions = pd.concat([predictions, forecast_for_short_series])
|
|
600
|
-
return predictions.reindex(original_item_id_order, level=ITEMID)
|
|
601
|
-
|
|
602
|
-
def
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
752
|
+
predictions = pd.concat([predictions, forecast_for_short_series]) # type: ignore
|
|
753
|
+
return predictions.reindex(original_item_id_order, level=TimeSeriesDataFrame.ITEMID)
|
|
754
|
+
|
|
755
|
+
def _create_tabular_model(self, model_name: str, model_hyperparameters: dict[str, Any]) -> TabularModel:
|
|
756
|
+
model_class = ag_model_registry.key_to_cls(model_name)
|
|
757
|
+
return TabularModel(
|
|
758
|
+
model_class=model_class,
|
|
759
|
+
model_kwargs={
|
|
760
|
+
"path": "",
|
|
761
|
+
"name": model_class.__name__,
|
|
762
|
+
"hyperparameters": model_hyperparameters,
|
|
763
|
+
"problem_type": ag.constants.REGRESSION,
|
|
764
|
+
"eval_metric": self.eval_metric.equivalent_tabular_regression_metric or "mean_absolute_error",
|
|
765
|
+
},
|
|
766
|
+
)
|