autogluon.timeseries 1.0.1b20240327__tar.gz → 1.0.1b20240403__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.

Files changed (62) hide show
  1. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/PKG-INFO +2 -2
  2. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/dataset/ts_dataframe.py +11 -3
  3. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/learner.py +28 -1
  4. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/abstract/abstract_timeseries_model.py +33 -3
  5. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/autogluon_tabular/mlforecast.py +25 -3
  6. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/chronos/model.py +60 -11
  7. autogluon.timeseries-1.0.1b20240327/src/autogluon/timeseries/models/chronos/chronos.py → autogluon.timeseries-1.0.1b20240403/src/autogluon/timeseries/models/chronos/pipeline.py +80 -19
  8. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/gluonts/abstract_gluonts.py +3 -2
  9. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/local/abstract_local_model.py +67 -22
  10. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/local/naive.py +18 -14
  11. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/local/npts.py +3 -0
  12. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/local/statsforecast.py +2 -0
  13. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/multi_window/multi_window_model.py +8 -1
  14. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/predictor.py +77 -40
  15. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/trainer/abstract_trainer.py +70 -18
  16. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/features.py +62 -4
  17. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/version.py +1 -1
  18. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon.timeseries.egg-info/PKG-INFO +2 -2
  19. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon.timeseries.egg-info/SOURCES.txt +1 -1
  20. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon.timeseries.egg-info/requires.txt +4 -4
  21. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/setup.cfg +0 -0
  22. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/setup.py +0 -0
  23. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/__init__.py +0 -0
  24. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/configs/__init__.py +0 -0
  25. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/configs/presets_configs.py +0 -0
  26. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/dataset/__init__.py +0 -0
  27. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/evaluator.py +0 -0
  28. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/metrics/__init__.py +0 -0
  29. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/metrics/abstract.py +0 -0
  30. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/metrics/point.py +0 -0
  31. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/metrics/quantile.py +0 -0
  32. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/metrics/utils.py +0 -0
  33. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/__init__.py +0 -0
  34. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/abstract/__init__.py +0 -0
  35. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/abstract/model_trial.py +0 -0
  36. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/autogluon_tabular/__init__.py +0 -0
  37. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/autogluon_tabular/utils.py +0 -0
  38. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/chronos/__init__.py +0 -0
  39. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/ensemble/__init__.py +0 -0
  40. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/ensemble/abstract_timeseries_ensemble.py +0 -0
  41. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/ensemble/greedy_ensemble.py +0 -0
  42. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/gluonts/__init__.py +0 -0
  43. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/gluonts/torch/__init__.py +0 -0
  44. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/gluonts/torch/models.py +0 -0
  45. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/local/__init__.py +0 -0
  46. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/multi_window/__init__.py +0 -0
  47. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/models/presets.py +0 -0
  48. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/splitter.py +0 -0
  49. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/trainer/__init__.py +0 -0
  50. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/trainer/auto_trainer.py +0 -0
  51. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/__init__.py +0 -0
  52. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/datetime/__init__.py +0 -0
  53. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/datetime/base.py +0 -0
  54. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/datetime/lags.py +0 -0
  55. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/datetime/seasonality.py +0 -0
  56. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/datetime/time_features.py +0 -0
  57. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/forecast.py +0 -0
  58. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon/timeseries/utils/warning_filters.py +0 -0
  59. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon.timeseries.egg-info/dependency_links.txt +0 -0
  60. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon.timeseries.egg-info/namespace_packages.txt +0 -0
  61. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon.timeseries.egg-info/top_level.txt +0 -0
  62. {autogluon.timeseries-1.0.1b20240327 → autogluon.timeseries-1.0.1b20240403}/src/autogluon.timeseries.egg-info/zip-safe +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: autogluon.timeseries
3
- Version: 1.0.1b20240327
3
+ Version: 1.0.1b20240403
4
4
  Summary: AutoML for Image, Text, and Tabular Data
5
5
  Home-page: https://github.com/autogluon/autogluon
6
6
  Author: AutoGluon Community
@@ -26,7 +26,7 @@ Description:
26
26
  [![Continuous Integration](https://github.com/autogluon/autogluon/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/autogluon/autogluon/actions/workflows/continuous_integration.yml)
27
27
  [![Platform Tests](https://github.com/autogluon/autogluon/actions/workflows/platform_tests-command.yml/badge.svg?event=schedule)](https://github.com/autogluon/autogluon/actions/workflows/platform_tests-command.yml)
28
28
 
29
- [Install Instructions](https://auto.gluon.ai/stable/install.html) | [Documentation](https://auto.gluon.ai/stable/index.html) | [Release Notes](https://auto.gluon.ai/stable/whats_new/index.html)
29
+ [Installation](https://auto.gluon.ai/stable/install.html) | [Documentation](https://auto.gluon.ai/stable/index.html) | [Release Notes](https://auto.gluon.ai/stable/whats_new/index.html)
30
30
 
31
31
  AutoGluon automates machine learning tasks enabling you to easily achieve strong predictive performance in your applications. With just a few lines of code, you can train and deploy high-accuracy machine learning and deep learning models on image, text, time series, and tabular data.
32
32
  </div>
@@ -765,11 +765,19 @@ class TimeSeriesDataFrame(pd.DataFrame, TimeSeriesDataFrameDeprecatedMixin):
765
765
  "(for example, using the `convert_frequency` method)."
766
766
  )
767
767
 
768
- grouped_df = pd.DataFrame(self).groupby(level=ITEMID, sort=False, group_keys=False)
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
- # Fill missing values at the start of each time series with bfill
772
- filled_df = filled_df.groupby(level=ITEMID, sort=False, group_keys=False).bfill()
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(self, **kwargs) -> "AbstractTimeSeriesModel":
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
- return super().fit(**kwargs)
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: Any, **kwargs) -> Any:
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", "auto" if self.min_num_gpus == 0 else "bfloat16")
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 min_num_gpus(self):
204
- return MODEL_CONFIGS.get(self.model_path, {}).get("num_gpus", 0)
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 .chronos import OptimizedChronosPipeline
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
- # load model pipeline to device memory
287
- self.load_model_pipeline(context_length=context_length)
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
- # Author: Lorenzo Stella <stellalo@amazon.com>
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
- decoding_context
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, decoding_context: Any) -> 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
- decoding_context
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
- self.device = model.device
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
- if isinstance(context, list):
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
- try:
168
- result = self._predict_with_local_model(
169
- time_series=time_series,
170
- local_model_args=self._local_model_args.copy(),
171
- )
172
- if not np.isfinite(result.values).all():
173
- raise RuntimeError("Forecast contains NaN or Inf values.")
174
- model_failed = False
175
- except Exception:
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
- model_failed = True
184
- else:
185
- raise
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.mean(np.square(residuals)))
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
- forecast["mean"] = np.full(shape=[prediction_length], fill_value=target[-1])
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.mean(np.square(residuals)))
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