Repository: aeon-toolkit/aeon
Pull request #494 (2023-06-15 13:49:27)

[DOC] InceptionTime docstrings
changes series_length -> n_timepoints and tests if Ali is notified. 


Patch:
diff --git a/aeon/classification/deep_learning/inception_time.py b/aeon/classification/deep_learning/inception_time.py
index 3a874e70..49610f26 100644
--- a/aeon/classification/deep_learning/inception_time.py
+++ b/aeon/classification/deep_learning/inception_time.py
@@ -22,7 +22,7 @@ _check_dl_dependencies(severity="warning")
 class InceptionTimeClassifier(BaseClassifier):
     """InceptionTime ensemble classifier.
 
-    Ensemble of IndividualInceptionTimeClassifiers, as described in [1].
+    Ensemble of IndividualInceptionTimeClassifier objects, as described in [1].
 
     Parameters
     ----------
@@ -223,12 +223,12 @@ class InceptionTimeClassifier(BaseClassifier):
         super(InceptionTimeClassifier, self).__init__()
 
     def _fit(self, X, y):
-        """Fit each of the Individual Inception models.
+        """Fit the ensemble of IndividualInceptionClassifier models.
 
         Arguments:
         ----------
 
-        X : np.ndarray of shape = (n_instances (n), n_channels (c), series_length (m))
+        X : np.ndarray of shape = (n_instances (n), n_channels (c), n_timepoints (m))
             The training input samples.
         y : np.ndarray of shape n
             The training data class labels.
@@ -282,7 +282,7 @@ class InceptionTimeClassifier(BaseClassifier):
         Arguments:
         ---------
 
-        X : np.ndarray of shape = (n_instances (n), n_channels (c), series_length (m))
+        X : np.ndarray of shape = (n_instances (n), n_channels (c), n_timepoints (m))
             The testing input samples.
 
         Returns
@@ -304,7 +304,7 @@ class InceptionTimeClassifier(BaseClassifier):
         Arguments:
         ---------
 
-        X : np.ndarray of shape = (n_instances (n), n_channels (c), series_length (m))
+        X : np.ndarray of shape = (n_instances (n), n_channels (c), n_timepoints (m))
             The testing input samples.
 
         Returns
@@ -598,7 +598,7 @@ class IndividualInceptionClassifier(BaseDeepClassifier):
 
         Parameters
         ----------
-        X : array-like of shape = (n_instances, n_channels, series_length)
+        X : array-like of shape = (n_instances, n_channels, n_timepoints)
             The training input samples. If a 2D array-like is passed,
             n_channels is assumed to be 1.
         y : array-like, shape = (n_instances)
diff --git a/aeon/transformations/collection/dictionary_based/_paa.py b/aeon/transformations/collection/dictionary_based/_paa.py
index 13f2b430..eab07cee 100644
--- a/aeon/transformations/collection/dictionary_based/_paa.py
+++ b/aeon/transformations/collection/dictionary_based/_paa.py
@@ -4,19 +4,20 @@ import numpy as np
 
 from aeon.transformations.base import BaseTransformer
 
-__author__ = "MatthewMiddlehurst"
+__author__ = ["MatthewMiddlehurst", "hadifawaz1999"]
 
 
 class PAA(BaseTransformer):
     """Piecewise Aggregate Approximation Transformer (PAA).
 
     (PAA) Piecewise Aggregate Approximation Transformer, as described in [1]. For
-    each series reduce the dimensionality to num_intervals, where each value is the
+    each series reduce the dimensionality to n_segments, where each value is the
     mean of values in the interval.
 
     Parameters
     ----------
-    n_intervals   : int, dimension of the transformed data (default 8)
+    n_segments   : int, default = 8
+        Dimension of the transformed data
 
     Notes
     -----
@@ -27,10 +28,12 @@ class PAA(BaseTransformer):
     Examples
     --------
     >>> from aeon.transformations.collection.dictionary_based import PAA
-    >>> import numpy as np
-    >>> data = np.array([[[1,2,3,4,5,6,7,8,9,10]],[[5,5,5,5,5,5,5,5,5,5]]])
-    >>> paa = PAA(n_intervals=2)
-    >>> data2 = paa.fit_transform(data)
+    >>> from aeon.datasets import load_unit_test
+    >>> X_train, y_train = load_unit_test(split="train")
+    >>> X_test, y_test = load_unit_test(split="test")
+    >>> paa = PAA(n_segments=10)
+    >>> X_train_paa = paa.fit_transform(X_train)
+    >>> X_test_paa = paa.transform(X_test)
     """
 
     _tags = {
@@ -38,105 +41,103 @@ class PAA(BaseTransformer):
         "scitype:instancewise": True,
         "X_inner_mtype": "numpy3D",
         "y_inner_mtype": "None",
+        "capability:multivariate": True,
     }
 
-    def __init__(self, n_intervals=8):
-        self.n_intervals = n_intervals
+    def __init__(self, n_segments=8):
+        self.n_segments = n_segments
         super(PAA, self).__init__()
 
-    def set_num_intervals(self, n):
-        """Set self.num_intervals to n."""
-        self.n_intervals = n
-
     def _transform(self, X, y=None):
-        """Transform data.
+        """Transform the input time series to PAA segments.
 
         Parameters
         ----------
-        X : 3D np.ndarray of shape = [n_instances, n_channels, series_length]
-            collection of time series to transform
-        y : ignored argument for interface compatibility
+        X : np.ndarray of shape = (n_instances, n_channels, series_length)
+            The input time series
+        y : np.ndarray of shape = (n_instances,), default = None
+            The labels are not used
 
         Returns
         -------
-        X : 3D np.ndarray of shape = [n_instances, n_channels, series_length]
-            collection of transformed time series
+        X_paa : np.ndarray of shape = (n_instances, n_channels, n_segments)
+            The output of the PAA transformation
         """
