Asosiy qismga o'tish

ROC, PR, threshold va nomutanosib ma'lumot

9-bob 12 kod bloki 2735 so'z

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

Sahna: precision past — modelni qayta o'qitamizmi?

7-bobda modelimiz precision = 0.7476, recall = 0.8462 berdi. Menejer keladi:

"Reklama budjeti qisqardi. Men 'tez ketadi' degan e'lonlarning kamida 85 foizida to'g'ri bo'lishini xohlayman."

Siz nima qilasiz? Boshqa algoritm sinaysizmi? Ko'proq ma'lumot yig'asizmi? GPU buyurtma qilasizmi?

Yo'q. Siz bitta sonni o'zgartirasiz — va modelni umuman qayta o'qitmaysiz.

Bu bob shu haqda. Bu — protokolning 10-qadami ning davomi.

Model aslida 0 yoki 1 chiqarmaydi

Birinchi tushunish kerak bo'lgan narsa: predict() sizga javob beradi, lekin model ehtimollik hisoblaydi.

python26 qator
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression

df = pd.read_csv(DATA / "uy_ijara.csv")
son = ["xonalar", "maydon_m2", "qavat", "qavatlar_soni", "metro_km", "mebel", "qurilgan_yil"]
X, y = df[son + ["tuman"]], 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)

quvur = Pipeline([
    ("tayyorla", ColumnTransformer([
        ("son", Pipeline([("toldir", SimpleImputer(strategy="median")),
                          ("shkala", StandardScaler())]), son),
        ("matn", OneHotEncoder(handle_unknown="ignore"), ["tuman"]),
    ])),
    ("model", LogisticRegression(max_iter=2000)),
]).fit(X_tr, y_tr)

ehtimol = quvur.predict_proba(X_te)[:, 1]
taqqos = pd.DataFrame({"ehtimollik": ehtimol.round(3), "predict()": quvur.predict(X_te),
                       "haqiqat": y_te.values}).head(8)
show(taqqos)
ehtimollikpredict()haqiqat
0.3201
0.76711
0.6511
0.05600
0.83111
0.61511
0.59510
0.68210

predict() ichida yashirin qoida bor: ehtimollik > 0.5 bo'lsa 1, aks holda 0.

0.5 — bu matematik haqiqat emas. Bu sukut bo'yicha qiymat. Va siz uni o'zgartirishingiz mumkin.

Threshold ni surib ko'ramiz

python10 qator
from sklearn.metrics import precision_score, recall_score, f1_score, accuracy_score

print(f"{'chegara':>8} {'precision':>10} {'recall':>8} {'F1':>7} {'accuracy':>9} {'nechta ha':>10}")
print("-" * 56)
for t in [0.3, 0.4, 0.5, 0.6, 0.7, 0.8]:
    b = (ehtimol >= t).astype(int)
    print(f"{t:>8} {precision_score(y_te, b, zero_division=0):>10.4f} "
          f"{recall_score(y_te, b, zero_division=0):>8.4f} "
          f"{f1_score(y_te, b, zero_division=0):>7.4f} "
          f"{accuracy_score(y_te, b):>9.4f} {int(b.sum()):>10}")
Chiqish
 chegara  precision   recall      F1  accuracy  nechta ha
--------------------------------------------------------
     0.3     0.6181   0.9780  0.7574    0.6833        288
     0.4     0.6748   0.9121  0.7757    0.7333        246
     0.5     0.7476   0.8462  0.7938    0.7778        206
     0.6     0.8027   0.6484  0.7173    0.7417        147
     0.7     0.8370   0.4231  0.5620    0.6667         92
     0.8     0.9189   0.1868  0.3105    0.5806         37

Jadvalni o'qing. Chegara oshganda precision o'sadi, recall tushadi. Bu — qat'iy qoida, tasodif emas.

