Repository: scipy/scipy

Issue:
BENCH: spatial.distance.* "non-xdist" benchmarks
As @lgeiger noted at https://github.com/scipy/scipy/pull/19589#issuecomment-1826400845, we may want to consider expanding the `spatial` `asv` benchmarks to include the "single-vector" distance funcs (non-"xdist"). I think this maybe wasn't a priority in the past because how heavily-used the `cdist`/`pdist` flavors are, by comparison.

Probably low priority, but useful for i.e., assessing PRs that affect the single-vector versions.


Discussion:
hello.
by non xdist single vector do you mean squareform only? or also test for the predicates which check validity and find num observations?
> by non xdist single vector do you mean squareform only? 

I mean distance calculations that are not `pdist` or `cdist`; if you go to the page in the docs for distance calculations (https://scipy.github.io/devdocs/reference/spatial.distance.html#module-scipy.spatial.distance) and scroll down you'll see two sections:

> Distance functions between two numeric vectors u and v. Computing distances over a large collection of vectors is inefficient for these functions. Use pdist for this purpose.

> Distance functions between two boolean vectors (representing sets) u and v. As in the case of numerical vectors, pdist is more efficient for computing the distances between all pairs.

We prioritized benchmarks for `pdist`/`cdist`, so the slower single-vector versions could use some attention, though it probably isn't high-priority since `cdist`/`pdist` are often used for the large performance-critical case (since they accept multiple records/points).

Resolving patch:
diff --git a/benchmarks/benchmarks/spatial.py b/benchmarks/benchmarks/spatial.py
--- a/benchmarks/benchmarks/spatial.py
+++ b/benchmarks/benchmarks/spatial.py
@@ -388,6 +388,38 @@ def time_pdist(self, num_points, metric):
         distance.pdist(self.points, self.metric, **self.kwargs)
 
 
+class SingleDist(Benchmark):
+    params = (['euclidean', 'minkowski', 'cityblock',
+               'seuclidean', 'sqeuclidean', 'cosine', 'correlation',
+               'hamming', 'jaccard', 'jensenshannon', 'chebyshev', 'canberra',
+               'braycurtis', 'mahalanobis', 'yule', 'dice', 'kulczynski1',
+               'rogerstanimoto', 'russellrao', 'sokalmichener', 'sokalsneath',
+               'minkowski-P3'])
+    param_names = ['metric']
+
+    def setup(self, metric):
+        rng = np.random.default_rng(123)
+        self.points = rng.random((2, 3))
+        self.metric = metric
+        if metric == 'minkowski-P3':
+            # p=2 is just the euclidean metric, try another p value as well
+            self.kwargs = {'p': 3.0}
+            self.metric = 'minkowski'
+        elif metric == 'mahalanobis':
+            self.kwargs = {'VI': [[1, 0.5, 0.5], [0.5, 1, 0.5], [0.5, 0.5, 1]]}
+        elif metric == 'seuclidean':
+            self.kwargs = {'V': [1, 0.1, 0.1]}
+        else:
+            self.kwargs = {}
+
+    def time_dist(self, metric):
+        """Time distance metrics individually (without batching with
+        cdist or pdist).
+        """
+        getattr(distance, self.metric)(self.points[0], self.points[1],
+                                       **self.kwargs)
+
+
 class XdistWeighted(Benchmark):
     params = (
         [10, 20, 100],
@@ -419,6 +451,32 @@ def time_pdist(self, num_points, metric):
         distance.pdist(self.points, self.metric, w=self.weights, **self.kwargs)
 
 
+class SingleDistWeighted(Benchmark):
+    params = (['euclidean', 'minkowski', 'cityblock', 'sqeuclidean', 'cosine',
+               'correlation', 'hamming', 'jaccard', 'chebyshev', 'canberra',
+               'braycurtis', 'yule', 'dice', 'kulczynski1', 'rogerstanimoto',
+               'russellrao', 'sokalmichener', 'sokalsneath', 'minkowski-P3'])
+    param_names = ['metric']
+
+    def setup(self, metric):
+        rng = np.random.default_rng(123)
+        self.points = rng.random((2, 3))
+        self.metric = metric
+        if metric == 'minkowski-P3':
+            # p=2 is just the euclidean metric, try another p value as well
+            self.kwargs = {'p': 3.0, 'w': np.ones(3)}
+            self.metric = 'minkowski'
+        else:
+            self.kwargs = {'w': np.ones(3)}
+
+    def time_dist_weighted(self, metric):
+        """Time weighted distance metrics individually (without batching
+        with cdist or pdist).
+        """
+        getattr(distance, self.metric)(self.points[0], self.points[1],
+                                       **self.kwargs)
+
+
 class ConvexHullBench(Benchmark):
     params = ([10, 100, 1000, 5000], [True, False])
     param_names = ['num_points', 'incremental']


Test patch: