Chapter 17 — Classification
Classification assigns inputs to discrete categories. This chapter builds and evaluates several classifiers — logistic regression, k-NN, and decision trees — and introduces the key metrics and visualizations for classification performance.
Learning Objectives
- Train logistic regression, k-NN, and decision tree classifiers.
- Evaluate with accuracy, precision, recall, and F1.
- Read a confusion matrix.
- Visualize a decision boundary.
- Handle multiclass classification.
- Recognize the effect of scaling on distance-based classifiers.
Prerequisites / Imports
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer, make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.pipeline import Pipeline
from sklearn.metrics import (accuracy_score, classification_report, ConfusionMatrixDisplay)
1 The Breast Cancer Dataset
Binary classification: malignant vs benign from 30 cell-nucleus features.
cancer = load_breast_cancer(as_frame=True)
X, y = cancer.data, cancer.target
print('shape:', X.shape, 'classes:', cancer.target_names.tolist())
print('class counts:', np.bincount(y))
X.head()
shape: (569, 30) classes: ['malignant', 'benign'] class counts: [212 357]
| mean radius | mean texture | mean perimeter | mean area | mean smoothness | mean compactness | mean concavity | mean concave points | mean symmetry | mean fractal dimension | radius error | texture error | perimeter error | area error | smoothness error | compactness error | concavity error | concave points error | symmetry error | fractal dimension error | worst radius | worst texture | worst perimeter | worst area | worst smoothness | worst compactness | worst concavity | worst concave points | worst symmetry | worst fractal dimension | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 17.99 | 10.38 | 122.80 | 1001.0 | 0.11840 | 0.27760 | 0.3001 | 0.14710 | 0.2419 | 0.07871 | 1.0950 | 0.9053 | 8.589 | 153.40 | 0.006399 | 0.04904 | 0.05373 | 0.01587 | 0.03003 | 0.006193 | 25.38 | 17.33 | 184.60 | 2019.0 | 0.1622 | 0.6656 | 0.7119 | 0.2654 | 0.4601 | 0.11890 |
| 1 | 20.57 | 17.77 | 132.90 | 1326.0 | 0.08474 | 0.07864 | 0.0869 | 0.07017 | 0.1812 | 0.05667 | 0.5435 | 0.7339 | 3.398 | 74.08 | 0.005225 | 0.01308 | 0.01860 | 0.01340 | 0.01389 | 0.003532 | 24.99 | 23.41 | 158.80 | 1956.0 | 0.1238 | 0.1866 | 0.2416 | 0.1860 | 0.2750 | 0.08902 |
| 2 | 19.69 | 21.25 | 130.00 | 1203.0 | 0.10960 | 0.15990 | 0.1974 | 0.12790 | 0.2069 | 0.05999 | 0.7456 | 0.7869 | 4.585 | 94.03 | 0.006150 | 0.04006 | 0.03832 | 0.02058 | 0.02250 | 0.004571 | 23.57 | 25.53 | 152.50 | 1709.0 | 0.1444 | 0.4245 | 0.4504 | 0.2430 | 0.3613 | 0.08758 |
| 3 | 11.42 | 20.38 | 77.58 | 386.1 | 0.14250 | 0.28390 | 0.2414 | 0.10520 | 0.2597 | 0.09744 | 0.4956 | 1.1560 | 3.445 | 27.23 | 0.009110 | 0.07458 | 0.05661 | 0.01867 | 0.05963 | 0.009208 | 14.91 | 26.50 | 98.87 | 567.7 | 0.2098 | 0.8663 | 0.6869 | 0.2575 | 0.6638 | 0.17300 |
| 4 | 20.29 | 14.34 | 135.10 | 1297.0 | 0.10030 | 0.13280 | 0.1980 | 0.10430 | 0.1809 | 0.05883 | 0.7572 | 0.7813 | 5.438 | 94.44 | 0.011490 | 0.02461 | 0.05688 | 0.01885 | 0.01756 | 0.005115 | 22.54 | 16.67 | 152.20 | 1575.0 | 0.1374 | 0.2050 | 0.4000 | 0.1625 | 0.2364 | 0.07678 |
2 Train/Test Split and Baseline
Always establish a train/test split and a simple baseline before tuning.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
print('majority-class baseline accuracy:', round(max(np.bincount(y_test))/len(y_test), 3))
majority-class baseline accuracy: 0.626
3 Logistic Regression
A linear, probabilistic classifier. Scaling improves convergence and performance.
pipe = Pipeline([('scaler', StandardScaler()), ('clf', LogisticRegression(max_iter=5000))])
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
print('accuracy:', round(accuracy_score(y_test, y_pred), 3))
print(classification_report(y_test, y_pred, target_names=cancer.target_names))
accuracy: 0.988
precision recall f1-score support
malignant 0.98 0.98 0.98 64
benign 0.99 0.99 0.99 107
accuracy 0.99 171
macro avg 0.99 0.99 0.99 171
weighted avg 0.99 0.99 0.99 171
4 Confusion Matrix
Shows true/false positives and negatives for each class.
ConfusionMatrixDisplay.from_predictions(y_test, y_pred, display_labels=cancer.target_names, cmap='Blues')
plt.title('Confusion matrix (logistic regression)')
plt.show()
5 Comparing Classifiers
Benchmark several models with the same split.
models = {
'LogisticRegression': Pipeline([('scaler', StandardScaler()), ('clf', LogisticRegression(max_iter=5000))]),
'KNN': Pipeline([('scaler', StandardScaler()), ('clf', KNeighborsClassifier())]),
'DecisionTree': DecisionTreeClassifier(random_state=42),
}
for name, m in models.items():
m.fit(X_train, y_train)
print(f'{name:18s} accuracy: {m.score(X_test, y_test):.3f}')
LogisticRegression accuracy: 0.988 KNN accuracy: 0.959 DecisionTree accuracy: 0.918
6 Decision Boundary
Train on two synthetic features so we can visualize the boundary each model learns.
Xb, yb = make_classification(n_samples=300, n_features=2, n_redundant=0, n_clusters_per_class=1, random_state=7)
Xtr, Xte, ytr, yte = train_test_split(Xb, yb, test_size=0.3, random_state=7)
fig, axes = plt.subplots(1, 3, figsize=(14,4))
for ax, (name, model) in zip(axes, [('k-NN', KNeighborsClassifier(7)), ('Logistic', LogisticRegression()), ('Tree', DecisionTreeClassifier(random_state=7))]):
model.fit(Xtr, ytr)
x_min, x_max = Xb[:,0].min()-1, Xb[:,0].max()+1
y_min, y_max = Xb[:,1].min()-1, Xb[:,1].max()+1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.05), np.arange(y_min, y_max, 0.05))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
ax.scatter(Xtr[:,0], Xtr[:,1], c=ytr, edgecolor='k', cmap='coolwarm')
ax.set_title(f'{name} (acc={model.score(Xte,yte):.2f})')
plt.tight_layout(); plt.show()
7 Precision, Recall, and the Tradeoff
Precision: of predicted positives, how many are correct. Recall: of actual positives, how many we caught. In medicine, recall (sensitivity) often matters most.
Case Study: Tumor Diagnosis with Cost-Sensitive Recall
In cancer screening, missing a malignant case (false negative) is costly. We weight the malignant class more heavily to raise recall.
pipe = Pipeline([('scaler', StandardScaler()), ('clf', LogisticRegression(max_iter=5000, class_weight='balanced'))])
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
print('accuracy:', round(accuracy_score(y_test, y_pred), 3))
print(classification_report(y_test, y_pred, target_names=cancer.target_names))
ConfusionMatrixDisplay.from_predictions(y_test, y_pred, display_labels=cancer.target_names, cmap='Greens')
plt.title('Balanced logistic regression'); plt.show()
accuracy: 0.971
precision recall f1-score support
malignant 0.94 0.98 0.96 64
benign 0.99 0.96 0.98 107
accuracy 0.97 171
macro avg 0.97 0.97 0.97 171
weighted avg 0.97 0.97 0.97 171
Exercises
- Load the breast cancer data and report the class distribution.
- Train a logistic regression pipeline and print accuracy.
- Display a confusion matrix for the predictions.
- Compare accuracy of logistic regression, k-NN, and a decision tree.
- Plot decision boundaries for three models on 2-D synthetic data.
- Explain why scaling matters for k-NN but not for a decision tree.
- Compute precision and recall for the malignant class.
- Explain when you would prioritize recall over precision.
- Use
classification_reportand identify the class with lower recall. - Train a classifier on the wine dataset and report accuracy.
Python Data Science: From Foundations to Applications — Chapter 17