Repository: sktime/sktime

Issue:
[BUG] `SlidingWindowSplitter` and  `ExpandingWindowSplitter` only returns a single item for test split
**Describe the bug**
When splitting a dataset based on `SlidingWindowSplitter` or `ExpandingWindowSplitter`, the test set for splits has only the last test index instead of an array of indices. The train set is as expected. Check below for a sample output of the issue.

**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.model_selection import SlidingWindowSplitter, ExpandingWindowSplitter

df = pd.DataFrame(list(range(0, 50)))

sliding_splitter = SlidingWindowSplitter(fh=5, window_length=10, step_length=1)
s = list(sliding_splitter.split(df))
print(f"First split of SlidingWindowSplitter: {s[0]}")

expanding_splitter = ExpandingWindowSplitter(fh=5, initial_window=5, step_length=2)
ss = list(expanding_splitter.split(df))
print(f"First split of ExpandingWindowSplitter: {ss[0]}")
```
Above will result in 
```
>>> First split of SlidingWindowSplitter: (array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), array([14]))
>>> First split of ExpandingWindowSplitter: (array([0, 1, 2, 3, 4]), array([9]))
```

**Expected behavior**
For `SlidingWindowSplitter` the first split based on give arguments must be 
`(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), array([10, 11, 12, 13, 14])`

For `ExpandingWindowSplitter` the first split based on give arguments must be 
`(array([0, 1, 2, 3, 4]), array([5, 6, 7, 8, 9]))`

**Additional context**
<!--
Add any other context about the problem here.
-->

**Versions**
<details>

System:
    python: 3.8.13 (default, Mar 28 2022, 11:38:47)  [GCC 7.5.0]
executable: <removed>
   machine: Linux-5.10.60.1-microsoft-standard-WSL2-x86_64-with-glibc2.17

Python dependencies:
          pip: 22.1.2
   setuptools: 61.2.0
      sklearn: 1.1.1
       sktime: 0.12.1
  statsmodels: 0.13.2
        numpy: 1.22.4
        scipy: 1.8.1
       pandas: 1.4.3
   matplotlib: 3.5.2
       joblib: 1.1.0
        numba: 0.55.2
     pmdarima: 1.8.5
      tsfresh: 0.19.0
</details>

<!-- Thanks for contributing! -->



Discussion:
@e-alizadeh Thanks a lot for reporting this! Actually, I wouldn't say this is a bug, but rather a badly documented feature. Test window will include only those observations that are explicitly listed in the forecasting horizon. The desired result is achieved by providing `np.arange(1, 6)` to the `fh` argument:
```
import numpy as np
import pandas as pd
from sktime.forecasting.model_selection import SlidingWindowSplitter, ExpandingWindowSplitter

df = pd.DataFrame(list(range(0, 50)))

sliding_splitter = SlidingWindowSplitter(fh=np.arange(1, 6), window_length=10, step_length=1)
s = list(sliding_splitter.split(df))
print(f"First split of SlidingWindowSplitter: {s[0]}")

expanding_splitter = ExpandingWindowSplitter(fh=np.arange(1, 6), initial_window=5, step_length=2)
ss = list(expanding_splitter.split(df))
print(f"First split of ExpandingWindowSplitter: {ss[0]}")
```
I will try to add a few lines to the docstring of the splitters in the coming days. I hope this would be sufficient. Changing splitters behaviour to expand test window up to the maximum forecasting horizon is less explicit and therefore not a very good idea. What do you think?
Possible alternative approach:
adding an argument `forecast_window_length`, where `forecast_window_length=x` is equivalent to passing `fh=np.range(1, x+1)`, and not both arguments `fh` and `forecast_window_length` can be passed (exactly one must be given)

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
@@ -297,7 +297,7 @@ class BaseSplitter(BaseObject):
     and :math:`t(k)<t(k+1)` are ordered time indices.
     The exact set of indices depends on a concrete splitter.
     Method `.split` is used to generate a pair of index sets:
-    train :math:`\{t(1),\ldots,t(k)\}` and test :math:`\{t(k+1),\ldots,t(k+l)\}`.
+    train :math:`(t(1),\ldots,t(k))` and test :math:`(t(k+1),\ldots,t(k+l))`.
 
     In case `window_length` and `fh` are integer valued,
     they translate into :math:`k` and :math:`l`, respectively.
@@ -671,16 +671,22 @@ class CutoffSplitter(BaseSplitter):
 
     Here the user is expected to provide a set of cutoffs (train set endpoints),
     which using the notation provided in :class:`BaseSplitter`,
-    can be written as :math:`\{k_1,\ldots,k_n\}` for integer based indexing,
-    or :math:`\{t(k_1),\ldots,t(k_n)\}` for datetime based indexing.
-    Training window's last point is equal to the cutoff,
-    while test window starts from the next observation in `y`.
+    can be written as :math:`(k_1,\ldots,k_n)` for integer based indexing,
+    or :math:`(t(k_1),\ldots,t(k_n))` for datetime based indexing.
+    Training window's last point is equal to the cutoff.
+
+    Test window is defined by forecasting horizons
+    relative to the end of the training window.
+    It will contain as many indices
+    as there are forecasting horizons provided to the `fh` argument.
+    For a forecasating horizon :math:`(h_1,\ldots,h_H)`, the training window will
+    consist of the indices :math:`(k_n+h_1,\ldots, k_n+h_H)`.
 
     The number of splits returned by `.get_n_splits`
     is then trivially equal to :math:`n`.
 
     The sorted array of cutoffs returned by `.get_cutoffs` is then equal to
-    :math:`\{t(k_1),\ldots,t(k_n)\}` with :math:`k_i<k_{i+1}`.
+    :math:(t(k_1),\ldots,t(k_n))` with :math:`k_i<k_{i+1}`.
 
     Parameters
     ----------
