Repository: sktime/sktime

Issue:
[BUG] get_n_splits of window splitter does not reflect the number of splits for hierarchical data
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->

`get_n_splits` method returns the wrong number of splits when hierarchical data is passed into window based splitter. 

**To Reproduce**

```python
from sktime.utils._testing.hierarchical import _make_hierarchical
from sktime.forecasting.model_selection import ExpandingWindowSplitter, SlidingWindowSplitter

y_hierarchical = _make_hierarchical(hierarchy_levels=(2, 3), min_timepoints=12, max_timepoints=12, random_state=0)
fh = [1]
step_length = 1
window_length = 10

# each unique series (lowest hierachical level) should have two splits
cv = ExpandingWindowSplitter(initial_window=window_length, fh=fh, step_length=step_length)
sliding = SlidingWindowSplitter(fh=fh, window_length=window_length, step_length=step_length, start_with_window=True)

n_splits_cv = cv.get_n_splits(y_hierarchical)
n_splits_sliding = sliding.get_n_splits(y_hierarchical)

print(f"Expanding Window splits : {n_splits_cv}")
print(f"Sliding Window Number of splits : {n_splits_sliding}")
```

Output
```
Expanding Window Number of splits : 62
Sliding Window Number of splits : 62
```
**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
 
Shouldnt the number of split is based on either of these two?:
1. Return number of splits based on datatype train/test splits which is two in this case
```python
print(f"Expanding Window splits: {len(list(cv.split(y_hierarchical)))}")
print(f"Sliding Window splits: {len(list(sliding.split(y_hierarchical)))}")
```
2. Return number of splits based on splits of the lowest hierachical level series. In this case it will be 6 * 2 = 12

**Versions**
<details>

System:
python: 3.9.17 (main, Jul  5 2023, 20:47:11) [MSC v.1916 64 bit (AMD64)]
machine: Windows-10-10.0.19045-SP0

Python dependencies:
pip: 23.2.1
sktime: 0.21.0
sklearn: 1.3.0
skbase: 0.4.6
numpy: 1.24.3
scipy: 1.11.1
pandas: 2.0.3
matplotlib: 3.7.2
joblib: 1.3.1
statsmodels: 0.14.0
numba: 0.57.1
pmdarima: 2.0.3
tsfresh: 0.20.1
tensorflow: 2.13.0
tensorflow_probability: None

</details>


Comments:

Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 3441d6560..ed91edebc 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1740,6 +1740,7 @@
       "name": "Benedikt Heidrich",
       "profile": "https://github.com/benheid",
       "contributions": [
+        "bug",
         "code"
       ]
     },
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ad94a9858..238a9a1e6 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -50,7 +50,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
     <tr>
       <td align="center" valign="top" width="11.11%"><a href="https://github.com/BandaSaiTejaReddy"><img src="https://avatars0.githubusercontent.com/u/31387911?v=4?s=100" width="100px;" alt="BANDASAITEJAREDDY"/><br /><sub><b>BANDASAITEJAREDDY</b></sub></a><br /><a href="https://github.com/sktime/sktime/commits?author=BandaSaiTejaReddy" title="Code">💻</a> <a href="https://github.com/sktime/sktime/commits?author=BandaSaiTejaReddy" title="Documentation">📖</a></td>
       <td align="center" valign="top" width="11.11%"><a href="https://github.com/badrmarani"><img src="https://avatars.githubusercontent.com/badrmarani?s=100" width="100px;" alt="Badr-Eddine Marani"/><br /><sub><b>Badr-Eddine Marani</b></sub></a><br /><a href="https://github.com/sktime/sktime/commits?author=badrmarani" title="Code">💻</a></td>
