Skip to content

Exercise 3: hidden leakage

Chapter 45 8 code blocks 1955 words

The code in this chapter uses these filesrun it on your own machine

Why this exercise

In Chapter 1 I showed you the agent_komissiyasi column and said "this is leakage". That was easy: its name and its meaning were both obvious.

In this exercise I tell you nothing.

The file contains leakage. It looks like an ID column, and you know the rule "drop IDs", so you'll drop it. But the question is different: why is it leakage, and how will you find this kind of trap next time?

The file also has a second suspicious column. It is not leakage: it's a real signal. Telling the two apart is the main work of this exercise.

Column names

This synthetic student dataset has Uzbek column names. talaba_id student id, kirish_bali entrance score, davomat_foiz attendance %, uy_vazifa_bali homework score, ishlaydi has a job (0/1), masofa_km distance from home in km, oxirgi_tolov_oyi the last month the fee was paid, diplom_seriya diploma serial number. The target bitirdi means graduated.

Getting to know the data

python8 lines
import pandas as pd

td = pd.read_csv(DATA / "talaba_bitirdi.csv", keep_default_na=False, na_values=[""])
print("shape:", td.shape)
print()
print(td.dtypes.to_string())
print()
show(td.head(8))
Output
shape: (900, 9)

talaba_id            object
kirish_bali           int64
davomat_foiz          int64
uy_vazifa_bali        int64
ishlaydi              int64
masofa_km           float64
oxirgi_tolov_oyi      int64
diplom_seriya        object
bitirdi               int64
talaba_idkirish_balidavomat_foizuy_vazifa_baliishlaydimasofa_kmoxirgi_tolov_oyidiplom_seriyabitirdi
T-5000062938203.610D-2000001
T-50001661006907.210D-2000011
T-50002581004212.49D-2000021
T-50003507510011610D-2000031
T-5000456885505.78D-2000041
T-5000548807407.512D-2000051
T-50006637974025.78D-2000061
T-5000781989516.614D-2000071
python8 lines
print("Target:")
print(td.bitirdi.value_counts(normalize=True).sort_index().round(4).to_string())
print()
print(f"BASELINE (saying '1' to everyone): {td.bitirdi.mean():.4f}")
print()
missing = td.isna().sum()
print("Missing values:")
print(missing[missing > 0].to_string() if (missing > 0).any() else "  none")
Output
Target:
bitirdi
0    0.1144
1    0.8856

BASELINE (saying '1' to everyone): 0.8856

Missing values:
diplom_seriya    103

The task

Walk the 12 steps from Chapter 1 yourself. Nobody shows you the way. Write the following in your notebook:

python15 lines
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
1How many rows, how many columns
2"I predict ___ from ___": one sentence
3Task type: regression / binary / multiclass
4The target ratio and the BASELINE number
5Did you split? Do you need stratify?
6The dummy result
7Which columns you dropped, and WHY
8What you did about missing values and types
9Your first honest result
10Which metric you chose, and WHY
11Which group the mistakes cluster in
12Which 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.

The one hint

I won't help any further. Just one thing:

Ask this question of every column

"Will I have this column AT PREDICTION TIME?"

A new student has just enrolled. Which columns are known about them right now? Which only appear later?

This question finds leakage without looking at the column's name.

The baseline is 0.8852: accuracy will fool you

The target is 88.6% "graduated". So a model that says "graduated to everyone" gets ~0.885.

If your model scores 0.89, that means almost nothing. You need another metric. Which one? Reread Chapters 7 and 9.

Checking yourself

Work it out yourself first. Really. Then look below.

python17 lines
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, f1_score, recall_score, precision_score

honest = ["kirish_bali", "davomat_foiz", "uy_vazifa_bali", "ishlaydi", "masofa_km"]
y = td["bitirdi"]
tr, te, ytr, yte = train_test_split(td, y, test_size=0.3, random_state=42, stratify=y)

baseline = accuracy_score(yte, DummyClassifier(strategy="most_frequent").fit(tr[honest], ytr).predict(te[honest]))
m1 = LogisticRegression(max_iter=2000).fit(tr[honest], ytr)
b1 = m1.predict(te[honest])

print(f"BASELINE                    : {baseline:.4f}")
print(f"5 honest columns: accuracy  : {accuracy_score(yte, b1):.4f}   (gap {accuracy_score(yte, b1)-baseline:+.4f})")
print(f"5 honest columns: F1        : {f1_score(yte, b1):.4f}")
print(f"5 honest columns: recall    : {recall_score(yte, b1):.4f}")
Output
BASELINE                    : 0.8852
5 honest columns: accuracy  : 0.8963   (gap +0.0111)
5 honest columns: F1        : 0.9447
5 honest columns: recall    : 1.0000

