Repository: sktime/sktime

Issue:
[ENH] removing redundant parameter `start_with_window` in `ExpandingWindowSplitter`
The parameter `start_with_window` in `ExpandingWindowSplitter` is redundant with `initial_window`.

If `start_with_window` is `False` (a non-default), the `ExpandingWindowSplitter` behaves as if `initial_window` were zero.
However, `initial_window` cannot be set to zero.

I feel the above choice contributes to user confusion.

I think `start_with_window` should just be removed, and `initial_window=0` should be possible, mapping on the prior behaviour of `start_with_window=False`.

The change needs to be accompanied by deprecation, as follows:

* `initial_window=0` should become possible
* `start_with_window` is retained, but a value of `False` raises a deprecation message along the lines of "please use `initial_window=0` instead"

For deprecation conventions in `sktime`, see this guide:
https://www.sktime.org/en/stable/developer_guide/deprecation.html


Discussion:
FYI @keepersas
@fkiraly I would be able to take a look at this issue if you still need the assistance.
much appreciated, @mojo-py!
All yours!
@fkiraly I'm taking this on and already have code that may be working as planned. My questions before I submit the PR:
1. Do we also want to remove `start_with_window` for `SlidingWindowSplitter`? The design choice of allowing `initial_window=0` replace `start_with_window` would seem the same to me but slightly different implementations.
2. If we do want to remove this for `SlidingWindowSplitter`, what is the expected behavior for the params `initial_window=0`, `window_length=4`, and `step_size=2`? The first training window would be `[]`. Would the second follow the logic of `step_size` and contain `[0,1]` (where the end of the training set took a step of 2 past the last one), or would it always obey `window_length` and have a next two training sets of `[0,1,2,3]` and `[2,3,4,5]` (where the end of the training window went up by more than `step_size`)?

After the initial empty window we can only respect one of `step_size` or `window_length` in this case.
> 1. Do we also want to remove `start_with_window` for `SlidingWindowSplitter`? The design choice of allowing `initial_window=0` replace `start_with_window` would seem the same to me but slightly different implementations.

Hm, not sure what that would be.
What is the current behaviour if `start_with_window` is `False`?

Either case, I would say this should be a separate PR/issue.
> What is the current behaviour if start_with_window is False?

The current implementation respects the `step_length`, because the internal logic increments the end of the window by that length to create each new window.
```
splitter = SlidingWindowSplitter(fh=[1,2], step_length=2, window_length=4, start_with_window=False)
list(splitter.split(ts))

out >
[(array([], dtype=int64), array([0, 1])),
 (array([0, 1]), array([2, 3])),
 (array([0, 1, 2, 3]), array([4, 5])),
 (array([2, 3, 4, 5]), array([6, 7])),
 (array([4, 5, 6, 7]), array([8, 9]))]
```

> Either case, I would say this should be a separate PR/issue.

The changes I've made for this issue in `BaseWindowSplitter` would change the design for both subclasses (to allowing `initial_window=0`) and would keep the behavior described above the same for the `SlidingWindowSplitter` (respecting `step_length`. In this case could this just be one PR?
Also @fkiraly could you point me to an example of correct usage for `deprecated` when it is just for a single parameter or piece of functionality and not a whole method, function, or class like in the above document? That could also be a potential opportunity for improvement in [the doc](https://www.sktime.org/en/stable/developer_guide/deprecation.html) for next time somebody has the same question.

Resolving patch:
diff --git a/sktime/forecasting/model_selection/_split.py b/sktime/forecasting/model_selection/_split.py
--- a/sktime/forecasting/model_selection/_split.py
+++ b/sktime/forecasting/model_selection/_split.py
@@ -10,9 +10,10 @@
     "SingleWindowSplitter",
     "temporal_train_test_split",
 ]
-__author__ = ["mloning", "kkoralturk", "khrapovs"]
+__author__ = ["mloning", "kkoralturk", "khrapovs", "chillerobscuro"]
 
 from typing import Iterator, Optional, Tuple, Union
+from warnings import warn
 
 import numpy as np
 import pandas as pd
