Repository: sktime/sktime
Pull request: [ENH] add missing `fit` parameters to `statsmodels` Holt-Winters exponential smoothing interface
This PR adds parameters of `statsmodels.tsa.holtwinters` in `fit` as loopthrough to `sktime`'s `ExponentialSmoothing` interface.

So far, only the constructor parameters were accessible, not the parameters in `fit`.

Solves #1845.

Problem statement:
[ENH] Add option to set smoothing parameters for ExponentialSmoothing class based models
`smoothing_level`, `smoothing_trend`, and `smoothing_seasonal` should be added as parameters to `ExponentialSmoothing` which wraps `statsmodels` exponential smoothing.

In `statsmodels`, these parameters are passed to `fit`, but in `sktime`, they should be passed to `__init__` together with the other hyper-parameters.

Adding other parameters which are arguments to `fit` in `statsmodels` to `__init__` in `sktime` should also be considered.

---

**PROBLEM** 
I have been using the ExponentialSmoothing class recently. Can't seem to find a way to input/set the smoothing_level, smoothing_trend and the smoothing_seasonal parameters. This functionality is provided by statsmodels and imo should be offered by sktime too.  

**Solution**
Add a way to input smoothing_level, smoothing_trend, and smoothing_seasonal parameters for the ExponentialSmoothing models.

Hints from issue discussion:
Hm, could you kindly paste a reference to the parameters you mentioned?

I just looked in `statsmodels`, and here is the list of parameters I could find.
The list does not include `smoothing_level`, `smoothing_trend`, or `smoothing_seasonal`.

```
        endog,
        trend=None,
        damped_trend=False,
        seasonal=None,
        seasonal_periods=None,
        initialization_method=None,  # Future: 'estimated',
        initial_level=None,
        initial_trend=None,
        initial_seasonal=None,
        use_boxcox=None,
        bounds=None,
        dates=None,
        freq=None,
        missing="none",
```
They have been mentioned in their tutorial notebook: https://github.com/statsmodels/statsmodels/blob/main/examples/python/exponential_smoothing.py

These three parameters are called while fitting the model and not while initialising it. The below image shows a snippet from the code in the above link.

