IlmHamroh
Data Science va sun'iy intellekt/Loyihalar va karyera3/12-dars55 daqiqa
Mundarija (23)

29.3-dars: To'liq loyiha: modellashtirish va baholash

29-QISM — LOYIHALAR VA KARYERA · 3-dars


1. Kirish va motivatsiya

Oldingi darsda ma'lumot tayyorlandi. Belgilar faqat as-of gacha olindi va snapshot testidan o'tdi. Sifat tekshiruvlari va qarorga yo'naltirilgan EDA qilindi. Bo'lish vaqt bo'yicha: o'quv 7-14, val 16-18, test 20-22. Test oylari hali yopiq. 29.1 dan esa bitta ochiq savol qoldi: 3 oylik val da model bitta ustunli qoidadan sezilarli yaxshi chiqmagan edi (+4.59, SE 2.93). Bu darsda o'sha savolga ko'proq dalil bilan javob beramiz, modelni qarorga tayyorlaymiz va testni bir marta ochamiz.

Real vaziyat. Bank DS jamoasi uch hafta davomida o'nlab variantni sinadi: belgilar to'plamlari, giperparametrlar, uch xil model, ikki xil kalibrlash. Har safar val natijasiga qarab "eng yaxshisi" tanlandi. Oxirida test ochildi: natija val dan ancha past chiqdi. Jamoa "test oylari omadsiz" deb yana bir nechta variantni testda sinab ko'rdi va eng yaxshisini hisobotga yozdi. Olti oydan keyin ishlab chiqarishdagi natija hisobotdagidan sezilarli past edi. Xato modelda emas, jarayonda edi. Qaror qoidasi oldindan yozilmagan, val ko'p marta qayta ishlatilgan 18.11-bob, test esa val ga aylantirilgan edi.

Bu darsda boshqacha ishlaymiz. Baholash rejasi natija ko'rilishidan oldin yoziladi va xesh bilan "muhrlanadi". Nomzodlar soddalik tartibida vaqt bo'yicha CV da juftlashtirib solishtiriladi. Tanlangan model kalibrlanadi va qo'ng'iroq markazi sig'imiga mos siyosatga aylantiriladi. Xato segmentlar bo'yicha tahlil qilinadi. Test bir marta ochiladi va natija qanday bo'lmasin, halol yoziladi.

Bu darsda modelni "yaxshi AUC" dan "pul bilan o'lchangan, sig'imga mos, kalibrlangan va bir marta tekshirilgan qaror" ga aylantiramiz.

Bu darsda:

  • Oldindan yozilgan baholash rejasi va uning "muhri"
  • Nomzodlar zinapoyasi: qoida → LogReg → HistGB → kichik tarmoq
  • Vaqt bo'yicha CV, oylar bo'yicha juftlashgan taqqoslash, qaror qoidasi
  • Kalibrlash: nega ehtimol qaror uchun kerak; class_weight="balanced" tuzog'i; Platt
  • Siyosat va sig'im: top-K, kutilgan tejam, sig'im egri chizig'i, qo'shimcha operator qiymati
  • Segmentlar bo'yicha xato tahlili
  • Test bir marta: qulf, drift va halol natija
  • Model kartasi

ℹ Misollar real numpy/pandas/sklearn bilan (Python 3.14). Kichik tarmoq sifatida MLPClassifier ishlatiladi: 20-21-qismlardagi PyTorch tarmog'i ham xuddi shu rejaga nomzod sifatida qo'shiladi. Generator va belgilar 29.1-29.2 dagi bilan aynan bir xil.


2. Nazariya — chuqur tushuntirish

2.1. Oldindan yozilgan baholash rejasi

Val ga qarab o'nlab qaror qabul qilinsa, val bahosi optimistik bo'lib qoladi. 18.11-darsda buni "validatsiyaga overfitting" deb o'lchagan edik. Himoya — natijani ko'rishdan oldin reja yozish:

text
BAHOLASH REJASI (natijadan oldin yoziladi):
  1. MAQSAD VA METRIKA   oylik tejam, top-200, ball = p * zarar   (29.1 dan)
  2. YORDAMCHI           AUC, Brier, p/haqiqiy (kalibrlash)
  3. SXEMA               har oy t = 12-18: o'quv as-of <= t-2, baho t-oyda (7 juft)
  4. NOMZODLAR           soddalik tartibida: qoida -> LogReg -> HistGB -> MLP
  5. QAROR QOIDASI       eng yaxshisidan farqi > -2*SE bo'lgan ENG SODDA nomzod
  6. KEYINGI QADAMLAR    kalibrlash, siyosat, segmentlar - faqat val da
  7. TEST                20-22, bir marta, hamma narsa muhrlangandan keyin
MUHR: sha256(json(reja)) -> hisobotga va model kartasiga yoziladi

Muhr texnik jihatdan oddiy narsa. Uning qiymati intizomda: hisobotdagi natija qaysi reja bo'yicha olingani tekshiriladigan bo'ladi. Reja o'zgarsa (bu ham bo'ladi), yangi muhr va o'zgarish sababi yoziladi. Bu klinik tadqiqotlardagi oldindan ro'yxatdan o'tkazish (pre-registration) g'oyasining kichik nusxasi.

2.2. Nomzodlar zinapoyasi

text
NOMZOD                 NEGA                                 NARXI
qoida: util            29.1 dagi bazaviy, darhol ishlaydi   0 (allaqachon bor)
LogReg                 chiziqli, kalibrlangan, tushunarli   past
HistGB                 chiziqsizlik va o'zaro ta'sir        o'rta (sozlash, tushuntirish)
MLP (kichik tarmoq)    moslashuvchan, katta ma'lumotda      yuqori (sozlash, barqarorlik)

Jadval ko'rinishidagi ma'lumotda gradient boosting ko'pincha kuchli nomzod bo'ladi. Lekin "ko'pincha" "har doim" degani emas. Bizning belgilar (utilizatsiya, to'lov ulushi, naqd yechish) yashirin xavf bilan asosan monoton bog'langan: 29.2 EDA si buni ko'rsatdi, sakrash esa kam uchraydi. Bunday holda chiziqli model kam yutqazadi. Qaysi nomzod yutishini oldindan bilmaymiz, shuning uchun rejadagi qoida hal qiladi.

2.3. Vaqt bo'yicha CV va juftlashgan taqqoslash

text
as-of:  7  8  9 10 11 12 13 14 15 16 17 18
fold 1: [o'quv 7-10 ]  .  B              <- baho: 12  (11 - bo'shliq, belgi yetilmagan)
fold 2: [o'quv 7-11    ]  .  B           <- 13
 ...
fold 7: [o'quv 7-16               ]  .  B <- 18
         kengayuvchi oyna (expanding window), bo'shliq 1 oy

HAR FOLD - BITTA OY -> har nomzodning oylik tejami
JUFTLASHGAN FARQ: d_t = tejam(nomzod, t) - tejam(eng yaxshi, t)
  farq = mean(d),  SE = std(d) / sqrt(7)
  "sezilarli yomon", agar farq < -2*SE
JUFTLASH NEGA: oylar bir-biridan keskin farq qiladi (bazaviy ulush 0.015-0.026),
  lekin bir oyda hamma nomzod bir xil mijozlarda baholanadi -> oy shovqini ayiriladi

Nega 7 ta oy? 29.1 da 3 oylik val da farqni aniqlab bo'lmadi. Tejam — top-200 dan hisoblanadigan shovqinli metrika, va unga ko'proq "juft" kerak. Kengayuvchi oyna ishlab chiqarishni simulyatsiya qiladi: har oy model mavjud barcha yetilgan belgilarda qayta o'qitiladi.

2.4. Kalibrlash: ehtimol qaror uchun

14.10-darsda kalibrlashni metrika sifatida ko'rgan edik. Loyihada u pul masalasi:

text
QAROR:         qo'ng'iroq, agar  EV = p * 0.30 * zarar - 0.02 > 0
KUTILGAN TEJAM: sum(EV) - rahbariyatga beriladigan prognoz
IKKALASI HAM p ning O'ZIGA bog'liq, faqat tartibiga emas

p 19 barobar oshirilsa (class_weight="balanced"):
  AUC        o'zgarmaydi 0.8483-bob
  EV > 0     deyarli hamma mijoz -> oyiga 4105 qo'ng'iroq
  prognoz    1308.8 mln so'm, haqiqat 0.47

TUZATISH (kalibrlash ma'lumoti o'quvdan ALOHIDA bo'lishi shart):
  Platt:     logit(p) ustida bir o'lchamli logistik regressiya (2 parametr)
  izotonik:  monoton pog'onali funksiya - ko'p ma'lumot kerak
  bizda:     7-12 da o'qitish, 13-14 da Platt, 16-18 da baholash
O'LCHOVLAR:  Brier, log-loss, ECE (kvantil bo'laklarda |p - ulush|), p/haqiqiy,
             ishonchlilik jadvali (bo'laklar bo'yicha bashorat/haqiqiy)

class_weight="balanced" nomutanosib sinflarda 14.9-bob tez-tez tavsiya qilinadi. U tartibni deyarli o'zgartirmaydi, lekin ehtimolni buzadi. Agar qaror ehtimolni narx bilan solishtirsa, bu buzilish to'g'ridan-to'g'ri zararga aylanadi (2-misol).

2.5. Siyosat va sig'im

Model ehtimol beradi, siyosat esa harakat. Asosiy variantlar:

text
1. TOP-K (sig'im cheklovi)    har oy ball bo'yicha eng yuqori K ta
2. CHEGARA (sig'imsiz)         EV > 0 bo'lgan hammaga
3. ARALASH                     top-K, lekin faqat EV > 0 bo'lsa (ortiqcha qo'ng'iroq yo'q)

BALL: p emas, p * zarar (kutilgan tejam tartibi)

SIG'IM EGRI CHIZIG'I: K = 100, 200, ..., har biri uchun kutilgan tejam
  CHEGARAVIY QIYMAT: (tejam(K2) - tejam(K1)) / (K2 - K1)
  -> "yana 100 qo'ng'iroq oyiga qancha beradi?"
  -> qo'shimcha operator xarajati bilan solishtiriladi
  optimal K: chegaraviy qiymat 0 ga tushgan joy (EV > 0 lar soni)

Kutilgan tejam egri chizig'i kalibrlangan ehtimoldan quriladi va silliq bo'ladi. Haqiqiy tejam esa shovqinli, ayniqsa katta K da. Rahbariyatga sig'im haqidagi taklif kutilgan egri chiziq bilan beriladi, haqiqiy tejam esa uni tekshirish uchun ishlatiladi (3-misol).

2.6. Segmentlar bo'yicha xato tahlili

14.13-darsdagi xato tahlili bu yerda uchta savolga javob beradi:

Savol O'lchov Nima qilamiz
Model segmentda xavfni to'g'ri baholayaptimi? p yig'indisi / haqiqiy DPD60 (SE bilan) 2*SE dan ko'p siljisa — segment kalibrlash yoki belgi
Qo'ng'iroqlar segmentlarga qanday taqsimlanmoqda? tanlangan ulush xavfga mos kelishi kerak
Segmentdagi DPD60 larning qanchasini ushlayapmiz? recall katta farq — adolatlilik savoli (29.11)

Kichik segmentda SE katta. Masalan, 30 ta DPD60 bo'lsa, nisbatning SE si ~0.2. Shuning uchun "Buxoroda kalibrlash 1.18" topilma emas, shovqin. Segmentlar ko'p bo'lsa, ko'p taqqoslash muammosi 11.9-bob ham paydo bo'ladi: 12 segmentdan bittasi tasodifan "shubhali" chiqishi mumkin. 29.4 da buni simulyatsiya bilan o'lchaymiz.

2.7. Test bir marta

text
TESTDAN OLDIN MUHRLANADI:  model turi, belgilar, o'quv oylari, siyosat, metrika
TEST:                       bitta ishga tushirish, natija jurnalga yoziladi
QULF:                       jurnal mavjud bo'lsa, qayta ochish rad etiladi
TEST YOMON CHIQSA:          natija O'SHANDAY yoziladi; tuzatish -> yangi reja,
                            yangi test ma'lumoti (keyingi oylar) bilan
TEST YAXSHI CHIQSA:         CV bilan farq tushuntiriladi (drift, tasodif), bayram emas

2.8. Model kartasi

markdown
# Model kartasi: kredit karta kechikishi - profilaktik qo'ng'iroq (v1)

## Vazifa
Har oy oxirida hozir kechikmagan mijozlar orasidan top-200 ni tanlash (ball = p * kutilgan zarar).
Belgi: 60 kun ichida 60+ kun kechikish (y60). Foydalanuvchi: qo'ng'iroq markazi.

## Model
LogReg + StandardScaler, 15 belgi (utilizatsiya, to'lov xulqi, naqd yechish, staj ...).
O'quv: as-of 2024-07 - 2025-06. Reja muhri: <sha256>.

## Baholash
- CV (7 oy, kengayuvchi oyna): tejam, qoidadan farq (SE), AUC
- Test (3 oy, bir marta): tejam, qoidadan farq (SE), AUC, kalibrlash
- Segmentlar: yangi/eski, maosh loyihasi, avto to'lov, hudud - kalibrlash va recall

## Qaror siyosati
top-200, p * zarar; sig'im egri chizig'i: +100 qo'ng'iroqning chegaraviy qiymati.

## Cheklovlar va xatarlar
- Qo'ng'iroq samarasi 0.30-bob va zarar (0.45 * balans) faraziy -> pilot (29.4)
- Makroiqtisodiy drift: bashorat darajasi siljiydi -> oylik monitoring, qayta kalibrlash
- Qaror halqasi: 5% nazorat guruhi doimiy

## Foydalanmang
Kredit berish, limit o'zgartirish, mijozni jazolash uchun.

2.9. Tuzoqlar

Asosiy tuzoqlar: rejasiz ko'p variant sinab, eng yaxshi val natijasini tanlash; val ni qayta-qayta ishlatish; nomzodlarni juftlashtirmasdan (har xil oylarda) solishtirish; "o'rtachada yuqori" ni "yaxshi" deb olish (SE siz); murakkab modelni faqat o'rtacha farq uchun tanlash; class_weight="balanced" dan keyin ehtimolni qarorga ishlatish; kalibrlashni o'quv ma'lumotining o'zida qilish; siyosatni p bo'yicha qurish (zarar e'tiborsiz); sig'imni berilgan deb qabul qilib, uning qiymatini hisoblamaslik; kichik segmentdagi farqni SE siz topilma deb e'lon qilish; testni bir necha marta ochish; test natijasi yomon chiqqanda "yana bir marta" sinash; test yaxshi chiqqanda uni tushuntirmaslik.


3. Tez ma'lumotnoma

python
import hashlib
import json

import numpy as np

# reja muhri
muhr = hashlib.sha256(json.dumps(REJA, sort_keys=True).encode()).hexdigest()[:12]

# vaqt bo'yicha CV (kengayuvchi oyna, bo'shliq 1 oy)
for t in range(12, 19):
    tr, te = df[df.oy <= t - 2], df[df.oy == t]
    p = model.fit(tr[BELGILAR], tr.y60).predict_proba(te[BELGILAR])[:, 1]
    tejam[nom].append(tejam_oy(te, p * te.zarar.to_numpy()))

# juftlashgan qaror qoidasi
d = np.array(tejam[nom]) - np.array(tejam[eng])
sezilarli_yomon = d.mean() < -2 * d.std(ddof=1) / np.sqrt(len(d))

# kalibrlash va siyosat
ev = p * SAMARA * zarar - NARX                    # kutilgan qiymat
tanla = np.argsort(-(p * zarar))[:K]              # top-K
kutilgan_tejam = ev[tanla].sum()

# Platt (alohida kalibrlash oylarida)
lg = np.log(p_kal / (1 - p_kal))[:, None]
platt = LogisticRegression(C=1e6).fit(lg, y_kal)

Modellashtirish bosqichi nazorat ro'yxati

Qadam Artefakt Tekshiruv
Reja REJA + muhr natijadan oldin yozilgan
CV oylar × nomzodlar jadvali juftlashgan farq, SE
Tanlov eng sodda munosib qaror qoidasi
Kalibrlash Brier, ECE, p/haqiqiy EV prognozi ≈ haqiqat
Siyosat top-K, sig'im egri chizig'i chegaraviy qiymat
Segmentlar kalibrlash va recall jadvali 2*SE
Test jurnal (qulf) bir marta
Hujjat model kartasi raqamlar koddan

Modellashtirish xulosasi

reja (muhr) -> vaqt bo'yicha CV (7 oy) -> juftlashgan farq -> eng sodda munosib
kalibrlash: EV = p * 0.30 * zarar - 0.02 -> p ning o'zi muhim; balanced -> Platt
siyosat: top-K (p * zarar); sig'im egri chizig'i -> +100 qo'ng'iroq qiymati
segmentlar: p/haqiqiy (SE bilan), recall -> 29.11
test: bir marta, qulf, drift halol yoziladi -> model kartasi

4. Batafsil misollar

Misollar real numpy/pandas/sklearn bilan (Python 3.14). Har misol mustaqil ishlaydi, generator va belgilar jadvali (yarat_bank, belgilar_jadvali) 29.1-29.2 dagi bilan aynan bir xil. 1-3-misollar faqat o'quv va val oylarida ishlaydi, test oylari (20-22) faqat 4-misolda, bir marta ochiladi.

Misol 1 — Oldindan yozilgan baholash rejasi va vaqt bo'yicha CV: qoida, LogReg, HistGB, kichik tarmoq

python
"""Oldindan yozilgan baholash rejasi va vaqt bo'yicha CV: qoida, LogReg, HistGB, kichik tarmoq."""

import hashlib
import json
import warnings

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.exceptions import ConvergenceWarning
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

HUDUDLAR = ["Toshkent", "Samarqand", "Farg'ona", "Andijon", "Buxoro", "Qashqadaryo"]
OYLAR = 24                               # 1 = 2024-01, ..., 24 = 2025-12
BELGILAR = ["yosh", "daromad", "limit", "avto_tolov", "maosh_loyihasi", "staj",
            "util", "util_3", "util_ozg", "kechikish_6", "tolov_nisbati_3",
            "tolov_kuni_3", "naqd_soni_3", "naqd_summa_3", "xarid_soni_3"]
K, SAMARA, NARX = 200, 0.30, 0.02        # oyiga qo'ng'iroq, oldini olish, mln so'm


def sigmoid(z):
    return 1 / (1 + np.exp(-z))


def oy_nomi(m):
    return f"{2024 + (m - 1) // 12}-{(m - 1) % 12 + 1:02d}"


def yarat_bank(seed=29, n=6000):
    """Kredit karta portfeli: mijozlar, hisobvaraqlar, tolovlar, tranzaksiyalar."""
    rng = np.random.default_rng(seed)
    ids = np.arange(1, n + 1)
    yosh = rng.integers(21, 66, n)
    maosh = rng.random(n) < 0.45
    daromad = np.round(np.exp(rng.normal(np.log(6.0), 0.45, n)), 2)   # mln so'm
    ochilgan = np.where(rng.random(n) < 0.7, rng.integers(-40, 1, n),
                        rng.integers(1, 20, n))
    xavf = (rng.normal(0, 1, n) - 0.5 * maosh - 0.8 * np.log(daromad / 6)
            + 0.015 * (35 - yosh))                     # yashirin: moliyaviy xavf
    unut = rng.normal(0, 1, n) + 0.03 * (35 - yosh)     # yashirin: unutuvchanlik
    avto = (rng.random(n) < sigmoid(-0.2 - 0.9 * unut)).astype(int)
    limit = np.round(daromad * rng.uniform(1.5, 3.5, n), 1)
    hudud = rng.choice(HUDUDLAR, n, p=[0.34, 0.15, 0.14, 0.13, 0.12, 0.12])
    mj = pd.DataFrame({"mijoz_id": ids, "yosh": yosh, "hudud": hudud,
                       "daromad": daromad, "limit": limit, "avto_tolov": avto,
                       "maosh_loyihasi": maosh.astype(int), "ochilgan_oy": ochilgan})
    s, oldingi, ketma = rng.normal(0, 1, n), np.zeros(n, int), np.zeros(n, int)
    hv, tl, tr = [], [], []
    for m in range(1, OYLAR + 1):
        faol = (ochilgan <= m) & (ketma < 3)            # 3 ketma-ket kechikish - defolt
        s = 0.75 * s + 0.66 * rng.normal(0, 1, n)
        d = 0.8 * xavf + 0.7 * s + 0.15 * (m % 12 == 1) + 0.08 * max(m - 18, 0)
        util = np.clip(sigmoid(-0.4 + 0.55 * d + rng.normal(0, 0.5, n)), 0.01, 1)
        balans = np.round(util * limit, 3)
        min_t = np.round(np.maximum(0.05 * balans, 0.05), 3)
        for turi, lam in [("xarid", np.exp(1.2 + 0.15 * np.log(daromad / 6) - 0.1 * d)),
                          ("naqd", np.exp(-1.6 + 0.6 * d)),
                          ("onlayn", np.exp(0.6 - 0.02 * (yosh - 35)))]:
            i = np.repeat(np.arange(n), rng.poisson(lam) * faol)
            summa = (rng.uniform(0.2, 1.5, i.size) * (1 + 0.3 * d[i].clip(0))
                     if turi == "naqd" else np.exp(rng.normal(np.log(0.25), 0.8, i.size)))
            tr.append(pd.DataFrame({"mijoz_id": ids[i], "oy": m,
                                    "kun": rng.integers(1, 29, i.size), "turi": turi,
                                    "summa": np.round(summa, 3)}))
        q = rng.random(n) < sigmoid(-3.7 + 1.3 * d + 1.2 * (util > 0.85)
                                    + 3.8 * (oldingi == 2))          # qiyinchilik
        u = ~q & (rng.random(n) < sigmoid(-3.4 + 1.1 * unut - 1.8 * avto)
                  * (oldingi != 1))                                  # unutish
        miss = (q | u) & faol
        ulush = sigmoid(0.3 - 1.2 * d + rng.normal(0, 0.5, n))
        summa = np.where(miss, min_t * rng.uniform(0, 0.6, n),
                         min_t + (balans - min_t).clip(0) * ulush)
        tolaydi = faol & (~miss | (rng.random(n) < 0.4))
        kun = np.clip(np.round(6 + 14 * sigmoid(unut + 0.5 * d)
                               + rng.normal(0, 3, n)), 1, 25)
        hv.append(pd.DataFrame({"mijoz_id": ids[faol], "oy": m,
                                "balans": balans[faol], "min_tolov": min_t[faol]}))
        if m < OYLAR:                    # m-oy hisobvarag'i (m+1)-oyda to'lanadi
            tl.append(pd.DataFrame({"mijoz_id": ids[tolaydi], "hisob_oy": m,
                                    "tolov_oy": m + 1,
                                    "tolov_kuni": kun[tolaydi].astype(int),
                                    "summa": np.round(summa[tolaydi], 3)}))
        ketma = np.where(faol, np.where(miss, ketma + 1, 0), ketma)
        oldingi = np.where(miss, np.where(q, 2, 1), 0)
    return {"mijozlar": mj, "hisobvaraqlar": pd.concat(hv, ignore_index=True),
            "tolovlar": pd.concat(tl, ignore_index=True),
            "tranzaksiyalar": pd.concat(tr, ignore_index=True)}


def orta(a):
    """Qator bo'yicha NaN siz o'rtacha (bo'sh qatorda NaN, ogohlantirishsiz)."""
    k = (~np.isnan(a)).sum(axis=1)
    return np.where(k > 0, np.nansum(a, axis=1) / np.maximum(k, 1), np.nan)


def belgilar_jadvali(bank, oylar=range(7, 23)):
    """Har as-of oy t (oy oxiri): faqat t gacha ma'lum belgilar + t, t+1 natijasi."""
    mj = bank["mijozlar"]
    ids, kol = mj["mijoz_id"].to_numpy(), np.arange(1, OYLAR + 1)

    def mat(df, qiymat, ustun="oy"):
        return (df.pivot(index="mijoz_id", columns=ustun, values=qiymat)
                .reindex(index=ids, columns=kol).to_numpy(dtype=float))

    hv, tl = bank["hisobvaraqlar"], bank["tolovlar"]
    bal, mint = mat(hv, "balans"), mat(hv, "min_tolov")
    tol, kun = mat(tl, "summa", "hisob_oy"), mat(tl, "tolov_kuni", "hisob_oy")
    bor = ~np.isnan(bal)
    miss = bor & ~(np.nan_to_num(tol) >= mint - 1e-9)    # minimal to'lov qilinmagan
    g = (bank["tranzaksiyalar"].groupby(["turi", "mijoz_id", "oy"])["summa"]
         .agg(["size", "sum"]))

    def tr_mat(turi, ust):
        return (g.loc[turi, ust].unstack("oy").reindex(index=ids, columns=kol)
                .fillna(0).to_numpy())

    naqd_n, naqd_s = tr_mat("naqd", "size"), tr_mat("naqd", "sum")
    xarid_n = tr_mat("xarid", "size")
    util = bal / mj["limit"].to_numpy()[:, None]
    och = mj["ochilgan_oy"].to_numpy()
    qismlar = []
    for t in oylar:
        c = t - 1                                        # t-oy ustuni (0 dan)
        ok = (och <= t - 1) & bor[:, c] & bor[:, c - 1] & ~miss[:, c - 1]
        u3 = util[:, c - 3]
        f = mj.drop(columns="ochilgan_oy").assign(
            oy=t, staj=t - och, util=util[:, c], util_3=orta(util[:, c - 2:c + 1]),
            util_ozg=util[:, c] - np.where(np.isnan(u3), util[:, c], u3),
            kechikish_6=miss[:, max(c - 6, 0):c].sum(axis=1),
            tolov_nisbati_3=orta(np.nan_to_num(tol[:, c - 3:c]) / bal[:, c - 3:c]),
            tolov_kuni_3=orta(kun[:, c - 3:c]),
            naqd_soni_3=naqd_n[:, c - 2:c + 1].sum(axis=1),
            naqd_summa_3=naqd_s[:, c - 2:c + 1].sum(axis=1),
            xarid_soni_3=xarid_n[:, c - 2:c + 1].sum(axis=1),
            balans=bal[:, c], y30=(miss[:, c] | miss[:, c + 1]).astype(int),
            y60=(miss[:, c] & miss[:, c + 1]).astype(int))
        qismlar.append(f[ok])
    df = pd.concat(qismlar, ignore_index=True)
    df["tolov_kuni_3"] = df["tolov_kuni_3"].fillna(25.0)
    df["tolov_nisbati_3"] = df["tolov_nisbati_3"].fillna(0.0)
    df["zarar"] = 0.45 * df["balans"]              # DPD60 bo'lsa kutilgan zarar
    df["qiymat"] = SAMARA * df["zarar"] * df["y60"] - NARX   # qo'ng'iroq qiymati
    return df

REJA = {
    "maqsad": "oyiga K=200 qo'ng'iroq, tejam = 0.30*zarar*y60 - 0.02",
    "asosiy_metrika": "oylik tejam (top-200, ball = p * zarar)",
    "yordamchi": ["AUC", "Brier", "p/haqiqiy"],
    "cv": "har oy t = 12-18: o'quv as-of <= t-2, baho t-oyda (7 juft)",
    "nomzodlar_soddalik_tartibida": ["qoida: util", "LogReg", "HistGB", "MLP (16)"],
    "qaror_qoidasi": "eng yaxshisidan farqi > -2*SE bo'lgan eng sodda nomzod",
    "test": "20-22, bir marta, tanlov va siyosat muhrlangandan keyin",
}


def nomzodlar():
    return {
        "LogReg": make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)),
        "HistGB": HistGradientBoostingClassifier(
            max_iter=200, learning_rate=0.05, max_leaf_nodes=15, min_samples_leaf=100,
            l2_regularization=1.0, random_state=0),
        "MLP (16)": make_pipeline(StandardScaler(), MLPClassifier(
            hidden_layer_sizes=(16,), alpha=1e-2, batch_size=1024, learning_rate_init=3e-3,
            max_iter=40, random_state=0)),
    }


def tejam_oy(g, ball, k=K):
    return g["qiymat"].to_numpy()[np.argsort(-ball, kind="stable")[:k]].sum()


def main() -> None:
    df = belgilar_jadvali(yarat_bank())
    muhr = hashlib.sha256(json.dumps(REJA, sort_keys=True).encode()).hexdigest()[:12]
    print("=== 1. Baholash rejasi (natijadan OLDIN yozildi va muhrlandi) ===")
    for k, v in REJA.items():
        print(f"  {k}: {v}")
    print(f"  reja muhri (sha256): {muhr}")

    print("\n=== 2. Vaqt bo'yicha CV: oylik tejam (mln so'm) ===")
    nomlar = REJA["nomzodlar_soddalik_tartibida"]
    tejam = {n: [] for n in nomlar}
    auc = {n: [] for n in nomlar}
    for t in range(12, 19):
        tr, te = df[df.oy <= t - 2], df[df.oy == t]
        z = te["zarar"].to_numpy()
        ballar = {"qoida: util": te["util"].to_numpy()}
        for nom, m in nomzodlar().items():
            with warnings.catch_warnings():       # MLP ataylab 40 epoxa: ogohlantirish
                warnings.simplefilter("ignore", ConvergenceWarning)
                m.fit(tr[BELGILAR], tr["y60"])
            ballar[nom] = m.predict_proba(te[BELGILAR])[:, 1]
        for nom in nomlar:
            b = ballar[nom] if nom == "qoida: util" else ballar[nom] * z
            tejam[nom].append(tejam_oy(te, b))
            auc[nom].append(roc_auc_score(te["y60"], ballar[nom]))
    print(f"  {'oy':<8}" + "".join(f"{n:>13}" for n in nomlar))
    for i, t in enumerate(range(12, 19)):
        print(f"  {oy_nomi(t):<8}" + "".join(f"{tejam[n][i]:>13.2f}" for n in nomlar))
    orta_nom = "o'rtacha"
    print(f"  {orta_nom:<8}" + "".join(f"{np.mean(tejam[n]):>13.2f}" for n in nomlar))
    print(f"  {'AUC':<8}" + "".join(f"{np.mean(auc[n]):>13.4f}" for n in nomlar))

    print("\n=== 3. Juftlashgan taqqoslash (oylar bo'yicha) va qaror qoidasi ===")
    eng = max(nomlar, key=lambda n: np.mean(tejam[n]))
    print(f"  eng yaxshi o'rtacha: {eng}")
    tanlov = None
    for nom in nomlar:
        d = np.array(tejam[nom]) - np.array(tejam[eng])
        farq, se = d.mean(), d.std(ddof=1) / np.sqrt(len(d))
        yomon = farq < -2 * se
        yutgan = int((d > 0).sum())
        if not yomon and tanlov is None:
            tanlov = nom
        print(f"  {nom:<12} farq {farq:+6.2f}  SE {se:5.2f}  sezilarli yomon: {str(yomon):<5}"
              f"  yutgan oylar {yutgan}/7")
    print(f"  TANLOV (eng sodda munosib): {tanlov}")

    print("\n=== 4. Xulosa (natijadan hisoblangan) ===")
    d = np.array(tejam["LogReg"]) - np.array(tejam["qoida: util"])
    farq, se = d.mean(), d.std(ddof=1) / np.sqrt(len(d))
    if farq > 2 * se:
        print(f"  29.1 dagi ochiq savol: model qoidadan oyiga {farq:+.2f} (SE {se:.2f}) "
              f"ko'p tejaydi -> sezilarli ({int((d > 0).sum())}/7 oy)")
    else:
        print(f"  model qoidadan sezilarli yaxshi emas ({farq:+.2f}, SE {se:.2f})")
    if tanlov != eng:
        print(f"  {eng} o'rtachada yuqori, lekin farq shovqin ichida -> {tanlov} tanlandi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Baholash rejasi (natijadan OLDIN yozildi va muhrlandi) ===
  maqsad: oyiga K=200 qo'ng'iroq, tejam = 0.30*zarar*y60 - 0.02
  asosiy_metrika: oylik tejam (top-200, ball = p * zarar)
  yordamchi: ['AUC', 'Brier', 'p/haqiqiy']
  cv: har oy t = 12-18: o'quv as-of <= t-2, baho t-oyda (7 juft)
  nomzodlar_soddalik_tartibida: ['qoida: util', 'LogReg', 'HistGB', 'MLP (16)']
  qaror_qoidasi: eng yaxshisidan farqi > -2*SE bo'lgan eng sodda nomzod
  test: 20-22, bir marta, tanlov va siyosat muhrlangandan keyin
  reja muhri (sha256): af71c572eab7

=== 2. Vaqt bo'yicha CV: oylik tejam (mln so'm) ===
  oy        qoida: util       LogReg       HistGB     MLP (16)
  2024-12         27.17        33.09        30.09        31.39
  2025-01         28.08        36.98        37.03        27.90
  2025-02         21.08        32.54        35.86        31.94
  2025-03         22.94        26.53        25.01        23.81
  2025-04         21.62        25.45        27.54        26.63
  2025-05         32.25        33.06        34.11        31.17
  2025-06         18.89        29.22        33.53        27.18
  o'rtacha        24.58        30.98        31.88        28.58
  AUC            0.8008       0.8380       0.8300       0.8167

=== 3. Juftlashgan taqqoslash (oylar bo'yicha) va qaror qoidasi ===
  eng yaxshi o'rtacha: HistGB
  qoida: util  farq  -7.30  SE  2.13  sezilarli yomon: True   yutgan oylar 0/7
  LogReg       farq  -0.90  SE  0.98  sezilarli yomon: False  yutgan oylar 2/7
  HistGB       farq  +0.00  SE  0.00  sezilarli yomon: False  yutgan oylar 0/7
  MLP (16)     farq  -3.31  SE  1.34  sezilarli yomon: True   yutgan oylar 1/7
  TANLOV (eng sodda munosib): LogReg

=== 4. Xulosa (natijadan hisoblangan) ===
  29.1 dagi ochiq savol: model qoidadan oyiga +6.40 (SE 1.49) ko'p tejaydi -> sezilarli (7/7 oy)
  HistGB o'rtachada yuqori, lekin farq shovqin ichida -> LogReg tanlandi

Natija tahlili.

1-bo'lim — reja natijadan oldin chop etildi va muhrlandi (af71c572eab7). Unda metrika, sxema, nomzodlar tartibi va qaror qoidasi bor. Keyingi hamma raqam shu reja bo'yicha olinadi, reja esa natijaga qarab o'zgartirilmaydi.

2-bo'lim — 7 oy × 4 nomzod. Oylar orasidagi farq nomzodlar orasidagi farqdan katta. LogReg tejami 25.45 dan 36.98 gacha o'zgaradi. Oylar bo'yicha juftlashtirish kerakligi shundan: har oy hamma nomzod bir xil mijozlarda baholanadi. O'rtacha tejam: qoida 24.58, LogReg 30.98, HistGB 31.88, MLP 28.58. AUC bo'yicha tartib boshqacha: LogReg 0.8380, HistGB 0.8300. 29.1 da ko'rganimizdek, AUC va tejam har doim ham bir xil gapirmaydi.

3-bo'lim — qaror qoidasi. O'rtachada eng yaxshisi — HistGB. Uning bilan taqqoslaganda:

  • qoida -7.30 (SE 2.13) — sezilarli yomon, 7 oyning birortasida ham yutmagan;
  • LogReg -0.90 (SE 0.98) — sezilarli yomon emas, 2 oyda yutgan;
  • MLP -3.31 (SE 1.34) — sezilarli yomon.

Qoida bo'yicha LogReg tanlanadi: eng sodda munosib nomzod. HistGB o'rtachada oyiga taxminan 0.9 mln so'm ko'p beradi, lekin bu farq shovqin ichida. Uning narxi esa haqiqiy: sozlash, tushuntirish, monitoring. Kichik tarmoq bu ma'lumotda ikkalasidan ham yomon. Jadval ko'rinishidagi, asosan monoton bog'liqlikli ma'lumotda bu kutilgan natija, va bu natija rejada yozilgan tartib bilan olindi.

4-bo'lim — 29.1 dagi ochiq savolga javob. 7 oylik juftlashgan taqqoslashda LogReg qoidadan oyiga +6.40 (SE 1.49) ko'p tejaydi va 7 oyning yettisida yutadi. 3 oylik val da noaniq bo'lgan farq (+4.59, SE 2.93) ko'proq dalil bilan sezilarli bo'ldi. 29.1 dagi darvoza ("model ustunligini ko'proq oyda isbotlash") o'tildi.

Misol 2 — Kalibrlash: ehtimollar qaror uchun kerak

python
"""Kalibrlash: ehtimollar qaror uchun kerak (Brier, ECE, "p * zarar > narx" siyosati)."""

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import brier_score_loss, log_loss, roc_auc_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

HUDUDLAR = ["Toshkent", "Samarqand", "Farg'ona", "Andijon", "Buxoro", "Qashqadaryo"]
OYLAR = 24                               # 1 = 2024-01, ..., 24 = 2025-12
BELGILAR = ["yosh", "daromad", "limit", "avto_tolov", "maosh_loyihasi", "staj",
            "util", "util_3", "util_ozg", "kechikish_6", "tolov_nisbati_3",
            "tolov_kuni_3", "naqd_soni_3", "naqd_summa_3", "xarid_soni_3"]
K, SAMARA, NARX = 200, 0.30, 0.02        # oyiga qo'ng'iroq, oldini olish, mln so'm


def sigmoid(z):
    return 1 / (1 + np.exp(-z))


def oy_nomi(m):
    return f"{2024 + (m - 1) // 12}-{(m - 1) % 12 + 1:02d}"


def yarat_bank(seed=29, n=6000):
    """Kredit karta portfeli: mijozlar, hisobvaraqlar, tolovlar, tranzaksiyalar."""
    rng = np.random.default_rng(seed)
    ids = np.arange(1, n + 1)
    yosh = rng.integers(21, 66, n)
    maosh = rng.random(n) < 0.45
    daromad = np.round(np.exp(rng.normal(np.log(6.0), 0.45, n)), 2)   # mln so'm
    ochilgan = np.where(rng.random(n) < 0.7, rng.integers(-40, 1, n),
                        rng.integers(1, 20, n))
    xavf = (rng.normal(0, 1, n) - 0.5 * maosh - 0.8 * np.log(daromad / 6)
            + 0.015 * (35 - yosh))                     # yashirin: moliyaviy xavf
    unut = rng.normal(0, 1, n) + 0.03 * (35 - yosh)     # yashirin: unutuvchanlik
    avto = (rng.random(n) < sigmoid(-0.2 - 0.9 * unut)).astype(int)
    limit = np.round(daromad * rng.uniform(1.5, 3.5, n), 1)
    hudud = rng.choice(HUDUDLAR, n, p=[0.34, 0.15, 0.14, 0.13, 0.12, 0.12])
    mj = pd.DataFrame({"mijoz_id": ids, "yosh": yosh, "hudud": hudud,
                       "daromad": daromad, "limit": limit, "avto_tolov": avto,
                       "maosh_loyihasi": maosh.astype(int), "ochilgan_oy": ochilgan})
    s, oldingi, ketma = rng.normal(0, 1, n), np.zeros(n, int), np.zeros(n, int)
    hv, tl, tr = [], [], []
    for m in range(1, OYLAR + 1):
        faol = (ochilgan <= m) & (ketma < 3)            # 3 ketma-ket kechikish - defolt
        s = 0.75 * s + 0.66 * rng.normal(0, 1, n)
        d = 0.8 * xavf + 0.7 * s + 0.15 * (m % 12 == 1) + 0.08 * max(m - 18, 0)
        util = np.clip(sigmoid(-0.4 + 0.55 * d + rng.normal(0, 0.5, n)), 0.01, 1)
        balans = np.round(util * limit, 3)
        min_t = np.round(np.maximum(0.05 * balans, 0.05), 3)
        for turi, lam in [("xarid", np.exp(1.2 + 0.15 * np.log(daromad / 6) - 0.1 * d)),
                          ("naqd", np.exp(-1.6 + 0.6 * d)),
                          ("onlayn", np.exp(0.6 - 0.02 * (yosh - 35)))]:
            i = np.repeat(np.arange(n), rng.poisson(lam) * faol)
            summa = (rng.uniform(0.2, 1.5, i.size) * (1 + 0.3 * d[i].clip(0))
                     if turi == "naqd" else np.exp(rng.normal(np.log(0.25), 0.8, i.size)))
            tr.append(pd.DataFrame({"mijoz_id": ids[i], "oy": m,
                                    "kun": rng.integers(1, 29, i.size), "turi": turi,
                                    "summa": np.round(summa, 3)}))
        q = rng.random(n) < sigmoid(-3.7 + 1.3 * d + 1.2 * (util > 0.85)
                                    + 3.8 * (oldingi == 2))          # qiyinchilik
        u = ~q & (rng.random(n) < sigmoid(-3.4 + 1.1 * unut - 1.8 * avto)
                  * (oldingi != 1))                                  # unutish
        miss = (q | u) & faol
        ulush = sigmoid(0.3 - 1.2 * d + rng.normal(0, 0.5, n))
        summa = np.where(miss, min_t * rng.uniform(0, 0.6, n),
                         min_t + (balans - min_t).clip(0) * ulush)
        tolaydi = faol & (~miss | (rng.random(n) < 0.4))
        kun = np.clip(np.round(6 + 14 * sigmoid(unut + 0.5 * d)
                               + rng.normal(0, 3, n)), 1, 25)
        hv.append(pd.DataFrame({"mijoz_id": ids[faol], "oy": m,
                                "balans": balans[faol], "min_tolov": min_t[faol]}))
        if m < OYLAR:                    # m-oy hisobvarag'i (m+1)-oyda to'lanadi
            tl.append(pd.DataFrame({"mijoz_id": ids[tolaydi], "hisob_oy": m,
                                    "tolov_oy": m + 1,
                                    "tolov_kuni": kun[tolaydi].astype(int),
                                    "summa": np.round(summa[tolaydi], 3)}))
        ketma = np.where(faol, np.where(miss, ketma + 1, 0), ketma)
        oldingi = np.where(miss, np.where(q, 2, 1), 0)
    return {"mijozlar": mj, "hisobvaraqlar": pd.concat(hv, ignore_index=True),
            "tolovlar": pd.concat(tl, ignore_index=True),
            "tranzaksiyalar": pd.concat(tr, ignore_index=True)}


def orta(a):
    """Qator bo'yicha NaN siz o'rtacha (bo'sh qatorda NaN, ogohlantirishsiz)."""
    k = (~np.isnan(a)).sum(axis=1)
    return np.where(k > 0, np.nansum(a, axis=1) / np.maximum(k, 1), np.nan)


def belgilar_jadvali(bank, oylar=range(7, 23)):
    """Har as-of oy t (oy oxiri): faqat t gacha ma'lum belgilar + t, t+1 natijasi."""
    mj = bank["mijozlar"]
    ids, kol = mj["mijoz_id"].to_numpy(), np.arange(1, OYLAR + 1)

    def mat(df, qiymat, ustun="oy"):
        return (df.pivot(index="mijoz_id", columns=ustun, values=qiymat)
                .reindex(index=ids, columns=kol).to_numpy(dtype=float))

    hv, tl = bank["hisobvaraqlar"], bank["tolovlar"]
    bal, mint = mat(hv, "balans"), mat(hv, "min_tolov")
    tol, kun = mat(tl, "summa", "hisob_oy"), mat(tl, "tolov_kuni", "hisob_oy")
    bor = ~np.isnan(bal)
    miss = bor & ~(np.nan_to_num(tol) >= mint - 1e-9)    # minimal to'lov qilinmagan
    g = (bank["tranzaksiyalar"].groupby(["turi", "mijoz_id", "oy"])["summa"]
         .agg(["size", "sum"]))

    def tr_mat(turi, ust):
        return (g.loc[turi, ust].unstack("oy").reindex(index=ids, columns=kol)
                .fillna(0).to_numpy())

    naqd_n, naqd_s = tr_mat("naqd", "size"), tr_mat("naqd", "sum")
    xarid_n = tr_mat("xarid", "size")
    util = bal / mj["limit"].to_numpy()[:, None]
    och = mj["ochilgan_oy"].to_numpy()
    qismlar = []
    for t in oylar:
        c = t - 1                                        # t-oy ustuni (0 dan)
        ok = (och <= t - 1) & bor[:, c] & bor[:, c - 1] & ~miss[:, c - 1]
        u3 = util[:, c - 3]
        f = mj.drop(columns="ochilgan_oy").assign(
            oy=t, staj=t - och, util=util[:, c], util_3=orta(util[:, c - 2:c + 1]),
            util_ozg=util[:, c] - np.where(np.isnan(u3), util[:, c], u3),
            kechikish_6=miss[:, max(c - 6, 0):c].sum(axis=1),
            tolov_nisbati_3=orta(np.nan_to_num(tol[:, c - 3:c]) / bal[:, c - 3:c]),
            tolov_kuni_3=orta(kun[:, c - 3:c]),
            naqd_soni_3=naqd_n[:, c - 2:c + 1].sum(axis=1),
            naqd_summa_3=naqd_s[:, c - 2:c + 1].sum(axis=1),
            xarid_soni_3=xarid_n[:, c - 2:c + 1].sum(axis=1),
            balans=bal[:, c], y30=(miss[:, c] | miss[:, c + 1]).astype(int),
            y60=(miss[:, c] & miss[:, c + 1]).astype(int))
        qismlar.append(f[ok])
    df = pd.concat(qismlar, ignore_index=True)
    df["tolov_kuni_3"] = df["tolov_kuni_3"].fillna(25.0)
    df["tolov_nisbati_3"] = df["tolov_nisbati_3"].fillna(0.0)
    df["zarar"] = 0.45 * df["balans"]              # DPD60 bo'lsa kutilgan zarar
    df["qiymat"] = SAMARA * df["zarar"] * df["y60"] - NARX   # qo'ng'iroq qiymati
    return df


def ece(y, p, bolak=10):
    """Kutilgan kalibrlash xatosi: kvantil bo'laklarda |o'rtacha p - haqiqiy ulush|."""
    chegara = np.quantile(p, np.linspace(0, 1, bolak + 1))
    idx = np.clip(np.searchsorted(chegara, p, side="right") - 1, 0, bolak - 1)
    return sum(abs(p[idx == b].mean() - y[idx == b].mean()) * (idx == b).mean()
               for b in range(bolak) if (idx == b).any())


def platt(p_kal, y_kal):
    """Platt: logit(p) ustida bir o'lchamli logistik regressiya."""
    lg = np.log(p_kal / (1 - p_kal))[:, None]
    m = LogisticRegression(C=1e6, max_iter=1000).fit(lg, y_kal)
    return lambda p: m.predict_proba(np.log(p / (1 - p))[:, None])[:, 1]


def siyosat_tejam(va, p):
    """Sig'imsiz siyosat: kutilgan qiymat musbat bo'lsa qo'ng'iroq."""
    ev = p * SAMARA * va["zarar"].to_numpy() - NARX
    tanlandi = ev > 0
    m = va["oy"].nunique()
    return (tanlandi.sum() / m, va["qiymat"].to_numpy()[tanlandi].sum() / m,
            ev[tanlandi].sum() / m)


def main() -> None:
    df = belgilar_jadvali(yarat_bank())
    tr, va = df[df.oy <= 14], df[df.oy.between(16, 18)].reset_index(drop=True)
    y = va["y60"].to_numpy()

    def lr(**kw):
        return make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, **kw))

    modellar = {}
    modellar["LogReg"] = lr().fit(tr[BELGILAR], tr.y60).predict_proba(va[BELGILAR])[:, 1]
    hgb = HistGradientBoostingClassifier(max_iter=200, learning_rate=0.05, max_leaf_nodes=15,
                                         min_samples_leaf=100, l2_regularization=1.0,
                                         random_state=0)
    modellar["HistGB"] = hgb.fit(tr[BELGILAR], tr.y60).predict_proba(va[BELGILAR])[:, 1]
    bal = lr(class_weight="balanced").fit(tr[BELGILAR], tr.y60)
    modellar["LogReg balanced"] = bal.predict_proba(va[BELGILAR])[:, 1]
    # tuzatish: 7-12 da o'qitib, 13-14 da Platt (kalibrlash ma'lumoti o'quvdan ajratilgan)
    a, b = tr[tr.oy <= 12], tr[tr.oy >= 13]
    bal2 = lr(class_weight="balanced").fit(a[BELGILAR], a.y60)
    f = platt(bal2.predict_proba(b[BELGILAR])[:, 1], b.y60.to_numpy())
    modellar["balanced + Platt"] = f(bal2.predict_proba(va[BELGILAR])[:, 1])

    print("=== 1. Kalibrlash o'lchovlari (o'quv 7-14, val 16-18) ===")
    print(f"  haqiqiy y60 ulushi val da: {y.mean():.4f}")
    print(f"  {'model':<18} {'AUC':>7} {'Brier':>8} {'log-loss':>9} {'ECE':>8} {'p/haqiqiy':>10}")
    for nom, p in modellar.items():
        print(f"  {nom:<18} {roc_auc_score(y, p):>7.4f} {brier_score_loss(y, p):>8.5f} "
              f"{log_loss(y, p):>9.4f} {ece(y, p):>8.4f} {p.mean() / y.mean():>10.2f}")

    print("\n=== 2. Ishonchlilik jadvali (LogReg va LogReg balanced, 5 kvantil) ===")
    for nom in ["LogReg", "LogReg balanced"]:
        p = modellar[nom]
        q = pd.qcut(p, 5, labels=False)
        qator = [f"{p[q == i].mean():.3f}/{y[q == i].mean():.3f}" for i in range(5)]
        print(f"  {nom:<16} (bashorat/haqiqiy): " + "  ".join(qator))

    print("\n=== 3. Qaror: \"p * 0.30 * zarar > 0.02\" bo'lsa qo'ng'iroq (sig'imsiz) ===")
    sarlavha = "qo'ng'iroq/oy"
    print(f"  {'model':<18} {sarlavha:>13} {'haqiqiy tejam':>14} {'kutilgan':>9}")
    natija = {}
    for nom, p in modellar.items():
        natija[nom] = siyosat_tejam(va, p)
        n, h, k = natija[nom]
        print(f"  {nom:<18} {n:>13.0f} {h:>14.2f} {k:>9.2f}")

    print("\n=== 4. Top-200 (p * zarar) - sig'im bilan ===")
    for nom, p in modellar.items():
        t = np.mean([va["qiymat"].to_numpy()[g][np.argsort(-(p * va["zarar"].to_numpy())[g],
                                                           kind="stable")[:K]].sum()
                     for g in [np.flatnonzero(va.oy.to_numpy() == m) for m in (16, 17, 18)]])
        print(f"  {nom:<18} tejam {t:6.2f} mln so'm/oy")

    print("\n=== 5. Xulosa (natijadan hisoblangan) ===")
    for nom, (n, h, k) in natija.items():
        if abs(k - h) > 0.25 * abs(h) + 1:
            print(f"  {nom}: oyiga {n:.0f} qo'ng'iroq, kutilgan {k:.1f}, haqiqiy {h:.2f} "
                  f"-> kalibrlanmagan ehtimol bilan qaror va prognoz noto'g'ri")
        else:
            print(f"  {nom}: kutilgan {k:.1f} va haqiqiy {h:.1f} - yaqin")
    print(f"  sig'imsiz optimal: oyiga ~{natija['LogReg'][0]:.0f} qo'ng'iroq; sig'im {K} "
          f"-> cheklov bog'lovchi (3-misol)")

if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Kalibrlash o'lchovlari (o'quv 7-14, val 16-18) ===
  haqiqiy y60 ulushi val da: 0.0174
  model                  AUC    Brier  log-loss      ECE  p/haqiqiy
  LogReg              0.8482  0.01616    0.0722   0.0022       1.09
  HistGB              0.8319  0.01618    0.0730   0.0022       1.08
  LogReg balanced     0.8483  0.16114    0.4895   0.3160      19.14
  balanced + Platt    0.8486  0.01621    0.0723   0.0031       1.18

=== 2. Ishonchlilik jadvali (LogReg va LogReg balanced, 5 kvantil) ===
  LogReg           (bashorat/haqiqiy): 0.002/0.000  0.004/0.002  0.008/0.007  0.017/0.018  0.063/0.060
  LogReg balanced  (bashorat/haqiqiy): 0.076/0.000  0.157/0.003  0.272/0.007  0.444/0.016  0.718/0.061

=== 3. Qaror: "p * 0.30 * zarar > 0.02" bo'lsa qo'ng'iroq (sig'imsiz) ===
  model              qo'ng'iroq/oy  haqiqiy tejam  kutilgan
  LogReg                      1015          49.12     51.13
  HistGB                      1002          46.27     50.18
  LogReg balanced             4105           0.47   1308.80
  balanced + Platt            1060          49.54     57.67

