Repository: aeon-toolkit/aeon
Pull request #1939 (2024-08-09 17:59:10)

[DOC] Update type hints for similarity search
Issue: #1901 

Updated type hints for `base.py` and `query_search.py` in the `similarity search` module.

Updated documentation in many places to say `X : np.ndarray, 2D array of shape (x, y)` or `X : np.ndarray, 3D array of shape (x, y, z)` as suggested in Slack discussion.

aeon-actions-bot[bot]: 
## Thank you for contributing to `aeon`

I have added the following labels to this PR based on the title: **[ $\\color{#F3B9F8}{\\textsf{documentation}}$ ]**.
I have added the following labels to this PR based on the changes made: **[ $\\color{#006b75}{\\textsf{similarity search}}$ ]**. Feel free to change these if they do not properly represent the PR.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/1939/checks) tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.

If our `pre-commit` code quality check fails, any trivial fixes will automatically be pushed to your PR unless it is a draft.

Don't hesitate to ask questions on the `aeon` [Slack](https://join.slack.com/t/aeon-toolkit/shared_invite/zt-22vwvut29-HDpCu~7VBUozyfL_8j3dLA) channel if you have any.

### PR CI actions

These checkboxes will add labels to enable/disable CI functionality for this PR. This may not take effect immediately, and a new commit may be required to run the new configuration.

- [ ] Run `pre-commit` checks for all files
- [ ] Run all `pytest` tests and configurations
- [ ] Run all notebook example tests
- [ ] Run numba-disabled `codecov` tests
- [ ] Stop automatic `pre-commit` fixes (always disabled for drafts)
- [ ] Push an empty commit to re-run CI checks
    

Patch:
diff --git a/aeon/classification/shapelet_based/_sast.py b/aeon/classification/shapelet_based/_sast.py
index e12bf811..26b51446 100644
--- a/aeon/classification/shapelet_based/_sast.py
+++ b/aeon/classification/shapelet_based/_sast.py
@@ -61,7 +61,7 @@ class SASTClassifier(BaseClassifier):
     _tags = {
         "capability:multithreading": True,
         "capability:multivariate": False,
-        "algorithm_type": "subsequence",
+        "algorithm_type": "shapelet",
     }
 
     def __init__(
diff --git a/aeon/clustering/_clara.py b/aeon/clustering/_clara.py
index fda6644f..d6cdc895 100644
--- a/aeon/clustering/_clara.py
+++ b/aeon/clustering/_clara.py
@@ -10,6 +10,7 @@ from sklearn.utils import check_random_state
 
 from aeon.clustering._k_medoids import TimeSeriesKMedoids
 from aeon.clustering.base import BaseClusterer
+from aeon.distances import pairwise_distance
 
 
 class TimeSeriesCLARA(BaseClusterer):
@@ -158,8 +159,9 @@ class TimeSeriesCLARA(BaseClusterer):
         else:
             n_samples = self.n_samples
 
-        best_score = np.inf
+        best_inertia = np.inf
         best_pam = None
+        best_labels = None
         for _ in range(self.n_sampling_iters):
             sample_idxs = np.arange(n_samples)
             if n_samples < n_cases:
@@ -181,11 +183,24 @@ class TimeSeriesCLARA(BaseClusterer):
                 method="pam",
             )
             pam.fit(X[sample_idxs])
-            if pam.inertia_ < best_score:
+            curr_centers = pam.cluster_centers_
+            if isinstance(pam.distance, str):
+                pairwise_matrix = pairwise_distance(
+                    X, curr_centers, metric=self.distance, **pam._distance_params
+                )
+            else:
+                pairwise_matrix = pairwise_distance(
+                    X, curr_centers, pam._distance_callable, **pam._distance_params
+                )
+            curr_td = pairwise_matrix.min(axis=1).sum()
+
+            if curr_td < best_inertia:
                 best_pam = pam
+                best_inertia = curr_td
+                best_labels = pairwise_matrix.argmin(axis=1)
 
-        self.labels_ = best_pam.labels_
-        self.inertia_ = best_pam.inertia_
+        self.labels_ = best_labels
+        self.inertia_ = best_inertia
         self.cluster_centers_ = best_pam.cluster_centers_
         self.n_iter_ = best_pam.n_iter_
         self._kmedoids_instance = best_pam
diff --git a/aeon/similarity_search/base.py b/aeon/similarity_search/base.py
index ab4c2c2c..1673b012 100644
--- a/aeon/similarity_search/base.py
+++ b/aeon/similarity_search/base.py
@@ -3,8 +3,9 @@
 __maintainer__ = ["baraline"]
 
 from abc import ABC, abstractmethod
-from typing import Union, final
+from typing import Optional, final
 
+import numpy as np
 from numba import get_num_threads, set_num_threads
 from numba.typed import List
 
@@ -39,7 +40,7 @@ class BaseSimilaritySearch(BaseCollectionEstimator, ABC):
 
     Attributes
     ----------
-    X_ : array, shape (n_cases, n_channels, n_timepoints)
+    X_ : np.ndarray, 3D array of shape (n_cases, n_channels, n_timepoints)
         The input time series stored during the fit method.
 
     Notes
@@ -59,7 +60,7 @@ class BaseSimilaritySearch(BaseCollectionEstimator, ABC):
     def __init__(
         self,
         distance: str = "euclidean",
-        distance_args: Union[None, dict] = None,
+        distance_args: Optional[dict] = None,
         inverse_distance: bool = False,
         normalize: bool = False,
         speed_up: str = "fastest",
@@ -74,14 +75,14 @@ class BaseSimilaritySearch(BaseCollectionEstimator, ABC):
         super().__init__()
 
     @final
-    def fit(self, X, y=None):
+    def fit(self, X: np.ndarray, y=None):
         """
         Fit method: data preprocessing and storage.
 
         Parameters
         ----------
-        X : array, shape (n_cases, n_channels, n_timepoints)
-            Input array to used as database for the similarity search
+        X : np.ndarray, 3D array of shape (n_cases, n_channels, n_timepoints)
+            Input array to be used as database for the similarity search
         y : optional
             Not used.
 
diff --git a/aeon/similarity_search/query_search.py b/aeon/similarity_search/query_search.py
index ceb534b2..2fa3011e 100644
--- a/aeon/similarity_search/query_search.py
+++ b/aeon/similarity_search/query_search.py
@@ -4,7 +4,7 @@ __maintainer__ = ["baraline"]
 
 import warnings
 from collections.abc import Iterable
-from typing import Union, final
+from typing import Optional, final
 
 import numpy as np
 from numba import get_num_threads, set_num_threads
@@ -74,7 +74,7 @@ class QuerySearch(BaseSimilaritySearch):
 
     Attributes
     ----------
-    X_ : array, shape (n_cases, n_channels, n_timepoints)
+    X_ : np.ndarray, 3D array of shape (n_cases, n_channels, n_timepoints)
         The input time series stored during the fit method. This is the
         database we search in when given a query.
     distance_profile_function : function
@@ -94,7 +94,7 @@ class QuerySearch(BaseSimilaritySearch):
         k: int = 1,
         threshold: float = np.inf,
         distance: str = "euclidean",
-        distance_args: Union[None, dict] = None,
+        distance_args: Optional[dict] = None,
         inverse_distance: bool = False,
         normalize: bool = False,
         speed_up: str = "fastest",
@@ -116,13 +116,13 @@ class QuerySearch(BaseSimilaritySearch):
             n_jobs=n_jobs,
         )
 
-    def _fit(self, X, y=None):
+    def _fit(self, X: np.ndarray, y=None):
         """
         Check input format and store it to be used as search space during predict.
 
         Parameters
         ----------
-        X : array, shape (n_cases, n_channels, n_timepoints)
+        X : np.ndarray, 3D array of shape (n_cases, n_channels, n_timepoints)
             Input array to used as database for the similarity search
         y : optional
             Not used.
@@ -145,23 +145,23 @@ class QuerySearch(BaseSimilaritySearch):
     @final
     def predict(
         self,
-        X,
+        X: np.ndarray,
         axis=1,
         X_index=None,
         exclusion_factor=2.0,
         apply_exclusion_to_result=False,
-    ):
+    ) -> np.ndarray:
         """
-        Predict method: Check the shape of X and call _predict to perform the search.
+        Predict method : Check the shape of X and call _predict to perform the search.
 
         If the distance profile function is normalized, it stores the mean and stds
         from X and X_, with X_ the training data.
 
         Parameters
         ----------
-        X :  array, shape (n_channels, query_length)
+        X : np.ndarray, 2D array of shape (n_channels, query_length)
             Input query used for similarity search.
-        axis: int
+        axis : int
             The time point axis of the input series if it is 2D. If ``axis==0``, it is
             assumed each column is a time series and each row is a time point. i.e. the
             shape of the data is ``(n_timepoints,n_channels)``. ``axis==1`` indicates
@@ -182,7 +182,7 @@ class QuerySearch(BaseSimilaritySearch):
             the matching conditions defined by child classes. For example, with
             TopKSimilaritySearch, the k best matches are also subject to the exclusion
             zone, but with :math:`id_timestamp` the index of one of the k matches.
-        apply_exclusion_to_result: bool, default=False
+        apply_exclusion_to_result : bool, default=False
             Wheter to apply the exclusion factor to the output of the similarity search.
             This means that two matches of the query from the same sample must be at
             least spaced by +/- :math:`query_length//exclusion_factor`.
@@ -200,7 +200,7 @@ class QuerySearch(BaseSimilaritySearch):
 
         Returns
         -------
-        array, shape (n_matches, 2)
+        np.ndarray, 2D array of shape (n_matches, 2)
             An array containing the indexes of the matches between X and X_.
             The decision of wheter a candidate of size query_length from X_ is matched
             with X depends on the subclasses that implent the _predict method
@@ -240,7 +240,9 @@ class QuerySearch(BaseSimilaritySearch):
         set_num_threads(prev_threads)
         return X_preds
 
-    def _predict(self, distance_profiles, exclusion_size=None):
+    def _predict(
+        self, distance_profiles: np.ndarray, exclusion_size: Optional[int] = None
+    ) -> np.ndarray:
         """
         Private predict method for QuerySearch.
 
@@ -249,7 +251,7 @@ class QuerySearch(BaseSimilaritySearch):
 
         Parameters
         ----------
-        distance_profiles : array, shape (n_cases, n_timepoints - query_length + 1)
+        distance_profiles : np.ndarray, 2D array of shape (n_cases, n_timepoints - query_length + 1)  # noqa: E501
             Precomputed distance profile.
         exclusion_size : int, optional
             The size of the exclusion zone used to prevent returning as top k candidates
@@ -262,7 +264,7 @@ class QuerySearch(BaseSimilaritySearch):
 
         Returns
         -------
-        array
+        np.ndarray
             An array containing the indexes of the best k matches between q and _X.
 
         """
@@ -347,15 +349,19 @@ class QuerySearch(BaseSimilaritySearch):
             return top_k[:n_inserted]
 
     def _init_X_index_mask(
-        self, X_index, query_dim, query_length, exclusion_factor=2.0
-    ):
+        self,
+        X_index: Optional[Iterable[int]],
+        query_dim: int,
+        query_length: int,
+        exclusion_factor: Optional[float] = 2.0,
+    ) -> np.ndarray:
         """
         Initiliaze the mask indicating the candidates to be evaluated in the search.
 
         Parameters
         ----------
         X_index : Iterable
-            An Interable (tuple, list, array) of length two used to specify the index of
+            An Iterable (tuple, list, array) of length two used to specify the index of
             the query X if it was extracted from the input data X given during the fit
             method. Given the tuple (id_sample, id_timestamp), the similarity search
             will define an exclusion zone around the X_index in order to avoid matching
@@ -381,7 +387,7 @@ class QuerySearch(BaseSimilaritySearch):
 
         Returns
         -------
-        mask : array, shape=(n_cases, n_timepoints - query_length + 1)
+        mask : np.ndarray, 2D array of shape (n_cases, n_timepoints - query_length + 1)
             Boolean array which indicates the candidates that should be evaluated in the
             similarity search.
 
@@ -505,21 +511,21 @@ class QuerySearch(BaseSimilaritySearch):
         else:
             return naive_distance_profile
 
-    def _call_distance_profile(self, X, mask):
+    def _call_distance_profile(self, X: np.ndarray, mask: np.ndarray) -> np.ndarray:
         """
         Obtain the distance profile function and call it with the query and the mask.
 
         Parameters
         ----------
-        X :  array, shape (n_channels, query_length)
+        X : np.ndarray, 2D array of shape (n_channels, query_length)
             Input query used for similarity search.
-         mask : array, shape=(n_cases, n_timepoints - query_length + 1)
-             Boolean array which indicates the candidates that should be evaluated in
-             the similarity search.
+        mask : np.ndarray, 2D array of shape (n_cases, n_timepoints - query_length + 1)
+            Boolean array which indicates the candidates that should be evaluated in
+            the similarity search.
 
         Returns
         -------
-        distance_profiles : array, shape=(n_cases, n_timepoints - query_length + 1)
+        distance_profiles : np.ndarray, 2D array of shape (n_cases, n_timepoints - query_length + 1)  # noqa: E501
             The distance profiles between the input time series and the query.
 
         """
@@ -566,7 +572,7 @@ class QuerySearch(BaseSimilaritySearch):
             distance_profiles = distance_profiles.sum(axis=1)
         return distance_profiles
 
-    def _store_mean_std_from_inputs(self, query_length):
+    def _store_mean_std_from_inputs(self, query_length: int) -> None:
         """
         Store the mean and std of each subsequence of size query_length in X_.
 
@@ -577,7 +583,7 @@ class QuerySearch(BaseSimilaritySearch):
 
         Returns
         -------
-        None.
+        None
 
         """
         means = []
@@ -593,7 +599,7 @@ class QuerySearch(BaseSimilaritySearch):
         self.X_stds_ = List(stds)
 
     @classmethod
-    def get_speedup_function_names(self):
+    def get_speedup_function_names(self) -> dict:
         """
         Get available speedup for similarity search in aeon.
 
diff --git a/aeon/transformations/collection/shapelet_based/_sast.py b/aeon/transformations/collection/shapelet_based/_sast.py
index 513ef0ac..9b1b2ec9 100644
--- a/aeon/transformations/collection/shapelet_based/_sast.py
+++ b/aeon/transformations/collection/shapelet_based/_sast.py
@@ -83,7 +83,7 @@ class SAST(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": False,
-        "algorithm_type": "subsequence",
+        "algorithm_type": "shapelet",
     }
 
     def __init__(


Test patch:
diff --git a/aeon/clustering/tests/test_clara.py b/aeon/clustering/tests/test_clara.py
index 72383c99..ebfb3dad 100644
--- a/aeon/clustering/tests/test_clara.py
+++ b/aeon/clustering/tests/test_clara.py
@@ -34,17 +34,19 @@ def test_clara_uni():
     proba = clara.predict_proba(X_test)
     assert np.array_equal(
         test_medoids_result,
-        [0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1],
+        [1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0],
     )
     assert np.array_equal(
         train_medoids_result,
-        [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0],
+        [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1],
     )
-    assert test_score == 0.4789473684210526
-    assert train_score == 0.4789473684210526
-    assert np.isclose(clara.inertia_, 44.156819900725814)
+    assert test_score == 0.5210526315789473
+    assert train_score == 0.5578947368421052
+    assert np.isclose(clara.inertia_, 78.79839208236065)
     assert clara.n_iter_ == 2
-    assert np.array_equal(clara.labels_, [0, 0, 1, 0, 1, 1, 1, 0, 1, 1])
+    assert np.array_equal(
+        clara.labels_, [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1]
+    )
     assert isinstance(clara.cluster_centers_, np.ndarray)
     for val in proba:
         assert np.count_nonzero(val == 1.0) == 1
@@ -77,17 +79,19 @@ def test_clara_multi():
     proba = clara.predict_proba(X_test)
     assert np.array_equal(
         test_medoids_result,
-        [0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1],
+        [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
     )
     assert np.array_equal(
         train_medoids_result,
-        [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0],
+        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1],
     )
     assert test_score == 0.4789473684210526
     assert train_score == 0.5578947368421052
-    assert np.isclose(clara.inertia_, 95.91411895660636)
+    assert np.isclose(clara.inertia_, 1900.6752544011563)
     assert clara.n_iter_ == 3
-    assert np.array_equal(clara.labels_, [1, 1, 1, 1, 1, 1, 0, 1, 1, 1])
+    assert np.array_equal(
+        clara.labels_, [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1]
+    )
     assert isinstance(clara.cluster_centers_, np.ndarray)
     for val in proba:
         assert np.count_nonzero(val == 1.0) == 1