autogluon.timeseries 1.0.1b20240327__tar.gz → 1.0.1b20240330__tar.gz
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-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/PKG-INFO +1 -1
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/dataset/ts_dataframe.py +11 -3
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/learner.py +28 -1
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/abstract/abstract_timeseries_model.py +33 -3
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/autogluon_tabular/mlforecast.py +25 -3
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/chronos/model.py +60 -11
- autogluon.timeseries-1.0.1b20240327/src/autogluon/timeseries/models/chronos/chronos.py → autogluon.timeseries-1.0.1b20240330/src/autogluon/timeseries/models/chronos/pipeline.py +80 -19
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/gluonts/abstract_gluonts.py +3 -2
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/local/abstract_local_model.py +67 -22
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/local/naive.py +18 -14
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/local/npts.py +3 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/local/statsforecast.py +2 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/multi_window/multi_window_model.py +8 -1
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/predictor.py +77 -40
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/trainer/abstract_trainer.py +70 -18
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/features.py +62 -4
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/version.py +1 -1
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon.timeseries.egg-info/PKG-INFO +1 -1
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon.timeseries.egg-info/SOURCES.txt +1 -1
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon.timeseries.egg-info/requires.txt +3 -3
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/setup.cfg +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/setup.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/configs/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/configs/presets_configs.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/dataset/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/evaluator.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/metrics/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/metrics/abstract.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/metrics/point.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/metrics/quantile.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/metrics/utils.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/abstract/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/abstract/model_trial.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/autogluon_tabular/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/autogluon_tabular/utils.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/chronos/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/ensemble/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/ensemble/abstract_timeseries_ensemble.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/ensemble/greedy_ensemble.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/gluonts/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/gluonts/torch/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/gluonts/torch/models.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/local/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/multi_window/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/models/presets.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/splitter.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/trainer/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/trainer/auto_trainer.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/datetime/__init__.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/datetime/base.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/datetime/lags.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/datetime/seasonality.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/datetime/time_features.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/forecast.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon/timeseries/utils/warning_filters.py +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon.timeseries.egg-info/dependency_links.txt +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon.timeseries.egg-info/namespace_packages.txt +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon.timeseries.egg-info/top_level.txt +0 -0
- {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240330}/src/autogluon.timeseries.egg-info/zip-safe +0 -0
|
@@ -765,11 +765,19 @@ class TimeSeriesDataFrame(pd.DataFrame, TimeSeriesDataFrameDeprecatedMixin):
|
|
|
765
765
|
"(for example, using the `convert_frequency` method)."
|
|
766
766
|
)
|
|
767
767
|
|
|
768
|
-
|
|
768
|
+
# Convert to pd.DataFrame for faster processing
|
|
769
|
+
df = pd.DataFrame(self)
|
|
770
|
+
|
|
771
|
+
# Skip filling if there are no NaNs
|
|
772
|
+
if not df.isna().any(axis=None):
|
|
773
|
+
return self
|
|
774
|
+
|
|
775
|
+
grouped_df = df.groupby(level=ITEMID, sort=False, group_keys=False)
|
|
769
776
|
if method == "auto":
|
|
770
777
|
filled_df = grouped_df.ffill()
|
|
771
|
-
#
|
|
772
|
-
|
|
778
|
+
# If necessary, fill missing values at the start of each time series with bfill
|
|
779
|
+
if filled_df.isna().any(axis=None):
|
|
780
|
+
filled_df = filled_df.groupby(level=ITEMID, sort=False, group_keys=False).bfill()
|
|
773
781
|
elif method in ["ffill", "pad"]:
|
|
774
782
|
filled_df = grouped_df.ffill()
|
|
775
783
|
elif method in ["bfill", "backfill"]:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import reprlib
|
|
3
3
|
import time
|
|
4
|
-
from typing import Any, Dict, List, Optional, Type, Union
|
|
4
|
+
from typing import Any, Dict, List, Literal, Optional, Type, Union
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
@@ -228,5 +228,32 @@ class TimeSeriesLearner(AbstractLearner):
|
|
|
228
228
|
learner_info.pop("random_state", None)
|
|
229
229
|
return learner_info
|
|
230
230
|
|
|
231
|
+
def persist_trainer(
|
|
232
|
+
self, models: Union[Literal["all", "best"], List[str]] = "all", with_ancestors: bool = False
|
|
233
|
+
) -> List[str]:
|
|
234
|
+
"""Loads models and trainer in memory so that they don't have to be
|
|
235
|
+
loaded during predictions
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
list_of_models : List[str]
|
|
240
|
+
List of models persisted in memory
|
|
241
|
+
"""
|
|
242
|
+
self.trainer = self.load_trainer()
|
|
243
|
+
return self.trainer.persist(models, with_ancestors=with_ancestors)
|
|
244
|
+
|
|
245
|
+
def unpersist_trainer(self) -> List[str]:
|
|
246
|
+
"""Unloads models and trainer from memory. Models will have to be reloaded from disk
|
|
247
|
+
when predicting.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
list_of_models : List[str]
|
|
252
|
+
List of models removed from memory
|
|
253
|
+
"""
|
|
254
|
+
unpersisted_models = self.load_trainer().unpersist()
|
|
255
|
+
self.trainer = None
|
|
256
|
+
return unpersisted_models
|
|
257
|
+
|
|
231
258
|
def refit_full(self, model: str = "all") -> Dict[str, str]:
|
|
232
259
|
return self.load_trainer().refit_full(model=model)
|
|
@@ -201,7 +201,9 @@ class AbstractTimeSeriesModel(AbstractModel):
|
|
|
201
201
|
}
|
|
202
202
|
return info
|
|
203
203
|
|
|
204
|
-
def fit(
|
|
204
|
+
def fit(
|
|
205
|
+
self, train_data: TimeSeriesDataFrame, val_data: Optional[TimeSeriesDataFrame] = None, **kwargs
|
|
206
|
+
) -> "AbstractTimeSeriesModel":
|
|
205
207
|
"""Fit timeseries model.
|
|
206
208
|
|
|
207
209
|
Models should not override the `fit` method, but instead override the `_fit` method which
|
|
@@ -235,7 +237,10 @@ class AbstractTimeSeriesModel(AbstractModel):
|
|
|
235
237
|
model: AbstractTimeSeriesModel
|
|
236
238
|
The fitted model object
|
|
237
239
|
"""
|
|
238
|
-
|
|
240
|
+
train_data = self.preprocess(train_data, is_train=True)
|
|
241
|
+
if self._get_tags()["can_use_val_data"] and val_data is not None:
|
|
242
|
+
val_data = self.preprocess(val_data, is_train=False)
|
|
243
|
+
return super().fit(train_data=train_data, val_data=val_data, **kwargs)
|
|
239
244
|
|
|
240
245
|
def _fit(
|
|
241
246
|
self,
|
|
@@ -290,6 +295,7 @@ class AbstractTimeSeriesModel(AbstractModel):
|
|
|
290
295
|
data is given as a separate forecast item in the dictionary, keyed by the `item_id`s
|
|
291
296
|
of input items.
|
|
292
297
|
"""
|
|
298
|
+
data = self.preprocess(data, is_train=False)
|
|
293
299
|
predictions = self._predict(data=data, known_covariates=known_covariates, **kwargs)
|
|
294
300
|
logger.debug(f"Predicting with model {self.name}")
|
|
295
301
|
# "0.5" might be missing from the quantiles if self is a wrapper (MultiWindowBacktestingModel or ensemble)
|
|
@@ -415,6 +421,13 @@ class AbstractTimeSeriesModel(AbstractModel):
|
|
|
415
421
|
hpo_executor.register_resources(self, k_fold=1, **kwargs)
|
|
416
422
|
return self._hyperparameter_tune(hpo_executor=hpo_executor, **kwargs)
|
|
417
423
|
|
|
424
|
+
def persist(self) -> "AbstractTimeSeriesModel":
|
|
425
|
+
"""Ask the model to persist its assets in memory, i.e., to predict with low latency. In practice
|
|
426
|
+
this is used for pretrained models that have to lazy-load model parameters to device memory at
|
|
427
|
+
prediction time.
|
|
428
|
+
"""
|
|
429
|
+
return self
|
|
430
|
+
|
|
418
431
|
def _hyperparameter_tune(
|
|
419
432
|
self,
|
|
420
433
|
train_data: TimeSeriesDataFrame,
|
|
@@ -481,7 +494,7 @@ class AbstractTimeSeriesModel(AbstractModel):
|
|
|
481
494
|
|
|
482
495
|
return hpo_models, analysis
|
|
483
496
|
|
|
484
|
-
def preprocess(self, data:
|
|
497
|
+
def preprocess(self, data: TimeSeriesDataFrame, is_train: bool = False, **kwargs) -> Any:
|
|
485
498
|
return data
|
|
486
499
|
|
|
487
500
|
def get_memory_size(self, **kwargs) -> Optional[int]:
|
|
@@ -499,3 +512,20 @@ class AbstractTimeSeriesModel(AbstractModel):
|
|
|
499
512
|
return {}
|
|
500
513
|
else:
|
|
501
514
|
return self._user_params.copy()
|
|
515
|
+
|
|
516
|
+
def _more_tags(self) -> dict:
|
|
517
|
+
"""Encode model properties using tags, similar to sklearn & autogluon.tabular.
|
|
518
|
+
|
|
519
|
+
For more details, see `autogluon.core.models.abstract.AbstractModel._get_tags()` and https://scikit-learn.org/stable/_sources/developers/develop.rst.txt.
|
|
520
|
+
|
|
521
|
+
List of currently supported tags:
|
|
522
|
+
- allow_nan: Can the model handle data with missing values represented by np.nan?
|
|
523
|
+
- can_refit_full: Does it make sense to retrain the model without validation data?
|
|
524
|
+
See `autogluon.core.models.abstract._tags._DEFAULT_TAGS` for more details.
|
|
525
|
+
- can_use_val_data: Can model use val_data if it's provided to model.fit()?
|
|
526
|
+
"""
|
|
527
|
+
return {
|
|
528
|
+
"allow_nan": False,
|
|
529
|
+
"can_refit_full": False,
|
|
530
|
+
"can_use_val_data": False,
|
|
531
|
+
}
|
|
@@ -85,6 +85,21 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
85
85
|
self._scaler: Optional[BaseTargetTransform] = None
|
|
86
86
|
self._residuals_std_per_item: Optional[pd.Series] = None
|
|
87
87
|
self._avg_residuals_std: Optional[float] = None
|
|
88
|
+
self._train_target_median: Optional[float] = None
|
|
89
|
+
|
|
90
|
+
def preprocess(self, data: TimeSeriesDataFrame, is_train: bool = False, **kwargs) -> Any:
|
|
91
|
+
if is_train:
|
|
92
|
+
# All-NaN series are removed; partially-NaN series in train_data are handled inside _generate_train_val_dfs
|
|
93
|
+
all_nan_items = data.item_ids[data[self.target].isna().groupby(ITEMID, sort=False).all()]
|
|
94
|
+
if len(all_nan_items):
|
|
95
|
+
data = data.query("item_id not in @all_nan_items")
|
|
96
|
+
return data
|
|
97
|
+
else:
|
|
98
|
+
data = data.fill_missing_values()
|
|
99
|
+
# Fill time series consisting of all NaNs with the median of target in train_data
|
|
100
|
+
if data.isna().any(axis=None):
|
|
101
|
+
data[self.target] = data[self.target].fillna(value=self._train_target_median)
|
|
102
|
+
return data
|
|
88
103
|
|
|
89
104
|
def _get_extra_tabular_init_kwargs(self) -> dict:
|
|
90
105
|
raise NotImplementedError
|
|
@@ -98,8 +113,6 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
98
113
|
return model_params
|
|
99
114
|
|
|
100
115
|
def _get_mlforecast_init_args(self, train_data: TimeSeriesDataFrame, model_params: dict) -> dict:
|
|
101
|
-
# TODO: Support lag generation for all pandas frequencies
|
|
102
|
-
# TODO: Support date_feature generation for all pandas frequencies
|
|
103
116
|
from mlforecast.target_transforms import Differences
|
|
104
117
|
|
|
105
118
|
from .utils import MeanAbsScaler, StandardScaler
|
|
@@ -181,6 +194,10 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
181
194
|
items_to_keep = data.item_ids.to_series().sample(n=int(max_num_items)) # noqa: F841
|
|
182
195
|
data = data.query("item_id in @items_to_keep")
|
|
183
196
|
|
|
197
|
+
# MLForecast.preprocess does not support missing values, but we will exclude them later from the training set
|
|
198
|
+
missing_entries = data.index[data[self.target].isna()]
|
|
199
|
+
data = data.fill_missing_values()
|
|
200
|
+
|
|
184
201
|
num_items = data.num_items
|
|
185
202
|
mlforecast_df = self._to_mlforecast_df(data, data.static_features)
|
|
186
203
|
|
|
@@ -197,6 +214,10 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
197
214
|
|
|
198
215
|
df = self._mask_df(df)
|
|
199
216
|
|
|
217
|
+
# We remove originally missing values filled via imputation from the training set
|
|
218
|
+
if len(missing_entries):
|
|
219
|
+
df = df.set_index(["unique_id", "ds"]).drop(missing_entries, errors="ignore").reset_index()
|
|
220
|
+
|
|
200
221
|
if max_num_samples is not None and len(df) > max_num_samples:
|
|
201
222
|
df = df.sample(n=max_num_samples)
|
|
202
223
|
|
|
@@ -246,6 +267,7 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
246
267
|
|
|
247
268
|
self._check_fit_params()
|
|
248
269
|
fit_start_time = time.time()
|
|
270
|
+
self._train_target_median = train_data[self.target].median()
|
|
249
271
|
# TabularEstimator is passed to MLForecast later to include tuning_data
|
|
250
272
|
model_params = self._get_model_params()
|
|
251
273
|
|
|
@@ -355,7 +377,7 @@ class AbstractMLForecastModel(AbstractTimeSeriesModel):
|
|
|
355
377
|
return predictions
|
|
356
378
|
|
|
357
379
|
def _more_tags(self) -> dict:
|
|
358
|
-
return {"can_refit_full": True}
|
|
380
|
+
return {"allow_nan": True, "can_refit_full": True}
|
|
359
381
|
|
|
360
382
|
|
|
361
383
|
class DirectTabularModel(AbstractMLForecastModel):
|
|
@@ -18,11 +18,29 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
MODEL_CONFIGS = {
|
|
19
19
|
"amazon/chronos-t5-tiny": {
|
|
20
20
|
"num_gpus": 0, # minimum number of required GPUs
|
|
21
|
+
"default_torch_dtype": "auto",
|
|
22
|
+
"default_batch_size": 16,
|
|
23
|
+
},
|
|
24
|
+
"amazon/chronos-t5-mini": {
|
|
25
|
+
"num_gpus": 0,
|
|
26
|
+
"default_torch_dtype": "auto",
|
|
27
|
+
"default_batch_size": 16,
|
|
28
|
+
},
|
|
29
|
+
"amazon/chronos-t5-small": {
|
|
30
|
+
"num_gpus": 1,
|
|
31
|
+
"default_torch_dtype": "bfloat16",
|
|
32
|
+
"default_batch_size": 16,
|
|
33
|
+
},
|
|
34
|
+
"amazon/chronos-t5-base": {
|
|
35
|
+
"num_gpus": 1,
|
|
36
|
+
"default_torch_dtype": "bfloat16",
|
|
37
|
+
"default_batch_size": 16,
|
|
38
|
+
},
|
|
39
|
+
"amazon/chronos-t5-large": {
|
|
40
|
+
"num_gpus": 1,
|
|
41
|
+
"default_torch_dtype": "bfloat16",
|
|
42
|
+
"default_batch_size": 8,
|
|
21
43
|
},
|
|
22
|
-
"amazon/chronos-t5-mini": {"num_gpus": 0},
|
|
23
|
-
"amazon/chronos-t5-small": {"num_gpus": 1},
|
|
24
|
-
"amazon/chronos-t5-base": {"num_gpus": 1},
|
|
25
|
-
"amazon/chronos-t5-large": {"num_gpus": 1},
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
|
|
@@ -124,7 +142,6 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
|
124
142
|
|
|
125
143
|
# default number of samples for prediction
|
|
126
144
|
default_num_samples: int = 20
|
|
127
|
-
default_batch_size: int = 16
|
|
128
145
|
default_model_path = "amazon/chronos-t5-small"
|
|
129
146
|
maximum_context_length = 512
|
|
130
147
|
|
|
@@ -149,7 +166,7 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
|
149
166
|
self.device = hyperparameters.get("device")
|
|
150
167
|
|
|
151
168
|
# if the model requires a GPU, set the torch dtype to bfloat16
|
|
152
|
-
self.torch_dtype = hyperparameters.get("torch_dtype",
|
|
169
|
+
self.torch_dtype = hyperparameters.get("torch_dtype", self.default_torch_dtype)
|
|
153
170
|
|
|
154
171
|
self.data_loader_num_workers = hyperparameters.get("data_loader_num_workers", 0)
|
|
155
172
|
self.optimization_strategy: Optional[Literal["onnx", "openvino"]] = hyperparameters.get(
|
|
@@ -200,8 +217,32 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
|
200
217
|
return torch.cuda.is_available()
|
|
201
218
|
|
|
202
219
|
@property
|
|
203
|
-
def
|
|
204
|
-
|
|
220
|
+
def ag_default_config(self) -> Dict[str, Any]:
|
|
221
|
+
"""The default configuration of the model used by AutoGluon if the model is one of those
|
|
222
|
+
defined in MODEL_CONFIGS. For now, these are ``amazon/chronos-t5-*`` family of models.
|
|
223
|
+
"""
|
|
224
|
+
return MODEL_CONFIGS.get(self.model_path, {})
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def min_num_gpus(self) -> int:
|
|
228
|
+
"""Minimum number of GPUs required for the model. For models not defined in AutoGluon,
|
|
229
|
+
this value defaults to 0.
|
|
230
|
+
"""
|
|
231
|
+
return self.ag_default_config.get("num_gpus", 0)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def default_batch_size(self) -> int:
|
|
235
|
+
"""Default batch size used for the model. For models not defined in AutoGluon, this value
|
|
236
|
+
defaults to 8.
|
|
237
|
+
"""
|
|
238
|
+
return self.ag_default_config.get("default_batch_size", 8)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def default_torch_dtype(self) -> Any:
|
|
242
|
+
"""Default torch data type used for the model. For models not defined in AutoGluon, this value
|
|
243
|
+
defaults to "auto".
|
|
244
|
+
"""
|
|
245
|
+
return self.ag_default_config.get("default_torch_dtype", "auto")
|
|
205
246
|
|
|
206
247
|
def get_minimum_resources(self, is_gpu_available: bool = False) -> Dict[str, Union[int, float]]:
|
|
207
248
|
minimum_resources = {"num_cpus": 1}
|
|
@@ -211,7 +252,7 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
|
211
252
|
return minimum_resources
|
|
212
253
|
|
|
213
254
|
def load_model_pipeline(self, context_length: Optional[int] = None):
|
|
214
|
-
from .
|
|
255
|
+
from .pipeline import OptimizedChronosPipeline
|
|
215
256
|
|
|
216
257
|
gpu_available = self._is_gpu_available()
|
|
217
258
|
|
|
@@ -234,6 +275,10 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
|
234
275
|
|
|
235
276
|
self.model_pipeline = pipeline
|
|
236
277
|
|
|
278
|
+
def persist(self) -> "ChronosModel":
|
|
279
|
+
self.load_model_pipeline(context_length=self.context_length or self.maximum_context_length)
|
|
280
|
+
return self
|
|
281
|
+
|
|
237
282
|
def _fit(
|
|
238
283
|
self,
|
|
239
284
|
train_data: TimeSeriesDataFrame,
|
|
@@ -283,8 +328,9 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
|
283
328
|
with warning_filter(all_warnings=True):
|
|
284
329
|
import torch
|
|
285
330
|
|
|
286
|
-
|
|
287
|
-
|
|
331
|
+
if self.model_pipeline is None:
|
|
332
|
+
# load model pipeline to device memory
|
|
333
|
+
self.load_model_pipeline(context_length=context_length)
|
|
288
334
|
|
|
289
335
|
self.model_pipeline.model.eval()
|
|
290
336
|
with torch.inference_mode():
|
|
@@ -317,3 +363,6 @@ class ChronosModel(AbstractTimeSeriesModel):
|
|
|
317
363
|
)
|
|
318
364
|
|
|
319
365
|
return TimeSeriesDataFrame(df)
|
|
366
|
+
|
|
367
|
+
def _more_tags(self) -> Dict:
|
|
368
|
+
return {"allow_nan": True}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
# Original Source: https://github.com/amazon-science/chronos-forecasting
|
|
5
|
-
#
|
|
5
|
+
# Authors: Lorenzo Stella <stellalo@amazon.com>, Abdul Fatir Ansari <ansarnd@amazon.com>
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
import warnings
|
|
@@ -18,6 +18,9 @@ from autogluon.timeseries.utils.warning_filters import set_loggers_level
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
__all__ = ["ChronosConfig", "ChronosPipeline", "OptimizedChronosPipeline"]
|
|
22
|
+
|
|
23
|
+
|
|
21
24
|
@dataclass
|
|
22
25
|
class ChronosConfig:
|
|
23
26
|
"""
|
|
@@ -81,14 +84,14 @@ class ChronosTokenizer:
|
|
|
81
84
|
A boolean tensor, same shape as ``token_ids``, indicating
|
|
82
85
|
which input observations are not ``torch.nan`` (i.e. not
|
|
83
86
|
missing nor padding).
|
|
84
|
-
|
|
87
|
+
tokenizer_state
|
|
85
88
|
An object that will be passed to ``output_transform``.
|
|
86
89
|
Contains the relevant context to decode output samples into
|
|
87
90
|
real values, such as location and scale parameters.
|
|
88
91
|
"""
|
|
89
92
|
raise NotImplementedError()
|
|
90
93
|
|
|
91
|
-
def output_transform(self, samples: torch.Tensor,
|
|
94
|
+
def output_transform(self, samples: torch.Tensor, tokenizer_state: Any) -> torch.Tensor:
|
|
92
95
|
"""
|
|
93
96
|
Turn a batch of sample token IDs into real values.
|
|
94
97
|
|
|
@@ -97,7 +100,7 @@ class ChronosTokenizer:
|
|
|
97
100
|
samples
|
|
98
101
|
A tensor of integers, shaped (batch_size, num_samples, time_length),
|
|
99
102
|
containing token IDs of sample trajectories.
|
|
100
|
-
|
|
103
|
+
tokenizer_state
|
|
101
104
|
An object returned by ``input_transform`` containing
|
|
102
105
|
relevant context to decode samples, such as location and scale.
|
|
103
106
|
The nature of this depends on the specific tokenizer.
|
|
@@ -132,13 +135,6 @@ class MeanScaleUniformBins(ChronosTokenizer):
|
|
|
132
135
|
|
|
133
136
|
if length > self.config.context_length:
|
|
134
137
|
context = context[..., -self.config.context_length :]
|
|
135
|
-
elif length < self.config.context_length:
|
|
136
|
-
padding_size = (
|
|
137
|
-
*context.shape[:-1],
|
|
138
|
-
self.config.context_length - length,
|
|
139
|
-
)
|
|
140
|
-
padding = torch.full(size=padding_size, fill_value=torch.nan)
|
|
141
|
-
context = torch.concat((padding, context), dim=-1)
|
|
142
138
|
|
|
143
139
|
attention_mask = ~torch.isnan(context)
|
|
144
140
|
scale = torch.nansum(torch.abs(context) * attention_mask, dim=-1) / torch.nansum(attention_mask, dim=-1)
|
|
@@ -191,7 +187,36 @@ class ChronosPretrainedModel(nn.Module):
|
|
|
191
187
|
super().__init__()
|
|
192
188
|
self.config = config
|
|
193
189
|
self.model = model
|
|
194
|
-
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def device(self):
|
|
193
|
+
return self.model.device
|
|
194
|
+
|
|
195
|
+
def encode(
|
|
196
|
+
self,
|
|
197
|
+
input_ids: torch.Tensor,
|
|
198
|
+
attention_mask: torch.Tensor,
|
|
199
|
+
):
|
|
200
|
+
"""
|
|
201
|
+
Extract the encoder embedding for the given token sequences.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
input_ids
|
|
206
|
+
Tensor of indices of input sequence tokens in the vocabulary
|
|
207
|
+
with shape (batch_size, sequence_length).
|
|
208
|
+
attention_mask
|
|
209
|
+
A mask tensor of the same shape as input_ids to avoid attending
|
|
210
|
+
on padding or missing tokens.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
embedding
|
|
215
|
+
A tensor of encoder embeddings with shape
|
|
216
|
+
(batch_size, sequence_length, d_model).
|
|
217
|
+
"""
|
|
218
|
+
assert self.config.model_type == "seq2seq", "Encoder embeddings are only supported for encoder-decoder models"
|
|
219
|
+
return self.model.encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
|
|
195
220
|
|
|
196
221
|
def forward(
|
|
197
222
|
self,
|
|
@@ -288,6 +313,48 @@ class ChronosPipeline:
|
|
|
288
313
|
self.tokenizer = tokenizer
|
|
289
314
|
self.model = model
|
|
290
315
|
|
|
316
|
+
def _prepare_and_validate_context(self, context: Union[torch.Tensor, List[torch.Tensor]]):
|
|
317
|
+
if isinstance(context, list):
|
|
318
|
+
context = left_pad_and_stack_1D(context)
|
|
319
|
+
assert isinstance(context, torch.Tensor)
|
|
320
|
+
if context.ndim == 1:
|
|
321
|
+
context = context.unsqueeze(0)
|
|
322
|
+
assert context.ndim == 2
|
|
323
|
+
|
|
324
|
+
return context
|
|
325
|
+
|
|
326
|
+
@torch.no_grad()
|
|
327
|
+
def embed(self, context: Union[torch.Tensor, List[torch.Tensor]]) -> Tuple[torch.Tensor, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Get encoder embeddings for the given time series.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
context
|
|
334
|
+
Input series. This is either a 1D tensor, or a list
|
|
335
|
+
of 1D tensors, or a 2D tensor whose first dimension
|
|
336
|
+
is batch. In the latter case, use left-padding with
|
|
337
|
+
``torch.nan`` to align series of different lengths.
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
embeddings, tokenizer_state
|
|
342
|
+
A tuple of two tensors: the encoder embeddings and the tokenizer_state,
|
|
343
|
+
e.g., the scale of the time series in the case of mean scaling.
|
|
344
|
+
The encoder embeddings are shaped (batch_size, context_length, d_model)
|
|
345
|
+
or (batch_size, context_length + 1, d_model), where context_length
|
|
346
|
+
is the size of the context along the time axis if a 2D tensor was provided
|
|
347
|
+
or the length of the longest time series, if a list of 1D tensors was
|
|
348
|
+
provided, and the extra 1 is for EOS.
|
|
349
|
+
"""
|
|
350
|
+
context = self._prepare_and_validate_context(context=context)
|
|
351
|
+
token_ids, attention_mask, tokenizer_state = self.tokenizer.input_transform(context)
|
|
352
|
+
embeddings = self.model.encode(
|
|
353
|
+
input_ids=token_ids.to(self.model.device),
|
|
354
|
+
attention_mask=attention_mask.to(self.model.device),
|
|
355
|
+
).cpu()
|
|
356
|
+
return embeddings, tokenizer_state
|
|
357
|
+
|
|
291
358
|
def predict(
|
|
292
359
|
self,
|
|
293
360
|
context: Union[torch.Tensor, List[torch.Tensor]],
|
|
@@ -335,13 +402,7 @@ class ChronosPipeline:
|
|
|
335
402
|
Tensor of sample forecasts, of shape
|
|
336
403
|
(batch_size, num_samples, prediction_length).
|
|
337
404
|
"""
|
|
338
|
-
|
|
339
|
-
context = left_pad_and_stack_1D(context)
|
|
340
|
-
assert isinstance(context, torch.Tensor)
|
|
341
|
-
if context.ndim == 1:
|
|
342
|
-
context = context.unsqueeze(0)
|
|
343
|
-
assert context.ndim == 2
|
|
344
|
-
|
|
405
|
+
context = self._prepare_and_validate_context(context=context)
|
|
345
406
|
if prediction_length is None:
|
|
346
407
|
prediction_length = self.model.config.prediction_length
|
|
347
408
|
|
|
@@ -328,8 +328,6 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
|
|
|
328
328
|
|
|
329
329
|
if self.num_feat_static_real > 0:
|
|
330
330
|
feat_static_real = time_series_df.static_features[self.metadata.static_features_real]
|
|
331
|
-
if feat_static_real.isna().values.any():
|
|
332
|
-
feat_static_real = feat_static_real.fillna(feat_static_real.mean())
|
|
333
331
|
else:
|
|
334
332
|
feat_static_real = None
|
|
335
333
|
|
|
@@ -548,3 +546,6 @@ class AbstractGluonTSModel(AbstractTimeSeriesModel):
|
|
|
548
546
|
|
|
549
547
|
forecast_df.index = forecast_index
|
|
550
548
|
return TimeSeriesDataFrame(forecast_df)
|
|
549
|
+
|
|
550
|
+
def _more_tags(self) -> dict:
|
|
551
|
+
return {"allow_nan": True, "can_use_val_data": True}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
3
|
from multiprocessing import TimeoutError, cpu_count
|
|
4
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
@@ -85,6 +85,12 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
|
|
|
85
85
|
self._local_model_args: Dict[str, Any] = None
|
|
86
86
|
self._seasonal_period: Optional[int] = None
|
|
87
87
|
self.time_limit: Optional[float] = None
|
|
88
|
+
self._dummy_forecast: Optional[pd.DataFrame] = None
|
|
89
|
+
|
|
90
|
+
def preprocess(self, data: TimeSeriesDataFrame, is_train: bool = False, **kwargs) -> Any:
|
|
91
|
+
if not self._get_tags()["allow_nan"]:
|
|
92
|
+
data = data.fill_missing_values()
|
|
93
|
+
return data
|
|
88
94
|
|
|
89
95
|
def _fit(self, train_data: TimeSeriesDataFrame, time_limit: Optional[int] = None, **kwargs):
|
|
90
96
|
self._check_fit_params()
|
|
@@ -115,8 +121,16 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
|
|
|
115
121
|
|
|
116
122
|
self._local_model_args = self._update_local_model_args(local_model_args=local_model_args)
|
|
117
123
|
self.time_limit = time_limit
|
|
124
|
+
|
|
125
|
+
self._dummy_forecast = self._get_dummy_forecast(train_data)
|
|
118
126
|
return self
|
|
119
127
|
|
|
128
|
+
def _get_dummy_forecast(self, train_data: TimeSeriesDataFrame) -> pd.DataFrame:
|
|
129
|
+
agg_functions = ["mean"] + [get_quantile_function(q) for q in self.quantile_levels]
|
|
130
|
+
stats_marginal = train_data[self.target].agg(agg_functions)
|
|
131
|
+
stats_repeated = np.tile(stats_marginal.values, [self.prediction_length, 1])
|
|
132
|
+
return pd.DataFrame(stats_repeated, columns=stats_marginal.index)
|
|
133
|
+
|
|
120
134
|
def _update_local_model_args(self, local_model_args: Dict[str, Any]) -> Dict[str, Any]:
|
|
121
135
|
return local_model_args
|
|
122
136
|
|
|
@@ -164,25 +178,30 @@ class AbstractLocalModel(AbstractTimeSeriesModel):
|
|
|
164
178
|
def _predict_wrapper(self, time_series: pd.Series, end_time: Optional[float] = None) -> Tuple[pd.DataFrame, bool]:
|
|
165
179
|
if end_time is not None and time.time() >= end_time:
|
|
166
180
|
raise TimeLimitExceeded
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if self.use_fallback_model:
|
|
177
|
-
result = seasonal_naive_forecast(
|
|
178
|
-
target=time_series.values.ravel(),
|
|
179
|
-
prediction_length=self.prediction_length,
|
|
180
|
-
quantile_levels=self.quantile_levels,
|
|
181
|
-
seasonal_period=self._seasonal_period,
|
|
181
|
+
|
|
182
|
+
if time_series.isna().all():
|
|
183
|
+
result = self._dummy_forecast.copy()
|
|
184
|
+
model_failed = True
|
|
185
|
+
else:
|
|
186
|
+
try:
|
|
187
|
+
result = self._predict_with_local_model(
|
|
188
|
+
time_series=time_series,
|
|
189
|
+
local_model_args=self._local_model_args.copy(),
|
|
182
190
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
if not np.isfinite(result.values).all():
|
|
192
|
+
raise RuntimeError("Forecast contains NaN or Inf values.")
|
|
193
|
+
model_failed = False
|
|
194
|
+
except Exception:
|
|
195
|
+
if self.use_fallback_model:
|
|
196
|
+
result = seasonal_naive_forecast(
|
|
197
|
+
target=time_series.values.ravel(),
|
|
198
|
+
prediction_length=self.prediction_length,
|
|
199
|
+
quantile_levels=self.quantile_levels,
|
|
200
|
+
seasonal_period=self._seasonal_period,
|
|
201
|
+
)
|
|
202
|
+
model_failed = True
|
|
203
|
+
else:
|
|
204
|
+
raise
|
|
186
205
|
return result, model_failed
|
|
187
206
|
|
|
188
207
|
def _predict_with_local_model(
|
|
@@ -197,25 +216,51 @@ def seasonal_naive_forecast(
|
|
|
197
216
|
target: np.ndarray, prediction_length: int, quantile_levels: List[float], seasonal_period: int
|
|
198
217
|
) -> pd.DataFrame:
|
|
199
218
|
"""Generate seasonal naive forecast, predicting the last observed value from the same period."""
|
|
219
|
+
|
|
220
|
+
def numpy_ffill(arr: np.ndarray) -> np.ndarray:
|
|
221
|
+
"""Fast implementation of forward fill in numpy."""
|
|
222
|
+
idx = np.arange(len(arr))
|
|
223
|
+
mask = np.isnan(arr)
|
|
224
|
+
idx[mask] = 0
|
|
225
|
+
return arr[np.maximum.accumulate(idx)]
|
|
226
|
+
|
|
200
227
|
forecast = {}
|
|
228
|
+
# Convert to float64 since std computation can be unstable in float32
|
|
229
|
+
target = target.astype(np.float64)
|
|
201
230
|
# At least seasonal_period + 2 values are required to compute sigma for seasonal naive
|
|
202
231
|
if len(target) > seasonal_period + 1 and seasonal_period > 1:
|
|
232
|
+
if np.isnan(target[-(seasonal_period + 2) :]).any():
|
|
233
|
+
target = numpy_ffill(target)
|
|
234
|
+
|
|
203
235
|
indices = [len(target) - seasonal_period + k % seasonal_period for k in range(prediction_length)]
|
|
204
236
|
forecast["mean"] = target[indices]
|
|
205
237
|
residuals = target[seasonal_period:] - target[:-seasonal_period]
|
|
206
238
|
|
|
207
|
-
sigma = np.sqrt(np.
|
|
239
|
+
sigma = np.sqrt(np.nanmean(np.square(residuals)))
|
|
208
240
|
num_full_seasons = np.arange(1, prediction_length + 1) // seasonal_period
|
|
209
241
|
sigma_per_timestep = sigma * np.sqrt(num_full_seasons + 1)
|
|
210
242
|
else:
|
|
211
243
|
# Fall back to naive forecast
|
|
212
|
-
|
|
244
|
+
last_observed_value = target[np.isfinite(target)][-1]
|
|
245
|
+
forecast["mean"] = np.full(shape=[prediction_length], fill_value=last_observed_value)
|
|
213
246
|
residuals = target[1:] - target[:-1]
|
|
214
247
|
|
|
215
|
-
sigma = np.sqrt(np.
|
|
248
|
+
sigma = np.sqrt(np.nanmean(np.square(residuals)))
|
|
249
|
+
if np.isnan(sigma): # happens if there are no two consecutive non-nan observations
|
|
250
|
+
sigma = 0.0
|
|
216
251
|
sigma_per_timestep = sigma * np.sqrt(np.arange(1, prediction_length + 1))
|
|
217
252
|
|
|
218
253
|
for q in quantile_levels:
|
|
219
254
|
forecast[str(q)] = forecast["mean"] + norm.ppf(q) * sigma_per_timestep
|
|
220
255
|
|
|
221
256
|
return pd.DataFrame(forecast)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_quantile_function(q: float) -> Callable:
|
|
260
|
+
"""Returns a function with name "q" that computes the q'th quantile of a pandas.Series."""
|
|
261
|
+
|
|
262
|
+
def quantile_fn(x: pd.Series) -> pd.Series:
|
|
263
|
+
return x.quantile(q)
|
|
264
|
+
|
|
265
|
+
quantile_fn.__name__ = str(q)
|
|
266
|
+
return quantile_fn
|