Repository: sktime/sktime

Issue:
[BUG] Seasonal periodicity seems to work incorrectly (at least for naive forecasts with mean strategy)
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->
The interpretation of seasonal periodicity seems to be off by a number of steps in some cases, resulting in shifted forecasts.

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```python
import pandas as pd
from sktime.forecasting.base import ForecastingHorizon
from sktime.forecasting.naive import NaiveForecaster

# given 4 hours of data with a seasonal periodicity of 3 hours
sp = 3
train_period = pd.date_range("2021-06-01 00:00", "2021-06-1 04:00", freq="1h", closed="left")
train_data = pd.Series(
    index=train_period,
    data=(list(range(1, sp + 1))*len(train_period))[:len(train_period)],
)
print(train_data)

# 2021-06-01 00:00:00    1
# 2021-06-01 01:00:00    2
# 2021-06-01 02:00:00    3
# 2021-06-01 03:00:00    1
# Freq: H, dtype: int64

# let's forecast the next 5 hours with a periodicity of 3 hours
test_period = pd.date_range("2021-06-01 04:00", "2021-06-1 09:00", freq="1h", closed="left")
fh = ForecastingHorizon(test_period, is_relative=False)
model = NaiveForecaster(strategy="mean", sp=sp)
model.fit(train_data)
test_data = model.predict(fh)
print(test_data)

# 2021-06-01 04:00:00    1.0
# 2021-06-01 05:00:00    2.0
# 2021-06-01 06:00:00    3.0
# 2021-06-01 07:00:00    1.0
# 2021-06-01 08:00:00    2.0
# dtype: float64
```

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
```python
# the result I expected is

# 2021-06-01 04:00:00    2.0  # (value of 3 hours earlier)
# 2021-06-01 05:00:00    3.0  # (value of 3 hours earlier)
# 2021-06-01 06:00:00    1.0  # (mean value of 3 and 6 hours earlier)
# 2021-06-01 07:00:00    2.0  # (value of 6 hours earlier)
# 2021-06-01 08:00:00    3.0  # (value of 6 hours earlier)
# dtype: float64
```

**Additional context**
<!--
Add any other context about the problem here.
-->
In the above, `sp=3` gives forecasts shifted by 1 step. Changing `sp=3` to `sp=1`, `sp=2` or `sp=4` all yield results as expected. With a training dataset of 5 hours and a seasonality of 3 hours, the forecasts are shifted by 2 steps. It this somehow caused by the training set not being divisible by the periodicity?

**Versions**
<details>

<!--
Please run the following code snippet and paste the output here:
 
from sktime import show_versions; show_versions()
-->
System:
    python: 3.8.6 (default, Jan 27 2021, 15:42:20)  [GCC 10.2.0]
executable: /home/felix/PycharmProjects/sktime/venv/bin/python
   machine: Linux-5.11.0-7614-generic-x86_64-with-glibc2.32

Python dependencies:
          pip: 21.1.2
   setuptools: 57.0.0
      sklearn: 0.24.2
       sktime: 0.6.1
  statsmodels: 0.12.2
        numpy: 1.20.3
        scipy: 1.6.3
       Cython: 0.29.23
       pandas: 1.2.4
   matplotlib: None
       joblib: 1.0.1
        numba: 0.53.1
     pmdarima: None
      tsfresh: None
</details>

<!-- Thanks for contributing! -->



Discussion:
>It this somehow caused by the training set not being divisible by the periodicity?

