Exercise 1: penguins, three classes
Why this exercise
Until now everything was binary: yes or no, 0 or 1. precision_score(y, b) returned one number.
Now there are three classes. And the pattern you learned breaks: precision_score raises an error, the confusion matrix is 3x3 instead of 2x2, and the thing called "TP" disappears.
That's transfer: learning what to do when the pattern breaks.
Getting to know the data
import pandas as pd pg = pd.read_csv(DATA / "penguins.csv") print("shape:", pg.shape) print() print(pg.dtypes.to_string()) print() show(pg.head(6))
shape: (344, 7) species object island object bill_length_mm float64 bill_depth_mm float64 flipper_length_mm float64 body_mass_g float64 sex object
| species | island | bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | sex |
|---|---|---|---|---|---|---|
| Adelie | Torgersen | 39.1 | 18.7 | 181 | 3 750 | Male |
| Adelie | Torgersen | 39.5 | 17.4 | 186 | 3 800 | Female |
| Adelie | Torgersen | 40.3 | 18 | 195 | 3 250 | Female |
| Adelie | Torgersen | NaN | NaN | NaN | NaN | NaN |
| Adelie | Torgersen | 36.7 | 19.3 | 193 | 3 450 | Female |
| Adelie | Torgersen | 39.3 | 20.6 | 190 | 3 650 | Male |
print("Classes:") print(pg.species.value_counts().to_string()) print() print("ratio:") print(pg.species.value_counts(normalize=True).round(3).to_string()) print() missing = pg.isna().sum() print("Missing values:") print(missing[missing > 0].to_string())
Classes: species Adelie 152 Gentoo 124 Chinstrap 68 ratio: species Adelie 0.442 Gentoo 0.360 Chinstrap 0.198 Missing values: bill_length_mm 2 bill_depth_mm 2 flipper_length_mm 2 body_mass_g 2 sex 11
The task
Walk the 12 steps from Chapter 1 yourself. Nobody shows you the way. Write the following in your notebook:
import pandas as pd show(pd.DataFrame([ {"#": 1, "Answer to write down": "How many rows, how many columns"}, {"#": 2, "Answer to write down": "\"I predict ___ from ___\": one sentence"}, {"#": 3, "Answer to write down": "Task type: regression / binary / multiclass"}, {"#": 4, "Answer to write down": "The target ratio and the BASELINE number"}, {"#": 5, "Answer to write down": "Did you split? Do you need stratify?"}, {"#": 6, "Answer to write down": "The dummy result"}, {"#": 7, "Answer to write down": "Which columns you dropped, and WHY"}, {"#": 8, "Answer to write down": "What you did about missing values and types"}, {"#": 9, "Answer to write down": "Your first honest result"}, {"#": 10, "Answer to write down": "Which metric you chose, and WHY"}, {"#": 11, "Answer to write down": "Which group the mistakes cluster in"}, {"#": 12, "Answer to write down": "Which ONE change you made, and the result"}, ]))
| # | Answer to write down |
|---|---|
| 1 | How many rows, how many columns |
| 2 | "I predict ___ from ___": one sentence |
| 3 | Task type: regression / binary / multiclass |
| 4 | The target ratio and the BASELINE number |
| 5 | Did you split? Do you need stratify? |
| 6 | The dummy result |
| 7 | Which columns you dropped, and WHY |
| 8 | What you did about missing values and types |
| 9 | Your first honest result |
| 10 | Which metric you chose, and WHY |
| 11 | Which group the mistakes cluster in |
| 12 | Which ONE change you made, and the result |
And most important, question 13:
Which of your hypotheses was REJECTED?
If not a single one was rejected, you didn't test anything. Go back.
Warnings, but not the solution
Binary metrics don't work
precision_score(y_te, pred) raises an error with three classes:
ValueError: Target is multiclass but average='binary'
You need the average= parameter. Which do you choose: macro, micro or weighted? Reread Chapters 8 and 9. The classes are 152 / 124 / 68, which is not an even split.
The sex column has 11 missing values
It's a text column. SimpleImputer(strategy="median") doesn't work with text. What do you do?
There are three ways and each has a price. Choose one and write down why you chose it.
Where your first suspicion should be
Look at the island column. groupby it against the target. What do you see?
It isn't leakage, but it tells you something important about the data.
Checking yourself
Work it out yourself first. Then look at these numbers.
from sklearn.model_selection import train_test_split, cross_val_score from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression from sklearn.dummy import DummyClassifier from sklearn.metrics import accuracy_score, f1_score, confusion_matrix, classification_report num = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"] cat = ["island", "sex"] X, y = pg[num + cat], pg["species"] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) baseline = accuracy_score(y_te, DummyClassifier(strategy="most_frequent").fit(X_tr[num], y_tr).predict(X_te[num])) pipe = Pipeline([ ("prep", ColumnTransformer([ ("num", Pipeline([("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler())]), num), ("cat", Pipeline([("impute", SimpleImputer(strategy="most_frequent")), ("encode", OneHotEncoder(handle_unknown="ignore"))]), cat), ])), ("model", LogisticRegression(max_iter=3000)), ]).fit(X_tr, y_tr) b = pipe.predict(X_te) print(f"BASELINE (most_frequent): {baseline:.4f}") print(f"Model accuracy : {accuracy_score(y_te, b):.4f}") print(f"Model macro F1 : {f1_score(y_te, b, average='macro'):.4f}") print(f"Model weighted F1 : {f1_score(y_te, b, average='weighted'):.4f}") cv = cross_val_score(pipe, X, y, cv=5, scoring="f1_macro") print(f"CV macro F1 : {cv.mean():.4f} ±{cv.std():.4f}")
BASELINE (most_frequent): 0.4423 Model accuracy : 0.9904 Model macro F1 : 0.9918 Model weighted F1 : 0.9904 CV macro F1 : 0.9939 ±0.0076
print("The 3x3 confusion matrix:") classes = sorted(y.unique()) cm = confusion_matrix(y_te, b, labels=classes) print(pd.DataFrame(cm, index=[f"truth: {s}" for s in classes], columns=[f"predicted: {s}" for s in classes]).to_string()) print() print(classification_report(y_te, b, digits=4))
The 3x3 confusion matrix:
predicted: Adelie predicted: Chinstrap predicted: Gentoo
truth: Adelie 46 0 0
truth: Chinstrap 0 21 0
truth: Gentoo 1 0 36
precision recall f1-score support
Adelie 0.9787 1.0000 0.9892 46
Chinstrap 1.0000 1.0000 1.0000 21
Gentoo 1.0000 0.9730 0.9863 37
accuracy 0.9904 104
macro avg 0.9929 0.9910 0.9918 104
weighted avg 0.9906 0.9904 0.9904 104
print("The island column against the target:") print(pd.crosstab(pg.island, pg.species).to_string())
The island column against the target: species Adelie Chinstrap Gentoo island Biscoe 44 0 124 Dream 56 68 0 Torgersen 52 0 0
Look at the last table. Only Adelie live on Torgersen island. Gentoo dominate on Biscoe. That isn't leakage, it's real geography. So island is a useful feature, and it shouldn't be dropped.
What you learned
If your result came out very high, don't be surprised. The penguins dataset is easy: the classes separate almost completely by bill and flipper measurements.
That's a lesson too: on an easy dataset every model does well. To compare models you need a hard dataset. The next exercise is harder.
Questions about this chapter?
The course, homework review and the other five books are in Uzbek. Questions in English are welcome on Telegram.