Repository: aeon-toolkit/aeon (python)

Issue:
[MNT] Overnight tests failing
### Describe the issue

overnights are failing due to slow tests for shape_dtw and occasionally TWE exceeding  the 1800 secs limit when numba is turned off. 

### Suggest a potential alternative/fix

We should either reduce the data test size or even exclude them from the non numba tests. The time limit is  30 mins, we will get to the point the overnight tests take more than the night to run!

=========================== short test summary info ============================
FAILED aeon/distances/tests/test_sklearn_compatibility.py::test_multivariate[task0-3-dist14] - Failed: Timeout >1800.0s
FAILED aeon/regression/distance_based/tests/test_time_series_neighbors.py::test_knn_neighbors[twe] - Failed: Timeout >1800.0s

### Additional context

_No response_

Pull request:
[ENH] Speed up the regression distances tests
hopefully fixes #2051 by sorting out sources of timeout when numba is off by replacing baked in data with random data

Edit, just seen #1991 

Patch:
diff --git a/aeon/transformations/series/__init__.py b/aeon/transformations/series/__init__.py
index cf2ba2bd..2ee1c0c8 100644
--- a/aeon/transformations/series/__init__.py
+++ b/aeon/transformations/series/__init__.py
@@ -17,6 +17,7 @@ __all__ = [
     "BoxCoxTransformer",
     "Dobin",
     "ScaledLogitSeriesTransformer",
+    "SIVSeriesTransformer",
     "PCASeriesTransformer",
 ]
 
@@ -36,5 +37,6 @@ from aeon.transformations.series._pca import PCASeriesTransformer
 from aeon.transformations.series._pla import PLASeriesTransformer
 from aeon.transformations.series._scaled_logit import ScaledLogitSeriesTransformer
 from aeon.transformations.series._sg import SGSeriesTransformer
+from aeon.transformations.series._siv import SIVSeriesTransformer
 from aeon.transformations.series._theta import ThetaTransformer
 from aeon.transformations.series.base import BaseSeriesTransformer
