Repository: sktime/sktime
Pull request: [ENH] Implementing SplitterSummarizer Transformer
<!--
Thanks for contributing a pull request! Please ensure you have taken a look
at our contribution guide: https://github.com/sktime/sktime/blob/main/CONTRIBUTING.md
-->

#### Reference Issues/PRs
<!--
Example: Fixes #1234. See also #3456.

Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Fixes #4664 .

#### What does this implement/fix? Explain your changes.
<!--
A clear and concise description of what you have implemented.
-->
This PR introduces a new series-to-series transformer SplitterSummarizer.

#### Does your contribution introduce a new dependency? If yes, which one?
No
<!--
If your contribution does add a new hard dependency, we may suggest to initially develop your contribution in a separate companion package in https://github.com/sktime/ to keep external dependencies of the core sktime package to a minimum.
-->



<!-- This section is particularly useful if you have a pull request that is still in development. You can guide the reviews to focus on the parts that are ready for their comments. We suggest using bullets (indicated by * or -) and filled checkboxes [x] here -->



<!-- This section is useful if you have added a test in addition to the existing ones. This will ensure that further changes to these files won't introduce the same kind of bug. It is considered good practice to add tests with newly added code to enforce the fact that the code actually works. This will reduce the chance of introducing logical bugs.
-->

#### Any other comments?
<!--
Please be aware that we are a loose team of volunteers so patience is necessary; assistance handling other issues is very welcome. We value all user contributions, no matter how minor they are. If we are slow to review, either the pull request needs some benchmarking, tinkering, convincing, etc. or more likely the reviewers are simply busy. In either case, we ask for your understanding during the review process.
-->
The testing isn't working 

#### PR checklist
<!--
Please go through the checklist below. Please feel free to remove points if they are not applicable.
-->

##### 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, I've added myself and possibly others to the [CODEOWNERS](https://github.com/sktime/sktime/blob/main/CODEOWNERS) file - 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).


<!--
Thanks for contributing!
-->


Problem statement:
[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()`

Hints from issue 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)

Commit message: new code version

Patch to review:
diff --git a/sktime/transformations/series/summarize.py b/sktime/transformations/series/summarize.py
index e7dde126074..4a23c33d982 100644
--- a/sktime/transformations/series/summarize.py
+++ b/sktime/transformations/series/summarize.py
@@ -2,8 +2,8 @@
 # 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
@@ -750,3 +750,140 @@ 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
+    to each series' train split.
+
+    It splits the series using a specified splitter and transforms each resulting
+    split using the provided transformer.
+
+    This transformer aims to provide a summarization of the series based on the
+    transformer and splitter given.
+
+    Attributes
+    ----------
+    transformer : series-to-primitives transformer used to convert series
+        to primitives.
+
+    splitter : splitter used to divide the series.
+
+    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.
+
+    Notes
+    -----
+    Empty
+
+    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, index="last"):
+        self.transformer = transformer
+        self.splitter = splitter
+        self.index = index
+
+        super().__init__()
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        Transforms the series `X` into a new series `Xt` by applying the
+        series-to-primitives transformer `t` on each train split of `X`,
+         which is created using the splitter `cv`.
+
+         The `i-th` row of `Xt` is equivalent to
+         `t.fit_transform(cv.split_series(X)[i][0])`.
+
+        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
+        """
+        if not hasattr(self.transformer, "fit_transform"):
+            raise ValueError("Transformer should have a fit_transform method")
+        if not hasattr(self.splitter, "split_series"):
+            raise ValueError("Splitter should have a split_series method")
+
+        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`
+        """
+        from sktime.forecasting.model_selection import ExpandingWindowSplitter
+
+        params1 = {
+            "transformer": SummaryTransformer(),
+            "splitter": ExpandingWindowSplitter(),
+        }
+
+        return [params1]


Code review comments:
diff_hunk: @@ -750,3 +750,140 @@ 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
+    to each series' train split.
+
+    It splits the series using a specified splitter and transforms each resulting
+    split using the provided transformer.
+
+    This transformer aims to provide a summarization of the series based on the
+    transformer and splitter given.
+
+    Attributes
+    ----------
+    transformer : series-to-primitives transformer used to convert series
+        to primitives.
+
+    splitter : splitter used to divide the series.
+
+    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.
+
+    Notes
+    -----
+    Empty
+
+    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, index="last"):
path: sktime/transformations/series/summarize.py
text: @user1:
maybe `ExpandingWindowSplitter` is the most sensible default for `splitter`?

If we want this default, then the `__init__` default should be `None`, and the default is written to `self._splitter`.

@author:
Made changes regarding splitter's default object with  `ExpandingWindowSplitter` . I am thinking maybe `SlidingWindowSplitter(window_length=10, step_length=10, fh=1, start_with_window=True)` would be more appropriate for a default splitter here ?

@user1:
Hm, let's think carefully in terms of user expectations. A good default has no "surprising" choices, i.e., it agrees with the user guess if you don't tell them what the choice is.

* I agree that sliding window is more in line with user expectation than expanding.
* however, sliding has the problem of having to specify window and step length.
* I disagree with `step_length=10` because it is an arbitrary choice, and default user expectation imo is `step_length=1`.
* `window_length=10` is also arbitrary, but consistent with the default of `SlidingWindowSplitter`. Perhaps not overriding the default is the best choice here.

@user1:
I think you did not change this?

diff_hunk: @@ -750,3 +750,140 @@ 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
+    to each series' train split.
+
+    It splits the series using a specified splitter and transforms each resulting
+    split using the provided transformer.
+
+    This transformer aims to provide a summarization of the series based on the
+    transformer and splitter given.
+
+    Attributes
+    ----------
+    transformer : series-to-primitives transformer used to convert series
+        to primitives.
+
+    splitter : splitter used to divide the series.
+
+    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.
+
+    Notes
+    -----
+    Empty
+
+    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, index="last"):
path: sktime/transformations/series/summarize.py
text: @user1:
does the `index` arg to anything? Not mentioned in docstring 

@author:
I have added `index` in class docstring