-        # Get information about the dataframe
-        # PAA acts on each dimension, so its best to reshape prior to transform
-        n_cases, n_channels, series_length = X.shape
-        _X = np.swapaxes(X, 0, 1)
+        length_TS = int(X.shape[-1])
+        all_indices = np.arange(length_TS)
+
+        # The following will include the left out indices
+        # For instance if the length of the TS is 10 and the number
+        # of segments is 3, the indices will be [0:3], [3:6] and [6:10]
+        # so 3 segments, two of length 3 and one of length 4
+        split_segments = np.array_split(all_indices, self.n_segments)
 
-        # Check the parameters are appropriate
-        self._check_parameters(series_length)
+        # If the series length is divisible by the number of segments
+        # then the transformation can be done in one line
+        # If not, a for loop is needed only on the segments while
+        # parallelizing the transformation
 
-        # On each dimension, perform PAA
-        channels = []
-        for i in range(n_channels):
-            channels.append(self._perform_paa_along_dim(_X[i]))
-        result = np.array(channels)
-        result = np.swapaxes(result, 0, 1)
+        if length_TS % self.n_segments == 0:
+            X_paa = X[:, :, split_segments].mean(axis=-1)
+            return X_paa
 
-        return result
+        else:
+            n_samples, n_channels, _ = X.shape
+            X_paa = np.zeros(shape=(n_samples, n_channels, self.n_segments))
 
-    def _perform_paa_along_dim(self, X):
-        n_cases, series_length = X.shape
-        data = []
+            for _s, segment in enumerate(split_segments):
+                X_paa[:, :, _s] = X[:, :, segment].mean(axis=-1)
 
-        for i in range(n_cases):
-            series = X[i, :]
+            return X_paa
 
-            frames = []
-            current_frame = 0
-            current_frame_size = 0
-            frame_length = series_length / self.n_intervals
-            frame_sum = 0
+    def inverse_paa(self, X, original_length):
+        """Produce the inverse PAA transformation.
 
-            for n in range(series_length):
-                remaining = frame_length - current_frame_size
+        Parameters
+        ----------
+        X : np.ndarray of shape = (n_instances, n_channels, n_segments)
+            The output of the PAA transformation
 
-                if remaining > 1:
-                    frame_sum += series[n]
-                    current_frame_size += 1
-                else:
-                    frame_sum += remaining * series[n]
-                    current_frame_size += remaining
+        Returns
+        -------
+        paa_inverse : np.ndarray(n_instances, n_channels, series_length)
+            The inverse of paa transform
+        """
+        if original_length % self.n_segments == 0:
+            return np.repeat(X, repeats=int(original_length / self.n_segments), axis=-1)
 
-                if current_frame_size == frame_length:
-                    frames.append(frame_sum / frame_length)
-                    current_frame += 1
+        else:
+            n_samples, n_channels, _ = X.shape
+            X_inverse_paa = np.zeros(shape=(n_samples, n_channels, original_length))
 
-                    frame_sum = (1 - remaining) * series[n]
-                    current_frame_size = 1 - remaining
+            all_indices = np.arange(original_length)
+            split_segments = np.array_split(all_indices, self.n_segments)
 
-            # if the last frame was lost due to double imprecision
-            if current_frame == self.n_intervals - 1:
-                frames.append(frame_sum / frame_length)
+            for _s, segment in enumerate(split_segments):
+                X_inverse_paa[:, :, segment] = np.repeat(
+                    X[:, :, [_s]], repeats=len(segment), axis=-1
+                )
 
-            data.append(frames)
+            return X_inverse_paa
 
-        return data
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
 
-    def _check_parameters(self, series_length):
-        """Check parameters of PAA.
+        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.
 
-        Function for checking the values of parameters inserted into PAA.
-        For example, the number of subsequences cannot be larger than the
-        time series length.
 
-        Throws
-        ------
-        ValueError or TypeError if a parameters input is invalid.
+        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`
         """
