Repository: scipy/scipy

Issue:
DEP: remove `scipy.spatial.distance.kulsinski`
Follow-up to #15125 / #16265


Discussion:

Resolving patch:
diff --git a/benchmarks/benchmarks/spatial.py b/benchmarks/benchmarks/spatial.py
--- a/benchmarks/benchmarks/spatial.py
+++ b/benchmarks/benchmarks/spatial.py
@@ -325,11 +325,13 @@ def time_spherical_polygon_area_calculation(self, num_points):
 
 
 class Xdist(Benchmark):
-    params = ([10, 100, 1000], ['euclidean', 'minkowski', 'cityblock',
-    'seuclidean', 'sqeuclidean', 'cosine', 'correlation', 'hamming', 'jaccard',
-    'jensenshannon', 'chebyshev', 'canberra', 'braycurtis', 'mahalanobis',
-    'yule', 'dice', 'kulsinski', 'rogerstanimoto', 'russellrao',
-    'sokalmichener', 'sokalsneath', 'wminkowski', 'minkowski-P3'])
+    params = ([10, 100, 1000],
+              ['euclidean', 'minkowski', 'cityblock',
+               'seuclidean', 'sqeuclidean', 'cosine', 'correlation',
+               'hamming', 'jaccard', 'jensenshannon', 'chebyshev', 'canberra',
+               'braycurtis', 'mahalanobis', 'yule', 'dice', 'kulczynski1',
+               'rogerstanimoto', 'russellrao', 'sokalmichener', 'sokalsneath',
+               'wminkowski', 'minkowski-P3'])
     param_names = ['num_points', 'metric']
 
     def setup(self, num_points, metric):
@@ -364,7 +366,7 @@ class XdistWeighted(Benchmark):
         [10, 20, 100],
         ['euclidean', 'minkowski', 'cityblock', 'sqeuclidean', 'cosine',
          'correlation', 'hamming', 'jaccard', 'chebyshev', 'canberra',
-         'braycurtis', 'yule', 'dice', 'kulsinski', 'rogerstanimoto',
+         'braycurtis', 'yule', 'dice', 'kulczynski1', 'rogerstanimoto',
          'russellrao', 'sokalmichener', 'sokalsneath', 'minkowski-P3'])
     param_names = ['num_points', 'metric']
 
diff --git a/scipy/spatial/distance.py b/scipy/spatial/distance.py
--- a/scipy/spatial/distance.py
+++ b/scipy/spatial/distance.py
@@ -60,7 +60,6 @@
    dice             -- the Dice dissimilarity.
    hamming          -- the Hamming distance.
    jaccard          -- the Jaccard distance.
-   kulsinski        -- the Kulsinski distance.
    kulczynski1      -- the Kulczynski 1 distance.
    rogerstanimoto   -- the Rogers-Tanimoto dissimilarity.
    russellrao       -- the Russell-Rao dissimilarity.
@@ -89,7 +88,6 @@
     'is_valid_y',
     'jaccard',
     'jensenshannon',
-    'kulsinski',
     'kulczynski1',
     'mahalanobis',
     'minkowski',
@@ -123,8 +121,6 @@
 
 from . import _distance_pybind
 
-from .._lib.deprecation import _deprecated
-
 def _copy_array_if_base_present(a):
     """Copy the array if its base points to a parent array."""
     if a.base is not None:
@@ -798,70 +794,6 @@ def jaccard(u, v, w=None):
     return (a / b) if b != 0 else 0
 
 
