Repository: statsmodels/statsmodels

Issue:
BUG: transformed states when fitting an exponential smoothing model with boxcox
#### Description

When fitting a `statsmodels.tsa.holtwinters.ExponentialSmoothing` with `use_boxcox=True`, the states (level, slope, season) are back-transformed into the original data space.
This leads to unexpected outcomes, for example negative slope values when there's clearly a positive trend (see below).

In my opinion it doesn't really make sense to perform a back-transformation of the states, only the fitted values should be back-transformed.

I am unsure if this is a real issue, and whether this should be fixed.
As discussed in #6560, fixing it could lead to backwards compatibility issues, if someone relies on the current implementation. I am opening this issue mainly to shift discussion from #6560 to somewhere more prominent.

#### Code Sample

In the following example, an exponential smoothing model is fitted to the australian tourists data. As can be seen from the data, there's a clear positive trend, so the slope should be positive.

```python
import pandas as pd
from statsmodels.tsa.holtwinters import ExponentialSmoothing

data = [41.7275,  24.0418,  32.3281,  37.3287,  
        46.2132,  29.3463, 36.4829,  42.9777,  
        48.9015,  31.1802,  37.7179,  40.4202, 
        51.2069,  31.8872,  40.9783,  43.7725,  
        55.5586,  33.8509, 42.0764,  45.6423,  
        59.7668,  35.1919,  44.3197,  47.9137]
index= pd.date_range(start='2005', end='2010-Q4', freq='QS-OCT')
aust = pd.Series(data, index)

fit = ExponentialSmoothing(aust, seasonal_periods=4, trend="add", seasonal="mul").fit(use_boxcox=True)
fit.slope
```

The resulting slope is, however, negative:

    2005-01-01   -36.535594
    2005-04-01   -35.870488
    2005-07-01   -37.289832
    2005-10-01   -39.177333
    2006-01-01   -40.314514
    2006-04-01   -42.097277
    2006-07-01   -43.109932
    2006-10-01   -45.655884
    2007-01-01   -45.129759
    2007-04-01   -46.417821
    2007-07-01   -46.394352
    2007-10-01   -46.177375
    2008-01-01   -46.766383
    2008-04-01   -47.843718
    2008-07-01   -49.248722
    2008-10-01   -49.583715
    2009-01-01   -50.611379
    2009-04-01   -51.524685
    2009-07-01   -52.034369
    2009-10-01   -52.319109
    2010-01-01   -54.104202
    2010-04-01   -54.509413
    2010-07-01   -55.173299
    2010-10-01   -55.398304
    Freq: QS-OCT, dtype: float64


Discussion:
My feeling is that we can classify this as a bug, and as a result we could just change the behavior.  I guess this is the direction that we will go, unless there are different opinions.

Resolving patch:
diff --git a/statsmodels/tsa/holtwinters/model.py b/statsmodels/tsa/holtwinters/model.py
--- a/statsmodels/tsa/holtwinters/model.py
+++ b/statsmodels/tsa/holtwinters/model.py
@@ -1083,7 +1083,7 @@ def fit(
                 )
             warnings.warn(
                 "Setting initial values during fit is deprecated and will be "
-                "removed after 0.13. These should be set during model"
+                "removed after 0.13. These should be set during model "
                 "initialization."
             )
         if use_boxcox is not None:
@@ -1405,19 +1405,7 @@ def _predict(
             fitted = trend
         level = lvls[1 : nobs + 1].copy()
         if use_boxcox or use_boxcox == "log" or isinstance(use_boxcox, float):
-            # store untransformed values in private attribute, transforming
-            # here is probably a bug
-            self._untransformed_level = level
-            self._untransformed_trend = _trend
-            self._untransformed_season = season
-
             fitted = inv_boxcox(fitted, lamda)
-            level = inv_boxcox(level, lamda)
-            _trend = detrend(trend[:nobs], level)
-            if seasonal == "add":
-                season = (fitted - inv_boxcox(trend, lamda))[:nobs]
-            else:  # seasonal == 'mul':
-                season = (fitted / inv_boxcox(trend, lamda))[:nobs]
         err = fitted[: -h - 1] - data
         sse = err.T @ err
         # (s0 + gamma) + (b0 + beta) + (l0 + alpha) + phi
diff --git a/statsmodels/tsa/holtwinters/results.py b/statsmodels/tsa/holtwinters/results.py
--- a/statsmodels/tsa/holtwinters/results.py
+++ b/statsmodels/tsa/holtwinters/results.py
@@ -635,14 +635,9 @@ def simulate(
             neutral_s = 0
 
         # set initial values
-        if use_boxcox:
-            level = self.model._untransformed_level
-            _trend = self.model._untransformed_trend
-            season = self.model._untransformed_season
-        else:
-            level = self.level
-            _trend = self.trend
-            season = self.season
+        level = self.level
+        _trend = self.trend
+        season = self.season
         # (notation as in https://otexts.com/fpp2/ets.html)
         y = np.empty((nsimulations, repetitions))
         # lvl instead of l because of E741


Test patch:
diff --git a/statsmodels/tsa/holtwinters/tests/test_holtwinters.py b/statsmodels/tsa/holtwinters/tests/test_holtwinters.py
--- a/statsmodels/tsa/holtwinters/tests/test_holtwinters.py
+++ b/statsmodels/tsa/holtwinters/tests/test_holtwinters.py
@@ -1851,3 +1851,16 @@ def test_forecast_index_types(ses, index_typ):
         fcast = res.forecast(36)
     assert isinstance(fcast, pd.Series)
     pd.testing.assert_index_equal(fcast.index, fcast_index)
+
+
+def test_boxcox_components(ses):
+    mod = ExponentialSmoothing(
+        ses + 1 - ses.min(), initialization_method="estimated", use_boxcox=True
+    )
+    res = mod.fit()
+    with pytest.raises(AssertionError):
+        # Must be different since level is not transformed
+        assert_allclose(res.level, res.fittedvalues)
+    assert not hasattr(res, "_untransformed_level")
+    assert not hasattr(res, "_untransformed_trend")
+    assert not hasattr(res, "_untransformed_seasonal")