🔍 KNN Core Philosophy
🎯 Lazy Learning & Instance-Based
K-Nearest Neighbors (KNN) is a non-parametric, lazy learning algorithm. It stores all training instances and makes predictions based on the k closest neighbors. No explicit training phase — all computation happens during inference.
Why "Lazy"?
No model is built during training — KNN simply memorizes the dataset. Prediction involves computing distances between the query point and all training points, then aggregating the labels of the k nearest neighbors. This makes it highly interpretable but computationally expensive at scale.
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=100, n_features=4, random_state=42)
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X, y) # nothing really happens here
print(f"Predictions: {knn.predict(X[:3])}")
print(f"Neighbors indices: {knn.kneighbors(X[:1], return_distance=False)}")📏 Distance Metrics
🎯 Measuring Proximity
Euclidean (L2), Manhattan (L1), Minkowski, Chebyshev, Mahalanobis.
Choosing the Right Metric
Euclidean works well for continuous features. Manhattan is robust to outliers. Minkowski generalizes both with p parameter. Mahalanobis accounts for feature correlations.
from sklearn.neighbors import DistanceMetric
dist = DistanceMetric.get_metric('euclidean')
X_sample = [[0, 0], [3, 4]]
print(f"Euclidean: {dist.pairwise(X_sample)}")
# Minkowski p=3
dist_p3 = DistanceMetric.get_metric('minkowski', p=3)
print(f"Minkowski p=3: {dist_p3.pairwise(X_sample)}")🎯 Choosing the Optimal K
🎯 Bias-Variance Tradeoff
Small K → high variance, overfitting. Large K → high bias, underfitting.
Hyperparameter Tuning
Use cross-validation to select K. Typically, K = √N is a good starting point. Odd K avoids ties in binary classification.
from sklearn.model_selection import cross_val_score
import numpy as np
k_range = range(1, 31)
cv_scores = []
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X, y, cv=5)
cv_scores.append(scores.mean())
best_k = k_range[np.argmax(cv_scores)]
print(f"Best K: {best_k}")⚖️ Weighted Voting & Distance Weighting
🎯 Closer Neighbors = More Influence
Weight votes by inverse distance or uniform.
Distance-Weighted KNN
Closer neighbors contribute more to the decision. Reduces impact of distant outliers.
knn_weighted = KNeighborsClassifier(n_neighbors=5, weights='distance')
knn_weighted.fit(X, y)
print(f"Weighted predictions: {knn_weighted.predict(X[:3])}")🌀 Curse of Dimensionality
🎯 Distance Becomes Meaningless in High D
As dimensions increase, all points become equidistant.
Mitigation Strategies
Feature selection, PCA, or use distance metrics that handle high dimensions (e.g., cosine).
# Demonstrate curse: increasing dimensions -> distances converge
import numpy as np
for d in [2, 10, 100]:
points = np.random.rand(1000, d)
dists = np.linalg.norm(points - points.mean(axis=0), axis=1)
print(f"Dim {d}: mean distance {dists.mean():.3f}, std {dists.std():.3f}")📐 Feature Scaling & Normalization
🎯 Scale Matters for Distance
StandardScaler, MinMaxScaler, RobustScaler.
Why Scaling is Critical
Features with larger ranges dominate distance calculations. Always scale before KNN.
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
knn_scaled = KNeighborsClassifier().fit(X_scaled, y)
print(f"Scaled accuracy: {knn_scaled.score(X_scaled, y):.3f}")⚙️ Brute-Force vs KD-Tree vs Ball-Tree
🎯 Efficient Nearest Neighbor Search
Brute-force O(N), KD-Tree O(log N) for low D, Ball-Tree for high D.
Algorithm Selection
KD-Tree works well for D < 20. Ball-Tree handles higher dimensions. Auto selects based on data.
knn_auto = KNeighborsClassifier(algorithm='auto')
knn_ball = KNeighborsClassifier(algorithm='ball_tree')
knn_kd = KNeighborsClassifier(algorithm='kd_tree')🏷️ Classification with KNN
🎯 Majority Vote
Assign class based on majority vote of k neighbors.
Decision Boundaries
KNN creates non-linear, complex decision boundaries that adapt to local data structure.
from sklearn.datasets import load_iris
iris = load_iris()
knn_clf = KNeighborsClassifier(n_neighbors=3)
knn_clf.fit(iris.data, iris.target)
print(f"Accuracy: {knn_clf.score(iris.data, iris.target):.3f}")📈 Regression with KNN
🎯 Average of Neighbors
Predict continuous value as average (or weighted average) of target values of k neighbors.
KNN Regressor
Works well for low-dimensional, smooth functions. Not extrapolative.
from sklearn.neighbors import KNeighborsRegressor
from sklearn.datasets import make_regression
X_reg, y_reg = make_regression(n_samples=100, n_features=1, noise=0.1)
knn_reg = KNeighborsRegressor(n_neighbors=3)
knn_reg.fit(X_reg, y_reg)
print(f"R²: {knn_reg.score(X_reg, y_reg):.3f}")🛡️ Outliers & Noise Robustness
🎯 Sensitivity to Noise
Outliers can distort predictions, especially with small K.
Mitigation
Use larger K, distance weighting, or robust preprocessing.
# Adding outliers
X_out = np.vstack([X, [[100, 100, 100, 100]]])
y_out = np.hstack([y, [1]])
knn_out = KNeighborsClassifier(n_neighbors=5).fit(X_out, y_out)
# Outlier might affect local neighborhoods🧩 Missing Value Imputation
🎯 KNN Imputer
Fill missing values using average of k nearest neighbors.
Multivariate Imputation
KNN imputer leverages correlations between features to estimate missing values.
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=3)
X_missing = X.copy()
X_missing[0, 0] = np.nan
X_imputed = imputer.fit_transform(X_missing)
print(f"Imputed value: {X_imputed[0, 0]:.3f}")📉 Dimensionality Reduction & KNN
🎯 PCA, t-SNE, UMAP
Reduce dimensions before KNN to mitigate curse of dimensionality.
When to Reduce
For high-dimensional datasets (D > 20), PCA or autoencoders can improve KNN performance.
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
knn_pca = KNeighborsClassifier().fit(X_pca, y)
print(f"Accuracy after PCA: {knn_pca.score(X_pca, y):.3f}")⚙️ sklearn KNN API & Pipelines
🎯 KNeighborsClassifier, KNeighborsRegressor
Unified API, GridSearchCV, Pipelines.
Production Setup
Combine scaling and KNN in a Pipeline with hyperparameter tuning.
from sklearn.pipeline import Pipeline
pipe = Pipeline([('scaler', StandardScaler()), ('knn', KNeighborsClassifier())])
params = {'knn__n_neighbors': [3,5,7,9]}
grid = GridSearchCV(pipe, params, cv=5)
grid.fit(X, y)
print(f"Best K: {grid.best_params_}")🚀 Deployment & Performance
🎯 Scalability & Serving
KNN is memory-heavy and slow for large datasets. Use approximate methods (ANN).
Production Strategies
Use Faiss, Annoy, or HNSW for approximate nearest neighbors. Store model as pickle.
import joblib
joblib.dump(knn, 'knn_model.joblib')
loaded_knn = joblib.load('knn_model.joblib')
print(loaded_knn.predict(X[:1]))