Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
01eb337
transformer mixin, composables and pipeline fix
MatthewMiddlehurst May 17, 2025
494e3c6
transform changes
MatthewMiddlehurst May 21, 2025
cda1312
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst May 21, 2025
8aaafe5
fixing tests
MatthewMiddlehurst May 22, 2025
12e268f
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst Jun 8, 2025
8c7e5f6
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst Jun 9, 2025
7e3ce1a
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst Jun 9, 2025
1a3e821
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst Jun 12, 2025
daf56f2
skip test
MatthewMiddlehurst Jun 12, 2025
91513c0
no mixin
MatthewMiddlehurst Jun 12, 2025
5e434de
yes mixin
MatthewMiddlehurst Jun 12, 2025
931eee6
Merge branch 'main' into mm/transformer-mixin
MatthewMiddlehurst Jun 14, 2025
171689a
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst Jul 30, 2025
f1c1ec6
Merge remote-tracking branch 'origin/mm/transformer-mixin' into mm/tr…
MatthewMiddlehurst Jul 30, 2025
de1d3d0
docstring
MatthewMiddlehurst Jul 30, 2025
112b62e
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst Aug 7, 2025
126bbda
import
MatthewMiddlehurst Aug 7, 2025
10240de
Merge remote-tracking branch 'origin/main' into mm/transformer-mixin
MatthewMiddlehurst Oct 26, 2025
3818d0f
doctest
MatthewMiddlehurst Oct 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions aeon/testing/testing_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
],
# needs investigation
"LeftSTAMPi": ["check_series_anomaly_detector_output"],
"SeriesToCollectionBroadcaster": ["check_transform_inverse_transform_equivalent"],
"CollectionToSeriesWrapper": ["check_transform_inverse_transform_equivalent"],
# missed in legacy testing, changes state in predict/transform
"FLUSSSegmenter": ["check_non_state_changing_method"],
"ClaSPSegmenter": ["check_non_state_changing_method"],
Expand Down
5 changes: 2 additions & 3 deletions aeon/transformations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

import numpy as np
import pandas as pd
from sklearn.base import TransformerMixin

from aeon.base import BaseAeonEstimator


class BaseTransformer(BaseAeonEstimator):
class BaseTransformer(TransformerMixin, BaseAeonEstimator):
"""Transformer base class."""

