Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion aeon/transformations/collection/_dwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
27 changes: 16 additions & 11 deletions aeon/transformations/collection/_slope.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
__all__ = ["SlopeTransformer"]
__maintainer__ = []

import math

import numpy as np

from aeon.transformations.collection.base import BaseCollectionTransformer
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)(
Expand All @@ -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
Expand Down Expand Up @@ -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_):
Expand Down Expand Up @@ -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,
Expand All @@ -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 (
Expand All @@ -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)

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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
21 changes: 21 additions & 0 deletions aeon/transformations/collection/tests/test_dwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
21 changes: 21 additions & 0 deletions aeon/transformations/collection/tests/test_slope_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion aeon/transformations/collection/unequal_length/_resize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment on lines +99 to +100

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the current testing util to generate variable length list allows dtype args, so this one is fine for now.

]

Xt = Resizer(resized_length=6).fit_transform(X)

expected_dtype = np.float32 if dtype == "float32" else np.float64
assert Xt.dtype == expected_dtype
12 changes: 8 additions & 4 deletions aeon/transformations/series/_pla.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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):
Expand Down
29 changes: 29 additions & 0 deletions aeon/transformations/series/tests/test_pla.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading