Decision tree (CART)
Sahna: halqani ajratish
15-bobda chiziqli model halqa shaklidagi ma'lumotda yiqildi — aniqlik 0.5350, ya'ni tanga tashlash darajasi. Daraxt esa 0.9883 berdi.
Nega? Chunki daraxt butunlay boshqa g'oya bilan ishlaydi.
Chiziqli modelda koeffitsiyent bor, gradient descent bor. Daraxtda ikkisi ham yo'q. Faqat savollar bor:
"Maydon 60 m2 dan kattami? Ha -> o'ngga. Yo'q -> chapga."
Shu savollarni ketma-ket berib, javobga yetib boradi.
Birinchi daraxt
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, cross_val_score from sklearn.tree import DecisionTreeClassifier, export_text df = pd.read_csv(DATA / "uy_ijara.csv") son = ["xonalar", "maydon_m2", "qavat", "qavatlar_soni", "metro_km", "mebel"] X, y = df[son], 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) daraxt = DecisionTreeClassifier(max_depth=3, random_state=42).fit(X_tr, y_tr) print(export_text(daraxt, feature_names=son, decimals=1))
|--- maydon_m2 <= 58.8 | |--- maydon_m2 <= 36.0 | | |--- maydon_m2 <= 28.7 | | | |--- class: 1 | | |--- maydon_m2 > 28.7 | | | |--- class: 1 | |--- maydon_m2 > 36.0 | | |--- qavat <= 13.5 | | | |--- class: 1 | | |--- qavat > 13.5 | | | |--- class: 0 |--- maydon_m2 > 58.8 | |--- maydon_m2 <= 77.2 | | |--- metro_km <= 0.1 | | | |--- class: 0 | | |--- metro_km > 0.1 | | | |--- class: 0 | |--- maydon_m2 > 77.2 | | |--- metro_km <= 0.4 | | | |--- class: 0 | | |--- metro_km > 0.4 | | | |--- class: 0
Yuqoridagi matn — butun model. Boshqa hech narsa yo'q: koeffitsiyent yo'q, vazn yo'q.
Siz bu daraxtni qog'ozga chizib, qo'lda ham bashorat qilishingiz mumkin. Hech bir boshqa model bunday ochiq emas.
Nega bu amalda qimmatli
Bank kredit bermasa, mijoz "nega?" deb so'raydi. Daraxt bilan javob aniq: "daromadingiz X dan past va qarzingiz Y dan yuqori".
Logistic regression bilan javob "koeffitsiyentlar yig'indisi chegaradan past" bo'ladi — bu mijozga tushunarsiz va yuridik jihatdan muammoli.
Qanday bo'linadi: Gini va entropiya
Daraxt har qadamda eng yaxshi savolni izlaydi. "Eng yaxshi" nima degani?
Javob: bo'lingandan keyin guruhlar eng toza bo'lsin.
def gini(y): if len(y) == 0: return 0.0 p = np.bincount(y, minlength=2) / len(y) return float(1 - np.sum(p ** 2)) def entropiya(y): if len(y) == 0: return 0.0 p = np.bincount(y, minlength=2) / len(y) p = p[p > 0] return float(-np.sum(p * np.log2(p))) print(f"{'holat':<34} {'Gini':>8} {'Entropiya':>11}") print("-" * 55) for nom, arr in [ ("Toza: hammasi 1 (10 ta)", np.ones(10, int)), ("Toza: hammasi 0 (10 ta)", np.zeros(10, int)), ("Deyarli toza: 9 ta 1, 1 ta 0", np.r_[np.ones(9, int), np.zeros(1, int)]), ("Yarmi-yarmi: 5 ta 1, 5 ta 0", np.r_[np.ones(5, int), np.zeros(5, int)]), ]: print(f"{nom:<34} {gini(arr):>8.4f} {entropiya(arr):>11.4f}")
holat Gini Entropiya ------------------------------------------------------- Toza: hammasi 1 (10 ta) 0.0000 -0.0000 Toza: hammasi 0 (10 ta) 0.0000 -0.0000 Deyarli toza: 9 ta 1, 1 ta 0 0.1800 0.4690 Yarmi-yarmi: 5 ta 1, 5 ta 0 0.5000 1.0000
Ikkisi ham nopoklikni o'lchaydi: 0 = toza guruh, maksimum = teng aralash.
Daraxt hamma mumkin bo'lgan savolni sinab, nopoklikni eng ko'p kamaytiradiganini tanlaydi.
def eng_yaxshi_bolinish(X, y, ustun): qiymatlar = np.unique(X[ustun]) chegaralar = (qiymatlar[:-1] + qiymatlar[1:]) / 2 eng, eng_ch = -1, None boshlangich = gini(y.to_numpy()) for ch in chegaralar: chap = y[X[ustun] <= ch].to_numpy() ong = y[X[ustun] > ch].to_numpy() if len(chap) == 0 or len(ong) == 0: continue vaznli = (len(chap) * gini(chap) + len(ong) * gini(ong)) / len(y) foyda = boshlangich - vaznli if foyda > eng: eng, eng_ch = foyda, ch return eng_ch, eng print("Har bir ustun uchun ENG YAXSHI savol (Gini foydasi bo'yicha):") natija = [] for u in son: ch, foyda = eng_yaxshi_bolinish(X_tr, y_tr, u) natija.append({"ustun": u, "eng yaxshi chegara": round(float(ch), 2), "Gini foydasi": round(foyda, 5)}) show(pd.DataFrame(natija).sort_values("Gini foydasi", ascending=False))
Har bir ustun uchun ENG YAXSHI savol (Gini foydasi bo'yicha):
| ustun | eng yaxshi chegara | Gini foydasi |
|---|---|---|
| maydon_m2 | 58.75 | 0.076 |
| xonalar | 2.5 | 0.074 |
| metro_km | 7.22 | 0.002 |
| qavatlar_soni | 13.5 | 0.002 |
| qavat | 4.5 | 0.002 |
| mebel | 0.5 | 0 |
Eng katta foyda beradigan ustun — daraxtning birinchi savoli bo'ladi. Yuqoridagi export_text natijasi bilan solishtiring: mos keladi.
show(pd.DataFrame([ {"Mezon": "gini", "Formula": "1 - sum(p^2)", "Tezlik": "Tezroq", "sklearn da": "SUKUT BO'YICHA", "Amalda": "Deyarli har doim shu"}, {"Mezon": "entropy", "Formula": "-sum(p * log2(p))", "Tezlik": "Sekinroq (log hisobi)", "sklearn da": "criterion='entropy'", "Amalda": "Natija deyarli bir xil"}, ]))
| Mezon | Formula | Tezlik | sklearn da | Amalda |
|---|---|---|---|---|
| gini | 1 - sum(p^2) | Tezroq | SUKUT BO'YICHA | Deyarli har doim shu |
| entropy | -sum(p * log2(p)) | Sekinroq (log hisobi) | criterion='entropy' | Natija deyarli bir xil |
print("Ikki mezon o'lchab solishtiriladi:") for mezon in ["gini", "entropy"]: b = cross_val_score(DecisionTreeClassifier(criterion=mezon, max_depth=6, random_state=42), X, y, cv=5, scoring="f1") print(f" criterion={mezon:<8} F1 = {b.mean():.4f} ±{b.std():.4f}") print() print("Farq odatda ahamiyatsiz — shuning uchun sukut qiymatni o'zgartirmaysiz.")
Ikki mezon o'lchab solishtiriladi: criterion=gini F1 = 0.6794 ±0.0179 criterion=entropy F1 = 0.7021 ±0.0191 Farq odatda ahamiyatsiz — shuning uchun sukut qiymatni o'zgartirmaysiz.
Overfitting: daraxtning asosiy kasalligi
Daraxt cheklanmasa, har bir qatorni yodlab oladi.
print(f"{'max_depth':>10} {'train F1':>10} {'test F1':>9} {'farq':>8} {'barglar':>9}") print("-" * 52) from sklearn.metrics import f1_score for d in [2, 3, 5, 8, 15, None]: m = DecisionTreeClassifier(max_depth=d, random_state=42).fit(X_tr, y_tr) tr_f = f1_score(y_tr, m.predict(X_tr)) te_f = f1_score(y_te, m.predict(X_te)) belgi = " <-- YODLADI" if tr_f - te_f > 0.2 else "" print(f"{str(d):>10} {tr_f:>10.4f} {te_f:>9.4f} {tr_f-te_f:>8.4f} {m.get_n_leaves():>9}{belgi}")
max_depth train F1 test F1 farq barglar
----------------------------------------------------
2 0.7098 0.7784 -0.0686 4
3 0.7138 0.7644 -0.0506 8
5 0.7275 0.7405 -0.0131 24
8 0.8049 0.6997 0.1051 65
15 0.9518 0.6114 0.3404 187 <-- YODLADI
None 1.0000 0.6264 0.3736 222 <-- YODLADI
Chegarasiz daraxt train da deyarli mukammal bo'ladi va test da qulab tushadi. Barglar soni esa yuzlab bo'ladi — ya'ni har barg bir-ikki qatorni "yodlagan".
Daraxtni cheklash: to'rtta tutqich
show(pd.DataFrame([ {"Parametr": "max_depth", "Nima qiladi": "Daraxt chuqurligi chegarasi", "Amaliy qiymat": "3-10"}, {"Parametr": "min_samples_leaf", "Nima qiladi": "Bargda kamida shuncha namuna bo'lishi shart", "Amaliy qiymat": "5-50"}, {"Parametr": "min_samples_split", "Nima qiladi": "Bo'linish uchun kamida shuncha namuna kerak", "Amaliy qiymat": "10-100"}, {"Parametr": "ccp_alpha", "Nima qiladi": "Pruning — o'sgandan keyin kesish", "Amaliy qiymat": "0.0-0.02"}, ]))
| Parametr | Nima qiladi | Amaliy qiymat |
|---|---|---|
| max_depth | Daraxt chuqurligi chegarasi | 3-10 |
| min_samples_leaf | Bargda kamida shuncha namuna bo'lishi shart | 5-50 |
| min_samples_split | Bo'linish uchun kamida shuncha namuna kerak | 10-100 |
| ccp_alpha | Pruning — o'sgandan keyin kesish | 0.0-0.02 |
print("min_samples_leaf eng foydali tutqich:") print(f"{'min_samples_leaf':>18} {'CV F1':>9} {'±':>8} {'barglar':>9}") print("-" * 48) for msl in [1, 5, 20, 50, 100]: m = DecisionTreeClassifier(min_samples_leaf=msl, random_state=42) b = cross_val_score(m, X, y, cv=5, scoring="f1") m.fit(X_tr, y_tr) print(f"{msl:>18} {b.mean():>9.4f} {b.std():>8.4f} {m.get_n_leaves():>9}")
min_samples_leaf eng foydali tutqich:
min_samples_leaf CV F1 ± barglar
------------------------------------------------
1 0.6504 0.0318 222
5 0.6203 0.0248 106
20 0.6703 0.0184 33
50 0.7058 0.0129 13
100 0.7296 0.0108 6
`max_depth` emas, `min_samples_leaf` ni sozlang
Talabalar odatda faqat max_depth ni o'zgartiradi. Lekin min_samples_leaf ko'pincha yaxshiroq ishlaydi, chunki u daraxtga kerakli joyda chuqur, keraksiz joyda sayoz bo'lishga ruxsat beradi.
max_depth esa hamma shoxni bir xil kesadi.
Xususiyat muhimligi — bepul sovg'a
m = DecisionTreeClassifier(max_depth=6, min_samples_leaf=20, random_state=42).fit(X_tr, y_tr) muhim = pd.DataFrame({"xususiyat": son, "muhimligi": m.feature_importances_.round(4)}) show(muhim.sort_values("muhimligi", ascending=False)) print() print(f"Yig'indi: {m.feature_importances_.sum():.4f} (har doim 1.0)")
Yig'indi: 1.0000 (har doim 1.0)
| xususiyat | muhimligi |
|---|---|
| maydon_m2 | 0.819 |
| metro_km | 0.142 |
| qavat | 0.03 |
| qavatlar_soni | 0.009 |
| xonalar | 0 |
| mebel | 0 |
`feature_importances_` ikki tomondan aldaydi
- Ko'p qiymatli ustunni ustun qo'yadi. Agar siz
elon_idni qoldirsangiz (0-bob), u eng "muhim" chiqadi — chunki u har qatorni ajratadi. - Bog'liq ustunlar orasida tasodifiy tanlaydi.
xonalarvamaydon_m2korrelyatsiyasi 0.94 — daraxt bittasini tanlaydi, ikkinchisining muhimligi 0 ga tushadi. Bu "ikkinchisi befoyda" degani emas.
Ishonchliroq usul — permutation importance yoki SHAP (25-bob).
from sklearn.inspection import permutation_importance pi = permutation_importance(m, X_te, y_te, n_repeats=20, random_state=42, scoring="f1") taqqos = pd.DataFrame({ "xususiyat": son, "daraxt ichidagi": m.feature_importances_.round(4), "permutation": pi.importances_mean.round(4), }).sort_values("permutation", ascending=False) show(taqqos)
| xususiyat | daraxt ichidagi | permutation |
|---|---|---|
| maydon_m2 | 0.819 | 0.205 |
| xonalar | 0 | 0 |
| qavat | 0.03 | 0 |
| qavatlar_soni | 0.009 | 0 |
| mebel | 0 | 0 |
| metro_km | 0.142 | -0.014 |
Ikki ustunni solishtiring — tartib farq qilishi mumkin. Permutation importance ishonchliroq, chunki u modelni haqiqatan sinab ko'radi: ustunni aralashtiradi va natija qancha yomonlashganini o'lchaydi.
Daraxtning kuchli va kuchsiz tomonlari
show(pd.DataFrame([ {"Xususiyat": "Scaling kerakmi", "Daraxt": "YO'Q — faqat 'kattami/kichikmi' deb so'raydi", "Chiziqli model": "Ha"}, {"Xususiyat": "Chiziqli bo'lmagan naqsh", "Daraxt": "Eplaydi (halqa: 0.9883)", "Chiziqli model": "Eplamaydi (0.5350)"}, {"Xususiyat": "Tushuntirish", "Daraxt": "Eng oson — chizib ko'rsatiladi", "Chiziqli model": "Koeffitsiyent orqali"}, {"Xususiyat": "Matn ustun", "Daraxt": "sklearn da ENCODING KERAK", "Chiziqli model": "Encoding kerak"}, {"Xususiyat": "Overfitting", "Daraxt": "JUDA moyil — cheklash shart", "Chiziqli model": "Kamroq moyil"}, {"Xususiyat": "Barqarorlik", "Daraxt": "Past — ma'lumot bir oz o'zgarsa daraxt butunlay boshqa", "Chiziqli model": "Yuqoriroq"}, ]))
| Xususiyat | Daraxt | Chiziqli model |
|---|---|---|
| Scaling kerakmi | YO'Q — faqat 'kattami/kichikmi' deb so'raydi | Ha |
| Chiziqli bo'lmagan naqsh | Eplaydi (halqa: 0.9883) | Eplamaydi (0.5350) |
| Tushuntirish | Eng oson — chizib ko'rsatiladi | Koeffitsiyent orqali |
| Matn ustun | sklearn da ENCODING KERAK | Encoding kerak |
| Overfitting | JUDA moyil — cheklash shart | Kamroq moyil |
| Barqarorlik | Past — ma'lumot bir oz o'zgarsa daraxt butunlay boshqa | Yuqoriroq |
Oxirgi qator — daraxtning eng katta kamchiligi. O'lchab ko'rsataman:
print("Bitta daraxt, turli train bo'linishlarida birinchi savol:") for rs in [0, 1, 7, 42, 99]: Xa, _, ya, _ = train_test_split(X, y, test_size=0.3, random_state=rs, stratify=y) d1 = DecisionTreeClassifier(max_depth=2, random_state=42).fit(Xa, ya) ildiz = son[d1.tree_.feature[0]] chegara = d1.tree_.threshold[0] print(f" random_state={rs:<4} birinchi savol: {ildiz} <= {chegara:.2f}")
Bitta daraxt, turli train bo'linishlarida birinchi savol: random_state=0 birinchi savol: xonalar <= 2.50 random_state=1 birinchi savol: xonalar <= 2.50 random_state=7 birinchi savol: xonalar <= 2.50 random_state=42 birinchi savol: maydon_m2 <= 58.75 random_state=99 birinchi savol: xonalar <= 2.50
Ma'lumot bir oz o'zgarsa, daraxtning ildiz savoli ham o'zgarishi mumkin. Ya'ni bitta daraxt — beqaror model.
Bu beqarorlik 18-bobning sababi
Agar bitta daraxt beqaror bo'lsa, nima qilamiz? Ko'p daraxt o'stiramiz va ovoz beramiz.
Aynan shu — Random Forest. 18-bob shu g'oyadan boshlanadi.
Matn ustun bilan: sklearn cheklovi
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder try: DecisionTreeClassifier().fit(df[["tuman", "maydon_m2"]], y) except ValueError as e: print("Matn ustunni to'g'ridan-to'g'ri bersak:") print(f" ValueError: {str(e)[:78]}") print() print("Shuning uchun daraxt uchun ham encoding kerak:") quvur = Pipeline([ ("t", ColumnTransformer([("matn", OneHotEncoder(handle_unknown="ignore"), ["tuman"])], remainder="passthrough")), ("m", DecisionTreeClassifier(max_depth=6, min_samples_leaf=20, random_state=42)), ]) b = cross_val_score(quvur, df[son + ["tuman"]], y, cv=5, scoring="f1") print(f" tuman bilan F1 = {b.mean():.4f} ±{b.std():.4f}") b2 = cross_val_score(DecisionTreeClassifier(max_depth=6, min_samples_leaf=20, random_state=42), X, y, cv=5, scoring="f1") print(f" tuman BUSIZ F1 = {b2.mean():.4f} ±{b2.std():.4f}")
Matn ustunni to'g'ridan-to'g'ri bersak: ValueError: could not convert string to float: 'Chilonzor' Shuning uchun daraxt uchun ham encoding kerak: tuman bilan F1 = 0.6715 ±0.0258 tuman BUSIZ F1 = 0.6715 ±0.0246
Bu — 4-bobda eski kitobda tuzatgan xato: "daraxt aralash numeric va categorical ni eplaydi" degan da'vo sklearn da to'g'ri emas. Nazariy jihatdan CART kategoriyani eplaydi, lekin sklearn ning amalga oshirilishi faqat son qabul qiladi.
Bugun nima o'rgandik
show(pd.DataFrame([ {"Tushuncha": "G'oya", "Bitta jumla": "Koeffitsiyent emas — ketma-ket savollar"}, {"Tushuncha": "Bo'linish mezoni", "Bitta jumla": "Gini yoki entropiya — nopoklikni kamaytirish"}, {"Tushuncha": "gini vs entropy", "Bitta jumla": "Farq ahamiyatsiz — sukutni o'zgartirmang"}, {"Tushuncha": "Overfitting", "Bitta jumla": "Chegarasiz daraxt yodlab oladi"}, {"Tushuncha": "Eng foydali tutqich", "Bitta jumla": "min_samples_leaf, max_depth dan ko'ra"}, {"Tushuncha": "feature_importances_", "Bitta jumla": "Bepul, lekin aldaydi — permutation ishonchliroq"}, {"Tushuncha": "Scaling", "Bitta jumla": "KERAK EMAS"}, {"Tushuncha": "Matn ustun", "Bitta jumla": "sklearn da encoding SHART — ValueError beradi"}, {"Tushuncha": "Eng katta kamchilik", "Bitta jumla": "Beqaror — ildiz savoli bo'linishdan bo'linishga o'zgaradi"}, ]))
| Tushuncha | Bitta jumla |
|---|---|
| G'oya | Koeffitsiyent emas — ketma-ket savollar |
| Bo'linish mezoni | Gini yoki entropiya — nopoklikni kamaytirish |
| gini vs entropy | Farq ahamiyatsiz — sukutni o'zgartirmang |
| Overfitting | Chegarasiz daraxt yodlab oladi |
| Eng foydali tutqich | min_samples_leaf, max_depth dan ko'ra |
| feature_importances_ | Bepul, lekin aldaydi — permutation ishonchliroq |
| Scaling | KERAK EMAS |
| Matn ustun | sklearn da encoding SHART — ValueError beradi |
| Eng katta kamchilik | Beqaror — ildiz savoli bo'linishdan bo'linishga o'zgaradi |
Keyingi bobda shu beqarorlikni yechamiz: bitta daraxt o'rniga yuztasini o'stiramiz.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.