@@ -986,7 +987,7 @@ def _get_start(self, y: pd.Index, fh: ForecastingHorizon) -> int:
         # length.
         if hasattr(self, "start_with_window") and self.start_with_window:
 
-            if self._initial_window is not None:
+            if self._initial_window not in [None, 0]:
 
                 if is_timedelta_or_date_offset(x=self._initial_window):
                     start = y.get_loc(
@@ -1199,7 +1200,7 @@ def __init__(
         fh: FORECASTING_HORIZON_TYPES = DEFAULT_FH,
         initial_window: ACCEPTED_WINDOW_LENGTH_TYPES = DEFAULT_WINDOW_LENGTH,
         step_length: NON_FLOAT_WINDOW_LENGTH_TYPES = DEFAULT_STEP_LENGTH,
-        start_with_window: bool = True,
+        start_with_window: bool = True,  # TODO: remove 0.15.0
     ) -> None:
         # Note that we pass the initial window as the window_length below. This
         # allows us to use the common logic from the parent class, while at the same
@@ -1209,9 +1210,15 @@ def __init__(
             window_length=initial_window,
             initial_window=None,
             step_length=step_length,
-            start_with_window=start_with_window,
+            start_with_window=start_with_window,  # TODO: remove 0.15.0
         )
 
+        if not start_with_window:  # TODO: remove 0.15.0
+            warn(
+                '"start_with_window" will be depreciated in 0.15.0, '
+                "use initial_window=0 instead"
+            )
+
         # initial_window needs to be written to self for sklearn compatibility
         self.initial_window = initial_window
         # this class still acts as if it were overwritten with None,
diff --git a/sktime/utils/validation/__init__.py b/sktime/utils/validation/__init__.py
--- a/sktime/utils/validation/__init__.py
+++ b/sktime/utils/validation/__init__.py
@@ -178,7 +178,7 @@ def check_window_length(
     if window_length is None:
         return window_length
 
-    elif is_int(window_length) and window_length >= 1:
+    elif is_int(window_length) and window_length >= 0:
         return window_length
 
     elif is_float(window_length) and 0 < window_length < 1:
@@ -202,6 +202,6 @@ def check_window_length(
 
     else:
         raise ValueError(
-            f"`{name}` must be a positive integer >= 1, or"
+            f"`{name}` must be a positive integer >= 0, or "
             f"float in (0, 1) or None, but found: {window_length}."
         )


Test patch:
diff --git a/sktime/forecasting/model_selection/tests/test_split.py b/sktime/forecasting/model_selection/tests/test_split.py
--- a/sktime/forecasting/model_selection/tests/test_split.py
+++ b/sktime/forecasting/model_selection/tests/test_split.py
@@ -325,6 +325,31 @@ def test_sliding_window_splitter_start_with_empty_window(
             )
 
 
+@pytest.mark.parametrize("y", TEST_YS)
+@pytest.mark.parametrize("fh", [*TEST_FHS, *TEST_FHS_TIMEDELTA])
+@pytest.mark.parametrize("step_length", TEST_STEP_LENGTHS)
+def test_expanding_window_splitter_start_with_initial_window_zero(y, fh, step_length):
+    """Test ExpandingWindowSplitter."""
+    initial_window = 0
+    if _inputs_are_supported([fh, step_length, initial_window]):
+        cv = ExpandingWindowSplitter(
+            fh=fh,
+            step_length=step_length,
+            initial_window=initial_window,
+        )
+        train_windows, test_windows, _, n_splits = _check_cv(
+            cv, y, allow_empty_window=True
+        )
+
+        assert np.vstack(test_windows).shape == (n_splits, len(check_fh(fh)))
+    else:
+        match = "Unsupported combination of types"
+        with pytest.raises(TypeError, match=match):
+            ExpandingWindowSplitter(
+                fh=fh, initial_window=initial_window, step_length=step_length
+            )
+
+
 def test_sliding_window_splitter_initial_window_start_with_empty_window_raises_error():
     """Test SlidingWindowSplitter."""
     y = _make_series()