Repository: aeon-toolkit/aeon
Pull request #2040 (2024-09-08 09:17:05)

[ENH] TimeSeriesKMeans init_algorithm
#2036 
 
What does this implement/fix? Explain your changes.
This change renames the init_algorithm argument in the aeon KMeans implementation to init, aligning it with the naming convention used in scikit-learn

### PR checklist
##### For all contributions
- [x] I've added myself to the [list of contributors](https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc). Alternatively, you can use the [@all-contributors](https://allcontributors.org/docs/en/bot/usage) bot to do this for you.
- [x] The PR title starts with either [ENH], [MNT], [DOC], [BUG], [REF], [DEP] or [GOV] indicating whether the PR topic is related to enhancement, maintenance, documentation, bugs, refactoring, deprecation or governance.


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

I have added the following labels to this PR based on the title: **[ $\\color{#FEF1BE}{\\textsf{enhancement}}$ ]**.
I have added the following labels to this PR based on the changes made: **[ $\\color{#4011F3}{\\textsf{clustering}}$ ]**. Feel free to change these if they do not properly represent the PR.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/2040/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
    
TonyBagnall: thanks for doing this, normally we would need this to be deprecated. This means keeping the old and new variables, default the old one to deprecated, then if it is set in the constructor, issue a warning and set init to the value passed. Think this is correct @MatthewMiddlehurst ? Is there a current example?
Sharathchenna: @TonyBagnall Made the changes. Please tell me if I need to do anything else.
TonyBagnall: @all-contributors add @Sharathchenna for code
allcontributors[bot]: @TonyBagnall 

@Sharathchenna already contributed before to code

Patch:
diff --git a/aeon/clustering/_k_means.py b/aeon/clustering/_k_means.py
index 8c46a5df..854d7101 100644
--- a/aeon/clustering/_k_means.py
+++ b/aeon/clustering/_k_means.py
@@ -40,7 +40,7 @@ class TimeSeriesKMeans(BaseClusterer):
     ----------
     n_clusters : int, default=8
         The number of clusters to form as well as the number of centroids to generate.
-    init_algorithm : str or np.ndarray, default='random'
+    init : str or np.ndarray, default='random'
         Random is the default and simply chooses k time series at random as
         centroids. It is fast but sometimes yields sub-optimal clustering.
         Kmeans++ [2] and is slower but often more
@@ -156,7 +156,7 @@ class TimeSeriesKMeans(BaseClusterer):
     def __init__(
         self,
         n_clusters: int = 8,
-        init_algorithm: Union[str, np.ndarray] = "random",
+        init: Union[str, np.ndarray] = "random",
         distance: Union[str, Callable] = "msm",
         n_init: int = 10,
         max_iter: int = 300,
@@ -166,7 +166,20 @@ class TimeSeriesKMeans(BaseClusterer):
         averaging_method: Union[str, Callable[[np.ndarray], np.ndarray]] = "ba",
         distance_params: dict = None,
         average_params: dict = None,
+        init_algorithm: Union[str, np.ndarray] = None,
     ):
+        if init_algorithm is not None:
+            import warnings
+
+            warnings.warn(
+                "The 'init_algorithm' parameter is deprecated and will be "
+                "removed in a future. Version Use 'init' instead.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+            self.init = self.init_algorithm
+        else:
+            self.init = init
         self.init_algorithm = init_algorithm
         self.distance = distance
         self.n_init = n_init
@@ -184,7 +197,7 @@ class TimeSeriesKMeans(BaseClusterer):
         self.n_iter_ = 0
 
         self._random_state = None
-        self._init_algorithm = None
+        self._init = None
         self._averaging_method = None
         self._average_params = None
 
@@ -225,10 +238,10 @@ class TimeSeriesKMeans(BaseClusterer):
         self.n_iter_ = best_iters
 
     def _fit_one_init(self, X: np.ndarray) -> tuple:
-        if isinstance(self._init_algorithm, Callable):
-            cluster_centres = self._init_algorithm(X)
+        if isinstance(self._init, Callable):
+            cluster_centres = self._init(X)
         else:
-            cluster_centres = self._init_algorithm.copy()
+            cluster_centres = self._init.copy()
         prev_inertia = np.inf
         prev_labels = None
         for i in range(self.max_iter):
@@ -287,22 +300,19 @@ class TimeSeriesKMeans(BaseClusterer):
     def _check_params(self, X: np.ndarray) -> None:
         self._random_state = check_random_state(self.random_state)
 
-        if isinstance(self.init_algorithm, str):
-            if self.init_algorithm == "random":
-                self._init_algorithm = self._random_center_initializer
-            elif self.init_algorithm == "kmeans++":
-                self._init_algorithm = self._kmeans_plus_plus_center_initializer
-            elif self.init_algorithm == "first":
-                self._init_algorithm = self._first_center_initializer
+        if isinstance(self.init, str):
+            if self.init == "random":
+                self._init = self._random_center_initializer
+            elif self.init == "kmeans++":
+                self._init = self._kmeans_plus_plus_center_initializer
+            elif self.init == "first":
+                self._init = self._first_center_initializer
         else:
-            if (
-                isinstance(self.init_algorithm, np.ndarray)
-                and len(self.init_algorithm) == self.n_clusters
-            ):
-                self._init_algorithm = self.init_algorithm.copy()
+            if isinstance(self.init, np.ndarray) and len(self.init) == self.n_clusters:
+                self._init = self.init.copy()
             else:
                 raise ValueError(
-                    f"The value provided for init_algorithm: {self.init_algorithm} is "
+                    f"The value provided for init: {self.init} is "
                     f"invalid. The following are a list of valid init algorithms "
                     f"strings: random, kmedoids++, first. You can also pass a"
                     f"np.ndarray of size (n_clusters, n_channels, n_timepoints)"


Test patch:
diff --git a/aeon/clustering/tests/test_k_means.py b/aeon/clustering/tests/test_k_means.py
index 139c4a1e..50ac19e4 100644
--- a/aeon/clustering/tests/test_k_means.py
+++ b/aeon/clustering/tests/test_k_means.py
@@ -124,7 +124,7 @@ def test_kmeans():
         random_state=1,
         n_init=2,
         n_clusters=2,
-        init_algorithm="kmeans++",
+        init="kmeans++",
         distance="euclidean",
     )
     train_predict = kmeans.fit_predict(X_train)
@@ -162,7 +162,7 @@ def test_kmeans_dba():
         random_state=1,
         n_init=2,
         n_clusters=2,
-        init_algorithm="kmeans++",
+        init="kmeans++",
         distance="dtw",
     )
     train_predict = kmeans.fit_predict(X_train[0:num_test_values])
@@ -191,7 +191,7 @@ def _get_model_centres(data, distance, average_params=None, distance_params=None
         random_state=1,
         n_init=2,
         n_clusters=2,
-        init_algorithm="random",
+        init="random",
         distance=distance,
         average_params=average_params,
         distance_params=distance_params,
@@ -292,7 +292,7 @@ def test_means_init():
         random_state=1,
         n_init=1,
         max_iter=5,
-        init_algorithm="first",
+        init="first",
         distance="euclidean",
         n_clusters=num_clusters,
     )
@@ -312,7 +312,7 @@ def test_means_init():
         random_state=1,
         n_init=1,
         max_iter=5,
-        init_algorithm=custom_init_centres,
+        init=custom_init_centres,
         distance="euclidean",
         n_clusters=num_clusters,
     )
@@ -359,7 +359,7 @@ def test_empty_cluster():
         random_state=1,
         n_init=1,
         max_iter=5,
-        init_algorithm=init_centres,
+        init=init_centres,
         distance="euclidean",
         averaging_method="mean",
         n_clusters=3,
@@ -377,7 +377,7 @@ def test_empty_cluster():
         random_state=1,
         n_init=1,
         max_iter=5,
-        init_algorithm=init_centres,
+        init=init_centres,
         distance="euclidean",
         averaging_method="mean",
         n_clusters=3,
@@ -394,7 +394,7 @@ def test_empty_cluster():
         random_state=1,
         n_init=1,
         max_iter=5,
-        init_algorithm=init_centres,
+        init=init_centres,
         distance="euclidean",
         averaging_method="mean",
         n_clusters=2,
@@ -411,7 +411,7 @@ def test_empty_cluster():
         random_state=1,
         n_init=1,
         max_iter=5,
-        init_algorithm=init_centres,
+        init=init_centres,
         distance="euclidean",
         averaging_method="mean",
         n_clusters=3,