=== 4. Top-200 (p * zarar) - sig'im bilan ===
  LogReg             tejam  28.85 mln so'm/oy
  HistGB             tejam  29.17 mln so'm/oy
  LogReg balanced    tejam  28.05 mln so'm/oy
  balanced + Platt   tejam  29.34 mln so'm/oy

=== 5. Xulosa (natijadan hisoblangan) ===
  LogReg: kutilgan 51.1 va haqiqiy 49.1 - yaqin
  HistGB: kutilgan 50.2 va haqiqiy 46.3 - yaqin
  LogReg balanced: oyiga 4105 qo'ng'iroq, kutilgan 1308.8, haqiqiy 0.47 -> kalibrlanmagan ehtimol bilan qaror va prognoz noto'g'ri
  balanced + Platt: kutilgan 57.7 va haqiqiy 49.5 - yaqin
  sig'imsiz optimal: oyiga ~1015 qo'ng'iroq; sig'im 200 -> cheklov bog'lovchi (3-misol)

Natija tahlili.

1-bo'lim — to'rtta model bir xil val da. Ularning AUC si deyarli bir xil (0.8319-0.8486), kalibrlashi esa keskin farq qiladi. LogReg va HistGB ning ECE si 0.0022, bashorat darajasi haqiqatdan 8-9% yuqori (1.09 va 1.08). Bu 29.2 da ko'rgan narsa: val oylarining bazaviy ulushi (0.0174) o'quvdagidan past. class_weight="balanced" bilan o'qitilgan LogReg ning AUC si 0.8483, lekin ehtimollari 19.14 barobar oshirilgan: Brier 0.16114, ECE 0.3160. Uni 13-14 oylarida Platt bilan kalibrlasak, tartib saqlanadi (AUC 0.8486), Brier esa 0.01621 ga qaytadi.

