Asosiy qismga o'tish

Decision tree (CART)

18-bob 14 kod bloki 2974 so'z

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

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

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

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

python22 qator
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))
Chiqish
Har bir ustun uchun ENG YAXSHI savol (Gini foydasi bo'yicha):
ustuneng yaxshi chegaraGini foydasi
maydon_m258.750.076
xonalar2.50.074
metro_km7.220.002
qavatlar_soni13.50.002
qavat4.50.002
mebel0.50

Eng katta foyda beradigan ustun — daraxtning birinchi savoli bo'ladi. Yuqoridagi export_text natijasi bilan solishtiring: mos keladi.

python6 qator
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"},
]))
MezonFormulaTezliksklearn daAmalda
gini1 - sum(p^2)TezroqSUKUT BO'YICHADeyarli har doim shu
entropy-sum(p * log2(p))Sekinroq (log hisobi)criterion='entropy'Natija deyarli bir xil
python7 qator
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.")
Chiqish
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.

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

python10 qator
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"},
]))
ParametrNima qiladiAmaliy qiymat
max_depthDaraxt chuqurligi chegarasi3-10
min_samples_leafBargda kamida shuncha namuna bo'lishi shart5-50
min_samples_splitBo'linish uchun kamida shuncha namuna kerak10-100
ccp_alphaPruning — o'sgandan keyin kesish0.0-0.02
python8 qator
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}")
Chiqish
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

python5 qator
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)")
Chiqish
Yig'indi: 1.0000  (har doim 1.0)
xususiyatmuhimligi
maydon_m20.819
metro_km0.142
qavat0.03
qavatlar_soni0.009
xonalar0
mebel0

`feature_importances_` ikki tomondan aldaydi

  1. Ko'p qiymatli ustunni ustun qo'yadi. Agar siz elon_id ni qoldirsangiz (0-bob), u eng "muhim" chiqadi — chunki u har qatorni ajratadi.
  2. Bog'liq ustunlar orasida tasodifiy tanlaydi. xonalar va maydon_m2 korrelyatsiyasi 0.94 — daraxt bittasini tanlaydi, ikkinchisining muhimligi 0 ga tushadi. Bu "ikkinchisi befoyda" degani emas.

Ishonchliroq usul — permutation importance yoki SHAP (25-bob).

python8 qator
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)
xususiyatdaraxt ichidagipermutation
maydon_m20.8190.205
xonalar00
qavat0.030
qavatlar_soni0.0090
mebel00
metro_km0.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

python14 qator
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"},
]))
XususiyatDaraxtChiziqli model
Scaling kerakmiYO'Q — faqat 'kattami/kichikmi' deb so'raydiHa
Chiziqli bo'lmagan naqshEplaydi (halqa: 0.9883)Eplamaydi (0.5350)
TushuntirishEng oson — chizib ko'rsatiladiKoeffitsiyent orqali
Matn ustunsklearn da ENCODING KERAKEncoding kerak
OverfittingJUDA moyil — cheklash shartKamroq moyil
BarqarorlikPast — ma'lumot bir oz o'zgarsa daraxt butunlay boshqaYuqoriroq

Oxirgi qator — daraxtning eng katta kamchiligi. O'lchab ko'rsataman:

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

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

python11 qator
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"},
]))
TushunchaBitta jumla
G'oyaKoeffitsiyent emas — ketma-ket savollar
Bo'linish mezoniGini yoki entropiya — nopoklikni kamaytirish
gini vs entropyFarq ahamiyatsiz — sukutni o'zgartirmang
OverfittingChegarasiz daraxt yodlab oladi
Eng foydali tutqichmin_samples_leaf, max_depth dan ko'ra
feature_importances_Bepul, lekin aldaydi — permutation ishonchliroq
ScalingKERAK EMAS
Matn ustunsklearn da encoding SHART — ValueError beradi
Eng katta kamchilikBeqaror — 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.

Telegramda yozish Narxlar