Repository: statsmodels/statsmodels

Issue:
ETSModel.fit() reads initial_seasonal array in the wrong order.
#### Describe the bug

When I fit my ETSModel it looks as if fit() reads the initial_seasonal array in the wrong order. This makes the seasonal component of the fitted model look strange and the resulting forecast is not accurate.

In the example below there is two years of data (24 months) with very similar pattern between year one and two. Even so the forecast is not good and the model fails to capture the seasonal pattern. When you look at the details you see that the inital_seasonal component of the ETS model looks fine but when you look at the season component in the fitted model it seem to be wrong. More precisely, if you look at the first twelve months of the season component it looks like a mirror image of the inital_seasonal values. This makes me suspect that the .fit() method reads the array in the wrong order.

#### Code Sample, a copy-pastable example if possible
```python
from statsmodels.tsa.exponential_smoothing.ets import ETSModel as ETS
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

time_series = [
                23939.4,
                26092.0, 
                30088.1, 
                18531.1, 
                21384.0, 
                19387.5,
                0.0,
                0.0,
                0.0,
                0.0,
                0.0, 
                7229.3,
                27747.5,
                31152.0, 
                35640.0, 
                28433.4, 
                29304.0,
                23336.5,
                4378.0,
                0.0,
                0.0,
                0.0,
                0.0,
                1526.6
            ]

first = pd.Series(time_series[:12])
last = pd.Series(time_series[-12:])
time_series = pd.Series(time_series)

model = ETS(
    endog=time_series,
    initialization_method='heuristic',
    error='add',
    seasonal='add',
    seasonal_periods=12
)
fitted = model.fit(
    full_output=False,
    disp=False,
)
forecast = fitted.forecast(12)

fig, (ax) = plt.subplots(1, 1, figsize=(11, 6))
ax = first.plot(color='blue', label='time_series first 12 months', title="Seasonal profiles")
ax = last.plot(ax=ax, label="time_series last 12 months", color='dodgerblue')
ax = pd.Series(model.initial_seasonal).plot(ax=ax, label='initial_seasonal', color='green')
ax = pd.Series(fitted.season[12:]).plot(ax=ax, label='first 12 months fitted.season', color='red')
_ = ax.legend()
plt.show()
```

#### Small gamma suggest that seasonal component shouldn't change too dramatically.
print(fitted.gamma)
#### As you can see in the plot above the fitted.season values are basically a mirror of the intial_seasonal values. I would expect them to be much more similar, especially given the small gamma value (= 0.01747). 

#### Forecast, as you can see it get's it completely wrong.
fig, (ax) = plt.subplots(1, 1, figsize=(11, 6))
ax = time_series.plot(color='blue', label='time_series', title="Time series and forecast")
ax = pd.Series(forecast).plot(ax=ax, label='Forecast', color='red')
_ = ax.legend()
plt.show()


<details>

**Note**: As you can see, there are many issues on our GitHub tracker, so it is very possible that your issue has been posted before. Please check first before submitting so that we do not have to handle and close duplicates.

**Note**: Please be sure you are using the latest released version of `statsmodels`, or a recent build of `main`. If your problem has been fixed in an unreleased version, you might be able to use `main` until a new release occurs. 

**Note**: If you are using a released version, have you verified that the bug exists in the main branch of this repository? It helps the limited resources if we know problems exist in the current main branch so that they do not need to check whether the code sample produces a bug in the next release.

</details>


If the issue has not been resolved, please file it in the issue tracker.

#### Expected Output

I would expect the seasonal component of the fitted model to be more similar to the initial_seasonal values from the ETSModel.

#### Output of ``import statsmodels.api as sm; sm.show_versions()``

<details>

[paste the output of ``import statsmodels.api as sm; sm.show_versions()`` here below this line]
INSTALLED VERSIONS
------------------
Python: 3.9.7.final.0

statsmodels
===========

Installed: 0.13.0 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\statsmodels)

Required Dependencies
=====================

cython: Not installed
numpy: 1.21.4 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\numpy)
scipy: 1.7.1 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\scipy)
pandas: 1.3.4 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\pandas)
    dateutil: 2.8.2 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\dateutil)
patsy: 0.5.2 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\patsy)

Optional Dependencies
=====================

matplotlib: 3.4.3 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\matplotlib)
    backend: module://matplotlib_inline.backend_inline 
cvxopt: Not installed
joblib: 1.1.0 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\joblib)

Developer Tools
================

IPython: 7.29.0 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\IPython)
    jinja2: Not installed
sphinx: Not installed
    pygments: 2.10.0 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\pygments)
