III-18 · Distance Metrics and Curse of Dimensionality
Nearness depends on a measuring rule
Change the metric and its reference scales, then ask what adding dimensions actually does to a particular distribution.
A metric chooses which differences matter
Drag the hollow query diamond or focus it and use arrow keys: 0.1 per key, Shift for 0.5. Pointer movement snaps to a 0.1 lattice and stops at ±3. Numeric entries retain finer precision. Only the handle captures touch gestures; the page can scroll elsewhere.
Dashed contour: scaled distance 1. Dotted segment: q to z. Coordinates have different units; dividing by their reference scales makes the displayed distance dimensionless.
LCG seed 42; 24 rows, 32 draws per row, prefixes retained; cube model: pseudo-random uniform-coordinate draws in [−1,1]. Two-point reference scales affect the upper diagram only; this simulation uses unit scales.
- Scaled pair distance
- ≈ 2.2361
- Mean origin distance
- ≈ 0.6868
- (max − min) / mean
- ≈ 1.5516
- Population SD / mean
- ≈ 0.4701
Compare dimensions with the same seed, metric and distribution. An observed reduction in relative spread depends on those choices. Adding zero coordinates to the embedded line changes no raw distance.
- Pair feature unitsReference scales carry those units; scaled differences and distance are dimensionless.
- metres, seconds
- Simulation X
- 24 × 2
- Origin query
- 2 × 1
- Distances
- 24 × 1
- Seed
- 42
- NormalizationLp norm
- raw
| ID | Feature row | Raw distance | Displayed distance |
|---|---|---|---|
| d1 | -0.49530965043231845, -0.8237499091774225 | ≈ 0.9612 | ≈ 0.9612 |
| d2 | 0.6599205289967358, 0.6906642373651266 | ≈ 0.9553 | ≈ 0.9553 |
| d3 | -0.7940545375458896, 0.8430273737758398 | ≈ 1.1581 | ≈ 1.1581 |
| d4 | 0.9574624155648053, 0.5994039531797171 | ≈ 1.1296 | ≈ 1.1296 |
| d5 | -0.48958134604617953, 0.08210842870175838 | ≈ 0.4964 | ≈ 0.4964 |
| d6 | 0.24201144324615598, 0.5697052534669638 | ≈ 0.619 | ≈ 0.619 |
| d7 | 0.3106880490668118, 0.4970088806003332 | ≈ 0.5861 | ≈ 0.5861 |
| d8 | 0.6561457370407879, -0.5449162367731333 | ≈ 0.8529 | ≈ 0.8529 |
| d9 | -0.000668227206915617, 0.1912443544715643 | ≈ 0.1912 | ≈ 0.1912 |
| d10 | 0.8424434219487011, 0.6090551074594259 | ≈ 1.0395 | ≈ 1.0395 |
| d11 | -0.5310720498673618, -0.23166952468454838 | ≈ 0.5794 | ≈ 0.5794 |
| d12 | -0.0565173770301044, -0.11486508883535862 | ≈ 0.128 | ≈ 0.128 |
| d13 | 0.1120547060854733, 0.3317828681319952 | ≈ 0.3502 | ≈ 0.3502 |
| d14 | -0.3981585348956287, -0.3631612006574869 | ≈ 0.5389 | ≈ 0.5389 |
| d15 | -0.17870983434841037, -0.514882842078805 | ≈ 0.545 | ≈ 0.545 |
| d16 | -0.03990192664787173, 0.717682396993041 | ≈ 0.7188 | ≈ 0.7188 |
| d17 | 0.9892124538309872, -0.6681510303169489 | ≈ 1.1937 | ≈ 1.1937 |
| d18 | -0.3391694272868335, 0.48118132911622524 | ≈ 0.5887 | ≈ 0.5887 |
| d19 | 0.508399695623666, 0.47549392841756344 | ≈ 0.6961 | ≈ 0.6961 |
| d20 | -0.15338291181251407, 0.7808512207120657 | ≈ 0.7958 | ≈ 0.7958 |
| d21 | -0.22856998397037387, 0.019567659124732018 | ≈ 0.2294 | ≈ 0.2294 |
| d22 | 0.16003574477508664, -0.0297923032194376 | ≈ 0.1628 | ≈ 0.1628 |
| d23 | 0.6708815400488675, -0.43241421319544315 | ≈ 0.7982 | ≈ 0.7982 |
| d24 | 0.7436646674759686, 0.9027663823217154 | ≈ 1.1696 | ≈ 1.1696 |
Frame 1 / 1 · edits begin at the displayed state.
A distance encodes a judgment
A difference of two metres and a difference of one second cannot be added as if they had the same physical unit. The upper sheet states one reference scale for each coordinate. Dividing each difference by its reference scale produces dimensionless quantities that can enter a norm.
At the original pair and , with reference scales one metre and one second, the scaled differences are . L1 gives ; L2 gives . The same points therefore have different numerical distances under different rules.
The dashed unit-distance contour changes shape when you choose a metric or change a scale. It is a geometric explanation of the rule, not a new source of mathematical state. The table and numeric distance use the same active rule as the contour.
Three norms, one explicit representation
For positive reference scales , the Minkowski family is
L1, or Manhattan distance, uses . L2, or Euclidean distance, uses . The Lp control supplies another finite power from 1 to 8. When L1 or L2 is selected, its fixed power applies; the stored Lp control is preserved for switching back.
Absolute values make each coordinate contribution nonnegative. The final root returns the norm from its powered sum. If , this expression can violate the triangle inequality, which is why that range is excluded here. As r becomes very large, the largest scaled component increasingly dominates; the infinity norm is a limiting connection, not an implemented control.
With unit scales, differences give and . For differences , .
For the sheet’s example, changing the first reference scale to two metres gives scaled differences and L2 distance . This says that a two-metre difference should count as one reference unit. It changes the rule.
A pure unit conversion is different. Writing two metres as 200 centimetres while converting its reference scale from one metre to 100 centimetres keeps . Converting the coordinate but leaving its numerical reference scale at 1 would assign a new meaning to nearness.
What the dimension experiment actually samples
The lower sheet fixes 24 row identities. The shared 32-bit LCG uses
It draws 32 coordinates per row, then maps each draw to . A selected dimension uses that row’s first d coordinates. Changing d keeps the same prefixes rather than replacing the observations with unrelated draws.
The cube model represents uniform-coordinate sampling in . The generator is deterministic pseudorandom arithmetic; a seed is a reproducibility specification, not evidence that a dataset came from a real population. The embedded line keeps only the first generated coordinate and sets the rest to zero.
The origin query is a column. The simulation batch is , with a distance column. These dimensionless simulation coordinates are separate from the metres/seconds example above. Changing its two reference scales affects the pair diagram, while the simulation retains unit reference scales.
Derive a distribution-specific tendency
For genuinely independent uniform coordinates on , let . The second and fourth moments are
Independence makes the variances add, giving
Consequently the coefficient of variation of squared Euclidean distance under this model is . This is a statement about that distribution and that quantity. It is not the coefficient of variation of Euclidean distance itself, and it does not describe every finite dataset.
The experiment reports ordinary distances with their mean, population standard deviation, relative spread and coefficient of variation . “Population” here means dividing by the 24 displayed rows; it is not an unbiased estimator of an unknown population variance.
Inspect a worked seeded result and its counterexample
For seed 42, L2, raw normalization and cube data, d=2 gives mean distance approximately and coefficient of variation approximately . At d=32, the same coordinate-prefix experiment gives mean approximately and coefficient of variation approximately . The observed relative spread changes from approximately to .
Those are committed results for this finite experiment. Another seed or distribution can produce different comparisons, and every intermediate dimension need not change monotonically.
Switch to the embedded line. At every dimension,
For seed 42 its raw mean is approximately and coefficient of variation approximately at both d=1 and d=32. Ambient dimension increased, but intrinsic variation did not. This is a concrete counterexample to a claim that adding dimensions alone forces distances to become nearly equal.
Select per-coordinate normalization. It displays
This divides every distance in the current sample by the same positive factor. Magnitudes change; neighbor ordering, relative spread and coefficient of variation do not. A new normalization should not be mistaken for a new sample distribution.
Execute and reproduce the mechanism
Run .venv/bin/python -B code/python/p03/distance_metrics.py. It emits before/after states and all dimension-replay frames, including actual coordinates and distances.
"""Feature units, scaled Lp distances and a bounded seeded dimension experiment."""
import copy
import json
import numpy as np
from common import distance, lcg, number, power_for
CANONICAL = {"query":{"x":0,"y":0},"point":{"x":2,"y":1},"metric":"L2","p":2,"scales":[1,1],
"seed":42,"dimension":2,"distribution":"cube","normalization":"raw"}
def summarize(values):
if not isinstance(values, (list,tuple)) or not values:
raise ValueError("A distance summary needs observations.")
array = np.asarray([number(v,"Summary distance",0) for v in values],dtype=np.float64).reshape(-1,1)
lo, hi = float(np.min(array)), float(np.max(array))
if hi == 0:
return {"min":lo,"max":hi,"mean":0.0,"sd":0.0,"relativeSpread":None,"coefficientOfVariation":None,
"meanUnderflow":False,"sdUnderflow":False,
"reason":"All distances are zero; relative spread and coefficient of variation are undefined."}
unit = array/hi
unit_mean = float(np.mean(unit))
unit_sd = float(np.sqrt(np.mean((unit-unit_mean)**2)))
mean, sd = hi*unit_mean, hi*unit_sd
mean_underflow, sd_underflow = mean == 0, unit_sd > 0 and sd == 0
return {"min":lo,"max":hi,"mean":mean,"sd":sd,
"relativeSpread":(1-lo/hi)/unit_mean,"coefficientOfVariation":unit_sd/unit_mean,
"meanUnderflow":mean_underflow,"sdUnderflow":sd_underflow,
"reason":"Positive mean or standard deviation underflowed to zero; relative metrics use normalized arithmetic." if mean_underflow or sd_underflow else None}
def derive(state):
power = power_for(state["metric"],state["p"])
seed = int(number(state["seed"],"Seed",0,2**32-1,True))
dimension = int(number(state["dimension"],"Dimension",1,32,True))
if state["distribution"] not in ("cube","line") or state["normalization"] not in ("raw","per-coordinate"):
raise ValueError("Choose a supported distribution and normalization.")
pair = distance([state["point"]["x"],state["point"]["y"]],[state["query"]["x"],state["query"]["y"]],state["metric"],state["p"],state["scales"])
draws = lcg(seed)
full = np.asarray([[2*next(draws)-1 for _ in range(32)] for _ in range(24)],dtype=np.float64)
X = full[:,:dimension].copy()
if state["distribution"] == "line":
X[:,1:] = 0
divisor = 1 if state["normalization"] == "raw" else dimension**(1/power)
rows = []
for j,column in enumerate(X):
raw = distance(column.tolist(),[0]*dimension,state["metric"],state["p"])
rows.append({"id":"d"+str(j+1),"features":column.tolist(),"raw":raw,"distance":raw/divisor})
return {"state":copy.deepcopy(state),"power":power,"pairDistance":pair,"rows":rows,
"summary":summarize([row["distance"] for row in rows]),
"shapes":{"X":[24,dimension],"query":[dimension,1],"distances":[24,1]},
"provenance":f"LCG seed {seed}; 24 rows, 32 draws per row, prefixes retained; "+
("cube model: pseudo-random uniform-coordinate draws in [−1,1]" if state["distribution"] == "cube" else "one uniform coordinate, remaining coordinates exactly zero")}
def trace(state):
states = [copy.deepcopy(state)] + [dict(copy.deepcopy(state),dimension=d) for d in (1,2,8,32)]
return [{"index":j,"state":value,"derived":derive(value)} for j,value in enumerate(states)]
if __name__ == "__main__":
print(json.dumps({"before":derive(CANONICAL),"after":derive(dict(CANONICAL,dimension=32)),"trace":trace(CANONICAL)},allow_nan=False))"""Part III boundary checks. Numbers are quantities, never bools or numeric strings."""
import math
import numpy as np
def number(value, name, lower=None, upper=None, integer=False):
try:
valid = isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(float(value))
except (OverflowError, TypeError):
valid = False
normalized = float(value) if valid else 0
if not valid or (lower is not None and normalized < lower) or (upper is not None and normalized > upper) or (integer and normalized != math.floor(normalized)):
raise ValueError(f"{name} must be a finite {'integer' if integer else 'number'} in its supported range.")
return int(normalized) if integer else normalized
def power_for(metric, p):
number(p, "Minkowski power", 1, 8)
if metric not in ("L1", "L2", "Lp"):
raise ValueError("Choose L1, L2 or Lp.")
return 1 if metric == "L1" else 2 if metric == "L2" else p
_UNIT_SCALES = object()
def distance(a, b, metric="L2", p=2, scales=_UNIT_SCALES):
"""Column-vector norm; rescale before powers. Scales carry the feature units."""
if not isinstance(a, (list, tuple)) or not isinstance(b, (list, tuple)) or not 1 <= len(a) <= 32 or len(a) != len(b):
raise ValueError("Distance needs matching vectors of 1 to 32 components.")
if scales is not _UNIT_SCALES and (not isinstance(scales, (list, tuple)) or len(scales) != len(a)):
raise ValueError("Distance needs a matching scale vector.")
q = power_for(metric, p)
differences = []
for j, (left, right) in enumerate(zip(a, b)):
left = number(left, "First component"); right = number(right, "Second component")
scale = 1 if scales is _UNIT_SCALES else number(scales[j], "Scale")
if scale <= 0:
raise ValueError("Scales must be positive.")
delta = number(left-right, "Coordinate difference")
differences.append(number(abs(delta)/scale, "Scaled difference"))
column = np.asarray(differences, dtype=np.float64).reshape(-1, 1)
largest = float(np.max(column))
if largest == 0:
return 0.0
result = largest * float(np.sum((column/largest)**q))**(1/q)
return float(number(result, "Distance arithmetic"))
def id_key(value):
return value.encode("utf-16-be", errors="surrogatepass")
def lcg(seed):
number(seed, "Seed", 0, 2**32-1, True)
state = int(seed)
while True:
state = (1664525*state+1013904223) % 2**32
yield state/2**32Independent goldens use a closed-form modular expression for each generator state instead of translating the stepwise LCG. Fraction coordinates and high-precision powered sums check the distances. Exact small-summary tests cover tiny values and subnormal representation limits.
Explore and connect the measurements
Compare L1, L2 and Lp with the same seed and distribution. Change the seed and check whether your explanation still names the distribution, norm, query and normalization. Compare cube data with the line before interpreting a dimension effect.
For k-NN, small relative distance differences can make neighborhoods sensitive to features or noise. This does not establish that all high-dimensional prediction is impossible. Useful structure, sparsity, representation, sample size and the target relationship matter.
Replay selects complete mathematical states; controls, features, shapes, diagrams and summaries all follow the selected frame. A manual edit starts there and ends replay. Reset restores the original pair and seeded experiment.
Numerical limits
The plot accepts pair coordinates in and reference scales from to . The simulation uses 24 rows, dimensions 1–32 and unsigned 32-bit seeds. Lp power is 1–8. Unsupported numeric entries are rejected without altering the valid state.
The norm rescales coordinate differences before powers. Finite inputs can still yield an overflowing subtraction, scale division or final norm; those cases have an explicit arithmetic error. Given coordinates retain full decimal text; displayed distances are approximate.
Summary calculations normalize by the largest actual distance before computing mean and deviation. This preserves, for example, the SD and CV 1 for . Relative metrics are undefined only when every distance is actually zero. If a positive absolute mean or SD rounds to zero by underflow, a separate flag/reason distinguishes that representation limit from zero observations; relative metrics remain calculated at the normalized scale.
This is a bounded synthetic origin-distance experiment, not a universal concentration theorem, a benchmark of search algorithms or an estimate of a task’s prediction error.
Check your understanding
Checkpoints
You can state a distance with its representation, units and scales; distinguish changing a measuring rule from changing units; reproduce the seeded data; and describe a dimension effect with its assumptions. A claim about nearness is incomplete until the geometry and distribution are named.