Repository: sktime/sktime

Issue:
Extend NaiveForecaster
Extend `NaiveForecaster` to include all common naive forecasting strategies. For an overview, see this [chapter](https://otexts.com/fpp2/simple-methods.html#simple-methods).

- [x] introduce `seasonal` as boolean kwarg, refactor "seasonal_last" and implement "seasonal_mean", so that we can set `seasonal=True` and `strategy="mean"` for example
- [x] add "drift" strategy, the forecasts should be similar to the forecasts from `ReducedRegressionForecaster(regressor=LinearRegression(), ...)`
- [ ] implement more efficient `update_predict` routine


Discussion:

Resolving patch:
diff --git a/sktime/forecasting/naive.py b/sktime/forecasting/naive.py
--- a/sktime/forecasting/naive.py
+++ b/sktime/forecasting/naive.py
@@ -3,7 +3,7 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 
 __all__ = ["NaiveForecaster"]
-__author__ = "Markus Löning"
+__author__ = ["Markus Löning", "Piyush Gade"]
 
 from warnings import warn
 
@@ -23,35 +23,35 @@ class NaiveForecaster(OptionalForecastingHorizonMixin,
 
     Parameters
     ----------
-    strategy : str{"last", "mean", "seasonal_last"}, optional (default="last")
+    strategy : str{"last", "mean"}, optional (default="last")
         Strategy used to make forecasts:
 
-        * "last": forecast the last value in the training series
-        * "mean": forecast the mean of (a given window) of the training series
-        * "seasonal_last": forecasts the last value of the same season in
-        the training series
+        * "last" : forecast the last value in the
+                    training series when sp is 1.
+                    When sp is not 1,
+                    last value of each season
+                    in the last window will be
+                    forecasted for each season.
+        * "mean" : forecast the mean of last window
+                     of training series when sp is 1.
+                     When sp is not 1, mean of all values
+                     in a season from last window will be
+                     forecasted for each season.
+
+    sp : int, optional (default=1)
+        Seasonal periodicity to use in the seasonal forecasting.
 
     window_length : int or None, optional (default=None)
         Window length to use in the `mean` strategy. If None, entire training
             series will be used.
     """
 
-    def __init__(self, strategy="last", window_length=None, sp=None):
+    def __init__(self, strategy="last", window_length=None, sp=1):
         super(NaiveForecaster, self).__init__()
-        # input checks
-        # allowed strategies to include: last, constant, seasonal-last,
-        # mean, median
-        allowed_strategies = ("last", "mean", "seasonal_last")
-        if strategy not in allowed_strategies:
-            raise ValueError(
-                f"Unknown strategy: {strategy}; expected one of "
-                f"{allowed_strategies}")
         self.strategy = strategy
         self.sp = sp
         self.window_length = window_length
 
-        self.sp_ = None
-
     def fit(self, y_train, fh=None, X_train=None):
         """Fit to training data.
 
@@ -70,42 +70,44 @@ def fit(self, y_train, fh=None, X_train=None):
         self._set_oh(y_train)
         self._set_fh(fh)
 
-        if self.strategy in ("last", "seasonal_last"):
-            if self.window_length is not None:
-                warn("For the `last` and `seasonal_last` strategy, "
-                     "the `window_length_` value will be ignored.")
-
-        if self.strategy in ("last", "mean"):
-            if self.sp is not None:
-                warn("For the `last` and `mean` strategy, "
-                     "the `sp` value will be ignored.")
-
         if self.strategy == "last":
-            self.window_length_ = 1
-
-        if self.strategy == "seasonal_last":
-            if self.sp is None:
-                raise NotImplementedError(
-                    "Automatic estimation of the seasonal periodicity `sp` "
-                    "from the data is not implemented yet; "
-                    "please specify the `sp` value.")
-            self.sp_ = check_sp(self.sp)
+            if self.sp == 1:
+                if self.window_length is not None:
+                    warn("For the `last` strategy, "
+                         "the `window_length` value will be ignored if `sp` "
+                         "== 1.")
+                self.window_length_ = 1
 
-            # window length we need for forecasts is just the
-            # length of seasonal periodicity
-            self.window_length_ = self.sp_
+            else:
+                self.sp_ = check_sp(self.sp)
 
-        if self.strategy == "mean":
+                # window length we need for forecasts is just the
+                # length of seasonal periodicity
+                self.window_length_ = self.sp_
+
+        elif self.strategy == "mean":
+            # check window length is greater than sp for seasonal mean
+            if self.window_length is not None and self.sp != 1:
+                if self.window_length < self.sp:
+                    raise ValueError(f"The `window_length`: "
+                                     f"{self.window_length} is smaller than "
+                                     f"`sp`: {self.sp}.")
             self.window_length_ = check_window_length(self.window_length)
+            self.sp_ = check_sp(self.sp)
+
+            #  if not given, set default window length for the mean strategy
+            if self.window_length is None:
+                self.window_length_ = len(y_train)
 
-        #  if not given, set default window length for the mean strategy
-        if self.strategy == "mean" and self.window_length is None:
-            self.window_length_ = len(y_train)
+        else:
+            allowed_strategies = ("last", "mean")
+            raise ValueError(f"Unknown strategy: {self.strategy}. Expected "
+                             f"one of: {allowed_strategies}.")
 
         # check window length
         if self.window_length_ > len(self.oh):
-            param = "sp" if self.strategy == "seasonal_last" else \
-                "window_length_"
+            param = "sp" if self.strategy == "last" and self.sp != 1 \
+                else "window_length_"
             raise ValueError(
                 f"The {param}: {self.window_length_} is larger than "
                 f"the training series.")
@@ -123,21 +125,54 @@ def _predict_last_window(self, fh, X=None, return_pred_int=False,
             return self._predict_nan(fh)
 
         elif self.strategy == "last":
-            return np.repeat(last_window[-1], len(fh))
-
-        elif self.strategy == "seasonal_last":
-            # we need to replicate the last window if max(fh) is larger than
-            # sp,
-            # so that we still make forecasts by repeating the last value
-            # for that season,
-            # assume fh is sorted, i.e. max(fh) == fh[-1]
-            if fh[-1] > self.sp_:
-                reps = np.int(np.ceil(fh[-1] / self.sp_))
-                last_window = np.tile(last_window, reps=reps)
-
-            # get zero-based index by subtracting the minimum
-            fh_idx = fh.index_like(self.cutoff)
-            return last_window[fh_idx]
+            if self.sp == 1:
+                return np.repeat(last_window[-1], len(fh))
+
+            else:
+                # we need to replicate the last window if max(fh) is larger
+                # than sp,so that we still make forecasts by repeating the
+                # last value for that season, assume fh is sorted, i.e. max(
+                # fh) == fh[-1]
+                if fh[-1] > self.sp_:
+                    reps = np.int(np.ceil(fh[-1] / self.sp_))
+                    last_window = np.tile(last_window, reps=reps)
+
+                # get zero-based index by subtracting the minimum
+                fh_idx = fh.index_like(self.cutoff)
+                return last_window[fh_idx]
 
         elif self.strategy == "mean":
-            return np.repeat(np.nanmean(last_window), len(fh))
+            if self.sp == 1:
+                return np.repeat(np.nanmean(last_window), len(fh))
+
+            else:
+                # if the window length is not a multiple of sp, we pad the
+                # window with nan values for easy computation of the mean
+                remainder = self.window_length_ % self.sp_
+                if remainder > 0:
+                    pad_width = self.sp_ - remainder
+                else:
+                    pad_width = 0
+                last_window = np.hstack([last_window,
+                                         np.full(pad_width, np.nan)])
+
+                # reshape last window, one column per season
+                last_window = last_window.reshape(np.int(np.ceil(
+                    self.window_length_ / self.sp_)), self.sp_)
+
+                # compute seasonal mean, averaging over rows
+                y_pred = np.nanmean(last_window, axis=0)
+
+                # we need to replicate the last window if max(fh) is
+                # larger than sp,
+                # so that we still make forecasts by repeating the
+                # last value for that season,
+                # assume fh is sorted, i.e. max(fh) == fh[-1]
+                # only slicing all the last seasons into last_window
+                if fh[-1] > self.sp_:
+                    reps = np.int(np.ceil(fh[-1] / self.sp_))
+                    y_pred = np.tile(y_pred, reps=reps)
+
+                # get zero-based index by subtracting the minimum
+                fh_idx = fh.index_like(self.cutoff)
+                return y_pred[fh_idx]


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
@@ -2,7 +2,7 @@
 # coding: utf-8
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 
-__author__ = "Markus Löning"
+__author__ = ["Markus Löning", "Piyush Gade"]
 
 import numpy as np
 import pandas as pd
@@ -45,17 +45,67 @@ def test_strategy_mean(fh, window_length):
 
 @pytest.mark.parametrize("fh", TEST_OOS_FHS)
 @pytest.mark.parametrize("sp", TEST_SPS)
-def test_strategy_seasonal_last(fh, sp):
-    f = NaiveForecaster(strategy="seasonal_last", sp=sp)
+def test_strategy_last_seasonal(fh, sp):
+    f = NaiveForecaster(strategy="last", sp=sp)
     f.fit(y_train)
     y_pred = f.predict(fh)
 
     # check predicted index
     np.testing.assert_array_equal(y_train.index[-1] + check_fh(fh),
                                   y_pred.index)
-
     # check values
     fh = check_fh(fh)  # get well formatted fh
     reps = np.int(np.ceil(max(fh) / sp))
     expected = np.tile(y_train.iloc[-sp:], reps=reps)[fh - 1]
     np.testing.assert_array_equal(y_pred, expected)
+
+
+@pytest.mark.parametrize("fh", TEST_OOS_FHS)
+@pytest.mark.parametrize("sp", TEST_SPS)
+@pytest.mark.parametrize("window_length", [*TEST_WINDOW_LENGTHS, None])
+def test_strategy_mean_seasonal(fh, sp, window_length):
+    if ((window_length is not None and window_length > sp) or
+            (window_length is None)):
+        f = NaiveForecaster(strategy="mean", sp=sp,
+                            window_length=window_length)
+        f.fit(y_train)
+        y_pred = f.predict(fh)
+
+        # check predicted index
+        np.testing.assert_array_equal(y_train.index[-1] + check_fh(fh),
+                                      y_pred.index)
+
+        if window_length is None:
+            window_length = len(y_train)
+
+        # check values
+        fh = check_fh(fh)  # get well formatted fh
+        reps = np.int(np.ceil(max(fh) / sp))
+        last_window = y_train.iloc[-window_length:].values
+        last_window = np.pad(last_window, (0, sp - len(last_window) % sp),
+                             'constant', constant_values=np.nan)
+
+        last_window = last_window.reshape(np.int(np.ceil(len(last_window) /
+                                                         sp)), sp)
+        expected = np.tile(np.nanmean(last_window, axis=0), reps=reps)[fh - 1]
+        np.testing.assert_array_equal(y_pred, expected)
+
+
+@pytest.mark.parametrize("n_seasons", [1, 3])
+@pytest.mark.parametrize("sp", TEST_SPS)
+def test_strategy_mean_seasonal_simple(n_seasons, sp):
+
+    # create 2d matrix, rows are different seasons, columns time points of
+    # each season
+    values = np.random.normal(size=(n_seasons, sp))
+    y = pd.Series(values.ravel())
+
+    expected = values.mean(axis=0)
+    assert expected.shape == (sp,)
+
+    f = NaiveForecaster(strategy="mean", sp=sp)
+    f.fit(y)
+    fh = np.arange(1, sp + 1)
+    y_pred = f.predict(fh)
+
+    np.testing.assert_array_equal(y_pred, expected)