Repository: sktime/sktime
Pull request #7550 (2024-12-20 21:26:13)

[DOC] Update the docstring of sameloc splitter with a proper mathematical description
#### Reference Issues/PRs

#7196 

#### What does this implement/fix? Explain your changes.

Rewrite the docstring using a more precise mathematical description.

#### Does your contribution introduce a new dependency? If yes, which one?

No

#### What should a reviewer concentrate their feedback on?

- The documentation is correct
- Correctly explained all the points asked by the Issue #7196 

#### Did you add any tests for the change?
No.

#### Any other comments?

#### PR checklist

##### For all contributions
- [ ] I've added myself to the [list of contributors](https://github.com/sktime/sktime/blob/main/CONTRIBUTORS.md) with any new badges I've earned :-)
  How to: add yourself to the [all-contributors file](https://github.com/sktime/sktime/blob/main/.all-contributorsrc) in the `sktime` root directory (not the `CONTRIBUTORS.md`). Common badges: `code` - fixing a bug, or adding code logic. `doc` - writing or improving documentation or docstrings. `bug` - reporting or diagnosing a bug (get this plus `code` if you also fixed the bug in the PR).`maintenance` - CI, test framework, release.
  See here for [full badge reference](https://allcontributors.org/docs/en/emoji-key)
- [ ] Optionally, for added estimators: I've added myself and possibly to the `maintainers` tag - do this if you want to become the owner or maintainer of an estimator you added.
  See here for further details on the [algorithm maintainer role](https://www.sktime.net/en/latest/get_involved/governance.html#algorithm-maintainers).
- [x] The PR title starts with either [ENH], [MNT], [DOC], or [BUG]. [BUG] - bugfix, [MNT] - CI, test framework, [ENH] - adding or improving code, [DOC] - writing or improving documentation or docstrings.

##### For new estimators
- [ ] I've added the estimator to the API reference - in `docs/source/api_reference/taskname.rst`, follow the pattern.
- [ ] I've added one or more illustrative usage examples to the docstring, in a pydocstyle compliant `Examples` section.
- [ ] If the estimator relies on a soft dependency, I've set the `python_dependencies` tag and ensured
  dependency isolation, see the [estimator dependencies guide](https://www.sktime.net/en/latest/developer_guide/dependencies.html#adding-a-soft-dependency).



Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index ad4440829..919ba3eab 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -3261,7 +3261,7 @@
     },
     {
       "login": "TanviPooranmal",
-      "name": "Tanvi Pooranmal Meena"
+      "name": "Tanvi Pooranmal Meena",
       "avatar_url": "https://avatars.githubusercontent.com/TanviPooranmal",
       "profile": "https://github.com/TanviPooranmal",
       "contributions": [
@@ -3315,6 +3315,15 @@
         "code"
       ]
     },
+    {
+      "login": "lenaklosik",
+      "name": "Lena Klosik",
+      "avatar_url": "https://avatars.githubusercontent.com/lenaklosik",
+      "profile": "https://github.com/lenaklosik",
+      "contributions": [
+        "code"
+      ]
+    },
     {
       "login": "marcosfelt",
       "name": "Kobi Felton",
diff --git a/docs/source/conf.py b/docs/source/conf.py
index c79466ffb..17c30a2cf 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -324,7 +324,7 @@ def _make_estimator_overview(app):
         with the final name always preceded by "&".
         """
         if isinstance(author_info, str) and author_info.lower() == "sktime developers":
-            link = '<a href="about/team">' "sktime developers</a>"
+            link = '<a href="about/team.html">' "sktime developers</a>"
             return link
 
         if not isinstance(author_info, list):
diff --git a/sktime/benchmarking/forecasting.py b/sktime/benchmarking/forecasting.py
index 47eb852c5..6b51c30e6 100644
--- a/sktime/benchmarking/forecasting.py
+++ b/sktime/benchmarking/forecasting.py
@@ -4,6 +4,8 @@ import functools
 from collections.abc import Callable
 from typing import Optional, Union
 
+import numpy as np
+
 from sktime.benchmarking.benchmarks import BaseBenchmark
 from sktime.forecasting.base import BaseForecaster
 from sktime.forecasting.model_evaluation import evaluate
@@ -18,6 +20,8 @@ def forecasting_validation(
     estimator: BaseForecaster,
     backend=None,
     backend_params=None,
+    strategy="refit",
+    error_score=np.nan,
     **kwargs,
 ) -> dict[str, Union[float, str]]:
     """Run validation for a forecasting estimator.
@@ -65,6 +69,15 @@ def forecasting_validation(
         will default to ``joblib`` defaults.
         - "dask": any valid keys for ``dask.compute`` can be passed,
         e.g., ``scheduler``
+    error_score : "raise" or numeric, default=np.nan
+        Value to assign to the score if an exception occurs in estimator fitting. If set
+        to "raise", the exception is raised. If a numeric value is given,
+        FitFailedWarning is raised.
+    strategy : {"refit", "update", "no-update_params"}, optional, default="refit"
+        defines the ingestion mode when the forecaster sees new data when window expands
+        "refit" = forecaster is refitted to each training window
+        "update" = forecaster is updated with training window data, in sequence provided
+        "no-update_params" = fit to first training window, re-used without fit or update
 
     Returns
     -------
@@ -82,6 +95,8 @@ def forecasting_validation(
             scoring=scorers,
             backend=backend,
             backend_params=backend_params,
+            error_score=error_score,
+            strategy=strategy,
         )
     else:
         scores_df = evaluate(
@@ -91,6 +106,8 @@ def forecasting_validation(
             scoring=scorers,
             backend=backend,
             backend_params=backend_params,
+            error_score=error_score,
+            strategy=strategy,
         )
 
     for scorer in scorers:
@@ -108,6 +125,8 @@ def _factory_forecasting_validation(
     scorers: list[BaseMetric],
     backend=None,
     backend_params=None,
+    error_score=np.nan,
+    strategy="refit",
 ) -> Callable:
     """Build validation func which just takes a forecasting estimator."""
     return functools.partial(
@@ -117,6 +136,8 @@ def _factory_forecasting_validation(
         scorers,
         backend=backend,
         backend_params=backend_params,
+        error_score=error_score,
+        strategy=strategy,
     )
 
 
@@ -183,6 +204,8 @@ class ForecastingBenchmark(BaseBenchmark):
         cv_splitter: BaseSplitter,
         scorers: list[BaseMetric],
         task_id: Optional[str] = None,
+        error_score=np.nan,
+        strategy="refit",
     ):
         """Register a forecasting task to the benchmark.
 
@@ -197,6 +220,16 @@ class ForecastingBenchmark(BaseBenchmark):
         task_id : str, optional (default=None)
             Identifier for the benchmark task. If none given then uses dataset loader
             name combined with cv_splitter class name.
+        error_score : "raise" or numeric, default=np.nan
+            Value to assign to the score if an exception occurs in estimator fitting.
+            If set to "raise", the exception is raised. If a numeric value is given,
+            FitFailedWarning is raised.
+        strategy : {"refit", "update", "no-update_params"}, optional, default="refit"
+            defines the ingestion mode when the forecaster sees new data when window
+            expands "refit" = forecaster is refitted to each training window
+            "update" = forecaster is updated with training window data, in sequence
+            provided "no-update_params" = fit to first training window, re-used
+            without fit or update
 
         Returns
         -------
@@ -217,6 +250,8 @@ class ForecastingBenchmark(BaseBenchmark):
                 _factory_forecasting_validation,
                 backend=self.backend,
                 backend_params=self.backend_params,
+                error_score=error_score,
+                strategy=strategy,
             ),
             task_kwargs,
             task_id=task_id,
diff --git a/sktime/classification/sklearn/_rotation_forest.py b/sktime/classification/sklearn/_rotation_forest.py
index 645e590d7..338100a19 100644
--- a/sktime/classification/sklearn/_rotation_forest.py
+++ b/sktime/classification/sklearn/_rotation_forest.py
@@ -10,18 +10,17 @@ __all__ = ["RotationForest"]
 import time
 
 import numpy as np
-import pandas as pd
-from sklearn.base import BaseEstimator
+from sklearn.base import BaseEstimator, ClassifierMixin
 from sklearn.decomposition import PCA
 from sklearn.tree import DecisionTreeClassifier
 from sklearn.utils import check_random_state
+from sklearn.utils.validation import check_is_fitted
 
 from sktime.base._base import _clone_estimator
-from sktime.exceptions import NotFittedError
 from sktime.utils.validation import check_n_jobs
 
 
-class RotationForest(BaseEstimator):
+class RotationForest(BaseEstimator, ClassifierMixin):
     """A rotation forest (RotF) vector classifier.
 
     Implementation of the Rotation Forest classifier described in Rodriguez et al
@@ -160,18 +159,14 @@ class RotationForest(BaseEstimator):
         """
         from joblib import Parallel, delayed
 
-        if isinstance(X, np.ndarray) and len(X.shape) == 3 and X.shape[1] == 1:
-            X = np.reshape(X, (X.shape[0], -1))
-        elif isinstance(X, pd.DataFrame) and len(X.shape) == 2:
-            X = X.to_numpy()
-        elif not isinstance(X, np.ndarray) or len(X.shape) > 2:
-            raise ValueError(
-                "RotationForest is not a time series classifier. "
-                "A valid sklearn input such as a 2d numpy array is required."
-                "Sparse input formats are currently not supported."
-            )
-        X, y = self._validate_data(X=X, y=y, ensure_min_samples=2)
-
+        X, y = self._validate_data(
+            X,
+            y,
+            dtype=[np.float32, np.float64],
+            ensure_2d=True,
+            allow_nd=True,
+            force_all_finite=True,
+        )
         self._n_jobs = check_n_jobs(self.n_jobs)
 
         self.n_instances_, self.n_atts_ = X.shape
@@ -269,6 +264,7 @@ class RotationForest(BaseEstimator):
         y : array-like, shape = [n_instances]
             Predicted class labels.
         """
+        check_is_fitted(self)
         rng = check_random_state(self.random_state)
         return np.array(
             [
@@ -292,27 +288,20 @@ class RotationForest(BaseEstimator):
         """
         from joblib import Parallel, delayed
 
-        if not self._is_fitted:
-            raise NotFittedError(
-                f"This instance of {self.__class__.__name__} has not "
-                f"been fitted yet; please call `fit` first."
-            )
+        check_is_fitted(self)
 
         # treat case of single class seen in fit
         if self.n_classes_ == 1:
             return np.repeat([[1]], X.shape[0], axis=0)
 
-        if isinstance(X, np.ndarray) and len(X.shape) == 3 and X.shape[1] == 1:
-            X = np.reshape(X, (X.shape[0], -1))
-        elif isinstance(X, pd.DataFrame) and len(X.shape) == 2:
-            X = X.to_numpy()
-        elif not isinstance(X, np.ndarray) or len(X.shape) > 2:
-            raise ValueError(
-                "RotationForest is not a time series classifier. "
-                "A valid sklearn input such as a 2d numpy array is required."
-                "Sparse input formats are currently not supported."
-            )
-        X = self._validate_data(X=X, reset=False)
+        X = self._validate_data(
+            X,
+            dtype=[np.float32, np.float64],
+            reset=False,
+            ensure_2d=True,
+            allow_nd=True,
+            force_all_finite=True,
+        )
 
         # replace missing values with 0 and remove useless attributes
         X = X[:, self._useful_atts]
@@ -338,21 +327,15 @@ class RotationForest(BaseEstimator):
     def _get_train_probs(self, X, y):
         from joblib import Parallel, delayed
 
-        if not self._is_fitted:
-            raise NotFittedError(
-                f"This instance of {self.__class__.__name__} has not "
-                f"been fitted yet; please call `fit` first."
-            )
-        if isinstance(X, np.ndarray) and len(X.shape) == 3 and X.shape[1] == 1:
-            X = np.reshape(X, (X.shape[0], -1))
-        elif isinstance(X, pd.DataFrame) and len(X.shape) == 2:
-            X = X.to_numpy()
-        elif not isinstance(X, np.ndarray) or len(X.shape) > 2:
-            raise ValueError(
-                "RotationForest is not a time series classifier. "
-                "A valid sklearn input such as a 2d numpy array is required."
-                "Sparse input formats are currently not supported."
-            )
+        check_is_fitted(self)
+        X = self._validate_data(
+            X,
+            dtype=[np.float32, np.float64],
+            reset=False,
+            ensure_2d=True,
+            allow_nd=True,
+            force_all_finite=True,
+        )
         X = self._validate_data(X=X, reset=False)
 
         # handle the single-class-label case
diff --git a/sktime/datatypes/_adapter/gluonts.py b/sktime/datatypes/_adapter/gluonts.py
index d01e2e996..60659a010 100644
--- a/sktime/datatypes/_adapter/gluonts.py
+++ b/sktime/datatypes/_adapter/gluonts.py
@@ -11,8 +11,11 @@ def convert_pandas_to_listDataset(pd_dataframe: pd.DataFrame):
 
     Returns
     -------
-    gluonts.dataset.common.ListDataset
-        A gluonTS ListDataset formed from `pd_dataframe`
+    list_dataset: list
+        A list of dict where each dict represents a time series with the following keys:
+
+        - "start": The starting timestamp.
+        - "target": The values of the time series, as a numpy array.
 
     Examples
     --------
@@ -36,15 +39,15 @@ def convert_pandas_to_listDataset(pd_dataframe: pd.DataFrame):
     >>> from sktime.datatypes._adapter.gluonts import convert_pandas_to_listDataset
     >>> X_list_dataset = convert_pandas_to_listDataset(X)
     """
-    from gluonts.dataset.common import ListDataset
-    from gluonts.dataset.field_names import FieldName
+    # use numpy to implement abstraction of gluonTS ListDataset
+    import numpy as np
 
     # For non-multiindexed DataFrames
     if not isinstance(pd_dataframe.index, pd.MultiIndex):
         start_datetime = pd_dataframe.index[0]
 
-        target_columns = pd_dataframe.columns[:]
-        target_values = pd_dataframe[target_columns]
+        target_columns = pd_dataframe.columns.difference(["instances", "timepoints"])
+        target_values = pd_dataframe[target_columns].to_numpy().astype(np.float32)
 
         if isinstance(pd_dataframe.index, pd.DatetimeIndex):
             freq = pd_dataframe.index.inferred_freq
@@ -56,13 +59,27 @@ def convert_pandas_to_listDataset(pd_dataframe: pd.DataFrame):
             start_datetime = pd.Timestamp(start_datetime)
             freq = "D"
 
-        return ListDataset(
-            [{FieldName.START: start_datetime, FieldName.TARGET: target_values}],
-            freq=freq,
-            one_dim_target=False,
-        )
+        return [
+            {
+                "start": pd.Period(start_datetime, freq),
+                "target": target_values,
+            }
+        ]
 
-    dataset = []
+    list_dataset = []
+
+    # Obtain the total amount of timesteps to assist with inferring frequency
+    time_index = pd_dataframe.index.get_level_values(1)
+    freq = None
+
+    if isinstance(time_index, pd.DatetimeIndex):
+        freq = time_index.inferred_freq
+
+    elif isinstance(time_index, pd.PeriodIndex):
+        freq = time_index.freq
+
+    if freq is None:
+        freq = "D"
 
     # By maintaining 2 levels in the DataFrame's indices
     # we can access each series and its timestep values with ease!
@@ -76,38 +93,17 @@ def convert_pandas_to_listDataset(pd_dataframe: pd.DataFrame):
             start_datetime = pd.Timestamp(start_datetime)
 
         # Isolating multivariate values for each time series
-        target_column = data.columns[:]
+        target_column = data.columns.difference(["instances"])
 
-        target_values = data[target_column]
-        target_values = target_values.reset_index(drop=True)
+        target_values = data[target_column].to_numpy().astype(np.float32)
 
-        dataset.append(
+        list_dataset.append(
             {
-                FieldName.START: start_datetime,
-                FieldName.TARGET: target_values,
+                "start": pd.Period(start_datetime, freq),
+                "target": target_values,
             }
         )
 
-    # Obtain the total amount of timesteps to assist with inferring frequency
-    time_index = pd_dataframe.index.get_level_values(1)
-    freq = None
-
-    if isinstance(time_index, pd.DatetimeIndex):
-        freq = time_index.inferred_freq
-
-    elif isinstance(time_index, pd.PeriodIndex):
-        freq = time_index.freq
-
-    if freq is None:
-        freq = "D"
-
-    # Converting the dataset to a GluonTS ListDataset
-    list_dataset = ListDataset(
-        dataset,
-        freq=freq,
-        one_dim_target=False,
-    )
-
     return list_dataset
 
 
diff --git a/sktime/datatypes/_panel/_check.py b/sktime/datatypes/_panel/_check.py
index 723056216..8d9ea65fe 100644
--- a/sktime/datatypes/_panel/_check.py
+++ b/sktime/datatypes/_panel/_check.py
@@ -1271,7 +1271,7 @@ class PanelGluontsList(ScitypePanel):
         "name_python": "panel_gluonts_list",  # lower_snake_case
         "name_aliases": [],
         "python_version": None,
-        "python_dependencies": "gluonts",
+        "python_dependencies": None,
         "capability:multivariate": True,
         "capability:unequally_spaced": True,
         "capability:missing_values": True,
@@ -1308,7 +1308,7 @@ class PanelGluontsList(ScitypePanel):
             or "target" not in obj[0]
             or len(obj[0]["target"]) <= 1
         ):
-            msg = f"{var_name} must be a gluonts.ListDataset, found {type(obj)}"
+            msg = f"{var_name} must be a listDataset, found {type(obj)}"
             return _ret(False, msg, None, return_metadata)
 
         # Check if there are no time series in the ListDataset
diff --git a/sktime/forecasting/compose/_reduce.py b/sktime/forecasting/compose/_reduce.py
index 3505ab2a2..29f4d2736 100644
--- a/sktime/forecasting/compose/_reduce.py
+++ b/sktime/forecasting/compose/_reduce.py
@@ -2374,8 +2374,12 @@ class RecursiveReductionForecaster(BaseForecaster, _ReducerMixin):
         # lagger_y_to_X_ will lag y to obtain the sklearn X
         lags = self._lags
         lagger_y_to_X = Lag(lags=lags, index_out="extend")
-        if impute_method is not None:
-            lagger_y_to_X = lagger_y_to_X * Imputer(method=impute_method)
+
+        if isinstance(self.impute_method, Imputer):
+            lagger_y_to_X = lagger_y_to_X * impute_method
+        else:
+            if self.impute_method is not None:
+                lagger_y_to_X = lagger_y_to_X * Imputer(method=impute_method)
         self.lagger_y_to_X_ = lagger_y_to_X
 
         Xt = lagger_y_to_X.fit_transform(y)
@@ -2482,8 +2486,12 @@ class RecursiveReductionForecaster(BaseForecaster, _ReducerMixin):
             Xt = lagger_y_to_X.transform(y_plus_preds)
 
             lag_plus = Lag(lags=1, index_out="extend")
-            if self.impute_method is not None:
-                lag_plus = lag_plus * Imputer(method=self.impute_method)
+
+            if isinstance(self.impute_method, Imputer):
+                lag_plus = lag_plus * self.impute_method
+            else:
+                if self.impute_method is not None:
+                    lag_plus = lag_plus * Imputer(method=self.impute_method)
 
             Xtt = lag_plus.fit_transform(Xt)
             y_plus_one = lag_plus.fit_transform(y_plus_preds)
@@ -2534,8 +2542,12 @@ class RecursiveReductionForecaster(BaseForecaster, _ReducerMixin):
         Xt = lagger_y_to_X.transform(y)
 
         lag_plus = Lag(lags=1, index_out="extend")
-        if self.impute_method is not None:
-            lag_plus = lag_plus * Imputer(method=self.impute_method)
+
+        if isinstance(self.impute_method, Imputer):
+            lag_plus = lag_plus * self.impute_method
+        else:
+            if self.impute_method is not None:
+                lag_plus = lag_plus * Imputer(method=self.impute_method)
 
         Xtt = lag_plus.fit_transform(Xt)
 
@@ -2582,7 +2594,14 @@ class RecursiveReductionForecaster(BaseForecaster, _ReducerMixin):
         """
         from sklearn.linear_model import LinearRegression
 
+        from sktime.forecasting.compose._reduce import DirectReductionForecaster
+        from sktime.transformations.series.impute import Imputer
+
         est = LinearRegression()
+        forecaster_imputer = Imputer(
+            method="forecaster", forecaster=DirectReductionForecaster(estimator=est)
+        )
+
         params1 = {
             "estimator": est,
             "window_length": 3,
@@ -2593,8 +2612,20 @@ class RecursiveReductionForecaster(BaseForecaster, _ReducerMixin):
             "window_length": 4,
             "pooling": "local",
         }
+        params3 = {
+            "estimator": est,
+            "window_length": 4,
+            "pooling": "local",
+            "impute_method": forecaster_imputer,  # test imputation with forecaster
+        }
+        params4 = {
+            "estimator": est,
+            "window_length": 4,
+            "pooling": "global",
+            "impute_method": forecaster_imputer,
+        }
 
-        return [params1, params2]
+        return [params1, params2, params3, params4]
 
 
 class YfromX(BaseForecaster, _ReducerMixin):
diff --git a/sktime/forecasting/hf_transformers_forecaster.py b/sktime/forecasting/hf_transformers_forecaster.py
index 4e7384aa7..5efb77935 100644
--- a/sktime/forecasting/hf_transformers_forecaster.py
+++ b/sktime/forecasting/hf_transformers_forecaster.py
@@ -36,6 +36,7 @@ class HFTransformersForecaster(BaseForecaster):
     fit_strategy : str, default="minimal"
         Strategy to use for fitting (fine-tuning) the model. This can be one of
         the following:
+
         - "minimal": Fine-tunes only a small subset of the model parameters,
           allowing for quick adaptation with limited computational resources.
         - "full": Fine-tunes all model parameters, which may result in better
@@ -45,6 +46,7 @@ class HFTransformersForecaster(BaseForecaster):
           Note: If the 'peft' package is not available, a `ModuleNotFoundError` will
           be raised, indicating that the 'peft' package is required. Please install
           it using `pip install peft` to use this fit strategy.
+
     validation_split : float, default=0.2
         Fraction of the data to use for validation
     config : dict, default={}
diff --git a/sktime/forecasting/ttm.py b/sktime/forecasting/ttm.py
index f6a391f76..cf77e9071 100644
--- a/sktime/forecasting/ttm.py
+++ b/sktime/forecasting/ttm.py
@@ -31,41 +31,101 @@ class TinyTimeMixerForecaster(_BaseGlobalForecaster):
     open-sourced by IBM Research. With less than 1 Million parameters, TTM introduces
     the notion of the first-ever "tiny" pre-trained models for Time-Series Forecasting.
 
+    **Zero-shot and Fine-tuning**:
+
+    This model is designed as a versatile foundation model, capable of both
+    zero-shot forecasting and fine-tuning on custom datasets.
+    The choice between zero-shot and fine-tuning is managed internally and
+    is determined by the final configuration used to initialize the model.
+
+    **Initialization Process**:
+
+    1. **Model Path**: The ``model_path`` parameter points to a local folder or
+       huggingface repo that contains both *configuration files*
+       and *pretrained weights*.
+
+    2. **Default Configuration**: The model loads its default configuration from the
+       *configuration files*.
+
+    3. **Custom Configuration**: Users can provide a custom configuration via the
+       ``config`` parameter during model initialization.
+
+    4. **Configuration Override**: If custom configuration is provided,
+       it overrides the default configuration.
+
+    5. **Forecasting Horizon**: If the forecasting horizon (``fh``) specified during
+       ``fit`` exceeds the default ``config.prediction_length``,
+       the configuration is updated to reflect ``max(fh)``.
+
+    6. **Model Architecture**: The final configuration is used to construct the
+       *model architecture*.
+
+    7. **Pretrained Weights**: *pretrained weights* are loded from the ``model_path``,
+       these weights are then aligned and loaded into the *model architechture*.
+
+    8. **Weight Alignment**: However sometimes, *pretrained weights* do not align with
+       the *model architechture*, because the config was changed which created a
+       *model architechture* of different size than the default one.
+       This causes some of the weights in *model architechture* to be reinitialized
+       randomly instead of using the pre-trained weights.
+
+    **Forecasting Modes**:
+
+    - **Zero-shot Forecasting**: When all the *pre-trained weights* are correctly
+      aligned with the *model architechture*, fine-tuing part is bypassed and
+      the model preforms zero-short forecasting.
+
+    - **Fine-tuning**: When not all the *pre-trained weights* are correctly aligned
+      with the *model architechture*, rather some weights are re-initialized,
+      these re-initialized weights are fine-tuned on the provided data.
+
     Parameters
     ----------
     model_path : str, default="ibm/TTM"
         Path to the Huggingface model to use for forecasting.
         This can be either:
+
         - The name of a Huggingface repository (e.g., "ibm/TTM")
+
         - A local path to a folder containing model files in a format supported
           by transformers. In this case, ensure that the directory contains all
           necessary files (e.g., configuration, tokenizer, and model weights).
+
     revision: str, default="main"
         Revision of the model to use:
+
         - "main": For loading model with context_length of 512
           and prediction_length of 96.
+
         - "1024_96_v1": For loading model with context_length of 1024
           and prediction_length of 96.
+
     validation_split : float, default=0.2
         Fraction of the data to use for validation
+
     config : dict, default={}
         Configuration to use for the model. See the ``transformers``
         documentation for details.
+
     training_args : dict, default={}
         Training arguments to use for the model. See ``transformers.TrainingArguments``
         for details.
         Note that the ``output_dir`` argument is required.
+
     compute_metrics : list, default=None
         List of metrics to compute during training. See ``transformers.Trainer``
         for details.
+
     callbacks : list, default=[]
         List of callbacks to use during training. See ``transformers.Trainer``
+
     broadcasting : bool, default=False
         if True, multiindex data input will be broadcasted to single series.
         For each single series, one copy of this forecaster will try to
         fit and predict on it. The broadcasting is happening inside automatically,
         from the outerside api perspective, the input and output are the same,
         only one multiindex output from ``predict``.
+
     use_source_package : bool, default=False
         If True, the model and configuration will be loaded directly from the source
         package ``tsfm_public.models.tinytimemixer``. This is useful if you
@@ -80,9 +140,9 @@ class TinyTimeMixerForecaster(_BaseGlobalForecaster):
     ----------
     .. [1] https://github.com/ibm-granite/granite-tsfm/tree/main/tsfm_public/models/tinytimemixer
     .. [2] Ekambaram, V., Jati, A., Dayama, P., Mukherjee, S.,
-    Nguyen, N.H., Gifford, W.M., Reddy, C. and Kalagnanam, J., 2024.
-    Tiny Time Mixers (TTMs): Fast Pre-trained Models for Enhanced
-    Zero/Few-Shot Forecasting of Multivariate Time Series. CoRR.
+           Nguyen, N.H., Gifford, W.M., Reddy, C. and Kalagnanam, J., 2024.
+           Tiny Time Mixers (TTMs): Fast Pre-trained Models for Enhanced
+           Zero/Few-Shot Forecasting of Multivariate Time Series. CoRR.
     .. [3] https://github.com/ibm-granite/granite-tsfm/blob/main/notebooks/tutorial/ttm_tutorial.ipynb
     .. [4] https://github.com/ibm-granite/granite-tsfm/tree/ttm
 
@@ -92,6 +152,7 @@ class TinyTimeMixerForecaster(_BaseGlobalForecaster):
     >>> from sktime.datasets import load_airline
     >>> y = load_airline()
     >>> forecaster = TinyTimeMixerForecaster() # doctest: +SKIP
+    >>> # performs zero-shot forecasting, as default config (unchanged) is used
     >>> forecaster.fit(y, fh=[1, 2, 3]) # doctest: +SKIP
     >>> y_pred = forecaster.predict() # doctest: +SKIP
 
@@ -118,7 +179,7 @@ class TinyTimeMixerForecaster(_BaseGlobalForecaster):
     ...     },
     ... ) # doctest: +SKIP
     >>>
-    >>> # train and predict
+    >>> # model is fine-tuned, as a config different from default is provided.
     >>> forecaster.fit(y, fh=[1, 2, 3]) # doctest: +SKIP
     >>> y_pred = forecaster.predict() # doctest: +SKIP
     """
diff --git a/sktime/performance_metrics/forecasting/_classes.py b/sktime/performance_metrics/forecasting/_classes.py
index c5986cb0f..b23d1d8d2 100644
--- a/sktime/performance_metrics/forecasting/_classes.py
+++ b/sktime/performance_metrics/forecasting/_classes.py
@@ -2139,22 +2139,48 @@ class MeanSquaredErrorPercentage(BaseForecastingErrorMetricFunc):
 
 
 class MedianSquaredError(BaseForecastingErrorMetricFunc):
-    """Median squared error (MdSE) or root median squared error (RMdSE).
+    r"""Median squared error (MdSE) or root median squared error (RMdSE).
 
     If ``square_root`` is False then calculates MdSE and if ``square_root`` is True
     then RMdSE is calculated. Both MdSE and RMdSE return non-negative floating
     point. The best value is 0.0.
 
+    The Median Squared Error (MdSE) is calculated as:
+
+    .. math::
+        \\text{MdSE} = \text{median}\left((y_{i} - \\widehat{y}_{i})^2\right)_{i=1}^{n}
+
+    where:
+    - \\( y_i \\) are the true values,
+    - \\( \\hat{y}_i \\) are the predicted values,
+    - \\( n \\) is the number of observations.
+
+    If ``square_root`` is set to True,
+    the Root Median Squared Error Percentage (RMdSE) is computed:
+
+    .. math::
+        \\text{RMdSE} = \\sqrt{ \\text{MdSE} }
+
     Like MSE, MdSE is measured in squared units of the input data. RMdSE is
-    on the same scale as the input data like RMSE. Because MdSE and RMdSE
-    square the forecast error rather than taking the absolute value, they
-    penalize large errors more than MAE or MdAE.
+    on the same scale as the input data, like RMSE.
 
     Taking the median instead of the mean of the squared errors makes
     this metric more robust to error outliers relative to a meean based metric
     since the median tends to be a more robust measure of central tendency in
     the presence of outliers.
 
+    Because the median commutes with monotonic transformations, MdSE and RMdSE
+    do not penalize large errors more than the MdAE.
+
+    ``evaluate_by_index`` returns, at a time index :math:`t_i`:
+
+    * if ``square_root`` is False, the squared error at that time index,
+        :math:`(y_i - \widehat{y}_i)^2`,
+    * if ``square_root`` is True, the absolute error at that time index,
+      the absolute error at that time index, :math:`|y_i - \widehat{y}_i|`,
+
+    for all time indices :math:`t_1, \dots, t_n` in the input.
+
     Parameters
     ----------
     square_root : bool, default = False
@@ -2234,6 +2260,53 @@ class MedianSquaredError(BaseForecastingErrorMetricFunc):
         self.square_root = square_root
         super().__init__(multioutput=multioutput, multilevel=multilevel)
 
+    def _evaluate_by_index(self, y_true, y_pred, **kwargs):
+        """Return the metric evaluated at each time point.
+
+        private _evaluate_by_index containing core logic, called from evaluate_by_index
+
+        Parameters
+        ----------
+        y_true : time series in sktime compatible pandas based data container format
+            Ground truth (correct) target values
+            y can be in one of the following formats:
+            Series scitype: pd.DataFrame
+            Panel scitype: pd.DataFrame with 2-level row MultiIndex
+            Hierarchical scitype: pd.DataFrame with 3 or more level row MultiIndex
+        y_pred : time series in sktime compatible data container format
+            Forecasted values to evaluate
+            must be of same format as y_true, same indices and columns if indexed
+
+        Returns
+        -------
+        loss : pd.Series or pd.DataFrame
+            Calculated metric, by time point.
+            pd.Series if self.multioutput="uniform_average" or array-like
+                index is equal to index of y_true
+                entry at index i is metric at time i, averaged over variables
+            pd.DataFrame if self.multioutput="raw_values"
+                index and columns equal to those of y_true
+                i,j-th entry is metric at time i, at variable j
+        """
+        multioutput = self.multioutput
+
+        raw_values = (y_true - y_pred) ** 2
+
+        if self.square_root:
+            raw_values = np.sqrt(raw_values)
+
+        raw_values = self._get_weighted_df(raw_values, **kwargs)
+
+        if isinstance(multioutput, str):
+            if multioutput == "raw_values":
+                return raw_values
+
+            if multioutput == "uniform_average":
+                return raw_values.median(axis=1)
+
+        # else, we expect multioutput to be array-like
+        return raw_values.dot(multioutput)
+
     @classmethod
     def get_test_params(cls, parameter_set="default"):
         """Return testing parameter settings for the estimator.
diff --git a/sktime/performance_metrics/forecasting/_functions.py b/sktime/performance_metrics/forecasting/_functions.py
index 28fa97357..b109c833d 100644
--- a/sktime/performance_metrics/forecasting/_functions.py
+++ b/sktime/performance_metrics/forecasting/_functions.py
@@ -1122,7 +1122,7 @@ def median_squared_error(
     penalize large errors more than MAE or MdAE.
 
     Taking the median instead of the mean of the squared errors makes
-    this metric more robust to error outliers relative to a meean based metric
+    this metric more robust to error outliers relative to a mean based metric
     since the median tends to be a more robust measure of central tendency in
     the presence of outliers.
 
diff --git a/sktime/split/sameloc.py b/sktime/split/sameloc.py
index 395533f85..38fd312ec 100644
--- a/sktime/split/sameloc.py
+++ b/sktime/split/sameloc.py
@@ -23,6 +23,18 @@ class SameLocSplitter(BaseSplitter):
     Splits ``y`` in ``split`` and ``split_loc`` such that ``loc`` indices of splits
     are identical to loc indices of ``cv`` applied to ``y_template``.
 
+    This splitter is useful when you need to replicate train-test splits across multiple
+    time series with consistent loc-based indexing.
+
+    Mathematically, let :math:`y_{template}` represent the reference time series,
+    corresponding to the input ``y_template``,
+    and let :math:`I_{train, 1}, I_{test, 1}, \ldots, I_{train, K}, I_{test, K}`
+    be the loc-based train-test splits generated by ``cv`` on ``y_template``.
+
+    The `SameLocSplitter` returns the corresponding positional indices in ``y``,
+    such that the loc-based splits are identical to the splits generated by ``cv``,
+    namely, :math:`I_{train, 1}, I_{test, 1}, \ldots, I_{train, K}, I_{test, K}`.
+
     Parameters
     ----------
     cv : BaseSplitter
diff --git a/sktime/transformations/panel/tsfresh.py b/sktime/transformations/panel/tsfresh.py
index c4e58eca6..d7c10d9bc 100644
--- a/sktime/transformations/panel/tsfresh.py
+++ b/sktime/transformations/panel/tsfresh.py
@@ -6,7 +6,7 @@ __author__ = ["AyushmaanSeth", "mloning", "alwinw", "MatthewMiddlehurst"]
 __all__ = ["TSFreshFeatureExtractor", "TSFreshRelevantFeatureExtractor"]
 
 from sktime.transformations.base import BaseTransformer
-from sktime.utils.dependencies import _check_soft_dependencies
+from sktime.utils.dependencies import _check_estimator_deps
 from sktime.utils.validation import check_n_jobs
 
 
@@ -24,7 +24,8 @@ class _TSFreshFeatureExtractor(BaseTransformer):
         "X_inner_mtype": "nested_univ",  # which mtypes do _fit/_predict support for X?
         "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for X?
         "fit_is_empty": True,  # is fit empty and can be skipped? Yes = True
-        "python_dependencies": "tsfresh",
+        "python_dependencies": ["tsfresh", "scipy<1.15"],
+        # todo 0.36.0 - check and remove the scipy bound if bug is fixed, else bump todo
     }
 
     def __init__(
@@ -484,6 +485,16 @@ class TSFreshRelevantFeatureExtractor(_TSFreshFeatureExtractor):
         hypotheses_independent=None,
         ml_task="auto",
     ):
+        self.test_for_binary_target_binary_feature = (
+            test_for_binary_target_binary_feature
+        )
+        self.test_for_binary_target_real_feature = test_for_binary_target_real_feature
+        self.test_for_real_target_binary_feature = test_for_real_target_binary_feature
+        self.test_for_real_target_real_feature = test_for_real_target_real_feature
+        self.fdr_level = fdr_level
+        self.hypotheses_independent = hypotheses_independent
+        self.ml_task = ml_task
+
         super().__init__(
             default_fc_parameters=default_fc_parameters,
             kind_to_fc_parameters=kind_to_fc_parameters,
@@ -498,16 +509,6 @@ class TSFreshRelevantFeatureExtractor(_TSFreshFeatureExtractor):
             distributor=distributor,
         )
 
-        self.test_for_binary_target_binary_feature = (
-            test_for_binary_target_binary_feature
-        )
-        self.test_for_binary_target_real_feature = test_for_binary_target_real_feature
-        self.test_for_real_target_binary_feature = test_for_real_target_binary_feature
-        self.test_for_real_target_real_feature = test_for_real_target_real_feature
-        self.fdr_level = fdr_level
-        self.hypotheses_independent = hypotheses_independent
-        self.ml_task = ml_task
-
         self.default_fs_parameters_ = self._get_selection_params()
 
     def _get_selection_params(self):
@@ -734,7 +735,7 @@ class TSFreshRelevantFeatureExtractor(_TSFreshFeatureExtractor):
         }
         params = [params, params2]
 
-        if _check_soft_dependencies("tsfresh", severity="none"):
+        if _check_estimator_deps(cls, severity="none"):
             from tsfresh.utilities.distribution import MapDistributor
 
             params3 = {


Test patch:
diff --git a/sktime/classification/sklearn/tests/test_rotation_forest.py b/sktime/classification/sklearn/tests/test_rotation_forest.py
index 9186e71e6..1ba351de6 100644
--- a/sktime/classification/sklearn/tests/test_rotation_forest.py
+++ b/sktime/classification/sklearn/tests/test_rotation_forest.py
@@ -2,12 +2,23 @@
 
 import numpy as np
 import pytest
+from sklearn.utils.estimator_checks import parametrize_with_checks
 
 from sktime.classification.sklearn import RotationForest
 from sktime.datasets import load_unit_test
 from sktime.tests.test_switch import run_test_for_class
 
 
+@pytest.mark.skipif(
+    not run_test_for_class(RotationForest),
+    reason="run test if softdeps are present and incrementally (if requested)",
+)
+@parametrize_with_checks([RotationForest()])
+def test_sklearn_compatible_estimator(estimator, check):
+    """Run sklearn estimator compatibility checks."""
+    check(estimator)
+
+
 @pytest.mark.skipif(
     not run_test_for_class(RotationForest),
     reason="run test only if softdeps are present and incrementally (if requested)",
diff --git a/sktime/datatypes/_adapter/tests/test_gluonts.py b/sktime/datatypes/_adapter/tests/test_gluonts.py
index a08e2cf76..daa3b0ad3 100644
--- a/sktime/datatypes/_adapter/tests/test_gluonts.py
+++ b/sktime/datatypes/_adapter/tests/test_gluonts.py
@@ -55,6 +55,26 @@ def test_pandas_to_ListDataset(pandas_df):
         np.testing.assert_allclose(group_data.values, generated_list[idx]["target"])
         idx += 1
 
+    from gluonts.dataset.common import ListDataset
+    from gluonts.dataset.field_names import FieldName
+
+    gluon_dataset = []
+    for entry in generated_list:
+        gluon_dataset.append(
+            {
+                FieldName.START: entry["start"],
+                FieldName.TARGET: entry["target"],
+            }
+        )
+
+    # Attempt to create ListDataset object
+    list_dataset = ListDataset(gluon_dataset, freq="D", one_dim_target=False)
+
+    first_item = next(iter(list_dataset))
+    assert "start" in first_item
+    assert "target" in first_item
+    np.testing.assert_allclose(first_item["target"], generated_list[0]["target"])
+
 
 @pytest.mark.skipif(
     not _check_soft_dependencies("gluonts", severity="none"),