-@_deprecated("Kulsinski has been deprecated from scipy.spatial.distance"
-             " in SciPy 1.9.0 and it will be removed in SciPy 1.11.0."
-             " It is superseded by scipy.spatial.distance.kulczynski1.")
-def kulsinski(u, v, w=None):
-    """
-    Compute the Kulsinski dissimilarity between two boolean 1-D arrays.
-
-    The Kulsinski dissimilarity between two boolean 1-D arrays `u` and `v`,
-    is defined as
-
-    .. math::
-
-         \\frac{c_{TF} + c_{FT} - c_{TT} + n}
-              {c_{FT} + c_{TF} + n}
-
-    where :math:`c_{ij}` is the number of occurrences of
-    :math:`\\mathtt{u[k]} = i` and :math:`\\mathtt{v[k]} = j` for
-    :math:`k < n`.
-
-    .. deprecated:: 0.12.0
-        `kulsinski` has been deprecated from `scipy.spatial.distance` in
-        SciPy 1.9.0 and it will be removed in SciPy 1.11.0. It is superseded
-        by `scipy.spatial.distance.kulczynski1`.
-
-    Parameters
-    ----------
-    u : (N,) array_like, bool
-        Input array.
-    v : (N,) array_like, bool
-        Input array.
-    w : (N,) array_like, optional
-        The weights for each value in `u` and `v`. Default is None,
-        which gives each value a weight of 1.0
-
-    Returns
-    -------
-    kulsinski : double
-        The Kulsinski distance between vectors `u` and `v`.
-
-    Examples
-    --------
-    >>> from scipy.spatial import distance
-    >>> distance.kulsinski([1, 0, 0], [0, 1, 0])
-    1.0
-    >>> distance.kulsinski([1, 0, 0], [1, 1, 0])
-    0.75
-    >>> distance.kulsinski([1, 0, 0], [2, 1, 0])
-    0.33333333333333331
-    >>> distance.kulsinski([1, 0, 0], [3, 1, 0])
-    -0.5
-
-    """
-    u = _validate_vector(u)
-    v = _validate_vector(v)
-    if w is None:
-        n = float(len(u))
-    else:
-        w = _validate_weights(w)
-        n = w.sum()
-    (nff, nft, ntf, ntt) = _nbool_correspond_all(u, v, w=w)
-
-    return (ntf + nft - ntt + n) / (ntf + nft + n)
-
-
 def kulczynski1(u, v, *, w=None):
     """
     Compute the Kulczynski 1 dissimilarity between two boolean 1-D arrays.
@@ -1850,14 +1782,6 @@ class MetricInfo:
         cdist_func=CDistMetricWrapper('jensenshannon'),
         pdist_func=PDistMetricWrapper('jensenshannon'),
     ),
-    MetricInfo(
-        canonical_name='kulsinski',
-        aka={'kulsinski'},
-        types=['bool'],
-        dist_func=kulsinski,
-        cdist_func=CDistMetricWrapper('kulsinski'),
-        pdist_func=PDistMetricWrapper('kulsinski'),
-    ),
     MetricInfo(
         canonical_name='kulczynski1',
         aka={'kulczynski1'},


Test patch:
diff --git a/scipy/spatial/tests/test_distance.py b/scipy/spatial/tests/test_distance.py
--- a/scipy/spatial/tests/test_distance.py
+++ b/scipy/spatial/tests/test_distance.py
@@ -52,12 +52,12 @@
     squareform, pdist, cdist, num_obs_y, num_obs_dm, is_valid_dm, is_valid_y,
     _validate_vector, _METRICS_NAMES)
 
-# these were missing: chebyshev cityblock kulsinski
+# these were missing: chebyshev cityblock
 # jensenshannon  and seuclidean are referenced by string name.
 from scipy.spatial.distance import (braycurtis, canberra, chebyshev, cityblock,
                                     correlation, cosine, dice, euclidean,
                                     hamming, jaccard, jensenshannon,
-                                    kulsinski, kulczynski1, mahalanobis,
+                                    kulczynski1, mahalanobis,
                                     minkowski, rogerstanimoto,
                                     russellrao, seuclidean, sokalmichener,
                                     sokalsneath, sqeuclidean, yule)
@@ -368,7 +368,6 @@ def wrapped(*args, **kwargs):
 wchebyshev = _weight_checked(chebyshev)
 wcosine = _weight_checked(cosine)
 wcorrelation = _weight_checked(correlation)
-wkulsinski = _weight_checked(kulsinski)
 wkulczynski1 = _weight_checked(kulczynski1)
 wjaccard = _weight_checked(jaccard)
 weuclidean = _weight_checked(euclidean, const_test=False)
@@ -401,22 +400,16 @@ def _my_metric(x, y, arg, kwarg=1, kwarg2=2):
         kwargs = {'N0tV4l1D_p4raM': 3.14, "w":np.arange(3)}
         args = [3.14] * 200
         for metric in _METRICS_NAMES:
-            with np.testing.suppress_warnings() as sup:
-                if metric == "kulsinski":
-                    sup.filter(DeprecationWarning,
-                               "Kulsinski has been deprecated from")
-                assert_raises(TypeError, cdist, X1, X2,
-                              metric=metric, **kwargs)
-                assert_raises(TypeError, cdist, X1, X2,
-                              metric=eval(metric), **kwargs)
-                assert_raises(TypeError, cdist, X1, X2,
-                              metric="test_" + metric, **kwargs)
-                assert_raises(TypeError, cdist, X1, X2,
-                              metric=metric, *args)
-                assert_raises(TypeError, cdist, X1, X2,
-                              metric=eval(metric), *args)
-                assert_raises(TypeError, cdist, X1, X2,
-                              metric="test_" + metric, *args)
+            assert_raises(TypeError, cdist, X1, X2, metric=metric, **kwargs)
+            assert_raises(TypeError, cdist, X1, X2, metric=eval(metric),
+                          **kwargs)
+            assert_raises(TypeError, cdist, X1, X2, metric="test_" + metric,
+                          **kwargs)
+            assert_raises(TypeError, cdist, X1, X2, metric=metric, *args)
+            assert_raises(TypeError, cdist, X1, X2, metric=eval(metric),
+                          *args)
+            assert_raises(TypeError, cdist, X1, X2, metric="test_" + metric,
+                          *args)
 
         assert_raises(TypeError, cdist, X1, X2, _my_metric)
         assert_raises(TypeError, cdist, X1, X2, _my_metric, *args)
@@ -528,18 +521,14 @@ def test_cdist_calling_conventions(self):
             for metric in _METRICS_NAMES:
                 if verbose > 2:
                     print("testing: ", metric, " with: ", eo_name)
-                if metric in {'dice', 'yule', 'kulsinski',
+                if metric in {'dice', 'yule',
                               'rogerstanimoto',
                               'russellrao', 'sokalmichener',
                               'sokalsneath',
                               'kulczynski1'} and 'bool' not in eo_name:
                     # python version permits non-bools e.g. for fuzzy logic
                     continue
-                with np.testing.suppress_warnings() as sup:
-                    if metric == "kulsinski":
-                        sup.filter(DeprecationWarning,
-                                   "Kulsinski has been deprecated from")
-                    self._check_calling_conventions(X1, X2, metric)
+                self._check_calling_conventions(X1, X2, metric)
 
                 # Testing built-in metrics with extra args
                 if metric == "seuclidean":
@@ -683,19 +672,14 @@ def _my_metric(x, y, arg, kwarg=1, kwarg2=2):
         kwargs = {'N0tV4l1D_p4raM': 3.14, "w":np.arange(2)}
         args = [3.14] * 200
         for metric in _METRICS_NAMES:
-            with np.testing.suppress_warnings() as sup:
-                if metric == "kulsinski":
-                    sup.filter(DeprecationWarning,
-                               "Kulsinski has been deprecated from")
-                assert_raises(TypeError, pdist, X1, metric=metric, **kwargs)
-                assert_raises(TypeError, pdist, X1,
-                              metric=eval(metric), **kwargs)
-                assert_raises(TypeError, pdist, X1,
-                              metric="test_" + metric, **kwargs)
-                assert_raises(TypeError, pdist, X1, metric=metric, *args)
-                assert_raises(TypeError, pdist, X1, metric=eval(metric), *args)
-                assert_raises(TypeError, pdist, X1,
-                              metric="test_" + metric, *args)
+            assert_raises(TypeError, pdist, X1, metric=metric, **kwargs)
+            assert_raises(TypeError, pdist, X1, metric=eval(metric), **kwargs)
+            assert_raises(TypeError, pdist, X1, metric="test_" + metric,
+                          **kwargs)
+            assert_raises(TypeError, pdist, X1, metric=metric, *args)
+            assert_raises(TypeError, pdist, X1, metric=eval(metric), *args)
+            assert_raises(TypeError, pdist, X1, metric="test_" + metric,
+                          *args)
 
         assert_raises(TypeError, pdist, X1, _my_metric)
         assert_raises(TypeError, pdist, X1, _my_metric, *args)
@@ -1442,17 +1426,13 @@ def test_pdist_calling_conventions(self):
             for metric in _METRICS_NAMES:
                 if verbose > 2:
                     print("testing: ", metric, " with: ", eo_name)
-                if metric in {'dice', 'yule', 'kulsinski', 'matching',
+                if metric in {'dice', 'yule', 'matching',
                               'rogerstanimoto', 'russellrao', 'sokalmichener',
                               'sokalsneath',
                               'kulczynski1'} and 'bool' not in eo_name:
                     # python version permits non-bools e.g. for fuzzy logic
                     continue
-                with np.testing.suppress_warnings() as sup:
-                    if metric == "kulsinski":
-                        sup.filter(DeprecationWarning,
-                                   "Kulsinski has been deprecated from")
-                    self._check_calling_conventions(X, metric)
+                self._check_calling_conventions(X, metric)
 
                 # Testing built-in metrics with extra args
                 if metric == "seuclidean":
@@ -2098,13 +2078,9 @@ def test_Xdist_non_negative_weights():
     for metric in _METRICS_NAMES:
         if metric in ['seuclidean', 'mahalanobis', 'jensenshannon']:
             continue
-        with np.testing.suppress_warnings() as sup:
-            if metric == "kulsinski":
-                sup.filter(DeprecationWarning,
-                           "Kulsinski has been deprecated from")
-            for m in [metric, eval(metric), "test_" + metric]:
-                assert_raises(ValueError, pdist, X, m, w=w)
-                assert_raises(ValueError, cdist, X, X, m, w=w)
+        for m in [metric, eval(metric), "test_" + metric]:
+            assert_raises(ValueError, pdist, X, m, w=w)
+            assert_raises(ValueError, cdist, X, X, m, w=w)
 
 
 def test__validate_vector():
@@ -2176,11 +2152,3 @@ def test_jensenshannon():
                         [0.1954288, 0.1447697, 0.1138377, 0.0927636])
     assert_almost_equal(jensenshannon(a, b, axis=1),
                         [0.1402339, 0.0399106, 0.0201815])
-
-
-def test_kulsinski_deprecation():
-    msg = ("Kulsinski has been deprecated from scipy.spatial.distance"
-           " in SciPy 1.9.0 and it will be removed in SciPy 1.11.0."
-           " It is superseded by scipy.spatial.distance.kulczynski1.")
-    with pytest.warns(DeprecationWarning, match=msg):
-        kulsinski([], [])