2-bo'lim — ishonchlilik jadvali. LogReg ning bashorati har bo'lakda haqiqatga yaqin: eng xavfli beshdan birda 0.063 va 0.060. Balanced modelning eng past xavfli beshdan biri 0.076 deydi, haqiqat esa 0.000.

3-bo'lim — qaror ehtimolga tayanadi. Sig'imsiz siyosat "EV > 0 bo'lsa qo'ng'iroq" qilinsa:

  • LogReg: oyiga 1015 qo'ng'iroq, haqiqiy tejam 49.12, kutilgan 51.13. Prognoz haqiqatga yaqin.
  • Balanced: 4105 qo'ng'iroq, ya'ni deyarli hammaga. Haqiqiy tejam 0.47, "kutilgan" 1308.8 mln so'm.
  • Platt dan keyin: 1060 qo'ng'iroq, 49.54. Muammo tuzatildi.

Kalibrlanmagan model rahbariyatga ikki marta yolg'on gapiradi: noto'g'ri qaror beradi va noto'g'ri prognoz qiladi.

4-bo'lim — sig'im bilan (top-200) to'rttasi ham 28.05-29.34 oralig'ida. Tartib deyarli bir xil bo'lgani uchun top-K kalibrlashga kam sezgir. Lekin to'liq befarq ham emas: p * zarar da p ning shakli muhim, va balanced model eng past tejamni (28.05) berdi. Asosiy saboq: siyosat faqat top-K bo'lsa, kalibrlash xatosi yashirinib qoladi. Qaror "EV > 0" ga yoki kutilgan tejam prognoziga o'tgan kuni esa bu xato pulga aylanadi.

