Pairwise Distances without Loops

Intuition

k-NN, k-means and retrieval all need the distance from every query to every reference point. A double Python loop over 1000 × 1000 points is a million slow iterations; one matrix expression does it in milliseconds.

Precise explanation

For rows aia_i of AA (m×dm\times d) and bjb_j of BB (n×dn\times d):

Dij=∥ai−bj∥2=∥ai∥2+∥bj∥2−2 ai⋅bj.D_{ij} = \lVert a_i - b_j\rVert^2 = \lVert a_i\rVert^2 + \lVert b_j\rVert^2 - 2\,a_i\cdot b_j.

In NumPy:

aa = (A**2).sum(axis=1)[:, None]    # (m, 1)
bb = (B**2).sum(axis=1)[None, :]    # (1, n)
D = aa + bb - 2 * A @ B.T           # (m, n)

Rounding can make tiny entries slightly negative (e.g. −1e−15); clip with np.maximum(D, 0). An alternative, ((A[:, None, :] - B[None, :, :])**2).sum(-1), is simpler but builds an (m, n, d) temporary that uses much more memory.

Worked example

A = [[0, 0], [1, 1]], B = [[1, 0], [3, 4]]: D = [[1, 25], [1, 13]]; nearest B-row for each A-row: [0, 0].

Common mistakes

  • Forgetting the transpose in A @ B.T.
  • Taking sqrt of slightly negative values → NaN.
  • Mixing up which axis to argmin over (axis=1 → nearest B for each A).

Why this matters

This exact function is the engine of k-NN (ML-13), k-means (ML-26), embedding retrieval (NL-11) and non-max suppression ideas in CV.

Library implementation

The expansion ∥a−b∥2=∥a∥2+∥b∥2−2a⋅b\lVert a-b\rVert^2 = \lVert a\rVert^2 + \lVert b\rVert^2 - 2a\cdot b is what fast libraries use. SciPy exposes it as scipy.spatial.distance.cdist(A, B, "sqeuclidean") and scikit-learn as sklearn.metrics.pairwise.euclidean_distances(A, B, squared=True). The tests compare with a plain double loop and, when installed, with SciPy.

References