A logistic-regression baseline you can inspect¶
This notebook builds a deterministic baseline on scikit-learn's Breast Cancer Wisconsin diagnostic dataset. The goal is not a clinically deployable model. It is to make the usual tabular-classification decisions visible: class semantics, stratification, scaling, cross-validation, and aggregate error counts.
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
ConfusionMatrixDisplay,
RocCurveDisplay,
accuracy_score,
classification_report,
roc_auc_score,
)
from sklearn.model_selection import StratifiedKFold, cross_validate, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
Data and class semantics¶
The dataset contains 569 samples and 30 numeric features derived from digitized images of fine-needle aspirates. Its target order is explicit: 0 = malignant, 1 = benign. Because missing a malignant case is the consequential error in this example, the plots below treat malignant as the positive condition even though its numeric label is zero.
data = load_breast_cancer()
X, y = data.data, data.target
assert list(data.target_names) == ['malignant', 'benign']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
model = make_pipeline(
StandardScaler(),
LogisticRegression(max_iter=2_000, random_state=42),
)
model.fit(X_train, y_train)
predictions = model.predict(X_test)
probability_malignant = model.predict_proba(X_test)[:, 0]
print(f'Train/test samples: {len(X_train)}/{len(X_test)}')
print(f'Hold-out accuracy: {accuracy_score(y_test, predictions):.3f}')
print(classification_report(y_test, predictions, target_names=data.target_names))
Train/test samples: 455/114
Hold-out accuracy: 0.982
precision recall f1-score support
malignant 0.98 0.98 0.98 42
benign 0.99 0.99 0.99 72
accuracy 0.98 114
macro avg 0.98 0.98 0.98 114
weighted avg 0.98 0.98 0.98 114
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
ConfusionMatrixDisplay.from_predictions(
y_test, predictions, display_labels=data.target_names, cmap='Blues', ax=axes[0]
)
RocCurveDisplay.from_predictions(
(y_test == 0).astype(int),
probability_malignant,
name='malignant',
ax=axes[1],
)
axes[1].set_title('ROC curve: malignant as positive')
fig.tight_layout()
plt.show()
Variation across splits¶
One hold-out split is useful for inspection but weak evidence on its own. Five-fold stratified cross-validation gives a small distribution rather than one favorable number. The preprocessing remains inside the pipeline, so each fold learns its scaling parameters from its training partition only.
def malignant_roc_auc(estimator, features, target):
malignant = (target == 0).astype(int)
probability = estimator.predict_proba(features)[:, 0]
return roc_auc_score(malignant, probability)
folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_validate(
model,
X,
y,
cv=folds,
scoring={
'accuracy': 'accuracy',
'balanced_accuracy': 'balanced_accuracy',
'malignant_roc_auc': malignant_roc_auc,
},
)
summary = pd.DataFrame(
{metric.removeprefix('test_'): values for metric, values in scores.items() if metric.startswith('test_')}
).agg(['mean', 'std']).T.round(3)
summary
| mean | std | |
|---|---|---|
| accuracy | 0.974 | 0.019 |
| balanced_accuracy | 0.968 | 0.026 |
| malignant_roc_auc | 0.995 | 0.006 |
Limits¶
These metrics describe a small, curated benchmark, not prospective clinical performance. They do not test distribution shift, calibration under changed prevalence, fairness across patient groups, or the operational cost of false negatives. The useful result here is the baseline and its evaluation protocol—not a medical claim.
Sources¶
- W. H. Wolberg, W. N. Street, and O. L. Mangasarian, Breast Cancer Wisconsin (Diagnostic), UCI Machine Learning Repository, DOI: 10.24432/C5DW2B.
- scikit-learn,
load_breast_cancerdataset documentation.