5-bo'lim — xulosa. Sig'imsiz optimal siyosat oyiga ~`1015qo'ng'iroq qilardi, sig'im esa200`. Demak cheklov bog'lovchi: qo'shimcha sig'im qiymat beradi, va bu 3-misolda o'lchanadi.

Misol 3 — Siyosat va sig'im: top-K, kutilgan tejam, qo'shimcha operator qiymati va segmentlar

python
"""Siyosat va sig'im: top-K, kutilgan tejam, qo'shimcha operator qiymati; segmentlar bo'yicha xato."""

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

HUDUDLAR = ["Toshkent", "Samarqand", "Farg'ona", "Andijon", "Buxoro", "Qashqadaryo"]
OYLAR = 24                               # 1 = 2024-01, ..., 24 = 2025-12
BELGILAR = ["yosh", "daromad", "limit", "avto_tolov", "maosh_loyihasi", "staj",
            "util", "util_3", "util_ozg", "kechikish_6", "tolov_nisbati_3",
            "tolov_kuni_3", "naqd_soni_3", "naqd_summa_3", "xarid_soni_3"]
K, SAMARA, NARX = 200, 0.30, 0.02        # oyiga qo'ng'iroq, oldini olish, mln so'm


def sigmoid(z):
    return 1 / (1 + np.exp(-z))


def oy_nomi(m):
    return f"{2024 + (m - 1) // 12}-{(m - 1) % 12 + 1:02d}"


def yarat_bank(seed=29, n=6000):
    """Kredit karta portfeli: mijozlar, hisobvaraqlar, tolovlar, tranzaksiyalar."""
    rng = np.random.default_rng(seed)
    ids = np.arange(1, n + 1)
    yosh = rng.integers(21, 66, n)
    maosh = rng.random(n) < 0.45
    daromad = np.round(np.exp(rng.normal(np.log(6.0), 0.45, n)), 2)   # mln so'm
    ochilgan = np.where(rng.random(n) < 0.7, rng.integers(-40, 1, n),
                        rng.integers(1, 20, n))
    xavf = (rng.normal(0, 1, n) - 0.5 * maosh - 0.8 * np.log(daromad / 6)
            + 0.015 * (35 - yosh))                     # yashirin: moliyaviy xavf
    unut = rng.normal(0, 1, n) + 0.03 * (35 - yosh)     # yashirin: unutuvchanlik
    avto = (rng.random(n) < sigmoid(-0.2 - 0.9 * unut)).astype(int)
    limit = np.round(daromad * rng.uniform(1.5, 3.5, n), 1)
    hudud = rng.choice(HUDUDLAR, n, p=[0.34, 0.15, 0.14, 0.13, 0.12, 0.12])
    mj = pd.DataFrame({"mijoz_id": ids, "yosh": yosh, "hudud": hudud,
                       "daromad": daromad, "limit": limit, "avto_tolov": avto,
                       "maosh_loyihasi": maosh.astype(int), "ochilgan_oy": ochilgan})
    s, oldingi, ketma = rng.normal(0, 1, n), np.zeros(n, int), np.zeros(n, int)
    hv, tl, tr = [], [], []
    for m in range(1, OYLAR + 1):
        faol = (ochilgan <= m) & (ketma < 3)            # 3 ketma-ket kechikish - defolt
        s = 0.75 * s + 0.66 * rng.normal(0, 1, n)
        d = 0.8 * xavf + 0.7 * s + 0.15 * (m % 12 == 1) + 0.08 * max(m - 18, 0)
        util = np.clip(sigmoid(-0.4 + 0.55 * d + rng.normal(0, 0.5, n)), 0.01, 1)
        balans = np.round(util * limit, 3)
        min_t = np.round(np.maximum(0.05 * balans, 0.05), 3)
        for turi, lam in [("xarid", np.exp(1.2 + 0.15 * np.log(daromad / 6) - 0.1 * d)),
                          ("naqd", np.exp(-1.6 + 0.6 * d)),
                          ("onlayn", np.exp(0.6 - 0.02 * (yosh - 35)))]:
            i = np.repeat(np.arange(n), rng.poisson(lam) * faol)
            summa = (rng.uniform(0.2, 1.5, i.size) * (1 + 0.3 * d[i].clip(0))
                     if turi == "naqd" else np.exp(rng.normal(np.log(0.25), 0.8, i.size)))
            tr.append(pd.DataFrame({"mijoz_id": ids[i], "oy": m,
                                    "kun": rng.integers(1, 29, i.size), "turi": turi,
                                    "summa": np.round(summa, 3)}))
        q = rng.random(n) < sigmoid(-3.7 + 1.3 * d + 1.2 * (util > 0.85)
                                    + 3.8 * (oldingi == 2))          # qiyinchilik
        u = ~q & (rng.random(n) < sigmoid(-3.4 + 1.1 * unut - 1.8 * avto)
                  * (oldingi != 1))                                  # unutish
        miss = (q | u) & faol
        ulush = sigmoid(0.3 - 1.2 * d + rng.normal(0, 0.5, n))
        summa = np.where(miss, min_t * rng.uniform(0, 0.6, n),
                         min_t + (balans - min_t).clip(0) * ulush)
        tolaydi = faol & (~miss | (rng.random(n) < 0.4))
        kun = np.clip(np.round(6 + 14 * sigmoid(unut + 0.5 * d)
                               + rng.normal(0, 3, n)), 1, 25)
        hv.append(pd.DataFrame({"mijoz_id": ids[faol], "oy": m,
                                "balans": balans[faol], "min_tolov": min_t[faol]}))
        if m < OYLAR:                    # m-oy hisobvarag'i (m+1)-oyda to'lanadi
            tl.append(pd.DataFrame({"mijoz_id": ids[tolaydi], "hisob_oy": m,
                                    "tolov_oy": m + 1,
                                    "tolov_kuni": kun[tolaydi].astype(int),
                                    "summa": np.round(summa[tolaydi], 3)}))
        ketma = np.where(faol, np.where(miss, ketma + 1, 0), ketma)
        oldingi = np.where(miss, np.where(q, 2, 1), 0)
    return {"mijozlar": mj, "hisobvaraqlar": pd.concat(hv, ignore_index=True),
            "tolovlar": pd.concat(tl, ignore_index=True),
            "tranzaksiyalar": pd.concat(tr, ignore_index=True)}


def orta(a):
    """Qator bo'yicha NaN siz o'rtacha (bo'sh qatorda NaN, ogohlantirishsiz)."""
    k = (~np.isnan(a)).sum(axis=1)
    return np.where(k > 0, np.nansum(a, axis=1) / np.maximum(k, 1), np.nan)


def belgilar_jadvali(bank, oylar=range(7, 23)):
    """Har as-of oy t (oy oxiri): faqat t gacha ma'lum belgilar + t, t+1 natijasi."""
    mj = bank["mijozlar"]
    ids, kol = mj["mijoz_id"].to_numpy(), np.arange(1, OYLAR + 1)

    def mat(df, qiymat, ustun="oy"):
        return (df.pivot(index="mijoz_id", columns=ustun, values=qiymat)
                .reindex(index=ids, columns=kol).to_numpy(dtype=float))

    hv, tl = bank["hisobvaraqlar"], bank["tolovlar"]
    bal, mint = mat(hv, "balans"), mat(hv, "min_tolov")
    tol, kun = mat(tl, "summa", "hisob_oy"), mat(tl, "tolov_kuni", "hisob_oy")
    bor = ~np.isnan(bal)
    miss = bor & ~(np.nan_to_num(tol) >= mint - 1e-9)    # minimal to'lov qilinmagan
    g = (bank["tranzaksiyalar"].groupby(["turi", "mijoz_id", "oy"])["summa"]
         .agg(["size", "sum"]))

    def tr_mat(turi, ust):
        return (g.loc[turi, ust].unstack("oy").reindex(index=ids, columns=kol)
                .fillna(0).to_numpy())

    naqd_n, naqd_s = tr_mat("naqd", "size"), tr_mat("naqd", "sum")
    xarid_n = tr_mat("xarid", "size")
    util = bal / mj["limit"].to_numpy()[:, None]
    och = mj["ochilgan_oy"].to_numpy()
    qismlar = []
    for t in oylar:
        c = t - 1                                        # t-oy ustuni (0 dan)
        ok = (och <= t - 1) & bor[:, c] & bor[:, c - 1] & ~miss[:, c - 1]
        u3 = util[:, c - 3]
        f = mj.drop(columns="ochilgan_oy").assign(
            oy=t, staj=t - och, util=util[:, c], util_3=orta(util[:, c - 2:c + 1]),
            util_ozg=util[:, c] - np.where(np.isnan(u3), util[:, c], u3),
            kechikish_6=miss[:, max(c - 6, 0):c].sum(axis=1),
            tolov_nisbati_3=orta(np.nan_to_num(tol[:, c - 3:c]) / bal[:, c - 3:c]),
            tolov_kuni_3=orta(kun[:, c - 3:c]),
            naqd_soni_3=naqd_n[:, c - 2:c + 1].sum(axis=1),
            naqd_summa_3=naqd_s[:, c - 2:c + 1].sum(axis=1),
            xarid_soni_3=xarid_n[:, c - 2:c + 1].sum(axis=1),
            balans=bal[:, c], y30=(miss[:, c] | miss[:, c + 1]).astype(int),
            y60=(miss[:, c] & miss[:, c + 1]).astype(int))
        qismlar.append(f[ok])
    df = pd.concat(qismlar, ignore_index=True)
    df["tolov_kuni_3"] = df["tolov_kuni_3"].fillna(25.0)
    df["tolov_nisbati_3"] = df["tolov_nisbati_3"].fillna(0.0)
    df["zarar"] = 0.45 * df["balans"]              # DPD60 bo'lsa kutilgan zarar
    df["qiymat"] = SAMARA * df["zarar"] * df["y60"] - NARX   # qo'ng'iroq qiymati
    return df


def tanla(va, ball, k, faqat_musbat_ev=False, p=None):
    """Har oyda ball bo'yicha top-k indekslari (ixtiyoriy: kutilgan qiymat > 0 shart)."""
    oy = va["oy"].to_numpy()
    tanlangan = []
    for m in np.unique(oy):
        g = np.flatnonzero(oy == m)
        g = g[np.argsort(-ball[g], kind="stable")[:k]]
        if faqat_musbat_ev:
            g = g[p[g] * SAMARA * va["zarar"].to_numpy()[g] > NARX]
        tanlangan.append(g)
    return np.concatenate(tanlangan)


def main() -> None:
    df = belgilar_jadvali(yarat_bank())
    tr, va = df[df.oy <= 14], df[df.oy.between(16, 18)].reset_index(drop=True)
    m = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
    p = m.fit(tr[BELGILAR], tr["y60"]).predict_proba(va[BELGILAR])[:, 1]
    z, q, y = va["zarar"].to_numpy(), va["qiymat"].to_numpy(), va["y60"].to_numpy()
    ev = p * SAMARA * z - NARX                     # har qo'ng'iroqning kutilgan qiymati
    oylar = va["oy"].nunique()

    print("=== 1. Siyosatlar (K = 200, val 16-18, LogReg) ===")
    siyosatlar = {
        "top-200: p": tanla(va, p, K),
        "top-200: p * zarar": tanla(va, p * z, K),
        "top-200: p * zarar, EV > 0": tanla(va, p * z, K, True, p),
    }
    for nom, s in siyosatlar.items():
        print(f"  {nom:<28} qo'ng'iroq/oy {len(s) / oylar:6.1f}  tejam {q[s].sum() / oylar:6.2f}"
              f"  kutilgan {ev[s].sum() / oylar:6.2f}")

    print("\n=== 2. Sig'im egri chizig'i: K oshsa (oyiga, mln so'm) ===")
    sarlavha = "+100 qo'ng'iroq"
    print(f"  {'K':>5} {'tejam':>7} {'kutilgan':>9} {sarlavha:>16}")
    oldingi = None
    egri = {}
    for k in [100, 200, 300, 400, 600, 800, 1000, 1500]:
        s = tanla(va, p * z, k)
        egri[k] = (q[s].sum() / oylar, ev[s].sum() / oylar)
        chegara = ("" if oldingi is None
                   else f"{(egri[k][1] - oldingi[1]) / (k - oldingi[0]) * 100:+.2f}")
        print(f"  {k:>5} {egri[k][0]:>7.2f} {egri[k][1]:>9.2f} {chegara:>16}")
        oldingi = (k, egri[k][1])
    eng_k = max(egri, key=lambda k: egri[k][1])
    print(f"  kutilgan tejam maksimumi: K = {eng_k} atrofida")

    print("\n=== 3. Segmentlar bo'yicha xato tahlili (K = 200, p * zarar) ===")
    tan = np.zeros(len(va), bool)
    tan[siyosatlar["top-200: p * zarar"]] = True
    segmentlar = {
        "yangi (staj<6)": va.staj < 6, "eski": va.staj >= 6,
        "maosh loyihasi": va.maosh_loyihasi == 1, "maoshsiz": va.maosh_loyihasi == 0,
        "avto to'lov": va.avto_tolov == 1, "avto yo'q": va.avto_tolov == 0,
    }
    for h in sorted(va.hudud.unique()):
        segmentlar[h] = va.hudud == h
    print(f"  {'segment':<16} {'n':>6} {'y60':>5} {'p/haqiqiy':>10} {'SE':>6} "
          f"{'tanlangan':>10} {'recall':>7}")
    shubhali = []
    for nom, s in segmentlar.items():
        s = s.to_numpy()
        n1 = y[s].sum()
        kal = p[s].sum() / max(n1, 1)
        se = kal / np.sqrt(max(n1, 1))                 # Puasson: nisbiy xato ~ 1/sqrt(n)
        recall = (tan & s & (y == 1)).sum() / max(n1, 1)
        print(f"  {nom:<16} {s.sum():>6} {n1:>5} {kal:>10.2f} {se:>6.2f} "
              f"{tan[s].mean():>10.3f} {recall:>7.3f}")
        if abs(kal - 1) > 2 * se:
            shubhali.append(nom)

    print("\n=== 4. Xulosa (natijadan hisoblangan) ===")
    a, b = egri[200][1], egri[300][1]
    print(f"  sig'im 200 -> 300: sof kutilgan tejam {b - a:+.2f} mln so'm/oy "
          f"(qo'ng'iroq narxidan keyin, har qo'shimcha qo'ng'iroq ~{(b - a) / 100 * 1000:.0f} ming)")
    print(f"  qo'shimcha sig'imning boshqa xarajatlari (ish o'rni, o'qitish) oyiga "
          f"{b - a:.1f} mln so'mdan kam bo'lsa - foydali")
    if shubhali:
        print(f"  kalibrlash 2*SE dan ko'p siljigan segmentlar: {shubhali}")
    else:
        print("  hech bir segmentda kalibrlash 2*SE dan ko'p siljimagan")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Siyosatlar (K = 200, val 16-18, LogReg) ===
  top-200: p                   qo'ng'iroq/oy  200.0  tejam  26.32  kutilgan  30.49
  top-200: p * zarar           qo'ng'iroq/oy  200.0  tejam  28.85  kutilgan  33.66
  top-200: p * zarar, EV > 0   qo'ng'iroq/oy  200.0  tejam  28.85  kutilgan  33.66

