diff --git a/aeon/transformations/collection/_dwt.py b/aeon/transformations/collection/_dwt.py index d211276dc8..d54bfd0727 100644 --- a/aeon/transformations/collection/_dwt.py +++ b/aeon/transformations/collection/_dwt.py @@ -51,7 +51,8 @@ def _transform(self, X, y=None): Xt : 3D np.ndarray of shape = [n_cases, n_channels, n_timepoints] collection of transformed time series """ - _X = np.array(X, dtype=np.float64) + out_dtype = np.float32 if X.dtype == np.float32 else np.float64 + _X = np.array(X, dtype=out_dtype) n_cases, n_channels, n_timepoints = _X.shape _X = np.swapaxes(_X, 0, 1) self._check_parameters() diff --git a/aeon/transformations/collection/_slope.py b/aeon/transformations/collection/_slope.py index feb5ebdb68..066bea8365 100644 --- a/aeon/transformations/collection/_slope.py +++ b/aeon/transformations/collection/_slope.py @@ -3,8 +3,6 @@ __all__ = ["SlopeTransformer"] __maintainer__ = [] -import math - import numpy as np from aeon.transformations.collection.base import BaseCollectionTransformer @@ -62,17 +60,20 @@ def _transform(self, X, y=None): # Get information about the dataframe n_cases, n_channels, n_timepoints = X.shape self._check_parameters(n_timepoints) - full_data = [] + + X = X / 1 + + Xt = np.empty( + (n_cases, n_channels, self.n_intervals), + dtype=X.dtype, + ) + for i in range(n_cases): - case_data = [] for j in range(n_channels): splits = split_series(X[i][j], self.n_intervals) - # Calculate gradients - res = [self._get_gradient(x) for x in splits] - case_data.append(res) - full_data.append(np.asarray(case_data)) + Xt[i, j] = [self._get_gradient(split) for split in splits] - return np.array(full_data) + return Xt def _get_gradient(self, Y): """Get gradient of lines. @@ -92,7 +93,11 @@ def _get_gradient(self, Y): m : an int corresponding to the gradient of the best fit line. """ # Create an array that contains 1,2,3,...,len(Y) for the x coordinates. - X = np.arange(1, len(Y) + 1) + X = np.arange( + 1, + len(Y) + 1, + dtype=Y.dtype, + ) # Calculate the mean of both arrays meanX = np.mean(X) @@ -115,7 +120,7 @@ def _get_gradient(self, Y): m = 0 else: # Gradient is defined as (w+sqrt(w^2+r^2))/r - m = (w + math.sqrt(w**2 + r**2)) / r + m = (w + np.sqrt(w**2 + r**2)) / r return m diff --git a/aeon/transformations/collection/interval_based/_supervised_intervals.py b/aeon/transformations/collection/interval_based/_supervised_intervals.py index f4ba2c4c40..62e4dbf969 100644 --- a/aeon/transformations/collection/interval_based/_supervised_intervals.py +++ b/aeon/transformations/collection/interval_based/_supervised_intervals.py @@ -241,6 +241,8 @@ def _fit(self, X, y=None): return self def _transform(self, X, y=None): + X = X / 1 + transform = Parallel( n_jobs=self._n_jobs, backend=self.parallel_backend, prefer="threads" )( @@ -251,13 +253,18 @@ def _transform(self, X, y=None): for i in range(len(self.intervals_)) ) - Xt = np.zeros((X.shape[0], len(transform))) + Xt = np.zeros( + (X.shape[0], len(transform)), + dtype=X.dtype, + ) for i, t in enumerate(transform): Xt[:, i] = t return Xt def _fit_setup(self, X, y): + X = X / 1 + self.intervals_ = [] self.n_cases_, self.n_channels_, self.n_timepoints_ = X.shape @@ -356,7 +363,7 @@ def _fit_setup(self, X, y): def _generate_intervals(self, X, X_norm, y, seed, keep_transform): rng = check_random_state(seed) - Xt = np.empty((self.n_cases_, 0)) if keep_transform else None + Xt = np.empty((self.n_cases_, 0), dtype=X.dtype) if keep_transform else None intervals = [] for i in range(self.n_channels_): @@ -406,14 +413,16 @@ def _generate_intervals(self, X, X_norm, y, seed, keep_transform): def _transform_intervals(self, X, idx): if not self._transform_features[idx]: - return np.zeros(X.shape[0]) + return np.zeros(X.shape[0], dtype=X.dtype) start, end, dim, feature = self.intervals_[idx] if isinstance(feature, BaseTransformer): - return feature.transform(X[:, dim, start:end]).flatten() + Xt = feature.transform(X[:, dim, start:end]).flatten() else: - return feature(X[:, dim, start:end]) + Xt = feature(X[:, dim, start:end]) + + return np.asarray(Xt, dtype=X.dtype) def _supervised_search( self, @@ -428,7 +437,7 @@ def _supervised_search( feature_is_transformer, ): intervals = [] - Xt = np.empty((X.shape[0], 0)) if keep_transform else None + Xt = np.empty((X.shape[0], 0), dtype=X.dtype) if keep_transform else None while X.shape[1] >= self._min_interval_length * 2: if ( @@ -451,6 +460,9 @@ def _supervised_search( interval_feature_0 = feature(sub_interval_0) interval_feature_1 = feature(sub_interval_1) + interval_feature_0 = np.asarray(interval_feature_0, dtype=X.dtype) + interval_feature_1 = np.asarray(interval_feature_1, dtype=X.dtype) + score_0 = self._metric(interval_feature_0, y) score_1 = self._metric(interval_feature_1, y) @@ -471,6 +483,11 @@ def _supervised_search( else: interval_feature_to_use = interval_feature_0 + interval_feature_to_use = np.asarray( + interval_feature_to_use, + dtype=X.dtype, + ) + Xt = np.hstack( ( Xt, @@ -498,6 +515,11 @@ def _supervised_search( else: interval_feature_to_use = interval_feature_1 + interval_feature_to_use = np.asarray( + interval_feature_to_use, + dtype=X.dtype, + ) + Xt = np.hstack( ( Xt, diff --git a/aeon/transformations/collection/interval_based/tests/test_intervals.py b/aeon/transformations/collection/interval_based/tests/test_intervals.py index f63edac3a7..d9399a92e6 100644 --- a/aeon/transformations/collection/interval_based/tests/test_intervals.py +++ b/aeon/transformations/collection/interval_based/tests/test_intervals.py @@ -1,5 +1,8 @@ """Interval extraction test code.""" +import numpy as np +import pytest + from aeon.testing.data_generation import make_example_3d_numpy from aeon.transformations.collection.feature_based import Catch22, SevenNumberSummary from aeon.transformations.collection.interval_based import ( @@ -56,3 +59,37 @@ def test_supervised_transformers(): X_t = sit.fit_transform(X, y) assert X_t.shape == (X.shape[0], 8) + + +@pytest.mark.parametrize( + "dtype", + ["int32", "int64", "float32", "float64"], +) +def test_supervised_intervals_preserves_float_precision(dtype): + """Test SupervisedIntervals preserves float32 and promotes integer input.""" + X, y = make_example_3d_numpy( + random_state=0, + n_channels=1, + n_timepoints=20, + ) + X = X.astype(dtype) + + expected_dtype = np.float32 if dtype == "float32" else np.float64 + + sit = SupervisedIntervals( + features=[row_mean], + n_intervals=2, + random_state=0, + ) + sit.fit(X, y) + Xt = sit.transform(X) + + sit = SupervisedIntervals( + features=[row_mean], + n_intervals=2, + random_state=0, + ) + Xt_fit_transform = sit.fit_transform(X, y) + + assert Xt.dtype == expected_dtype + assert Xt_fit_transform.dtype == expected_dtype diff --git a/aeon/transformations/collection/tests/test_dwt.py b/aeon/transformations/collection/tests/test_dwt.py index 1c83e9ce8a..02db8298c6 100644 --- a/aeon/transformations/collection/tests/test_dwt.py +++ b/aeon/transformations/collection/tests/test_dwt.py @@ -5,6 +5,7 @@ import numpy as np import pytest +from aeon.testing.data_generation import make_example_3d_numpy from aeon.transformations.collection import DWTTransformer @@ -111,3 +112,23 @@ def test_dwt_performs_correcly_along_each_dim(): ] ) np.testing.assert_array_almost_equal(res, orig) + + +@pytest.mark.parametrize( + "dtype", + ["int32", "int64", "float32", "float64"], +) +def test_dwt_preserves_float_precision(dtype): + """Check dwt preserves float32 and promotes proper dtype output.""" + X = make_example_3d_numpy( + n_cases=2, + n_channels=1, + n_timepoints=8, + return_y=False, + random_state=0, + ).astype(dtype) + + Xt = DWTTransformer(n_levels=2).fit_transform(X) + + expected_dtype = np.float32 if dtype == "float32" else np.float64 + assert Xt.dtype == expected_dtype diff --git a/aeon/transformations/collection/tests/test_slope_transformer.py b/aeon/transformations/collection/tests/test_slope_transformer.py index 399a7a79f5..5c65d1485b 100644 --- a/aeon/transformations/collection/tests/test_slope_transformer.py +++ b/aeon/transformations/collection/tests/test_slope_transformer.py @@ -5,6 +5,7 @@ import numpy as np import pytest +from aeon.testing.data_generation import make_example_3d_numpy from aeon.transformations.collection import SlopeTransformer @@ -75,3 +76,23 @@ def test_slope_performs_correcly_along_each_dim(): ] ) np.testing.assert_array_almost_equal(res, orig, decimal=5) + + +@pytest.mark.parametrize( + "dtype", + ["int32", "int64", "float32", "float64"], +) +def test_slope_preserves_float_precision(dtype): + """Check that Slope preserved float32 and promotes proper dtype output.""" + X = make_example_3d_numpy( + n_cases=2, + n_channels=1, + n_timepoints=8, + return_y=False, + random_state=0, + ).astype(dtype) + + Xt = SlopeTransformer(n_intervals=2).fit_transform(X) + + expected_dtype = np.float32 if dtype == "float32" else np.float64 + assert Xt.dtype == expected_dtype diff --git a/aeon/transformations/collection/unequal_length/_resize.py b/aeon/transformations/collection/unequal_length/_resize.py index 371de9da32..9c52ebeb0d 100644 --- a/aeon/transformations/collection/unequal_length/_resize.py +++ b/aeon/transformations/collection/unequal_length/_resize.py @@ -104,7 +104,9 @@ def _transform(self, X, y=None): Xt = [] for x in X: - x_new = np.zeros((x.shape[0], length)) + out_dtype = np.float32 if x.dtype == np.float32 else np.float64 + x_new = np.zeros((x.shape[0], length), dtype=out_dtype) + x2 = np.linspace(0, 1, x.shape[1]) x3 = np.linspace(0, 1, length) for i, row in enumerate(x): diff --git a/aeon/transformations/collection/unequal_length/tests/test_resize.py b/aeon/transformations/collection/unequal_length/tests/test_resize.py index dc92adefe9..47a1a8f735 100644 --- a/aeon/transformations/collection/unequal_length/tests/test_resize.py +++ b/aeon/transformations/collection/unequal_length/tests/test_resize.py @@ -87,3 +87,20 @@ def test_incorrect_arguments(): resizer = Resizer(resized_length="invalid") with pytest.raises(ValueError, match="resized_length must be"): resizer.fit_transform(X) + + +@pytest.mark.parametrize( + "dtype", + ["int32", "int64", "float32", "float64"], +) +def test_resizer_preserves_float_precision(dtype): + """Test Resizer preserved float32 and promotes proper dtype output.""" + X = [ + np.array([[0, 1, 2, 3]], dtype=dtype), + np.array([[0, 2, 4, 6, 8]], dtype=dtype), + ] + + Xt = Resizer(resized_length=6).fit_transform(X) + + expected_dtype = np.float32 if dtype == "float32" else np.float64 + assert Xt.dtype == expected_dtype diff --git a/aeon/transformations/series/_pla.py b/aeon/transformations/series/_pla.py index 33c907e833..8540e03f3d 100644 --- a/aeon/transformations/series/_pla.py +++ b/aeon/transformations/series/_pla.py @@ -88,6 +88,7 @@ def _transform(self, X, y=None): if not (self.buffer_size is None or isinstance(self.buffer_size, (int, float))): raise ValueError("Invalid buffer_size: use a number only or keep empty.") results = None + X = X / 1 X = np.concatenate(X) if isinstance(self.transformer, (str)): if self.transformer.lower() == "sliding window": @@ -276,7 +277,7 @@ def _SWAB(self, X): current_data_point = current_data_point + len(seg) buffer = np.append(buffer, seg) else: - buffer = np.array([]) + buffer = np.empty(0, dtype=X.dtype) t = t[1:] for i in range(len(t)): seg_ts.append(t[i]) @@ -338,11 +339,14 @@ def _linear_regression(self, time_series): List of transformed segmented time series """ n = len(time_series) - Y = np.array(time_series) - X = np.arange(n).reshape(-1, 1) + Y = np.asarray(time_series) + X = np.arange( + n, + dtype=Y.dtype, + ).reshape(-1, 1) linearRegression = LinearRegression() linearRegression.fit(X, Y) - regression_line = np.array(linearRegression.predict(X)) + regression_line = linearRegression.predict(X) return regression_line def _calculate_error(self, X): diff --git a/aeon/transformations/series/tests/test_pla.py b/aeon/transformations/series/tests/test_pla.py index 1e9e5ba849..321dd7b15f 100644 --- a/aeon/transformations/series/tests/test_pla.py +++ b/aeon/transformations/series/tests/test_pla.py @@ -244,3 +244,32 @@ def test_piecewise_linear_approximation_one_segment(X): pla = PLASeriesTransformer(10, "bottom up") result = pla.fit_transform(X) np.testing.assert_array_almost_equal(X, result, decimal=1) + + +@pytest.mark.parametrize( + "transformer", ["sliding window", "top down", "bottom up", "swab"] +) +@pytest.mark.parametrize( + "dtype", + ["int32", "int64", "float32", "float64"], +) +def test_pla_preserves_float_precision(X, transformer, dtype): + """Test PLA float precision transformer.""" + Xt = PLASeriesTransformer( + max_error=100_000, + transformer=transformer, + ).fit_transform(X.astype(dtype)) + + expected_dtype = np.float32 if dtype == "float32" else np.float64 + assert Xt.dtype == expected_dtype + + +@pytest.mark.parametrize("dtype", ["float32", "float64"]) +def test_pla_linear_regression_preserves_float_precision(dtype): + """Test that PLA linear regression computes using the input precision.""" + X = np.arange(8, dtype=dtype) + + pla = PLASeriesTransformer() + Xt = pla._linear_regression(X) + + assert Xt.dtype == np.dtype(dtype)