Repository: aeon-toolkit/aeon
Pull request #447 (2023-05-18 13:45:30)

[BUG] KNN distance params 
<!--
Thanks for contributing a pull request! Please ensure you have taken a look at our contribution guide: https://github.com/aeon-toolkit/aeon/blob/main/CONTRIBUTING.md

Feel free to delete sections of this template if they do not apply to your PR, avoid submitting a blank template or empty sections.
-->

#### Reference Issues/PRs
Issue #446 
<!--
Example: Fixes #1234. See also #3456.

Please use keywords (e.g., Fixes) to create link to the issues or pull requests you resolved, so that they will automatically be closed when your pull request is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->

#### What does this implement/fix? Explain your changes.
There was a bug where custom distance params were not being passed through to the distance function in the knn and knnregressor. These have been fixed.
<!--
A clear and concise description of what you have implemented.
-->

#### Does your contribution introduce a new dependency? If yes, which one?

<!--
If your contribution does add a dependency, we may suggest adding it as an optional/soft dependency to keep external dependencies of the core aeon package to a minimum.
-->

#### What should a reviewer concentrate their feedback on?

<!--
This section is particularly useful if you have a pull request that is still in development. You can guide the reviews to focus on the parts that are ready for their comments. We suggest using bullets (indicated by * or -) and filled checkboxes [x] here
-->

#### Did you add any tests for the change?

<!--
This section is useful if you have added a test in addition to the existing ones. This will ensure that further changes to these files won't introduce the same kind of bug. It is considered good practice to add tests with newly added code to enforce the fact that the code actually works. This will reduce the chance of introducing logical bugs.
-->

#### Any other comments?

<!--
Please be aware that we are a loose team of volunteers so patience is necessary; assistance handling other issues is very welcome. We value all user contributions, no matter how minor they are. If we are slow to review, either the pull request needs some benchmarking, tinkering, convincing, etc. or more likely the reviewers are simply busy. In either case, we ask for your understanding during the review process.
-->

### PR checklist

<!--
Please go through the checklist below. Please feel free to remove points if they are not applicable.
-->

##### For all contributions
- [x] I've added myself to the [list of contributors](https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc).
- [x] Optionally, I've updated aeon's [CODEOWNERS](https://github.com/aeon-toolkit/aeon/blob/main/CODEOWNERS) to receive notifications about future changes to these files.
- [x] The PR title starts with either [ENH], [MNT], [DOC], or [BUG] indicating whether the PR topic is related to enhancement, maintenance, documentation, or bug.
- [x] For new estimators, I've added the estimator to the online documentation.


<!--
Thanks for contributing!
-->



Patch:
diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py
index 4dfd300f..4290bb53 100644
--- a/aeon/classification/distance_based/_time_series_neighbors.py
+++ b/aeon/classification/distance_based/_time_series_neighbors.py
@@ -40,7 +40,7 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier):
         this will substitute a hard-coded distance metric from aeon.distances
         When mpdist is used, the subsequence length (parameter m) must be set
             Example: knn_mpdist = KNeighborsTimeSeriesClassifier(
-                                metric='mpdist', metric_params={'m':30})
+                                distance='mpdist', distance_params={'m':30})
         if callable, must be of signature (X: np.ndarray, X2: np.ndarray) -> np.ndarray
     distance_params : dict, optional. default = None.
         dictionary for metric parameters for the case that distance is a str
@@ -83,6 +83,10 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier):
         self.n_neighbors = n_neighbors
         self.n_jobs = n_jobs
 
