From c3ad7ce3813b6f4dbe016152cbb8e29b3fc6b8d7 Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Tue, 13 Jun 2023 16:59:01 +0200 Subject: [PATCH 01/15] Fix #1049 - add prior_scale and mode arguments to prophet model's add_seasonality --- darts/models/forecasting/prophet_model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index 60890aacf3..6f26b44547 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -186,6 +186,8 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non name=seasonality_name, period=attributes["seasonal_periods"] * interval_length, fourier_order=attributes["fourier_order"], + prior_scale=attributes["prior_scale"], + mode=attributes["mode"], ) # add covariates From a6d53f08c5661c760b3be37c9bee9329f9e107f6 Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Wed, 14 Jun 2023 19:24:57 +0200 Subject: [PATCH 02/15] Add option to treat seasonality as conditional --- darts/models/forecasting/prophet_model.py | 37 ++++++++++++++++++- .../tests/models/forecasting/test_prophet.py | 5 ++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index 6f26b44547..fc1cca76ce 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -5,6 +5,7 @@ import logging import re +import types from typing import Callable, List, Optional, Sequence, Union import numpy as np @@ -182,12 +183,19 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non # add user defined seasonalities (from model creation and/or pre-fit self.add_seasonalities()) interval_length = self._freq_to_days(series.freq_str) for seasonality_name, attributes in self._add_seasonalities.items(): + condition_name = None + if attributes["condition_func"] is not None: + fit_df[seasonality_name] = fit_df["ds"].apply( + attributes["condition_func"] + ) + condition_name = seasonality_name self.model.add_seasonality( name=seasonality_name, period=attributes["seasonal_periods"] * interval_length, fourier_order=attributes["fourier_order"], prior_scale=attributes["prior_scale"], mode=attributes["mode"], + condition_name=condition_name, ) # add covariates @@ -226,6 +234,12 @@ def _predict( predict_df = self._generate_predict_df(n=n, future_covariates=future_covariates) + for seasonality_name, attributes in self._add_seasonalities.items(): + if attributes["condition_func"] is not None: + predict_df[seasonality_name] = predict_df["ds"].apply( + attributes["condition_func"] + ) + if num_samples == 1: forecast = self.model.predict(predict_df, vectorized=True)["yhat"].values else: @@ -320,13 +334,20 @@ def add_seasonality( fourier_order: int, prior_scale: Optional[float] = None, mode: Optional[str] = None, + condition_func: Optional[types.FunctionType] = None, ) -> None: """Adds a custom seasonality to the model that repeats after every n `seasonal_periods` timesteps. An example for `seasonal_periods`: If you have hourly data (frequency='H') and your seasonal cycle repeats after 48 hours -> `seasonal_periods=48`. - Apart from `seasonal_periods`, this is very similar to how you would call Facebook Prophet's - `add_seasonality()` method. For information about the parameters see: + Apart from `seasonal_periods` and `conditional_name`, this is very similar to how you would call + Facebook Prophet's `add_seasonality()` method. In the case of a conditional seasonality, + the condition_name is set to be the seasonality name and the condition is supplied in the form of a function + that operates on the time index and returns a boolean mask indicating the timesteps + to include in the seasonality component. + For more details on conditional seasonalities see: + https://facebook.github.io/prophet/docs/seasonality,_holiday_effects,_and_regressors.html#seasonalities-that-depend-on-other-factors + For information about the parameters see: `The Prophet source code `_. Parameters @@ -341,6 +362,10 @@ def add_seasonality( optionally, a prior scale for this component mode optionally, 'additive' or 'multiplicative' + condition_func + optionally, a function that takes the time index and returns a boolean mask indicating the timesteps + to include in the seasonality component. + The masking function will be applied as follows: df[name] = df["ds"].apply(condition_func) """ function_call = { "name": name, @@ -348,6 +373,7 @@ def add_seasonality( "fourier_order": fourier_order, "prior_scale": prior_scale, "mode": mode, + "condition_func": condition_func, } self._store_add_seasonality_call(seasonality_call=function_call) @@ -379,12 +405,18 @@ def _store_add_seasonality_call( "fourier_order": {"default": None, "dtype": int}, "prior_scale": {"default": None, "dtype": float}, "mode": {"default": None, "dtype": str}, + "condition_func": {"default": None, "dtype": types.FunctionType}, } seasonality_default = { kw: seasonality_properties[kw]["default"] for kw in seasonality_properties } mandatory_keywords = ["name", "seasonal_periods", "fourier_order"] + if ( + "condition_func" in seasonality_call.keys() + and seasonality_call["condition_func"] is not None + ): + mandatory_keywords.append("condition_func") add_seasonality_call = dict(seasonality_default, **seasonality_call) @@ -428,6 +460,7 @@ def _store_add_seasonality_call( f'of type {[seasonality_properties[kw]["dtype"] for kw in invalid_types]}.', logger, ) + self._add_seasonalities[seasonality_name] = add_seasonality_call @staticmethod diff --git a/darts/tests/models/forecasting/test_prophet.py b/darts/tests/models/forecasting/test_prophet.py index fc363654a9..5913d06462 100644 --- a/darts/tests/models/forecasting/test_prophet.py +++ b/darts/tests/models/forecasting/test_prophet.py @@ -25,7 +25,10 @@ def test_add_seasonality_calls(self): "seasonal_periods": 24, "fourier_order": 1, } - kwargs_all = dict(kwargs_mandatory, **{"prior_scale": 1.0, "mode": "additive"}) + kwargs_all = dict( + kwargs_mandatory, + **{"prior_scale": 1.0, "mode": "additive", "condition_func": lambda x: x} + ) model1 = Prophet(add_seasonalities=kwargs_all) model2 = Prophet() model2.add_seasonality(**kwargs_all) From 5713aba4fabd8c259e74cd412e39db9736546bb4 Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Tue, 4 Jul 2023 14:09:37 +0200 Subject: [PATCH 03/15] Add seasonality conditions with a condition_name and future_covariates --- darts/models/forecasting/prophet_model.py | 86 ++++++++++++++--------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index fc1cca76ce..9a7bd2d8e1 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -5,7 +5,6 @@ import logging import re -import types from typing import Callable, List, Optional, Sequence, Union import numpy as np @@ -182,13 +181,27 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non # add user defined seasonalities (from model creation and/or pre-fit self.add_seasonalities()) interval_length = self._freq_to_days(series.freq_str) + conditional_seasonality_covariates = [] + if future_covariates is not None: + future_covariates_columns = future_covariates.columns + else: + future_covariates_columns = [] + for seasonality_name, attributes in self._add_seasonalities.items(): - condition_name = None - if attributes["condition_func"] is not None: - fit_df[seasonality_name] = fit_df["ds"].apply( - attributes["condition_func"] - ) - condition_name = seasonality_name + condition_name = attributes["condition_name"] + if condition_name is not None: + if condition_name in future_covariates_columns: + conditional_seasonality_covariates.append( + attributes["condition_name"] + ) + else: + raise_if( + True, + f"Condition name '{attributes['condition_name']}' is required by the custom " + f"seasonality '{seasonality_name}', but either it is not found in future_covariates " + f"or future_covariates is None.", + ) + self.model.add_seasonality( name=seasonality_name, period=attributes["seasonal_periods"] * interval_length, @@ -198,7 +211,7 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non condition_name=condition_name, ) - # add covariates + # add covariates as additional regressors if future_covariates is not None: fit_df = fit_df.merge( future_covariates.pd_dataframe(), @@ -206,8 +219,9 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non right_index=True, how="left", ) - for covariate in future_covariates.columns: - self.model.add_regressor(covariate) + for covariate in future_covariates_columns: + if covariate not in conditional_seasonality_covariates: + self.model.add_regressor(covariate) # add built-in country holidays if self.country_holidays is not None: @@ -230,16 +244,26 @@ def _predict( verbose: bool = False, ) -> TimeSeries: + for seasonality_name, attributes in self._add_seasonalities.items(): + if attributes["condition_name"] is not None: + raise_if( + future_covariates is None, + f"Condition name '{attributes['condition_name']}' is required by " + f"the custom seasonality '{seasonality_name}', but future_covariates is None. In addition, " + f"the model should be re-trained with future_covariates.", + logger, + ) + raise_if( + attributes["condition_name"] not in future_covariates.columns, + f"Condition name '{attributes['condition_name']}' is required by " + f"the custom seasonality '{seasonality_name}', but it is not found in future_covariates.", + logger, + ) + super()._predict(n, future_covariates, num_samples) predict_df = self._generate_predict_df(n=n, future_covariates=future_covariates) - for seasonality_name, attributes in self._add_seasonalities.items(): - if attributes["condition_func"] is not None: - predict_df[seasonality_name] = predict_df["ds"].apply( - attributes["condition_func"] - ) - if num_samples == 1: forecast = self.model.predict(predict_df, vectorized=True)["yhat"].values else: @@ -334,21 +358,20 @@ def add_seasonality( fourier_order: int, prior_scale: Optional[float] = None, mode: Optional[str] = None, - condition_func: Optional[types.FunctionType] = None, + condition_name: Optional[str] = None, ) -> None: """Adds a custom seasonality to the model that repeats after every n `seasonal_periods` timesteps. An example for `seasonal_periods`: If you have hourly data (frequency='H') and your seasonal cycle repeats after 48 hours -> `seasonal_periods=48`. - Apart from `seasonal_periods` and `conditional_name`, this is very similar to how you would call - Facebook Prophet's `add_seasonality()` method. In the case of a conditional seasonality, - the condition_name is set to be the seasonality name and the condition is supplied in the form of a function - that operates on the time index and returns a boolean mask indicating the timesteps - to include in the seasonality component. + Apart from `seasonal_periods`, this is very similar to how you would call + Facebook Prophet's `add_seasonality()` method. To add conditional seasonalities, + a condition_name has to be given here and a future_covariates TimeSeries + with a component (column) named condition_name is expected to be passed to the 'fit()' and 'predict()' methods. For more details on conditional seasonalities see: https://facebook.github.io/prophet/docs/seasonality,_holiday_effects,_and_regressors.html#seasonalities-that-depend-on-other-factors For information about the parameters see: - `The Prophet source code `_. + `The Prophet source code `. Parameters ---------- @@ -362,10 +385,10 @@ def add_seasonality( optionally, a prior scale for this component mode optionally, 'additive' or 'multiplicative' - condition_func - optionally, a function that takes the time index and returns a boolean mask indicating the timesteps - to include in the seasonality component. - The masking function will be applied as follows: df[name] = df["ds"].apply(condition_func) + condition_name + optionally, the name of the condition on which the seasonality depends. If not None, a future_covariates + TimeSeries with a component (column) named condition_name is expected to be passed to the 'fit()' and + 'predict()' methods. """ function_call = { "name": name, @@ -373,7 +396,7 @@ def add_seasonality( "fourier_order": fourier_order, "prior_scale": prior_scale, "mode": mode, - "condition_func": condition_func, + "condition_name": condition_name, } self._store_add_seasonality_call(seasonality_call=function_call) @@ -405,18 +428,13 @@ def _store_add_seasonality_call( "fourier_order": {"default": None, "dtype": int}, "prior_scale": {"default": None, "dtype": float}, "mode": {"default": None, "dtype": str}, - "condition_func": {"default": None, "dtype": types.FunctionType}, + "condition_name": {"default": None, "dtype": str}, } seasonality_default = { kw: seasonality_properties[kw]["default"] for kw in seasonality_properties } mandatory_keywords = ["name", "seasonal_periods", "fourier_order"] - if ( - "condition_func" in seasonality_call.keys() - and seasonality_call["condition_func"] is not None - ): - mandatory_keywords.append("condition_func") add_seasonality_call = dict(seasonality_default, **seasonality_call) From cf55740efb8cc14457453f771b41d89554b7ae73 Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Tue, 4 Jul 2023 14:12:29 +0200 Subject: [PATCH 04/15] Add test for custom conditional seasonality --- .../tests/models/forecasting/test_prophet.py | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/darts/tests/models/forecasting/test_prophet.py b/darts/tests/models/forecasting/test_prophet.py index 5913d06462..70814bfbcc 100644 --- a/darts/tests/models/forecasting/test_prophet.py +++ b/darts/tests/models/forecasting/test_prophet.py @@ -27,7 +27,11 @@ def test_add_seasonality_calls(self): } kwargs_all = dict( kwargs_mandatory, - **{"prior_scale": 1.0, "mode": "additive", "condition_func": lambda x: x} + **{ + "prior_scale": 1.0, + "mode": "additive", + "condition_name": "custom_condition", + } ) model1 = Prophet(add_seasonalities=kwargs_all) model2 = Prophet() @@ -237,3 +241,39 @@ def helper_test_prophet_model(self, period, freq, compare_all_models=False): for pred in compare_preds: for val_i, pred_i in zip(val.univariate_values(), pred.univariate_values()): self.assertAlmostEqual(val_i, pred_i, delta=0.1) + + def test_conditional_seasonality(self): + """ + Test that conditional seasonality is correctly incorporated by the model + """ + df = pd.DataFrame() + df["ds"] = pd.date_range(start="2022-01-02", periods=395) + df["y"] = [i + 10 * (i % 7 == 0) for i in range(395)] + df["is_sunday"] = df["ds"].apply(lambda x: int(x.weekday() == 6)) + + ts = TimeSeries.from_dataframe( + df[:-30], time_col="ds", value_cols="y", freq="D" + ) + future_covariates = TimeSeries.from_dataframe( + df, time_col="ds", value_cols=["is_sunday"], freq="D" + ) + expected_result = TimeSeries.from_dataframe( + df[-30:], time_col="ds", value_cols="y", freq="D" + ) + + model = Prophet(seasonality_mode="additive") + model.add_seasonality( + name="weekly_sun", + seasonal_periods=7, + fourier_order=2, + condition_name="is_sunday", + ) + + model.fit(ts, future_covariates=future_covariates) + + forecast = model.predict(30, future_covariates=future_covariates) + + for val_i, pred_i in zip( + expected_result.univariate_values(), forecast.univariate_values() + ): + self.assertAlmostEqual(val_i, pred_i, delta=0.1) From 3d5fc4a506cf9e160d16c47fd741c11b18aa2b3a Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Tue, 4 Jul 2023 14:13:26 +0200 Subject: [PATCH 05/15] Add entry for pr #1829 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03489ff964..544672408e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co - Fixed a bug when loading the weights of a `TorchForecastingModel` trained with encoders or a Likelihood. [#1744](https://github.com/unit8co/darts/pull/1744) by [Antoine Madrona](https://github.com/madtoinou). - Fixed a bug when using selected `target_components` with `ShapExplainer. [#1803](https://github.com/unit8co/darts/pull/#1803) by [Dennis Bader](https://github.com/dennisbader). - Fixed `TimeSeries.__getitem__()` for series with a RangeIndex with start != 0 and freq != 1. [#1868](https://github.com/unit8co/darts/pull/#1868) by [Dennis Bader](https://github.com/dennisbader). +- Fixed an issue with `prophet_model.Prophet.add_seasonality()` to allow proper use of all passed parameters. [#1829](https://github.com/unit8co/darts/pull/#1829) by [Idan Shilon](https://github.com/id5h). **Removed** - Removed support for Python 3.7 [#1864](https://github.com/unit8co/darts/pull/#1864) by [Dennis Bader](https://github.com/dennisbader). From 801e80a8cfbd37c0a9f30bce88d68a4f3eca0fb6 Mon Sep 17 00:00:00 2001 From: id5h Date: Wed, 5 Jul 2023 14:43:08 +0200 Subject: [PATCH 06/15] Update darts/models/forecasting/prophet_model.py Co-authored-by: Dennis Bader --- darts/models/forecasting/prophet_model.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index a0be8993c4..0dc21747f9 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -368,14 +368,16 @@ def add_seasonality( An example for `seasonal_periods`: If you have hourly data (frequency='H') and your seasonal cycle repeats after 48 hours -> `seasonal_periods=48`. - Apart from `seasonal_periods`, this is very similar to how you would call - Facebook Prophet's `add_seasonality()` method. To add conditional seasonalities, - a condition_name has to be given here and a future_covariates TimeSeries - with a component (column) named condition_name is expected to be passed to the 'fit()' and 'predict()' methods. - For more details on conditional seasonalities see: - https://facebook.github.io/prophet/docs/seasonality,_holiday_effects,_and_regressors.html#seasonalities-that-depend-on-other-factors + Apart from `seasonal_periods`, this is very similar to how you would call Facebook Prophet's + `add_seasonality()` method. + + To add conditional seasonalities, provide `condition_name` here, and add a boolean (binary) component/column + named `condition_name` to the `future_covariates` series passed to `fit()` and `predict()`. + For information about the parameters see: `The Prophet source code `. + For more details on conditional seasonalities see: + https://facebook.github.io/prophet/docs/seasonality,_holiday_effects,_and_regressors.html#seasonalities-that-depend-on-other-factors Parameters ---------- From 5b0321c04927bcc580d570ead2ef0b55f4332571 Mon Sep 17 00:00:00 2001 From: id5h Date: Wed, 5 Jul 2023 14:43:43 +0200 Subject: [PATCH 07/15] Update darts/models/forecasting/prophet_model.py Co-authored-by: Dennis Bader --- darts/models/forecasting/prophet_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index 0dc21747f9..e8721edb4f 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -392,9 +392,9 @@ def add_seasonality( mode optionally, 'additive' or 'multiplicative' condition_name - optionally, the name of the condition on which the seasonality depends. If not None, a future_covariates - TimeSeries with a component (column) named condition_name is expected to be passed to the 'fit()' and - 'predict()' methods. + optionally, the name of the condition on which the seasonality depends. If not `None`, expects a + `future_covariates` time series with a component/column named `condition_name` to be passed to `fit()` + and `predict()`. """ function_call = { "name": name, From 50a70d411d98d9d412fea10ebb04e8a745cdfe58 Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Wed, 5 Jul 2023 23:31:30 +0200 Subject: [PATCH 08/15] Validate seasonality considitions through a private method when calling fit() and predict() --- darts/models/forecasting/prophet_model.py | 114 ++++++++++++++-------- 1 file changed, 75 insertions(+), 39 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index e8721edb4f..00dd0361a8 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -181,34 +181,17 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non # add user defined seasonalities (from model creation and/or pre-fit self.add_seasonalities()) interval_length = self._freq_to_days(series.freq_str) - conditional_seasonality_covariates = [] - if future_covariates is not None: - future_covariates_columns = future_covariates.columns - else: - future_covariates_columns = [] - + conditional_seasonality_covariates = self._check_seasonality_conditions( + future_covariates=future_covariates + ) for seasonality_name, attributes in self._add_seasonalities.items(): - condition_name = attributes["condition_name"] - if condition_name is not None: - if condition_name in future_covariates_columns: - conditional_seasonality_covariates.append( - attributes["condition_name"] - ) - else: - raise_if( - True, - f"Condition name '{attributes['condition_name']}' is required by the custom " - f"seasonality '{seasonality_name}', but either it is not found in future_covariates " - f"or future_covariates is None.", - ) - self.model.add_seasonality( name=seasonality_name, period=attributes["seasonal_periods"] * interval_length, fourier_order=attributes["fourier_order"], prior_scale=attributes["prior_scale"], mode=attributes["mode"], - condition_name=condition_name, + condition_name=attributes["condition_name"], ) # add covariates as additional regressors @@ -219,7 +202,7 @@ def _fit(self, series: TimeSeries, future_covariates: Optional[TimeSeries] = Non right_index=True, how="left", ) - for covariate in future_covariates_columns: + for covariate in future_covariates.columns: if covariate not in conditional_seasonality_covariates: self.model.add_regressor(covariate) @@ -244,21 +227,7 @@ def _predict( verbose: bool = False, ) -> TimeSeries: - for seasonality_name, attributes in self._add_seasonalities.items(): - if attributes["condition_name"] is not None: - raise_if( - future_covariates is None, - f"Condition name '{attributes['condition_name']}' is required by " - f"the custom seasonality '{seasonality_name}', but future_covariates is None. In addition, " - f"the model should be re-trained with future_covariates.", - logger, - ) - raise_if( - attributes["condition_name"] not in future_covariates.columns, - f"Condition name '{attributes['condition_name']}' is required by " - f"the custom seasonality '{seasonality_name}', but it is not found in future_covariates.", - logger, - ) + _ = self._check_seasonality_conditions(future_covariates=future_covariates) super()._predict(n, future_covariates, num_samples) @@ -307,6 +276,73 @@ def _generate_predict_df( ) return predict_df + def _check_seasonality_conditions( + self, future_covariates: Optional[TimeSeries] = None + ) -> List[str]: + """ + Checks if the conditions for custom conditional seasonalities are met. Each custom seasonality that has a + `condition_name` other than None is checked. If the `condition_name` is not a column in the `future_covariates` + or if the values in the column are not all True or False, an error is raised. + Returns a list of the `condition_name`s of the conditional seasonalities that have been checked. + + Parameters + ---------- + future_covariates + optionally, a TimeSeries containing the future covariates and including the columns that are used as + conditions for the conditional seasonalities when necessary + + Raises + ------ + ValueError + if a seasonality has a `condition_name` and a column named `condition_name` is missing in + the `future_covariates` + + if a seasonality has a `condition_name` and the values in the corresponding column in `future_covariates` + are not binary values (True or False, 1 or 0) + """ + + conditional_seasonality_covariates = [] + invalid_conditional_seasonalities = [] + if future_covariates is not None: + future_covariates_columns = future_covariates.columns + else: + future_covariates_columns = [] + + for seasonality_name, attributes in self._add_seasonalities.items(): + condition_name = attributes["condition_name"] + if condition_name is not None: + if condition_name not in future_covariates_columns: + invalid_conditional_seasonalities.append( + (seasonality_name, condition_name, "column missing") + ) + continue + if ( + not future_covariates[condition_name] + .pd_series() + .isin([True, False]) + .all() + ): + invalid_conditional_seasonalities.append( + (seasonality_name, condition_name, "invalid values") + ) + continue + conditional_seasonality_covariates.append(condition_name) + + formatted_issues_str = ", ".join( + f"'{name}' (condition_name: '{cond}'; issue: {reason})" + for name, cond, reason in invalid_conditional_seasonalities + ) + raise_if( + len(invalid_conditional_seasonalities) > 0, + f"The following seasonalities have invalid conditions: " + f"{formatted_issues_str}. " + f"Each conditional seasonality must be accompanied by a binary component/column in the future_covariates " + f"with the same name as the condition_name. These components must only contain " + f"True or False values (or 1 or 0).", + logger, + ) + return conditional_seasonality_covariates + @property def supports_multivariate(self) -> bool: return False @@ -392,8 +428,8 @@ def add_seasonality( mode optionally, 'additive' or 'multiplicative' condition_name - optionally, the name of the condition on which the seasonality depends. If not `None`, expects a - `future_covariates` time series with a component/column named `condition_name` to be passed to `fit()` + optionally, the name of the condition on which the seasonality depends. If not `None`, expects a + `future_covariates` time series with a component/column named `condition_name` to be passed to `fit()` and `predict()`. """ function_call = { From 0fe11d2cf8910e58f1410488aefb18d4ad21a7bc Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Wed, 5 Jul 2023 23:33:01 +0200 Subject: [PATCH 09/15] Reduce predict horizon to 7. Add tests for missing and invalid conditions --- .../tests/models/forecasting/test_prophet.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/darts/tests/models/forecasting/test_prophet.py b/darts/tests/models/forecasting/test_prophet.py index 70814bfbcc..0486665fb2 100644 --- a/darts/tests/models/forecasting/test_prophet.py +++ b/darts/tests/models/forecasting/test_prophet.py @@ -2,6 +2,7 @@ import numpy as np import pandas as pd +import pytest from darts import TimeSeries from darts.logging import get_logger @@ -246,19 +247,21 @@ def test_conditional_seasonality(self): """ Test that conditional seasonality is correctly incorporated by the model """ + duration = 395 + horizon = 7 df = pd.DataFrame() df["ds"] = pd.date_range(start="2022-01-02", periods=395) - df["y"] = [i + 10 * (i % 7 == 0) for i in range(395)] + df["y"] = [i + 10 * (i % 7 == 0) for i in range(duration)] df["is_sunday"] = df["ds"].apply(lambda x: int(x.weekday() == 6)) ts = TimeSeries.from_dataframe( - df[:-30], time_col="ds", value_cols="y", freq="D" + df[:-horizon], time_col="ds", value_cols="y", freq="D" ) future_covariates = TimeSeries.from_dataframe( df, time_col="ds", value_cols=["is_sunday"], freq="D" ) expected_result = TimeSeries.from_dataframe( - df[-30:], time_col="ds", value_cols="y", freq="D" + df[-horizon:], time_col="ds", value_cols="y", freq="D" ) model = Prophet(seasonality_mode="additive") @@ -271,9 +274,22 @@ def test_conditional_seasonality(self): model.fit(ts, future_covariates=future_covariates) - forecast = model.predict(30, future_covariates=future_covariates) + forecast = model.predict(horizon, future_covariates=future_covariates) for val_i, pred_i in zip( expected_result.univariate_values(), forecast.univariate_values() ): self.assertAlmostEqual(val_i, pred_i, delta=0.1) + + invalid_future_covariates = future_covariates.with_values( + np.reshape(np.random.randint(0, 3, duration), (-1, 1, 1)).astype("float") + ) + + with pytest.raises(ValueError): + model.fit(ts, future_covariates=invalid_future_covariates) + + with pytest.raises(ValueError): + model.fit( + ts, + future_covariates=invalid_future_covariates.drop_columns("is_sunday"), + ) From a96a15f1a29346683ef774e7ffa241eab4dd9ccf Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Wed, 5 Jul 2023 23:33:37 +0200 Subject: [PATCH 10/15] Move entry to models improvements section --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc192efac..f389f76b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ but cannot always guarantee backwards compatibility. Changes that may **break co - Added support for `PathLike` to the `save()` and `load()` functions of all non-deep learning based models. [#1754](https://github.com/unit8co/darts/pull/1754) by [Simon Sudrich](https://github.com/sudrich). - Improved efficiency of `historical_forecasts()` and `backtest()` for all models giving significant process time reduction for larger number of predict iterations and series. [#1801](https://github.com/unit8co/darts/pull/1801) by [Dennis Bader](https://github.com/dennisbader). - Added model property `ForecastingModel.supports_multivariate` to indicate whether the model supports multivariate forecasting. [#1848](https://github.com/unit8co/darts/pull/1848) by [Felix Divo](https://github.com/felixdivo). + - `Prophet` now supports conditional seasonalities, and properly handles all parameters passed to `Prophet.add_seasonality()` and model creation parameter `add_seasonalities` [#1829](https://github.com/unit8co/darts/pull/#1829) by [Idan Shilon](https://github.com/id5h). + - Improvements to `EnsembleModel`: - Model creation parameter `forecasting_models` now supports a mix of `LocalForecastingModel` and `GlobalForecastingModel` (single `TimeSeries` training/inference only, due to the local models). [#1745](https://github.com/unit8co/darts/pull/1745) by [Antoine Madrona](https://github.com/madtoinou). - Future and past covariates can now be used even if `forecasting_models` have different covariates support. The covariates passed to `fit()`/`predict()` are used only by models that support it. [#1745](https://github.com/unit8co/darts/pull/1745) by [Antoine Madrona](https://github.com/madtoinou). @@ -29,7 +31,6 @@ but cannot always guarantee backwards compatibility. Changes that may **break co - Fixed a bug when loading the weights of a `TorchForecastingModel` trained with encoders or a Likelihood. [#1744](https://github.com/unit8co/darts/pull/1744) by [Antoine Madrona](https://github.com/madtoinou). - Fixed a bug when using selected `target_components` with `ShapExplainer. [#1803](https://github.com/unit8co/darts/pull/#1803) by [Dennis Bader](https://github.com/dennisbader). - Fixed `TimeSeries.__getitem__()` for series with a RangeIndex with start != 0 and freq != 1. [#1868](https://github.com/unit8co/darts/pull/#1868) by [Dennis Bader](https://github.com/dennisbader). -- Fixed an issue with `prophet_model.Prophet.add_seasonality()` to allow proper use of all passed parameters. [#1829](https://github.com/unit8co/darts/pull/#1829) by [Idan Shilon](https://github.com/id5h). **Removed** - Removed support for Python 3.7 [#1864](https://github.com/unit8co/darts/pull/#1864) by [Dennis Bader](https://github.com/dennisbader). From e9418a6c07a35f81341bcb8c9ce8d78f93e64465 Mon Sep 17 00:00:00 2001 From: id5h Date: Fri, 7 Jul 2023 21:30:10 +0200 Subject: [PATCH 11/15] Update err msg in _check_seasonality_conditions Co-authored-by: Dennis Bader --- darts/models/forecasting/prophet_model.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index 00dd0361a8..a951171634 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -332,15 +332,19 @@ def _check_seasonality_conditions( f"'{name}' (condition_name: '{cond}'; issue: {reason})" for name, cond, reason in invalid_conditional_seasonalities ) - raise_if( - len(invalid_conditional_seasonalities) > 0, - f"The following seasonalities have invalid conditions: " - f"{formatted_issues_str}. " - f"Each conditional seasonality must be accompanied by a binary component/column in the future_covariates " - f"with the same name as the condition_name. These components must only contain " - f"True or False values (or 1 or 0).", - logger, - ) + if len(invalid_conditional_seasonalities) > 0: + formatted_issues_str = ", ".join( + f"'{name}' (condition_name: '{cond}'; issue: {reason})" + for name, cond, reason in invalid_conditional_seasonalities + ) + raise_log( + ValueError( + f"The following seasonalities have invalid conditions: {formatted_issues_str}. " + f"Each conditional seasonality must be accompanied by a binary component/column in the " + f"`future_covariates` with the same name as the `condition_name`" + ), + logger + ) return conditional_seasonality_covariates @property From 9057345aa1660ee2fa7517dbb16150d2e5b11f85 Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Fri, 7 Jul 2023 21:48:14 +0200 Subject: [PATCH 12/15] Import raise_log. Initialize formatted str when necessary. --- darts/models/forecasting/prophet_model.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index a951171634..83b5ef1aef 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -11,7 +11,7 @@ import pandas as pd import prophet -from darts.logging import execute_and_suppress_output, get_logger, raise_if +from darts.logging import execute_and_suppress_output, get_logger, raise_if, raise_log from darts.models.forecasting.forecasting_model import ( FutureCovariatesLocalForecastingModel, ) @@ -328,10 +328,6 @@ def _check_seasonality_conditions( continue conditional_seasonality_covariates.append(condition_name) - formatted_issues_str = ", ".join( - f"'{name}' (condition_name: '{cond}'; issue: {reason})" - for name, cond, reason in invalid_conditional_seasonalities - ) if len(invalid_conditional_seasonalities) > 0: formatted_issues_str = ", ".join( f"'{name}' (condition_name: '{cond}'; issue: {reason})" @@ -343,7 +339,7 @@ def _check_seasonality_conditions( f"Each conditional seasonality must be accompanied by a binary component/column in the " f"`future_covariates` with the same name as the `condition_name`" ), - logger + logger, ) return conditional_seasonality_covariates From 157f206d6cb85f9206c00812d3d147dfc2d8f154 Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Fri, 7 Jul 2023 21:55:30 +0200 Subject: [PATCH 13/15] Accept float seasonalities as well. Update test --- darts/models/forecasting/prophet_model.py | 4 ++-- darts/tests/models/forecasting/test_prophet.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index 83b5ef1aef..2e0f6bb278 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -394,7 +394,7 @@ def predict_raw( def add_seasonality( self, name: str, - seasonal_periods: int, + seasonal_periods: Union[int, float], fourier_order: int, prior_scale: Optional[float] = None, mode: Optional[str] = None, @@ -466,7 +466,7 @@ def _store_add_seasonality_call( seasonality_properties = { "name": {"default": None, "dtype": str}, - "seasonal_periods": {"default": None, "dtype": int}, + "seasonal_periods": {"default": None, "dtype": Union[int, float]}, "fourier_order": {"default": None, "dtype": int}, "prior_scale": {"default": None, "dtype": float}, "mode": {"default": None, "dtype": str}, diff --git a/darts/tests/models/forecasting/test_prophet.py b/darts/tests/models/forecasting/test_prophet.py index 0486665fb2..af8bffca41 100644 --- a/darts/tests/models/forecasting/test_prophet.py +++ b/darts/tests/models/forecasting/test_prophet.py @@ -23,7 +23,7 @@ def test_add_seasonality_calls(self): } kwargs_mandatory2 = { "name": "custom2", - "seasonal_periods": 24, + "seasonal_periods": 24.9, "fourier_order": 1, } kwargs_all = dict( From 7c03f75c81e586532ddfb9ca33f0afc6d76477fb Mon Sep 17 00:00:00 2001 From: Idan Shilon Date: Mon, 10 Jul 2023 13:35:44 +0200 Subject: [PATCH 14/15] Fix dtype of seasonal_periods. Update docstrings. --- darts/models/forecasting/prophet_model.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index 2e0f6bb278..53338e0a70 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -53,7 +53,7 @@ def __init__( dict({ 'name': str # (name of the seasonality component), - 'seasonal_periods': int # (nr of steps composing a season), + 'seasonal_periods': Union[int, float] # (nr of steps composing a season), 'fourier_order': int # (number of Fourier components to use), 'prior_scale': Optional[float] # (a prior scale for this component), 'mode': Optional[str] # ('additive' or 'multiplicative') @@ -61,7 +61,9 @@ def __init__( .. An example for `seasonal_periods`: If you have hourly data (frequency='H') and your seasonal cycle repeats - after 48 hours then set `seasonal_periods=48`. + after 48 hours then set `seasonal_periods=48`. Notice that this value will be multiplied by the inferred + number of days for the TimeSeries frequency (1 / 24 in this example) to be consistent with the + `add_seasonality()` method of Facebook Prophet, where the `period` parameter is specified in days. Apart from `seasonal_periods`, this is very similar to how you would call Facebook Prophet's `add_seasonality()` method. @@ -420,7 +422,9 @@ def add_seasonality( name name of the seasonality component seasonal_periods - number of timesteps after which the seasonal cycle repeats + number of timesteps after which the seasonal cycle repeats. This value will be multiplied by the inferred + number of days for the TimeSeries frequency (e.g. 365.25 for a yearly frequency) to be consistent with the + `add_seasonality()` method of Facebook Prophet. fourier_order number of Fourier components to use prior_scale @@ -466,7 +470,7 @@ def _store_add_seasonality_call( seasonality_properties = { "name": {"default": None, "dtype": str}, - "seasonal_periods": {"default": None, "dtype": Union[int, float]}, + "seasonal_periods": {"default": None, "dtype": (int, float)}, "fourier_order": {"default": None, "dtype": int}, "prior_scale": {"default": None, "dtype": float}, "mode": {"default": None, "dtype": str}, From 61abeea594870f48caeacfc5e0567a29dac46c36 Mon Sep 17 00:00:00 2001 From: dennisbader Date: Mon, 17 Jul 2023 15:08:35 +0200 Subject: [PATCH 15/15] update docstring --- darts/models/forecasting/prophet_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/darts/models/forecasting/prophet_model.py b/darts/models/forecasting/prophet_model.py index 53338e0a70..9ded2cba78 100644 --- a/darts/models/forecasting/prophet_model.py +++ b/darts/models/forecasting/prophet_model.py @@ -424,7 +424,8 @@ def add_seasonality( seasonal_periods number of timesteps after which the seasonal cycle repeats. This value will be multiplied by the inferred number of days for the TimeSeries frequency (e.g. 365.25 for a yearly frequency) to be consistent with the - `add_seasonality()` method of Facebook Prophet. + `add_seasonality()` method of Facebook Prophet. The inferred number of days can be obtained with + `model._freq_to_days(series.freq)`, where `model` is the `Prophet` model and `series` is the target series. fourier_order number of Fourier components to use prior_scale