=== 2. Sig'im egri chizig'i: K oshsa (oyiga, mln so'm) ===
      K   tejam  kutilgan  +100 qo'ng'iroq
    100   23.27     24.46
    200   28.85     33.66            +9.20
    300   35.83     39.45            +5.80
    400   38.55     43.33            +3.88
    600   48.28     48.10            +2.39
    800   48.44     50.42            +1.16
   1000   48.76     51.11            +0.35
   1500   45.62     48.90            -0.44
  kutilgan tejam maksimumi: K = 1000 atrofida

=== 3. Segmentlar bo'yicha xato tahlili (K = 200, p * zarar) ===
  segment               n   y60  p/haqiqiy     SE  tanlangan  recall
  yangi (staj<6)     1369    30       1.19   0.22      0.073   0.400
  eski              11834   200       1.07   0.08      0.042   0.250
  maosh loyihasi     6383    82       1.20   0.13      0.028   0.195
  maoshsiz           6820   148       1.03   0.08      0.062   0.311
  avto to'lov        6848   111       1.04   0.10      0.037   0.234
  avto yo'q          6355   119       1.13   0.10      0.055   0.303
  Andijon            1773    35       1.04   0.18      0.049   0.286
  Buxoro             1543    25       1.18   0.24      0.047   0.240
  Farg'ona           1883    31       1.07   0.19      0.040   0.323
  Qashqadaryo        1505    31       0.97   0.17      0.049   0.258
  Samarqand          2004    34       1.14   0.20      0.054   0.235
  Toshkent           4495    74       1.11   0.13      0.041   0.270

=== 4. Xulosa (natijadan hisoblangan) ===
  sig'im 200 -> 300: sof kutilgan tejam +5.80 mln so'm/oy (qo'ng'iroq narxidan keyin, har qo'shimcha qo'ng'iroq ~58 ming)
  qo'shimcha sig'imning boshqa xarajatlari (ish o'rni, o'qitish) oyiga 5.8 mln so'mdan kam bo'lsa - foydali
  hech bir segmentda kalibrlash 2*SE dan ko'p siljimagan

Natija tahlili.

1-bo'lim — siyosatlar. p bo'yicha top-200 tejami 26.32, p * zarar bo'yicha 28.85. "EV > 0" sharti hech narsani o'zgartirmadi: top-200 ning hammasida kutilgan qiymat musbat, chunki sig'im bog'lovchi. Kutilgan tejam (33.66) haqiqiydan (28.85) biroz yuqori. Sabab: val da bashorat darajasi 1.09 (2-misol), ya'ni model xavfni biroz oshirib baholaydi.

2-bo'lim — sig'im egri chizig'i. Kutilgan tejam K = 1000 atrofida maksimumga yetadi (51.11), K = 1500 da esa kamayadi. Bu 2-misoldagi "EV > 0 bo'lganlar ~1015" bilan mos. Chegaraviy qiymat tez kamayadi:

Sig'im o'zgarishi +100 qo'ng'iroqning chegaraviy qiymati (mln so'm)
100 → 200 +9.20
200 → 300 +5.80
300 → 400 +3.88
400 → 600 +2.39
600 → 800 +1.16

Haqiqiy tejam ham shu yo'nalishda, lekin shovqinli (400 dan 600 ga 38.55 → 48.28 sakrash). Shuning uchun sig'im haqidagi taklif kutilgan egri chiziq asosida beriladi.

3-bo'lim — segmentlar. 12 ta segmentning hech birida kalibrlash nisbati 1 dan 2*SE dan ko'p farq qilmadi. Eng katta farqlar: maosh loyihasi 1.20 (SE 0.13), yangi mijozlar 1.19 (SE 0.22), Buxoro 1.18 (SE 0.24). Tanlangan ulush va recall esa segmentlar orasida keskin farq qiladi:

  • maosh loyihasidagi mijozlar 2.8% tanlangan (recall 0.195), maoshsizlar 6.2% (recall 0.311);
  • yangi mijozlarda recall 0.400, eskilarida 0.250.

Bu model xatosi emas. Maosh loyihasidagi mijozlarning xavfi pastroq, va bir xil xavfda ham ularning balansi (zarar) boshqacha bo'lishi mumkin. Lekin "qaysi guruhdagi DPD60 lar kamroq ushlanadi" degan savol adolatlilik savoli. U 29.11 da o'lchanadi va loyiha hujjatida ochiq qoladi.

4-bo'lim — biznes tilida. Sig'imni 200 dan 300 ga oshirish oyiga +5.80 mln so'm sof kutilgan tejam beradi (qo'ng'iroq narxi ayirilgandan keyin). Ya'ni har qo'shimcha qo'ng'iroq taxminan 58 ming so'm. Agar qo'shimcha sig'imning boshqa xarajatlari (ish o'rni, o'qitish) oyiga 5.8 mln so'mdan kam bo'lsa, taklif o'zini oqlaydi. Bu raqam qo'ng'iroq markazi rahbariga modelning AUC sidan ko'ra ko'proq narsani aytadi.

Misol 4 — Test bir marta: yakuniy model, test qulfi, qoida bilan juftlashgan natija va model kartasi

python
"""Test bir marta: yakuniy model, test qulfi, qoida bilan juftlashgan natija va model kartasi."""

import hashlib
import json
import pathlib
import tempfile

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

HUDUDLAR = ["Toshkent", "Samarqand", "Farg'ona", "Andijon", "Buxoro", "Qashqadaryo"]
OYLAR = 24                               # 1 = 2024-01, ..., 24 = 2025-12
BELGILAR = ["yosh", "daromad", "limit", "avto_tolov", "maosh_loyihasi", "staj",
            "util", "util_3", "util_ozg", "kechikish_6", "tolov_nisbati_3",
            "tolov_kuni_3", "naqd_soni_3", "naqd_summa_3", "xarid_soni_3"]
K, SAMARA, NARX = 200, 0.30, 0.02        # oyiga qo'ng'iroq, oldini olish, mln so'm


def sigmoid(z):
    return 1 / (1 + np.exp(-z))


def oy_nomi(m):
    return f"{2024 + (m - 1) // 12}-{(m - 1) % 12 + 1:02d}"


def yarat_bank(seed=29, n=6000):
    """Kredit karta portfeli: mijozlar, hisobvaraqlar, tolovlar, tranzaksiyalar."""
    rng = np.random.default_rng(seed)
    ids = np.arange(1, n + 1)
    yosh = rng.integers(21, 66, n)
    maosh = rng.random(n) < 0.45
    daromad = np.round(np.exp(rng.normal(np.log(6.0), 0.45, n)), 2)   # mln so'm
    ochilgan = np.where(rng.random(n) < 0.7, rng.integers(-40, 1, n),
                        rng.integers(1, 20, n))
    xavf = (rng.normal(0, 1, n) - 0.5 * maosh - 0.8 * np.log(daromad / 6)
            + 0.015 * (35 - yosh))                     # yashirin: moliyaviy xavf
    unut = rng.normal(0, 1, n) + 0.03 * (35 - yosh)     # yashirin: unutuvchanlik
    avto = (rng.random(n) < sigmoid(-0.2 - 0.9 * unut)).astype(int)
    limit = np.round(daromad * rng.uniform(1.5, 3.5, n), 1)
    hudud = rng.choice(HUDUDLAR, n, p=[0.34, 0.15, 0.14, 0.13, 0.12, 0.12])
    mj = pd.DataFrame({"mijoz_id": ids, "yosh": yosh, "hudud": hudud,
                       "daromad": daromad, "limit": limit, "avto_tolov": avto,
                       "maosh_loyihasi": maosh.astype(int), "ochilgan_oy": ochilgan})
    s, oldingi, ketma = rng.normal(0, 1, n), np.zeros(n, int), np.zeros(n, int)
    hv, tl, tr = [], [], []
    for m in range(1, OYLAR + 1):
        faol = (ochilgan <= m) & (ketma < 3)            # 3 ketma-ket kechikish - defolt
        s = 0.75 * s + 0.66 * rng.normal(0, 1, n)
        d = 0.8 * xavf + 0.7 * s + 0.15 * (m % 12 == 1) + 0.08 * max(m - 18, 0)
        util = np.clip(sigmoid(-0.4 + 0.55 * d + rng.normal(0, 0.5, n)), 0.01, 1)
        balans = np.round(util * limit, 3)
        min_t = np.round(np.maximum(0.05 * balans, 0.05), 3)
        for turi, lam in [("xarid", np.exp(1.2 + 0.15 * np.log(daromad / 6) - 0.1 * d)),
                          ("naqd", np.exp(-1.6 + 0.6 * d)),
                          ("onlayn", np.exp(0.6 - 0.02 * (yosh - 35)))]:
            i = np.repeat(np.arange(n), rng.poisson(lam) * faol)
            summa = (rng.uniform(0.2, 1.5, i.size) * (1 + 0.3 * d[i].clip(0))
                     if turi == "naqd" else np.exp(rng.normal(np.log(0.25), 0.8, i.size)))
            tr.append(pd.DataFrame({"mijoz_id": ids[i], "oy": m,
                                    "kun": rng.integers(1, 29, i.size), "turi": turi,
                                    "summa": np.round(summa, 3)}))
        q = rng.random(n) < sigmoid(-3.7 + 1.3 * d + 1.2 * (util > 0.85)
                                    + 3.8 * (oldingi == 2))          # qiyinchilik
        u = ~q & (rng.random(n) < sigmoid(-3.4 + 1.1 * unut - 1.8 * avto)
                  * (oldingi != 1))                                  # unutish
        miss = (q | u) & faol
        ulush = sigmoid(0.3 - 1.2 * d + rng.normal(0, 0.5, n))
        summa = np.where(miss, min_t * rng.uniform(0, 0.6, n),
                         min_t + (balans - min_t).clip(0) * ulush)
        tolaydi = faol & (~miss | (rng.random(n) < 0.4))
        kun = np.clip(np.round(6 + 14 * sigmoid(unut + 0.5 * d)
                               + rng.normal(0, 3, n)), 1, 25)
        hv.append(pd.DataFrame({"mijoz_id": ids[faol], "oy": m,
                                "balans": balans[faol], "min_tolov": min_t[faol]}))
        if m < OYLAR:                    # m-oy hisobvarag'i (m+1)-oyda to'lanadi
            tl.append(pd.DataFrame({"mijoz_id": ids[tolaydi], "hisob_oy": m,
                                    "tolov_oy": m + 1,
                                    "tolov_kuni": kun[tolaydi].astype(int),
                                    "summa": np.round(summa[tolaydi], 3)}))
        ketma = np.where(faol, np.where(miss, ketma + 1, 0), ketma)
        oldingi = np.where(miss, np.where(q, 2, 1), 0)
    return {"mijozlar": mj, "hisobvaraqlar": pd.concat(hv, ignore_index=True),
            "tolovlar": pd.concat(tl, ignore_index=True),
            "tranzaksiyalar": pd.concat(tr, ignore_index=True)}


def orta(a):
    """Qator bo'yicha NaN siz o'rtacha (bo'sh qatorda NaN, ogohlantirishsiz)."""
    k = (~np.isnan(a)).sum(axis=1)
    return np.where(k > 0, np.nansum(a, axis=1) / np.maximum(k, 1), np.nan)


def belgilar_jadvali(bank, oylar=range(7, 23)):
    """Har as-of oy t (oy oxiri): faqat t gacha ma'lum belgilar + t, t+1 natijasi."""
    mj = bank["mijozlar"]
    ids, kol = mj["mijoz_id"].to_numpy(), np.arange(1, OYLAR + 1)

    def mat(df, qiymat, ustun="oy"):
        return (df.pivot(index="mijoz_id", columns=ustun, values=qiymat)
                .reindex(index=ids, columns=kol).to_numpy(dtype=float))

    hv, tl = bank["hisobvaraqlar"], bank["tolovlar"]
    bal, mint = mat(hv, "balans"), mat(hv, "min_tolov")
    tol, kun = mat(tl, "summa", "hisob_oy"), mat(tl, "tolov_kuni", "hisob_oy")
    bor = ~np.isnan(bal)
    miss = bor & ~(np.nan_to_num(tol) >= mint - 1e-9)    # minimal to'lov qilinmagan
    g = (bank["tranzaksiyalar"].groupby(["turi", "mijoz_id", "oy"])["summa"]
         .agg(["size", "sum"]))

    def tr_mat(turi, ust):
        return (g.loc[turi, ust].unstack("oy").reindex(index=ids, columns=kol)
                .fillna(0).to_numpy())

    naqd_n, naqd_s = tr_mat("naqd", "size"), tr_mat("naqd", "sum")
    xarid_n = tr_mat("xarid", "size")
    util = bal / mj["limit"].to_numpy()[:, None]
    och = mj["ochilgan_oy"].to_numpy()
    qismlar = []
    for t in oylar:
        c = t - 1                                        # t-oy ustuni (0 dan)
        ok = (och <= t - 1) & bor[:, c] & bor[:, c - 1] & ~miss[:, c - 1]
        u3 = util[:, c - 3]
        f = mj.drop(columns="ochilgan_oy").assign(
            oy=t, staj=t - och, util=util[:, c], util_3=orta(util[:, c - 2:c + 1]),
            util_ozg=util[:, c] - np.where(np.isnan(u3), util[:, c], u3),
            kechikish_6=miss[:, max(c - 6, 0):c].sum(axis=1),
            tolov_nisbati_3=orta(np.nan_to_num(tol[:, c - 3:c]) / bal[:, c - 3:c]),
            tolov_kuni_3=orta(kun[:, c - 3:c]),
            naqd_soni_3=naqd_n[:, c - 2:c + 1].sum(axis=1),
            naqd_summa_3=naqd_s[:, c - 2:c + 1].sum(axis=1),
            xarid_soni_3=xarid_n[:, c - 2:c + 1].sum(axis=1),
            balans=bal[:, c], y30=(miss[:, c] | miss[:, c + 1]).astype(int),
            y60=(miss[:, c] & miss[:, c + 1]).astype(int))
        qismlar.append(f[ok])
    df = pd.concat(qismlar, ignore_index=True)
    df["tolov_kuni_3"] = df["tolov_kuni_3"].fillna(25.0)
    df["tolov_nisbati_3"] = df["tolov_nisbati_3"].fillna(0.0)
    df["zarar"] = 0.45 * df["balans"]              # DPD60 bo'lsa kutilgan zarar
    df["qiymat"] = SAMARA * df["zarar"] * df["y60"] - NARX   # qo'ng'iroq qiymati
    return df


def lr():
    return make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))


def oylik_tejam(d, ball, k=K):
    oy = d["oy"].to_numpy()
    q = d["qiymat"].to_numpy()
    return np.array([q[g][np.argsort(-ball[g], kind="stable")[:k]].sum()
                     for g in [np.flatnonzero(oy == m) for m in np.unique(oy)]])


def testni_och(papka, yozuv):
    """Test belgilariga faqat bir marta ruxsat: jurnal fayli qulf vazifasini bajaradi."""
    jurnal = pathlib.Path(papka) / "test_jurnali.json"
    if jurnal.exists():
        eski = json.loads(jurnal.read_text(encoding="utf8"))
        raise RuntimeError(f"test allaqachon ochilgan (reja {eski['reja_muhri']})")
    jurnal.write_text(json.dumps(yozuv, ensure_ascii=False), encoding="utf8")


def main() -> None:
    df = belgilar_jadvali(yarat_bank())
    reja = {"model": "LogReg", "siyosat": "top-200, p * zarar", "oquv": "as-of 7-18",
            "test": "as-of 20-22", "metrika": "oylik tejam; qoida bilan juftlashgan"}
    muhr = hashlib.sha256(json.dumps(reja, sort_keys=True).encode()).hexdigest()[:12]

    print("=== 1. Testdan oldin: CV bahosi (7 oy, 12-18) ===")
    cv_m, cv_q = [], []
    for t in range(12, 19):
        tr, te = df[df.oy <= t - 2], df[df.oy == t]
        p = lr().fit(tr[BELGILAR], tr.y60).predict_proba(te[BELGILAR])[:, 1]
        cv_m.append(oylik_tejam(te, p * te.zarar.to_numpy())[0])
        cv_q.append(oylik_tejam(te, te.util.to_numpy())[0])
    d = np.array(cv_m) - np.array(cv_q)
    print(f"  model {np.mean(cv_m):.2f}, qoida {np.mean(cv_q):.2f}, farq {d.mean():+.2f} "
          f"(SE {d.std(ddof=1) / np.sqrt(len(d)):.2f}) mln so'm/oy")
    print(f"  reja muhri: {muhr} -> yakuniy model as-of 7-18 da o'qitiladi (19 - bo'shliq)")

    oquv = df[df.oy <= 18]
    yakuniy = lr().fit(oquv[BELGILAR], oquv.y60)
    test = df[df.oy.between(20, 22)].reset_index(drop=True)
    with tempfile.TemporaryDirectory() as papka:
        testni_och(papka, {"reja_muhri": muhr, **reja})
        print("\n=== 2. TEST OCHILDI (bir marta): as-of 20-22 ===")
        p = yakuniy.predict_proba(test[BELGILAR])[:, 1]
        z = test.zarar.to_numpy()
        tm, tq = oylik_tejam(test, p * z), oylik_tejam(test, test.util.to_numpy())
        for i, m in enumerate(range(20, 23)):
            g = test.oy == m
            print(f"  {oy_nomi(m)}: model {tm[i]:6.2f}, qoida {tq[i]:6.2f}, "
                  f"y60 {test.y60[g].mean():.4f}, bashorat {p[g].mean():.4f}")
        rng = np.random.default_rng(3)
        guruh = [np.flatnonzero(test.oy.to_numpy() == m) for m in range(20, 23)]
        farqlar = []
        for _ in range(300):
            idx = [rng.choice(g, len(g)) for g in guruh]
            sub = test.iloc[np.concatenate(idx)].reset_index(drop=True)
            pp = p[np.concatenate(idx)]
            farqlar.append(oylik_tejam(sub, pp * sub.zarar.to_numpy()).mean()
                           - oylik_tejam(sub, sub.util.to_numpy()).mean())
        farq, se = tm.mean() - tq.mean(), np.std(farqlar, ddof=1)
        print(f"  o'rtacha: model {tm.mean():.2f}, qoida {tq.mean():.2f}, farq {farq:+.2f} "
              f"(bootstrap SE {se:.2f}), sezilarli: {farq > 2 * se}")
        auc = roc_auc_score(test.y60, p)
        kal = p.mean() / test.y60.mean()
        print(f"  AUC {auc:.4f}; bashorat / haqiqiy ulush: {kal:.3f}")

        print("\n=== 3. Testni qayta ochishga urinish ===")
        try:
            testni_och(papka, {"reja_muhri": "yangi", **reja})
        except RuntimeError as xato:
            print(f"  rad etildi: {xato}")

    print("\n=== 4. Model kartasi (raqamlar natijadan) ===")
    karta = [
        "## Model kartasi: kredit karta kechikishi - profilaktik qo'ng'iroq (v1)",
        "- Vazifa: har oy top-200 mijoz (p * kutilgan zarar); belgi y60 (60 kunda 60+ kun)",
        f"- Model: LogReg + StandardScaler, {len(BELGILAR)} belgi; o'quv as-of 7-18 "
        f"({len(oquv)} qator)",
        f"- CV (12-18): tejam {np.mean(cv_m):.2f} mln so'm/oy, qoidadan {d.mean():+.2f}",
        f"- Test (20-22): tejam {tm.mean():.2f} mln so'm/oy, qoidadan {farq:+.2f} "
        f"(SE {se:.2f}); AUC {auc:.4f}",
        f"- Kalibrlash testda: bashorat/haqiqiy {kal:.3f} -> oylik monitoring va qayta "
        f"kalibrlash",
        "- Cheklovlar: samara 0.30 va zarar 0.45*balans faraziy; pilot kerak 29.4-bob",
        "- Foydalanmang: kredit berish yoki limit qarori uchun",
        f"- Reja muhri: {muhr}",
    ]
    for q in karta:
        print("  " + q)

    print("\n=== 5. Xulosa (natijadan hisoblangan) ===")
    if farq > 2 * se:
        print(f"  testda model qoidadan sezilarli yaxshi: {farq:+.2f} mln so'm/oy")
    else:
        print(f"  testda farq sezilarli emas ({farq:+.2f}, SE {se:.2f}) - halol yoziladi")
    print(f"  CV bahosi {np.mean(cv_m):.2f}, test {tm.mean():.2f}: "
          f"{'test yuqori' if tm.mean() > np.mean(cv_m) else 'test past'}")
    if kal < 0.9:
        print("  testda model xavfni kam baholadi (drift) -> kalibrlash monitoringi shart")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Testdan oldin: CV bahosi (7 oy, 12-18) ===
  model 30.98, qoida 24.58, farq +6.40 (SE 1.49) mln so'm/oy
  reja muhri: dbfafe034225 -> yakuniy model as-of 7-18 da o'qitiladi (19 - bo'shliq)

