Chapter 106 · Solutions
Distance Metrics · Solutions
Five challenges, each verified in code.
Chapter 106 · Practice Challenges, Solved¶
Five exercises on distance metrics, worked with scipy.spatial.distance and the Chapter 106 product catalog.
In [1]:
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import seaborn as sns # seaborn = high-level statistical plots (heatmaps, pairplots, count/bar plots)
from matplotlib.colors import ListedColormap
EM="#4338ca"; DEEP="#3730a3"; LIGHT="#c7d2fe"; INK="#1a2138"; GRID="#e6e9f2"; RED="#ef4444"; AMBER="#d97706"; GREEN="#059669"; BLUE="#2563eb"; PUR="#9333ea"; GREY="#94a3b8"; SLATE="#475569"; ORG="#4338ca"; CYAN="#0891b2"
plt.rcParams.update({"figure.facecolor":"white","axes.facecolor":"white","figure.dpi":110,"font.size":11,
"axes.edgecolor":GRID,"axes.grid":True,"grid.color":GRID,"axes.axisbelow":True,"axes.spines.top":False,
"axes.spines.right":False,"axes.titlesize":12,"axes.titleweight":"bold","legend.frameon":False})
sns.set_style("whitegrid")
BASE="https://raw.githubusercontent.com/johnfisher-ai/Statistics-Data-Science-AI-Visual-Book/main/data/"
In [2]:
import warnings; warnings.filterwarnings('ignore')
from scipy.spatial.distance import euclidean, cityblock, minkowski, cosine, jaccard, hamming, cdist
from sklearn.preprocessing import StandardScaler
try: df = pd.read_excel('../../data/distance-metrics--products.xlsx', sheet_name='Data')
except FileNotFoundError: df = pd.read_excel(BASE + 'distance-metrics--products.xlsx', sheet_name='Data')
print('loaded', df.shape)
loaded (120, 11)
CHALLENGE 1
Euclidean vs Manhattan
For A=(0,0) and B=(3,4), compute both distances and explain the difference.
In [3]:
A, B = np.array([0,0]), np.array([3,4])
print(f'Euclidean = {euclidean(A,B):.1f} (straight line) Manhattan = {cityblock(A,B):.1f} (3 across + 4 up)')
Euclidean = 5.0 (straight line) Manhattan = 7.0 (3 across + 4 up)
Solution. Euclidean is the hypotenuse (5); Manhattan walks the grid (7). Manhattan is always at least Euclidean.
CHALLENGE 2
The Minkowski family
Show that Minkowski with p=1 equals Manhattan and p=2 equals Euclidean.
In [4]:
for p in [1,2,3]: print(f'Minkowski p={p}: {minkowski(A,B,p):.3f}')
print('p=1 -> Manhattan, p=2 -> Euclidean; larger p weights the biggest coordinate gap more')
Minkowski p=1: 7.000 Minkowski p=2: 5.000 Minkowski p=3: 4.498 p=1 -> Manhattan, p=2 -> Euclidean; larger p weights the biggest coordinate gap more
Solution. Minkowski generalizes both; the parameter p tunes how much the largest difference dominates.
CHALLENGE 3
Standardize before distance
Show the nearest neighbor to P1 changes after scaling when features have different ranges.
In [5]:
two = pd.DataFrame({'price':[50,60,250],'rating':[3.0,4.9,3.1]}, index=['P1','P2','P3'])
raw = cdist(two.iloc[[0]], two, 'euclidean')[0]; std = cdist(StandardScaler().fit_transform(two)[[0]], StandardScaler().fit_transform(two), 'euclidean')[0]
print('raw nearest to P1 :', two.index[np.argsort(raw)[1]])
print('scaled nearest to P1:', two.index[np.argsort(std)[1]])
raw nearest to P1 : P2 scaled nearest to P1: P3
Solution. Unscaled, price (in the hundreds) dominates; after standardizing, rating counts too and the neighbor can change.
CHALLENGE 4
Cosine ignores magnitude
Show two vectors pointing the same way have cosine similarity 1 but a large Euclidean distance.
In [6]:
u, v = np.array([2,1]), np.array([8,4])
print(f'cosine similarity = {1-cosine(u,v):.3f} Euclidean distance = {euclidean(u,v):.2f}')
cosine similarity = 1.000 Euclidean distance = 6.71
Solution. v is u scaled 4x, so the angle is zero (cosine 1) even though the straight-line distance is large.
CHALLENGE 5
Real data: nearest by two metrics
For the first product, find the nearest neighbor by scaled-Euclidean and by Jaccard on the flags.
In [7]:
num=['price','weight_g','battery_hrs','rating']; flags=['noise_cancel','waterproof','has_mic','fast_charge','foldable']
Xs = StandardScaler().fit_transform(df[num])
de = cdist(Xs[[0]], Xs, 'euclidean')[0]; dj = cdist(df[flags].values[[0]].astype(bool), df[flags].values.astype(bool), 'jaccard')[0]
ne = np.argsort(de); nj = np.argsort(dj)
print('nearest by Euclidean:', df.loc[ne[ne!=0][0],'product_id'])
print('nearest by Jaccard :', df.loc[nj[nj!=0][0],'product_id'])
nearest by Euclidean: H383 nearest by Jaccard : H336
Solution. The specs-based and flags-based neighbors differ, the metric encodes what you mean by 'similar'.
Statistics, Data Science and AI: A Visual Handbook · © 2026 John Fisher