| name | run2_custom_distance_metrics |
| description | Implement weighted Euclidean distance metric for DBSCAN with shape parameter |
Custom Distance Metrics for Shape-Weighted Clustering
Mathematical Foundation
Distance Formula
The custom weighted Euclidean distance is defined as:
d(a, b) = sqrt((w * Δx)² + ((2 - w) * Δy)²)
Where:
w = shape_weight parameter (controls aspect ratio)
Δx = x₁ - x₂ (horizontal difference)
Δy = y₁ - y₂ (vertical difference)
Shape Weight Interpretation
- w = 1.0: Standard Euclidean distance (equal weighting)
- w > 1.0: Attenuates y-distances (horizontal features more prominent)
- w < 1.0: Attenuates x-distances (vertical features more prominent)
- w = 1.9: Maximum y-attenuation (strongly favors horizontal clustering)
- w = 0.9: Maximum x-attenuation (strongly favors vertical clustering)
Validation
Ensure w ∈ [0.9, 1.9] to maintain meaningful distance properties.
Implementation: Precomputed Distance Matrix
For DBSCAN with precomputed distances, compute the full pairwise matrix once per image:
import numpy as np
from sklearn.cluster import DBSCAN
def compute_custom_distance_matrix(points, shape_weight):
"""
Compute pairwise custom distance matrix efficiently.
Parameters:
- points: (n, 2) array of [x, y] coordinates
- shape_weight: float in [0.9, 1.9]
Returns:
- distances: (n, n) symmetric distance matrix
"""
n = len(points)
distances = np.zeros((n, n))
for i in range(n):
dx = points[i, 0] - points[:, 0]
dy = points[i, 1] - points[:, 1]
distances[i, :] = np.sqrt(
(shape_weight * dx)**2 + ((2 - shape_weight) * dy)**2
)
return distances
citsci_points = np.array([[100, 50], [110, 55], [200, 100]])
shape_weight = 1.2
distance_matrix = compute_custom_distance_matrix(citsci_points, shape_weight)
clusterer = DBSCAN(eps=8, min_samples=5, metric='precomputed')
labels = clusterer.fit_predict(distance_matrix)
Important Design Decisions
1. Precomputed vs. Callable Metric
- Precomputed: Compute once, reuse with multiple epsilon values ✓ Use this
- Callable: Recompute for each distance query (slower) ✗
2. Distance Matrix Properties
- Symmetric: d(i, j) = d(j, i)
- Non-negative: d(i, j) ≥ 0
- Identity: d(i, i) = 0
- Triangle inequality: may not hold (acceptable for DBSCAN)
3. Per-Image Computation
Always compute distance matrices per image, not globally:
for image_id in unique_images:
image_points = citsci_df[citsci_df['file_rad'] == image_id][['x', 'y']].values
dist_matrix = compute_custom_distance_matrix(image_points, shape_weight)
clusterer = DBSCAN(eps=epsilon, min_samples=min_samples, metric='precomputed')
labels = clusterer.fit_predict(dist_matrix)
Edge Cases and Validation
1. Empty Image Points
if len(image_points) == 0:
return 0.0, np.nan
2. Single Point
if len(image_points) == 1:
3. Shape Weight Rounding
shape_weights = np.round(np.arange(0.9, 2.0, 0.1), 1)
Comparison: Custom vs. Standard Distance
d_std = np.sqrt(dx**2 + dy**2)
d_custom = np.sqrt((1.2*dx)**2 + (0.8*dy)**2)
Testing Sanity Checks
d_test = compute_custom_distance_matrix(
np.array([[0, 0], [3, 4]]),
shape_weight=1.0
)
assert d_test[0, 1] == 5.0, "w=1.0 should give standard distance 5"
d = compute_custom_distance_matrix(citsci_points, 1.2)
assert np.allclose(d, d.T), "Distance matrix must be symmetric"
assert np.allclose(np.diag(d), 0), "Diagonal should be all zeros"
Performance Optimization
For large images (>1000 points), precomputation is still fast:
- n=1000: ~1 million distance values
- NumPy vectorized computation: typically <100ms
- Acceptable for grid search with 847 hyperparameter combinations