From 3e571c36ac5e2d084cfef39dde4e78accb75e770 Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Thu, 5 Jan 2023 14:45:16 +0100 Subject: [PATCH 01/13] StatsForecastETS now is probabilistic in the same way as StatsForecastAutoARIMA --- darts/models/forecasting/sf_ets.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py index 2f5f622d90..bbe757ca65 100644 --- a/darts/models/forecasting/sf_ets.py +++ b/darts/models/forecasting/sf_ets.py @@ -5,6 +5,7 @@ from typing import Optional +import numpy as np from statsforecast.models import ETS from darts import TimeSeries @@ -97,9 +98,18 @@ def _predict( forecast_df = self.model.predict( h=n, X=future_covariates.values(copy=False) if future_covariates else None, + level=(68.27,), # ask one std for the confidence interval ) - return self._build_forecast_series(forecast_df["mean"]) + mu = forecast_df["mean"] + if num_samples > 1: + std = forecast_df["hi-68.27"] - mu + samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T + samples = np.expand_dims(samples, axis=1) + else: + samples = mu + + return self._build_forecast_series(samples) @property def min_train_series_length(self) -> int: @@ -109,4 +119,4 @@ def _supports_range_index(self) -> bool: return True def _is_probabilistic(self) -> bool: - return False + return True From 60253a5a937ab6baa0badf3ef488a1443e2f52da Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Fri, 6 Jan 2023 10:42:38 +0100 Subject: [PATCH 02/13] include future covariates in sf_ets --- darts/models/forecasting/sf_ets.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py index bbe757ca65..39cb4239dd 100644 --- a/darts/models/forecasting/sf_ets.py +++ b/darts/models/forecasting/sf_ets.py @@ -9,6 +9,7 @@ from statsforecast.models import ETS from darts import TimeSeries +from darts.models import LinearRegressionModel from darts.models.forecasting.forecasting_model import ( FutureCovariatesLocalForecastingModel, ) @@ -81,9 +82,19 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non super()._fit(series, future_covariates) self._assert_univariate(series) series = self.training_series + + if future_covariates is not None: + linreg = LinearRegressionModel(lags_future_covariates=[0]) + resids = linreg.residuals( + series, future_covariates=series.slice_intersect(future_covariates) + ) + self._linreg = linreg + target = resids + else: + target = series + self.model.fit( - series.values(copy=False).flatten(), - X=future_covariates.values(copy=False) if future_covariates else None, + target.values(copy=False).flatten(), ) return self @@ -97,11 +108,19 @@ def _predict( super()._predict(n, future_covariates, num_samples) forecast_df = self.model.predict( h=n, - X=future_covariates.values(copy=False) if future_covariates else None, level=(68.27,), # ask one std for the confidence interval ) - mu = forecast_df["mean"] + if future_covariates is not None: + # TODO: match the future covariates to the index of the forecast + linreg_forecast = self._linreg.predict( + n, future_covariates=future_covariates + ) + linreg_forecast_pd = linreg_forecast.pd_series() + mu = forecast_df["mean"] + linreg_forecast_pd + else: + mu = forecast_df["mean"] + if num_samples > 1: std = forecast_df["hi-68.27"] - mu samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T From c7f20a369d6f78cdcbcc7d2c285538df4b026ab0 Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Sat, 7 Jan 2023 14:27:29 +0100 Subject: [PATCH 03/13] sf_ets with future_covariates works.. probably it is underestimating the uncertainty because it doesn't take into account the uncertainty of the coef esimation of the OLS --- darts/models/forecasting/sf_ets.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py index 39cb4239dd..0c8d8790c9 100644 --- a/darts/models/forecasting/sf_ets.py +++ b/darts/models/forecasting/sf_ets.py @@ -85,9 +85,14 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non if future_covariates is not None: linreg = LinearRegressionModel(lags_future_covariates=[0]) - resids = linreg.residuals( - series, future_covariates=series.slice_intersect(future_covariates) + linreg.fit(series, future_covariates=future_covariates) + fitted_values = linreg.model.predict( + X=future_covariates.slice_intersect(series).values() ) + fitted_values_ts = TimeSeries.from_times_and_values( + times=series.time_index, values=fitted_values + ) + resids = series - fitted_values_ts self._linreg = linreg target = resids else: @@ -106,28 +111,28 @@ def _predict( verbose: bool = False, ): super()._predict(n, future_covariates, num_samples) - forecast_df = self.model.predict( + forecast_dict = self.model.predict( h=n, level=(68.27,), # ask one std for the confidence interval ) if future_covariates is not None: - # TODO: match the future covariates to the index of the forecast linreg_forecast = self._linreg.predict( n, future_covariates=future_covariates ) - linreg_forecast_pd = linreg_forecast.pd_series() - mu = forecast_df["mean"] + linreg_forecast_pd + linreg_forecast_values = linreg_forecast.values().reshape( + n, + ) + mu = forecast_dict["mean"] + linreg_forecast_values else: - mu = forecast_df["mean"] + mu = forecast_dict["mean"] if num_samples > 1: - std = forecast_df["hi-68.27"] - mu + std = forecast_dict["hi-68.27"] - forecast_dict["mean"] samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T samples = np.expand_dims(samples, axis=1) else: samples = mu - return self._build_forecast_series(samples) @property From ece2badac75afe8326862448422709c61ec69aac Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Sun, 8 Jan 2023 11:36:40 +0100 Subject: [PATCH 04/13] Create separate file for StatsForecast models and extract some functions. --- darts/models/forecasting/sf_ets.py | 13 +- darts/models/forecasting/sf_models.py | 264 ++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 darts/models/forecasting/sf_models.py diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py index 0c8d8790c9..4b1ca2f792 100644 --- a/darts/models/forecasting/sf_ets.py +++ b/darts/models/forecasting/sf_ets.py @@ -116,19 +116,18 @@ def _predict( level=(68.27,), # ask one std for the confidence interval ) + mu_ets = forecast_dict["mean"] if future_covariates is not None: - linreg_forecast = self._linreg.predict( - n, future_covariates=future_covariates - ) - linreg_forecast_values = linreg_forecast.values().reshape( + mu_linreg = self._linreg.predict(n, future_covariates=future_covariates) + mu_linreg_values = mu_linreg.values().reshape( n, ) - mu = forecast_dict["mean"] + linreg_forecast_values + mu = mu_ets + mu_linreg_values else: - mu = forecast_dict["mean"] + mu = mu_ets if num_samples > 1: - std = forecast_dict["hi-68.27"] - forecast_dict["mean"] + std = forecast_dict["hi-68.27"] - mu_ets samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T samples = np.expand_dims(samples, axis=1) else: diff --git a/darts/models/forecasting/sf_models.py b/darts/models/forecasting/sf_models.py new file mode 100644 index 0000000000..929a41a7da --- /dev/null +++ b/darts/models/forecasting/sf_models.py @@ -0,0 +1,264 @@ +""" +StatsForecast Models + +- AutoETS +- AutoARIMA +- AutoTheta +- AutoCES +----------- +""" + +from typing import Optional + +import numpy as np +from statsforecast.models import ETS +from statsforecast.models import AutoARIMA as SFAutoARIMA + +from darts import TimeSeries +from darts.models import LinearRegressionModel +from darts.models.forecasting.forecasting_model import ( + FutureCovariatesLocalForecastingModel, +) + + +class StatsForecastETS(FutureCovariatesLocalForecastingModel): + def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs): + """ETS based on `Statsforecasts package + `_. + + This implementation can perform faster than the :class:`ExponentialSmoothing` model, + but typically requires more time on the first call, because it relies + on Numba and jit compilation. + + This model accepts the same arguments as the `statsforecast ETS + `_. package. + + Parameters + ---------- + season_length + Number of observations per cycle. Default: 1. + model + Three-character string identifying method using the framework + terminology of Hyndman et al. (2002). Possible values are: + + * "A" or "M" for error state, + * "N", "A" or "Ad" for trend state, + * "N", "A" or "M" for season state. + + For instance, "ANN" means additive error, no trend and no seasonality. + Furthermore, the character "Z" is a placeholder telling statsforecast + to search for the best model using AICs. Default: "ZZZ". + add_encoders + A large number of future covariates can be automatically generated with `add_encoders`. + This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that + will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to + transform the generated covariates. This happens all under one hood and only needs to be specified at + model creation. + Read :meth:`SequentialEncoder ` to find out more about + ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: + + .. highlight:: python + .. code-block:: python + + add_encoders={ + 'cyclic': {'future': ['month']}, + 'datetime_attribute': {'future': ['hour', 'dayofweek']}, + 'position': {'future': ['relative']}, + 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, + 'transformer': Scaler() + } + .. + + Examples + -------- + >>> from darts.datasets import AirPassengersDataset + >>> from darts.models import StatsForecastETS + >>> series = AirPassengersDataset().load() + >>> model = StatsForecastETS(season_length=12, model="AZZ") + >>> model.fit(series[:-36]) + >>> pred = model.predict(36) + """ + super().__init__(add_encoders=add_encoders) + self.model = ETS(*ets_args, **ets_kwargs) + + def __str__(self): + return "ETS-Statsforecasts" + + def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): + super()._fit(series, future_covariates) + self._assert_univariate(series) + series = self.training_series + + if future_covariates is not None: + linreg = LinearRegressionModel(lags_future_covariates=[0]) + linreg.fit(series, future_covariates=future_covariates) + fitted_values = linreg.model.predict( + X=future_covariates.slice_intersect(series).values() + ) + fitted_values_ts = TimeSeries.from_times_and_values( + times=series.time_index, values=fitted_values + ) + resids = series - fitted_values_ts + self._linreg = linreg + target = resids + else: + target = series + + self.model.fit( + target.values(copy=False).flatten(), + ) + return self + + def _predict( + self, + n: int, + future_covariates: Optional[TimeSeries] = None, + num_samples: int = 1, + verbose: bool = False, + ): + super()._predict(n, future_covariates, num_samples) + forecast_dict = self.model.predict( + h=n, + level=(68.27,), # ask one std for the confidence interval + ) + + mu_ets = forecast_dict["mean"] + if future_covariates is not None: + mu_linreg = self._linreg.predict(n, future_covariates=future_covariates) + mu_linreg_values = mu_linreg.values().reshape( + n, + ) + mu = mu_ets + mu_linreg_values + else: + mu = mu_ets + + if num_samples > 1: + samples = sf_create_samples(mu, forecast_dict, num_samples, n) + else: + samples = mu + return self._build_forecast_series(samples) + + @property + def min_train_series_length(self) -> int: + return 10 + + def _supports_range_index(self) -> bool: + return True + + def _is_probabilistic(self) -> bool: + return True + + +class StatsForecastAutoARIMA(FutureCovariatesLocalForecastingModel): + def __init__( + self, *autoarima_args, add_encoders: Optional[dict] = None, **autoarima_kwargs + ): + """Auto-ARIMA based on `Statsforecasts package + `_. + + This implementation can perform faster than the :class:`AutoARIMA` model, + but typically requires more time on the first call, because it relies + on Numba and jit compilation. + + It is probabilistic, whereas :class:`AutoARIMA` is not. + + We refer to the `statsforecast AutoARIMA documentation + `_ + for the documentation of the arguments. + + Parameters + ---------- + autoarima_args + Positional arguments for ``statsforecasts.models.AutoARIMA``. + autoarima_kwargs + Keyword arguments for ``statsforecasts.models.AutoARIMA``. + add_encoders + A large number of future covariates can be automatically generated with `add_encoders`. + This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that + will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to + transform the generated covariates. This happens all under one hood and only needs to be specified at + model creation. + Read :meth:`SequentialEncoder ` to find out more about + ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: + + .. highlight:: python + .. code-block:: python + + add_encoders={ + 'cyclic': {'future': ['month']}, + 'datetime_attribute': {'future': ['hour', 'dayofweek']}, + 'position': {'future': ['relative']}, + 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, + 'transformer': Scaler() + } + .. + + Examples + -------- + >>> from darts.models import StatsForecastAutoARIMA + >>> from darts.datasets import AirPassengersDataset + >>> series = AirPassengersDataset().load() + >>> model = StatsForecastAutoARIMA(season_length=12) + >>> model.fit(series[:-36]) + >>> pred = model.predict(36, num_samples=100) + """ + super().__init__(add_encoders=add_encoders) + self.model = SFAutoARIMA(*autoarima_args, **autoarima_kwargs) + + def __str__(self): + return "Auto-ARIMA-Statsforecasts" + + def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): + super()._fit(series, future_covariates) + self._assert_univariate(series) + series = self.training_series + self.model.fit( + series.values(copy=False).flatten(), + X=future_covariates.values(copy=False) if future_covariates else None, + ) + return self + + def _predict( + self, + n: int, + future_covariates: Optional[TimeSeries] = None, + num_samples: int = 1, + verbose: bool = False, + ): + super()._predict(n, future_covariates, num_samples) + forecast_dict = self.model.predict( + h=n, + X=future_covariates.values(copy=False) if future_covariates else None, + level=(68.27,), # ask one std for the confidence interval. + ) + + mu = forecast_dict["mean"] + if num_samples > 1: + samples = sf_create_samples(mu, forecast_dict, num_samples, n) + else: + samples = mu + + return self._build_forecast_series(samples) + + @property + def min_train_series_length(self) -> int: + return 10 + + def _supports_range_index(self) -> bool: + return True + + def _is_probabilistic(self) -> bool: + return True + + +def sf_create_samples( + mu: float, + forecast_dict: dict, + num_samples: int, + n: int, +): + """Generate samples for StatsForecast Models""" + std = forecast_dict["hi-68.27"] - mu + samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T + samples = np.expand_dims(samples, axis=1) + return samples From 4131cb090e30f66add0170620281627f7b7c978b Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Mon, 9 Jan 2023 14:17:46 +0100 Subject: [PATCH 05/13] Added AutoTheta from the StatsForecast package. --- darts/models/forecasting/sf_models.py | 111 +++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 9 deletions(-) diff --git a/darts/models/forecasting/sf_models.py b/darts/models/forecasting/sf_models.py index 929a41a7da..1498b3a264 100644 --- a/darts/models/forecasting/sf_models.py +++ b/darts/models/forecasting/sf_models.py @@ -13,11 +13,13 @@ import numpy as np from statsforecast.models import ETS from statsforecast.models import AutoARIMA as SFAutoARIMA +from statsforecast.models import AutoTheta as SFAutoTheta from darts import TimeSeries from darts.models import LinearRegressionModel from darts.models.forecasting.forecasting_model import ( FutureCovariatesLocalForecastingModel, + LocalForecastingModel, ) @@ -90,6 +92,7 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non series = self.training_series if future_covariates is not None: + # perform OLS and get in-sample residuals linreg = LinearRegressionModel(lags_future_covariates=[0]) linreg.fit(series, future_covariates=future_covariates) fitted_values = linreg.model.predict( @@ -122,7 +125,8 @@ def _predict( level=(68.27,), # ask one std for the confidence interval ) - mu_ets = forecast_dict["mean"] + mu_ets, std = unpack_sf_dict(forecast_dict) + if future_covariates is not None: mu_linreg = self._linreg.predict(n, future_covariates=future_covariates) mu_linreg_values = mu_linreg.values().reshape( @@ -133,7 +137,7 @@ def _predict( mu = mu_ets if num_samples > 1: - samples = sf_create_samples(mu, forecast_dict, num_samples, n) + samples = create_normal_samples(mu, std, num_samples, n) else: samples = mu return self._build_forecast_series(samples) @@ -232,9 +236,90 @@ def _predict( level=(68.27,), # ask one std for the confidence interval. ) - mu = forecast_dict["mean"] + mu, std = unpack_sf_dict(forecast_dict) + if num_samples > 1: + samples = create_normal_samples(mu, std, num_samples, n) + else: + samples = mu + + return self._build_forecast_series(samples) + + @property + def min_train_series_length(self) -> int: + return 10 + + def _supports_range_index(self) -> bool: + return True + + def _is_probabilistic(self) -> bool: + return True + + +class StatsForecastAutoTheta(LocalForecastingModel): + def __init__( + self, *autotheta_args, add_encoders: Optional[dict] = None, **autotheta_kwargs + ): + """Auto-Theta based on `Statsforecasts package + `_. + + Automatically selects the best Theta (Standard Theta Model (‘STM’), Optimized Theta Model (‘OTM’), + Dynamic Standard Theta Model (‘DSTM’), Dynamic Optimized Theta Model (‘DOTM’)) model using mse. + + + It is probabilistic, whereas :class:`FourTheta` is not. + + We refer to the `statsforecast AutoTheta documentation + `_ + for the documentation of the arguments. + + Parameters + ---------- + autotheta_args + Positional arguments for ``statsforecasts.models.AutoTheta``. + autotheta_kwargs + Keyword arguments for ``statsforecasts.models.AutoTheta``. + + .. + + Examples + -------- + >>> from darts.models import StatsForecastAutoTheta + >>> from darts.datasets import AirPassengersDataset + >>> series = AirPassengersDataset().load() + >>> model = StatsForecastAutoTheta(season_length=12) + >>> model.fit(series[:-36]) + >>> pred = model.predict(36, num_samples=100) + """ + super().__init__() + self.model = SFAutoTheta(*autotheta_args, **autotheta_kwargs) + + def __str__(self): + return "Auto-Theta-Statsforecasts" + + def fit(self, series: TimeSeries): + super().fit(series) + self._assert_univariate(series) + series = self.training_series + self.model.fit( + series.values(copy=False).flatten(), + ) + return self + + def predict( + self, + n: int, + num_samples: int = 1, + verbose: bool = False, + ): + super().predict(n, num_samples) + forecast_dict = self.model.predict( + h=n, + level=(68.27,), # ask one std for the confidence interval. + ) + + mu, std = unpack_sf_dict(forecast_dict) if num_samples > 1: - samples = sf_create_samples(mu, forecast_dict, num_samples, n) + samples = create_normal_samples(mu, std, num_samples, n) else: samples = mu @@ -251,14 +336,22 @@ def _is_probabilistic(self) -> bool: return True -def sf_create_samples( +def create_normal_samples( mu: float, - forecast_dict: dict, + std: float, num_samples: int, n: int, -): - """Generate samples for StatsForecast Models""" - std = forecast_dict["hi-68.27"] - mu +) -> np.array: + """Generate samples assuming a Normal distribution.""" samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T samples = np.expand_dims(samples, axis=1) return samples + + +def unpack_sf_dict( + forecast_dict: dict, +): + """Unpack the dictionary that is returned by the StatsForecast 'predict()' method.""" + mu = forecast_dict["mean"] + std = forecast_dict["hi-68.27"] - mu + return mu, std From 2921d5213e8f8f6c6159c033dfe723adfbc0b32c Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Mon, 9 Jan 2023 14:23:05 +0100 Subject: [PATCH 06/13] Deleted sf_auto_arima.py and sf_ets.py, because the code is now included in sf_models.py. --- darts/models/__init__.py | 6 +- darts/models/forecasting/sf_auto_arima.py | 118 ------------------ darts/models/forecasting/sf_ets.py | 145 ---------------------- 3 files changed, 4 insertions(+), 265 deletions(-) delete mode 100644 darts/models/forecasting/sf_auto_arima.py delete mode 100644 darts/models/forecasting/sf_ets.py diff --git a/darts/models/__init__.py b/darts/models/__init__.py index cc982fe94b..855ebe9f27 100644 --- a/darts/models/__init__.py +++ b/darts/models/__init__.py @@ -89,8 +89,10 @@ class NotImportedCatBoostModel: try: from darts.models.forecasting.croston import Croston - from darts.models.forecasting.sf_auto_arima import StatsForecastAutoARIMA - from darts.models.forecasting.sf_ets import StatsForecastETS + from darts.models.forecasting.sf_models import ( + StatsForecastAutoARIMA, + StatsForecastETS, + ) except ImportError: logger.warning( "The statsforecast module could not be imported. " diff --git a/darts/models/forecasting/sf_auto_arima.py b/darts/models/forecasting/sf_auto_arima.py deleted file mode 100644 index 66e53876a2..0000000000 --- a/darts/models/forecasting/sf_auto_arima.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -StatsForecastAutoARIMA ------------ -""" - -from typing import Optional - -import numpy as np -from statsforecast.models import AutoARIMA as SFAutoARIMA - -from darts import TimeSeries -from darts.models.forecasting.forecasting_model import ( - FutureCovariatesLocalForecastingModel, -) - - -class StatsForecastAutoARIMA(FutureCovariatesLocalForecastingModel): - def __init__( - self, *autoarima_args, add_encoders: Optional[dict] = None, **autoarima_kwargs - ): - """Auto-ARIMA based on `Statsforecasts package - `_. - - This implementation can perform faster than the :class:`AutoARIMA` model, - but typically requires more time on the first call, because it relies - on Numba and jit compilation. - - It is probabilistic, whereas :class:`AutoARIMA` is not. - - We refer to the `statsforecast AutoARIMA documentation - `_ - for the documentation of the arguments. - - Parameters - ---------- - autoarima_args - Positional arguments for ``statsforecasts.models.AutoARIMA``. - autoarima_kwargs - Keyword arguments for ``statsforecasts.models.AutoARIMA``. - add_encoders - A large number of future covariates can be automatically generated with `add_encoders`. - This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that - will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to - transform the generated covariates. This happens all under one hood and only needs to be specified at - model creation. - Read :meth:`SequentialEncoder ` to find out more about - ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: - - .. highlight:: python - .. code-block:: python - - add_encoders={ - 'cyclic': {'future': ['month']}, - 'datetime_attribute': {'future': ['hour', 'dayofweek']}, - 'position': {'future': ['relative']}, - 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, - 'transformer': Scaler() - } - .. - - Examples - -------- - >>> from darts.models import StatsForecastAutoARIMA - >>> from darts.datasets import AirPassengersDataset - >>> series = AirPassengersDataset().load() - >>> model = StatsForecastAutoARIMA(season_length=12) - >>> model.fit(series[:-36]) - >>> pred = model.predict(36, num_samples=100) - """ - super().__init__(add_encoders=add_encoders) - self.model = SFAutoARIMA(*autoarima_args, **autoarima_kwargs) - - def __str__(self): - return "Auto-ARIMA-Statsforecasts" - - def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): - super()._fit(series, future_covariates) - self._assert_univariate(series) - series = self.training_series - self.model.fit( - series.values(copy=False).flatten(), - X=future_covariates.values(copy=False) if future_covariates else None, - ) - return self - - def _predict( - self, - n: int, - future_covariates: Optional[TimeSeries] = None, - num_samples: int = 1, - verbose: bool = False, - ): - super()._predict(n, future_covariates, num_samples) - forecast_df = self.model.predict( - h=n, - X=future_covariates.values(copy=False) if future_covariates else None, - level=(68.27,), # ask one std for the confidence interval. - ) - - mu = forecast_df["mean"] - if num_samples > 1: - std = forecast_df["hi-68.27"] - mu - samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T - samples = np.expand_dims(samples, axis=1) - else: - samples = mu - - return self._build_forecast_series(samples) - - @property - def min_train_series_length(self) -> int: - return 10 - - def _supports_range_index(self) -> bool: - return True - - def _is_probabilistic(self) -> bool: - return True diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py deleted file mode 100644 index 4b1ca2f792..0000000000 --- a/darts/models/forecasting/sf_ets.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -StatsForecastETS ------------ -""" - -from typing import Optional - -import numpy as np -from statsforecast.models import ETS - -from darts import TimeSeries -from darts.models import LinearRegressionModel -from darts.models.forecasting.forecasting_model import ( - FutureCovariatesLocalForecastingModel, -) - - -class StatsForecastETS(FutureCovariatesLocalForecastingModel): - def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs): - """ETS based on `Statsforecasts package - `_. - - This implementation can perform faster than the :class:`ExponentialSmoothing` model, - but typically requires more time on the first call, because it relies - on Numba and jit compilation. - - This model accepts the same arguments as the `statsforecast ETS - `_. package. - - Parameters - ---------- - season_length - Number of observations per cycle. Default: 1. - model - Three-character string identifying method using the framework - terminology of Hyndman et al. (2002). Possible values are: - - * "A" or "M" for error state, - * "N", "A" or "Ad" for trend state, - * "N", "A" or "M" for season state. - - For instance, "ANN" means additive error, no trend and no seasonality. - Furthermore, the character "Z" is a placeholder telling statsforecast - to search for the best model using AICs. Default: "ZZZ". - add_encoders - A large number of future covariates can be automatically generated with `add_encoders`. - This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that - will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to - transform the generated covariates. This happens all under one hood and only needs to be specified at - model creation. - Read :meth:`SequentialEncoder ` to find out more about - ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: - - .. highlight:: python - .. code-block:: python - - add_encoders={ - 'cyclic': {'future': ['month']}, - 'datetime_attribute': {'future': ['hour', 'dayofweek']}, - 'position': {'future': ['relative']}, - 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, - 'transformer': Scaler() - } - .. - - Examples - -------- - >>> from darts.datasets import AirPassengersDataset - >>> from darts.models import StatsForecastETS - >>> series = AirPassengersDataset().load() - >>> model = StatsForecastETS(season_length=12, model="AZZ") - >>> model.fit(series[:-36]) - >>> pred = model.predict(36) - """ - super().__init__(add_encoders=add_encoders) - self.model = ETS(*ets_args, **ets_kwargs) - - def __str__(self): - return "ETS-Statsforecasts" - - def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): - super()._fit(series, future_covariates) - self._assert_univariate(series) - series = self.training_series - - if future_covariates is not None: - linreg = LinearRegressionModel(lags_future_covariates=[0]) - linreg.fit(series, future_covariates=future_covariates) - fitted_values = linreg.model.predict( - X=future_covariates.slice_intersect(series).values() - ) - fitted_values_ts = TimeSeries.from_times_and_values( - times=series.time_index, values=fitted_values - ) - resids = series - fitted_values_ts - self._linreg = linreg - target = resids - else: - target = series - - self.model.fit( - target.values(copy=False).flatten(), - ) - return self - - def _predict( - self, - n: int, - future_covariates: Optional[TimeSeries] = None, - num_samples: int = 1, - verbose: bool = False, - ): - super()._predict(n, future_covariates, num_samples) - forecast_dict = self.model.predict( - h=n, - level=(68.27,), # ask one std for the confidence interval - ) - - mu_ets = forecast_dict["mean"] - if future_covariates is not None: - mu_linreg = self._linreg.predict(n, future_covariates=future_covariates) - mu_linreg_values = mu_linreg.values().reshape( - n, - ) - mu = mu_ets + mu_linreg_values - else: - mu = mu_ets - - if num_samples > 1: - std = forecast_dict["hi-68.27"] - mu_ets - samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T - samples = np.expand_dims(samples, axis=1) - else: - samples = mu - return self._build_forecast_series(samples) - - @property - def min_train_series_length(self) -> int: - return 10 - - def _supports_range_index(self) -> bool: - return True - - def _is_probabilistic(self) -> bool: - return True From 6a514d99af2f47b597c10e29ff82d4fc77daab13 Mon Sep 17 00:00:00 2001 From: Beerstabr <108391625+Beerstabr@users.noreply.github.com> Date: Thu, 26 Jan 2023 16:31:10 +0100 Subject: [PATCH 07/13] Update darts/models/forecasting/sf_models.py Co-authored-by: Julien Herzen --- darts/models/forecasting/sf_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/darts/models/forecasting/sf_models.py b/darts/models/forecasting/sf_models.py index 1498b3a264..7734795f20 100644 --- a/darts/models/forecasting/sf_models.py +++ b/darts/models/forecasting/sf_models.py @@ -129,7 +129,7 @@ def _predict( if future_covariates is not None: mu_linreg = self._linreg.predict(n, future_covariates=future_covariates) - mu_linreg_values = mu_linreg.values().reshape( + mu_linreg_values = mu_linreg.values(copy=False).reshape( n, ) mu = mu_ets + mu_linreg_values From b81fddb444e75638ca3fb65f77dbdae6689ee88c Mon Sep 17 00:00:00 2001 From: Beerstabr <108391625+Beerstabr@users.noreply.github.com> Date: Thu, 26 Jan 2023 16:31:26 +0100 Subject: [PATCH 08/13] Update darts/models/forecasting/sf_models.py Co-authored-by: Julien Herzen --- darts/models/forecasting/sf_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/darts/models/forecasting/sf_models.py b/darts/models/forecasting/sf_models.py index 7734795f20..d6f31d2bd3 100644 --- a/darts/models/forecasting/sf_models.py +++ b/darts/models/forecasting/sf_models.py @@ -96,7 +96,7 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non linreg = LinearRegressionModel(lags_future_covariates=[0]) linreg.fit(series, future_covariates=future_covariates) fitted_values = linreg.model.predict( - X=future_covariates.slice_intersect(series).values() + X=future_covariates.slice_intersect(series).values(copy=False) ) fitted_values_ts = TimeSeries.from_times_and_values( times=series.time_index, values=fitted_values From 67d48fa2bb06d0bf5363572bd4be65f4cf96ee07 Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Fri, 27 Jan 2023 12:15:40 +0100 Subject: [PATCH 09/13] Moved all statsforecast models to their own .py file. Added some comments explaining the handling of future covariates by StatsForecastETS. Included StatsForecastTheta in the tests. Moved the utility functions that the statsforecast models share to a singly .py file. Added the CES model which is supposed to be probabilistic, but that doesn't work yet eventhough it is supposed to be included in statsforecast 1.4.0. Trying to figure out why it isn't working. Removed sf_models.py. --- darts/models/__init__.py | 9 +- .../models/components/statsforecast_utils.py | 30 ++ darts/models/forecasting/sf_auto_arima.py | 120 ++++++ darts/models/forecasting/sf_auto_ces.py | 90 +++++ darts/models/forecasting/sf_auto_theta.py | 93 +++++ darts/models/forecasting/sf_ets.py | 155 ++++++++ darts/models/forecasting/sf_models.py | 357 ------------------ .../test_local_forecasting_models.py | 6 + requirements/core.txt | 2 +- 9 files changed, 500 insertions(+), 362 deletions(-) create mode 100644 darts/models/components/statsforecast_utils.py create mode 100644 darts/models/forecasting/sf_auto_arima.py create mode 100644 darts/models/forecasting/sf_auto_ces.py create mode 100644 darts/models/forecasting/sf_auto_theta.py create mode 100644 darts/models/forecasting/sf_ets.py delete mode 100644 darts/models/forecasting/sf_models.py diff --git a/darts/models/__init__.py b/darts/models/__init__.py index 855ebe9f27..c9066fafbb 100644 --- a/darts/models/__init__.py +++ b/darts/models/__init__.py @@ -89,10 +89,11 @@ class NotImportedCatBoostModel: try: from darts.models.forecasting.croston import Croston - from darts.models.forecasting.sf_models import ( - StatsForecastAutoARIMA, - StatsForecastETS, - ) + from darts.models.forecasting.sf_auto_arima import StatsForecastAutoARIMA + from darts.models.forecasting.sf_auto_ces import StatsForecastAutoCES + from darts.models.forecasting.sf_auto_theta import StatsForecastAutoTheta + from darts.models.forecasting.sf_ets import StatsForecastETS + except ImportError: logger.warning( "The statsforecast module could not be imported. " diff --git a/darts/models/components/statsforecast_utils.py b/darts/models/components/statsforecast_utils.py new file mode 100644 index 0000000000..9659ec86a3 --- /dev/null +++ b/darts/models/components/statsforecast_utils.py @@ -0,0 +1,30 @@ +""" +StatsForecast utils +----------- +""" + +import numpy as np + +# In a normal distribution, 68.27 percentage of values lie within one standard deviation of the mean +one_sigma_rule = 68.27 + + +def create_normal_samples( + mu: float, + std: float, + num_samples: int, + n: int, +) -> np.array: + """Generate samples assuming a Normal distribution.""" + samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T + samples = np.expand_dims(samples, axis=1) + return samples + + +def unpack_sf_dict( + forecast_dict: dict, +): + """Unpack the dictionary that is returned by the StatsForecast 'predict()' method.""" + mu = forecast_dict["mean"] + std = forecast_dict[f"hi-{one_sigma_rule}"] - mu + return mu, std diff --git a/darts/models/forecasting/sf_auto_arima.py b/darts/models/forecasting/sf_auto_arima.py new file mode 100644 index 0000000000..a6a048926a --- /dev/null +++ b/darts/models/forecasting/sf_auto_arima.py @@ -0,0 +1,120 @@ +""" +StatsForecastAutoARIMA +----------- +""" + +from typing import Optional + +from statsforecast.models import AutoARIMA as SFAutoARIMA + +from darts import TimeSeries +from darts.models.components.statsforecast_utils import ( + create_normal_samples, + one_sigma_rule, + unpack_sf_dict, +) +from darts.models.forecasting.forecasting_model import ( + FutureCovariatesLocalForecastingModel, +) + + +class StatsForecastAutoARIMA(FutureCovariatesLocalForecastingModel): + def __init__( + self, *autoarima_args, add_encoders: Optional[dict] = None, **autoarima_kwargs + ): + """Auto-ARIMA based on `Statsforecasts package + `_. + + This implementation can perform faster than the :class:`AutoARIMA` model, + but typically requires more time on the first call, because it relies + on Numba and jit compilation. + + It is probabilistic, whereas :class:`AutoARIMA` is not. + + We refer to the `statsforecast AutoARIMA documentation + `_ + for the documentation of the arguments. + + Parameters + ---------- + autoarima_args + Positional arguments for ``statsforecasts.models.AutoARIMA``. + autoarima_kwargs + Keyword arguments for ``statsforecasts.models.AutoARIMA``. + add_encoders + A large number of future covariates can be automatically generated with `add_encoders`. + This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that + will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to + transform the generated covariates. This happens all under one hood and only needs to be specified at + model creation. + Read :meth:`SequentialEncoder ` to find out more about + ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: + + .. highlight:: python + .. code-block:: python + + add_encoders={ + 'cyclic': {'future': ['month']}, + 'datetime_attribute': {'future': ['hour', 'dayofweek']}, + 'position': {'future': ['relative']}, + 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, + 'transformer': Scaler() + } + .. + + Examples + -------- + >>> from darts.models import StatsForecastAutoARIMA + >>> from darts.datasets import AirPassengersDataset + >>> series = AirPassengersDataset().load() + >>> model = StatsForecastAutoARIMA(season_length=12) + >>> model.fit(series[:-36]) + >>> pred = model.predict(36, num_samples=100) + """ + super().__init__(add_encoders=add_encoders) + self.model = SFAutoARIMA(*autoarima_args, **autoarima_kwargs) + + def __str__(self): + return "Auto-ARIMA-Statsforecasts" + + def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): + super()._fit(series, future_covariates) + self._assert_univariate(series) + series = self.training_series + self.model.fit( + series.values(copy=False).flatten(), + X=future_covariates.values(copy=False) if future_covariates else None, + ) + return self + + def _predict( + self, + n: int, + future_covariates: Optional[TimeSeries] = None, + num_samples: int = 1, + verbose: bool = False, + ): + super()._predict(n, future_covariates, num_samples) + forecast_dict = self.model.predict( + h=n, + X=future_covariates.values(copy=False) if future_covariates else None, + level=(one_sigma_rule,), # ask one std for the confidence interval. + ) + + mu, std = unpack_sf_dict(forecast_dict) + if num_samples > 1: + samples = create_normal_samples(mu, std, num_samples, n) + else: + samples = mu + + return self._build_forecast_series(samples) + + @property + def min_train_series_length(self) -> int: + return 10 + + def _supports_range_index(self) -> bool: + return True + + def _is_probabilistic(self) -> bool: + return True diff --git a/darts/models/forecasting/sf_auto_ces.py b/darts/models/forecasting/sf_auto_ces.py new file mode 100644 index 0000000000..c8d0408671 --- /dev/null +++ b/darts/models/forecasting/sf_auto_ces.py @@ -0,0 +1,90 @@ +""" +StatsForecastAutoCES +----------- +""" + +from statsforecast.models import AutoCES as SFAutoCES + +from darts import TimeSeries +from darts.models.components.statsforecast_utils import ( + create_normal_samples, + one_sigma_rule, + unpack_sf_dict, +) +from darts.models.forecasting.forecasting_model import LocalForecastingModel + + +class StatsForecastAutoCES(LocalForecastingModel): + def __init__(self, *autoces_args, **autoces_kwargs): + """Auto-CES based on `Statsforecasts package + `_. + + Automatically selects the best Complex Exponential Smoothing model using an information criterion. + + + We refer to the `statsforecast AutoCES documentation + `_ + for the documentation of the arguments. + + Parameters + ---------- + autoces_args + Positional arguments for ``statsforecasts.models.AutoCES``. + autoces_kwargs + Keyword arguments for ``statsforecasts.models.AutoCES``. + + .. + + Examples + -------- + >>> from darts.models import StatsForecastAutoCES + >>> from darts.datasets import AirPassengersDataset + >>> series = AirPassengersDataset().load() + >>> model = StatsForecastAutoCES(season_length=12) + >>> model.fit(series[:-36]) + >>> pred = model.predict(36, num_samples=100) + """ + super().__init__() + self.model = SFAutoCES(*autoces_args, **autoces_kwargs) + + def __str__(self): + return "Auto-CES-Statsforecasts" + + def fit(self, series: TimeSeries): + super().fit(series) + self._assert_univariate(series) + series = self.training_series + self.model.fit( + series.values(copy=False).flatten(), + ) + return self + + def predict( + self, + n: int, + num_samples: int = 1, + verbose: bool = False, + ): + super().predict(n, num_samples) + forecast_dict = self.model.predict( + h=n, + level=(one_sigma_rule,), # ask one std for the confidence interval. + ) + + mu, std = unpack_sf_dict(forecast_dict) + if num_samples > 1: + samples = create_normal_samples(mu, std, num_samples, n) + else: + samples = mu + + return self._build_forecast_series(samples) + + @property + def min_train_series_length(self) -> int: + return 10 + + def _supports_range_index(self) -> bool: + return True + + def _is_probabilistic(self) -> bool: + return True diff --git a/darts/models/forecasting/sf_auto_theta.py b/darts/models/forecasting/sf_auto_theta.py new file mode 100644 index 0000000000..3280a16722 --- /dev/null +++ b/darts/models/forecasting/sf_auto_theta.py @@ -0,0 +1,93 @@ +""" +StatsForecastAutoTheta +----------- +""" + +from statsforecast.models import AutoTheta as SFAutoTheta + +from darts import TimeSeries +from darts.models.components.statsforecast_utils import ( + create_normal_samples, + one_sigma_rule, + unpack_sf_dict, +) +from darts.models.forecasting.forecasting_model import LocalForecastingModel + + +class StatsForecastAutoTheta(LocalForecastingModel): + def __init__(self, *autotheta_args, **autotheta_kwargs): + """Auto-Theta based on `Statsforecasts package + `_. + + Automatically selects the best Theta (Standard Theta Model (‘STM’), Optimized Theta Model (‘OTM’), + Dynamic Standard Theta Model (‘DSTM’), Dynamic Optimized Theta Model (‘DOTM’)) model using mse. + + + It is probabilistic, whereas :class:`FourTheta` is not. + + We refer to the `statsforecast AutoTheta documentation + `_ + for the documentation of the arguments. + + Parameters + ---------- + autotheta_args + Positional arguments for ``statsforecasts.models.AutoTheta``. + autotheta_kwargs + Keyword arguments for ``statsforecasts.models.AutoTheta``. + + .. + + Examples + -------- + >>> from darts.models import StatsForecastAutoTheta + >>> from darts.datasets import AirPassengersDataset + >>> series = AirPassengersDataset().load() + >>> model = StatsForecastAutoTheta(season_length=12) + >>> model.fit(series[:-36]) + >>> pred = model.predict(36, num_samples=100) + """ + super().__init__() + self.model = SFAutoTheta(*autotheta_args, **autotheta_kwargs) + + def __str__(self): + return "Auto-Theta-Statsforecasts" + + def fit(self, series: TimeSeries): + super().fit(series) + self._assert_univariate(series) + series = self.training_series + self.model.fit( + series.values(copy=False).flatten(), + ) + return self + + def predict( + self, + n: int, + num_samples: int = 1, + verbose: bool = False, + ): + super().predict(n, num_samples) + forecast_dict = self.model.predict( + h=n, + level=(one_sigma_rule,), # ask one std for the confidence interval. + ) + + mu, std = unpack_sf_dict(forecast_dict) + if num_samples > 1: + samples = create_normal_samples(mu, std, num_samples, n) + else: + samples = mu + + return self._build_forecast_series(samples) + + @property + def min_train_series_length(self) -> int: + return 10 + + def _supports_range_index(self) -> bool: + return True + + def _is_probabilistic(self) -> bool: + return True diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py new file mode 100644 index 0000000000..62e998818f --- /dev/null +++ b/darts/models/forecasting/sf_ets.py @@ -0,0 +1,155 @@ +""" +StatsForecastETS +----------- +""" + +from typing import Optional + +from statsforecast.models import AutoETS + +from darts import TimeSeries +from darts.models import LinearRegressionModel +from darts.models.components.statsforecast_utils import ( + create_normal_samples, + one_sigma_rule, + unpack_sf_dict, +) +from darts.models.forecasting.forecasting_model import ( + FutureCovariatesLocalForecastingModel, +) + + +class StatsForecastETS(FutureCovariatesLocalForecastingModel): + def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs): + """ETS based on `Statsforecasts package + `_. + + This implementation can perform faster than the :class:`ExponentialSmoothing` model, + but typically requires more time on the first call, because it relies + on Numba and jit compilation. + + This model accepts the same arguments as the `statsforecast ETS + `_. package. + + In addition to the StatsForecast implementation, this model can handle future covariates. It does so by first + regressing the series against the future covariates using the :class:'LinearRegressionModel' model and then + running StatsForecast's AutoETS on the in-sample residuals from this original regression. This approach was + inspired by 'this post of Stephan Kolassa< https://stats.stackexchange.com/q/220885>'_. + + + Parameters + ---------- + season_length + Number of observations per cycle. Default: 1. + model + Three-character string identifying method using the framework + terminology of Hyndman et al. (2002). Possible values are: + + * "A" or "M" for error state, + * "N", "A" or "Ad" for trend state, + * "N", "A" or "M" for season state. + + For instance, "ANN" means additive error, no trend and no seasonality. + Furthermore, the character "Z" is a placeholder telling statsforecast + to search for the best model using AICs. Default: "ZZZ". + add_encoders + A large number of future covariates can be automatically generated with `add_encoders`. + This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that + will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to + transform the generated covariates. This happens all under one hood and only needs to be specified at + model creation. + Read :meth:`SequentialEncoder ` to find out more about + ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: + + .. highlight:: python + .. code-block:: python + + add_encoders={ + 'cyclic': {'future': ['month']}, + 'datetime_attribute': {'future': ['hour', 'dayofweek']}, + 'position': {'future': ['relative']}, + 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, + 'transformer': Scaler() + } + .. + + Examples + -------- + >>> from darts.datasets import AirPassengersDataset + >>> from darts.models import StatsForecastETS + >>> series = AirPassengersDataset().load() + >>> model = StatsForecastETS(season_length=12, model="AZZ") + >>> model.fit(series[:-36]) + >>> pred = model.predict(36) + """ + super().__init__(add_encoders=add_encoders) + self.model = AutoETS(*ets_args, **ets_kwargs) + + def __str__(self): + return "ETS-Statsforecasts" + + def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): + super()._fit(series, future_covariates) + self._assert_univariate(series) + series = self.training_series + + if future_covariates is not None: + # perform OLS and get in-sample residuals + linreg = LinearRegressionModel(lags_future_covariates=[0]) + linreg.fit(series, future_covariates=future_covariates) + fitted_values = linreg.model.predict( + X=future_covariates.slice_intersect(series).values(copy=False) + ) + fitted_values_ts = TimeSeries.from_times_and_values( + times=series.time_index, values=fitted_values + ) + resids = series - fitted_values_ts + self._linreg = linreg + target = resids + else: + target = series + + self.model.fit( + target.values(copy=False).flatten(), + ) + return self + + def _predict( + self, + n: int, + future_covariates: Optional[TimeSeries] = None, + num_samples: int = 1, + verbose: bool = False, + ): + super()._predict(n, future_covariates, num_samples) + forecast_dict = self.model.predict( + h=n, + level=(one_sigma_rule,), # ask one std for the confidence interval + ) + + mu_ets, std = unpack_sf_dict(forecast_dict) + + if future_covariates is not None: + mu_linreg = self._linreg.predict(n, future_covariates=future_covariates) + mu_linreg_values = mu_linreg.values(copy=False).reshape( + n, + ) + mu = mu_ets + mu_linreg_values + else: + mu = mu_ets + + if num_samples > 1: + samples = create_normal_samples(mu, std, num_samples, n) + else: + samples = mu + return self._build_forecast_series(samples) + + @property + def min_train_series_length(self) -> int: + return 10 + + def _supports_range_index(self) -> bool: + return True + + def _is_probabilistic(self) -> bool: + return True diff --git a/darts/models/forecasting/sf_models.py b/darts/models/forecasting/sf_models.py deleted file mode 100644 index d6f31d2bd3..0000000000 --- a/darts/models/forecasting/sf_models.py +++ /dev/null @@ -1,357 +0,0 @@ -""" -StatsForecast Models - -- AutoETS -- AutoARIMA -- AutoTheta -- AutoCES ------------ -""" - -from typing import Optional - -import numpy as np -from statsforecast.models import ETS -from statsforecast.models import AutoARIMA as SFAutoARIMA -from statsforecast.models import AutoTheta as SFAutoTheta - -from darts import TimeSeries -from darts.models import LinearRegressionModel -from darts.models.forecasting.forecasting_model import ( - FutureCovariatesLocalForecastingModel, - LocalForecastingModel, -) - - -class StatsForecastETS(FutureCovariatesLocalForecastingModel): - def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs): - """ETS based on `Statsforecasts package - `_. - - This implementation can perform faster than the :class:`ExponentialSmoothing` model, - but typically requires more time on the first call, because it relies - on Numba and jit compilation. - - This model accepts the same arguments as the `statsforecast ETS - `_. package. - - Parameters - ---------- - season_length - Number of observations per cycle. Default: 1. - model - Three-character string identifying method using the framework - terminology of Hyndman et al. (2002). Possible values are: - - * "A" or "M" for error state, - * "N", "A" or "Ad" for trend state, - * "N", "A" or "M" for season state. - - For instance, "ANN" means additive error, no trend and no seasonality. - Furthermore, the character "Z" is a placeholder telling statsforecast - to search for the best model using AICs. Default: "ZZZ". - add_encoders - A large number of future covariates can be automatically generated with `add_encoders`. - This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that - will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to - transform the generated covariates. This happens all under one hood and only needs to be specified at - model creation. - Read :meth:`SequentialEncoder ` to find out more about - ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: - - .. highlight:: python - .. code-block:: python - - add_encoders={ - 'cyclic': {'future': ['month']}, - 'datetime_attribute': {'future': ['hour', 'dayofweek']}, - 'position': {'future': ['relative']}, - 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, - 'transformer': Scaler() - } - .. - - Examples - -------- - >>> from darts.datasets import AirPassengersDataset - >>> from darts.models import StatsForecastETS - >>> series = AirPassengersDataset().load() - >>> model = StatsForecastETS(season_length=12, model="AZZ") - >>> model.fit(series[:-36]) - >>> pred = model.predict(36) - """ - super().__init__(add_encoders=add_encoders) - self.model = ETS(*ets_args, **ets_kwargs) - - def __str__(self): - return "ETS-Statsforecasts" - - def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): - super()._fit(series, future_covariates) - self._assert_univariate(series) - series = self.training_series - - if future_covariates is not None: - # perform OLS and get in-sample residuals - linreg = LinearRegressionModel(lags_future_covariates=[0]) - linreg.fit(series, future_covariates=future_covariates) - fitted_values = linreg.model.predict( - X=future_covariates.slice_intersect(series).values(copy=False) - ) - fitted_values_ts = TimeSeries.from_times_and_values( - times=series.time_index, values=fitted_values - ) - resids = series - fitted_values_ts - self._linreg = linreg - target = resids - else: - target = series - - self.model.fit( - target.values(copy=False).flatten(), - ) - return self - - def _predict( - self, - n: int, - future_covariates: Optional[TimeSeries] = None, - num_samples: int = 1, - verbose: bool = False, - ): - super()._predict(n, future_covariates, num_samples) - forecast_dict = self.model.predict( - h=n, - level=(68.27,), # ask one std for the confidence interval - ) - - mu_ets, std = unpack_sf_dict(forecast_dict) - - if future_covariates is not None: - mu_linreg = self._linreg.predict(n, future_covariates=future_covariates) - mu_linreg_values = mu_linreg.values(copy=False).reshape( - n, - ) - mu = mu_ets + mu_linreg_values - else: - mu = mu_ets - - if num_samples > 1: - samples = create_normal_samples(mu, std, num_samples, n) - else: - samples = mu - return self._build_forecast_series(samples) - - @property - def min_train_series_length(self) -> int: - return 10 - - def _supports_range_index(self) -> bool: - return True - - def _is_probabilistic(self) -> bool: - return True - - -class StatsForecastAutoARIMA(FutureCovariatesLocalForecastingModel): - def __init__( - self, *autoarima_args, add_encoders: Optional[dict] = None, **autoarima_kwargs - ): - """Auto-ARIMA based on `Statsforecasts package - `_. - - This implementation can perform faster than the :class:`AutoARIMA` model, - but typically requires more time on the first call, because it relies - on Numba and jit compilation. - - It is probabilistic, whereas :class:`AutoARIMA` is not. - - We refer to the `statsforecast AutoARIMA documentation - `_ - for the documentation of the arguments. - - Parameters - ---------- - autoarima_args - Positional arguments for ``statsforecasts.models.AutoARIMA``. - autoarima_kwargs - Keyword arguments for ``statsforecasts.models.AutoARIMA``. - add_encoders - A large number of future covariates can be automatically generated with `add_encoders`. - This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that - will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to - transform the generated covariates. This happens all under one hood and only needs to be specified at - model creation. - Read :meth:`SequentialEncoder ` to find out more about - ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: - - .. highlight:: python - .. code-block:: python - - add_encoders={ - 'cyclic': {'future': ['month']}, - 'datetime_attribute': {'future': ['hour', 'dayofweek']}, - 'position': {'future': ['relative']}, - 'custom': {'future': [lambda idx: (idx.year - 1950) / 50]}, - 'transformer': Scaler() - } - .. - - Examples - -------- - >>> from darts.models import StatsForecastAutoARIMA - >>> from darts.datasets import AirPassengersDataset - >>> series = AirPassengersDataset().load() - >>> model = StatsForecastAutoARIMA(season_length=12) - >>> model.fit(series[:-36]) - >>> pred = model.predict(36, num_samples=100) - """ - super().__init__(add_encoders=add_encoders) - self.model = SFAutoARIMA(*autoarima_args, **autoarima_kwargs) - - def __str__(self): - return "Auto-ARIMA-Statsforecasts" - - def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = None): - super()._fit(series, future_covariates) - self._assert_univariate(series) - series = self.training_series - self.model.fit( - series.values(copy=False).flatten(), - X=future_covariates.values(copy=False) if future_covariates else None, - ) - return self - - def _predict( - self, - n: int, - future_covariates: Optional[TimeSeries] = None, - num_samples: int = 1, - verbose: bool = False, - ): - super()._predict(n, future_covariates, num_samples) - forecast_dict = self.model.predict( - h=n, - X=future_covariates.values(copy=False) if future_covariates else None, - level=(68.27,), # ask one std for the confidence interval. - ) - - mu, std = unpack_sf_dict(forecast_dict) - if num_samples > 1: - samples = create_normal_samples(mu, std, num_samples, n) - else: - samples = mu - - return self._build_forecast_series(samples) - - @property - def min_train_series_length(self) -> int: - return 10 - - def _supports_range_index(self) -> bool: - return True - - def _is_probabilistic(self) -> bool: - return True - - -class StatsForecastAutoTheta(LocalForecastingModel): - def __init__( - self, *autotheta_args, add_encoders: Optional[dict] = None, **autotheta_kwargs - ): - """Auto-Theta based on `Statsforecasts package - `_. - - Automatically selects the best Theta (Standard Theta Model (‘STM’), Optimized Theta Model (‘OTM’), - Dynamic Standard Theta Model (‘DSTM’), Dynamic Optimized Theta Model (‘DOTM’)) model using mse. - - - It is probabilistic, whereas :class:`FourTheta` is not. - - We refer to the `statsforecast AutoTheta documentation - `_ - for the documentation of the arguments. - - Parameters - ---------- - autotheta_args - Positional arguments for ``statsforecasts.models.AutoTheta``. - autotheta_kwargs - Keyword arguments for ``statsforecasts.models.AutoTheta``. - - .. - - Examples - -------- - >>> from darts.models import StatsForecastAutoTheta - >>> from darts.datasets import AirPassengersDataset - >>> series = AirPassengersDataset().load() - >>> model = StatsForecastAutoTheta(season_length=12) - >>> model.fit(series[:-36]) - >>> pred = model.predict(36, num_samples=100) - """ - super().__init__() - self.model = SFAutoTheta(*autotheta_args, **autotheta_kwargs) - - def __str__(self): - return "Auto-Theta-Statsforecasts" - - def fit(self, series: TimeSeries): - super().fit(series) - self._assert_univariate(series) - series = self.training_series - self.model.fit( - series.values(copy=False).flatten(), - ) - return self - - def predict( - self, - n: int, - num_samples: int = 1, - verbose: bool = False, - ): - super().predict(n, num_samples) - forecast_dict = self.model.predict( - h=n, - level=(68.27,), # ask one std for the confidence interval. - ) - - mu, std = unpack_sf_dict(forecast_dict) - if num_samples > 1: - samples = create_normal_samples(mu, std, num_samples, n) - else: - samples = mu - - return self._build_forecast_series(samples) - - @property - def min_train_series_length(self) -> int: - return 10 - - def _supports_range_index(self) -> bool: - return True - - def _is_probabilistic(self) -> bool: - return True - - -def create_normal_samples( - mu: float, - std: float, - num_samples: int, - n: int, -) -> np.array: - """Generate samples assuming a Normal distribution.""" - samples = np.random.normal(loc=mu, scale=std, size=(num_samples, n)).T - samples = np.expand_dims(samples, axis=1) - return samples - - -def unpack_sf_dict( - forecast_dict: dict, -): - """Unpack the dictionary that is returned by the StatsForecast 'predict()' method.""" - mu = forecast_dict["mean"] - std = forecast_dict["hi-68.27"] - mu - return mu, std diff --git a/darts/tests/models/forecasting/test_local_forecasting_models.py b/darts/tests/models/forecasting/test_local_forecasting_models.py index 63b5c9d82a..6c46e704d9 100644 --- a/darts/tests/models/forecasting/test_local_forecasting_models.py +++ b/darts/tests/models/forecasting/test_local_forecasting_models.py @@ -31,6 +31,7 @@ RandomForest, RegressionModel, StatsForecastAutoARIMA, + StatsForecastAutoTheta, StatsForecastETS, Theta, ) @@ -51,6 +52,11 @@ (ARIMA(12, 2, 1), 5.2), (ARIMA(1, 1, 1), 24), (StatsForecastAutoARIMA(season_length=12), 4.6), + ( + StatsForecastAutoTheta(season_length=12, decomposition_type="multiplicative"), + 5.5, + ), + (StatsForecastAutoTheta(season_length=12, decomposition_type="additive"), 7.9), (StatsForecastETS(season_length=12, model="AAZ"), 4.1), (Croston(version="classic"), 23), (Croston(version="tsb", alpha_d=0.1, alpha_p=0.1), 23), diff --git a/requirements/core.txt b/requirements/core.txt index ccc0ebe5f4..a1b6758911 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -13,7 +13,7 @@ requests>=2.22.0 scikit-learn>=1.0.1 scipy>=1.3.2 shap>=0.40.0 -statsforecast>=1.0.0 +statsforecast>=1.4.0 statsmodels>=0.13.0 tbats>=1.1.0 tqdm>=4.60.0 From 6a2f73fd1c4cc919d04f608ac7a1c8fe75f8f6a3 Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Sat, 28 Jan 2023 10:28:46 +0100 Subject: [PATCH 10/13] Beginning of test for fit on residuals for statsforecast ets. --- darts/models/forecasting/sf_ets.py | 1 + .../test_local_forecasting_models.py | 9 ++-- darts/tests/models/forecasting/test_sf_ets.py | 53 +++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 darts/tests/models/forecasting/test_sf_ets.py diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py index 62e998818f..4d0d482c93 100644 --- a/darts/models/forecasting/sf_ets.py +++ b/darts/models/forecasting/sf_ets.py @@ -84,6 +84,7 @@ def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs) """ super().__init__(add_encoders=add_encoders) self.model = AutoETS(*ets_args, **ets_kwargs) + self._linreg = None def __str__(self): return "ETS-Statsforecasts" diff --git a/darts/tests/models/forecasting/test_local_forecasting_models.py b/darts/tests/models/forecasting/test_local_forecasting_models.py index 6c46e704d9..2805ada409 100644 --- a/darts/tests/models/forecasting/test_local_forecasting_models.py +++ b/darts/tests/models/forecasting/test_local_forecasting_models.py @@ -12,6 +12,7 @@ from darts.datasets import AirPassengersDataset, IceCreamHeaterDataset from darts.logging import get_logger from darts.metrics import mape +from darts.models import Theta # StatsForecastAutoCES, from darts.models import ( ARIMA, BATS, @@ -33,7 +34,6 @@ StatsForecastAutoARIMA, StatsForecastAutoTheta, StatsForecastETS, - Theta, ) from darts.models.forecasting.forecasting_model import ( LocalForecastingModel, @@ -52,12 +52,11 @@ (ARIMA(12, 2, 1), 5.2), (ARIMA(1, 1, 1), 24), (StatsForecastAutoARIMA(season_length=12), 4.6), - ( - StatsForecastAutoTheta(season_length=12, decomposition_type="multiplicative"), - 5.5, - ), + (StatsForecastAutoTheta(season_length=12), 5.5), (StatsForecastAutoTheta(season_length=12, decomposition_type="additive"), 7.9), + # (StatsForecastAutoCES(season_length=12, model="Z"), 4.1), (StatsForecastETS(season_length=12, model="AAZ"), 4.1), + (StatsForecastETS(season_length=12, model="AAZ", damped=True), 7.0), (Croston(version="classic"), 23), (Croston(version="tsb", alpha_d=0.1, alpha_p=0.1), 23), (Theta(), 11), diff --git a/darts/tests/models/forecasting/test_sf_ets.py b/darts/tests/models/forecasting/test_sf_ets.py new file mode 100644 index 0000000000..bc6f6a1a09 --- /dev/null +++ b/darts/tests/models/forecasting/test_sf_ets.py @@ -0,0 +1,53 @@ +import numpy as np +import pandas as pd + +from darts import TimeSeries +from darts.datasets import AirPassengersDataset + +# from darts.metrics import mape +from darts.models import StatsForecastETS +from darts.tests.base_test_class import DartsBaseTestClass + + +class StatsForecastETSTestCase(DartsBaseTestClass): + # real timeseries for functionality tests + ts_passengers = AirPassengersDataset().load() + ts_pass_train, ts_pass_val = ts_passengers.split_after(pd.Timestamp("19570101")) + + # as future covariates we want a trend + trend_values = np.arange(start=1, stop=len(ts_passengers) + 1) + trend_times = ts_passengers.time_index + ts_trend = TimeSeries.from_times_and_values( + times=trend_times, values=trend_values, columns=["trend"] + ) + ts_trend_train, ts_trend_val = ts_trend.split_after(pd.Timestamp("19570101")) + + def test_fit_on_residuals(self): + model = StatsForecastETS(season_length=12, model="ZZN") + + # test if we are indeed fitting the AutoETS on the residuals of the linear regression + model.fit(series=self.ts_pass_train, future_covariates=self.ts_trend_train) + + # check if linear regression was fit + self.assertIsNotNone(model._linreg) + self.assertTrue(model._linreg._fit_called) + + # create the residuals from the linear regression + fitted_values = model._linreg.model.predict( + X=self.ts_trend_train.values(copy=False) + ) + fitted_values_ts = TimeSeries.from_times_and_values( + times=self.ts_pass_train.time_index, values=fitted_values + ) + resids = self.ts_pass_train - fitted_values_ts + + # now make in-sample predictions with the AutoETS model + in_sample_preds = model.model.predict_in_sample()["fitted"] + ts_in_sample_preds = TimeSeries.from_times_and_values( + times=self.ts_pass_train.time_index, values=in_sample_preds + ) + + # compare in-sample predictions to the residuals they have supposedly been fitted on + # current_mape = mape(resids, ts_in_sample_preds) + + return resids, ts_in_sample_preds From 6ea16b4d107e121c98ea68983552101f98bee928 Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Wed, 1 Feb 2023 00:16:05 +0100 Subject: [PATCH 11/13] - AutoCES not probablisitc anymore, because that is not yet released in statsforecast 1.4.0 - changed AutoETS to SFAutoETS - added models to the base tests - wrote two units tests for future covariates use for sf_ets --- darts/models/forecasting/sf_auto_ces.py | 16 ++------ darts/models/forecasting/sf_ets.py | 4 +- .../test_local_forecasting_models.py | 7 ++-- darts/tests/models/forecasting/test_sf_ets.py | 38 ++++++++++++------- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/darts/models/forecasting/sf_auto_ces.py b/darts/models/forecasting/sf_auto_ces.py index c8d0408671..6d18713595 100644 --- a/darts/models/forecasting/sf_auto_ces.py +++ b/darts/models/forecasting/sf_auto_ces.py @@ -6,11 +6,6 @@ from statsforecast.models import AutoCES as SFAutoCES from darts import TimeSeries -from darts.models.components.statsforecast_utils import ( - create_normal_samples, - one_sigma_rule, - unpack_sf_dict, -) from darts.models.forecasting.forecasting_model import LocalForecastingModel @@ -68,16 +63,11 @@ def predict( super().predict(n, num_samples) forecast_dict = self.model.predict( h=n, - level=(one_sigma_rule,), # ask one std for the confidence interval. ) - mu, std = unpack_sf_dict(forecast_dict) - if num_samples > 1: - samples = create_normal_samples(mu, std, num_samples, n) - else: - samples = mu + mu = forecast_dict["mean"] - return self._build_forecast_series(samples) + return self._build_forecast_series(mu) @property def min_train_series_length(self) -> int: @@ -87,4 +77,4 @@ def _supports_range_index(self) -> bool: return True def _is_probabilistic(self) -> bool: - return True + return False diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_ets.py index 4d0d482c93..67c022d24b 100644 --- a/darts/models/forecasting/sf_ets.py +++ b/darts/models/forecasting/sf_ets.py @@ -5,7 +5,7 @@ from typing import Optional -from statsforecast.models import AutoETS +from statsforecast.models import ETS as SFAutoETS from darts import TimeSeries from darts.models import LinearRegressionModel @@ -83,7 +83,7 @@ def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs) >>> pred = model.predict(36) """ super().__init__(add_encoders=add_encoders) - self.model = AutoETS(*ets_args, **ets_kwargs) + self.model = SFAutoETS(*ets_args, **ets_kwargs) self._linreg = None def __str__(self): diff --git a/darts/tests/models/forecasting/test_local_forecasting_models.py b/darts/tests/models/forecasting/test_local_forecasting_models.py index 2805ada409..606679c880 100644 --- a/darts/tests/models/forecasting/test_local_forecasting_models.py +++ b/darts/tests/models/forecasting/test_local_forecasting_models.py @@ -12,7 +12,6 @@ from darts.datasets import AirPassengersDataset, IceCreamHeaterDataset from darts.logging import get_logger from darts.metrics import mape -from darts.models import Theta # StatsForecastAutoCES, from darts.models import ( ARIMA, BATS, @@ -32,8 +31,10 @@ RandomForest, RegressionModel, StatsForecastAutoARIMA, + StatsForecastAutoCES, StatsForecastAutoTheta, StatsForecastETS, + Theta, ) from darts.models.forecasting.forecasting_model import ( LocalForecastingModel, @@ -53,10 +54,8 @@ (ARIMA(1, 1, 1), 24), (StatsForecastAutoARIMA(season_length=12), 4.6), (StatsForecastAutoTheta(season_length=12), 5.5), - (StatsForecastAutoTheta(season_length=12, decomposition_type="additive"), 7.9), - # (StatsForecastAutoCES(season_length=12, model="Z"), 4.1), + (StatsForecastAutoCES(season_length=12, model="Z"), 7.3), (StatsForecastETS(season_length=12, model="AAZ"), 4.1), - (StatsForecastETS(season_length=12, model="AAZ", damped=True), 7.0), (Croston(version="classic"), 23), (Croston(version="tsb", alpha_d=0.1, alpha_p=0.1), 23), (Theta(), 11), diff --git a/darts/tests/models/forecasting/test_sf_ets.py b/darts/tests/models/forecasting/test_sf_ets.py index bc6f6a1a09..a2862bfaff 100644 --- a/darts/tests/models/forecasting/test_sf_ets.py +++ b/darts/tests/models/forecasting/test_sf_ets.py @@ -3,9 +3,8 @@ from darts import TimeSeries from darts.datasets import AirPassengersDataset - -# from darts.metrics import mape -from darts.models import StatsForecastETS +from darts.metrics import mae +from darts.models import LinearRegressionModel, StatsForecastETS from darts.tests.base_test_class import DartsBaseTestClass @@ -23,23 +22,19 @@ class StatsForecastETSTestCase(DartsBaseTestClass): ts_trend_train, ts_trend_val = ts_trend.split_after(pd.Timestamp("19570101")) def test_fit_on_residuals(self): - model = StatsForecastETS(season_length=12, model="ZZN") + model = StatsForecastETS(season_length=12, model="ZZZ") # test if we are indeed fitting the AutoETS on the residuals of the linear regression model.fit(series=self.ts_pass_train, future_covariates=self.ts_trend_train) - # check if linear regression was fit - self.assertIsNotNone(model._linreg) - self.assertTrue(model._linreg._fit_called) - # create the residuals from the linear regression - fitted_values = model._linreg.model.predict( + fitted_values_linreg = model._linreg.model.predict( X=self.ts_trend_train.values(copy=False) ) - fitted_values_ts = TimeSeries.from_times_and_values( - times=self.ts_pass_train.time_index, values=fitted_values + fitted_values_linreg_ts = TimeSeries.from_times_and_values( + times=self.ts_pass_train.time_index, values=fitted_values_linreg ) - resids = self.ts_pass_train - fitted_values_ts + resids = self.ts_pass_train - fitted_values_linreg_ts # now make in-sample predictions with the AutoETS model in_sample_preds = model.model.predict_in_sample()["fitted"] @@ -48,6 +43,21 @@ def test_fit_on_residuals(self): ) # compare in-sample predictions to the residuals they have supposedly been fitted on - # current_mape = mape(resids, ts_in_sample_preds) + current_mae = mae(resids, ts_in_sample_preds) + + self.assertTrue(current_mae < 9) + + def test_fit_a_linreg(self): + model = StatsForecastETS(season_length=12, model="ZZN") + model.fit(series=self.ts_pass_train, future_covariates=self.ts_trend_train) + + # check if linear regression was fit + self.assertIsNotNone(model._linreg) + self.assertTrue(model._linreg._fit_called) + + # fit a linear regression + linreg = LinearRegressionModel(lags_future_covariates=[0]) + linreg.fit(series=self.ts_pass_train, future_covariates=self.ts_trend_train) - return resids, ts_in_sample_preds + # check if the linear regression was fit on the same data by checking if the coefficients are equal + self.assertEqual(model._linreg.model.coef_, linreg.model.coef_) From b74c49a3456cc2d5fac7636f304df4b614d9b186 Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Wed, 1 Feb 2023 00:17:45 +0100 Subject: [PATCH 12/13] - AutoCES not probablisitc anymore, because that is not yet released in statsforecast 1.4.0 - changed AutoETS to SFAutoETS - added models to the base tests - wrote two units tests for future covariates use for sf_ets --- darts/tests/models/forecasting/test_sf_ets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/darts/tests/models/forecasting/test_sf_ets.py b/darts/tests/models/forecasting/test_sf_ets.py index a2862bfaff..a8c9535661 100644 --- a/darts/tests/models/forecasting/test_sf_ets.py +++ b/darts/tests/models/forecasting/test_sf_ets.py @@ -48,7 +48,7 @@ def test_fit_on_residuals(self): self.assertTrue(current_mae < 9) def test_fit_a_linreg(self): - model = StatsForecastETS(season_length=12, model="ZZN") + model = StatsForecastETS(season_length=12, model="ZZZ") model.fit(series=self.ts_pass_train, future_covariates=self.ts_trend_train) # check if linear regression was fit From 83f08dad63106aa4cb8f1f2e0506ec4f217b98b4 Mon Sep 17 00:00:00 2001 From: Beerstabr Date: Tue, 7 Feb 2023 17:53:20 +0100 Subject: [PATCH 13/13] Changed StatsForecastETS to StatsForecastAutoETS. --- darts/models/__init__.py | 8 ++++---- darts/models/forecasting/{sf_ets.py => sf_auto_ets.py} | 10 +++++----- .../forecasting/test_local_forecasting_models.py | 6 +++--- .../{test_sf_ets.py => test_sf_auto_ets.py} | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) rename darts/models/forecasting/{sf_ets.py => sf_auto_ets.py} (95%) rename darts/tests/models/forecasting/{test_sf_ets.py => test_sf_auto_ets.py} (90%) diff --git a/darts/models/__init__.py b/darts/models/__init__.py index c9066fafbb..60ac60b572 100644 --- a/darts/models/__init__.py +++ b/darts/models/__init__.py @@ -91,14 +91,14 @@ class NotImportedCatBoostModel: from darts.models.forecasting.croston import Croston from darts.models.forecasting.sf_auto_arima import StatsForecastAutoARIMA from darts.models.forecasting.sf_auto_ces import StatsForecastAutoCES + from darts.models.forecasting.sf_auto_ets import StatsForecastAutoETS from darts.models.forecasting.sf_auto_theta import StatsForecastAutoTheta - from darts.models.forecasting.sf_ets import StatsForecastETS except ImportError: logger.warning( "The statsforecast module could not be imported. " "To enable support for the StatsForecastAutoARIMA, " - "StatsForecastETS and Croston models, please consider " + "StatsForecastAutoETS and Croston models, please consider " "installing it." ) @@ -107,10 +107,10 @@ class NotImportedStatsForecastAutoARIMA: StatsForecastAutoARIMA = NotImportedStatsForecastAutoARIMA() - class NotImportedStatsForecastETS: + class NotImportedStatsForecastAutoETS: usable = False - StatsForecastETS = NotImportedStatsForecastETS() + StatsForecastAutoETS = NotImportedStatsForecastAutoETS() class NotImportedCroston: usable = False diff --git a/darts/models/forecasting/sf_ets.py b/darts/models/forecasting/sf_auto_ets.py similarity index 95% rename from darts/models/forecasting/sf_ets.py rename to darts/models/forecasting/sf_auto_ets.py index 67c022d24b..db12fd320d 100644 --- a/darts/models/forecasting/sf_ets.py +++ b/darts/models/forecasting/sf_auto_ets.py @@ -1,11 +1,11 @@ """ -StatsForecastETS +StatsForecastAutoETS ----------- """ from typing import Optional -from statsforecast.models import ETS as SFAutoETS +from statsforecast.models import AutoETS as SFAutoETS from darts import TimeSeries from darts.models import LinearRegressionModel @@ -19,7 +19,7 @@ ) -class StatsForecastETS(FutureCovariatesLocalForecastingModel): +class StatsForecastAutoETS(FutureCovariatesLocalForecastingModel): def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs): """ETS based on `Statsforecasts package `_. @@ -76,9 +76,9 @@ def __init__(self, *ets_args, add_encoders: Optional[dict] = None, **ets_kwargs) Examples -------- >>> from darts.datasets import AirPassengersDataset - >>> from darts.models import StatsForecastETS + >>> from darts.models import StatsForecastAutoETS >>> series = AirPassengersDataset().load() - >>> model = StatsForecastETS(season_length=12, model="AZZ") + >>> model = StatsForecastAutoETS(season_length=12, model="AZZ") >>> model.fit(series[:-36]) >>> pred = model.predict(36) """ diff --git a/darts/tests/models/forecasting/test_local_forecasting_models.py b/darts/tests/models/forecasting/test_local_forecasting_models.py index 606679c880..04fc2ce771 100644 --- a/darts/tests/models/forecasting/test_local_forecasting_models.py +++ b/darts/tests/models/forecasting/test_local_forecasting_models.py @@ -32,8 +32,8 @@ RegressionModel, StatsForecastAutoARIMA, StatsForecastAutoCES, + StatsForecastAutoETS, StatsForecastAutoTheta, - StatsForecastETS, Theta, ) from darts.models.forecasting.forecasting_model import ( @@ -55,7 +55,7 @@ (StatsForecastAutoARIMA(season_length=12), 4.6), (StatsForecastAutoTheta(season_length=12), 5.5), (StatsForecastAutoCES(season_length=12, model="Z"), 7.3), - (StatsForecastETS(season_length=12, model="AAZ"), 4.1), + (StatsForecastAutoETS(season_length=12, model="AAZ"), 4.1), (Croston(version="classic"), 23), (Croston(version="tsb", alpha_d=0.1, alpha_p=0.1), 23), (Theta(), 11), @@ -89,7 +89,7 @@ dual_models = [ ARIMA(), StatsForecastAutoARIMA(season_length=12), - StatsForecastETS(season_length=12), + StatsForecastAutoETS(season_length=12), Prophet(), AutoARIMA(), ] diff --git a/darts/tests/models/forecasting/test_sf_ets.py b/darts/tests/models/forecasting/test_sf_auto_ets.py similarity index 90% rename from darts/tests/models/forecasting/test_sf_ets.py rename to darts/tests/models/forecasting/test_sf_auto_ets.py index a8c9535661..00927b9977 100644 --- a/darts/tests/models/forecasting/test_sf_ets.py +++ b/darts/tests/models/forecasting/test_sf_auto_ets.py @@ -4,11 +4,11 @@ from darts import TimeSeries from darts.datasets import AirPassengersDataset from darts.metrics import mae -from darts.models import LinearRegressionModel, StatsForecastETS +from darts.models import LinearRegressionModel, StatsForecastAutoETS from darts.tests.base_test_class import DartsBaseTestClass -class StatsForecastETSTestCase(DartsBaseTestClass): +class StatsForecastAutoETSTestCase(DartsBaseTestClass): # real timeseries for functionality tests ts_passengers = AirPassengersDataset().load() ts_pass_train, ts_pass_val = ts_passengers.split_after(pd.Timestamp("19570101")) @@ -22,7 +22,7 @@ class StatsForecastETSTestCase(DartsBaseTestClass): ts_trend_train, ts_trend_val = ts_trend.split_after(pd.Timestamp("19570101")) def test_fit_on_residuals(self): - model = StatsForecastETS(season_length=12, model="ZZZ") + model = StatsForecastAutoETS(season_length=12, model="ZZZ") # test if we are indeed fitting the AutoETS on the residuals of the linear regression model.fit(series=self.ts_pass_train, future_covariates=self.ts_trend_train) @@ -48,7 +48,7 @@ def test_fit_on_residuals(self): self.assertTrue(current_mae < 9) def test_fit_a_linreg(self): - model = StatsForecastETS(season_length=12, model="ZZZ") + model = StatsForecastAutoETS(season_length=12, model="ZZZ") model.fit(series=self.ts_pass_train, future_covariates=self.ts_trend_train) # check if linear regression was fit