There's the first lesson: the model is only slightly above the baseline. Accuracy is almost useless here.

Now let's open the trap.

python10 lines
td2 = td.copy()
td2["has_diploma"] = td2.diplom_seriya.notna().astype(int)
tr2, te2 = td2.loc[tr.index], td2.loc[te.index]

m2 = LogisticRegression(max_iter=2000).fit(tr2[honest + ["has_diploma"]], ytr)
b2 = m2.predict(te2[honest + ["has_diploma"]])
print(f"+ diplom_seriya (present/absent): accuracy {accuracy_score(yte, b2):.4f}   <-- PERFECT")
print()
print("Why? Let's check:")
print(pd.crosstab(td2.has_diploma, td2.bitirdi).to_string())
Output
+ diplom_seriya (present/absent): accuracy 1.0000   <-- PERFECT

Why? Let's check:
bitirdi        0    1
has_diploma          
0            103    0
1              0  797

There it is. diplom_seriya exists only for the students who graduated.

Notice: the problem is not that it's an ID. The problem is that whether it's empty or not tells you the target. Whether you feed it to OneHotEncoder or apply notna(), the result is the same lie.

And logically it's obvious too: a diploma serial number is issued AFTER graduating. At prediction time it doesn't exist.

Now the second suspicious column.

python6 lines
m3 = LogisticRegression(max_iter=2000).fit(tr[honest + ["oxirgi_tolov_oyi"]], ytr)
b3 = m3.predict(te[honest + ["oxirgi_tolov_oyi"]])
print(f"+ oxirgi_tolov_oyi: accuracy {accuracy_score(yte, b3):.4f}  F1 {f1_score(yte, b3):.4f}")
print()
print("Means by group:")
print(td.groupby("bitirdi").oxirgi_tolov_oyi.agg(["mean", "min", "max"]).round(2).to_string())
Output
+ oxirgi_tolov_oyi: accuracy 0.9037  F1 0.9465

Means by group:
          mean  min  max
bitirdi                 
0         7.71    2   14
1        11.05    6   14

This column is related to the target too: those who graduated paid for more months. But it is not leakage. See the difference:

python10 lines
show(pd.DataFrame([
    {"Column": "diplom_seriya",
     "Related to the target": "Completely (100%)",
     "Available at prediction time": "NO: issued after graduating",
     "Verdict": "LEAKAGE: drop it"},
    {"Column": "oxirgi_tolov_oyi",
     "Related to the target": "Partly",
     "Available at prediction time": "YES: known every month",
     "Verdict": "A real signal: use it (but carefully)"},
]))
ColumnRelated to the targetAvailable at prediction timeVerdict
diplom_seriyaCompletely (100%)NO: issued after graduatingLEAKAGE: drop it
oxirgi_tolov_oyiPartlyYES: known every monthA real signal: use it (but carefully)

"Related to the target" and "leakage" are NOT the same thing

If you adopt the rule "drop every column strongly related to the target", you'll throw away your most useful features. The whole point of building a model is to find columns like that.

The question that separates leakage is not the strength of the relationship. The question is time: does this information exist at prediction time?

oxirgi_tolov_oyi has a point to be careful about: if you predict at the end of the semester, it's honest. If you want to predict on a student's first day, it's leakage too, because there's no payment history yet.

So leakage depends on the question, not on the column.

After the three exercises

python8 lines
show(pd.DataFrame([
    {"Exercise": "43. penguins", "What it tests": "A broken pattern: from binary to multiclass",
     "Main lesson": "average='macro' and a 3x3 matrix"},
    {"Exercise": "44. dirty sales", "What it tests": "Cleaning is 80% of the work",
     "Main lesson": "Cyrillic/Latin, text-as-number, duplicates; and the honest 'no model is possible'"},
    {"Exercise": "45. hidden leakage", "What it tests": "Finding the trap yourself",
     "Main lesson": "TIME separates leakage, not the strength of the relationship"},
]))
ExerciseWhat it testsMain lesson
43. penguinsA broken pattern: from binary to multiclassaverage='macro' and a 3x3 matrix
44. dirty salesCleaning is 80% of the workCyrillic/Latin, text-as-number, duplicates; and the honest 'no model is possible'
45. hidden leakageFinding the trap yourselfTIME separates leakage, not the strength of the relationship

If you walked all three exercises yourself, you're now someone who doesn't freeze in front of a new dataset.

Remember the protocol. Measure the baseline first. Be suspicious of a perfect result. Read the mistakes. And write down every rejected hypothesis: they are your real knowledge.

Questions about this chapter?

The course, homework review and the other five books are in Uzbek. Questions in English are welcome on Telegram.

Write on Telegram Contents