Repository: sktime/sktime

Issue:
[ENH] `SplitterSummarizer` which applies a series-to-primitive transformer per split
From discussion with @abishalom on discord.

I infer that it would be great to have a "splitter summarizer", which is a series-to-series transformer.

It takes as a parameter a series-to-primitives transformer `t` and a splitter `cv`.

It transforms a single series `X` into a single series `Xt`, such that: the `i`-th row of `Xt` is `t.fit_transform(cv.split_y(X)[i][0])` (i.e., the `t`-transform of the `i`-th train split of `X`), where all series are `pd.DataFrame` (otherwise equivalent index). The `fit` is empty.

Optionally, there is a parameter that determines the index of `Xt`, it could be the integer index, or the last index of the train split.

FYI @abishalom - if you pipeline `SplitterSummarizer(PCA(), cv=ExpandingWindowSplitter())` with `ARIMA`, it should be what you want?

That is, the forecaster you want (if I understand right?) is

`SplitterSummarizer(PCA(), cv=ExpandingWindowSplitter()) * ARIMA()`


Discussion:
might be a nice (but somewhat challenging) good first issue, using the `transformer_simple` extension template.
Hello, I would love to take this issue for a first contribution if possible. Thanks
of course! Let us know if you need help getting started.

Here's the guide for implementing estimators: https://www.sktime.net/en/stable/developer_guide/add_estimators.html
(this should be a transformer, series to series)

Resolving patch:
diff --git a/sktime/transformations/series/summarize.py b/sktime/transformations/series/summarize.py
--- a/sktime/transformations/series/summarize.py
+++ b/sktime/transformations/series/summarize.py
@@ -2,12 +2,16 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 """Implement transformers for summarizing a time series."""
 
-__author__ = ["mloning", "RNKuhns", "danbartl", "grzegorzrut"]
-__all__ = ["SummaryTransformer", "WindowSummarizer"]
+__author__ = ["mloning", "RNKuhns", "danbartl", "grzegorzrut", "BensHamza"]
+__all__ = ["SummaryTransformer", "WindowSummarizer", "SplitterSummarizer"]
 
 import pandas as pd
 from joblib import Parallel, delayed
 
+from sktime.forecasting.model_selection import (
+    ExpandingWindowSplitter,
+    SlidingWindowSplitter,
+)
 from sktime.transformations.base import BaseTransformer
 from sktime.utils.multiindex import flatten_multiindex
 
@@ -750,3 +754,167 @@ def get_test_params(cls, parameter_set="default"):
         params4 = {"summary_function": None, "quantiles": (0.1, 0.2, 0.25)}
 
         return [params1, params2, params3, params4]
+
+
+class SplitterSummarizer(BaseTransformer):
+    """Create summary values of a time series' splits.
+
+    A series-to-series transformer that applies a series-to-primitives transformer t
+    to each series' train split created using the splitter `s`.
+
+    The i-th row of the resulting series is equivalent to
+    t.fit_transform(s.split_series(X)[i][0]).
+
+    The output series aims to provide a summarization of the input series based on the
+    given transformer and splitter.
+
+    Parameters
+    ----------
+    transformer : `sktime` transformer inheriting from `BaseTransformer`
+    series-to-primitives transformer used to convert series to primitives.
+
+    splitter : `sktime` splitter inheriting from `BaseSplitter`, optional (default=None)
+    splitter used to divide the series.
+    If None, it takes `ExpandingWindowSplitter` with `start_with_window=False`
+    and otherwise default parameters.
+
+    index : str, optional (default="last")
+    Determines the indexing approach for the resulting series.
+    If "last", the index of each series' row is used.
+    If anything else, the row's number becomes the index.
+
+    Methods
+    -------
+    transform(X) : Transforms the series according to the specified
+        series-to-primitives transformer and splitter.
+
+    See Also
+    --------
+    SummaryTransformer: Calculates summary value of a time series.
+
+    Examples
+    --------
+    >>> from sktime.transformations.series.summarize import SplitterSummarizer
+    >>> from sktime.transformations.series.summarize import SummaryTransformer
+    >>> from sktime.forecasting.model_selection import ExpandingWindowSplitter
+    >>> from sktime.datasets import load_airline
+    >>> y = load_airline()
+    >>> transformer = SplitterSummarizer(
+    ...     transformer=SummaryTransformer(),
+    ...     splitter=ExpandingWindowSplitter())
+    >>> y_splitsummarized = transformer.fit_transform(y)
+    """
+
+    _tags = {
+        "scitype:transform-input": "Series",
+        # what is the scitype of X: Series, or Panel
+        "scitype:transform-output": "Series",
+        # what scitype is returned: Primitives, Series, Panel
+        "scitype:instancewise": True,  # is this an instance-wise transform?
+        "X_inner_mtype": ["pd.DataFrame", "pd.Series"],
+        # which mtypes do _fit/_predict support for X?
+        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for X?
+        "fit_is_empty": True,
+    }
+
+    def __init__(self, transformer, splitter=None, index="last"):
+        self.transformer = transformer
+        self.index = index
+        self.splitter = splitter
+
+        if splitter is None:
+            self._splitter = SlidingWindowSplitter(start_with_window=False)
+        else:
+            self._splitter = splitter
+
+        super().__init__()
+
+        if not hasattr(self.transformer, "fit_transform"):
+            raise ValueError(
+                f"Error in {self.__class__.__name__}, transformer parameter "
+                "should be an estimator with a fit_transform method"
+            )
+        if not hasattr(self._splitter, "split_series"):
+            raise ValueError(
+                f"Error in {self.__class__.__name__}, splitter parameter, if passed, "
+                "should be an BaseSplitter descendant with a seplit_series method"
+            )
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing the core logic, called from transform
+
+        Parameters
+        ----------
+        X : pd.Series or pd.DataFrame
+            Data to be transformed
+        y : ignored argument for interface compatibility
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        Xt : pd.DataFrame
+            The transformed Data
+        """
+        transformed_series = []
+        splits = self._splitter.split_series(X)
+
+        for split in splits:
+            tf = self.transformer.clone()
+            transformed_split = tf.fit_transform(split[0])
+            transformed_split.index = [split[0].index[-1]]
+            transformed_series.append(transformed_split)
+
+        Xt = pd.concat(transformed_series)
+
+        if self.index != "last":
+            Xt = Xt.reset_index(drop=True)
+
+        return Xt
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the transformer.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        params1 = {
+            "transformer": SummaryTransformer(),
+            "splitter": ExpandingWindowSplitter(initial_window=3),
+        }
+
+        params2 = {
+            "transformer": SummaryTransformer(
+                summary_function=["mad"], quantiles=(0.7,)
+            ),
+            "splitter": SlidingWindowSplitter(window_length=3, step_length=2),
+            "index": None,
+        }
+
+        params3 = {
+            "transformer": SummaryTransformer(),
+            "splitter": SlidingWindowSplitter(window_length=3, step_length=2),
+        }
+
+        params4 = {
+            "transformer": SummaryTransformer(),
+            "splitter": SlidingWindowSplitter(
+                window_length=3, step_length=2, fh=1, start_with_window=True
+            ),
+            "index": "last",
+        }
+
+        return [params1, params2, params3, params4]


Test patch: