From f465960f1f27e7a26f731efb0ca73133f176dc3b Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Tue, 21 May 2024 20:49:01 +0100 Subject: [PATCH 1/4] convert Fourier --- aeon/transformations/scaledlogit.py | 8 + aeon/transformations/series/_scaled_logit.py | 197 ++++++++++++++++++ .../tests/test_scaled_logit.py} | 6 +- 3 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 aeon/transformations/series/_scaled_logit.py rename aeon/transformations/{tests/test_scaledlogit.py => series/tests/test_scaled_logit.py} (86%) diff --git a/aeon/transformations/scaledlogit.py b/aeon/transformations/scaledlogit.py index 80b8ef76fe..341632a9b1 100644 --- a/aeon/transformations/scaledlogit.py +++ b/aeon/transformations/scaledlogit.py @@ -7,10 +7,18 @@ from warnings import warn import numpy as np +from deprecated.sphinx import deprecated from aeon.transformations.base import BaseTransformer +# TODO: remove in v0.10.0 +@deprecated( + version="0.9.0", + reason="ScaledLogitTransformer will be removed in version 0.10 and replaced with a " + "BaseSeriesTransformer version in the transformations.series module.", + category=FutureWarning, +) class ScaledLogitTransformer(BaseTransformer): r"""Scaled logit transform or Log transform. diff --git a/aeon/transformations/series/_scaled_logit.py b/aeon/transformations/series/_scaled_logit.py new file mode 100644 index 0000000000..4ab2e2c7a9 --- /dev/null +++ b/aeon/transformations/series/_scaled_logit.py @@ -0,0 +1,197 @@ +"""Implements the scaled logit transformation.""" + +__maintainer__ = [] +__all__ = ["ScaledLogitTransformer"] + +from copy import deepcopy +from warnings import warn + +import numpy as np + +from aeon.transformations.series.base import BaseSeriesTransformer + + +class ScaledLogitTransformer(BaseSeriesTransformer): + r"""Scaled logit transform or Log transform. + + If both lower_bound and upper_bound are not None, a scaled logit transform is + applied to the data. Otherwise, the transform applied is a log transform variation + that ensures the resulting values from the inverse transform are bounded + accordingly. The transform is applied to all scalar elements of the input array + individually. + + Combined with an aeon.forecasting.compose.TransformedTargetForecaster, it ensures + that the forecast stays between the specified bounds (lower_bound, upper_bound). + + Default is lower_bound = upper_bound = None, i.e., the identity transform. + + The logarithm transform is obtained for lower_bound = 0, upper_bound = None. + + Parameters + ---------- + lower_bound : float, optional, default=None + lower bound of inverse transform function + upper_bound : float, optional, default=None + upper bound of inverse transform function + + See Also + -------- + aeon.transformations.boxcox.LogTransformer : + Transformer input data using natural log. Can help normalize data and + compress variance of the series. + aeon.transformations.boxcox.BoxCoxTransformer : + Applies Box-Cox power transformation. Can help normalize data and + compress variance of the series. + aeon.transformations.exponent.ExponentTransformer : + Transform input data by raising it to an exponent. Can help compress + variance of series if a fractional exponent is supplied. + aeon.transformations.exponent.SqrtTransformer : + Transform input data by taking its square root. Can help compress + variance of input series. + + Notes + ----- + | The scaled logit transform is applied if both upper_bound and lower_bound are + | not None: + | :math:`log(\frac{x - a}{b - x})`, where a is the lower and b is the upper bound. + + | If upper_bound is None and lower_bound is not None the transform applied is + | a log transform of the form: + | :math:`log(x - a)` + + | If lower_bound is None and upper_bound is not None the transform applied is + | a log transform of the form: + | :math:`- log(b - x)` + + References + ---------- + .. [1] Hyndsight - Forecasting within limits: + https://robjhyndman.com/hyndsight/forecasting-within-limits/ + .. [2] Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and + practice, 3rd edition, OTexts: Melbourne, Australia. OTexts.com/fpp3. + Accessed on January 24th 2022. + + Examples + -------- + >>> import numpy as np + >>> from aeon.datasets import load_airline + >>> from aeon.transformations.scaledlogit import BaseSeriesTransformer + >>> from aeon.forecasting.trend import PolynomialTrendForecaster + >>> from aeon.forecasting.compose import TransformedTargetForecaster + >>> y = load_airline() + >>> fcaster = TransformedTargetForecaster([ + ... ("scaled_logit", BaseSeriesTransformer(0, 650)), + ... ("poly", PolynomialTrendForecaster(degree=2)) + ... ]) + >>> fcaster.fit(y) + TransformedTargetForecaster(...) + >>> y_pred = fcaster.predict(fh = np.arange(32)) + """ + + _tags = { + "X_inner_type": "np.ndarray", + "fit_is_empty": True, + "capability:multivariate": True, + "capability:inverse_transform": True, + } + + def __init__(self, lower_bound=None, upper_bound=None): + self.lower_bound = lower_bound + self.upper_bound = upper_bound + + super().__init__(axis=0) + + def _transform(self, X, y=None): + """Transform X and return a transformed version. + + private _transform containing core logic, called from transform + + Parameters + ---------- + X : 2D np.ndarray + y : Ignored argument for interface compatibility + + Returns + ------- + transformed version of X + """ + if self.upper_bound is not None and np.any(X >= self.upper_bound): + warn( + "X in ScaledLogitTransformer should not have values " + "greater than upper_bound", + RuntimeWarning, + ) + + if self.lower_bound is not None and np.any(X <= self.lower_bound): + warn( + "X in ScaledLogitTransformer should not have values " + "lower than lower_bound", + RuntimeWarning, + ) + + if self.upper_bound and self.lower_bound: + X_transformed = np.log((X - self.lower_bound) / (self.upper_bound - X)) + elif self.upper_bound is not None: + X_transformed = -np.log(self.upper_bound - X) + elif self.lower_bound is not None: + X_transformed = np.log(X - self.lower_bound) + else: + X_transformed = deepcopy(X) + + return X_transformed + + def _inverse_transform(self, X, y=None): + """Inverse transform, inverse operation to transform. + + private _inverse_transform containing core logic, called from inverse_transform + + Parameters + ---------- + X : 2D np.ndarray + Data to be inverse transformed + y : data of y_inner_type, default=None + Ignored argument for interface compatibility + + Returns + ------- + inverse transformed version of X + """ + if self.upper_bound and self.lower_bound: + X_inv_transformed = (self.upper_bound * np.exp(X) + self.lower_bound) / ( + np.exp(X) + 1 + ) + elif self.upper_bound is not None: + X_inv_transformed = self.upper_bound - np.exp(-X) + elif self.lower_bound is not None: + X_inv_transformed = np.exp(X) + self.lower_bound + else: + X_inv_transformed = deepcopy(X) + + return X_inv_transformed + + @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. + `create_test_instance` uses the first (or only) dictionary in `params` + """ + test_params = [ + {"lower_bound": None, "upper_bound": None}, + {"lower_bound": -(10**6), "upper_bound": None}, + {"lower_bound": None, "upper_bound": 10**6}, + {"lower_bound": -(10**6), "upper_bound": 10**6}, + ] + return test_params diff --git a/aeon/transformations/tests/test_scaledlogit.py b/aeon/transformations/series/tests/test_scaled_logit.py similarity index 86% rename from aeon/transformations/tests/test_scaledlogit.py rename to aeon/transformations/series/tests/test_scaled_logit.py index 2cf5085d39..78cc3b4979 100644 --- a/aeon/transformations/tests/test_scaledlogit.py +++ b/aeon/transformations/series/tests/test_scaled_logit.py @@ -8,7 +8,7 @@ import pytest from aeon.datasets import load_airline -from aeon.transformations.scaledlogit import ScaledLogitTransformer +from aeon.transformations.series._scaled_logit import ScaledLogitTransformer TEST_SERIES = np.array([30, 40, 60]) @@ -22,7 +22,7 @@ (None, None, TEST_SERIES), ], ) -def test_scaledlogit_transform(lower, upper, output): +def test_scaled_logit_transform(lower, upper, output): """Test that we get the right output.""" transformer = ScaledLogitTransformer(lower, upper) y_transformed = transformer.fit_transform(TEST_SERIES) @@ -47,7 +47,7 @@ def test_scaledlogit_transform(lower, upper, output): ), ], ) -def test_scaledlogit_bound_errors(lower, upper, message): +def test_scaled_logit_bound_errors(lower, upper, message): """Tests all exceptions.""" y = load_airline() with pytest.warns(RuntimeWarning): From 8a1a633b884e88ef59309b0608d00b49b5fd8140 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Wed, 22 May 2024 08:41:26 +0100 Subject: [PATCH 2/4] docstring --- aeon/transformations/scaledlogit.py | 12 ++++++------ aeon/transformations/series/_scaled_logit.py | 12 ++++++------ .../series/tests/test_scaled_logit.py | 11 ++++++----- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/aeon/transformations/scaledlogit.py b/aeon/transformations/scaledlogit.py index 341632a9b1..843fc4c24d 100644 --- a/aeon/transformations/scaledlogit.py +++ b/aeon/transformations/scaledlogit.py @@ -15,8 +15,8 @@ # TODO: remove in v0.10.0 @deprecated( version="0.9.0", - reason="ScaledLogitTransformer will be removed in version 0.10 and replaced with a " - "BaseSeriesTransformer version in the transformations.series module.", + reason="ScaledLogitSeriesTransformer will be removed in version 0.10 and replaced " + "with a BaseSeriesTransformer version in the transformations.series module.", category=FutureWarning, ) class ScaledLogitTransformer(BaseTransformer): @@ -83,12 +83,12 @@ class ScaledLogitTransformer(BaseTransformer): -------- >>> import numpy as np >>> from aeon.datasets import load_airline - >>> from aeon.transformations.scaledlogit import ScaledLogitTransformer + >>> from aeon.transformations.scaledlogit import ScaledLogitSeriesTransformer >>> from aeon.forecasting.trend import PolynomialTrendForecaster >>> from aeon.forecasting.compose import TransformedTargetForecaster >>> y = load_airline() >>> fcaster = TransformedTargetForecaster([ - ... ("scaled_logit", ScaledLogitTransformer(0, 650)), + ... ("scaled_logit", ScaledLogitSeriesTransformer(0, 650)), ... ("poly", PolynomialTrendForecaster(degree=2)) ... ]) >>> fcaster.fit(y) @@ -135,14 +135,14 @@ def _transform(self, X, y=None): """ if self.upper_bound is not None and np.any(X >= self.upper_bound): warn( - "X in ScaledLogitTransformer should not have values " + "X in ScaledLogitSeriesTransformer should not have values " "greater than upper_bound", RuntimeWarning, ) if self.lower_bound is not None and np.any(X <= self.lower_bound): warn( - "X in ScaledLogitTransformer should not have values " + "X in ScaledLogitSeriesTransformer should not have values " "lower than lower_bound", RuntimeWarning, ) diff --git a/aeon/transformations/series/_scaled_logit.py b/aeon/transformations/series/_scaled_logit.py index 4ab2e2c7a9..ae7e71cc11 100644 --- a/aeon/transformations/series/_scaled_logit.py +++ b/aeon/transformations/series/_scaled_logit.py @@ -1,7 +1,7 @@ """Implements the scaled logit transformation.""" __maintainer__ = [] -__all__ = ["ScaledLogitTransformer"] +__all__ = ["ScaledLogitSeriesTransformer"] from copy import deepcopy from warnings import warn @@ -11,7 +11,7 @@ from aeon.transformations.series.base import BaseSeriesTransformer -class ScaledLogitTransformer(BaseSeriesTransformer): +class ScaledLogitSeriesTransformer(BaseSeriesTransformer): r"""Scaled logit transform or Log transform. If both lower_bound and upper_bound are not None, a scaled logit transform is @@ -75,12 +75,12 @@ class ScaledLogitTransformer(BaseSeriesTransformer): -------- >>> import numpy as np >>> from aeon.datasets import load_airline - >>> from aeon.transformations.scaledlogit import BaseSeriesTransformer + >>> from aeon.transformations.scaledlogit import ScaledLogitSeriesTransformer >>> from aeon.forecasting.trend import PolynomialTrendForecaster >>> from aeon.forecasting.compose import TransformedTargetForecaster >>> y = load_airline() >>> fcaster = TransformedTargetForecaster([ - ... ("scaled_logit", BaseSeriesTransformer(0, 650)), + ... ("scaled_logit", ScaledLogitSeriesTransformer(0, 650)), ... ("poly", PolynomialTrendForecaster(degree=2)) ... ]) >>> fcaster.fit(y) @@ -117,14 +117,14 @@ def _transform(self, X, y=None): """ if self.upper_bound is not None and np.any(X >= self.upper_bound): warn( - "X in ScaledLogitTransformer should not have values " + "X in ScaledLogitSeriesTransformer should not have values " "greater than upper_bound", RuntimeWarning, ) if self.lower_bound is not None and np.any(X <= self.lower_bound): warn( - "X in ScaledLogitTransformer should not have values " + "X in ScaledLogitSeriesTransformer should not have values " "lower than lower_bound", RuntimeWarning, ) diff --git a/aeon/transformations/series/tests/test_scaled_logit.py b/aeon/transformations/series/tests/test_scaled_logit.py index 78cc3b4979..adb8c88085 100644 --- a/aeon/transformations/series/tests/test_scaled_logit.py +++ b/aeon/transformations/series/tests/test_scaled_logit.py @@ -8,7 +8,7 @@ import pytest from aeon.datasets import load_airline -from aeon.transformations.series._scaled_logit import ScaledLogitTransformer +from aeon.transformations.series._scaled_logit import ScaledLogitSeriesTransformer TEST_SERIES = np.array([30, 40, 60]) @@ -24,7 +24,7 @@ ) def test_scaled_logit_transform(lower, upper, output): """Test that we get the right output.""" - transformer = ScaledLogitTransformer(lower, upper) + transformer = ScaledLogitSeriesTransformer(lower, upper) y_transformed = transformer.fit_transform(TEST_SERIES) assert np.all(output == y_transformed) @@ -36,14 +36,15 @@ def test_scaled_logit_transform(lower, upper, output): 0, 300, ( - "X in ScaledLogitTransformer should not have values greater" + "X in ScaledLogitSeriesTransformer should not have values greater" "than upper_bound" ), ), ( 300, 700, - "X in ScaledLogitTransformer should not have values lower than lower_bound", + "X in ScaledLogitSeriesTransformer should not have values lower than " + "lower_bound", ), ], ) @@ -51,5 +52,5 @@ def test_scaled_logit_bound_errors(lower, upper, message): """Tests all exceptions.""" y = load_airline() with pytest.warns(RuntimeWarning): - ScaledLogitTransformer(lower, upper).fit_transform(y) + ScaledLogitSeriesTransformer(lower, upper).fit_transform(y) warn(message, RuntimeWarning) From 09b1d3bc1814acfb793c4bc85f601440c0859546 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Wed, 22 May 2024 08:58:36 +0100 Subject: [PATCH 3/4] docstring --- aeon/transformations/scaledlogit.py | 16 ++++------------ aeon/transformations/series/_scaled_logit.py | 4 ++-- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/aeon/transformations/scaledlogit.py b/aeon/transformations/scaledlogit.py index 843fc4c24d..80b8ef76fe 100644 --- a/aeon/transformations/scaledlogit.py +++ b/aeon/transformations/scaledlogit.py @@ -7,18 +7,10 @@ from warnings import warn import numpy as np -from deprecated.sphinx import deprecated from aeon.transformations.base import BaseTransformer -# TODO: remove in v0.10.0 -@deprecated( - version="0.9.0", - reason="ScaledLogitSeriesTransformer will be removed in version 0.10 and replaced " - "with a BaseSeriesTransformer version in the transformations.series module.", - category=FutureWarning, -) class ScaledLogitTransformer(BaseTransformer): r"""Scaled logit transform or Log transform. @@ -83,12 +75,12 @@ class ScaledLogitTransformer(BaseTransformer): -------- >>> import numpy as np >>> from aeon.datasets import load_airline - >>> from aeon.transformations.scaledlogit import ScaledLogitSeriesTransformer + >>> from aeon.transformations.scaledlogit import ScaledLogitTransformer >>> from aeon.forecasting.trend import PolynomialTrendForecaster >>> from aeon.forecasting.compose import TransformedTargetForecaster >>> y = load_airline() >>> fcaster = TransformedTargetForecaster([ - ... ("scaled_logit", ScaledLogitSeriesTransformer(0, 650)), + ... ("scaled_logit", ScaledLogitTransformer(0, 650)), ... ("poly", PolynomialTrendForecaster(degree=2)) ... ]) >>> fcaster.fit(y) @@ -135,14 +127,14 @@ def _transform(self, X, y=None): """ if self.upper_bound is not None and np.any(X >= self.upper_bound): warn( - "X in ScaledLogitSeriesTransformer should not have values " + "X in ScaledLogitTransformer should not have values " "greater than upper_bound", RuntimeWarning, ) if self.lower_bound is not None and np.any(X <= self.lower_bound): warn( - "X in ScaledLogitSeriesTransformer should not have values " + "X in ScaledLogitTransformer should not have values " "lower than lower_bound", RuntimeWarning, ) diff --git a/aeon/transformations/series/_scaled_logit.py b/aeon/transformations/series/_scaled_logit.py index ae7e71cc11..ee9fe559bb 100644 --- a/aeon/transformations/series/_scaled_logit.py +++ b/aeon/transformations/series/_scaled_logit.py @@ -75,12 +75,12 @@ class ScaledLogitSeriesTransformer(BaseSeriesTransformer): -------- >>> import numpy as np >>> from aeon.datasets import load_airline - >>> from aeon.transformations.scaledlogit import ScaledLogitSeriesTransformer + >>> import aeon.transformations.series._scaled_logit as sl >>> from aeon.forecasting.trend import PolynomialTrendForecaster >>> from aeon.forecasting.compose import TransformedTargetForecaster >>> y = load_airline() >>> fcaster = TransformedTargetForecaster([ - ... ("scaled_logit", ScaledLogitSeriesTransformer(0, 650)), + ... ("scaled_logit", sl.ScaledLogitSeriesTransformer(0, 650)), ... ("poly", PolynomialTrendForecaster(degree=2)) ... ]) >>> fcaster.fit(y) From 9ba240044526f141abfbdbf8322be0bf284b70d4 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Wed, 22 May 2024 09:07:02 +0100 Subject: [PATCH 4/4] docstring --- aeon/transformations/series/_scaled_logit.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/aeon/transformations/series/_scaled_logit.py b/aeon/transformations/series/_scaled_logit.py index ee9fe559bb..1d1ff584b9 100644 --- a/aeon/transformations/series/_scaled_logit.py +++ b/aeon/transformations/series/_scaled_logit.py @@ -63,6 +63,10 @@ class ScaledLogitSeriesTransformer(BaseSeriesTransformer): | a log transform of the form: | :math:`- log(b - x)` + | The transform is independent of the axis, so the data can be shape + | `` (n_timepoints, n_channels)`` (axis == 0) or + | ``(n_channels, n_timepoints)`` (axis ==1) + References ---------- .. [1] Hyndsight - Forecasting within limits: @@ -99,7 +103,7 @@ def __init__(self, lower_bound=None, upper_bound=None): self.lower_bound = lower_bound self.upper_bound = upper_bound - super().__init__(axis=0) + super().__init__() def _transform(self, X, y=None): """Transform X and return a transformed version. @@ -109,6 +113,7 @@ def _transform(self, X, y=None): Parameters ---------- X : 2D np.ndarray + Time series of shape (n_timepoints, n_channels) y : Ignored argument for interface compatibility Returns @@ -149,8 +154,7 @@ def _inverse_transform(self, X, y=None): ---------- X : 2D np.ndarray Data to be inverse transformed - y : data of y_inner_type, default=None - Ignored argument for interface compatibility + y : Ignored argument for interface compatibility Returns -------