# 6.1. Distance Metrics
Distance is a basic idea behind most classical machine learning. k-nearest neighbors ranks candidates by distance. DBSCAN grows a cluster by testing distance against a threshold. Silhouette scoring averages distances. k-means minimizes a squared distance.
RustyML defines all of this in one small module, [`crate::math::distance`](https://docs.rs/rustyml/latest/rustyml/math/distance/index.html). The module holds the only definition of "how far apart are two points". Every metric-aware estimator shares the same dispatcher. This page documents the public surface: what exists, what does not exist, and where the numerical shortcuts live.
The module has two layers. The bottom layer holds 3 free functions. These are allocation-free kernels, and each works on one pair of vectors at a time. The top layer is [`DistanceCalculationMetric`](https://docs.rs/rustyml/latest/rustyml/math/enum.DistanceCalculationMetric.html), a small enum. The enum names a metric and dispatches to the kernels.
Estimators store the enum, not a function pointer, because the enum is `Copy`, it supports `serde` serialization, and a `match` on it costs little. Use the kernels directly to build your own nearest-neighbor logic. Use the enum to get the same metric abstraction the library uses.
## 6.1.1. The 3 row kernels
All 3 kernels are re-exported from `rustyml::math`. Their names matter: there is no function named `euclidean_distance_row`. The Euclidean kernel is named `squared_euclidean_distance_row`. It returns the sum of squared differences and never takes a square root.
This is not an oversight. It is the whole design. Callers that need only ordering, such as finding the nearest point, testing a radius, or finding the closest centroid, never need the root. Taking a `sqrt` for every pair would waste work. To get the true Euclidean distance, take the root yourself, or use the dispatcher.
```rust,ignore
pub fn squared_euclidean_distance_row<S1, S2>(x1: &ArrayBase<S1, Ix1>, x2: &ArrayBase<S2, Ix1>) -> f64;
pub fn manhattan_distance_row<S1, S2>(x1: &ArrayBase<S1, Ix1>, x2: &ArrayBase<S2, Ix1>) -> f64;
pub fn minkowski_distance_row<S1, S2>(x1: &ArrayBase<S1, Ix1>, x2: &ArrayBase<S2, Ix1>, p: f64) -> f64;
// where S1: Data<Elem = f64>, S2: Data<Elem = f64>
```
The input types accept both views and slices. Each function takes a reference to a 1-D `ndarray` array. Each function is generic over storage, through `S: Data<Elem = f64>`. An owned `Array1<f64>`, a borrowed `ArrayView1<f64>`, and a row of a matrix, such as `&data.row(i)`, all work without change.
You do not copy a row into a `Vec<f64>` first. The element type is fixed at `f64`. There is no `f32` path. `S1` and `S2` are independent type parameters, so the two arguments can use different storage types. You can compare an owned query vector against a matrix row without a problem.
None of the 3 functions panics by itself on a length mismatch. `ndarray`'s `Zip` requires equal lengths and panics there if you break that rule. Treat equal dimensionality as a precondition that you must keep true.
```rust
use ndarray::array;
use rustyml::math::{
manhattan_distance_row, minkowski_distance_row, squared_euclidean_distance_row,
};
fn main() {
let a = array![1.0, 2.0, 3.0];
let b = array![4.0, 6.0, 8.0];
// Squared L2. No square root taken. Take the root yourself for the metric distance.
let sq = squared_euclidean_distance_row(&a, &b);
let euclidean = sq.sqrt();
let l1 = manhattan_distance_row(&a, &b);
let l3 = minkowski_distance_row(&a, &b, 3.0);
// Minkowski is a superset: p = 1 recovers Manhattan, p = 2 recovers Euclidean.
let mink1 = minkowski_distance_row(&a, &b, 1.0);
let mink2 = minkowski_distance_row(&a, &b, 2.0);
assert!((mink1 - l1).abs() < 1e-12);
assert!((mink2 - euclidean).abs() < 1e-12);
println!("sq={sq} l2={euclidean} l1={l1} l3={l3}");
}
```
`minkowski_distance_row` is the general form. It sums `|a_i - b_i|^p` over every coordinate, then raises the total to the power `1/p`. It is the only one of the 3 functions that can panic. It panics when `p` is less than 1.0, or when `p` is `NaN`. The panic message ends with the value you passed:
```text
invalid parameter `p`: Minkowski order must be at least 1.0, got 0.5
```
Orders below 1.0 are rejected because they do not form a metric. The triangle inequality fails for those orders. The kd-tree pruning described in 6.1.5 depends on the triangle inequality. An index that relies on it would return a wrong answer for those orders, not just an odd one.
The raw kernel does not reject `p = f64::INFINITY`. Infinity is not less than 1.0, and it is not `NaN`, so the guard passes. The function then computes a degenerate `powf(inf)` expression, not the Chebyshev (L-infinity) limit. Do not pass an infinite order and expect that limit. Section 6.1.4 shows that the estimator builders reject this case. The raw kernel does not.
## 6.1.2. The `DistanceCalculationMetric` dispatcher
`DistanceCalculationMetric` is the configurable-metric layer. It has 3 variants, and `Euclidean` is the `Default`:
| Variant | Meaning | Dispatches to |
|---|---|---|
| `Euclidean` | L2 norm (default) | `squared_euclidean_distance_row(...).sqrt()` |
| `Manhattan` | L1 norm | `manhattan_distance_row(...)` |
| `Minkowski(f64)` | general p-norm, `p` carried inline | `minkowski_distance_row(..., p)` |
The variant is the whole configuration. `Minkowski` carries its `p` value as its own payload. A metric is therefore a self-contained value, with no separate parameter to keep in sync elsewhere.
The enum derives `Clone`, `Copy`, `PartialEq`, and `Default`. Under the `machine_learning` or `utils` feature, it also derives `Serialize` and `Deserialize`. This makes the enum easy to store in a struct field. It also means the enum survives model persistence (see [7.2. Model Persistence in Depth](../Chapter-07/7.2._Model_Persistence_in_Depth.md)).
Two methods on `DistanceCalculationMetric` are public. The first is `distance(&self, a: ArrayView1<f64>, b: ArrayView1<f64>) -> f64`. It is the single source of truth for metric dispatch. Every metric-aware estimator calls it, instead of writing its own `match` over the variants. It takes `ArrayView1<f64>` by value, not by reference. `ArrayView1` is `Copy`, so you can pass `v.view()` or a matrix row directly, and reuse the same view across many calls without cloning.
The second public method is `within(&self, a, b, threshold) -> bool`. It answers whether `distance(a, b) <= threshold`, without computing the actual distance first. It compares instead in the metric's root-free space, for example `sq_dist <= threshold * threshold` for `Euclidean`. This mapping is monotonic on non-negative values, so the result always matches a plain comparison, but it skips the `sqrt`. Prefer `within` over `distance(...) <= r` for a radius query, for this reason.
```rust
use ndarray::array;
use rustyml::math::DistanceCalculationMetric;
fn main() {
let a = array![0.0, 0.0];
let b = array![3.0, 4.0];
let euclidean = DistanceCalculationMetric::Euclidean;
let manhattan = DistanceCalculationMetric::Manhattan;
let minkowski = DistanceCalculationMetric::Minkowski(3.0);
// ArrayView1<f64> is Copy, so the same view is reused across calls.
assert_eq!(euclidean.distance(a.view(), b.view()), 5.0); // sqrt(9 + 16)
assert_eq!(manhattan.distance(a.view(), b.view()), 7.0); // 3 + 4
// `within` compares in root-free space: 5^2 <= 5^2 is true, 5^2 <= 4.9^2 is false.
assert!(euclidean.within(a.view(), b.view(), 5.0));
assert!(!euclidean.within(a.view(), b.view(), 4.9));
let _ = minkowski.distance(a.view(), b.view());
}
```
You can import the dispatcher from 2 equivalent places. Its home is `rustyml::math::DistanceCalculationMetric`. A re-export for ML users is `rustyml::machine_learning::DistanceCalculationMetric`. Both names refer to the same type. Use whichever path matches the module you already import from.
## 6.1.3. Metric properties and the Minkowski order
All 3 metrics share several properties. Each is non-negative and symmetric: `d(a, b) == d(b, a)`. Each is zero only when the two vectors are identical. Each satisfies the triangle inequality, for the orders the library allows. These properties make the kd-tree pruning in 6.1.5 correct. The estimators depend on them.
The Minkowski order `p` moves between the named metrics. It controls how much a single large coordinate gap dominates the result. At `p = 1`, you get Manhattan distance. Each axis then contributes its raw absolute difference. A diagonal move costs the sum of the 2 legs.
At `p = 2`, you get Euclidean distance, the ordinary straight-line distance. As `p` grows past 2, the largest single-axis difference dominates the sum more and more. Each difference is raised to the power `p`, so a larger `p` favors the biggest gap. The metric then behaves more like the largest coordinate difference alone. That behavior is the Chebyshev limit, also called the L-infinity limit.
RustyML does not offer that limit as a usable metric. There is no `Chebyshev` variant. `Minkowski(f64::INFINITY)` is not a valid configuration for the estimators (see 6.1.4). To get behavior close to the largest-coordinate-difference limit, pick a large finite `p` instead. Treat the result as an approximation, not the true limit.
Fractional orders between 1 and 2, such as `Minkowski(1.5)`, are valid metrics. They sit between city-block and straight-line geometry. Use them to soften Euclidean's sensitivity to outliers, without moving all the way to Manhattan distance.
## 6.1.4. Which estimators accept which metrics
2 estimators let you choose a metric. Both use the same pattern: a `with_metric` builder that returns `Result`. It returns `Result` because it validates the Minkowski order at the moment you set it.
| Estimator | How to set the metric | Metrics honored |
|---|---|---|
| [`KNN`](../Chapter-02/2.3._K_Nearest_Neighbors.md) | `.with_metric(...)?` builder | Euclidean, Manhattan, Minkowski(p >= 1, finite) |
| [`DBSCAN`](../Chapter-02/2.8._DBSCAN.md) | `.with_metric(...)?` builder | Euclidean, Manhattan, Minkowski(p >= 1, finite) |
| [`silhouette_score`](../Chapter-05/5.3._Clustering_Metrics.md) | `metric` function argument | Euclidean, Manhattan, Minkowski(p >= 1) |
`with_metric` is stricter than the raw kernel. It rejects `Minkowski(p)` when `p < 1.0`, or when `p` is not finite. In that case, it returns `Error::InvalidParameter` (see [1.6. Error Handling](../Chapter-01/1.6._Error_Handling.md)). It does not defer the failure to a panic at fit time. This is why `with_metric` returns `Result`: the default constructors never fail on the metric, but overriding it can fail. The failure then appears immediately, at the builder call.
Both estimators default to `Euclidean`. You only call `with_metric` when you want a different metric.
```rust
use ndarray::array;
use rustyml::machine_learning::neighbors::{KNN, WeightingStrategy};
use rustyml::math::DistanceCalculationMetric;
fn main() {
let x_train = array![[0.0, 0.0], [10.0, 0.0], [0.0, 10.0]];
let y_train = array![0_i32, 1, 1];
let mut knn = KNN::<i32>::new(1)
.unwrap()
.with_weighting_strategy(WeightingStrategy::Uniform)
.with_metric(DistanceCalculationMetric::Manhattan)
.unwrap();
knn.fit(&x_train, &y_train).unwrap();
let x_test = array![[0.5, 0.0], [9.5, 0.0]];
let preds = knn.predict(&x_test).unwrap();
println!("{preds:?}");
// An order below 1 is not a metric. The builder rejects it before any work happens.
let bad = KNN::<i32>::new(1)
.unwrap()
.with_metric(DistanceCalculationMetric::Minkowski(0.5));
assert!(bad.is_err());
}
```
[`KMeans`](../Chapter-02/2.7._KMeans_Clustering.md) and [`MeanShift`](../Chapter-02/2.9._Mean_Shift.md) do not take a metric parameter. Both call `squared_euclidean_distance_row` directly, so both are Euclidean-only by construction. k-means minimizes the within-cluster squared L2 distance, by definition. Swapping the metric would break the centroid-update math, so there is no option to change it. DBSCAN is the estimator to use for non-Euclidean clustering.
Passing a metric to `silhouette_score` lets you evaluate a clustering under the same geometry you used to build it. For example, score a DBSCAN result that used Manhattan distance with `DistanceCalculationMetric::Manhattan`. This keeps the evaluation consistent with the clustering.
```rust
use ndarray::array;
use rustyml::math::DistanceCalculationMetric;
use rustyml::metrics::silhouette_score;
fn main() {
// Two well-separated blobs.
let x = array![
[0.0, 0.0], [0.2, 0.1], [0.1, -0.2],
[10.0, 10.0], [10.1, 9.8], [9.9, 10.2],
];
let labels = array![0_isize, 0, 0, 1, 1, 1];
let s_euclidean = silhouette_score(&x, &labels, DistanceCalculationMetric::Euclidean);
let s_manhattan = silhouette_score(&x, &labels, DistanceCalculationMetric::Manhattan);
println!("euclidean={s_euclidean} manhattan={s_manhattan}");
}
```
## 6.1.5. Squared distances and the comparable-space optimization
`squared_euclidean_distance_row` exposes the squared-distance shortcut as a public function. The library also uses the same shortcut internally, through a private idea called comparable space. Every metric has a monotonic, root-free form: square for Euclidean, the power `p` for Minkowski, and the identity for Manhattan. This transform is monotonic on non-negative inputs.
Some decisions depend only on ordering. Examples include which point is nearer, whether a point falls inside a radius, and which point is the k-th closest. Any such decision can run in comparable space, and it never pays the cost of a root.
The internal kd-tree does exactly this. KNN and DBSCAN use it automatically, in low dimensions. It stores each candidate distance in comparable space. It prunes branches using a per-axis lower bound, in that same space. It converts a comparable-space value back to a true distance only for the few results it actually returns. `within` (see 6.1.2) is the public tip of this idea.
The practical lesson applies to your own code too. When you rank points or set a threshold, rather than report a distance to a person, stay in squared space. Comparing `squared_euclidean_distance_row` values is correct for deciding which point is closer. It is also strictly cheaper than comparing the square roots. Call `.sqrt()` only at the point where a real distance leaves your loop.
## 6.1.6. Worked example: a pairwise distance matrix
The kernels are all you need to build a full pairwise-distance matrix, for your own nearest-neighbor logic. Every metric here guarantees 2 structural facts: the matrix is symmetric, and its diagonal is zero. Compute each unordered pair once, then mirror the value across the diagonal, and leave the diagonal at zero. This halves the number of distance evaluations. Inside the hot double loop, work in squared space, and take the root once per entry. Skip the root entirely if you only need ordering downstream.
```rust
use ndarray::{Array2, array};
use rustyml::math::squared_euclidean_distance_row;
fn main() {
let data = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let n = data.nrows();
// Symmetric with a zero diagonal: fill only the upper triangle, then mirror.
let mut dist = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in (i + 1)..n {
// Rows pass by reference with no copy. The sqrt runs once per entry.
let d = squared_euclidean_distance_row(&data.row(i), &data.row(j)).sqrt();
dist[[i, j]] = d;
dist[[j, i]] = d;
}
}
// For one query, skip the matrix and scan instead:
let query = data.row(0);
let nearest = (1..n)
.min_by(|&a, &b| {
// Compare in squared space. The root is not needed to pick the minimum.
let da = squared_euclidean_distance_row(&query, &data.row(a));
let db = squared_euclidean_distance_row(&query, &data.row(b));
da.total_cmp(&db)
})
.unwrap();
println!("matrix=\n{dist:?}\nnearest to row 0: {nearest}");
}
```
The `min_by` half of the example is the real point. A full `O(n^2)` matrix is the wrong tool when you query only a few points. For a single nearest-neighbor lookup, scan once in `O(n)` and never touch a root. Build the full matrix only when a downstream algorithm consumes all of it, such as hierarchical clustering or an MDS embedding. Do not build it just to find one neighbor.
## 6.1.7. Performance, SIMD, and parallelism
Each kernel runs in a single pass, with no intermediate allocation. `squared_euclidean_distance_row` and `manhattan_distance_row` each fold one `ndarray::Zip` over the two inputs. `minkowski_distance_row` does the same, but adds one `powf` call per element, plus one final `powf(1.0 / p)` call. All 3 functions carry the `#[inline]` attribute.
Cost per call is linear in the dimensionality `d`. Euclidean and Manhattan cost one subtraction, plus one multiply or one absolute value, per element. Both are cheap. Minkowski costs one transcendental `powf` call per element, which makes it noticeably slower. Prefer `Euclidean` or `Manhattan` when either fits your need. Avoid `Minkowski(2.0)` or `Minkowski(1.0)`, which compute the same numbers through the slower path.
These kernels contain no hand-written SIMD code, and there is no `f32` path. The `Zip` loops are tight and branch-free, except in Minkowski. An optimizing compiler can autovectorize code of this shape. Nothing in the source forces vectorization, though, so do not assume a specific instruction set.
A single distance call is entirely serial, with no rayon call inside any kernel. This is the right choice: one row is not enough work to justify the cost of thread dispatch. Parallelism instead lives one level up, at the caller. `silhouette_score` splits its pairwise scan across the rayon pool, above a tunable element threshold. KNN offers a `predict_parallel` method, and DBSCAN parallelizes its neighborhood queries.
If your own pairwise-matrix loop is the bottleneck, parallelize its outer loop over rows with rayon yourself. The kernels are pure functions and are `Send`-safe, so this composes cleanly. For the broader picture of when parallelism pays off, and how to tune the gates, see [7.3. Performance Tuning and Parallelism](../Chapter-07/7.3._Performance_Tuning_and_Parallelism.md). For the sibling numeric primitives this module ships alongside, see [6.2. Matrix Multiplication](./6.2._Matrix_Multiplication.md) and [6.3. Parallel Reductions](./6.3._Parallel_Reductions.md).