=== 2. TEST OCHILDI (bir marta): as-of 20-22 ===
  2025-08: model  47.69, qoida  32.88, y60 0.0260, bashorat 0.0198
  2025-09: model  43.71, qoida  30.55, y60 0.0227, bashorat 0.0203
  2025-10: model  35.58, qoida  29.79, y60 0.0212, bashorat 0.0215
  o'rtacha: model 42.33, qoida 31.08, farq +11.25 (bootstrap SE 3.98), sezilarli: True
  AUC 0.8296; bashorat / haqiqiy ulush: 0.880

=== 3. Testni qayta ochishga urinish ===
  rad etildi: test allaqachon ochilgan (reja dbfafe034225)

=== 4. Model kartasi (raqamlar natijadan) ===
  ## Model kartasi: kredit karta kechikishi - profilaktik qo'ng'iroq (v1)
  - Vazifa: har oy top-200 mijoz (p * kutilgan zarar); belgi y60 (60 kunda 60+ kun)
  - Model: LogReg + StandardScaler, 15 belgi; o'quv as-of 7-18 (50894 qator)
  - CV (12-18): tejam 30.98 mln so'm/oy, qoidadan +6.40
  - Test (20-22): tejam 42.33 mln so'm/oy, qoidadan +11.25 (SE 3.98); AUC 0.8296
  - Kalibrlash testda: bashorat/haqiqiy 0.880 -> oylik monitoring va qayta kalibrlash
  - Cheklovlar: samara 0.30 va zarar 0.45*balans faraziy; pilot kerak 29.4-bob
  - Foydalanmang: kredit berish yoki limit qarori uchun
  - Reja muhri: dbfafe034225

=== 5. Xulosa (natijadan hisoblangan) ===
  testda model qoidadan sezilarli yaxshi: +11.25 mln so'm/oy
  CV bahosi 30.98, test 42.33: test yuqori
  testda model xavfni kam baholadi (drift) -> kalibrlash monitoringi shart

Natija tahlili.

1-bo'lim — testdan oldin CV bahosi qayd etildi: model oyiga 30.98, qoida 24.58, farq +6.40 (SE 1.49). Yakuniy reja muhrlandi (dbfafe034225): LogReg, top-200 p * zarar, o'quv as-of 7-18. Bu reja 1-misoldagi baholash rejasidan farq qiladi, chunki u endi tanlangan yakuniy konfiguratsiyani tasvirlaydi.

2-bo'lim — test bir marta ochildi. Model uch oyning uchalasida qoidadan yaxshi: 47.69 va 32.88, 43.71 va 30.55, 35.58 va 29.79. O'rtacha farq +11.25 (bootstrap SE 3.98) — sezilarli. Ikki narsa kutilmagan va halol yoziladi:

  1. Test tejami CV dan ancha yuqori (42.33 va 30.98). Bu model "yaxshilangani" uchun emas. Test oylarida makroiqtisodiy yuklama kuchaygan: 2025-08 da y60 ulushi 0.0260, o'quvda esa 0.0209. DPD60 ko'paygani uchun har qo'ng'iroqda tejash imkoniyati ham ko'paygan. Rahbariyatga "model 42 mln tejaydi" deb va'da berish xato. To'g'ri ifoda: "tejam yuklamaga bog'liq; CV da 31, yuqori yuklama oylarida 42 atrofida".
  2. Model xavfni kam baholadi. Bashorat / haqiqiy ulush 0.880, 2025-08 da esa 0.0198 va 0.0260. Top-K tartibi bundan deyarli zarar ko'rmadi (AUC 0.8296). Lekin kutilgan tejam prognozi va "EV > 0" qarori pastga siljiydi. Bu 27.11-27.12 dagi monitoringning aniq sababi: oylik kalibrlash nazorati va chegaradan oshsa qayta kalibrlash.

3-bo'lim — testni ikkinchi marta ochishga urinish jurnal (qulf) tomonidan rad etildi. Haqiqiy loyihada bu jurnal versiyalangan omborda (27.4-27.5) saqlanadi va test natijasi faqat birinchi yozuvdan olinadi.

4-bo'lim — model kartasi raqamlar bilan avtomatik to'ldirildi. Unda CV va test natijasi, kalibrlash siljishi, faraziy parametrlar va taqiqlangan foydalanishlar bor. 2.8 dagi shablonning qolgan bandlari (segmentlar, sig'im egri chizig'i) 3-misol natijalaridan qo'shiladi.

5-bo'lim — xulosa shartlar bilan chiqarildi. Model testda sezilarli yaxshi, test CV dan yuqori (sababi yuklama), bashorat darajasi past (drift). Uchalasi ham 29.4 da natijani taqdim etganda aytilishi kerak.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"Val da eng yaxshi natija bergan variantni olamiz" Oldindan yozilgan reja va qaror qoidasi
"HistGB o'rtachada yuqori — uni tanlaymiz" Farq -0.90 (SE 0.98) — shovqin; eng sodda munosib LogReg
"Tarmoq har doim kuchliroq" Bu jadval ma'lumotida MLP sezilarli yomon (-3.31)
"AUC bir xil — modellar bir xil" Balanced modelda AUC 0.8483, lekin "EV > 0" siyosati tejami 0.47
"Top-K da kalibrlash muhim emas" Tartib saqlanadi, lekin prognoz va EV qarori buziladi
"Sig'im — berilgan" +100 qo'ng'iroq oyiga +5.80 mln so'm — biznes qarori
"Segmentdagi 1.18 nisbat — muammo" SE 0.24 — shovqin; 2*SE qoidasi
"Test yaxshi chiqdi — model yaxshilangan" Test tejami yuklama (drift) tufayli yuqori
"Test yomon chiqsa, yana bir marta sinaymiz" Yangi reja va yangi test ma'lumoti bilan
"Model kartasi — rasmiyatchilik" Unda chegaralar, drift va taqiqlangan foydalanish yoziladi

