Asosiy qismga o'tish

Train / validation / test va leakage

6-bob 9 kod bloki 2060 so'z

Bu bobdagi kod shu fayllar bilan ishlaydio'z kompyuteringizda ishga tushiring

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:

  1. Yodlash — ko'rgan qatorlarni eslab qolish
  2. 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.

python11 qator
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")
Chiqish
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.

python10 qator
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%}")
Chiqish
 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.

python24 qator
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}")
Chiqish
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:

python12 qator
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"},
]))
QadamNimani 'ko'radi'Zarari
StandardScalerO'rtacha va standart og'ishKichik — bu misolda deyarli sezilmaydi
SimpleImputer(median)Test qatorlarining medianasiKichik-o'rtacha
Feature selection (butun ma'lumotda)Qaysi ustun target bilan bog'liqKATTA — eng ko'p uchraydigan halokat
Target encoding (butun ma'lumotda)Har kategoriyaning target o'rtachasiHALOKATLI — 0.99 beradi, serverda yiqiladi
SMOTE / oversampling (split dan oldin)Test qatorlarining nusxalari train ga tushadiHALOKATLI

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.

python19 qator
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")
Chiqish
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.

python8 qator
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'plamUlushiNima uchunNecha marta ishlatiladi
train~60%Model shu yerda o'rganadiCheksiz
validation~20%Model va parametr TANLASHCheksiz — qaror shu yerda
test~20%YAKUNIY bahoBIR MARTA, eng oxirida
python9 qator
# 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")
Chiqish
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.

python10 qator
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)")
Chiqish
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

python8 qator
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"},
]))
QoidaBatafsil
Split birinchiTozalashdan, grafikdan, hamma narsadan OLDIN
stratify=yClassification da majburiy
fit faqat train daScaler, imputer, encoder, selector — hammasi
Pipeline ishlatQoidani eslab qolish emas, tuzilma bilan majburla
Test bir martaQaror validation yoki CV da qabul qilinadi
Vaqt bo'lsa aralashtirmaslikTimeSeriesSplit

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.

Telegramda yozish Narxlar