diff --git a/aeon/transformations/series/_siv.py b/aeon/transformations/series/_siv.py
new file mode 100644
index 00000000..ac511ae0
--- /dev/null
+++ b/aeon/transformations/series/_siv.py
@@ -0,0 +1,86 @@
+"""Recursive Median Sieve filter transformation."""
+
+__maintainer__ = ["Cyril-Meyer"]
+__all__ = ["SIVSeriesTransformer"]
+
+
+import numpy as np
+from scipy.ndimage import median_filter
+
+from aeon.transformations.series.base import BaseSeriesTransformer
+
+
+class SIVSeriesTransformer(BaseSeriesTransformer):
+    """Filter a times series using Recursive Median Sieve (SIV).
+
+    Parameters
+    ----------
+    window_length : list of int or int, default=[3, 5, 7]
+        The filter windows lenths (recommended increasing value).
+
+    Notes
+    -----
+    Use scipy.ndimage.median_filter instead of scipy.signal.medfilt :
+    The more general function scipy.ndimage.median_filter has a more efficient
+    implementation of a median filter and therefore runs much faster.
+    https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.medfilt.html
+
+    References
+    ----------
+    .. [1] Bangham J. A. (1988).
+       Data-sieving hydrophobicity plots.
+       Analytical biochemistry, 174(1), 142–145.
+       https://doi.org/10.1016/0003-2697(88)90528-3
+    .. [2] Yli-Harja, O., Koivisto, P., Bangham, J. A., Cawley, G.,
+       Harvey, R., & Shmulevich, I. (2001).
+       Simplified implementation of the recursive median sieve.
+       Signal Process., 81(7), 1565–1570.
+       https://doi.org/10.1016/S0165-1684(01)00054-8
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.transformations.series._siv import SIVSeriesTransformer
+    >>> X = np.random.random((2, 100)) # Random series length 100
+    >>> siv = SIVSeriesTransformer()
+    >>> X_ = siv.fit_transform(X)
+    >>> X_.shape
+    (2, 100)
+    """
+
+    _tags = {
+        "capability:multivariate": True,
+        "X_inner_type": "np.ndarray",
+        "fit_is_empty": True,
+    }
+
+    def __init__(self, window_length=None):
+        self.window_length = window_length
+        super().__init__(axis=1)
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        Parameters
+        ----------
+        X : np.ndarray
+            time series in shape (n_channels, n_timepoints)
+        y : ignored argument for interface compatibility
+
+        Returns
+        -------
+        transformed version of X
+        """
+        if self.window_length is None:
+            self.window_length = [3, 5, 7]
+        if not isinstance(self.window_length, list):
+            self.window_length = [self.window_length]
+
+        # Compute SIV
+        X_ = X
+
+        for window_length in self.window_length:
+            footprint = np.ones((1, window_length))
+            X_ = median_filter(X_, footprint=footprint)
+
+        return X_
diff --git a/docs/api_reference/transformations.rst b/docs/api_reference/transformations.rst
index 58be8632..d56e9c6a 100644
--- a/docs/api_reference/transformations.rst
+++ b/docs/api_reference/transformations.rst
@@ -202,6 +202,14 @@ Filtering and denoising
 
     SGSeriesTransformer
 
+.. currentmodule:: aeon.transformations.series._siv
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: class.rst
+
+    SIVSeriesTransformer
+
 Slope
 ~~~~~~~~~~~~~~~~~~~~~~
 


Test patch:
diff --git a/aeon/distances/tests/test_sklearn_compatibility.py b/aeon/distances/tests/test_sklearn_compatibility.py
index 9d5bd65a..0faa7b82 100644
--- a/aeon/distances/tests/test_sklearn_compatibility.py
+++ b/aeon/distances/tests/test_sklearn_compatibility.py
@@ -9,12 +9,6 @@ from sklearn.preprocessing import FunctionTransformer
 from sklearn.svm import SVR
 
 from aeon.classification.distance_based import KNeighborsTimeSeriesClassifier
-from aeon.datasets import (
-    load_basic_motions,
-    load_cardano_sentiment,
-    load_covid_3month,
-    load_unit_test,
-)
 from aeon.distances._distance import DISTANCES
 from aeon.regression.distance_based import KNeighborsTimeSeriesRegressor
 from aeon.testing.data_generation import make_example_3d_numpy
@@ -41,10 +35,10 @@ def test_function_transformer(dist):
 def test_distance_based(dist):
     """Test all distances work with KNN in a pipeline."""
     X, y = make_example_3d_numpy(
-        n_cases=5, n_channels=1, n_timepoints=10, regression_target=True
+        n_cases=6, n_channels=1, n_timepoints=10, regression_target=True
     )
     X2, y2 = make_example_3d_numpy(
-        n_cases=10, n_channels=1, n_timepoints=10, regression_target=True
+        n_cases=6, n_channels=1, n_timepoints=10, regression_target=True
     )
     pairwise = dist["pairwise_distance"]
     Xt = pairwise(X)
@@ -73,8 +67,8 @@ def test_clusterer(dist):
 @pytest.mark.parametrize(
     "task",
     [
-        ["cls", load_unit_test, KNeighborsClassifier, KNeighborsTimeSeriesClassifier],
-        ["reg", load_covid_3month, KNeighborsRegressor, KNeighborsTimeSeriesRegressor],
+        ["cls", KNeighborsClassifier, KNeighborsTimeSeriesClassifier],
+        ["reg", KNeighborsRegressor, KNeighborsTimeSeriesRegressor],
     ],
 )
 def test_univariate(dist, k, task):
@@ -85,36 +79,41 @@ def test_univariate(dist, k, task):
         return
 
     # Test univariate with 2D format (compatible with sklearn)
-    problem_type, problem_loader, knn_sk_func, knn_aeon_func = task
+    problem_type, knn_sk_func, knn_aeon_func = task
 
-    # Load the unit test dataset as a 2D numpy array
-    X_train, y_train = problem_loader(split="train", return_type="numpy2D")
-    X_test, y_test = problem_loader(split="test", return_type="numpy2D")
-    indices = np.random.RandomState(0).choice(
-        min(len(y_test), len(y_train)), 6, replace=False
+    # Create a collection as a 2D numpy array
+    if problem_type == "cls":
+        reg = False
+    else:
+        reg = True
+    X_train, y_train = make_example_3d_numpy(
+        random_state=0, n_cases=6, regression_target=reg
+    )
+    X_test, y_test = make_example_3d_numpy(
+        random_state=2, n_cases=6, regression_target=reg
     )
+    X_train = np.squeeze(X_train)
+    X_test = np.squeeze(X_test)
     # Compute the pairwise distance matrix for working with sklearn knn with
     # precomputed distances.
-    X_train_precomp_distance = dist["pairwise_distance"](X_train[indices])
-    X_test_precomp_distance = dist["pairwise_distance"](
-        X_test[indices], X_train[indices]
-    )
+    X_train_precomp_distance = dist["pairwise_distance"](X_train)
+    X_test_precomp_distance = dist["pairwise_distance"](X_test, X_train)
 
     knn_aeon = knn_aeon_func(distance=dist["name"], n_neighbors=k)  # aeon
     knn_sk = knn_sk_func(metric=dist["distance"], n_neighbors=k)  # sklearn
     knn_sk_precomp = knn_sk_func(metric="precomputed", n_neighbors=k)  # sklearn pre
 
-    knn_aeon.fit(X_train[indices], y_train[indices])
-    knn_sk.fit(X_train[indices], y_train[indices])
-    knn_sk_precomp.fit(X_train_precomp_distance, y_train[indices])
+    knn_aeon.fit(X_train, y_train)
+    knn_sk.fit(X_train, y_train)
+    knn_sk_precomp.fit(X_train_precomp_distance, y_train)
 
     if problem_type == "cls":
-        knn_aeon_output = knn_aeon.predict_proba(X_test[indices])
-        knn_sk_output = knn_sk.predict_proba(X_test[indices])
+        knn_aeon_output = knn_aeon.predict_proba(X_test)
+        knn_sk_output = knn_sk.predict_proba(X_test)
         knn_sk_precomp_output = knn_sk_precomp.predict_proba(X_test_precomp_distance)
     elif problem_type == "reg":
-        knn_aeon_output = knn_aeon.predict(X_test[indices])
-        knn_sk_output = knn_sk.predict(X_test[indices])
+        knn_aeon_output = knn_aeon.predict(X_test)
+        knn_sk_output = knn_sk.predict(X_test)
         knn_sk_precomp_output = knn_sk_precomp.predict(X_test_precomp_distance)
 
     assert_allclose(knn_aeon_output, knn_sk_output)
@@ -128,13 +127,11 @@ def test_univariate(dist, k, task):
     [
         [
             "cls",
-            load_basic_motions,
             KNeighborsClassifier,
             KNeighborsTimeSeriesClassifier,
         ],
         [
             "reg",
-            load_cardano_sentiment,
             KNeighborsRegressor,
             KNeighborsTimeSeriesRegressor,
         ],
@@ -150,34 +147,36 @@ def test_multivariate(dist, k, task):
     # Test multivariate dataset in two ways: A) concatenating channels to be compatible
     # with sklearn, and B) precomputing distances.
 
-    problem_type, problem_loader, knn_sk_func, knn_aeon_func = task
-
-    # Load the basic motions dataset as a 3D numpy array
-    X_train, y_train = problem_loader(split="train", return_type="numpy3D")
-    X_test, y_test = problem_loader(split="test", return_type="numpy3D")
+    problem_type, knn_sk_func, knn_aeon_func = task
+    if problem_type == "cls":
+        reg = False
+    else:
+        reg = True
+    X_train, y_train = make_example_3d_numpy(
+        random_state=0, n_cases=6, regression_target=reg
+    )
+    X_test, y_test = make_example_3d_numpy(
+        random_state=2, n_cases=6, regression_target=reg
+    )
 
     # Transform to 2D format concatenating channels.
     X_train_concat = X_train.reshape(X_train.shape[0], -1)
     X_test_concat = X_test.reshape(X_test.shape[0], -1)
 
-    indices = np.random.RandomState(0).choice(
-        min(len(y_test), len(y_train)), 6, replace=False
-    )
-
     # A) Test multivariate with 2D format (concatenates channels to be compatible with
     # sklearn)
     knn_aeon = knn_aeon_func(distance=dist["name"], n_neighbors=k)  # aeon concat
     knn_sk = knn_sk_func(metric=dist["distance"], n_neighbors=k)  # sklearn concat
 
-    knn_aeon.fit(X_train_concat[indices], y_train[indices])
-    knn_sk.fit(X_train_concat[indices], y_train[indices])
+    knn_aeon.fit(X_train_concat, y_train)
+    knn_sk.fit(X_train_concat, y_train)
 
     if problem_type == "cls":
-        knn_aeon_output = knn_aeon.predict_proba(X_test_concat[indices])
-        knn_sk_output = knn_sk.predict_proba(X_test_concat[indices])
+        knn_aeon_output = knn_aeon.predict_proba(X_test_concat)
+        knn_sk_output = knn_sk.predict_proba(X_test_concat)
     elif problem_type == "reg":
-        knn_aeon_output = knn_aeon.predict(X_test_concat[indices])
-        knn_sk_output = knn_sk.predict(X_test_concat[indices])
+        knn_aeon_output = knn_aeon.predict(X_test_concat)
+        knn_sk_output = knn_sk.predict(X_test_concat)
 
     assert_allclose(knn_aeon_output, knn_sk_output)
 
@@ -186,22 +185,20 @@ def test_multivariate(dist, k, task):
     # distances)
 
     # Compute the pairwise distance matrix
-    X_train_precomp_distance = dist["pairwise_distance"](X_train[indices])
-    X_test_precomp_distance = dist["pairwise_distance"](
-        X_test[indices], X_train[indices]
-    )
+    X_train_precomp_distance = dist["pairwise_distance"](X_train)
+    X_test_precomp_distance = dist["pairwise_distance"](X_test, X_train)
 
     knn_aeon_3D = knn_aeon_func(distance=dist["name"], n_neighbors=k)  # aeon
     knn_sk_precomp = knn_sk_func(metric="precomputed", n_neighbors=k)  # sklearn precomp
 
-    knn_aeon_3D.fit(X_train[indices], y_train[indices])
-    knn_sk_precomp.fit(X_train_precomp_distance, y_train[indices])
+    knn_aeon_3D.fit(X_train, y_train)
+    knn_sk_precomp.fit(X_train_precomp_distance, y_train)
 
     if problem_type == "cls":
-        knn_aeon_3D_output = knn_aeon_3D.predict_proba(X_test[indices])
+        knn_aeon_3D_output = knn_aeon_3D.predict_proba(X_test)
         knn_sk_precomp_output = knn_sk_precomp.predict_proba(X_test_precomp_distance)
     elif problem_type == "reg":
-        knn_aeon_3D_output = knn_aeon_3D.predict(X_test[indices])
+        knn_aeon_3D_output = knn_aeon_3D.predict(X_test)
         knn_sk_precomp_output = knn_sk_precomp.predict(X_test_precomp_distance)
 
     assert_allclose(knn_aeon_3D_output, knn_sk_precomp_output)
diff --git a/aeon/regression/distance_based/tests/test_time_series_neighbors.py b/aeon/regression/distance_based/tests/test_time_series_neighbors.py
index cb814755..01c3326a 100644
--- a/aeon/regression/distance_based/tests/test_time_series_neighbors.py
+++ b/aeon/regression/distance_based/tests/test_time_series_neighbors.py
@@ -4,40 +4,40 @@ import pytest
 from numpy.testing import assert_almost_equal
 from sklearn.metrics import mean_squared_error
 
-from aeon.datasets import load_covid_3month
 from aeon.distances import get_distance_function
 from aeon.regression.distance_based import KNeighborsTimeSeriesRegressor
+from aeon.testing.data_generation import make_example_3d_numpy
 
 distance_functions = ["euclidean", "dtw", "wdtw", "msm", "erp", "adtw", "twe"]
 
 # expected mse on test set using default parameters.
 expected_mse = {
-    "euclidean": 0.002815386587822589,
-    "dtw": 0.002921957478363366,
-    "wdtw": 0.0025029139202436303,
-    "msm": 0.002427566155284863,
-    "erp": 0.002247674986547397,
-    "adtw": 0.00265555172857104,
-    "twe": 0.0028423024613138774,
+    "euclidean": 0.5958635513183005,
+    "dtw": 0.13862493928236033,
+    "wdtw": 0.13862493928236033,
+    "msm": 0.10700935790251886,
+    "erp": 0.2707789569252858,
+    "adtw": 0.1125922971718583,
+    "twe": 0.1668928688769102,
 }
 
 # expected mse on test set using window params.
 expected_mse_window = {
-    "dtw": 0.0027199984296669712,
-    "wdtw": 0.0026043512829531305,
-    "msm": 0.002413148537646331,
-    "erp": 0.0021331320891357546,
-    "adtw": 0.0027602314681382163,
-    "twe": 0.0030244991099088346,
+    "dtw": 0.19829606291787538,
+    "wdtw": 0.19829606291787538,
+    "msm": 0.10700935790251886,
+    "erp": 0.24372655531245097,
+    "adtw": 0.12166501682071837,
+    "twe": 0.15932454084282624,
 }
 
+X_train, y_train = make_example_3d_numpy(regression_target=True, random_state=0)
+X_test, y_test = make_example_3d_numpy(regression_target=True, random_state=2)
+
 
 @pytest.mark.parametrize("distance_key", distance_functions)
 def test_knn_neighbors(distance_key):
-    """Tests kneighbors method."""
-    X_train, y_train = load_covid_3month(split="train")
-    X_test, y_test = load_covid_3month(split="test")
-
+    """Tests kneighbours method."""
     model = KNeighborsTimeSeriesRegressor(
         n_neighbors=1, weights="distance", distance=distance_key
     )
@@ -53,8 +53,6 @@ def test_knn_bounding_matrix(distance_key):
     """Test knn with custom bounding parameters, and using callables."""
     if distance_key == "euclidean" or distance_key == "squared":
         return
-    X_train, y_train = load_covid_3month(split="train")
-    X_test, y_test = load_covid_3month(split="test")
     distance_callable = get_distance_function(distance_key)
 
     knn = KNeighborsTimeSeriesRegressor(
@@ -65,9 +63,3 @@ def test_knn_bounding_matrix(distance_key):
 
     mse = mean_squared_error(y_test, y_pred)
     assert_almost_equal(mse, expected_mse_window[distance_key])
-
-
-if __name__ == "__main__":
-    for distance_key in distance_functions:
-        test_knn_neighbors(distance_key)
-        test_knn_bounding_matrix(distance_key)
diff --git a/aeon/transformations/series/tests/test_siv.py b/aeon/transformations/series/tests/test_siv.py
new file mode 100644
index 00000000..c8042e9c
--- /dev/null
+++ b/aeon/transformations/series/tests/test_siv.py
@@ -0,0 +1,48 @@
+"""Tests for SIV transformation."""
+
+__maintainer__ = []
+
+import numpy as np
+import pytest
+
+
+@pytest.mark.parametrize(
+    "window_length", [1, 2, 3, 5, 7, 10, 11, [2, 3], [3, 5], [3, 5, 7], [3, 5, 7, 11]]
+)
+def test_siv(window_length):
+    """Test the functionality of SIV transformation."""
+    n_samples = 100
+    t = np.linspace(0, 10, n_samples)
+    x1 = (
+        0.5 * np.sin(2 * np.pi * 1 * t)
+        + 0.2 * np.sin(2 * np.pi * 5 * t)
+        + 0.1 * np.sin(2 * np.pi * 10 * t)
+    )
+    x2 = (
+        0.4 * np.sin(2 * np.pi * 1.5 * t)
+        + 0.3 * np.sin(2 * np.pi * 4 * t)
+        + 0.1 * np.sin(2 * np.pi * 8 * t)
+    )
+    x12 = np.array([x1, x2])
+    x12r = x12 + np.random.random((2, n_samples)) * 0.25
+
+    from aeon.transformations.series._siv import SIVSeriesTransformer
+
+    siv = SIVSeriesTransformer(window_length=window_length)
+    x_1 = siv.fit_transform(x1)
+    x_2 = siv.fit_transform(x2)
+    x_12 = siv.fit_transform(x12)
+    x_12_r = siv.fit_transform(x12r)
+
+    """
+    # Visualize smoothing
+    import matplotlib.pyplot as plt
+    plt.plot(x12r[0])
+    plt.plot(x_12_r[0])
+    plt.savefig(fname=f'SIV_{window_length}.png')
+    plt.clf()
+    """
+
+    np.testing.assert_almost_equal(x_1[0], x_12[0], decimal=4)
+    np.testing.assert_almost_equal(x_2[0], x_12[1], decimal=4)
+    assert x_12.shape == x_12_r.shape