+        self._distance_params = distance_params
+        if self._distance_params is None:
+            self._distance_params = {}
+
         if weights not in WEIGHTS_SUPPORTED:
             raise ValueError(
                 f"Unrecognised kNN weights: {weights}. "
@@ -192,7 +196,12 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier):
         ws : array
             Array representing the weights of each neighbor.
         """
-        distances = np.array([self.metric_(X, self.X_[j]) for j in range(len(self.X_))])
+        distances = np.array(
+            [
+                self.metric_(X, self.X_[j], **self._distance_params)
+                for j in range(len(self.X_))
+            ]
+        )
 
         # Find indices of k nearest neighbors using partitioning:
         # [0..k-1], [k], [k+1..n-1]
diff --git a/aeon/distances/_distance.py b/aeon/distances/_distance.py
index a1fe1e0d..d29632af 100644
--- a/aeon/distances/_distance.py
+++ b/aeon/distances/_distance.py
@@ -62,6 +62,7 @@ from aeon.distances._wdtw import (
     wdtw_distance,
     wdtw_pairwise_distance,
 )
+from aeon.distances.mpdist import mpdist
 
 DistanceFunction = Callable[[np.ndarray, np.ndarray, Any], float]
 AlignmentPathFunction = Callable[
@@ -138,6 +139,8 @@ def distance(
         return twe_distance(x, y, **kwargs)
     elif metric == "msm":
         return msm_distance(x, y, **kwargs)
+    elif metric == "mpdist":
+        return mpdist(x, y, **kwargs)
     else:
         if isinstance(metric, Callable):
             return metric(x, y, **kwargs)
@@ -231,6 +234,8 @@ def pairwise_distance(
         return twe_pairwise_distance(x, y, **kwargs)
     elif metric == "msm":
         return msm_pairwise_distance(x, y, **kwargs)
+    elif metric == "mpdist":
+        return _custom_func_pairwise(x, y, mpdist, **kwargs)
     else:
         if isinstance(metric, Callable):
             return _custom_func_pairwise(x, y, metric, **kwargs)
@@ -433,7 +438,7 @@ def get_distance_function(metric: Union[str, DistanceFunction]) -> DistanceFunct
         The distance metric to use.
         If a string is given, the value must be one of the following strings:
         'euclidean', 'squared', 'dtw', 'ddtw', 'wdtw', 'wddtw', 'lcss', 'edr', 'erp',
-        'msm'
+        'msm', 'mpdist'
         If a callable is given, the value must be a function that accepts two
         numpy arrays and **kwargs returns a float.
 
@@ -573,6 +578,8 @@ def get_cost_matrix_function(metric: str) -> CostMatrixFunction:
 
 
 def _resolve_key_from_distance(metric: str, key: str) -> Any:
+    if metric == "mpdist":
+        return mpdist
     dist = DISTANCES_DICT.get(metric)
     if dist is None:
         raise ValueError(f"Unknown metric {metric}")
diff --git a/aeon/regression/distance_based/_time_series_neighbors.py b/aeon/regression/distance_based/_time_series_neighbors.py
index 347645f7..362e9336 100644
--- a/aeon/regression/distance_based/_time_series_neighbors.py
+++ b/aeon/regression/distance_based/_time_series_neighbors.py
@@ -81,6 +81,10 @@ class KNeighborsTimeSeriesRegressor(BaseRegressor):
             )
         self.weights = weights
 
+        self._distance_params = distance_params
+        if self._distance_params is None:
+            self._distance_params = {}
+
         super(KNeighborsTimeSeriesRegressor, self).__init__()
 
     def _fit(self, X, y):
@@ -140,7 +144,10 @@ class KNeighborsTimeSeriesRegressor(BaseRegressor):
             Array representing the weights of each neighbor.
         """
         distances = np.array(
-            [self.metric_(X, self.X_[j]) for j in range(self.X_.shape[0])]
+            [
+                self.metric_(X, self.X_[j], **self._distance_params)
+                for j in range(self.X_.shape[0])
+            ]
         )
 
         # Find indices of k nearest neighbors using partitioning:


Test patch:
diff --git a/aeon/classification/distance_based/tests/test_time_series_neighbors.py b/aeon/classification/distance_based/tests/test_time_series_neighbors.py
index 40fd9c77..83fb8857 100644
--- a/aeon/classification/distance_based/tests/test_time_series_neighbors.py
+++ b/aeon/classification/distance_based/tests/test_time_series_neighbors.py
@@ -33,7 +33,7 @@ expected_correct_window = {
     "euclidean": 19,
     "dtw": 21,
     "wdtw": 21,
-    "msm": 20,
+    "msm": 10,
     "erp": 19,
     "edr": 20,
     "lcss": 12,
@@ -61,6 +61,8 @@ def test_knn_on_unit_test(distance_key):
 @pytest.mark.parametrize("distance_key", distance_functions)
 def test_knn_bounding_matrix(distance_key):
     """Test knn with custom bounding parameters."""
+    if distance_key == "euclidean" or distance_key == "squared":
+        return
     X_train, y_train = load_unit_test(split="train")
     X_test, y_test = load_unit_test(split="test")
     knn = KNeighborsTimeSeriesClassifier(
diff --git a/aeon/classification/tests/_expected_outputs.py b/aeon/classification/tests/_expected_outputs.py
index 9a36b9d0..22ae5050 100644
--- a/aeon/classification/tests/_expected_outputs.py
+++ b/aeon/classification/tests/_expected_outputs.py
@@ -86,7 +86,7 @@ unit_test_proba["ElasticEnsemble"] = np.array(
         [1.0, 0.0],
         [0.0, 1.0],
         [1.0, 0.0],
-        [0.66666667, 0.33333333],
+        [0.33333333, 0.66666667],
         [1.0, 0.0],
         [0.66666667, 0.33333333],
         [0.0, 1.0],