_tags = {
Expand All @@ -24,8 +25,6 @@ class BaseTransformer(BaseAeonEstimator):

@abstractmethod
def __init__(self):
self._estimator_type = "transformer"

super().__init__()

@abstractmethod
Expand Down
7 changes: 5 additions & 2 deletions aeon/transformations/collection/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Collection transformations."""

__all__ = [
# base class and series wrapper
# base class and series broadcaster
"BaseCollectionTransformer",
"SeriesToCollectionBroadcaster",
# transformers
"AutocorrelationFunctionTransformer",
"ARCoefficientTransformer",
Expand All @@ -22,7 +23,6 @@

from aeon.transformations.collection._acf import AutocorrelationFunctionTransformer
from aeon.transformations.collection._ar_coefficient import ARCoefficientTransformer
from aeon.transformations.collection._broadcaster import SeriesToCollectionBroadcaster
from aeon.transformations.collection._downsample import DownsampleTransformer
from aeon.transformations.collection._dwt import DWTTransformer
from aeon.transformations.collection._hog1d import HOG1DTransformer
Expand All @@ -31,5 +31,8 @@
from aeon.transformations.collection._periodogram import PeriodogramTransformer
from aeon.transformations.collection._reduce import Tabularizer
from aeon.transformations.collection._rescale import Centerer, MinMaxScaler, Normalizer
from aeon.transformations.collection._series_broadcaster import (
SeriesToCollectionBroadcaster,
)
from aeon.transformations.collection._slope import SlopeTransformer
from aeon.transformations.collection.base import BaseCollectionTransformer
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,23 @@ class SeriesToCollectionBroadcaster(BaseCollectionTransformer):
"input_data_type": "Collection",
"output_data_type": "Collection",
"capability:unequal_length": True,
"capability:inverse_transform": True,
"X_inner_type": ["numpy3D", "np-list"],
}

def __init__(
self,
transformer: BaseSeriesTransformer,
) -> None:
# Setting tags before __init__() causes them to be overwritten. Hence we make
# a copy before init from the series transformer, then copy the tags of the
# BaseSeriesTransformer to this BaseCollectionTransformer
self.transformer = transformer

super().__init__()

# Setting tags before __init__() causes them to be overwritten.
tags_to_keep = SeriesToCollectionBroadcaster._tags
tags_to_add = transformer.get_tags()
for key in tags_to_keep:
tags_to_add.pop(key, None)
super().__init__()
self.set_tags(**tags_to_add)

def _fit(self, X, y=None):
Expand Down Expand Up @@ -100,14 +101,14 @@ def _transform(self, X, y=None):

"""
n_cases = get_n_cases(X)
"""If fit is empty is true only single transform is used."""
Xt = []
if self.get_tag("fit_is_empty"):
for i in range(n_cases):
Xt.append(self.transformer._transform(X[i]))
else:
for i in range(n_cases):
Xt.append(self.single_transformers_[i]._transform(X[i]))

# Need to make it a valid collection
for i in range(n_cases):
if isinstance(Xt[i], np.ndarray) and Xt[i].ndim == 1:
Expand All @@ -134,14 +135,19 @@ def _inverse_transform(self, X, y=None):
The transformed collection of time series, either a 3D numpy or a
list of 2D numpy.
"""
n_cases = len(X)
n_cases = get_n_cases(X)
Xt = []
if self.get_tag("fit_is_empty"):
for i in range(n_cases):
Xt.append(self.transformer._inverse_transform(X[i]))
else:
for i in range(n_cases):
Xt.append(self.single_transformers_[i]._inverse_transform(X[i]))

# Need to make it a valid collection
for i in range(n_cases):
if isinstance(Xt[i], np.ndarray) and Xt[i].ndim == 1:
Xt[i] = Xt[i].reshape(1, -1)
return Xt

@classmethod
Expand Down
17 changes: 9 additions & 8 deletions aeon/transformations/collection/tests/test_broadcaster.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Tests for SeriesToCollectionBroadcaster transformer."""

__maintainer__ = ["baraline"]

import pytest
from numpy.testing import assert_array_almost_equal

Expand All @@ -14,7 +12,9 @@
MockSeriesTransformerNoFit,
MockUnivariateSeriesTransformer,
)
from aeon.transformations.collection._broadcaster import SeriesToCollectionBroadcaster
from aeon.transformations.collection._series_broadcaster import (
SeriesToCollectionBroadcaster,
)


def test_broadcaster_tag_inheritance():
Expand All @@ -41,10 +41,9 @@ def test_broadcaster_tag_inheritance():
assert post_constructor_tags[key] == mock_tags[key]


df = [make_example_3d_numpy, make_example_3d_numpy_list]


@pytest.mark.parametrize("data_gen", df)
@pytest.mark.parametrize(
"data_gen", [make_example_3d_numpy, make_example_3d_numpy_list]
)
def test_broadcaster_methods_univariate(data_gen):
"""Test the broadcaster fit, transform and inverse transform method."""
X, y = data_gen(n_channels=1)
Expand All @@ -64,7 +63,9 @@ def test_broadcaster_methods_univariate(data_gen):
assert_array_almost_equal(X[i], X2[i])


@pytest.mark.parametrize("data_gen", df)
@pytest.mark.parametrize(
"data_gen", [make_example_3d_numpy, make_example_3d_numpy_list]
)
def test_broadcaster_methods_multivariate(data_gen):
"""Test the broadcaster fit, transform and inverse transform method."""
X, y = data_gen(n_channels=3)
Expand Down
2 changes: 2 additions & 0 deletions aeon/transformations/series/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
__all__ = [
"AutoCorrelationSeriesTransformer",
"BaseSeriesTransformer",
"CollectionToSeriesWrapper",
"ClaSPTransformer",
"Dobin",
"MatrixProfileTransformer",
Expand All @@ -27,6 +28,7 @@
from aeon.transformations.series._bkfilter import BKFilter
from aeon.transformations.series._boxcox import BoxCoxTransformer
from aeon.transformations.series._clasp import ClaSPTransformer
from aeon.transformations.series._collection_wrapper import CollectionToSeriesWrapper
from aeon.transformations.series._diff import DifferenceTransformer
from aeon.transformations.series._dobin import Dobin
from aeon.transformations.series._log import LogTransformer
Expand Down
104 changes: 104 additions & 0 deletions aeon/transformations/series/_collection_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Class to wrap a collection transformer for single series."""

__maintainer__ = ["MatthewMiddlehurst"]
__all__ = ["CollectionToSeriesWrapper"]


from aeon.transformations.collection.base import BaseCollectionTransformer
from aeon.transformations.series.base import BaseSeriesTransformer


class CollectionToSeriesWrapper(BaseSeriesTransformer):
"""Wrap a ``BaseCollectionTransformer`` to run on single series datatypes.

Parameters
----------
transformer : BaseCollectionTransformer
The collection transformer to wrap.

Examples
--------
>>> from aeon.transformations.series import CollectionToSeriesWrapper
>>> from aeon.transformations.collection.unequal_length import Resizer
>>> import numpy as np
>>> X = np.random.rand(1, 10)
>>> transformer = Resizer(resized_length=5)
>>> wrapper = CollectionToSeriesWrapper(transformer)
>>> X_t = wrapper.fit_transform(X)
"""

# These tags are not set from the collection transformer.
_tags = {
"input_data_type": "Series",
"output_data_type": "Series",
"capability:inverse_transform": True,
"X_inner_type": "np.ndarray",
}

def __init__(
self,
transformer: BaseCollectionTransformer,
) -> None:
self.transformer = transformer

super().__init__(axis=1)

# Setting tags before __init__() causes them to be overwritten.
tags_to_keep = CollectionToSeriesWrapper._tags
tags_to_add = transformer.get_tags()
for key in tags_to_keep:
tags_to_add.pop(key, None)
for key in ["capability:unequal_length", "removes_unequal_length"]:
tags_to_add.pop(key, None)
self.set_tags(**tags_to_add)

def _fit(self, X, y=None):
X = X.reshape(1, X.shape[0], X.shape[1])
self.collection_transformer_ = self.transformer.clone()
self.collection_transformer_.fit(X, y)

def _transform(self, X, y=None):
X = X.reshape(1, X.shape[0], X.shape[1])

t = self.transformer
if not self.get_tag("fit_is_empty"):
t = self.collection_transformer_

return t.transform(X, y)

def _fit_transform(self, X, y=None):
X = X.reshape(1, X.shape[0], X.shape[1])
self.collection_transformer_ = self.transformer.clone()
return self.collection_transformer_.fit_transform(X, y)

def _inverse_transform(self, X, y=None):
X = X.reshape(1, X.shape[0], X.shape[1])

t = self.transformer
if not self.get_tag("fit_is_empty"):
t = self.collection_transformer_

return t.inverse_transform(X, y)

@classmethod
def _get_test_params(cls, parameter_set="default"):
"""Return testing parameter settings for the estimator.

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.
"""
from aeon.testing.mock_estimators._mock_collection_transformers import (
MockCollectionTransformer,
)

return {"transformer": MockCollectionTransformer()}
24 changes: 24 additions & 0 deletions aeon/transformations/series/tests/test_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Tests for SeriesToCollectionBroadcaster transformer."""

from aeon.testing.mock_estimators import MockCollectionTransformer
from aeon.transformations.series import CollectionToSeriesWrapper


def test_broadcaster_tag_inheritance():
"""Test the ability to inherit tags from the BaseCollectionTransformer.

The broadcaster should always keep some tags related to single series
"""
trans = MockCollectionTransformer()
class_tags = CollectionToSeriesWrapper._tags

bc = CollectionToSeriesWrapper(trans)

post_constructor_tags = bc.get_tags()
mock_tags = trans.get_tags()
# constructor_tags should match class_tags or, if not present, tags in transformer
for key in post_constructor_tags:
if key in class_tags:
assert post_constructor_tags[key] == class_tags[key]
elif key in mock_tags:
assert post_constructor_tags[key] == mock_tags[key]