greedy-matching-evaluation · git:20260424.d50cf5a · 2026-04-24 · sha256 b5925a171c01541d

greedy-matching-evaluation git:20260424.d50cf5aA

Immutable. This exact content never changes and is served at /api/v1/blob/b5925a171c01541d.

---
name: greedy-matching-evaluation
description: Greedy matching algorithm to pair predicted cluster centroids with ground truth points for F1 and distance metrics.
---

# Greedy Matching for Clustering Evaluation

To evaluate clustering performance against ground truth (expert) points, we need to match predicted centroids to expert points.

## Algorithm
1. Compute all pairwise standard Euclidean distances between predicted centroids and expert points.
2. Filter pairs with distance > `max_dist` (e.g., 100 pixels).
3. Sort remaining pairs by distance (closest first).
4. Iteratively pick the closest pair, ensuring each centroid and each expert point is matched at most once.

## F1 Score Calculation
- **True Positives (TP)**: Number of matched pairs.
- **False Positives (FP)**: Number of unmatched predicted centroids.
- **False Negatives (FN)**: Number of unmatched expert points.
- `Precision = TP / (TP + FP)`
- `Recall = TP / (TP + FN)`
- `F1 = 2 * (Precision * Recall) / (Precision + Recall)` (Handle division by zero)

## Python Implementation
```python
import numpy as np
from scipy.spatial.distance import cdist

def evaluate_clustering(centroids, experts, max_dist=100):
    if len(centroids) == 0:
        return 0.0, np.nan
    if len(experts) == 0:
        return 0.0, np.nan

    distances = cdist(centroids, experts, metric='euclidean')
    
    # Greedy matching
    indices = np.where(distances <= max_dist)
    pairs = sorted(zip(indices[0], indices[1]), key=lambda x: distances[x[0], x[1]])
    
    matched_centroids = set()
    matched_experts = set()
    match_distances = []
    
    for c_idx, e_idx in pairs:
        if c_idx not in matched_centroids and e_idx not in matched_experts:
            matched_centroids.add(c_idx)
            matched_experts.add(e_idx)
            match_distances.append(distances[c_idx, e_idx])
            
    tp = len(matched_centroids)
    fp = len(centroids) - tp
    fn = len(experts) - tp
    
    precision = tp / (tp + fp) if (tp + fp) > 0 else 0
    recall = tp / (tp + fn) if (tp + fn) > 0 else 0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
    
    avg_delta = np.mean(match_distances) if len(match_distances) > 0 else np.nan
    
    return f1, avg_delta
```