Huh, weird - but my educated guess (looking at outputs but not the code) would be the same: namely, that remainder is taken from the end of the series, not from the start, which is a problem only if training set size is not divisible by periodicity.
Thanks @Flix6x would you like to open a PR to fix this? The code lives [here](https://github.com/alan-turing-institute/sktime/blob/main/sktime/forecasting/naive.py).
Sure, I'll give it a go.

Resolving patch:
diff --git a/sktime/forecasting/naive.py b/sktime/forecasting/naive.py
--- a/sktime/forecasting/naive.py
+++ b/sktime/forecasting/naive.py
@@ -192,7 +192,7 @@ def _predict_last_window(
                     pad_width = self.sp_ - remainder
                 else:
                     pad_width = 0
-                last_window = np.hstack([last_window, np.full(pad_width, np.nan)])
+                last_window = np.hstack([np.full(pad_width, np.nan), last_window])
 
                 # reshape last window, one column per season
                 last_window = last_window.reshape(


Test patch:
diff --git a/sktime/forecasting/tests/test_naive.py b/sktime/forecasting/tests/test_naive.py
--- a/sktime/forecasting/tests/test_naive.py
+++ b/sktime/forecasting/tests/test_naive.py
@@ -8,6 +8,7 @@
 import pandas as pd
 import pytest
 
+from sktime.forecasting.base import ForecastingHorizon
 from sktime.forecasting.naive import NaiveForecaster
 from sktime.forecasting.tests._config import TEST_OOS_FHS
 from sktime.forecasting.tests._config import TEST_SPS
@@ -83,7 +84,7 @@ def test_strategy_mean_seasonal(fh, sp, window_length):
         last_window = y_train.iloc[-window_length:].to_numpy().astype(float)
         last_window = np.pad(
             last_window,
-            (0, sp - len(last_window) % sp),
+            (sp - len(last_window) % sp, 0),
             "constant",
             constant_values=np.nan,
         )
@@ -172,3 +173,62 @@ def test_strategy_drift_window_length(fh, window_length):
         expected = values[-1] + slope * fh
 
         np.testing.assert_array_equal(y_pred, expected)
+
+
+@pytest.mark.parametrize("n", [3, 5])
+@pytest.mark.parametrize("window_length", list({4, 5, *TEST_WINDOW_LENGTHS}))
+@pytest.mark.parametrize("sp", list({3, 4, 8, *TEST_SPS}))
+def test_strategy_mean_seasonal_additional_combinations(n, window_length, sp):
+    """Check time series of n * window_length with a 1:n-1 train/test split,
+    for different combinations of the period and seasonal periodicity.
+    The time series contains perfectly cyclic data.
+    """
+
+    # given <window_length> hours of data with a seasonal periodicity of <sp> hours
+    freq = pd.Timedelta("1H")
+    data = pd.Series(
+        index=pd.date_range(
+            "2021-06-01 00:00", periods=n * window_length, freq=freq, closed="left"
+        ),
+        data=([float(i) for i in range(1, sp + 1)] * n * window_length)[
+            : n * window_length
+        ],
+    )
+
+    # Split into train and test data
+    train_data = data[:window_length]
+    test_data = data[window_length:]
+
+    # Forecast data does not retain the original frequency
+    test_data.index.freq = None
+
+    # For example, for n=2, periods=4 and sp=3:
+
+    # print(train_data)
+    # 2021-06-01 00:00:00    1.0
+    # 2021-06-01 01:00:00    2.0
+    # 2021-06-01 02:00:00    3.0
+    # 2021-06-01 03:00:00    1.0
+    # Freq: H, dtype: int64
+
+    # print(test_data)
+    # 2021-06-01 04:00:00    2.0  # (value of 3 hours earlier)
+    # 2021-06-01 05:00:00    3.0  # (value of 3 hours earlier)
+    # 2021-06-01 06:00:00    1.0  # (mean value of 3 and 6 hours earlier)
+    # 2021-06-01 07:00:00    2.0  # (value of 6 hours earlier)
+    # dtype: float64
+
+    # let's forecast the next <2 x period> hours with a periodicity of <sp> hours
+    fh = ForecastingHorizon(test_data.index, is_relative=False)
+    model = NaiveForecaster(strategy="mean", sp=sp)
+    model.fit(train_data)
+    forecast_data = model.predict(fh)
+
+    if sp < window_length:
+        # We expect a perfect forecast given our perfectly cyclic data
+        pd.testing.assert_series_equal(forecast_data, test_data)
+    else:
+        # We expect a few forecasts yield NaN values
+        for i in range(1 + len(test_data) // sp):
+            test_data[i * sp : i * sp + sp - window_length] = np.nan
+        pd.testing.assert_series_equal(forecast_data, test_data)