Approach:Compute Euclidean distances between each test point and all train points, find k nearest neighbors, then predict by majority vote. If there's a tie in vote counts, break it by computing the average distance of the tied classes among those k neighbors and select the class with the smallest average distance. Use numpy for vectorized operations.python
import numpy as np
def knn_predict(X_train, y_train, X_test, k):
"""
Simple k-NN classifier (Euclidean distance).
Tie-breaking: if multiple classes have same vote count, choose the class
with the smallest average distance among its occurrences in the k neighbors.
Inputs:
- X_train: (n, d) numpy array of training features
- y_train: (n,) numpy array of training labels (can be numeric or strings)
- X_test: (m, d) numpy array of test features
- k: integer, number of neighbors (1 <= k <= n)
Returns:
- preds: (m,) numpy array of predicted labels (same dtype as y_train)
"""
X_train = np.asarray(X_train)
y_train = np.asarray(y_train)
X_test = np.asarray(X_test)
n = X_train.shape[0]
if k < 1 or k > n:
raise ValueError("k must be between 1 and number of training samples")
# Compute squared Euclidean distances: shape (m, n)
# Using (a-b)^2 = a^2 + b^2 - 2ab for efficiency
X_test_sq = np.sum(X_test**2, axis=1)[:, None] # (m,1)
X_train_sq = np.sum(X_train**2, axis=1)[None, :] # (1,n)
cross = X_test @ X_train.T # (m,n)
dists_sq = X_test_sq + X_train_sq - 2 * cross
# numerical stability
dists_sq = np.maximum(dists_sq, 0.0)
dists = np.sqrt(dists_sq)
m = X_test.shape[0]
preds = np.empty(m, dtype=y_train.dtype)
for i in range(m):
# indices of k nearest neighbors
nn_idx = np.argpartition(dists[i], k-1)[:k]
# sort those k by distance to have deterministic ordering
nn_idx = nn_idx[np.argsort(dists[i][nn_idx])]
nn_labels = y_train[nn_idx]
nn_dists = dists[i][nn_idx]
# Count votes
unique, counts = np.unique(nn_labels, return_counts=True)
max_count = counts.max()
# candidate labels with max votes
candidates = unique[counts == max_count]
if candidates.size == 1:
preds[i] = candidates[0]
else:
# tie-breaker: compute average distance among occurrences in the k neighbors
best_label = None
best_avg_dist = np.inf
for label in candidates:
mask = (nn_labels == label)
avg_dist = nn_dists[mask].mean()
# choose smaller avg distance; deterministic tie-breaker: smaller label if avg equal
if (avg_dist < best_avg_dist) or (avg_dist == best_avg_dist and label < best_label):
best_avg_dist = avg_dist
best_label = label
preds[i] = best_label
return preds
Key points:- Vectorized distance computation for speed.- argpartition finds k nearest efficiently; then sort those k for determinism.- Tie-breaking uses average neighbor distance per tied class; final deterministic tie-breaker uses label ordering.Time complexity: O(m * n * d) for distance computation (dominant), plus O(m * k log k) for per-sample neighbor processing. Space: O(m * n) for distance matrix; can be reduced by processing in batches.Edge cases: k out of range, identical points, non-numeric labels (works if comparable), very large n (batch distances to save memory). Alternative: KD-tree or BallTree for large datasets / high performance.