-      <td align="center" valign="top" width="11.11%"><a href="https://github.com/benheid"><img src="?s=100" width="100px;" alt="Benedikt Heidrich"/><br /><sub><b>Benedikt Heidrich</b></sub></a><br /><a href="https://github.com/sktime/sktime/commits?author=benheid" title="Code">💻</a></td>
+      <td align="center" valign="top" width="11.11%"><a href="https://github.com/benheid"><img src="?s=100" width="100px;" alt="Benedikt Heidrich"/><br /><sub><b>Benedikt Heidrich</b></sub></a><br /><a href="https://github.com/sktime/sktime/issues?q=author%3Abenheid" title="Bug reports">🐛</a> <a href="https://github.com/sktime/sktime/commits?author=benheid" title="Code">💻</a></td>
       <td align="center" valign="top" width="11.11%"><a href="https://github.com/benjaminbluhm"><img src="?s=100" width="100px;" alt="Benjamin Bluhm"/><br /><sub><b>Benjamin Bluhm</b></sub></a><br /><a href="https://github.com/sktime/sktime/commits?author=benjaminbluhm" title="Code">💻</a> <a href="https://github.com/sktime/sktime/commits?author=benjaminbluhm" title="Documentation">📖</a> <a href="#example-benjaminbluhm" title="Examples">💡</a></td>
       <td align="center" valign="top" width="11.11%"><a href="https://haskarb.github.io/"><img src="https://avatars.githubusercontent.com/u/20501023?v=4?s=100" width="100px;" alt="Bhaskar Dhariyal"/><br /><sub><b>Bhaskar Dhariyal</b></sub></a><br /><a href="https://github.com/sktime/sktime/commits?author=haskarb" title="Code">💻</a> <a href="https://github.com/sktime/sktime/commits?author=haskarb" title="Tests">⚠️</a></td>
       <td align="center" valign="top" width="11.11%"><a href="https://github.com/BINAYKUMAR943"><img src="https://avatars.githubusercontent.com/u/38756834?v=4?s=100" width="100px;" alt="Binay Kumar"/><br /><sub><b>Binay Kumar</b></sub></a><br /><a href="https://github.com/sktime/sktime/commits?author=BINAYKUMAR943" title="Code">💻</a> <a href="https://github.com/sktime/sktime/commits?author=BINAYKUMAR943" title="Documentation">📖</a> <a href="https://github.com/sktime/sktime/commits?author=BINAYKUMAR943" title="Tests">⚠️</a></td>
diff --git a/sktime/forecasting/model_selection/_split.py b/sktime/forecasting/model_selection/_split.py
index 9fca2a4da..81f7c2907 100644
--- a/sktime/forecasting/model_selection/_split.py
+++ b/sktime/forecasting/model_selection/_split.py
@@ -1089,12 +1089,38 @@ class BaseWindowSplitter(BaseSplitter):
         n_splits : int
             The number of splits.
         """
+        from sktime.datatypes import check_is_scitype, convert
+
         if y is None:
             raise ValueError(
                 f"{self.__class__.__name__} requires `y` to compute the "
                 f"number of splits."
             )
-        return len(self.get_cutoffs(y))
+
+        multi_scitypes = ["Hierarchical", "Panel"]
+        is_non_single, _, metadata = check_is_scitype(y, multi_scitypes, [])
+
+        # n_splits based on the first instance of the lowest level series cutoffs
+        if is_non_single:
+            from_mtype = metadata.get("mtype")
+            scitype = metadata.get("scitype")
+            if scitype == "Panel":
+                to_mtype = "pd-multiindex"
+            else:
+                to_mtype = "pd_multiindex_hier"
+
+            y = convert(y, from_type=from_mtype, to_type=to_mtype, as_scitype=scitype)
+
+            index = self._coerce_to_index(y)
+            for _, values in y.groupby(index.droplevel(-1)):
+                # convert to a single ts
+                instance_series = values.reset_index().iloc[:, -2:]
+                instance_series.set_index(instance_series.columns[0], inplace=True)
+                n_splits = len(self.get_cutoffs(instance_series))
+                break
+        else:
+            n_splits = len(self.get_cutoffs(y))
+        return n_splits
 
     def get_cutoffs(self, y: Optional[ACCEPTED_Y_TYPES] = None) -> np.ndarray:
         """Return the cutoff points in .iloc[] context.
diff --git a/sktime/transformations/series/subset.py b/sktime/transformations/series/subset.py
index a578e966d..d82b6a0ee 100644
--- a/sktime/transformations/series/subset.py
+++ b/sktime/transformations/series/subset.py
@@ -160,6 +160,7 @@ class ColumnSelect(BaseTransformer):
         "fit_is_empty": True,
         "univariate-only": False,
         "capability:inverse_transform": False,
+        "skip-inverse-transform": True,
     }
 
     def __init__(self, columns=None, integer_treatment="col", index_treatment="remove"):