pytest: 6.2.5 (C:\Miniconda3\envs\forecast_engine_test\lib\site-packages\pytest)
virtualenv: Not installed

</details>



Discussion:
The difference is very small.

```python
(fitted.season - fitted.season.shift(12)) / np.abs(fitted.season)
```
Maximum 1%

```
0          NaN
1          NaN
2          NaN
3          NaN
4          NaN
5          NaN
6          NaN
7          NaN
8          NaN
9          NaN
10         NaN
11         NaN
12    0.095213
13    0.012964
14    0.004916
15   -0.009245
16    0.000061
17   -0.008292
18   -0.113905
19   -0.015593
20    0.000783
21   -0.006444
22    0.004834
23    0.006897
```
Hi and thanks for a rapid response!

Yes, once you are updating the actual seasonal values as part of the fit it does it correctly.

However, look at the difference between the initial values generated by the ETSModel heuristic and the first twelve seasonal values. The difference is huge and it doesn't seem correct?

```python
(model.initial_seasonal - fitted.season[:12]) / np.abs(model.initial_seasonal)

0     1.697542
1     2.011674
2     1.744387
3     2.154344
4     2.024238
5     2.786466
6    -1.508185
7    -1.965162
8    -1.880522
9    -2.329182
10   -1.982986
11   -2.395582
Name: seasonal, dtype: float64
```

But if you flip the initial seasonal part the difference is much smaller, like you would expect. That's why I thought the .fit() method reads the initial_seasonal array from the ETSModel in the wrong order.

```python
(np.flip(model.initial_seasonal) - fitted.season[:12]) / np.abs(model.initial_seasonal)

0    -0.030294
1    -0.010839
2    -0.003078
3     0.017419
4    -0.002944
5     0.005557
6     0.053326
7     0.008375
8    -0.000957
9     0.008674
10   -0.005004
11   -0.021647
Name: seasonal, dtype: float64
```

//Pontus
Definitely no reversal:

```python
from statsmodels.tsa.exponential_smoothing.ets import ETSModel as ETS
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

seasonal = np.cos(np.arange(12)*2*np.pi/12) - np.arange(12)/2
time_series = seasonal.tolist() * 5 + np.random.randn(60)
time_series = pd.Series(time_series)

model = ETS(
    endog=time_series,
    initialization_method='heuristic',
    error='add',
    seasonal='add',
    seasonal_periods=12
)
fitted = model.fit(
    full_output=False,
    disp=False,
)
plt.plot(seasonal.reshape(-1, 12).T)
plt.plot(fitted.season.to_numpy().reshape(-1, 12).T)
plt.show()
```

![image](https://user-images.githubusercontent.com/5585221/142987724-5d83226b-cd61-44e1-9623-79c1f293a056.png)


Hi again, 
OK fair enough I will drop the issue for now and do some digging myself to see what it is in the fit method that causes the behavior in my example.
I still don't get how it can go from good inital_seasonal values to something that looks like an exact mirror image based on the update of state equation. That said it could well be something in my specific example that triggers this and not something that generalizes to other examples.
 
```python
from statsmodels.tsa.exponential_smoothing.ets import ETSModel as ETS
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
time_series = [
                23939.4,
                26092.0, 
                30088.1, 
                18531.1, 
                21384.0, 
                19387.5,
                0.0,
                0.0,
                0.0,
                0.0,
                0.0, 
                7229.3,
                27747.5,
                31152.0, 
                35640.0, 
                28433.4, 
                29304.0,
                23336.5,
                4378.0,
                0.0,
                0.0,
                0.0,
                0.0,
                1526.6
            ]
time_series = pd.Series(time_series)
model = ETS(
    endog=time_series,
    initialization_method='heuristic',
    error='add',
    seasonal='add',
    seasonal_periods=12
)
fitted = model.fit(
    full_output=False,
    disp=False,
)
fig, (ax) = plt.subplots(1, 1, figsize=(11, 6))
ax = pd.Series(model.initial_seasonal).plot(ax=ax, label='initial_seasonal', color='green')
ax = pd.Series(fitted.season).plot(ax=ax, label='fitted.season (mirror image of initial_seasonal', color='red')
_ = ax.legend()
plt.show()
```
![Figure_2](https://user-images.githubusercontent.com/13979779/143007204-ac76f933-77ae-48a2-8b3b-c456c6979666.png)

Regards,
Pontus


Sorry, since I couldn't get my fork working properly I will just add one last comment on this one. 
I did some tests on the series you provided and the results still make me believe that all is not right here. Bottom line is that by flipping the initial_seasonal array you get an enormous improvement of the metrics (MAE, AIC). Also, the way the gamma value behaves strengthens my suspicions, see my comments further down.

```python
from statsmodels.tsa.exponential_smoothing.ets import ETSModel as ETS
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

seasonal = np.cos(np.arange(12)*2*np.pi/12) - np.arange(12)/2
time_series = seasonal.tolist() * 5  #+ np.random.randn(60)  # removed random part for reproducibility.

model = ETS(
    endog=time_series,
    initialization_method='heuristic',
    error='add',
    seasonal='add',
    seasonal_periods=12
)
fitted = model.fit(
    full_output=False,
    disp=False,
)

print('gamma: ' + str(fitted.gamma))
print('MAE: ' + str(fitted.mae)) 
print('AIC:' + str(fitted.aic))
gamma: 0.9998000100000001
MAE: 0.666977106834887
AIC:238.27243291194432
``` 

Above gamma is very close to 1 meaning that the new seasonal value basically will be a copy of the latest known value. This could well indicate that the initial_seasonal values are not relevant for the model fit, i.e they are wrong.

Next step, I copy the inital_level and initial_seasonal values and use the in a new model with 'known' initialization. Most importantly I flip the seasonal_values.

```python
print(model.initial_level)
-2.7499999999999982
print(model.initial_seasonal)
array([ 3.75     ,  3.1160254,  2.25     ,  1.25     ,  0.25     ,
       -0.6160254, -1.25     , -1.6160254, -1.75     , -1.75     ,
       -1.75     , -1.8839746])

model = ETS(
    endog=time_series,
    initialization_method='known',
    initial_level= -2.7499999999999982, # Copied the values from the heuristic initialization.
    initial_seasonal= np.flip([3.75, 3.1160254, 2.25, 1.25, 0.25, -0.6160254, -1.25, -1.6160254, -1.75, -1.75, -1.75, -1.8839746]), # # Use the same value as the heuristic did but flip them.
    error='add',
    seasonal='add',
    seasonal_periods=12
)
fitted = model.fit(
    full_output=False,
    disp=False,
)

print('gamma: ' + str(fitted.gamma))
print('MAE: ' + str(fitted.mae))
print('AIC:' + str(fitted.aic))
gamma: 9.998999999250606e-05
MAE: 1.2614811898773818e-09
AIC: -2216.746308225096
```

As you can see the gamma is now almost 0. Meaning that the model finds no need to update the inital seasonal_values, i.e they are good. 
Also notice the dramatic improvement in MAE from 0.667 to 1.26e-09! In terms of % change it's infinitely better when we flip the initial_seasonal values. The same is true for AIC going from 238 to -2216, an impressive improvement.

I won't press this issue any further but I do believe that it would be good to just debug the fit method and see what is actually happening in order to remove any doubt.

Regards,
Pontus


> I did some tests on the series you provided and the results still make me believe that all is not right here. 

Taking a look at the code, I do think that you are right that the initial seasonal is in the wrong order for the `heuristic` and `simple` methods. Thanks for the report.
Thanks, no worries.

In large sample the intializer appears to be correct:

```python
from statsmodels.tsa.exponential_smoothing.ets import ETSModel as ETS
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

REPEATS = 100
seasonal = np.cos(np.arange(12)*2*np.pi/12) - np.arange(12)/2
seasonal -= seasonal.mean()
time_series = seasonal.tolist() * REPEATS  #+ np.random.randn(60)  # removed random part for reproducibility.

model = ETS(
    endog=time_series,
    initialization_method='heuristic',
    error='add',
    seasonal='add',
    seasonal_periods=12
)
fitted = model.fit(
    full_output=False,
    disp=False,
)

print(fitted.initial_seasonal - seasonal)
```

```
[0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
 1.11022302e-16 2.22044605e-16 0.00000000e+00 0.00000000e+00
 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00]
```

I think there is a deeper issue that the seasonal processes in the statespace representation are wrong.  Here is what leads me to that suggestion.

```python
model = ETS(
    endog=time_series,
    initialization_method='estimated',
    error='add',
    seasonal='add',
    seasonal_periods=12
)
fitted = model.fit(
    full_output=False,
    disp=False,
)
print(fitted.initial_seasonal - seasonal[::-1])
```

```
array([-3.75000001, -3.75000001, -3.75000001, -3.75000002, -3.75000002,
       -3.75000002, -3.75000003, -3.75000003, -3.75000003, -3.75000003,
       -3.75000004, -3.75      ])
```

Note that when you estimate things it is completely flat when `seasonal` is reversed.  This suggests that the 12 seasonal components in the statespace representation are being interpreted backward, rather than something like an initialization problem.

`ExponentialSmoothing` also appears to be broken in the same way.

Resolving patch:
diff --git a/statsmodels/tsa/exponential_smoothing/ets.py b/statsmodels/tsa/exponential_smoothing/ets.py
--- a/statsmodels/tsa/exponential_smoothing/ets.py
+++ b/statsmodels/tsa/exponential_smoothing/ets.py
@@ -655,6 +655,8 @@ def prepare_data(data):
         endog = np.array(data.orig_endog, order="C")
         if endog.ndim != 1:
             raise ValueError("endog must be 1-dimensional")
+        if endog.dtype != np.double:
+            endog = np.asarray(data.orig_endog, order="C", dtype=float)
         return endog, None
 
     @property
@@ -790,7 +792,11 @@ def _internal_params(self, params):
         if self.initialization_method != "estimated":
             internal[4] = self.initial_level
             internal[5] = self.initial_trend
-            internal[6:] = self.initial_seasonal
+            if np.isscalar(self.initial_seasonal):
+                internal[6:] = self.initial_seasonal
+            else:
+                # See GH 7893
+                internal[6:] = self.initial_seasonal[::-1]
         return internal
 
     def _model_params(self, internal):
@@ -1408,7 +1414,8 @@ def __init__(self, model, params, results):
             self.smoothing_trend = self.beta
         if self.has_seasonal:
             self.season = states[:, self.model._seasonal_index]
-            self.initial_seasonal = internal_params[6:]
+            # See GH 7893
+            self.initial_seasonal = internal_params[6:][::-1]
             self.initial_state[
                 self.model._seasonal_index :
             ] = self.initial_seasonal
diff --git a/statsmodels/tsa/statespace/exponential_smoothing.py b/statsmodels/tsa/statespace/exponential_smoothing.py
--- a/statsmodels/tsa/statespace/exponential_smoothing.py
+++ b/statsmodels/tsa/statespace/exponential_smoothing.py
@@ -4,7 +4,6 @@
 Author: Chad Fulton
 License: BSD-3
 """
-
 import numpy as np
 import pandas as pd
 from statsmodels.base.data import PandasData
@@ -105,6 +104,15 @@ def __init__(self, endog, trend=False, damped_trend=False, seasonal=None,
         self.damped_trend = bool_like(damped_trend, 'damped_trend')
         self.seasonal_periods = int_like(seasonal, 'seasonal', optional=True)
         self.seasonal = self.seasonal_periods is not None
+        if seasonal:
+            import warnings
+
+            warnings.warn(
+                "ExponentialSmoothing should not be used with seasonal "
+                "terms. It has a serious bug that has not been fixed. "
+                "Instead use ETSModel.",
+                RuntimeWarning
+            )
         self.initialization_method = string_like(
             initialization_method, 'initialization_method').lower()
         self.concentrate_scale = bool_like(concentrate_scale,


Test patch:
diff --git a/statsmodels/tsa/tests/test_exponential_smoothing.py b/statsmodels/tsa/tests/test_exponential_smoothing.py
--- a/statsmodels/tsa/tests/test_exponential_smoothing.py
+++ b/statsmodels/tsa/tests/test_exponential_smoothing.py
@@ -742,7 +742,8 @@ def statespace_comparison(austourists):
         initialization_method="known",
         initial_level=ets_results.initial_level,
         initial_trend=ets_results.initial_trend,
-        initial_seasonal=ets_results.initial_seasonal,
+        # See GH 7893
+        initial_seasonal=ets_results.initial_seasonal[::-1],
     )
     with statespace_model.fix_params(
         {
@@ -1047,3 +1048,22 @@ def test_estimated_initialization_short_data(oildata, trend, seasonal, nobs):
         initialization_method='estimated'
     ).fit()
     assert ~np.any(np.isnan(res.params))
+
+
+@pytest.mark.parametrize("method", ["estimated", "heuristic"])
+def test_seasonal_order(reset_randomstate, method):
+    seasonal = np.arange(12.0)
+    time_series = np.array(list(seasonal) * 100)
+    res = ETSModel(
+        time_series,
+        seasonal="add",
+        seasonal_periods=12,
+        initialization_method=method,
+    ).fit()
+    assert_allclose(
+        res.initial_seasonal + res.initial_level,
+        seasonal,
+        atol=1e-4,
+        rtol=1e-4,
+    )
+    assert res.mae < 1e-6