Train / validation / test va leakage
Sahna: notebookda 0.95, serverda 0.61
Tasavvur qiling. Siz ikki hafta ishladingiz. Model notebookda 0.95 beradi. Menejerga ko'rsatdingiz, u xursand. Modelni serverga chiqardingiz.
Bir oydan keyin hisobot keladi: haqiqiy aniqlik 0.61.
Nima bo'ldi? Model yomonlashmadi — u hech qachon 0.95 bo'lmagan edi. Siz uni imtihon savollarini ko'rsatib sinagan edingiz.
Bu bob shuni qanday oldini olishni o'rgatadi. Bu — 0-bobdagi protokolning 5-qadami.
Nega bo'lish kerak
Model ikki xil ishni qila oladi:
- Yodlash — ko'rgan qatorlarni eslab qolish
- Umumlashtirish — qoida topish va yangi qatorga qo'llash
Sizga ikkinchisi kerak. Lekin model ko'rgan ma'lumotda sinalsa, ikkisini ajratib bo'lmaydi.
Shuning uchun ma'lumot bo'linadi: bir qismida o'rganadi, boshqa qismida sinaladi.
import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv(DATA / "uy_ijara.csv") X = df.drop(columns=["ijaraga_berildi_30k", "narx_usd", "narx_matn", "elon_id", "agent_komissiyasi"]) y = df["ijaraga_berildi_30k"] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) print(f"jami : {len(X)} qator") print(f"train : {len(X_tr)} qator ({len(X_tr)/len(X):.0%}) -> model SHU YERDA o'rganadi") print(f"test : {len(X_te)} qator ({len(X_te)/len(X):.0%}) -> model BUNI KO'RMAYDI")
jami : 1200 qator train : 840 qator (70%) -> model SHU YERDA o'rganadi test : 360 qator (30%) -> model BUNI KO'RMAYDI
stratify — unutilsa raqamlar yolg'on bo'ladi
stratify=y train va test da sinf nisbatini bir xil saqlaydi. Busiz tasodif aralashadi.
import numpy as np print(f"{'random_state':>13} {'stratify YO`Q':>14} {'stratify BOR':>14}") print("-" * 45) for rs in [0, 1, 42, 7, 99]: _, _, _, y1 = train_test_split(X, y, test_size=0.3, random_state=rs) _, _, _, y2 = train_test_split(X, y, test_size=0.3, random_state=rs, stratify=y) print(f"{rs:>13} {y1.mean():>13.1%} {y2.mean():>14.1%}") print() print(f"Haqiqiy nisbat: {y.mean():.1%}")
random_state stratify YO`Q stratify BOR
---------------------------------------------
0 49.2% 50.6%
1 52.5% 50.6%
42 48.9% 50.6%
7 50.0% 50.6%
99 48.3% 50.6%
Haqiqiy nisbat: 50.7%
stratify bilan har safar bir xil nisbat. Busiz nisbat sakraydi — va modelingiz baholari qaysi bo'linish tushganiga bog'liq bo'lib qoladi.
Sinflar nomutanosib bo'lsa bu halokatli. Agar sizda 2% fraud bo'lsa va test to'plamiga tasodifan 0% fraud tushsa, recall ni o'lchab ham bo'lmaydi.
Classification da `stratify=y` — tanlov emas, majburiyat
Yozib qo'ying: train_test_split(X, y, test_size=0.3, random_state=42, stratify=y).
stratify ni faqat regression da tashlab ketasiz (target uzluksiz bo'lganda).
Leakage: eng qimmat xato
Endi eng muhim qismi. Leakage — test ma'lumoti o'rganish jarayoniga sizib kirishi.
Uning xavfliligi: xato bermaydi. Kod ishlaydi, raqam chiroyli chiqadi.
Keling, uni o'lchab ko'rsataman. Ikki xil yo'l bilan bir xil ishni qilamiz.
from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.impute import SimpleImputer from sklearn.metrics import accuracy_score son = ["xonalar", "maydon_m2", "qavat", "qavatlar_soni", "metro_km", "mebel", "qurilgan_yil"] # ===== NOTO'G'RI: scaler BUTUN ma'lumotda o'rganadi ===== toldir_yomon = SimpleImputer(strategy="median").fit(X[son]) # <-- butun X shkala_yomon = StandardScaler().fit(toldir_yomon.transform(X[son])) # <-- butun X Xs = shkala_yomon.transform(toldir_yomon.transform(X[son])) Xs = pd.DataFrame(Xs, columns=son, index=X.index) m_yomon = LogisticRegression(max_iter=2000).fit(Xs.loc[X_tr.index], y_tr) a_yomon = accuracy_score(y_te, m_yomon.predict(Xs.loc[X_te.index])) # ===== TO'G'RI: scaler FAQAT train da o'rganadi ===== toldir = SimpleImputer(strategy="median").fit(X_tr[son]) shkala = StandardScaler().fit(toldir.transform(X_tr[son])) m_yaxshi = LogisticRegression(max_iter=2000).fit(shkala.transform(toldir.transform(X_tr[son])), y_tr) a_yaxshi = accuracy_score(y_te, m_yaxshi.predict(shkala.transform(toldir.transform(X_te[son])))) print(f"NOTO'G'RI (scaler butun ma'lumotda) : {a_yomon:.4f}") print(f"TO'G'RI (scaler faqat train da) : {a_yaxshi:.4f}") print(f"Farq : {a_yomon - a_yaxshi:+.4f}")
NOTO'G'RI (scaler butun ma'lumotda) : 0.7694 TO'G'RI (scaler faqat train da) : 0.7694 Farq : +0.0000
Farqga qarang. U juda kichik.
Va mana shu — leakage ning eng xavfli tomoni. Bu misolda StandardScaler faqat o'rtacha va standart og'ishni "ko'rdi", shuning uchun zarar sezilmas darajada. Siz "demak muhim emas" degan xulosaga kelishingiz mumkin.
Kelmang. Bu yerda zarar kichik, chunki tozalash oddiy. Lekin:
show(pd.DataFrame([ {"Qadam": "StandardScaler", "Nimani 'ko'radi'": "O'rtacha va standart og'ish", "Zarari": "Kichik — bu misolda deyarli sezilmaydi"}, {"Qadam": "SimpleImputer(median)", "Nimani 'ko'radi'": "Test qatorlarining medianasi", "Zarari": "Kichik-o'rtacha"}, {"Qadam": "Feature selection (butun ma'lumotda)", "Nimani 'ko'radi'": "Qaysi ustun target bilan bog'liq", "Zarari": "KATTA — eng ko'p uchraydigan halokat"}, {"Qadam": "Target encoding (butun ma'lumotda)", "Nimani 'ko'radi'": "Har kategoriyaning target o'rtachasi", "Zarari": "HALOKATLI — 0.99 beradi, serverda yiqiladi"}, {"Qadam": "SMOTE / oversampling (split dan oldin)", "Nimani 'ko'radi'": "Test qatorlarining nusxalari train ga tushadi", "Zarari": "HALOKATLI"}, ]))
| Qadam | Nimani 'ko'radi' | Zarari |
|---|---|---|
| StandardScaler | O'rtacha va standart og'ish | Kichik — bu misolda deyarli sezilmaydi |
| SimpleImputer(median) | Test qatorlarining medianasi | Kichik-o'rtacha |
| Feature selection (butun ma'lumotda) | Qaysi ustun target bilan bog'liq | KATTA — eng ko'p uchraydigan halokat |
| Target encoding (butun ma'lumotda) | Har kategoriyaning target o'rtachasi | HALOKATLI — 0.99 beradi, serverda yiqiladi |
| SMOTE / oversampling (split dan oldin) | Test qatorlarining nusxalari train ga tushadi | HALOKATLI |
Oxirgi uchtasi notebookda 0.99 beradi va serverda yiqiladi. Shuning uchun qoida istisnosiz bo'lishi kerak: har qanday fit() faqat train da.
Yechim: Pipeline — qoidani tuzilma bilan majburlash
Qoidani eslab qolish qiyin. Shuning uchun uni unutish mumkin bo'lmaydigan qilib qo'yamiz.
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder quvur = Pipeline([ ("tayyorla", ColumnTransformer([ ("son", Pipeline([("toldir", SimpleImputer(strategy="median")), ("shkala", StandardScaler())]), son), ("matn", OneHotEncoder(handle_unknown="ignore"), ["tuman"]), ])), ("model", LogisticRegression(max_iter=2000)), ]) quvur.fit(X_tr, y_tr) print(f"Pipeline natijasi: {accuracy_score(y_te, quvur.predict(X_te)):.4f}") print() print("Nega bu xavfsiz:") print(" quvur.fit(X_tr, y_tr) -> ICHKI hamma qadam FAQAT X_tr ni ko'radi") print(" quvur.predict(X_te) -> faqat transform, qayta fit YO'Q")
Pipeline natijasi: 0.7778 Nega bu xavfsiz: quvur.fit(X_tr, y_tr) -> ICHKI hamma qadam FAQAT X_tr ni ko'radi quvur.predict(X_te) -> faqat transform, qayta fit YO'Q
Siz endi adashishingiz mumkin emas — fit chaqirilganda butun zanjir faqat berilgan ma'lumotni ko'radi.
Bitta qoida, butun kitob uchun
Agar fit, fit_transform yoki fit_resample ni split dan oldin chaqirsangiz — bu leakage.
Shu bitta jumlani eslab qolsangiz, ML dagi eng qimmat xatodan qutulasiz.
Uchinchi to'plam: validation
Hozirgacha ikki qism ishlatdik. Lekin amalda uchta kerak.
Nega? Chunki siz modelni bir marta sinamaysiz. Siz o'nlab marta sinaysiz: boshqa model, boshqa feature, boshqa parametr. Har safar test ga qarab qaror qabul qilsangiz — siz o'zingiz test ma'lumotini modelga sizdirgan bo'lasiz.
show(pd.DataFrame([ {"To'plam": "train", "Ulushi": "~60%", "Nima uchun": "Model shu yerda o'rganadi", "Necha marta ishlatiladi": "Cheksiz"}, {"To'plam": "validation", "Ulushi": "~20%", "Nima uchun": "Model va parametr TANLASH", "Necha marta ishlatiladi": "Cheksiz — qaror shu yerda"}, {"To'plam": "test", "Ulushi": "~20%", "Nima uchun": "YAKUNIY baho", "Necha marta ishlatiladi": "BIR MARTA, eng oxirida"}, ]))
| To'plam | Ulushi | Nima uchun | Necha marta ishlatiladi |
|---|---|---|---|
| train | ~60% | Model shu yerda o'rganadi | Cheksiz |
| validation | ~20% | Model va parametr TANLASH | Cheksiz — qaror shu yerda |
| test | ~20% | YAKUNIY baho | BIR MARTA, eng oxirida |
# Ikki bosqichda bo'lamiz: avval test ni ajratamiz, keyin qolganini bo'lamiz X_qolgan, X_test, y_qolgan, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) X_train, X_val, y_train, y_val = train_test_split( X_qolgan, y_qolgan, test_size=0.25, random_state=42, stratify=y_qolgan) print(f"train : {len(X_train):4d} qator ({len(X_train)/len(X):.0%})") print(f"validation : {len(X_val):4d} qator ({len(X_val)/len(X):.0%})") print(f"test : {len(X_test):4d} qator ({len(X_test)/len(X):.0%}) <-- SEYFDA")
train : 720 qator (60%) validation : 240 qator (20%) test : 240 qator (20%) <-- SEYFDA
Amalda esa ko'pincha validation o'rniga cross-validation ishlatiladi — u ma'lumotni tejaydi va barqarorroq baho beradi. 9-bob shu haqda.
Test to'plamiga qancha marta qaradingiz?
Agar siz "test da 0.79 chiqdi, keling boshqa modelni sinab ko'ramiz" deb o'ylagan bo'lsangiz — siz test ni validation sifatida ishlatdingiz. Endi uning bahosi optimistik.
Kaggle musobaqalarida buning nomi bor: leaderboard overfitting. Odamlar public leaderboard ga qarab yuzlab marta model o'zgartiradi va private leaderboard da yiqiladi.
Vaqtli ma'lumot: aralashtirish taqiqlanadi
Agar ma'lumotingizda vaqt bo'lsa (savdo, narx, log), train_test_split ni tasodifiy ishlatish — kelajakda o'rganib o'tmishni bashorat qilish degani.
kunlar = pd.date_range("2026-01-01", periods=10, freq="D") print("TASODIFIY bo'linish (NOTO'G'RI):") print(" train: 1, 3, 4, 7, 9, 10-kun") print(" test : 2, 5, 6, 8-kun <-- train kelajakni ko'rgan") print() print("VAQT bo'yicha bo'linish (TO'G'RI):") print(" train: 1-7-kun") print(" test : 8-10-kun <-- faqat o'tmishdan o'rganadi") print() print("sklearn da: TimeSeriesSplit (9-bobda)")
TASODIFIY bo'linish (NOTO'G'RI): train: 1, 3, 4, 7, 9, 10-kun test : 2, 5, 6, 8-kun <-- train kelajakni ko'rgan VAQT bo'yicha bo'linish (TO'G'RI): train: 1-7-kun test : 8-10-kun <-- faqat o'tmishdan o'rganadi sklearn da: TimeSeriesSplit (9-bobda)
Bugun nima o'rgandik
show(pd.DataFrame([ {"Qoida": "Split birinchi", "Batafsil": "Tozalashdan, grafikdan, hamma narsadan OLDIN"}, {"Qoida": "stratify=y", "Batafsil": "Classification da majburiy"}, {"Qoida": "fit faqat train da", "Batafsil": "Scaler, imputer, encoder, selector — hammasi"}, {"Qoida": "Pipeline ishlat", "Batafsil": "Qoidani eslab qolish emas, tuzilma bilan majburla"}, {"Qoida": "Test bir marta", "Batafsil": "Qaror validation yoki CV da qabul qilinadi"}, {"Qoida": "Vaqt bo'lsa aralashtirmaslik", "Batafsil": "TimeSeriesSplit"}, ]))
| Qoida | Batafsil |
|---|---|
| Split birinchi | Tozalashdan, grafikdan, hamma narsadan OLDIN |
| stratify=y | Classification da majburiy |
| fit faqat train da | Scaler, imputer, encoder, selector — hammasi |
| Pipeline ishlat | Qoidani eslab qolish emas, tuzilma bilan majburla |
| Test bir marta | Qaror validation yoki CV da qabul qilinadi |
| Vaqt bo'lsa aralashtirmaslik | TimeSeriesSplit |
Keyingi bobda: model qurdik, lekin natijasi yaxshi ekanini qanday bilamiz? Buning uchun chegara raqami kerak.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.