Repository: sktime/sktime
Pull request: [BUG] Fix Prophet with logistic growth #1079
#### Reference Issues/PRs
Fixes #1079 and #2444
Copy and paste of #1868 

#### What does this implement/fix? Explain your changes.
Adds the possibility to define the required "cap" and "floor" parameters for Prophet when its growth is set to logistic.

#### Any other comments?
Suggest closing old relevant issues and PRs.

##### For all contributions
- [x] Tests pass locally and [private github-action](https://github.com/k1m190r/sktime/actions/runs/2280446935)
- [x] The PR title starts with [BUG] 



Problem statement:
[BUG] Prophet with logistic growth does not work
**Describe the bug**
It is not compatible with the case that the growth parameter of prophet is equal to ‘logistic‘

**To Reproduce**
```python

from sktime.datasets import load_airline
y = load_airline()
y = y.to_timestamp(freq="M")
y_train, y_test = temporal_train_test_split(y, test_size=36)

y_train_df = pd.DataFrame(y_train,index = y_train.index,columns=['y'])
y_train_df["cap"] = max(y_train_df["y"])
y_train_df['floor'] = 0

forecaster = Prophet(
    growth = 'logistic'
)

forecaster.fit(y_train_df)
```

**Expected behavior**
<!--
-->
The growth parameter compatible with prophet is equal to ‘logistic‘
**Additional context**
<!--
Add any other context about the problem here.
-->

**Versions**
<details>

<!--
Please run the following code snippet and paste the output here:
 -->
from sktime import show_versions; show_versions()

System:
    python: 3.8.3 (default, Jul  2 2020, 11:26:31)  [Clang 10.0.0 ]
executable: /Users/baixiaotiao/opt/anaconda3/bin/python
   machine: macOS-10.16-x86_64-i386-64bit

Python dependencies:
          pip: 20.1.1
   setuptools: 49.2.0.post20200714
      sklearn: 0.24.2
       sktime: 0.6.1
  statsmodels: 0.12.2
        numpy: 1.20.3
        scipy: 1.5.0
       Cython: 0.29.21
       pandas: 1.2.5
   matplotlib: 3.2.2
       joblib: 0.16.0
        numba: 0.50.1
     pmdarima: 1.8.2
      tsfresh: None


</details>

<!-- Thanks for contributing! -->

Hints from issue discussion:
Yes, I can confirm this, this is because setting the growth parameter to "logistic" requires passing a `pd.DataFrame` with additional columns "cap" and "floor". 

This is related to #980. 
Well, but certainly not caused by #980 (because it's not merged yet), but potentially solved by #980, right?
> Yes, I can confirm this, this is because setting the growth parameter to "logistic" requires passing a `pd.DataFrame` with additional columns "cap" and "floor".

What do you mean by 'I can confirm this'. Are you saying there should be a bug? Or are you saying that passing the correct dataframe would fix the bug?

I ask because in the example code, a dataframe with columns cap and floor has been passed.


@Lovkush-A I just meant that I can reproduce the error and that I agree with @muzibai that we're currently not covering that use case. 
Thanks for explaining Markus. I think this bug would be a useful way to compare the different input/output frameworks that are in discussion!

@fkiraly How would your framework deal with a class in which the inner-type depends on user-input?
>@fkiraly How would your framework deal with a class in which the inner-type depends on user-input?

Assuming that "user input" means behaviour controlled by hyper-parameters set in construction, the paradigm would be reading that hyper-parameter in `_fit` and dealing with any additional conversion or input logic there. `fit` would be set up to pass through the supported inner types and otherwise convert to them.

Generally, implementers can do whatever they want in `_fit` in addition to the "generic" conversions which only take over if nothing smarter is specified.
Abandoned attempt to fix this here, maybe a good starting point for future developers
https://github.com/alan-turing-institute/sktime/pull/1868
Report of closely related behaviour by @ohbtorres here:
https://github.com/alan-turing-institute/sktime/issues/2444

Commit message: Merge branch 'issue_1079' of https://github.com/k1m190r/sktime into issue_1079

Patch to review:
diff --git a/sktime/forecasting/base/adapters/_fbprophet.py b/sktime/forecasting/base/adapters/_fbprophet.py
index 35b35198cbe..f4140607e78 100644
--- a/sktime/forecasting/base/adapters/_fbprophet.py
+++ b/sktime/forecasting/base/adapters/_fbprophet.py
@@ -66,6 +66,18 @@ def _fit(self, y, X=None, fh=None):
             for col in X.columns:
                 self._forecaster.add_regressor(col)
 
+        # Add floor and bottom when growth is logistic
+        if self.growth == "logistic":
+
+            if self.growth_cap is None:
+                raise ValueError(
+                    "Since `growth` param is set to 'logistic', expecting `growth_cap`"
+                    " to be non `None`: a float or array like of floats."
+                )
+
+            df["cap"] = self.growth_cap
+            df["floor"] = self.growth_floor
+
         if self.verbose:
             self._forecaster.fit(df=df)
         else:
@@ -108,6 +120,10 @@ def _predict(self, fh=None, X=None):
             X = X.copy()
             df, X = _merge_X(df, X)
 
+        if self.growth == "logistic":
+            df["cap"] = self.growth_cap
+            df["floor"] = self.growth_floor
+
         out = self._forecaster.predict(df)
 
         out.set_index("ds", inplace=True)
@@ -233,7 +249,7 @@ def _merge_X(df, X):
     ----------
     fh : sktime.ForecastingHorizon
     X : pd.DataFrame
-        Exog data
+        Exogeneous data
     df : pd.DataFrame
         Contains a DatetimeIndex column "ds"
 
diff --git a/sktime/forecasting/fbprophet.py b/sktime/forecasting/fbprophet.py
index 81af7a68f0c..965d807b116 100644
--- a/sktime/forecasting/fbprophet.py
+++ b/sktime/forecasting/fbprophet.py
@@ -38,6 +38,13 @@ class Prophet(_ProphetAdapter):
     growth: str, default="linear"
         String 'linear' or 'logistic' to specify a linear or logistic
         trend.
+    growth_floor: float, array like, default=0
+        Growth saturation minimum value. If float floor is constant.
+        If array like - Variable floor (must be same length as y).
+    growth_cap: float, array like
+        Growth saturation maximum aka carrying capacity. If float capacity is constant.
+        If array like - Variable capacity (must be same length as y).
+        Must be specified if `growth="logistic"`.
     changepoints: list or None, default=None
         List of dates at which to include potential changepoints. If
         not specified, potential changepoints are selected automatically.
@@ -126,6 +133,8 @@ def __init__(
         add_country_holidays=None,
         # Args of fbprophet
         growth="linear",
+        growth_floor=0.0,
+        growth_cap=None,
         changepoints=None,
         n_changepoints=25,
         changepoint_range=0.8,
@@ -142,7 +151,7 @@ def __init__(
         uncertainty_samples=1000,
         stan_backend=None,
         verbose=0,
-        interval_width=0,
+        interval_width=0.80,
     ):
         _check_soft_dependencies("prophet", severity="error", object=self)
 
@@ -151,6 +160,8 @@ def __init__(
         self.add_country_holidays = add_country_holidays
 
         self.growth = growth
+        self.growth_floor = growth_floor
+        self.growth_cap = growth_cap
         self.changepoints = changepoints
         self.n_changepoints = n_changepoints
         self.changepoint_range = changepoint_range


Code review comments:
diff_hunk: @@ -142,7 +151,7 @@ def __init__(
         uncertainty_samples=1000,
         stan_backend=None,
         verbose=0,
-        interval_width=0,
+        interval_width=0.80,
path: sktime/forecasting/fbprophet.py
text: @author:
Synced this [default parameter ](https://github.com/facebook/prophet/blob/b7d5ec448177b7ac2abf25cc3259cb55e4c44069/python/prophet/forecaster.py#L93)value with Prophet.