Repository: scipy/scipy

Issue:
ENH: allow p>0 for minkowski metric
The following [comment](https://github.com/scipy/scipy/pull/3163#issuecomment-34224341) has been carried around through a lot of metrics-related issues/PRs. I had brought it up [again](https://github.com/scipy/scipy/issues/14077#issuecomment-899031445) in the context of #14077, but since the `_distance_pybind`-rewrite doesn't cover the extension from `p>=1` to `p>=0`, I thought I'd open a separate issue (since the other points from that initial comment are now tracked separately, cf. again my [comment](https://github.com/scipy/scipy/issues/14077#issuecomment-899031445)).

For reference, here's the relevant quote from that original comment:

> 4. `minkowski` has a check for `p >= 1`. That condition is usually enforced for the Minkowski distance to be a metric, i.e. for the triangle inequality to hold. This is a very restrictive condition, because many of the other distances are not proper metrics. Furthermore, the condition is not enforced either in `pdist` or `cdist`, which will happily take `p`s smaller than 1. I would like to drop this as well.

Also of note is this more recent [comment](https://github.com/scipy/scipy/pull/3163#issuecomment-857648599) by @sturlamolden in #3163:
> Both the distance functions and kd-tree have this limitation. It is actually nonsence, as the minkowski distance is symmetric around the Mahattan distance at `p==1`. And in the same way the limit to `p==inf` is the Chebychev distance `max(abs(x-y))`, the limit to `p==0`is `min(abs(x-y))`. We should special case minkowski `for p in [0, 1, 2, inf]` and allow `p>=0` instead of `p>=1`.


Discussion:
I was actually thinking about implementing this, and was wondering about the limit for `p->0` again. Turns out that the quote in the OP is not correct.

The distance `min(abs(x-y))` is the limit for `p->-inf` (see e.g. [wikipedia](https://en.wikipedia.org/wiki/Minkowski_distance)), whereas the limit for `p->0` is something [weird](https://math.stackexchange.com/questions/385797/limits-of-the-minkowski-distance-as-related-to-the-generalized-mean). I think `p>0` is probably good enough here.

Resolving patch:
diff --git a/scipy/spatial/distance.py b/scipy/spatial/distance.py
--- a/scipy/spatial/distance.py
+++ b/scipy/spatial/distance.py
@@ -255,8 +255,8 @@ def _validate_minkowski_kwargs(X, m, n, **kwargs):
     if 'p' not in kwargs:
         kwargs['p'] = 2.
     else:
-        if kwargs['p'] < 1:
-            raise ValueError("p must be at least 1")
+        if kwargs['p'] <= 0:
+            raise ValueError("p must be greater than 0")
 
     return kwargs
 
@@ -428,7 +428,7 @@ def minkowski(u, v, p=2, w=None):
 
     .. math::
 
-       {||u-v||}_p = (\\sum{|u_i - v_i|^p})^{1/p}.
+       {\\|u-v\\|}_p = (\\sum{|u_i - v_i|^p})^{1/p}.
 
 
        \\left(\\sum{w_i(|(u_i - v_i)|^p)}\\right)^{1/p}.
@@ -440,7 +440,9 @@ def minkowski(u, v, p=2, w=None):
     v : (N,) array_like
         Input array.
     p : scalar
-        The order of the norm of the difference :math:`{||u-v||}_p`.
+        The order of the norm of the difference :math:`{\\|u-v\\|}_p`. Note
+        that for :math:`0 < p < 1`, the triangle inequality only holds with
+        an additional multiplicative factor, i.e. it is only a quasi-metric.
     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
@@ -469,8 +471,8 @@ def minkowski(u, v, p=2, w=None):
     """
     u = _validate_vector(u)
     v = _validate_vector(v)
-    if p < 1:
-        raise ValueError("p must be at least 1")
+    if p <= 0:
+        raise ValueError("p must be greater than 0")
     u_v = u - v
     if w is not None:
         w = _validate_weights(w)
@@ -496,7 +498,7 @@ def euclidean(u, v, w=None):
 
     .. math::
 
-       {||u-v||}_2
+       {\\|u-v\\|}_2
 
        \\left(\\sum{(w_i |(u_i - v_i)|^2)}\\right)^{1/2}
 
@@ -535,7 +537,7 @@ def sqeuclidean(u, v, w=None):
 
     .. math::
 
-       {||u-v||}_2^2
+       {\\|u-v\\|}_2^2
 
        \\left(\\sum{(w_i |(u_i - v_i)|^2)}\\right)
 
@@ -591,7 +593,7 @@ def correlation(u, v, w=None, centered=True):
     .. math::
 
         1 - \\frac{(u - \\bar{u}) \\cdot (v - \\bar{v})}
-                  {{||(u - \\bar{u})||}_2 {||(v - \\bar{v})||}_2}
+                  {{\\|(u - \\bar{u})\\|}_2 {\\|(v - \\bar{v})\\|}_2}
 
     where :math:`\\bar{u}` is the mean of the elements of `u`
     and :math:`x \\cdot y` is the dot product of :math:`x` and :math:`y`.
@@ -640,7 +642,7 @@ def cosine(u, v, w=None):
     .. math::
 
         1 - \\frac{u \\cdot v}
-                  {||u||_2 ||v||_2}.
+                  {\\|u\\|_2 \\|v\\|_2}.
 
     where :math:`u \\cdot v` is the dot product of :math:`u` and
     :math:`v`.
@@ -2031,7 +2033,8 @@ def pdist(X, metric='euclidean', *, out=None, **kwargs):
     2. ``Y = pdist(X, 'minkowski', p=2.)``
 
        Computes the distances using the Minkowski distance
-       :math:`||u-v||_p` (p-norm) where :math:`p \\geq 1`.
+       :math:`\\|u-v\\|_p` (:math:`p`-norm) where :math:`p > 0` (note
+       that this is only a quasi-metric if :math:`0 < p < 1`).
 
     3. ``Y = pdist(X, 'cityblock')``
 
@@ -2054,7 +2057,7 @@ def pdist(X, metric='euclidean', *, out=None, **kwargs):
 
     5. ``Y = pdist(X, 'sqeuclidean')``
 
-       Computes the squared Euclidean distance :math:`||u-v||_2^2` between
+       Computes the squared Euclidean distance :math:`\\|u-v\\|_2^2` between
        the vectors.
 
     6. ``Y = pdist(X, 'cosine')``
@@ -2064,9 +2067,9 @@ def pdist(X, metric='euclidean', *, out=None, **kwargs):
        .. math::
 
           1 - \\frac{u \\cdot v}
-                   {{||u||}_2 {||v||}_2}
+                   {{\\|u\\|}_2 {\\|v\\|}_2}
 
-       where :math:`||*||_2` is the 2-norm of its argument ``*``, and
+       where :math:`\\|*\\|_2` is the 2-norm of its argument ``*``, and
        :math:`u \\cdot v` is the dot product of ``u`` and ``v``.
 
     7. ``Y = pdist(X, 'correlation')``
@@ -2076,7 +2079,7 @@ def pdist(X, metric='euclidean', *, out=None, **kwargs):
        .. math::
 
           1 - \\frac{(u - \\bar{u}) \\cdot (v - \\bar{v})}
-                   {{||(u - \\bar{u})||}_2 {||(v - \\bar{v})||}_2}
+                   {{\\|(u - \\bar{u})\\|}_2 {\\|(v - \\bar{v})\\|}_2}
 
        where :math:`\\bar{v}` is the mean of the elements of vector v,
        and :math:`x \\cdot y` is the dot product of :math:`x` and :math:`y`.
@@ -2688,7 +2691,8 @@ def cdist(XA, XB, metric='euclidean', *, out=None, **kwargs):
     2. ``Y = cdist(XA, XB, 'minkowski', p=2.)``
 
        Computes the distances using the Minkowski distance
-       :math:`||u-v||_p` (:math:`p`-norm) where :math:`p \\geq 1`.
+       :math:`\\|u-v\\|_p` (:math:`p`-norm) where :math:`p > 0` (note
+       that this is only a quasi-metric if :math:`0 < p < 1`).
 
     3. ``Y = cdist(XA, XB, 'cityblock')``
 
@@ -2710,7 +2714,7 @@ def cdist(XA, XB, metric='euclidean', *, out=None, **kwargs):
 
     5. ``Y = cdist(XA, XB, 'sqeuclidean')``
 
-       Computes the squared Euclidean distance :math:`||u-v||_2^2` between
+       Computes the squared Euclidean distance :math:`\\|u-v\\|_2^2` between
        the vectors.
 
     6. ``Y = cdist(XA, XB, 'cosine')``
@@ -2720,9 +2724,9 @@ def cdist(XA, XB, metric='euclidean', *, out=None, **kwargs):
        .. math::
 
           1 - \\frac{u \\cdot v}
-                   {{||u||}_2 {||v||}_2}
+                   {{\\|u\\|}_2 {\\|v\\|}_2}
 
-       where :math:`||*||_2` is the 2-norm of its argument ``*``, and
+       where :math:`\\|*\\|_2` is the 2-norm of its argument ``*``, and
        :math:`u \\cdot v` is the dot product of :math:`u` and :math:`v`.
 
     7. ``Y = cdist(XA, XB, 'correlation')``
@@ -2732,7 +2736,7 @@ def cdist(XA, XB, metric='euclidean', *, out=None, **kwargs):
        .. math::
 
           1 - \\frac{(u - \\bar{u}) \\cdot (v - \\bar{v})}
-                   {{||(u - \\bar{u})||}_2 {||(v - \\bar{v})||}_2}
+                   {{\\|(u - \\bar{u})\\|}_2 {\\|(v - \\bar{v})\\|}_2}
 
        where :math:`\\bar{v}` is the mean of the elements of vector v,
        and :math:`x \\cdot y` is the dot product of :math:`x` and :math:`y`.


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
@@ -446,14 +446,15 @@ def test_cdist_euclidean_random_unicode(self):
         Y2 = wcdist_no_const(X1, X2, 'test_euclidean')
         _assert_within_tol(Y1, Y2, eps, verbose > 2)
 
-    @pytest.mark.parametrize("p", [1.0, 1.23, 2.0, 3.8, 4.6, np.inf])
+    @pytest.mark.parametrize("p", [0.1, 0.25, 1.0, 1.23,
+                                   2.0, 3.8, 4.6, np.inf])
     def test_cdist_minkowski_random(self, p):
-        eps = 1e-07
+        eps = 1e-13
         X1 = eo['cdist-X1']
         X2 = eo['cdist-X2']
         Y1 = wcdist_no_const(X1, X2, 'minkowski', p=p)
         Y2 = wcdist_no_const(X1, X2, 'test_minkowski', p=p)
-        _assert_within_tol(Y1, Y2, eps, verbose > 2)
+        _assert_within_tol(Y1, Y2, atol=0, rtol=eps, verbose_=verbose > 2)
 
     def test_cdist_cosine_random(self):
         eps = 1e-07
@@ -957,13 +958,13 @@ def test_pdist_correlation_iris_nonC(self):
         Y_test2 = wpdist(X, 'test_correlation')
         _assert_within_tol(Y_test2, Y_right, eps)
 
-    @pytest.mark.parametrize("p", [1.0, 2.0, 3.2, np.inf])
+    @pytest.mark.parametrize("p", [0.1, 0.25, 1.0, 2.0, 3.2, np.inf])
     def test_pdist_minkowski_random_p(self, p):
-        eps = 1e-05
+        eps = 1e-13
         X = eo['pdist-double-inp']
         Y1 = wpdist_no_const(X, 'minkowski', p=p)
         Y2 = wpdist_no_const(X, 'test_minkowski', p=p)
-        _assert_within_tol(Y1, Y2, eps)
+        _assert_within_tol(Y1, Y2, atol=0, rtol=eps)
 
     def test_pdist_minkowski_random(self):
         eps = 1e-05
@@ -1533,7 +1534,10 @@ def test_minkowski(self):
             assert_almost_equal(dist1, 3.0)
             dist1p5 = minkowski(x, y, p=1.5)
             assert_almost_equal(dist1p5, (1.0 + 2.0**1.5)**(2. / 3))
-            minkowski(x, y, p=2)
+            dist2 = minkowski(x, y, p=2)
+            assert_almost_equal(dist2, 5.0 ** 0.5)
+            dist0p25 = minkowski(x, y, p=0.25)
+            assert_almost_equal(dist0p25, (1.0 + 2.0 ** 0.25) ** 4)
 
         # Check that casting input to minimum scalar type doesn't affect result
         # (issue #10262). This could be extended to more test inputs with
@@ -1903,9 +1907,9 @@ def correct_n_by_n(self, n):
         return y
 
 
-def test_bad_p():
-    # Raise ValueError if p < 1.
-    p = 0.5
+@pytest.mark.parametrize("p", [-10.0, -0.5, 0.0])
+def test_bad_p(p):
+    # Raise ValueError if p <=0.
     assert_raises(ValueError, minkowski, [1, 2], [3, 4], p)
     assert_raises(ValueError, minkowski, [1, 2], [3, 4], p, [1, 1])