Test patch:
diff --git a/sktime/forecasting/model_selection/tests/test_split.py b/sktime/forecasting/model_selection/tests/test_split.py
index 82f1e03c4..12e9955fc 100644
--- a/sktime/forecasting/model_selection/tests/test_split.py
+++ b/sktime/forecasting/model_selection/tests/test_split.py
@@ -32,6 +32,7 @@ from sktime.forecasting.tests._config import (
 )
 from sktime.utils._testing.forecasting import _make_fh
 from sktime.utils._testing.hierarchical import _make_hierarchical
+from sktime.utils._testing.panel import _make_panel
 from sktime.utils._testing.series import _make_series
 from sktime.utils.datetime import _coerce_duration_to_int
 from sktime.utils.validation import (
@@ -43,6 +44,10 @@ from sktime.utils.validation import (
 from sktime.utils.validation.forecasting import check_fh
 
 N_TIMEPOINTS = 30
+TEST_Y_PANEL_HIERARCHICAL = [
+    _make_hierarchical((2, 2), N_TIMEPOINTS, N_TIMEPOINTS),
+    _make_panel(n_instances=2, n_timepoints=N_TIMEPOINTS),
+]
 
 
 def _get_windows(cv, y):
@@ -722,3 +727,32 @@ def test_hierachical_singlewindowsplitter():
     splitter = SingleWindowSplitter(fh=[1, 2], window_length=10)
     splits = list(splitter.split(y))
     assert len(splits) == 1, "Should only be one split"
+
+
+@pytest.mark.parametrize("CV", [SlidingWindowSplitter, ExpandingWindowSplitter])
+@pytest.mark.parametrize("fh", [*TEST_FHS, *TEST_FHS_TIMEDELTA])
+@pytest.mark.parametrize("window_length", TEST_WINDOW_LENGTHS)
+@pytest.mark.parametrize("step_length", TEST_STEP_LENGTHS)
+def test_windowbase_splitter_get_n_split_hierarchical(
+    CV, fh, window_length, step_length
+):
+    """Test that WindowBaseSplitter.get_n_splits works for hierarchical data."""
+    # see bugs 4971
+    y = TEST_Y_PANEL_HIERARCHICAL[0]  # hierachical data
+    if _inputs_are_supported([fh, window_length, step_length]):
+        cv = CV(fh, window_length, step_length)
+        assert cv.get_n_splits(y) == len(
+            list(cv.split(y))
+        ), "get_n_splits does not equal the number of splits in the output."
+
+
+@pytest.mark.parametrize("y", TEST_Y_PANEL_HIERARCHICAL)
+@pytest.mark.parametrize("CV", [SlidingWindowSplitter, ExpandingWindowSplitter])
+def test_windowbase_splitter_get_n_split_unequal_series(y, CV):
+    y_unequal = y.copy()  # avoid changing original dataset
+    y_unequal.iloc[:3, :] = None  # make the first series shorter than the rest
+    y_unequal.dropna(inplace=True)
+    cv = CV([1], 24, 1)
+    assert cv.get_n_splits(y_unequal) == len(
+        list(cv.split(y_unequal))
+    ), "get_n_splits does not equal the number of splits in the output."
diff --git a/sktime/transformations/series/tests/test_subset.py b/sktime/transformations/series/tests/test_subset.py
index 2137b95e5..f400a5794 100644
--- a/sktime/transformations/series/tests/test_subset.py
+++ b/sktime/transformations/series/tests/test_subset.py
@@ -7,6 +7,7 @@ import pandas as pd
 import pytest
 
 from sktime.datasets import load_airline, load_longley
+from sktime.forecasting.naive import NaiveForecaster
 from sktime.transformations.series.subset import ColumnSelect, IndexSubset
 
 
@@ -49,3 +50,10 @@ def test_columnselect_int():
     X_subset = transformer.fit_transform(X=X)
 
     assert X_subset.columns.equals(X.columns[[0, 2, 4]])
+
+
+def test_columnselect_as_first_step_in_transformedtargetforecaster():
+    y = load_longley()[1][["GNP", "UNEMP"]]
+    fc = ColumnSelect(["GNP"]) * NaiveForecaster()
+    fc.fit(y)
+    fc.predict(fh=[1])