![image](https://user-images.githubusercontent.com/96042791/148554304-640caa45-3e79-4378-a978-1759f5b6d098.png)
 
Ohhhh, it's passed to `fit` and not to the constructor! Thanks!
We overlooked these, since they were being passed to `fit` in `statsmodels`. Bit of a questionable design decision on the side of `statsmodels` to pass some parameters in the constructor, and some in `fit`...

Aaaanyway, I've marked this a good first issue, since it should be only a few lines of code to add/change. I've also updated the issue at the top, with an instruction of what to do.

Thanks for bringing this to our attention, @ishannangia001. Feel free to add yourself as a contributor with an "ideas" contribution if you want to.
Agreed. Though I would imagine that the intuition might have been to keep iteratively testing out fits with different smoothing parameters for the same type of models as that might help in deciding the final smoothing parameters through cross-validation, etc.

Great. Thanks a lot ! Will certainly do.
>Though I would imagine that the intuition might have been to keep iteratively testing out fits with different smoothing parameters for the same type of models as that might help in deciding the final smoothing parameters through cross-validation, etc.

Sure - you can do this using a layer of tuning on top of the model, with `ForecastingGridSearchCV` from `sktime` ... as long as the ETS model exposes the parameters.

Commit message: typos

Patch to review:
diff --git a/sktime/forecasting/exp_smoothing.py b/sktime/forecasting/exp_smoothing.py
index c7c1fa74e6e..5523a2318a8 100644
--- a/sktime/forecasting/exp_smoothing.py
+++ b/sktime/forecasting/exp_smoothing.py
@@ -14,6 +14,8 @@
 class ExponentialSmoothing(_StatsModelsAdapter):
     """Holt-Winters exponential smoothing forecaster.
 
+    Direct interface for `statsmodels.tsa.holtwinters`.
+
     Default settings use simple exponential smoothing without trend and
     seasonality components.
 
@@ -49,6 +51,44 @@ class ExponentialSmoothing(_StatsModelsAdapter):
         level, trend, and seasonal state. 'estimated' uses the same heuristic
         as initial guesses, but then estimates the initial states as part of
         the fitting process.
+    smoothing_level : float, optional
+        The alpha value of the simple exponential smoothing, if the value
+        is set then this value will be used as the value.
+    smoothing_trend :  float, optional
+        The beta value of the Holt's trend method, if the value is
+        set then this value will be used as the value.
+    smoothing_seasonal : float, optional
+        The gamma value of the holt winters seasonal method, if the value
+        is set then this value will be used as the value.
+    damping_trend : float, optional
+        The phi value of the damped method, if the value is
+        set then this value will be used as the value.
+    optimized : bool, optional
+        Estimate model parameters by maximizing the log-likelihood.
+    remove_bias : bool, optional
+        Remove bias from forecast values and fitted values by enforcing
+        that the average residual is equal to zero.
+    start_params : array_like, optional
+        Starting values to used when optimizing the fit.  If not provided,
+        starting values are determined using a combination of grid search
+        and reasonable values based on the initial values of the data. See
+        the notes for the structure of the model parameters.
+    method : str, default "L-BFGS-B"
+        The minimizer used. Valid options are "L-BFGS-B" , "TNC",
+        "SLSQP" (default), "Powell", "trust-constr", "basinhopping" (also
+        "bh") and "least_squares" (also "ls"). basinhopping tries multiple
+        starting values in an attempt to find a global minimizer in
+        non-convex problems, and so is slower than the others.
+    minimize_kwargs : dict[str, Any]
+        A dictionary of keyword arguments passed to SciPy's minimize
+        function if method is one of "L-BFGS-B", "TNC",
+        "SLSQP", "Powell", or "trust-constr", or SciPy's basinhopping
+        or least_squares functions. The valid keywords are optimizer
+        specific. Consult SciPy's documentation for the full set of
+        options.
+    use_brute : bool, optional
+        Search for good starting values using a brute force (grid)
+        optimizer. If False, a naive set of starting values is used.
 
     References
     ----------
@@ -86,6 +126,16 @@ def __init__(
         initial_seasonal=None,
         use_boxcox=None,
         initialization_method="estimated",
+        smoothing_level=None,
+        smoothing_trend=None,
+        smoothing_seasonal=None,
+        damping_trend=None,
+        optimized=True,
+        remove_bias=False,
+        start_params=None,
+        method=None,
+        minimize_kwargs=None,
+        use_brute=True,
     ):
         # Model params
         self.trend = trend
@@ -97,6 +147,16 @@ def __init__(
         self.initial_trend = initial_trend
         self.initial_seasonal = initial_seasonal
         self.initialization_method = initialization_method
+        self.smoothing_level = smoothing_level
+        self.smoothing_trend = smoothing_trend
+        self.smoothing_seasonal = smoothing_seasonal
+        self.damping_trend = damping_trend
+        self.optimized = optimized
+        self.remove_bias = remove_bias
+        self.start_params = start_params
+        self.method = method
+        self.minimize_kwargs = minimize_kwargs
+        self.use_brute = use_brute
 
         super(ExponentialSmoothing, self).__init__()
 
@@ -114,4 +174,15 @@ def _fit_forecaster(self, y, X=None):
             initialization_method=self.initialization_method,
         )
 
-        self._fitted_forecaster = self._forecaster.fit()
+        self._fitted_forecaster = self._forecaster.fit(
+            smoothing_level=self.smoothing_level,
+            smoothing_trend=self.smoothing_trend,
+            smoothing_seasonal=self.smoothing_seasonal,
+            damping_trend=self.damping_trend,
+            optimized=self.optimized,
+            remove_bias=self.remove_bias,
+            start_params=self.start_params,
+            method=self.method,
+            minimize_kwargs=self.minimize_kwargs,
+            use_brute=self.use_brute,
+        )


Code review comments:
diff_hunk: @@ -97,6 +147,16 @@ def __init__(
         self.initial_trend = initial_trend
         self.initial_seasonal = initial_seasonal
         self.initialization_method = initialization_method
+        self.smoothing_level = smoothing_level
+        self.smoothing_trend = smoothing_trend
+        self.smoothing_seasonal = smoothing_seasonal
+        self.damping_trend = damping_trend
+        self.optimized = optimized
+        self.remove_bias = remove_bias
+        self.start_params = start_params
+        self.method = method
+        self.minimize_kwargs = minimize_kwargs
+        self.use_brute = use_brute
 
         super(ExponentialSmoothing, self).__init__()
path: sktime/forecasting/exp_smoothing.py
text: @user1:
```suggestion
        super().__init__()
```

diff_hunk: @@ -97,6 +147,16 @@ def __init__(
         self.initial_trend = initial_trend
         self.initial_seasonal = initial_seasonal
         self.initialization_method = initialization_method
+        self.smoothing_level = smoothing_level
+        self.smoothing_trend = smoothing_trend
+        self.smoothing_seasonal = smoothing_seasonal
+        self.damping_trend = damping_trend
+        self.optimized = optimized
+        self.remove_bias = remove_bias
+        self.start_params = start_params
+        self.method = method
+        self.minimize_kwargs = minimize_kwargs
+        self.use_brute = use_brute
 
         super(ExponentialSmoothing, self).__init__()
path: sktime/forecasting/exp_smoothing.py
text: @user1:
is this call necessary when no argument are being passed?

@author:
they are being passed later, in `fit`? Or what do you mean?

@user1:
Sorry, I'm only referring to the call to `super` 

@author:
> is this call necessary when no argument are being passed?

ah, I see - yes, since it handles setting of the `_is_fitted` flag and, potentially, related concerns.

@user1:
I can only see `_StatsModelAdapter` setting `self._forecaster = None` and  `self._fitted_forecaster = None`, shouldn't the `_is_fitted` be set in `fit`? 

@author:
all fittable estimators should set `_is_fitted` to `False` in `__init__`.
In this case, it should happen two levels higher (after two `super` calls), in `BaseEstimator`.

@user1:
I think there is something to be said about the growing inheritance depth that does not make it straightforward to either review or write code extending functionality. That is probably a broader design discussion, but I want to make a comment here since it a good illustration of that point.