@@ -690,6 +696,15 @@ class CutoffSplitter(BaseSplitter):
     fh : int, timedelta, list or np.ndarray of ints or timedeltas
         Type should match the type of `cutoffs` input.
     window_length : int or timedelta or pd.DateOffset
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sktime.forecasting.model_selection import CutoffSplitter
+    >>> ts = np.arange(10)
+    >>> splitter = CutoffSplitter(fh=[2, 4], cutoffs=np.array([3, 5]), window_length=3)
+    >>> list(splitter.split(ts)) # doctest: +SKIP
+    [(array([1, 2, 3]), array([5, 7])), (array([3, 4, 5]), array([7, 9]))]
     """
 
     def __init__(
@@ -1030,11 +1045,18 @@ def get_cutoffs(self, y: Optional[ACCEPTED_Y_TYPES] = None) -> np.ndarray:
 
 
 class SlidingWindowSplitter(BaseWindowSplitter):
-    """Sliding window splitter.
+    r"""Sliding window splitter.
 
     Split time series repeatedly into a fixed-length training and test set.
 
-    For example for `window_length = 5`, `step_length = 1` and `fh = 3`
+    Test window is defined by forecasting horizons
+    relative to the end of the training window.
+    It will contain as many indices
+    as there are forecasting horizons provided to the `fh` argument.
+    For a forecasating horizon :math:`(h_1,\ldots,h_H)`, the training window will
+    consist of the indices :math:`(k_n+h_1,\ldots,k_n+h_H)`.
+
+    For example for `window_length = 5`, `step_length = 1` and `fh = [1, 2, 3]`
     here is a representation of the folds::
 
     |-----------------------|
@@ -1043,7 +1065,6 @@ class SlidingWindowSplitter(BaseWindowSplitter):
     | - - * * * * * x x x - |
     | - - - * * * * * x x x |
 
-
     ``*`` = training fold.
 
     ``x`` = test fold.
@@ -1061,6 +1082,16 @@ class SlidingWindowSplitter(BaseWindowSplitter):
     start_with_window : bool, optional (default=False)
         - If True, starts with full window.
         - If False, starts with empty window.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sktime.forecasting.model_selection import SlidingWindowSplitter
+    >>> ts = np.arange(10)
+    >>> splitter = SlidingWindowSplitter(fh=[2, 4], window_length=3, step_length=2)
+    >>> list(splitter.split(ts)) # doctest: +SKIP
+    [(array([0, 1, 2]), array([4, 6])), (array([2, 3, 4]), array([6, 8]))]
+
     """
 
     def __init__(
@@ -1084,11 +1115,18 @@ def _split_windows(self, **kwargs) -> SPLIT_GENERATOR_TYPE:
 
 
 class ExpandingWindowSplitter(BaseWindowSplitter):
-    """Expanding window splitter.
+    r"""Expanding window splitter.
 
     Split time series repeatedly into an growing training set and a fixed-size test set.
 
-    For example for `initial_window = 5`, `step_length = 1` and `fh = 3`
+    Test window is defined by forecasting horizons
+    relative to the end of the training window.
+    It will contain as many indices
+    as there are forecasting horizons provided to the `fh` argument.
+    For a forecasating horizon :math:`(h_1,\ldots,h_H)`, the training window will
+    consist of the indices :math:`(k_n+h_1,\ldots,k_n+h_H)`.
+
+    For example for `initial_window = 5`, `step_length = 1` and `fh = [1, 2, 3]`
     here is a representation of the folds::
 
     |-----------------------|
@@ -1113,6 +1151,16 @@ class ExpandingWindowSplitter(BaseWindowSplitter):
     start_with_window : bool, optional (default=True)
         - If True, starts with full window.
         - If False, starts with empty window.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sktime.forecasting.model_selection import ExpandingWindowSplitter
+    >>> ts = np.arange(10)
+    >>> splitter = ExpandingWindowSplitter(fh=[2, 4], initial_window=5, step_length=2)
+    >>> list(splitter.split(ts)) # doctest: +SKIP
+    '[(array([0, 1, 2, 3, 4]), array([6, 8]))]'
+
     """
 
     def __init__(
@@ -1138,17 +1186,34 @@ def _split_windows(self, **kwargs) -> SPLIT_GENERATOR_TYPE:
 
 
 class SingleWindowSplitter(BaseSplitter):
-    """Single window splitter.
+    r"""Single window splitter.
 
     Split time series once into a training and test set.
     See more details on what to expect from this splitter in :class:`BaseSplitter`.
 
+    Test window is defined by forecasting horizons
+    relative to the end of the training window.
+    It will contain as many indices
+    as there are forecasting horizons provided to the `fh` argument.
+    For a forecasating horizon :math:`(h_1,\ldots,h_H)`, the training window will
+    consist of the indices :math:`(k_n+h_1,\ldots,k_n+h_H)`.
+
     Parameters
     ----------
     fh : int, list or np.array
         Forecasting horizon
     window_length : int or timedelta or pd.DateOffset
         Window length
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sktime.forecasting.model_selection import SingleWindowSplitter
+    >>> ts = np.arange(10)
+    >>> splitter = SingleWindowSplitter(fh=[2, 4], window_length=3)
+    >>> list(splitter.split(ts)) # doctest: +SKIP
+    [(array([3, 4, 5]), array([7, 9]))]
+
     """
 
     def __init__(


Test patch: