Chapter 15 — K-Nearest Neighbors Classification
Chapter overview
K-nearest neighbors (KNN) classifies a new observation by comparing it with labeled training observations. The algorithm locates the k closest examples under a chosen distance measure and combines their labels. This local, example-based logic is intuitive, but its performance depends strongly on feature scale, neighborhood size, irrelevant dimensions, and the way evaluation is organized.
This chapter moves from geometric intuition to a leakage-safe scikit-learn workflow. Students calculate distances manually, inspect neighbor votes and probabilities, compare uniform and distance weighting, study the bias–variance pattern across values of k, and complete a practical lab using the breast cancer dataset built into scikit-learn.
Learning objectives
- Explain why nearby observations can provide evidence for a class label.
- Describe KNN as an instance-based, non-parametric, and lazy learning method.
- Calculate and interpret Euclidean, Manhattan, and Minkowski distances.
- Explain how n_neighbors, weights, metric, and p change a prediction.
- Standardize numerical features inside a leakage-safe pipeline.
- Relate small and large values of k to bias, variance, noise, and underfitting.
- Evaluate training and validation performance across candidate values of k.
- Recognize computational and high-dimensional limitations of KNN.
| CORE IDEA KNN does not learn one global equation. It stores labeled examples and makes a local decision around each new observation. |
Chapter map
Table 15.1. Questions answered in this chapter
Section | Central question | Practical focus |
|---|---|---|
15.1 | How does a local vote become a prediction? | Distances, neighbors, voting |
15.2 | Which settings control the neighborhood? | k, weights, metric, p |
15.3 | Why must features be comparable? | StandardScaler and Pipeline |
15.4 | How does k control complexity? | Training and validation curves |
15.5 | When is KNN useful or risky? | Strengths, limits, decision guide |
Lab | Which configuration generalizes best? | Controlled comparison on one split |
Prerequisites
Students should understand features X, target y, classification, training and validation sets, and basic accuracy. Familiarity with pandas, NumPy, and scikit-learn pipelines is helpful. The practical lab supplies complete code and can be run in Jupyter Notebook, JupyterLab, Google Colab, or a Python script.
Running example
Imagine classifying a cell sample as benign or malignant from numerical measurements. A new sample is compared with labeled historical samples. If most nearby samples are malignant, that local evidence supports a malignant prediction. The lab uses scikit-learn’s breast cancer dataset, but the same workflow applies to many classification problems with meaningful feature-space similarity.
| EVALUATION DISCIPLINE Choose k, the weighting rule, and the distance metric with validation data or cross-validation. Keep the final test set untouched until the modeling recipe is fixed. |
15.1 Basic principle
KNN rests on a local smoothness assumption: observations that are close in a useful feature space often share a label. The method turns that assumption into a repeatable prediction procedure.
Similar observations should have similar labels
Similarity is not discovered automatically; it is defined by the selected features, their representation, their scale, and the distance measure. Two customers may be similar in recent purchasing behavior but very different in age. Two images may be visually similar after feature extraction even though their raw pixel values are not. KNN succeeds when geometric closeness corresponds reasonably well to the class structure of the problem.
This assumption is local rather than global. The model does not require one straight line or one equation to describe every class boundary. Different regions can follow different patterns because every prediction is based on a neighborhood around the query point.
Table 15.2. The vocabulary of a KNN prediction
Term | Meaning in KNN | Question to ask |
|---|---|---|
Observation | One labeled training example | What does one row represent? |
Feature space | Coordinate system formed by the inputs | Do coordinates express useful similarity? |
Distance | Numerical dissimilarity between two points | Is the metric suitable and scaled? |
Neighborhood | The k closest training observations | How local should the decision be? |
Vote | Aggregation of neighbor labels | Equal or distance-based influence? |
Figure 15.1. Prediction with k-nearest neighbors
1 | 2 | 3 | 4 | 5 |
Receive a new point | Measure distances | Select the k closest | Aggregate their labels | Return class and score |
Distance between observations
For two observations a and b with p features, a distance function converts feature-by-feature differences into one nonnegative number. Smaller values indicate greater similarity under that definition. A distance of zero means the represented feature values are identical, though duplicated feature vectors can still have conflicting labels.
EUCLIDEAN DISTANCE (L2) Straight-line distance; large coordinate differences receive extra emphasis because they are squared. |
MANHATTAN DISTANCE (L1) Axis-aligned distance; differences are accumulated without squaring. |
MINKOWSKI DISTANCE A family of distances: p = 1 gives Manhattan and p = 2 gives Euclidean. |
A worked distance example
Suppose a = (2, 6) and b = (5, 2). Their coordinate differences are −3 and 4. The Euclidean distance is √(3² + 4²) = 5. The Manhattan distance is |−3| + |4| = 7. The ranking of candidate neighbors can change when the metric changes, especially in spaces with several dimensions or outliers.
PYTHON • Calculate distances from one query point import numpy as np
query = np.array([2.0, 6.0]) points = np.array([ [5.0, 2.0], [3.0, 5.0], [8.0, 7.0], ])
euclidean = np.sqrt(((points - query) ** 2).sum(axis=1)) manhattan = np.abs(points - query).sum(axis=1)
print("Euclidean:", euclidean.round(3)) print("Manhattan:", manhattan.round(3)) |
Nearest neighbors
After distances are calculated, the training observations are ordered from smallest to largest distance. The first k observations form the neighborhood. Only these neighbors contribute to a standard KNN prediction. A point just outside the neighborhood contributes nothing with uniform weighting, even if its distance is almost identical to that of the kth neighbor.
| TIE DETAIL If several training points are at identical distances near the neighborhood boundary, the selected set can depend on ordering and implementation details. Ties are a reason to inspect data duplication, increase k cautiously, or use distance weighting. |
Majority voting
With uniform weights, every neighbor contributes one vote. The predicted class is the class with the largest vote count. For k = 5, neighbor labels [A, A, B, A, B] produce class A because A receives three votes. In multiclass problems, the winner needs only more votes than every competing class; it does not always need more than half of all votes.
Table 15.3. Equal votes can disagree with inverse-distance votes
Neighbor rank | Distance | Label | Uniform contribution | Distance-weighted contribution |
|---|---|---|---|---|
1 | 0.20 | A | 1 | 5.00 |
2 | 0.40 | B | 1 | 2.50 |
3 | 0.50 | B | 1 | 2.00 |
Total A | — | — | 1 | 5.00 |
Total B | — | — | 2 | 4.50 |
Uniform voting predicts B because B has two of the three votes. An inverse-distance rule predicts A because the single A neighbor is much closer. This example shows that weights change the decision rule, not merely the reported probability.
Class probabilities in scikit-learn
KNeighborsClassifier can return class-probability estimates through predict_proba(). With uniform weights, the probability for a class is its fraction of the k neighbors. With distance weights, it is the class’s normalized total weight. These are local vote proportions, not automatically calibrated probabilities; their reliability should be evaluated when decisions depend on probability quality.
PYTHON • Fit a small classifier and inspect local probabilities from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler
X, y = load_iris(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y )
model = make_pipeline( StandardScaler(), KNeighborsClassifier(n_neighbors=5), ) model.fit(X_train, y_train) print("Prediction:", model.predict(X_test[:1])) print("Probabilities:", model.predict_proba(X_test[:1]).round(3)) |
Lazy learning and model fitting
KNN is often called a lazy learner because fitting does not estimate a compact predictive equation. The fit step mainly validates and stores the training data and prepares an internal neighbor-search structure when appropriate. Most computation occurs at prediction time, when distances or search queries are needed for new observations.
Table 15.4. Work performed during fitting and prediction
Stage | What KNN does | Practical implication |
|---|---|---|
Fit | Stores training features and labels | Usually fast, but memory grows with training size |
Predict | Finds neighbors and aggregates labels | Can be slow for many queries or high dimensions |
Update | Typically refit with the expanded dataset | New reference cases can change local decisions |
15.2 Main parameters
A KNN model is defined by the neighborhood size, the rule that weights neighbors, the distance metric, and search settings. These choices determine which examples influence a prediction and by how much.
Number of neighbors: n_neighbors
The parameter n_neighbors is k. A small k produces a highly local decision; a large k averages evidence over a broader region. The largest valid k cannot exceed the number of training observations available to the fitted model, and cross-validation folds may contain fewer training samples than the full dataset.
- k = 1 assigns the label of the single closest training observation.
- Increasing k usually smooths the decision boundary and reduces sensitivity to one noisy point.
- Very large k can make the majority class dominate and erase meaningful local structure.
- Odd values can reduce some two-class vote ties, but odd k is not a universal rule for multiclass or weighted voting.
| SELECTION RULE Treat k as a hyperparameter. Compare plausible values on validation data or through cross-validation; do not choose the value that performs best on the final test set. |
Uniform versus distance weighting
Table 15.5. Neighbor weighting options
weights setting | Contribution of each neighbor | When it may help | Main caution |
|---|---|---|---|
uniform | Every selected neighbor has equal influence | Dense, reasonably homogeneous neighborhoods | A distant kth neighbor counts as much as the closest |
distance | Closer neighbors receive larger inverse-distance weights | Neighborhoods with varying density or very close evidence | Can amplify duplicates, noise, or tiny distances |
callable | User-defined function maps distances to weights | Domain-specific influence rules | Requires careful testing and documentation |
When weights='distance', scikit-learn gives nearer observations greater influence. If a query exactly matches one or more training points, zero-distance handling is special: the coincident points dominate rather than causing division by zero. Conflicting duplicates should still be investigated because they indicate label ambiguity or data-quality problems.
PYTHON • Compare equal and distance-weighted voting from sklearn.neighbors import KNeighborsClassifier
uniform_knn = KNeighborsClassifier( n_neighbors=7, weights="uniform", ) distance_knn = KNeighborsClassifier( n_neighbors=7, weights="distance", )
uniform_knn.fit(X_train, y_train) distance_knn.fit(X_train, y_train)
print("Uniform accuracy:", uniform_knn.score(X_test, y_test)) print("Distance accuracy:", distance_knn.score(X_test, y_test)) |
Distance metric and Minkowski power p
The default metric is Minkowski with p = 2, which corresponds to Euclidean distance. Setting p = 1 gives Manhattan distance. The metric controls the geometry of neighborhoods; it should reflect how feature differences combine in the application. The same k can produce different neighbors under different metrics.
Table 15.6. Common distance choices
Metric | scikit-learn setting | Geometry | Useful consideration |
|---|---|---|---|
Euclidean | metric='minkowski', p=2 | Straight-line L2 distance | Common for standardized continuous features |
Manhattan | metric='minkowski', p=1 | Axis-aligned L1 distance | Less emphasis on a single large coordinate gap |
Minkowski | metric='minkowski', p>0 | General Lp family | p changes feature-difference emphasis |
Custom/callable | metric=function | Domain-defined | Flexible but slower and easier to misuse |
PYTHON • Configure Manhattan and Euclidean KNN from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler
knn_l1 = make_pipeline( StandardScaler(), KNeighborsClassifier( n_neighbors=9, metric="minkowski", p=1 ), ) knn_l2 = make_pipeline( StandardScaler(), KNeighborsClassifier( n_neighbors=9, metric="minkowski", p=2 ), )
knn_l1.fit(X_train, y_train) knn_l2.fit(X_train, y_train) |
Search algorithm, leaf_size, and n_jobs
KNeighborsClassifier also exposes implementation settings. algorithm='auto' lets scikit-learn select a suitable strategy among brute force, KD tree, and ball tree. The fastest choice depends on sample size, dimensionality, metric, and data representation. leaf_size affects tree construction, query speed, and memory, while n_jobs controls parallel work for neighbor searches where supported.
Table 15.7. Secondary implementation parameters
Parameter | Main role | Default starting point |
|---|---|---|
algorithm | Neighbor-search strategy | Use 'auto'; benchmark only when latency matters |
leaf_size | Tree construction/query trade-off | Keep default until profiling shows a need |
n_jobs | Parallelism for neighbor search | Use available resources responsibly |
metric_params | Extra arguments for a selected metric | Only when the metric requires them |
| PRIORITY ORDER Tune the statistical choices first: feature representation, scaling, k, weights, and metric. Optimize search settings only after predictive behavior is sound and prediction-time cost has been measured. |
15.3 Importance of scaling
Distance compares numerical coordinates. If one feature spans thousands and another spans fractions, the large-range feature can dominate the neighborhood even when it is not more informative.
Why large ranges dominate
Consider two customer features: age in years and annual income in dollars. A difference of 10 years contributes 100 to squared Euclidean distance, while a difference of $20,000 contributes 400,000,000. Without scaling, income almost completely determines which customers are called close. The algorithm is responding to units, not necessarily importance.
Table 15.8. Scaling changes the meaning of a coordinate difference
Feature | Raw values | Raw difference | Approximate standardized difference |
|---|---|---|---|
Age | 30 vs 40 years | 10 | 1.0 standard deviations |
Income | $60,000 vs $80,000 | 20,000 | 1.0 standard deviations |
STANDARD SCORE StandardScaler learns the training mean and standard deviation separately for each feature. |
Standardization before KNN
StandardScaler centers each numerical feature around its training mean and scales it by its training standard deviation. A difference of one transformed unit therefore represents approximately one training-set standard deviation. Standardization does not make a feature useful, remove outliers, or guarantee a normal distribution; it only places features on comparable statistical scales.
- Fit the scaler on training data only.
- Use the fitted scaler to transform validation, test, and future observations.
- Keep scaling and KNN together in one Pipeline so cross-validation refits preprocessing inside each training fold.
- Consider robust scaling or domain-specific transformations when extreme outliers distort means and standard deviations.
Leakage-safe pipeline
Applying fit_transform to the full dataset before splitting leaks information from the evaluation rows into the scaling statistics. A pipeline prevents this common error because fit is called separately within the appropriate training subset, while predict and score reuse the learned transformation.
PYTHON • Build a leakage-safe scaled KNN pipeline from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler
knn_pipeline = Pipeline([ ("scale", StandardScaler()), ("knn", KNeighborsClassifier( n_neighbors=7, weights="uniform", metric="minkowski", p=2, )), ])
knn_pipeline.fit(X_train, y_train) test_score = knn_pipeline.score(X_test, y_test) print(f"Test accuracy: {test_score:.3f}") |
A direct comparison: scaled versus unscaled
The following experiment uses the breast cancer dataset because its numerical features have very different ranges. Both models receive the same training and test rows and use the same k. Only the scaling step changes, so the comparison isolates the effect of feature scale.
PYTHON • Compare raw and standardized feature spaces from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split
X, y = load_breast_cancer(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y ) raw_knn = KNeighborsClassifier(n_neighbors=7) scaled_knn = make_pipeline( StandardScaler(), KNeighborsClassifier(n_neighbors=7) ) raw_knn.fit(X_train, y_train) scaled_knn.fit(X_train, y_train) print("Raw:", raw_knn.score(X_test, y_test)) print("Scaled:", scaled_knn.score(X_test, y_test)) |
| INTERPRETATION A higher scaled score is evidence that raw units distorted neighbor selection in this dataset. It is not proof that StandardScaler is always the best transformation for every feature or application. |
Categorical, binary, and sparse features
KNN is naturally expressed in a numerical feature space. Nominal categories usually require encoding, but ordinary one-hot encoding plus Euclidean distance can give a categorical mismatch a geometry that may not reflect domain similarity. Mixed numerical and categorical data may require carefully weighted transformations, a suitable metric, or another model family.
Binary indicators already have comparable numerical ranges, but their relative influence still depends on how many indicators exist and how they are weighted. For sparse matrices, centering can destroy sparsity; StandardScaler(with_mean=False) or MaxAbsScaler may be more appropriate. Always test the complete representation, not one preprocessing step in isolation.
Table 15.9. Representation decisions before KNN
Data characteristic | Potential treatment | KNN caution |
|---|---|---|
| Continuous numeric | StandardScaler or robust alternative | Outliers and units affect distance |
Ordinal | Meaningful numeric encoding, then scale | Spacing between levels may not be equal |
| Nominal categorical | One-hot or domain metric | Euclidean geometry may be arbitrary |
| Sparse high-dimensional | MaxAbsScaler or no centering | Distance concentration and slow queries |
Missing values | Impute inside a pipeline | Standard KNN cannot compare NaN values directly |
15.4 Bias–variance behavior
The value of k controls how much local detail the classifier follows. It therefore acts as a complexity parameter, even though KNN does not fit a traditional parametric equation.
Figure 15.2. The effect of k on model flexibility
SMALL k | MODERATE k | LARGE k |
Detailed boundary | Balanced locality | Smooth boundary |
Small k: flexible and potentially noisy
With k = 1, every training observation owns a local region and can determine predictions near itself. Training accuracy is often extremely high because each training point is its own nearest neighbor. This low-bias behavior can follow curved boundaries, but it also makes the classifier sensitive to mislabeled points, duplicates, sampling variation, and measurement noise.
Large k: smoother and potentially underfitted
As k increases, a prediction averages labels across a wider region. Local fluctuations matter less, which can reduce variance. If the neighborhood becomes too broad, distinct subgroups are blended together and the global majority class can dominate. Training and validation performance can then both decline, indicating underfitting.
Table 15.10. Interpreting training and validation curves
Pattern | Training score | Validation score | Likely diagnosis |
|---|---|---|---|
Very small k | Very high | Noticeably lower | High variance or sensitivity to noise |
Moderate k | High | Highest or stable | Useful bias–variance balance |
Very large k | Lower | Lower | High bias / oversmoothing |
| All k values weak | Weak | Weak | Representation, metric, or problem may be unsuitable |
Generate a validation curve
A validation curve holds the evaluation method constant while varying one hyperparameter. The next example uses stratified five-fold cross-validation. Because k is nested inside a pipeline step named knn, the parameter path is knn__n_neighbors.
PYTHON • Training and validation scores across k import numpy as np from sklearn.model_selection import validation_curve
k_values = np.arange(1, 32, 2) train_scores, validation_scores = validation_curve( scaled_knn, X, y, param_name="kneighborsclassifier__n_neighbors", param_range=k_values, |
PYTHON • Training and validation scores across k — continued cv=5, scoring="accuracy", n_jobs=-1, )
train_mean = train_scores.mean(axis=1) validation_mean = validation_scores.mean(axis=1) best_k = k_values[validation_mean.argmax()] print("Best cross-validated k:", best_k) |
PYTHON • Plot the bias–variance pattern import matplotlib.pyplot as plt
plt.figure(figsize=(8, 5)) plt.plot(k_values, train_mean, marker="o", label="Training") plt.plot(k_values, validation_mean, marker="o", label="Validation") plt.xlabel("Number of neighbors (k)") plt.ylabel("Accuracy") plt.title("KNN validation curve") plt.xticks(k_values) plt.grid(alpha=0.25) plt.legend() plt.tight_layout() plt.show() |
How to choose a useful k range
- Start with positive integer values that are smaller than every cross-validation training fold.
- Use a broad coarse range first; then examine a narrower range around promising values.
- Include both odd and even values when the task is multiclass or distance-weighted.
- Compare more than the maximum mean score: inspect variability, class-specific metrics, latency, and stability.
- If several neighboring values perform similarly, prefer a stable region rather than a fragile single peak.
| DO NOT OVERREAD ONE CURVE A validation curve is an estimate. A tiny difference between adjacent k values may be sampling noise. Report cross-validation variability and confirm that the chosen model meets the problem’s actual error costs. |
15.5 Advantages and limitations
KNN is valuable because its logic is transparent and flexible. The same properties that make it simple also create memory, latency, dimensionality, and representation challenges.
Advantages
- Simple and intuitive: the prediction can be explained through nearby examples.
- No explicit training equation: there is no assumption of one global linear decision boundary.
- Nonlinear decision boundaries: local neighborhoods can follow complex class shapes.
- Natural multiclass support: the vote can include any number of class labels.
- Few central hyperparameters: k, weights, distance metric, and preprocessing provide a clear experimental starting point.
- Useful benchmark: a scaled KNN model can reveal whether local similarity contains predictive signal.
Limitations
- Sensitive to irrelevant features: every included coordinate can distort distance.
- Sensitive to scale and units: preprocessing is part of the model definition.
- Slow prediction on large datasets: neighbor search is performed for each query.
- Memory intensive: the reference training observations must be retained.
- Affected by high dimensionality: distances become less discriminative as dimensions grow.
- Limited extrapolation: KNN predicts from observed local labels rather than a learned structural law.
- Vulnerable to class imbalance and uneven density: local votes can favor common classes.
- Awkward with mixed or structured inputs: meaningful similarity may require specialized representations.
The curse of dimensionality
Adding features increases the volume of the feature space rapidly. Training observations become sparse, and the nearest point may no longer be meaningfully close. Distances can concentrate so that the closest and farthest observations have similar values. More data, careful feature selection, dimensionality reduction, or a different model family may be necessary.
Table 15.11. Failure signals and responses
Problem signal | Why it harms KNN | Possible response |
|---|---|---|
| Many irrelevant features | Noise contributes to every distance | Feature selection or domain review |
| Thousands of sparse features | Neighbors become less distinct | Dimensionality reduction or linear model |
| Millions of training rows | Storage and query cost grow | Approximate search or another algorithm |
| Highly imbalanced target | Majority neighbors dominate | Class-aware metrics, resampling in training, or weights |
| Rapidly changing process | Old neighbors stop being representative | Time-aware validation and retraining |
When KNN is a sensible candidate
KNN is most attractive when the dataset is small or medium sized, features can be placed in a meaningful numerical space, prediction latency is not extremely constrained, and local similarity is plausible. It is less attractive when the data are enormous, very sparse, very high dimensional, strongly mixed-type, or when a compact equation and fast scoring are essential.
Table 15.12. A quick KNN suitability guide
Condition | Favors KNN | Suggests caution |
|---|---|---|
Dataset size | Small to medium | Very large training set |
Feature geometry | Scaled, meaningful numeric coordinates | Arbitrary encodings or mixed units |
Dimensionality | Low to moderate | Very high or sparse |
Deployment | Moderate query volume | Strict low-latency scoring |
Pattern | Local neighborhoods are informative | Similarity is poorly defined |
| MODEL-SELECTION PERSPECTIVE KNN should compete under the same split, preprocessing discipline, and metric as other candidate models. Its intuitive mechanism is not a substitute for validation. |
Practical lab — Study the effect of k
In this lab, students build a scaled KNN classifier for the breast cancer dataset, compare training and validation performance across values of k, investigate distance and weighting choices, select one configuration using validation data, and evaluate it once on an untouched test set.
Lab objectives
- Create reproducible training, validation, and test subsets with stratification.
- Build a StandardScaler–KNN pipeline that prevents preprocessing leakage.
- Record training and validation accuracy for a controlled range of k values.
- Plot the two curves and interpret underfitting and overfitting behavior.
- Compare uniform versus distance weighting and Manhattan versus Euclidean distance.
- Freeze the selected configuration before the final test evaluation.
Dataset and experimental rules
Table 15.13. Practical lab design
Element | Choice | Reason |
|---|---|---|
Dataset | Breast cancer Wisconsin (diagnostic) | Built into scikit-learn; numerical binary classification |
Split | 60% train / 20% validation / 20% test | Separate model selection from final evaluation |
Stratification | Use y in both split operations | Preserve benign/malignant proportions approximately |
Preprocessing | StandardScaler inside Pipeline | Comparable feature scales without leakage |
Primary metric | Accuracy for the k curve | Simple controlled study; supplement with recall and F1 |
Random state | 42 | Repeatable lab results |
| IMPORTANT This dataset is educational. A classroom score does not establish clinical validity, safety, or suitability for medical decisions. |
Step 1 — Load and inspect the data
PYTHON • Load the dataset and inspect the target import numpy as np import pandas as pd import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer from sklearn.metrics import classification_report, confusion_matrix from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler
data = load_breast_cancer(as_frame=True) X = data.data y = data.target
print("Shape:", X.shape) print("Class names:", list(data.target_names)) print(y.value_counts().sort_index()) |
The target labels are 0 for malignant and 1 for benign in this dataset. Write the label meaning next to every class-specific metric so that a high recall value cannot be misinterpreted.
Step 2 — Create protected subsets
PYTHON • Create 60/20/20 stratified subsets X_train, X_temp, y_train, y_temp = train_test_split( X, y, test_size=0.40, random_state=42, stratify=y, )
X_valid, X_test, y_valid, y_test = train_test_split( X_temp, |
PYTHON • Create 60/20/20 stratified subsets — continued y_temp, test_size=0.50, random_state=42, stratify=y_temp, )
print("Train:", X_train.shape) print("Validation:", X_valid.shape) print("Test:", X_test.shape) |
Step 3 — Audit the split
PYTHON • Compare class proportions def class_share(labels): return labels.value_counts(normalize=True).sort_index()
shares = pd.DataFrame({ "train": class_share(y_train), "validation": class_share(y_valid), "test": class_share(y_test), })
shares.index = ["malignant", "benign"] print(shares.round(3))
assert set(X_train.index).isdisjoint(X_valid.index) assert set(X_train.index).isdisjoint(X_test.index) assert set(X_valid.index).isdisjoint(X_test.index) |
Step 4 — Sweep candidate values of k
Every candidate uses exactly the same split, scaler, metric, and weighting rule. Only k changes. This controlled design makes the curve interpretable.
PYTHON • Record training and validation performance k_values = list(range(1, 32, 2)) records = []
for k in k_values: model = make_pipeline( StandardScaler(), KNeighborsClassifier( n_neighbors=k, weights="uniform", metric="minkowski", p=2, |
PYTHON • Record training and validation performance — continued ), ) model.fit(X_train, y_train) records.append({ "k": k, "train_accuracy": model.score(X_train, y_train), "validation_accuracy": model.score(X_valid, y_valid), })
results = pd.DataFrame(records) print(results.round(3)) |
Step 5 — Plot the training and validation curves
PYTHON • Visualize model complexity plt.figure(figsize=(8, 5)) plt.plot( results["k"], results["train_accuracy"], marker="o", label="Training accuracy", ) plt.plot( results["k"], results["validation_accuracy"], marker="o", label="Validation accuracy", ) plt.xlabel("Number of neighbors (k)") plt.ylabel("Accuracy") plt.title("KNN: training versus validation performance") plt.xticks(k_values) plt.grid(alpha=0.25) plt.legend() plt.tight_layout() plt.show() |
Questions for the curve
- At which k is training accuracy highest? Why is this expected?
- Which k values produce the strongest validation accuracy?
- Where is the training–validation gap largest?
- At what point do both curves begin to fall, if they do?
- Is the best result a broad plateau or a fragile isolated peak?
Step 6 — Select k without touching the test set
PYTHON • Identify the strongest validation candidate best_row = results.loc[ results["validation_accuracy"].idxmax() ] selected_k = int(best_row["k"])
print("Selected k:", selected_k) print( "Validation accuracy:", round(best_row["validation_accuracy"], 3), ) print( "Training accuracy:", round(best_row["train_accuracy"], 3), ) |
| TIE POLICY If several k values have the same validation score, prefer a value within a stable neighborhood of good results. Record the tie rule before examining the test score. |
Step 7 — Compare weights and metrics
Use the selected k and vary one additional choice at a time. The test set remains protected. The following four configurations compare equal versus distance weighting and Manhattan versus Euclidean distance.
PYTHON • Run a controlled configuration comparison configurations = []
for weights in ["uniform", "distance"]: for p in [1, 2]: model = make_pipeline( StandardScaler(), KNeighborsClassifier( n_neighbors=selected_k, weights=weights, metric="minkowski", p=p, |
PYTHON • Run a controlled configuration comparison — continued ), ) model.fit(X_train, y_train) configurations.append({ "weights": weights, "p": p, "validation_accuracy": model.score(X_valid, y_valid), })
comparison = pd.DataFrame(configurations) print(comparison.sort_values("validation_accuracy", ascending=False)) |
Step 8 — Freeze and evaluate the final model
Choose the final weights and p from the validation comparison. Then combine training and validation data, refit the complete pipeline, and evaluate once on the test set. The example below assumes the first row of the sorted comparison is selected.
PYTHON • Refit on development data and test once best_config = comparison.sort_values( "validation_accuracy", ascending=False ).iloc[0]
X_development = pd.concat([X_train, X_valid]) y_development = pd.concat([y_train, y_valid])
final_model = make_pipeline( StandardScaler(), KNeighborsClassifier( |
PYTHON • Refit on development data and test once — continued n_neighbors=selected_k, weights=best_config["weights"], metric="minkowski", p=int(best_config["p"]), ), )
final_model.fit(X_development, y_development) test_predictions = final_model.predict(X_test) |
PYTHON • Report final classification results print("Confusion matrix:") print(confusion_matrix(y_test, test_predictions))
print() print("Classification report:") print(classification_report( y_test, test_predictions, target_names=data.target_names, digits=3, ))
print( "Final test accuracy:", round(final_model.score(X_test, y_test), 3), ) |
Step 9 — Inspect the actual neighbors
A pipeline transforms the query before the KNN step can measure distance. The following code retrieves the five nearest development observations for one test case and displays their labels. This local evidence can support debugging, but it does not prove causality or clinical relevance.
PYTHON • Inspect one prediction’s nearest examples query = X_test.iloc[[0]] scaler = final_model.named_steps["standardscaler"] knn = final_model.named_steps["kneighborsclassifier"]
query_scaled = scaler.transform(query) distances, indices = knn.kneighbors( query_scaled, n_neighbors=5, )
neighbor_rows = X_development.iloc[indices[0]].copy() neighbor_rows["target"] = y_development.iloc[indices[0]].to_numpy() neighbor_rows["distance"] = distances[0]
print("Predicted class:", final_model.predict(query)[0]) print(neighbor_rows[["target", "distance"]]) |
Expected observations
- Training accuracy is usually greatest for the smallest k values.
- Validation accuracy often improves after moving away from k = 1, then stabilizes or falls.
- Scaling materially changes neighborhoods because the raw feature ranges differ.
- Uniform and distance weighting may rank configurations differently.
- Manhattan and Euclidean distance can produce different local neighborhoods.
- The final test score may be lower than the best validation score because selection favored validation noise.
Lab deliverables
- A reproducible notebook containing all code and outputs.
- A table of k, training accuracy, and validation accuracy.
- One labeled training-versus-validation curve.
- A comparison table for weights and p.
- A final confusion matrix and classification report.
- A 150–250 word interpretation explaining the selected k and the observed bias–variance behavior.
Extension challenges
- Replace the single validation set with StratifiedKFold cross-validation and report mean ± standard deviation.
- Compare the scaled model with an unscaled model using exactly the same data splits.
- Evaluate balanced accuracy, malignant-class recall, and F1 in addition to overall accuracy.
- Add deliberately irrelevant random features and measure how KNN performance changes.
- Time prediction for batches of increasing size and discuss deployment implications.
| RESPONSIBLE INTERPRETATION For a health-related dataset, false negatives and false positives have different consequences. Accuracy alone is not sufficient for a real clinical decision, and this lab is not a deployment study. |
Common mistakes and corrections
Table 15.14. Frequent KNN errors
Mistake | Why it causes trouble | Correction |
|---|---|---|
| Use raw mixed-scale features | Units determine the neighborhood | Scale numeric features inside a pipeline |
| Scale before splitting | Evaluation rows influence preprocessing | Fit transformations only on training data |
| Choose k on the test set | Test data becomes part of model selection | Use validation or cross-validation |
| Assume odd k prevents all ties | Multiclass and weighted votes can still tie | Inspect tie behavior and stability |
| Add every available feature | Irrelevant dimensions distort distance | Use domain review and validation |
| Interpret vote share as calibrated risk | Local proportions may be miscalibrated | Evaluate calibration separately |
| Ignore prediction cost | Lazy learning shifts work to scoring | Benchmark latency and memory |
| Use one metric for imbalance | Majority performance can hide minority errors | Report class-aware metrics |
Knowledge check
Choose one answer for each question. Complete the questions before consulting the answer key.
1. What does KNN primarily use to classify a new observation?
- A. A fitted linear equation
- B. Labels of nearby training observations
- C. The global target mean
- D. Random class assignment
2. With uniform weights and k = 5, how is the class selected?
- A. The farthest label wins
- B. The class with the most neighbor votes wins
- C. All classes receive equal probability
- D. The smallest numeric label wins
3. Minkowski distance with p = 2 is equivalent to:
- A. Manhattan distance
- B. Cosine similarity
- C. Euclidean distance
- D. Hamming distance
4. Why is scaling especially important for KNN?
- A. KNN requires normal targets
- B. Large-range features can dominate distance
- C. Scaling removes every outlier
- D. It creates more observations
5. What is the safest place for StandardScaler?
- A. Fit on all rows before splitting
- B. Inside a pipeline fitted on training data
- C. After final prediction
- D. Only on the target
6. A very small k usually produces:
- A. Higher bias and lower variance
- B. A more flexible, noise-sensitive boundary
- C. A global constant prediction
- D. Faster fitting of a linear equation
7. A very large k can underfit because:
- A. It ignores too many neighbors
- B. It averages over overly broad regions
- C. It always uses Manhattan distance
- D. It removes scaling
8. weights='distance' means:
- A. Farther neighbors count more
- B. Only one neighbor is used
- C. Closer neighbors have greater influence
- D. Features are standardized automatically
9. Which is a key high-dimensional limitation?
- A. Distances can become less discriminative
- B. KNN becomes a linear model
- C. The target becomes continuous
- D. Training labels disappear
10. When should the final test set be used?
- A. To select k repeatedly
- B. To choose scaling after inspecting results
- C. Once after the recipe is frozen
- D. Before training data are created
Answer key and explanations
Table 15.15. Knowledge-check solutions
Answer | Explanation |
|---|---|
1 — B | KNN bases a query’s prediction on labeled training examples that are closest under the selected distance. |
2 — B | Uniform weighting gives every selected neighbor one vote, and the largest vote total wins. |
3 — C | Minkowski p = 2 is Euclidean distance; p = 1 is Manhattan distance. |
4 — B | Without scaling, features with large numerical ranges can dominate the distance calculation. |
5 — B | A pipeline fits the scaler on each training subset and reuses it for held-out observations. |
6 — B | Small neighborhoods create detailed boundaries with lower bias and higher sensitivity to sampling noise. |
7 — B | Large neighborhoods blend labels across wide regions and can erase useful local structure. |
8 — C | Inverse-distance weighting gives closer selected neighbors greater influence. |
9 — A | In high dimensions, observations become sparse and near/far distances can be less distinguishable. |
10 — C | The test set is reserved for the final evaluation after preprocessing and hyperparameters are fixed. |
Score interpretation
- 9–10 correct: ready to build and evaluate a leakage-safe KNN classifier.
- 7–8 correct: solid understanding; revisit scaling or bias–variance behavior where needed.
- 5–6 correct: repeat the manual distance example and the k-curve lab.
- 0–4 correct: review the prediction sequence and parameter table before continuing.
Chapter summary
- KNN predicts from the labels of nearby training observations in a defined feature space.
- Distance, representation, and scale determine which observations are considered similar.
- Uniform weights give equal votes; distance weights give closer neighbors more influence.
- Minkowski p = 1 gives Manhattan distance and p = 2 gives Euclidean distance.
- Standardization should be fitted on training data inside a pipeline to prevent leakage.
- Small k creates flexible, high-variance behavior; large k creates smoother, higher-bias behavior.
- Validation curves reveal the relationship between k, training fit, and generalization.
- KNN is intuitive and nonlinear but can be slow, memory intensive, scale sensitive, and weak in high dimensions.
- The final model must be selected without using the protected test set.
| ONE SENTENCE TO REMEMBER KNN is only as meaningful as the geometry you create: choose useful features, scale them correctly, and validate the neighborhood size on unseen data. |
Key vocabulary
Table 15.16. Essential terminology
Term | Meaning |
|---|---|
| Instance-based learning | Prediction based directly on stored training examples |
Neighbor | A training observation close to a query under the chosen metric |
k | Number of neighbors used for a standard prediction |
| Minkowski distance | Lp distance family controlled by the power p |
| Uniform weighting | Equal contribution from every selected neighbor |
| Distance weighting | Greater contribution from closer selected neighbors |
Feature scaling | Transformation that makes numerical feature ranges comparable |
| Bias–variance trade-off | Balance between oversimplification and sensitivity to data variation |
| Curse of dimensionality | Loss of meaningful locality as feature-space dimensions increase |
Further reading
- scikit-learn: KNeighborsClassifier
- scikit-learn user guide: nearest neighbors
- scikit-learn: StandardScaler
- scikit-learn: Pipeline
- scikit-learn: validation_curve
What’s next?
The next chapter studies decision tree classification. Unlike KNN, a tree learns an explicit hierarchy of feature-based rules during training and usually predicts quickly without scaling. Comparing the two models reveals how algorithm assumptions, preprocessing needs, interpretability, and computational cost shape model selection.