Sabab oddiy: chegara balandlasa, model kamroq "ha" deydi. Kamroq "ha" degani — faqat eng ishonchli holatlarda aytadi (precision o'sadi), lekin ko'p haqiqiy holatni o'tkazib yuboradi (recall tushadi).

Menejerning talabi — precision ≥ 0.85. Qaysi chegara kerak?

python12 qator
import numpy as np
kerakli = None
for t in np.arange(0.30, 0.96, 0.01):
    b = (ehtimol >= t).astype(int)
    if b.sum() > 0 and precision_score(y_te, b, zero_division=0) >= 0.85:
        kerakli = t
        break
b = (ehtimol >= kerakli).astype(int)
print(f"Eng past chegara (precision >= 0.85): {kerakli:.2f}")
print(f"  precision: {precision_score(y_te, b):.4f}")
print(f"  recall   : {recall_score(y_te, b):.4f}")
print(f"  model nechta 'ha' dedi: {int(b.sum())} ta (0.5 da {int((ehtimol >= 0.5).sum())} ta edi)")
Chiqish
Eng past chegara (precision >= 0.85): 0.72
  precision: 0.8519
  recall   : 0.3791
  model nechta 'ha' dedi: 81 ta (0.5 da 206 ta edi)

Mana javob. Modelni qayta o'qitmadik, bitta satr kod ham o'zgartirmadik. Chegarani surdik.

Narxi bor: recall tushdi. Siz menejerga shuni aytasiz: "Precision 0.85 ga chiqdi, lekin endi tez ketadigan kvartiralarning kamrog'ini topamiz. Bu — savdo."

Eng yaxshi F1 beradigan chegarani avtomatik topish

python4 qator
prec, rec, thr = precision_recall_curve(y_te, ehtimol)
f1 = 2 * prec * rec / np.maximum(prec + rec, 1e-12)
eng = int(np.nanargmax(f1[:-1]))
print(thr[eng], f1[eng])

Lekin diqqat: bu chegarani test da topsangiz, u optimistik bo'ladi. To'g'ri yo'l — validation yoki CV da topish.

python6 qator
from sklearn.metrics import precision_recall_curve
prec, rec, thr = precision_recall_curve(y_te, ehtimol)
f1_egri = 2 * prec * rec / np.maximum(prec + rec, 1e-12)
eng = int(np.nanargmax(f1_egri[:-1]))
print(f"Eng yaxshi F1 beradigan chegara: {thr[eng]:.3f}  ->  F1 = {f1_egri[eng]:.4f}")
print(f"Sukut bo'yicha 0.5 da          :          F1 = {f1_score(y_te, quvur.predict(X_te)):.4f}")
Chiqish
Eng yaxshi F1 beradigan chegara: 0.511  ->  F1 = 0.7979
Sukut bo'yicha 0.5 da          :          F1 = 0.7938

Nomutanosib ma'lumot: ROC aldaydi, PR aldamaydi

Hozirgacha sinflar teng edi. Endi eng muhim holatga o'tamiz — nomutanosib ma'lumot. Bu amaliyotda eng ko'p uchraydigan holat.

Sun'iy nomutanosiblik yasaymiz: "tez ketdi" ulushini ~2% ga tushiramiz.

python8 qator
np.random.seed(42)
tez = y_te[y_te == 1].index
sekin = y_te[y_te == 0].index
tanlov = list(sekin) + list(np.random.choice(tez, size=max(1, int(len(sekin) * 0.02)), replace=False))

y_nom = y_te.loc[tanlov]
e_nom = pd.Series(ehtimol, index=y_te.index).loc[tanlov].values
print(f"Yangi to'plam: {len(y_nom)} qator, 'tez ketdi' ulushi {y_nom.mean():.2%}")
Chiqish
Yangi to'plam: 181 qator, 'tez ketdi' ulushi 1.66%

Endi ikki metrikani yonma-yon o'lchaymiz.

python11 qator
from sklearn.metrics import roc_auc_score, average_precision_score

auc = roc_auc_score(y_nom, e_nom)
ap = average_precision_score(y_nom, e_nom)

print(f"ROC-AUC          : {auc:.4f}")
print(f"Average Precision: {ap:.4f}")
print()
print("Tasodifiy model uchun baza:")
print(f"  ROC-AUC = 0.5000      (HAR DOIM, nomutanosiblikdan qat'i nazar)")
print(f"  AP      = {y_nom.mean():.4f}      (= pozitiv sinf ulushi)")
Chiqish
ROC-AUC          : 0.9551
Average Precision: 0.2342

Tasodifiy model uchun baza:
  ROC-AUC = 0.5000      (HAR DOIM, nomutanosiblikdan qat'i nazar)
  AP      = 0.0166      (= pozitiv sinf ulushi)

ROC-AUC chiroyli ko'rinadi. Lekin AP juda past.

Nega farq? Formulaga qaraymiz.

python14 qator
b_nom = (e_nom >= 0.5).astype(int)
from sklearn.metrics import confusion_matrix
tn, fp, fn, tp = confusion_matrix(y_nom, b_nom).ravel()
print(f"TN={tn}  FP={fp}  FN={fn}  TP={tp}")
print()
print("ROC ning ikkinchi o'qi — FPR:")
print(f"  FPR = FP / (FP + TN) = {fp} / ({fp} + {tn}) = {fp/(fp+tn):.4f}")
print()
print("TN ulkan (negativlar ko'p) -> maxraj ulkan -> FPR doim kichik.")
print("Natijada ROC egri chizig'i chap yuqoriga yopishadi va CHIROYLI ko'rinadi.")
print()
print("PR egri chizig'i esa TN ni UMUMAN ishlatmaydi:")
print(f"  precision = TP / (TP + FP) = {tp} / ({tp} + {fp}) = {tp/(tp+fp) if tp+fp else 0:.4f}")
print(f"  recall    = TP / (TP + FN) = {tp} / ({tp} + {fn}) = {tp/(tp+fn) if tp+fn else 0:.4f}")
Chiqish
TN=126  FP=52  FN=0  TP=3

ROC ning ikkinchi o'qi — FPR:
  FPR = FP / (FP + TN) = 52 / (52 + 126) = 0.2921

TN ulkan (negativlar ko'p) -> maxraj ulkan -> FPR doim kichik.
Natijada ROC egri chizig'i chap yuqoriga yopishadi va CHIROYLI ko'rinadi.

PR egri chizig'i esa TN ni UMUMAN ishlatmaydi:
  precision = TP / (TP + FP) = 3 / (3 + 52) = 0.0545
  recall    = TP / (TP + FN) = 3 / (3 + 0) = 1.0000

Mana sabab. TN — ROC ning yashirin yordamchisi. Negativlar ko'p bo'lsa, u ROC ni sun'iy ravishda yaxshi ko'rsatadi.

PR egri chizig'i faqat TP, FP, FN ni ishlatadi — ya'ni faqat siz qiziqqan sinf haqida gapiradi.

python19 qator
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve

fpr, tpr, _ = roc_curve(y_nom, e_nom)
p2, r2, _ = precision_recall_curve(y_nom, e_nom)

rasm, axes = plt.subplots(1, 2, figsize=(13, 5))
axes[0].plot(fpr, tpr, "b-", lw=2, label=f"AUC = {auc:.3f}")
axes[0].plot([0, 1], [0, 1], "k--", alpha=.5, label="Tasodifiy (0.5)")
axes[0].set_xlabel("False Positive Rate"); axes[0].set_ylabel("True Positive Rate")
axes[0].set_title("ROC — CHIROYLI ko'rinadi", color="darkorange", fontweight="bold")
axes[0].legend(); axes[0].grid(alpha=.3)

axes[1].plot(r2, p2, "r-", lw=2, label=f"AP = {ap:.3f}")
axes[1].axhline(y_nom.mean(), color="k", ls="--", alpha=.5, label=f"Tasodifiy ({y_nom.mean():.3f})")
axes[1].set_xlabel("Recall"); axes[1].set_ylabel("Precision")
axes[1].set_title("PR — HAQIQATNI ko'rsatadi", color="green", fontweight="bold")
axes[1].legend(); axes[1].grid(alpha=.3)
fig("roc_pr.png", caption="Bir xil model, bir xil ma'lumot. Chapdagi grafik sizni aldaydi.")

Bir xil model, bir xil ma'lumot. Chapdagi grafik sizni aldaydi.

Bir xil model, bir xil ma'lumot. Chapdagi grafik sizni aldaydi.

Qachon ROC, qachon PR

  • Sinflar taxminan teng -> ROC-AUC yaxshi ishlaydi, o'qish oson.
  • Sinflar nomutanosib (pozitiv < 10%) -> PR-AUC (average precision) ishlatiladi.
  • Maqolada "AUC 0.92" deb yozilgan bo'lsa va ma'lumot nomutanosib bo'lsa — sinf nisbatini so'rang.

Qoida: siz kam uchraydigan narsani qidirsangiz (fraud, kasallik, buzilish), PR ni o'qing.

class_weight="balanced" — sehrli tuzatish emas

Nomutanosiblikni "tuzatish" uchun eng ko'p beriladigan maslahat: class_weight="balanced". Keling, uni o'lchab ko'ramiz.

python28 qator
tz = list(set(tanlov))
X_nom = X_te.loc[tz]
y_nom2 = y_te.loc[tz]
X_ntr, X_nte, y_ntr, y_nte = train_test_split(X_nom, y_nom2, test_size=0.4,
                                              random_state=42, stratify=y_nom2)

def oz_quvur(**kw):
    return Pipeline([
        ("tayyorla", ColumnTransformer([
            ("son", Pipeline([("toldir", SimpleImputer(strategy="median")),
                              ("shkala", StandardScaler())]), son),
            ("matn", OneHotEncoder(handle_unknown="ignore"), ["tuman"]),
        ])),
        ("model", LogisticRegression(max_iter=3000, **kw)),
    ])

oddiy = oz_quvur().fit(X_ntr, y_ntr)
balans = oz_quvur(class_weight="balanced").fit(X_ntr, y_ntr)

qatorlar = []
for nom_m, m in [("Oddiy", oddiy), ("class_weight=balanced", balans)]:
    b = m.predict(X_nte)
    qatorlar.append({"Model": nom_m,
                     "Accuracy": round(accuracy_score(y_nte, b), 4),
                     "Precision": round(precision_score(y_nte, b, zero_division=0), 4),
                     "Recall": round(recall_score(y_nte, b, zero_division=0), 4),
                     "F1": round(f1_score(y_nte, b, zero_division=0), 4)})
show(pd.DataFrame(qatorlar))
ModelAccuracyPrecisionRecallF1
Oddiy0.986000
class_weight=balanced0.973000

class_weight="balanced" nima qildi? U modelga "kam uchraydigan sinfning xatosi qimmatroq" dedi. Natijada model ko'proq "ha" deyadi — recall o'sadi, precision tushadi.

Bu tuzatish emas — bu tarozini bir tomonga og'dirish. Siz threshold ni surganingizda ham xuddi shu narsa bo'ladi.

Nomutanosiblikni "hal qilish" degan narsa yo'q

class_weight, SMOTE, oversampling, undersampling — bularning hech biri ma'lumotga yangi bilim qo'shmaydi. Ular faqat qaysi xatoni afzal ko'rasiz degan savolga javob beradi.

Agar sizda 20 ta pozitiv namuna bo'lsa, hech qanday texnika uni 2000 ta qilib qo'ymaydi. Yechim — ko'proq ma'lumot yig'ish yoki muammoni qayta qo'yish.

macro va weighted — qaysi birini o'qiysiz

python3 qator
from sklearn.metrics import classification_report
print(classification_report(y_nte, oddiy.predict(X_nte),
                            target_names=["sekin", "tez"], digits=4, zero_division=0))
Chiqish
              precision    recall  f1-score   support

       sekin     0.9863    1.0000    0.9931        72
         tez     0.0000    0.0000    0.0000         1

    accuracy                         0.9863        73
   macro avg     0.4932    0.5000    0.4966        73
weighted avg     0.9728    0.9863    0.9795        73

Diqqat: weighted avg katta sinfning natijasini ko'rsatadi va kichik sinfni yashiradi. macro avg esa ikki sinfni teng hisoblaydi — shuning uchun u pastroq va halolroq.

Qoida: nomutanosib ma'lumotda macro ni o'qing.

Bugun nima o'rgandik

python9 qator
show(pd.DataFrame([
    {"Tushuncha": "predict() ichida 0.5 bor", "Bitta jumla": "U matematik haqiqat emas, sukut qiymat"},
    {"Tushuncha": "Threshold surish", "Bitta jumla": "Qayta o'qitmasdan precision/recall tanlovi"},
    {"Tushuncha": "Chegara oshsa", "Bitta jumla": "Precision o'sadi, recall tushadi — har doim"},
    {"Tushuncha": "ROC-AUC", "Bitta jumla": "Teng sinflarda yaxshi; nomutanosibda ALDAYDI (TN sababli)"},
    {"Tushuncha": "PR-AUC (AP)", "Bitta jumla": "TN ni ishlatmaydi — nomutanosibda haqiqatni aytadi"},
    {"Tushuncha": "class_weight=balanced", "Bitta jumla": "Tuzatish emas, tarozini og'dirish"},
    {"Tushuncha": "macro vs weighted", "Bitta jumla": "Nomutanosibda macro ni o'qi"},
]))
TushunchaBitta jumla
predict() ichida 0.5 borU matematik haqiqat emas, sukut qiymat
Threshold surishQayta o'qitmasdan precision/recall tanlovi
Chegara oshsaPrecision o'sadi, recall tushadi — har doim
ROC-AUCTeng sinflarda yaxshi; nomutanosibda ALDAYDI (TN sababli)
PR-AUC (AP)TN ni ishlatmaydi — nomutanosibda haqiqatni aytadi
class_weight=balancedTuzatish emas, tarozini og'dirish
macro vs weightedNomutanosibda macro ni o'qi

Keyingi bobda: hozirgacha hamma raqam bitta bo'linishdan olingan. Uni o'zgartirsak raqamlar ham o'zgaradi. Shuni hal qilamiz.

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar