Skip to main content

Palmer Penguins kNN Tuning (Python)

This Quarto document runs the Python translation of kNN_example_penguins2.qmd through reticulate.

library(reticulate)
py_require(c("pandas", "numpy", "scikit-learn", "matplotlib",
             "seaborn", "palmerpenguins"))

Load, clean, and visualize the data

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from sklearn.metrics import ConfusionMatrixDisplay, accuracy_score, confusion_matrix
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

try:
    from palmerpenguins import load_penguins
    penguins = load_penguins()
except ImportError:
    penguins = sns.load_dataset("penguins")

penguins = penguins.drop(columns=["island", "sex"])
print(penguins["species"].value_counts().sort_index())
species
Adelie       152
Chinstrap     68
Gentoo       124
Name: count, dtype: int64
print(penguins.isna().sum())
species              0
bill_length_mm       2
bill_depth_mm        2
flipper_length_mm    2
body_mass_g          2
dtype: int64
print(penguins.describe(include="all"))
       species  bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
count      344      342.000000     342.000000         342.000000   342.000000
unique       3             NaN            NaN                NaN          NaN
top     Adelie             NaN            NaN                NaN          NaN
freq       152             NaN            NaN                NaN          NaN
mean       NaN       43.921930      17.151170         200.915205  4201.754386
std        NaN        5.459584       1.974793          14.061714   801.954536
min        NaN       32.100000      13.100000         172.000000  2700.000000
25%        NaN       39.225000      15.600000         190.000000  3550.000000
50%        NaN       44.450000      17.300000         197.000000  4050.000000
75%        NaN       48.500000      18.700000         213.000000  4750.000000
max        NaN       59.600000      21.500000         231.000000  6300.000000
# Equivalent to step_naomit() in the R recipe.
penguins = penguins.dropna().copy()

sns.scatterplot(data=penguins, x="bill_length_mm", y="bill_depth_mm", hue="species")
plt.tight_layout()
plt.show()

sns.pairplot(penguins, hue="species")

plt.show()

Split the data and define the model

X = penguins.drop(columns="species")
y = penguins["species"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.20, stratify=y, random_state=123
)

# Scaling is inside the pipeline so each cross-validation fold learns its own
# scaling parameters, preventing information leakage.
pipeline = Pipeline(
    [
        ("scale", StandardScaler()),
        ("knn", KNeighborsClassifier(weights="uniform")),
    ]
)

Tune k with five-fold cross-validation

neighbor_grid = list(range(4, 16))
folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=234)

search = GridSearchCV(
    pipeline,
    param_grid={"knn__n_neighbors": neighbor_grid},
    cv=folds,
    scoring="accuracy",
    return_train_score=False,
)
search.fit(X_train, y_train)
GridSearchCV(cv=StratifiedKFold(n_splits=5, random_state=234, shuffle=True),
             estimator=Pipeline(steps=[('scale', StandardScaler()),
                                       ('knn', KNeighborsClassifier())]),
             param_grid={'knn__n_neighbors': [4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
                                              14, 15]},
             scoring='accuracy')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
cv_results = pd.DataFrame(search.cv_results_)[
    ["param_knn__n_neighbors", "mean_test_score", "std_test_score", "rank_test_score"]
].sort_values("rank_test_score")
print(cv_results.to_string(index=False))
 param_knn__n_neighbors  mean_test_score  std_test_score  rank_test_score
                      5         0.985320        0.007342                1
                      4         0.978047        0.013571                2
                      6         0.978047        0.017926                3
                     11         0.977980        0.013734                4
                     13         0.977980        0.013734                4
                      7         0.974343        0.014615                6
                      9         0.974343        0.014615                6
                      8         0.974343        0.014615                6
                     10         0.970640        0.014683                9
                     12         0.967003        0.017872               10
                     14         0.967003        0.017872               10
                     15         0.967003        0.007414               10
print(f"Best k: {search.best_params_['knn__n_neighbors']}")
Best k: 5
print(f"Best CV accuracy: {search.best_score_:.4f}")
Best CV accuracy: 0.9853

Evaluate the finalized model

# GridSearchCV refits the best pipeline on all training data.
final_model = search.best_estimator_
test_predictions = final_model.predict(X_test)
print(f"Test accuracy: {accuracy_score(y_test, test_predictions):.4f}")
Test accuracy: 1.0000
labels = final_model.classes_
cm = confusion_matrix(y_test, test_predictions, labels=labels)
print(pd.DataFrame(cm, index=labels, columns=labels))
           Adelie  Chinstrap  Gentoo
Adelie         30          0       0
Chinstrap       0         14       0
Gentoo          0          0      25
ConfusionMatrixDisplay(cm, display_labels=labels).plot(cmap="Blues")
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay object at 0x7f300267f380>
plt.title("Testing confusion matrix")
plt.tight_layout()
plt.show()