Repository: sktime/sktime

Issue:
[ENH] weighted version of scipy based distance class
Also related to #1884.

I think it would be a good idea to extend `ScipyDist` with an optional parameter `weights` (1D iterable of float) which multiples the i-th column of `X`, `X2` with the i-the element in `weights` before applying the distance. If weights are passed, there should be a check in `_transform` for `weights` having length equal to number of columns of `X`, `X2`.

Note: this should be done manually in `sktime`, since weighting is deprecated in `scipy`.


Discussion:
Hi, I wanted to get involved in the project. Can you assin me to this issue?
All yours @stepinski :tada: 


Resolving patch:
diff --git a/sktime/dists_kernels/scipy_dist.py b/sktime/dists_kernels/scipy_dist.py
--- a/sktime/dists_kernels/scipy_dist.py
+++ b/sktime/dists_kernels/scipy_dist.py
@@ -7,8 +7,8 @@
 
 __author__ = ["fkiraly"]
 
+import numpy as np
 import pandas as pd
-
 from scipy.spatial.distance import cdist
 
 from sktime.dists_kernels._base import BasePairwiseTransformer
@@ -23,15 +23,15 @@ class ScipyDist(BasePairwiseTransformer):
 
     Parameters
     ----------
-    metric: string or function, as in cdist; default = 'euclidean'
+    metric : string or function, as in cdist; default = 'euclidean'
         if string, one of: 'braycurtis', 'canberra', 'chebyshev', 'cityblock',
             'correlation', 'cosine', 'dice', 'euclidean', 'hamming', 'jaccard',
             'jensenshannon', 'kulsinski', 'mahalanobis', 'matching', 'minkowski',
             'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener',
             'sokalsneath', 'sqeuclidean', 'yule'
         if function, should have signature 1D-np.array x 1D-np.array -> float
-    p: if metric='minkowski', the "p" in "p-norm", otherwise irrelevant
-    colalign: string, one of 'intersect' (default), 'force-align', 'none'
+    p:  if metric='minkowski', the "p" in "p-norm", otherwise irrelevant
+    colalign : string, one of 'intersect' (default), 'force-align', 'none'
         controls column alignment if X, X2 passed in fit are pd.DataFrame
         columns between X and X2 are aligned via column names
         if 'intersect', distance is computed on columns occurring both in X and X2,
@@ -40,17 +40,34 @@ class ScipyDist(BasePairwiseTransformer):
             column ordering in X2 is copied from X
         if 'none', X and X2 are passed through unmodified (no columns are aligned)
             note: this will potentially align "non-matching" columns
+    var_weights : 1D np.array of float or None, default=None
+        weight/scaling vector applied to variables in X/X2
+        before being passed to cdist, i-th col of X/X2 is multiplied by var_weights[i]
+        if None, equivalent to all-ones vector
+    metric_kwargs : dict, optional, default=None
+        any kwargs passed to the metric in addition, i.e., to the function cdist
+        common kwargs: "w" : array-like, same length as X.columns, weights for metric
+        refer to scipy.spatial.distance.dist for a documentation of other extra kwargs
     """
 
     _tags = {
         "symmetric": True,  # all the distances are symmetric
     }
 
-    def __init__(self, metric="euclidean", p=2, colalign="intersect"):
+    def __init__(
+        self,
+        metric="euclidean",
+        p=2,
+        colalign="intersect",
+        var_weights=None,
+        metric_kwargs=None,
+    ):
 
         self.metric = metric
         self.p = p
         self.colalign = colalign
+        self.var_weights = var_weights
+        self.metric_kwargs = metric_kwargs
 
         super(ScipyDist, self).__init__()
 
@@ -78,6 +95,10 @@ def _transform(self, X, X2=None):
         """
         p = self.p
         metric = self.metric
+        var_weights = self.var_weights
+        metric_kwargs = self.metric_kwargs
+        if metric_kwargs is None:
+            metric_kwargs = {}
 
         if isinstance(X, pd.DataFrame):
             X = X.select_dtypes("number").to_numpy(dtype="float")
@@ -85,9 +106,45 @@ def _transform(self, X, X2=None):
         if isinstance(X2, pd.DataFrame):
             X2 = X2.select_dtypes("number").to_numpy(dtype="float")
 
-        if metric == "minkowski":
-            distmat = cdist(XA=X, XB=X2, metric=metric, p=p)
+        if np.ndim(var_weights) == 1:
+            if len(var_weights) == len(X.columns) == len(X2.columns):
+                X = var_weights * X
+                X2 = var_weights * X2
+            else:
+                raise ValueError(
+                    "weights vector length must be equal to X and X2 number of columns"
+                )
+
+        if metric == "minkowski" and "p" not in metric_kwargs.keys():
+            distmat = cdist(XA=X, XB=X2, metric=metric, p=p, **metric_kwargs)
         else:
-            distmat = cdist(XA=X, XB=X2, metric=metric)
+            distmat = cdist(XA=X, XB=X2, metric=metric, **metric_kwargs)
 
         return distmat
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        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.
+            There are currently no reserved values for distance/kernel transformers.
+
+        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`
+        """
+        # default settings
+        params1 = {}
+
+        # using kwargs
+        params2 = {"metric": "minkowski", "p": 3}
+
+        return [params1, params2]


Test patch: