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
@@ -23,7 +23,7 @@ class NaiveForecaster(OptionalForecastingHorizonMixin,
 
     Parameters
     ----------
-    strategy : str{"last", "mean"}, optional (default="last")
+    strategy : str{"last", "mean", "drift"}, optional (default="last")
         Strategy used to make forecasts:
 
         * "last" : forecast the last value in the
@@ -37,6 +37,9 @@ class NaiveForecaster(OptionalForecastingHorizonMixin,
                      When sp is not 1, mean of all values
                      in a season from last window will be
                      forecasted for each season.
+        * "drift": forecast by fitting a line between the
+                    first and last point of the window and
+                     extrapolating it into the future
 
     sp : int, optional (default=1)
         Seasonal periodicity to use in the seasonal forecasting.
@@ -99,8 +102,22 @@ def fit(self, y_train, fh=None, X_train=None):
             if self.window_length is None:
                 self.window_length_ = len(y_train)
 
+        elif self.strategy == "drift":
+            if self.sp != 1:
+                warn("For the `drift` strategy, "
+                     "the `sp` value will be ignored.")
+            # window length we need for forecasts is just the
+            # length of seasonal periodicity
+            self.window_length_ = check_window_length(self.window_length)
+            if self.window_length is None:
+                self.window_length_ = len(y_train)
+            if self.window_length == 1:
+                raise ValueError(f"For the `drift` strategy, "
+                                 f"the `window_length`: {self.window_length} "
+                                 f"value must be greater than one.")
+
         else:
-            allowed_strategies = ("last", "mean")
+            allowed_strategies = ("last", "mean", "drift")
             raise ValueError(f"Unknown strategy: {self.strategy}. Expected "
                              f"one of: {allowed_strategies}.")
 
@@ -176,3 +193,22 @@ def _predict_last_window(self, fh, X=None, return_pred_int=False,
                 # get zero-based index by subtracting the minimum
                 fh_idx = fh.index_like(self.cutoff)
                 return y_pred[fh_idx]
+
+        # if self.strategy == "drift":
+        else:
+            if self.window_length_ != 1:
+                if np.any(np.isnan(last_window[[0, -1]])):
+                    raise ValueError(f"For {self.strategy},"
+                                     f"first and last elements in the last "
+                                     f"window must not be a missing value.")
+                else:
+                    # formula for slope
+                    slope = (last_window[-1] -
+                             last_window[0]) / (self.window_length_ - 1)
+
+                    # get zero-based index by subtracting the minimum
+                    fh_idx = fh.index_like(self.cutoff)
+
+                    # linear extrapolation
+                    y_pred = last_window[-1] + (fh_idx + 1) * slope
+                    return y_pred


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
@@ -109,3 +109,72 @@ def test_strategy_mean_seasonal_simple(n_seasons, sp):
     y_pred = f.predict(fh)
 
     np.testing.assert_array_equal(y_pred, expected)
+
+
+@pytest.mark.parametrize("fh", TEST_OOS_FHS)
+@pytest.mark.parametrize("window_length",
+                         [*TEST_WINDOW_LENGTHS, None])
+def test_strategy_drift_unit_slope(fh, window_length):
+    # drift strategy for constant slope 1
+    if window_length != 1:
+        f = NaiveForecaster(strategy="drift",
+                            window_length=window_length)
+        f.fit(y_train)
+        y_pred = f.predict(fh)
+
+        if window_length is None:
+            window_length = len(y_train)
+
+        # get well formatted fh values
+        fh = check_fh(fh)
+
+        expected = y_train.iloc[-1] + np.arange(0, max(fh) + 1)[fh]
+        np.testing.assert_array_equal(y_pred, expected)
+
+
+@pytest.mark.parametrize("fh", TEST_OOS_FHS)
+@pytest.mark.parametrize("window_length",
+                         [*TEST_WINDOW_LENGTHS, None])
+def test_strategy_drift_flat_line(fh, window_length):
+    # test for flat time series data
+    if window_length != 1:
+        y_train = pd.Series(np.ones(20))
+        f = NaiveForecaster(strategy="drift",
+                            window_length=window_length)
+        f.fit(y_train)
+        y_pred = f.predict(fh)
+
+        if window_length is None:
+            window_length = len(y_train)
+
+        # get well formatted fh values
+        fh = check_fh(fh)
+        expected = np.ones(len(fh))
+
+        np.testing.assert_array_equal(y_pred, expected)
+
+
+@pytest.mark.parametrize("fh", TEST_OOS_FHS)
+@pytest.mark.parametrize("window_length",
+                         [*TEST_WINDOW_LENGTHS, None])
+def test_strategy_drift_window_length(fh, window_length):
+    # test for checking if window_length is properly working
+    if window_length != 1:
+        if window_length is None:
+            window_length = len(y_train)
+
+        values = np.random.normal(size=window_length)
+        y = pd.Series(values)
+        f = NaiveForecaster(strategy="drift",
+                            window_length=window_length)
+        f.fit(y)
+        y_pred = f.predict(fh)
+
+        slope = (values[-1] -
+                 values[0]) / (window_length - 1)
+
+        # get well formatted fh values
+        fh = check_fh(fh)
+        expected = values[-1] + slope * fh
+
+        np.testing.assert_array_equal(y_pred, expected)