6. Keng tarqalgan xatolar va yechimlari

1. Rejasiz tanlov

python
eng = max(variantlar, key=lambda v: val_auc(v))                         # ⚠️ 50 variant
REJA = {...}; muhr = sha256(REJA); eng = qaror_qoidasi(cv_natija)       # ✅

2. Juftlashmagan taqqoslash

python
print(np.mean(tejam_a), np.mean(tejam_b))                               # ⚠️
d = np.array(tejam_a) - np.array(tejam_b); se = d.std(ddof=1) / np.sqrt(len(d))  # ✅

3. Balanced ehtimol bilan qaror

python
m = LogisticRegression(class_weight="balanced"); qongiroq = p * 0.3 * z > 0.02  # ⚠️
p = platt(p_kal, y_kal)(p)  # alohida oylarda kalibrlash, keyin qaror           # ✅

4. Kalibrlash o'quvning o'zida

python
platt(model.predict_proba(X_oquv)[:, 1], y_oquv)                        # ⚠️ optimistik
platt(model.predict_proba(X_kal)[:, 1], y_kal)       # 13-14 oylar       # ✅

5. Zararsiz siyosat

python
tanla = np.argsort(-p)[:K]                                              # ⚠️
tanla = np.argsort(-(p * zarar))[:K]                                    # ✅

6. Testni qayta ochish

python
for v in variantlar: print(test_tejam(v))                               # ⚠️
testni_och(papka, {"reja_muhri": muhr, ...})   # ikkinchi marta - RuntimeError  # ✅

7. Test natijasini tushuntirmaslik

python
print(f"model {tm.mean():.1f} mln tejaydi")                            # ⚠️
print(f"CV {cv:.1f}, test {tm.mean():.1f} (y60 {y_test:.4f} va {y_oquv:.4f}); p/haqiqiy {kal:.2f}")  # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 14.9, 14.10, 14.13-darslar (o'tilgan): Nomutanosib sinflar, kalibrlash, xato tahlili
  • 15-qism (o'tilgan): Gradient boosting — HistGB nomzodi
  • 18.2, 18.10, 18.11-darslar (o'tilgan): CV turlari, juftlashgan taqqoslash, validatsiyaga overfitting
  • 20-21-qismlar (o'tilgan): Neyron tarmoqlar — kichik tarmoq nomzodi
  • 27.5, 27.11, 27.12-darslar (o'tilgan): Registr, monitoring, drift — model kartasi va kalibrlash nazorati
  • 29.4-dars: Bu natijalarni rahbariyatga taqdim etish, pilot rejasi
  • 29.10-dars: Intervyu case study: "modelingizni qanday baholadingiz?" savoliga javob

8. Eng yaxshi amaliyotlar

  1. Baholash rejasi natijadan oldin yoziladi va muhrlanadi.

  2. Nomzodlar soddalik tartibida; qaror — "eng sodda munosib".

  3. Vaqt bo'yicha CV, oylar bo'yicha juftlashtirish, SE bilan.

  4. Ehtimol qarorga kirsa — kalibrlash alohida ma'lumotda, Brier/ECE/p-haqiqiy bilan tekshiriladi.

  5. Siyosat kutilgan qiymat bo'yicha; sig'im egri chizig'i biznesga taklif sifatida.

  6. Segmentlar bo'yicha xato — SE bilan, adolatlilik savollari ochiq yoziladi.

  7. Test bir marta, qulf bilan; natija — tushuntirish bilan.

  8. Model kartasi raqamlar bilan koddan to'ldiriladi.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # 7 oylik CV da LogReg qoidadan necha oyda yutdi?
2.  # HistGB o'rtachada eng yaxshi - nega tanlanmadi?
3.  # class_weight="balanced" AUC ni qanchaga o'zgartiradi? p/haqiqiy?
4.  # balanced model bilan "EV > 0" siyosati oyiga nechta qo'ng'iroq?
5.  # top-200 da kalibrlash xatosi tejamni keskin o'zgartiradimi?
6.  # sig'im 200 -> 300: chegaraviy kutilgan tejam?
7.  # kutilgan tejam qaysi K da maksimal?
8.  # Buxoro segmentida p/haqiqiy 1.18 (SE 0.24) - muammomi?
9.  # test tejami CV dan yuqori chiqdi - model yaxshilanganmi?
10. # testda bashorat/haqiqiy 0.880 - nima qilish kerak?
Javoblar
  1. 7/7 (+6.40, SE 1.49)
  2. LogReg dan farqi -0.90 (SE 0.98) — shovqin; qoida eng soddasini tanlaydi
  3. AUC deyarli o'zgarmaydi (0.8483), p/haqiqiy 19.14
  4. 4105 (deyarli hamma), haqiqiy tejam 0.47
  5. Yo'q — 28.05-29.34 oralig'ida (tartib deyarli saqlanadi)
  6. +5.80 mln so'm/oy
  7. K = 1000 atrofida (51.11)
  8. Yo'q — 2*SE ichida (shovqin)
  9. Yo'q — test oylarida y60 ko'proq (yuklama), tejash imkoniyati katta
  10. Oylik kalibrlash monitoringi, chegaradan oshsa qayta kalibrlash (27.11-27.12)

Vazifa 2: Xatolarni tuzating

python
1.  for c in [0.01, 0.1, 1, 10]:
        for d in [3, 5, 8]:
            natija[(c, d)] = test_tejam(model(c, d))
    eng = max(natija, key=natija.get)

2.  m = LogisticRegression(class_weight="balanced").fit(X, y)
    qongiroq = m.predict_proba(X_yangi)[:, 1] * 0.3 * zarar > 0.02

3.  print("HistGB tanlandi: o'rtacha tejam eng yuqori 31.88-bob")

4.  segment = "Buxoro"; print("Buxoroda model xavfni 18% oshirib baholaydi - tuzatish kerak")
Javoblar
python
1.  # sozlash faqat CV da (o'quv/val), test - yakuniy tanlovdan keyin BIR marta
    natija = {(c, d): cv_tejam(model(c, d)) for c in ... for d in ...}

2.  p = platt(p_kal, y_kal)(m.predict_proba(X_yangi)[:, 1])   # 13-14 oylarda kalibrlash
    qongiroq = p * 0.3 * zarar > 0.02

3.  print("LogReg tanlandi: HistGB dan farq -0.90 (SE 0.98) - sezilarli emas, LogReg soddaroq")

4.  print("Buxoro: 1.18 (SE 0.24) - 2*SE ichida, shovqin; monitoringda kuzatiladi")

Vazifa 3: Nomzodlar

Modellang (1-misol asosida):

  1. HistGB giperparametrlarini faqat CV ichida sozlang (18.4 dagi ichki CV g'oyasi bilan) — LogReg dan farq sezilarli bo'ladimi?
  2. PyTorch da ikki qatlamli tarmoq yozing (21-qism), 1-misoldagi rejaga nomzod sifatida qo'shing
  3. "Qoida + LogReg" gibridi: util > 0.6 bo'lganlar orasida LogReg — natija?
  4. Bo'shliqni 0 va 2 oy qiling — CV bahosi qanday o'zgaradi?

Vazifa 4: Kalibrlash

Modellang (2-misol asosida):

  1. Izotonik kalibrlashni Platt bilan solishtiring (13-14 oylar, 8600 qator) — qaysi biri barqarorroq?
  2. HistGB ni kalibrlang — top-200 va "EV > 0" tejami o'zgaradimi?
  3. Kalibrlashni "o'zgaruvchan" qiling: har oy oxirgi 2 oyda Platt — test davridagi siljishni tuzatadimi?

Vazifa 5: Siyosat

Modellang (3-misol asosida):

  1. Sig'im kunlik: kuniga 10 ta, lekin ro'yxat oyiga bir marta — mijozlar oy boshida yoki oxirida qo'ng'iroq olishining ta'siri (faraz: samara vaqt o'tishi bilan kamayadi)
  2. Ikki xil harakat: qo'ng'iroq (0.02, samara 0.30) va SMS (0.001, samara 0.05) — optimal taqsimot
  3. Segmentlar uchun recall farqini kamaytiradigan siyosat (har segmentga minimal kvota) — tejam qancha kamayadi?

Vazifa 6: Test va karta

Modellang (4-misol asosida):

  1. Test jurnaliga natijaning xeshini ham yozing; natija qayta hisoblanganda xesh mosligini tekshiring
  2. Test oylari bo'yicha segment jadvalini (3-misoldagi kabi) chiqaring — drift qaysi segmentda kuchliroq?
  3. Model kartasini markdown fayl sifatida yozing va 3-misol natijalarini (sig'im egri chizig'i) qo'shing

Vazifa 7: O'ylash

Moliya direktori: "Test natijasi ajoyib: oyiga 42 mln so'm. Yiliga yarim milliard! Keyingi yil byudjetiga shu raqamni yozamiz. Qo'ng'iroq markaziga yana 5 operator olamiz, shunda tejam 2-3 barobar oshadi."

Javob

Qisqa javob: Byudjetga 42 mln emas, oraliq yoziladi. Sig'imni oshirish esa foydali, lekin chegaraviy qiymat kamayib boradi.

1. Nega "42 mln/oy" byudjet raqami emas.

python
# 1) CV (7 oy): 30.98; test (3 oy): 42.33 - farq yuklamadan (y60 0.0260 va 0.0209)
# 2) yuklama pasaysa, tejam ham pasayadi - bu yaxshi xabar, lekin byudjet kamayadi
# 3) samara 0.30 va zarar 0.45 * balans - FARAZIY; pilotgacha tasdiqlanmagan
# 4) testda bashorat/haqiqiy 0.880 - kalibrlash siljigan

2. Nega "2-3 barobar" emas. 3-misoldagi sig'im egri chizig'i: 200 dan 300 ga +5.80, 300 dan 400 ga +3.88, 400 dan 600 ga +2.39 (100 qo'ng'iroq uchun). Kutilgan tejam K = 1000 atrofida to'yinadi. 600 qo'ng'iroq (taxminan +20 qo'ng'iroq/kun) kutilgan tejamni 33.66 dan 48.10 ga oshiradi. Bu 2-3 barobar emas, taxminan 1.4 barobar (val oylarida).

3. Taklif.

  • Byudjetga oraliq: "oyiga 25-45 mln so'm, yuklamaga qarab; pilotdan keyin aniqlashtiriladi".
  • Sig'im: 5 emas, 1-2 operator bilan boshlash va chegaraviy qiymatni pilotda o'lchash.
  • Pilot 29.4-bob: samara 0.30-bob haqiqatda qancha ekanini A/B bilan aniqlash. Byudjetdagi eng katta noaniqlik shu yerda.

Direktorga javob: "42 mln — yuqori yuklamali uch oyning natijasi. Oddiy oylarda bu 30 atrofida edi. Bundan tashqari qo'ng'iroq samarasi hozircha faraz, u pilotda tekshiriladi. Byudjetga 25-45 oralig'ini yozishni taklif qilaman. Operatorlar bo'yicha: har keyingi 100 qo'ng'iroq oldingisidan kam foyda beradi. Shuning uchun 1-2 operator bilan boshlab, har birining haqiqiy qo'shgan qiymatini o'lchaymiz."

Nimani mustahkamlaydi: 2.1, 2.3, 2.4, 2.5, 2.7-bo'limlar.


Xulosa

Bu darsda loyihaning modellashtirish qismini qildik: rejadan testgacha, har qadam pul bilan o'lchanib va halol yozilib.

Eng muhim uch fikr:

  1. Reja oldindan, qaror qoidasi bilan. 1-misolda reja muhrlandi (af71c572eab7), keyin 7 oylik vaqt bo'yicha CV da to'rt nomzod juftlashtirildi. HistGB o'rtachada eng yuqori (31.88), lekin LogReg dan farqi -0.90 (SE 0.98), ya'ni shovqin. Shuning uchun eng sodda munosib — LogReg. Kichik tarmoq sezilarli yomon chiqdi (-3.31, SE 1.34). 29.1 dagi ochiq savol yopildi: model qoidadan oyiga +6.40 (SE 1.49) ko'p tejaydi va 7 oyning yettisida yutadi.

  2. Ehtimol qaror uchun, siyosat esa sig'im uchun. 2-misolda class_weight="balanced" AUC ni o'zgartirmadi (0.8483), lekin ehtimolni 19.14 barobar oshirdi. "EV > 0" siyosati oyiga 4105 qo'ng'iroq bilan 0.47 mln so'm berdi va 1308.8 mln "prognoz" qildi. Platt buni tuzatdi. 3-misolda sig'im egri chizig'i biznes savoliga raqam bilan javob berdi: +100 qo'ng'iroq oyiga +5.80 mln so'm, kutilgan tejam esa K = 1000 atrofida to'yinadi. 12 segmentning hech birida kalibrlash 2*SE dan ko'p siljimadi, recall farqlari esa 29.11 ga ochiq savol.

  3. Test bir marta, natija tushuntirish bilan. 4-misolda test qulf bilan bir marta ochildi. Model qoidadan +11.25 (SE 3.98) ko'p tejadi. Lekin test tejami (42.33) CV dan (30.98) yuqori chiqdi va buning sababi yuklama edi (y60 0.0260). Model xavfni kam baholadi (0.880), ya'ni kalibrlash monitoringi zarur. Model kartasi raqamlar bilan koddan to'ldirildi.

Keyingi darsda To'liq loyiha: natijani taqdim etish: loyihaning uchinchi qismi. Natijani texnik bo'lmagan auditoriyaga "piramida" tuzilishida yetkazamiz: avval xulosa va tavsiya, keyin dalil. Noaniqlikni "100 mijozdan N tasi" va oraliqlar bilan ifodalaymiz. Hisobotni raqamlar koddan olinadigan qilib avtomatik quramiz. Grafiklarni tekshiramiz. "Nima qilmaslik kerak"ni (cherry-picking) simulyatsiya bilan o'lchaymiz. Pilot/A-B rejasi va ijroiya xulosasi ham shu darsda.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
29.3-dars: To'liq loyiha: modellashtirish va baholash — IlmHamroh