-        if isinstance(self.n_intervals, int):
-            if self.n_intervals <= 0:
-                raise ValueError("num_intervals must have the value of at least 1")
-            if self.n_intervals > series_length:
-                raise ValueError(
-                    "num_intervals cannot be higher than the time series length."
-                )
-        else:
-            raise TypeError(
-                f"num_intervals must be an 'int'. Found"
-                f" {type(self.n_intervals).__name__} instead."
-            )
+        params = {"n_segments": 10}
+        return params
diff --git a/aeon/transformations/collection/dictionary_based/_sax.py b/aeon/transformations/collection/dictionary_based/_sax.py
index aa234004..86c73556 100644
--- a/aeon/transformations/collection/dictionary_based/_sax.py
+++ b/aeon/transformations/collection/dictionary_based/_sax.py
@@ -119,7 +119,7 @@ class SAX(BaseTransformer):
         if not self.znormalized:
             X = scipy.stats.zscore(X, axis=-1)
 
-        paa = PAA(n_intervals=self.n_segments)
+        paa = PAA(n_segments=self.n_segments)
         X_paa = paa.fit_transform(X=X)
 
         return X_paa


Test patch:
diff --git a/aeon/transformations/collection/dictionary_based/tests/test_paa.py b/aeon/transformations/collection/dictionary_based/tests/test_paa.py
index 0572e8c6..0a3bd753 100644
--- a/aeon/transformations/collection/dictionary_based/tests/test_paa.py
+++ b/aeon/transformations/collection/dictionary_based/tests/test_paa.py
@@ -2,45 +2,40 @@
 import numpy as np
 import pytest
 
-from aeon.transformations.collection.dictionary_based._paa import PAA
-
-
-# Check that exception is raised for bad num intervals.
-# input types - string, float, negative int, negative float, empty dict
-# and an int that is larger than the time series length.
-# correct input is meant to be a positive integer of 1 or more.
-@pytest.mark.parametrize("bad_num_intervals", ["str", 1.2, -1.2, -1, {}, 11, 0])
-def test_bad_input_args(bad_num_intervals):
-    X = np.array([[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]])
-    if not isinstance(bad_num_intervals, int):
-        with pytest.raises(TypeError):
-            PAA(n_intervals=bad_num_intervals).fit(X).transform(X)
-    else:
-        with pytest.raises(ValueError):
-            PAA(n_intervals=bad_num_intervals).fit_transform(X)
-
-
-# Check the transformer has changed the data correctly.
-def test_output_of_transformer():
-    X = np.array([[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]])
-    p = PAA(n_intervals=3).fit(X)
-    res = p.transform(X)
-    orig = np.array([[[2.2, 5.5, 8.8]]])
-    np.testing.assert_array_almost_equal(res, orig)
-
-
-def test_output_dimensions():
-    # test with univariate
-    X = np.random.rand(10, 1, 12)
-    p = PAA(n_intervals=5).fit(X)
-    res = p.transform(X)
-    assert res.shape[0] == 10 and res.shape[1] == 1 and res.shape[2] == 5
-
-
-# This is to check that PAA produces the same result along each dimension
-def test_paa_performs_correcly_along_each_dim():
-    X = np.array([[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]])
-    p = PAA(n_intervals=3).fit(X)
-    res = p.transform(X)
-    orig = [[[2.2, 5.5, 8.8], [2.2, 5.5, 8.8]]]
-    np.testing.assert_array_almost_equal(res, orig)
+from aeon.transformations.collection.dictionary_based import PAA
+
+
+@pytest.mark.parametrize("n_segments", [8])
+def test_equal_length_univariate_paa(n_segments):
+    X = np.random.normal(size=(10, 1, 100))
+
+    # normalize input
+    stds = np.std(X, axis=2, keepdims=True)
+    stds[stds == 0] = 1.0
+    X = (X - np.mean(X, axis=2, keepdims=True)) / stds
+
+    paa = PAA(n_segments=n_segments)
+
+    X_paa = paa.fit_transform(X=X)
+    X_paa_inv = paa.inverse_paa(X=X_paa, original_length=100)
+
+    assert X_paa.shape[-1] == n_segments
+    assert X_paa_inv.shape[-1] == X.shape[-1]
+
+
+@pytest.mark.parametrize("n_segments", [8])
+def test_equal_length_multivariate_paa(n_segments):
+    X = np.random.normal(size=(10, 3, 100))
+
+    # normalize input
+    stds = np.std(X, axis=2, keepdims=True)
+    stds[stds == 0] = 1.0
+    X = (X - np.mean(X, axis=2, keepdims=True)) / stds
+
+    paa = PAA(n_segments=n_segments)
+
+    X_paa = paa.fit_transform(X=X)
+    X_paa_inv = paa.inverse_paa(X=X_paa, original_length=100)
+
+    assert X_paa.shape[-1] == n_segments
+    assert X_paa_inv.shape[-1] == X.shape[-1]