From 2520add430d79aaed459a822bb765ed5c7ad9e8e Mon Sep 17 00:00:00 2001 From: datedote Date: Sat, 31 Aug 2024 11:57:23 -0700 Subject: [PATCH 1/8] start / add _exp_smoothing.py --- aeon/transformations/series/_exp_smoothing.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 aeon/transformations/series/_exp_smoothing.py diff --git a/aeon/transformations/series/_exp_smoothing.py b/aeon/transformations/series/_exp_smoothing.py new file mode 100644 index 0000000000..641b0c4b62 --- /dev/null +++ b/aeon/transformations/series/_exp_smoothing.py @@ -0,0 +1,92 @@ +"""Exponential smoothing transformation.""" + +__maintainer__ = ["Datadote"] +__all__ = "ExpSmoothingSeriesTransformer" + +from typing import Union + +import numpy as np + +from aeon.transformations.series.base import BaseSeriesTransformer + + +class ExpSmoothingSeriesTransformer(BaseSeriesTransformer): + """Filter a time series using exponential smoothing. + + Parameters + ---------- + alpha: float, default=0.2 + decaying weight. Range [0, 1]. Overwritten by window_size if window_size exists + window_size: int or float or None, default=None + If window_size is specified, alpha is set to 2. / (window_size + 1) + + References + ---------- + Large, J., Southam, P., Bagnall, A. (2019). + Can Automated Smoothing Significantly Improve Benchmark Time Series + Classification Algorithms?. In: Pérez García, H., Sánchez González, + L., CastejónLimas, M., Quintián Pardo, H., Corchado Rodríguez, E. (eds) Hybrid + Artificial Intelligent Systems. HAIS 2019. Lecture Notes in Computer Science(), + vol 11734. Springer, Cham. https://doi.org/10.1007/978-3-030-29859-3_5 + https://arxiv.org/abs/1811.00894 + + Examples + -------- + >>> import numpy as np + >>> from aeon.transformations.series._exp_smoothing import \ + >>> ExpSmoothingSeriesTransformer + >>> X = np.array([-2, -1, 0, 1, 2]) + >>> transformer = ExpSmoothingSeriesTransformer(0.5) + >>> Xt = transformer.fit_transform(X) + >>> print(Xt) + [[-2. -1.5 -0.75 0.125 1.0625]] + >>> X = np.array([[1, 2, 3, 4], + >>> [10, 9, 8, 7]]) + >>> Xt = transformer.fit_transform(X) + >>> print(Xt) + [[ 1. 1.5 2.25 3.125] + [10. 9.5 8.75 7.875]] + """ + + _tags = { + "capability:multivariate": True, + "X_inner_type": "np.ndarray", + "fit_is_empty": True, + } + + def __init__( + self, alpha: float = 0.2, window_size: Union[int, float, None] = None + ) -> None: + if not 0 <= alpha <= 1: + raise ValueError(f"alpha must be in range [0, 1], got {alpha}") + if window_size is not None and window_size <= 0: + raise ValueError(f"window_size must be > 0, got {window_size}") + super().__init__(axis=0) + self.alpha = alpha if window_size is None else 2.0 / (window_size + 1) + self.window_size = window_size + + def _transform(self, X, y=None): + """Transform X and return a transformed version. + + private _transform containing core logic, called from transform + + Parameters + ---------- + X : np.ndarray + Data to be transformed + y : ignored argument for interface compatibility + Additional data, e.g., labels for transformation + + Returns + ------- + Xt: 2D np.ndarray + transformed version of X + """ + X = X.astype("float") + if X.ndim == 1: + X = X.reshape(-1, 1) + Xt = np.zeros_like(X) + Xt[0, :] = X[0, :] + for i in range(1, Xt.shape[0]): + Xt[i, :] = self.alpha * X[i, :] + (1 - self.alpha) * Xt[i - 1, :] + return Xt From cade40252efcde4998464d17d6f5b5dbfaef45f3 Mon Sep 17 00:00:00 2001 From: datedote Date: Sat, 31 Aug 2024 11:57:35 -0700 Subject: [PATCH 2/8] start / add tests_exp_smoothing.py --- .../series/tests/test_exp_smoothing.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 aeon/transformations/series/tests/test_exp_smoothing.py diff --git a/aeon/transformations/series/tests/test_exp_smoothing.py b/aeon/transformations/series/tests/test_exp_smoothing.py new file mode 100644 index 0000000000..cb83a4e7da --- /dev/null +++ b/aeon/transformations/series/tests/test_exp_smoothing.py @@ -0,0 +1,60 @@ +"""Tests for ExpSmoothingSeriesTransformer.""" + +__maintainer__ = ["Datadote"] + +import numpy as np +import pytest + +from aeon.transformations.series._exp_smoothing import ExpSmoothingSeriesTransformer + +TEST_DATA = [np.array([-2, -1, 0, 1, 2]), np.array([[1, 2, 3, 4], [10, 9, 8, 7]])] +EXPECTED_RESULTS = [ + np.array([[-2.0, -1.5, -0.75, 0.125, 1.0625]]), + np.array([[1.0, 1.5, 2.25, 3.125], [10.0, 9.5, 8.75, 7.875]]), +] + + +def test_input_1d_array(): + """Test inputs of dimension 1.""" + transformer = ExpSmoothingSeriesTransformer(0.5) + idx_data = 0 + Xt = transformer.fit_transform(TEST_DATA[idx_data]) + np.testing.assert_almost_equal(Xt, EXPECTED_RESULTS[idx_data], decimal=5) + + +def test_input_2d_array(): + """Test inputs of dimension 2.""" + transformer = ExpSmoothingSeriesTransformer(0.5) + idx_data = 1 + Xt = transformer.fit_transform(TEST_DATA[idx_data]) + np.testing.assert_almost_equal(Xt, EXPECTED_RESULTS[idx_data], decimal=5) + + +@pytest.mark.parametrize("alpha_window", [(0.2, 9), (0.5, 3), (1, 1)]) +def test_window_size_matches_alpha(alpha_window): + """Check same output results using equivalent alpha and window_size.""" + alpha, window_size = alpha_window + transformer1 = ExpSmoothingSeriesTransformer(alpha=alpha) + transformer2 = ExpSmoothingSeriesTransformer(window_size=window_size) + for i in range(len(TEST_DATA)): + Xt1 = transformer1.fit_transform(TEST_DATA[i]) + Xt2 = transformer2.fit_transform(TEST_DATA[i]) + np.testing.assert_array_almost_equal(Xt1, Xt2, decimal=5) + + +def test_alpha_less_than_zero(): + """Test alpha less than zero.""" + with pytest.raises(ValueError): + ExpSmoothingSeriesTransformer(-0.5) + + +def test_alpha_greater_than_one(): + """Test alpha greater than one.""" + with pytest.raises(ValueError): + ExpSmoothingSeriesTransformer(2.0) + + +def test_window_size_than_one(): + """Test window_size < 0.""" + with pytest.raises(ValueError): + ExpSmoothingSeriesTransformer(window_size=0) From 2b02e1aea2e8feb068928b4241bc307d914ecddf Mon Sep 17 00:00:00 2001 From: datedote Date: Sat, 31 Aug 2024 13:17:12 -0700 Subject: [PATCH 3/8] Fix next line bugs in docstring example, removed >>> for multi line code --- aeon/transformations/series/_exp_smoothing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aeon/transformations/series/_exp_smoothing.py b/aeon/transformations/series/_exp_smoothing.py index 641b0c4b62..cfb291b035 100644 --- a/aeon/transformations/series/_exp_smoothing.py +++ b/aeon/transformations/series/_exp_smoothing.py @@ -34,14 +34,13 @@ class ExpSmoothingSeriesTransformer(BaseSeriesTransformer): -------- >>> import numpy as np >>> from aeon.transformations.series._exp_smoothing import \ - >>> ExpSmoothingSeriesTransformer + ExpSmoothingSeriesTransformer >>> X = np.array([-2, -1, 0, 1, 2]) >>> transformer = ExpSmoothingSeriesTransformer(0.5) >>> Xt = transformer.fit_transform(X) >>> print(Xt) [[-2. -1.5 -0.75 0.125 1.0625]] - >>> X = np.array([[1, 2, 3, 4], - >>> [10, 9, 8, 7]]) + >>> X = np.array([[1, 2, 3, 4], [10, 9, 8, 7]]) >>> Xt = transformer.fit_transform(X) >>> print(Xt) [[ 1. 1.5 2.25 3.125] From 546029381e734485b39b6497d280dc6b06ca3b3d Mon Sep 17 00:00:00 2001 From: datedote Date: Sat, 31 Aug 2024 14:22:52 -0700 Subject: [PATCH 4/8] add space to docstring example output --- aeon/transformations/series/_exp_smoothing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/transformations/series/_exp_smoothing.py b/aeon/transformations/series/_exp_smoothing.py index cfb291b035..502107a51d 100644 --- a/aeon/transformations/series/_exp_smoothing.py +++ b/aeon/transformations/series/_exp_smoothing.py @@ -44,7 +44,7 @@ class ExpSmoothingSeriesTransformer(BaseSeriesTransformer): >>> Xt = transformer.fit_transform(X) >>> print(Xt) [[ 1. 1.5 2.25 3.125] - [10. 9.5 8.75 7.875]] + [10. 9.5 8.75 7.875]] """ _tags = { From d6754ceea064de22365c94069d5e54316cd61732 Mon Sep 17 00:00:00 2001 From: datedote Date: Tue, 3 Sep 2024 15:05:59 -0700 Subject: [PATCH 5/8] make __all__ into a list --- aeon/transformations/series/_exp_smoothing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aeon/transformations/series/_exp_smoothing.py b/aeon/transformations/series/_exp_smoothing.py index 502107a51d..dee4c271ac 100644 --- a/aeon/transformations/series/_exp_smoothing.py +++ b/aeon/transformations/series/_exp_smoothing.py @@ -1,7 +1,7 @@ """Exponential smoothing transformation.""" __maintainer__ = ["Datadote"] -__all__ = "ExpSmoothingSeriesTransformer" +__all__ = ["ExpSmoothingSeriesTransformer"] from typing import Union @@ -54,7 +54,9 @@ class ExpSmoothingSeriesTransformer(BaseSeriesTransformer): } def __init__( - self, alpha: float = 0.2, window_size: Union[int, float, None] = None + self, + alpha:float = 0.2, + window_size: Union[int, float, None] = None ) -> None: if not 0 <= alpha <= 1: raise ValueError(f"alpha must be in range [0, 1], got {alpha}") From 6cb570cd1d9db427406ce615f66814ac8b6e4291 Mon Sep 17 00:00:00 2001 From: datedote Date: Tue, 3 Sep 2024 15:28:19 -0700 Subject: [PATCH 6/8] add exp smoothing description to class and precommit changes --- aeon/transformations/series/_exp_smoothing.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/aeon/transformations/series/_exp_smoothing.py b/aeon/transformations/series/_exp_smoothing.py index dee4c271ac..bc876826b3 100644 --- a/aeon/transformations/series/_exp_smoothing.py +++ b/aeon/transformations/series/_exp_smoothing.py @@ -13,6 +13,13 @@ class ExpSmoothingSeriesTransformer(BaseSeriesTransformer): """Filter a time series using exponential smoothing. + - Exponential smoothing (EXP) is a generalisaton of moving average smoothing that + assigns a decaying weight to each element rather than averaging over a window. + - Assume time series T = [t_0, ..., t_j], and smoothed values S = [s_0, ..., s_j] + - Then, s_0 = t_0 and s_j = alpha * t_j + (1 - alpha) * s_j-1 + where 0 ≤ alpha ≤ 1. If window_size is given, alpha is overwritten, and set as + alpha = 2. / (window_size + 1) + Parameters ---------- alpha: float, default=0.2 @@ -54,9 +61,7 @@ class ExpSmoothingSeriesTransformer(BaseSeriesTransformer): } def __init__( - self, - alpha:float = 0.2, - window_size: Union[int, float, None] = None + self, alpha: float = 0.2, window_size: Union[int, float, None] = None ) -> None: if not 0 <= alpha <= 1: raise ValueError(f"alpha must be in range [0, 1], got {alpha}") From 0ed378dd345d204a153c60a7d37fc4738e14c805 Mon Sep 17 00:00:00 2001 From: datedote Date: Thu, 5 Sep 2024 13:32:35 -0700 Subject: [PATCH 7/8] Remove redundant input shape check --- aeon/transformations/series/_exp_smoothing.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aeon/transformations/series/_exp_smoothing.py b/aeon/transformations/series/_exp_smoothing.py index bc876826b3..d59d0f9c77 100644 --- a/aeon/transformations/series/_exp_smoothing.py +++ b/aeon/transformations/series/_exp_smoothing.py @@ -88,10 +88,7 @@ def _transform(self, X, y=None): Xt: 2D np.ndarray transformed version of X """ - X = X.astype("float") - if X.ndim == 1: - X = X.reshape(-1, 1) - Xt = np.zeros_like(X) + Xt = np.zeros_like(X, dtype="float") Xt[0, :] = X[0, :] for i in range(1, Xt.shape[0]): Xt[i, :] = self.alpha * X[i, :] + (1 - self.alpha) * Xt[i - 1, :] From e12a10d1341229e38d64b4f6623ed9f6dee614ba Mon Sep 17 00:00:00 2001 From: datedote Date: Sun, 15 Sep 2024 14:00:41 -0700 Subject: [PATCH 8/8] change super().init__(axis=0) to super().init__(axis=1) --- aeon/transformations/series/_exp_smoothing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aeon/transformations/series/_exp_smoothing.py b/aeon/transformations/series/_exp_smoothing.py index d59d0f9c77..5566769463 100644 --- a/aeon/transformations/series/_exp_smoothing.py +++ b/aeon/transformations/series/_exp_smoothing.py @@ -67,7 +67,7 @@ def __init__( raise ValueError(f"alpha must be in range [0, 1], got {alpha}") if window_size is not None and window_size <= 0: raise ValueError(f"window_size must be > 0, got {window_size}") - super().__init__(axis=0) + super().__init__(axis=1) self.alpha = alpha if window_size is None else 2.0 / (window_size + 1) self.window_size = window_size @@ -89,7 +89,7 @@ def _transform(self, X, y=None): transformed version of X """ Xt = np.zeros_like(X, dtype="float") - Xt[0, :] = X[0, :] - for i in range(1, Xt.shape[0]): - Xt[i, :] = self.alpha * X[i, :] + (1 - self.alpha) * Xt[i - 1, :] + Xt[:, 0] = X[:, 0] + for i in range(1, Xt.shape[1]): + Xt[:, i] = self.alpha * X[:, i] + (1 - self.alpha) * Xt[:, i - 1] return Xt