IlmHamroh
Data Science va sun'iy intellekt/Loyihalar va karyera1/12-dars63 daqiqa
Mundarija (22)

29.1-dars: Data Science loyihasining hayot sikli

29-QISM — LOYIHALAR VA KARYERA · 1-dars


1. Kirish va motivatsiya

28-qism oxirida aytganimizdek, bu — kursning yakuniy qismi. 28 qism davomida biz alohida vositalarni o'rgandik: ma'lumotni yig'ish va tozalash, EDA, statistika va gipoteza testlari, klassik ML va chuqur o'rganish, baholash dizayni, MLOps va maxsus mavzular. Endi savol boshqacha: bu bo'laklardan qanday qilib foydali loyiha yig'iladi? Amalda eng qiyin qism ko'pincha model emas. Qiyinchilik savolni to'g'ri qo'yishda, muvaffaqiyatni to'g'ri o'lchashda va loyiha qayerda sinishi mumkinligini oldindan ko'rishda.

Real vaziyat. Tijorat bankining Data Science jamoasi kredit karta mijozlari "to'lovni kechiktiradimi" degan modelni qurdi. Validatsiyada AUC 0.86 chiqdi va hisobotda "yuqori sifatli model" deb yozildi. Model har oy 3 ming xavfli mijoz ro'yxatini chiqara boshladi. Lekin qo'ng'iroq markazi oyiga faqat bir necha yuz mijozga qo'ng'iroq qila oladi. Operatorlar ro'yxat boshidagilarga qo'ng'iroq qildi va ularning ko'pchiligi "ha, unutib qo'yibman, ertaga to'layman" deb javob berdi. Bu mijozlar qo'ng'iroqsiz ham to'lar edi. Model "30 kunlik kechikish" belgisida o'qitilgan edi, bankni esa haqiqiy zarar keltiradigan uzoq kechikishlar qiziqtirardi. Uch oydan keyin rahbariyat savol berdi: "bu model bizga qancha pul tejadi?" Jamoa javob bera olmadi: bunday o'lchov loyihaning boshida rejalashtirilmagan edi.

Bu to'rt darsda (29.1-29.4) aynan shu loyihani boshidan to'g'ri qilamiz. Vazifa: bankda kredit karta mijozlari orasidan kelgusi 60 kunda jiddiy kechikishga (60+ kun) tushadiganlarni oldindan aniqlash va cheklangan qo'ng'iroq markazi resursini ular orasida taqsimlash. Loyiha to'rt qismga bo'lingan:

  1. 29.1 (bu dars) — biznes savoli, muvaffaqiyat mezoni, bazaviylar, xatarlar, reja va loyiha hujjati.
  2. 29.2 — ma'lumot: bir nechta jadval, SQL bilan belgilar, vaqt nuqtasi va sizish, sifat, vaqt bo'yicha bo'lish.
  3. 29.3 — modellashtirish: bazaviylardan HistGB gacha, oldindan yozilgan baholash rejasi, kalibrlash, sig'im bilan siyosat, test.
  4. 29.4 — natijani texnik bo'lmagan auditoriyaga yetkazish: piramida, noaniqlik, avtomatik hisobot, pilot rejasi.

Hamma darsda bitta sintetik ma'lumot generatori (urug' 29) ishlatiladi: 6000 mijoz, 24 oy (2024-01 dan 2025-12 gacha), to'rtta jadval — mijozlar, hisobvaraqlar, to'lovlar va tranzaksiyalar.

Bu darsda loyiha hayot siklini ko'ramiz: biznes savolini DS vazifasiga aylantirish, muvaffaqiyatni pul bilan o'lchash, "model kerakmi?" savoliga raqam bilan javob berish va xatarlarni loyiha boshida o'lchash.

Bu darsda:

  • CRISP-DM va uning amaldagi ko'rinishi
  • Biznes savolidan DS vazifasiga: qaror kim tomonidan, qachon, qanday qabul qilinadi
  • Biznes metrikasi va model metrikasi: xarajat matritsasi, top-K, "tejalgan zarar"
  • Bazaviylar va "model kerakmi?" savoli
  • Loyiha xatarlari: ma'lumot mavjudligi, sizish, belgi ta'rifi, belgi yetilishi, qaror halqasi
  • Loyiha rejasi: tez prototip, baholash, ishlab chiqarish
  • Loyiha hujjati (problem statement) shabloni

ℹ Misollar real numpy/pandas/sklearn bilan (Python 3.14). Ma'lumot sintetik, lekin real portfelga o'xshash qurilgan: yashirin "moliyaviy qiyinchilik" va "unutuvchanlik" omillari bor, makroiqtisodiy yuklama 2025-yil ikkinchi yarmida oshadi. Biznes parametrlari faraziy: qo'ng'iroq DPD60 holatlarining 30% ini oldini oladi, bitta qo'ng'iroq narxi 20 ming so'm, DPD60 ga tushgan hisobning kutilgan zarari balansning 45% i. Real bankda bu raqamlar pilot va moliya bo'limi bilan birga aniqlanadi 29.4-bob.


2. Nazariya — chuqur tushuntirish

2.1. CRISP-DM: olti bosqich va amaldagi ko'rinishi

CRISP-DM (Cross-Industry Standard Process for Data Mining) — DS loyihasining eng ko'p tilga olinadigan jarayon modeli. U 1990-yillar oxirida sanoat konsorsiumi tomonidan yozilgan va hozir ham loyihani bosqichlarga ajratish uchun qulay "xarita" bo'lib qolmoqda.

text
   +--------------------------+
   | 1. BIZNESNI TUSHUNISH    |<----------------------------+
   +--------------------------+                             |
         |            ^                                     |
         v            |                                     |
   +--------------------------+                             |
   | 2. MA'LUMOTNI TUSHUNISH  |                             |
   +--------------------------+                             |
         |                                                  |
         v                                                  |
   +--------------------------+     +--------------------+  |
   | 3. MA'LUMOT TAYYORLASH   |<--->| 4. MODELLASHTIRISH |  |
   +--------------------------+     +--------------------+  |
                                              |             |
                                              v             |
                                      +--------------+      |
                                      | 5. BAHOLASH  |------+  yo'q
                                      +--------------+
                                              |  ha (biznes mezoni bajarildi)
                                              v
                                      +--------------+
                                      | 6. JORIY     |
                                      |    ETISH     |
                                      +--------------+
Bosqich Asosiy savol Natija (artefakt) Kursdagi joyi
1. Biznesni tushunish Qaysi qaror yaxshilanadi? Qanday o'lchaymiz? loyiha hujjati, muvaffaqiyat mezoni 29.1, 8.2
2. Ma'lumotni tushunish Qanday ma'lumot bor, qachon paydo bo'ladi? ma'lumot inventari, EDA, sifat hisoboti 7, 8, 29.2
3. Ma'lumot tayyorlash Tahlil birligi, belgilar, bo'lish belgilar jadvali, ma'lumot kartasi 6, 17, 29.2
4. Modellashtirish Qaysi yondashuv yetarli? bazaviylar, nomzodlar, tanlov 12-26, 29.3
5. Baholash Biznes mezoni bajarildimi? baholash hisoboti, model kartasi 18, 29.3
6. Joriy etish Qanday ishlatiladi va kuzatiladi? xizmat, monitoring, pilot 27, 29.4

Amaldagi ko'rinishi. Rasmdagi strelkalar loyiha chiziqli emasligini ko'rsatadi, va amalda bu yanada kuchliroq seziladi:

  • Orqaga qaytishlar odatiy. Modellashtirishda belgining noto'g'ri ta'riflangani ma'lum bo'ladi va 1-bosqichga qaytiladi. Baholashda sizish topiladi va 3-bosqich qayta qilinadi.
  • Vaqtning katta qismi 1-3-bosqichlarga ketadi. Buning aniq ulushi loyihaga bog'liq. Lekin model tanlash odatda eng qisqa bosqichlardan biri bo'ladi.
  • 6-bosqich — oxiri emas. 27-qismda ko'rganimizdek, joriy etilgan model monitoring, drift va qayta o'qitish bilan yashaydi. Qaror halqasi esa 2.5-bob keyingi ma'lumotni o'zgartiradi.
  • Rasmda yo'q, lekin muhim bosqich — "to'xtatish". Har bosqich oxirida "davom etamizmi?" degan qaror bo'lishi kerak. Loyihani vaqtida to'xtatish ham muvaffaqiyat hisoblanadi: resurs tejaladi.

Boshqa jarayon modellari ham bor. Masalan, TDSP va turli "ML lifecycle" sxemalari MLOps bosqichlarini batafsilroq ajratadi. Lekin ularning barchasi bitta skeletga tayanadi: savol → ma'lumot → model → baholash → joriy etish → kuzatish, va bu bosqichlar orasida qaytishlar bor.

2.2. Biznes savolidan DS vazifasiga

Buyurtmachi DS jamoasiga odatda savol bilan emas, istak bilan keladi: "kechikishlarni kamaytirish uchun model kerak". Bu hali vazifa emas. Uni vazifaga aylantirish uchun qaror haqida beshta savolga javob berish kerak:

text
1. QAROR NIMA?         qaysi mijozga qo'ng'iroq qilish (eslatma + restrukturizatsiya taklifi)
2. KIM QABUL QILADI?   qo'ng'iroq markazi rahbari; operatorlar ro'yxat bo'yicha ishlaydi
3. QACHON?             har oy oxirida (hisobvaraq chiqqanda), keyingi oy davomida qo'ng'iroq
4. QANDAY CHEKLOV?     oyiga K = 200 qo'ng'iroq (kuniga ~10 ta, ~20 ish kuni)
5. NIMA O'ZGARADI?     qo'ng'iroq qilingan xavfli mijozlarning bir qismi 60+ kunga tushmaydi
                       -> bank zarari kamayadi; qo'ng'iroq narxi -> xarajat

         |  tarjima
         v
TAHLIL BIRLIGI:   (mijoz, as-of oy) - oy oxiridagi holat
NOMZODLAR:        hozir kechikmagan (oxirgi hisobvaraq to'langan) faol mijozlar
AS-OF (vaqt nuqtasi): t-oy oxiri; belgilar FAQAT shu paytgacha ma'lum ma'lumotdan
GORIZONT:         keyingi 60 kun (t va t+1 hisobvaraqlari)
BELGI (label):    y60 = ikkala hisobvaraq bo'yicha minimal to'lov qilinmagan (60+ kun)
MODEL CHIQISHI:   p = P(y60 = 1 | t gacha ma'lumot) - kalibrlangan ehtimol
SIYOSAT:          har oy p * zarar bo'yicha top-200 ga qo'ng'iroq
BIZNES METRIKASI: tejalgan zarar - qo'ng'iroq xarajati (mln so'm/oy)

Ikki xil qo'yilishni solishtiring:

Yomon qo'yilish Yaxshi qo'yilish
"Kechikadiganlarni bashorat qiluvchi model" "Har oy 200 ta qo'ng'iroqni kutilgan tejam eng katta mijozlarga taqsimlash"
Belgi: "kechikdi" (qaysi? qachon?) Belgi: as-of dan keyingi 60 kunda 60+ kun kechikish
Metrika: AUC Metrika: oylik tejam (mln so'm); yordamchi: precision@200, AUC, kalibrlash
Natija: ro'yxat Natija: siyosat + kutilgan tejam + noaniqlik + pilot rejasi

Oltin savol: "model mukammal bo'lsa, kim nima qiladi va bu qancha turadi?" Agar bunga javob bo'lmasa, loyiha hali boshlanmagan. 1-misolda bu savolga raqam bilan javob beramiz: "kelajakni biladigan" (oracle) siyosat oyiga taxminan 78.6 mln so'm tejaydi. Bu — har qanday modelning yuqori chegarasi.

2.3. Muvaffaqiyat mezoni: biznes metrikasi va model metrikasi

Metrikalar zinapoyasi:

text
BIZNES METRIKASI     tejalgan zarar - xarajat (mln so'm/oy)     <- rahbariyat shu bilan qaror qiladi
      ^
QAROR METRIKASI      tejam@K, precision@K, recall@K              <- siyosat sifatini o'lchaydi
      ^
MODEL METRIKASI      AUC, log-loss, Brier, kalibrlash            <- model ichki sifati
      +
TO'SIQ (guardrail)   shikoyatlar, qayta qo'ng'iroqlar, segmentlar bo'yicha adolat (29.11)

Xarajat matritsasi. Qaror ikki xil: qo'ng'iroq qilish yoki qilmaslik. Holat ham ikki xil: mijoz DPD60 ga tushadi yoki tushmaydi. Qiymatni "qo'ng'iroq qilinmasa" holatiga nisbatan o'lchaymiz:

text
                              qo'ng'iroq qilindi            qo'ng'iroq qilinmadi
mijoz DPD60 ga tushadi (TP/FN)  +0.30 * zarar - 0.02          0  (zarar to'liq qoladi)
mijoz tushmaydi       (FP/TN)   -0.02                         0

zarar = 0.45 * balans (faraziy LGD), 0.30 - qo'ng'iroq samarasi (faraziy), 0.02 = 20 ming so'm
tejam(S) = sum_{i in S} ( 0.30 * zarar_i * y60_i - 0.02 )
qo'ng'iroq foydali, agar  p_i * 0.30 * zarar_i > 0.02   (p_i kalibrlangan bo'lishi SHART)

Nega AUC biznes metrikasi bilan mos kelmasligi mumkin:

  1. AUC butun tartibni o'lchaydi, qaror esa faqat top-K ga bog'liq. Top-K dan tashqaridagi tartib yaxshilansa, AUC oshadi, tejam esa o'zgarmaydi (2-misol, 3-bo'lim).
  2. AUC zarar hajmini bilmaydi. 10 mln balansli mijozni topish 1 mln balansli mijozni topishdan 10 barobar qimmatroq, AUC uchun esa ular teng.
  3. AUC kalibrlashni bilmaydi. Ehtimolni 10 ga ko'paytirsak ham AUC o'zgarmaydi. "p * zarar > narx" qarori esa butunlay o'zgaradi 29.3-bob.
  4. AUC sig'imni bilmaydi. K = 50 va K = 2000 da modellar orasidagi farq turlicha (3-misol, 4-bo'lim).

Bu model metrikasi keraksiz degani emas. AUC va log-loss modelni tez tanlash va diagnostika uchun qulay. Lekin loyiha muvaffaqiyati biznes metrikasi bilan e'lon qilinadi, va bu metrika loyiha boshlanishidan oldin yoziladi.

2.4. Bazaviylar va "model kerakmi?"

text
QIYMAT ZINAPOYASI (oyiga tejam, har loyihada o'lchanadi):
  0. hech narsa qilmaslik              0          <- har doim variant!
  1. tasodifiy K ta qo'ng'iroq         ~0         <- reyting qiymati shu bilan o'lchanadi
  2. oddiy qoida (util eng yuqori)     ?          <- bitta ustun, 5 daqiqa
  3. ekspert ball-kartasi              ?          <- bo'lim tajribasi
  4. oddiy model (LogReg)              ?
  5. murakkab model (HistGB, tarmoq)   ?
  6. oracle (kelajakni biladi)         yuqori chegara - erishib bo'lmaydi

MODEL QIYMATI = model - ENG YAXSHI QOIDA   (tasodifiydan emas!)
MODEL XARAJATI = ishlab chiqish + ma'lumot quvuri + monitoring + qayta o'qitish + xatar

"Model kerak emas" degan signallar:

  • oddiy qoida eng yaxshi modeldan sezilarli yomon emas (juftlashgan farq 2*SE ichida);
  • model foydasi yillik xarajatdan kichik;
  • qaror kam uchraydi yoki arzon, xato esa qimmat va tushuntirishni talab qiladi (regulyatsiya);
  • oracle ham kichik qiymat beradi, ya'ni muammo reytingda emas (masalan, sig'im juda kichik yoki harakat samarasiz).

Qoida bilan boshlashning afzalligi: u darhol ishlaydi, tushunarli va keyinchalik modelni o'lchash uchun "nazorat" bo'lib xizmat qiladi. 3-misolda kutilmagan natija olamiz: 3 oylik validatsiyada bitta ustundan iborat qoida modeldan sezilarli yomon emas.

2.5. Loyiha xatarlari

Xatar Savol Qanday o'lchanadi / ushlanadi
Ma'lumot mavjudligi Kerakli jadvallar bormi, kirish huquqi, qancha tarix? inventar; o'quv tarixi uzunligi bo'yicha egri chiziq (4-misol)
Sizish (leakage) Belgi as-of dan keyingi ma'lumotni ko'rmaydimi? vaqt nuqtasi auditi, "snapshot" testi (29.2)
Belgi ta'rifi "Kechikish" nima: 30 kunmi, 60 kunmi? har ta'rifda o'qitib, biznes metrikasi bilan solishtirish (4-misol)
Belgi yetilishi Belgi qachon ma'lum bo'ladi? as-of t uchun belgi t + gorizont da; o'quv oylari shunga moslanadi
Qaror halqasi Qo'ng'iroqlar boshlangach, belgilar o'zgaradimi? qo'ng'iroq qilinmaydigan nazorat guruhi (holdout), 29.4
Drift Dunyo o'zgaradimi? vaqt bo'yicha bo'lish, monitoring (27.11, 27.12)
Qabul qilish Operatorlar ro'yxatga ishonadimi? pilot, tushuntirish, sabab kodlari
Etika va regulyatsiya Qaror adolatlimi, taqiqlangan belgi yo'qmi? segmentlar bo'yicha tahlil 29.3-bob, 29.11

Belgi ta'rifi — loyiha qarori, texnik tafsilot emas. "30+ kun kechikish" belgisi "60+" dan besh barobar ko'p uchraydi. Shuning uchun model o'qitishga qulayroqdek ko'rinadi. Lekin 4-misolda ko'ramizki, y30 = 1 bo'lganlarning 81% i o'zi to'laydi: ular unutuvchan, lekin to'lashga qodir mijozlar. y30 da o'qitilgan model ro'yxatni aynan shular bilan to'ldiradi va y60 da o'qitilgan modeldan oyiga taxminan 7 mln so'm kam tejaydi. Bu farq 7 oyning har birida takrorlandi.

Qaror halqasi (feedback loop). Model joriy etilgach, eng xavfli mijozlarga qo'ng'iroq qilinadi va ularning bir qismi DPD60 ga tushmaydi. Keyingi o'qitishda model "bunday belgili mijozlar kechikmaydi" degan noto'g'ri saboqni olishi mumkin. Yechim — loyiha boshidanoq kichik tasodifiy nazorat guruhini (qo'ng'iroq qilinmaydigan) rejalashtirish. Bu guruh keyin haqiqiy samarani o'lchashga ham xizmat qiladi (27.13, 29.4).

2.6. Loyiha rejasi: tez prototip → baholash → ishlab chiqarish

text
BOSQICH 0. RAMKA (1-2 hafta)
  loyiha hujjati, biznes metrikasi, xarajat parametrlari (faraziy -> moliya tasdiqlaydi)
  ma'lumot inventari, kirish huquqlari, oracle va qoida bazaviysi
  DARVOZA: oracle qiymati loyiha xarajatidan sezilarli katta?  yo'q -> TO'XTATISH

BOSQICH 1. TEZ PROTOTIP (2-4 hafta)
  bitta jadval, sizishsiz belgilar, oddiy qoida va LogReg, bitta vaqt bo'yicha val
  DARVOZA: model eng yaxshi qoidadan yaxshiroqmi yoki hech bo'lmaganda istiqbollimi?

BOSQICH 2. TO'LIQ BAHOLASH (3-6 hafta)
  sifat tekshiruvlari, vaqt bo'yicha CV, nomzodlar, kalibrlash, siyosat, segmentlar
  oldindan yozilgan baholash rejasi -> test BIR MARTA -> model kartasi
  DARVOZA: test da biznes mezoni bajarildimi?

BOSQICH 3. PILOT (dastlabki taxmin 2-3 oy; 29.4 da namuna hajmi bilan tekshiriladi)
  mijozlarning bir qismida A/B: model ro'yxati vs qoida ro'yxati vs qo'ng'iroqsiz
  DARVOZA: haqiqiy samara (0.30 faraziyi) tasdiqlandimi?

BOSQICH 4. ISHLAB CHIQARISH
  quvur, xizmat, monitoring, qayta o'qitish qoidasi, doimiy nazorat guruhi (27-qism)

Muddatlar — namuna. Ular jamoa va ma'lumot holatiga bog'liq. Muhimi muddatda emas, darvozalarda: har bosqich oxirida oldindan kelishilgan mezon bo'yicha "davom etish, qayta ko'rib chiqish yoki to'xtatish" qarori qabul qilinadi.

2.7. Loyiha hujjati (problem statement) shabloni

Loyiha hujjati — 1-2 sahifali matn. Uni buyurtmachi, DS jamoasi va foydalanuvchi (qo'ng'iroq markazi) loyiha boshida birga imzolaydi. Keyinroq "biz buni kelishmagan edik" degan bahslarning ko'pchiligini u oldini oladi.

markdown
# Loyiha hujjati: kredit karta kechikishini oldindan aniqlash

**Egasi:** chakana kredit risk bo'limi · **DS jamoasi:** 2 kishi · **Versiya:** 0.3 (namuna)

## 1. Biznes muammosi
60+ kunlik kechikishga tushgan kredit karta hisoblari bank uchun asosiy zarar manbai.
Qo'ng'iroq markazi oyiga ~200 profilaktik qo'ng'iroq qila oladi, lekin hozir ro'yxat
qo'lda tuziladi (limitdan foydalanish ulushi bo'yicha).

## 2. Qaror va foydalanuvchi
- Qaror: har oy oxirida qaysi 200 mijozga qo'ng'iroq qilish.
- Foydalanuvchi: qo'ng'iroq markazi rahbari; ro'yxat CRM ga tushadi.
- Harakat: eslatma + to'lov jadvalini qayta ko'rib chiqish taklifi.

## 3. DS vazifasi
- Tahlil birligi: (mijoz, as-of oy); nomzodlar - hozir kechikmagan faol mijozlar.
- Belgi: as-of dan keyingi 60 kunda 60+ kun kechikish (y60).
- Chiqish: kalibrlangan ehtimol p; siyosat: p * kutilgan zarar bo'yicha top-K.

## 4. Muvaffaqiyat mezoni
- Asosiy: oylik tejam = 0.30 * sum(zarar * y60) - 0.02 * K (mln so'm), test oylarida.
- Talab: joriy qoidadan juftlashgan farq > 2*SE.
- Yordamchi: precision@200, recall@200, AUC, kalibrlash (bashorat / haqiqiy ulush).
- To'siqlar: segmentlar (hudud, yangi mijozlar) bo'yicha keskin farq yo'q; shikoyatlar oshmaydi.

## 5. Bazaviylar
Hech narsa; tasodifiy 200; qoida "util eng yuqori"; ekspert ball-kartasi.

## 6. Ma'lumot
mijozlar, hisobvaraqlar, to'lovlar, tranzaksiyalar (2024-01 dan boshlab), omborga kirish bor.
As-of qoidasi: faqat sanasi <= as-of bo'lgan yozuvlar.

## 7. Farazlar va xatarlar
- Faraziy: qo'ng'iroq samarasi 30%, qo'ng'iroq narxi 20 ming so'm, zarar = 45% balans
  -> pilotda va moliya bo'limida tasdiqlanadi.
- Belgi yetilishi 2 oy; qaror halqasi -> 5% nazorat guruhi doimiy.
- Drift (makroiqtisodiy) -> oylik monitoring.

## 8. Reja va darvozalar
Ramka (2 hafta) -> prototip (3 hafta) -> baholash (4 hafta) -> pilot (muddat - namuna hajmi hisobidan) -> ishlab chiqarish.
To'xtatish mezoni: prototipda model qoidadan ustun emas va oracle qiymati past.

## 9. Doiradan tashqari
Kredit limitini o'zgartirish, undiruv (collection) strategiyasi, yangi mijozlarni skoring.

2.8. Tuzoqlar

Asosiy tuzoqlar: savol o'rniga istak bilan boshlash ("bizga model kerak"); qarorni, uning egasini va sig'imni aniqlamaslik; muvaffaqiyatni faqat AUC bilan e'lon qilish; zarar hajmini e'tiborsiz qoldirib, faqat ehtimol bo'yicha tartiblash; oddiy qoida bazaviysini o'lchamaslik (model tasodifiydan yaxshi — bu hali qiymat emas); oracle chegarasini hisoblamaslik; belgini qulaylik uchun tanlash (ko'p musbat bo'lsin deb 30 kun); belgi yetilishini hisobga olmay oxirgi oylarni o'quvga qo'shish; qaror halqasi va nazorat guruhini rejalashtirmaslik; faraziy parametrlarni (samara, narx) fakt sifatida taqdim etish; darvozalarsiz reja — loyihani to'xtatib bo'lmaydi.


3. Tez ma'lumotnoma

python
import numpy as np

# qo'ng'iroq qiymati (xarajat matritsasi): qo'ng'iroq qilinsa, qo'ng'iroqsizga nisbatan
qiymat = SAMARA * zarar * y60 - NARX          # TP: +0.30*z-0.02, FP: -0.02

# har oy top-K siyosatining tejami (biznes metrikasi)
def tejam_k(qiymat, ball, guruhlar, k=200):
    return sum(qiymat[g[np.argsort(-ball[g])[:k]]].sum() for g in guruhlar) / len(guruhlar)

# qiymat zinapoyasi
tejam_k(qiymat, rng.random(n), guruhlar)       # tasodifiy ~0
tejam_k(qiymat, df.util.to_numpy(), guruhlar)  # oddiy qoida
tejam_k(qiymat, p * zarar, guruhlar)           # model: kutilgan zarar bo'yicha
tejam_k(qiymat, y60 * zarar, guruhlar)         # oracle - yuqori chegara

# qaror qoidasi: eng yaxshisidan sezilarli yomon bo'lmagan eng sodda
farq = baho[nom] - baho[eng]
sezilarli_yomon = farq < -2 * se               # se - juftlashgan bootstrap yoki oylar bo'yicha

Loyiha boshidagi savollar ro'yxati

Savol Bizning loyihada
Qaror nima va kim qabul qiladi? 200 ta qo'ng'iroq; qo'ng'iroq markazi rahbari
Qachon va qanday chastotada? har oy oxirida (as-of)
Sig'im? oyiga K = 200
Belgi va gorizont? 60 kun ichida 60+ kun kechikish
Biznes metrikasi? tejalgan zarar - qo'ng'iroq xarajati
Bazaviy? qoida "util eng yuqori"
Yuqori chegara? oracle: oyiga ~78.6 mln so'm (val)
Faraziy parametrlar? samara 0.30, narx 0.02, zarar 0.45 * balans
Nazorat guruhi? ha, doimiy

Hayot sikli xulosasi

istak -> qaror (kim, qachon, sig'im) -> DS vazifasi (birlik, as-of, gorizont, belgi)
metrikalar: biznes > qaror (top-K) > model (AUC) + to'siqlar
qiymat zinapoyasi: hech narsa, tasodifiy, qoida, model, oracle
xatarlar: mavjudlik, sizish, belgi ta'rifi va yetilishi, qaror halqasi, drift
reja: ramka -> prototip -> baholash -> pilot -> ishlab chiqarish (darvozalar bilan)

4. Batafsil misollar

Misollar real numpy/pandas/sklearn bilan (Python 3.14). Har misol mustaqil ishlaydi, shuning uchun loyiha generatori (yarat_bank) va belgilar jadvali (belgilar_jadvali) har birida takrorlanadi. Bu kod 29.2-29.4 darslarida ham aynan bir xil. Belgilar qanday qurilgani va nega aynan shunday ekani 29.2 da SQL bilan batafsil ko'riladi. Bu darsda ular tayyor vosita sifatida ishlatiladi.

Misol 1 — Biznes savolidan DS vazifasiga: tahlil birligi, xarajat matritsasi va qiymat chegaralari

python
"""Biznes savolidan DS vazifasiga: tahlil birligi, xarajat matritsasi, qiymat chegaralari."""

import numpy as np
import pandas as pd

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 top_k_qiymat(df, ball, k=K):
    """Har as-of oyda ball bo'yicha top-k ga qo'ng'iroq; oylik tejam (mln so'm)."""
    natija = []
    for _, g in df.assign(_b=ball).groupby("oy"):
        natija.append(g.nlargest(k, "_b")["qiymat"].sum())
    return np.array(natija)


def main() -> None:
    bank = yarat_bank()
    print("=== 1. Manba jadvallari (xom ma'lumot) ===")
    for nom in ["mijozlar", "hisobvaraqlar", "tolovlar", "tranzaksiyalar"]:
        print(f"  {nom:<15} {len(bank[nom]):>8} qator")
    print(f"  davr: {oy_nomi(1)} - {oy_nomi(OYLAR)}")

    df = belgilar_jadvali(bank)
    oylik = df.groupby("oy").agg(n=("y60", "size"), y30=("y30", "mean"),
                                 y60=("y60", "mean"), dpd60=("y60", "sum"))
    print("\n=== 2. Tahlil birligi: (mijoz, as-of oy) ===")
    print(f"  qatorlar: {len(df)}, as-of oylar: {oy_nomi(df.oy.min())} - "
          f"{oy_nomi(df.oy.max())} ({df.oy.nunique()} oy)")
    print(f"  oyiga nomzod mijozlar (hozir kechikmagan): {oylik.n.min()} - "
          f"{oylik.n.max()}")
    print(f"  60 kun ichida 30+ kun kechikish (y30): {df.y30.mean():.3f}")
    print(f"  60 kun ichida 60+ kun kechikish (y60): {df.y60.mean():.3f}")
    print(f"  oyiga DPD60 holatlari: o'rtacha {oylik.dpd60.mean():.1f} "
          f"(min {oylik.dpd60.min()}, max {oylik.dpd60.max()})")
    print(f"  qo'ng'iroq sig'imi: oyiga K = {K} ta = nomzodlarning "
          f"{K / oylik.n.mean():.1%} i")

    print("\n=== 3. Bitta qo'ng'iroqning qiymati (xarajat matritsasi, mln so'm) ===")
    z60 = df.loc[df.y60 == 1, "zarar"]
    print(f"  DPD60 bo'lgan mijozning kutilgan zarari: o'rtacha {z60.mean():.2f}, "
          f"mediana {z60.median():.2f}")
    print("  qo'ng'iroq qilinsa (qo'ng'iroqsiz holatga nisbatan):")
    print(f"    mijoz DPD60 ga tushadigan bo'lsa (TP):  +{SAMARA} * zarar - {NARX}")
    print(f"    mijoz DPD60 ga tushmaydigan bo'lsa (FP): -{NARX}")
    print("  qo'ng'iroq qilinmasa: 0 (FN da zarar to'liq qoladi, TN - hech narsa)")
    ulush = z60.sort_values(ascending=False)
    top20 = ulush.iloc[: int(0.2 * len(ulush))].sum() / ulush.sum()
    print(f"  zarar konsentratsiyasi: eng katta 20% DPD60 zararning {top20:.1%} i")
    p_chegara = NARX / (SAMARA * z60.median())
    print(f"  zararsizlik chegarasi (mediana zararda): p > {p_chegara:.3f}; "
          f"bazaviy ulush {df.y60.mean():.3f}")

    print("\n=== 4. Qiymat chegaralari: val oylari 16-18, oyiga tejam (mln so'm) ===")
    va = df[df.oy.between(16, 18)].copy()
    rng = np.random.default_rng(0)
    tasodifiy = np.mean([top_k_qiymat(va, rng.random(len(va))).mean()
                         for _ in range(50)])
    chegaralar = {
        "hech kimga qo'ng'iroq yo'q": 0.0,
        "tasodifiy 200 ta (50 marta)": tasodifiy,
        "qoida: util eng yuqori": top_k_qiymat(va, va.util).mean(),
        "oracle (kelajakni biladi)": top_k_qiymat(va, va.y60 * va.zarar).mean(),
    }
    for nom, v in chegaralar.items():
        print(f"  {nom:<30} {v:>+8.2f}")
    toliq = (SAMARA * va.zarar * va.y60).groupby(va.oy).sum().mean()
    print(f"  oldini olish mumkin bo'lgan jami zarar (hammaga qo'ng'iroq): {toliq:.1f}")

    print("\n=== 5. Xulosa (natijadan hisoblangan) ===")
    qoida = chegaralar["qoida: util eng yuqori"]
    oracle = chegaralar["oracle (kelajakni biladi)"]
    if abs(tasodifiy) < 0.05 * qoida:
        print(f"  tasodifiy qo'ng'iroq deyarli nol ({tasodifiy:+.2f}) -> reyting shart")
    print(f"  oddiy qoida oracle qiymatining {qoida / oracle:.0%} ini beradi")
    print(f"  model uchun o'yin maydoni: oyiga ko'pi bilan {oracle - qoida:.1f} mln so'm "
          f"(amalda ancha kam)")
    if oylik.dpd60.mean() < K:
        print("  sig'im DPD60 holatlaridan katta -> muammo reytingda, sig'imda emas")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Manba jadvallari (xom ma'lumot) ===
  mijozlar            6000 qator
  hisobvaraqlar     111058 qator
  tolovlar          100780 qator
  tranzaksiyalar    586885 qator
  davr: 2024-01 - 2025-12

=== 2. Tahlil birligi: (mijoz, as-of oy) ===
  qatorlar: 68532, as-of oylar: 2024-07 - 2025-10 (16 oy)
  oyiga nomzod mijozlar (hozir kechikmagan): 4064 - 4475
  60 kun ichida 30+ kun kechikish (y30): 0.109
  60 kun ichida 60+ kun kechikish (y60): 0.020
  oyiga DPD60 holatlari: o'rtacha 87.6 (min 66, max 116)
  qo'ng'iroq sig'imi: oyiga K = 200 ta = nomzodlarning 4.7% i

=== 3. Bitta qo'ng'iroqning qiymati (xarajat matritsasi, mln so'm) ===
  DPD60 bo'lgan mijozning kutilgan zarari: o'rtacha 3.64, mediana 3.12
  qo'ng'iroq qilinsa (qo'ng'iroqsiz holatga nisbatan):
    mijoz DPD60 ga tushadigan bo'lsa (TP):  +0.3 * zarar - 0.02
    mijoz DPD60 ga tushmaydigan bo'lsa (FP): -0.02
  qo'ng'iroq qilinmasa: 0 (FN da zarar to'liq qoladi, TN - hech narsa)
  zarar konsentratsiyasi: eng katta 20% DPD60 zararning 38.9% i
  zararsizlik chegarasi (mediana zararda): p > 0.021; bazaviy ulush 0.020

=== 4. Qiymat chegaralari: val oylari 16-18, oyiga tejam (mln so'm) ===
  hech kimga qo'ng'iroq yo'q        +0.00
  tasodifiy 200 ta (50 marta)       +0.01
  qoida: util eng yuqori           +24.26
  oracle (kelajakni biladi)        +78.57
  oldini olish mumkin bo'lgan jami zarar (hammaga qo'ng'iroq): 82.6

=== 5. Xulosa (natijadan hisoblangan) ===
  tasodifiy qo'ng'iroq deyarli nol (+0.01) -> reyting shart
  oddiy qoida oracle qiymatining 31% ini beradi
  model uchun o'yin maydoni: oyiga ko'pi bilan 54.3 mln so'm (amalda ancha kam)
  sig'im DPD60 holatlaridan katta -> muammo reytingda, sig'imda emas

Natija tahlili.

1-bo'lim — xom ma'lumot: to'rtta jadval va 24 oy. Tranzaksiyalar eng katta jadval (586885 qator). Mijozlar jadvali eng kichigi (6000), lekin undagi belgilar (daromad, limit, avto to'lov) har as-of oyda qayta ishlatiladi.

2-bo'lim — tahlil birligi. Model mijozni emas, (mijoz, as-of oy) juftini baholaydi: har oy oxirida "hozir kechikmagan" mijozlar nomzod bo'ladi, oyiga 4064 dan 4475 gacha. Jami 68532 qator, 16 ta as-of oy (2024-07 dan 2025-10 gacha). Birinchi 6 oy belgilar tarixi uchun ketadi, oxirgi 2 oy esa belgi yetilishi uchun: 2025-11 va 2025-12 dagi as-of lar uchun natija hali ma'lum emas. Ikki ta'rif keskin farq qiladi: 60 kun ichida 30+ kunlik kechikish 10.9%, 60+ kunlik esa 2.0%. Oyiga o'rtacha 87.6 ta DPD60 holati bor, sig'im esa 200 qo'ng'iroq. Demak sig'im muammo emas: mukammal model hamma DPD60 larga qo'ng'iroq qila olardi. Muammo — ularni 4 mingdan ortiq nomzod ichidan topish, ya'ni reyting.

3-bo'lim — bitta qo'ng'iroqning qiymati. TP qo'ng'iroq +0.3 * zarar - 0.02 beradi, FP esa -0.02. DPD60 ga tushadiganlarning o'rtacha kutilgan zarari 3.64 mln so'm, mediana 3.12. Taqsimot o'ngga qiyshiq: eng katta 20% holat jami zararning 38.9% ini beradi. Shuning uchun qaysi DPD60 ni ushlash ham muhim, faqat nechtasini ushlash emas. Zararsizlik chegarasi p > 0.021 va bazaviy ulushga (0.020) juda yaqin: tasodifan tanlangan mijozga qo'ng'iroq o'rtacha deyarli nol qiymat beradi. Bu 4-bo'limda tasdiqlanadi.

4-bo'lim — qiymat zinapoyasi val oylarida (16-18). Qo'ng'iroq qilmaslik 0. Tasodifiy 200 ta qo'ng'iroq (50 marta o'rtachasi) +0.01, ya'ni deyarli nol. Bitta ustunli qoida ("limitdan foydalanish ulushi eng yuqori 200") +24.26. Oracle +78.57 mln so'm/oy tejaydi. Hammaga qo'ng'iroq qilinsa (qo'ng'iroq narxisiz), oldini olish mumkin bo'lgan jami zarar 82.6, lekin 4 ming qo'ng'iroq sig'imdan 20 barobar katta.

5-bo'lim — xulosalar raqamdan chiqarildi. Oddiy qoida oracle qiymatining 31% ini beradi, demak modelning o'yin maydoni oyiga ko'pi bilan 54.3 mln so'm. Amalda model oracle ga yaqinlasha olmaydi: kelajakdagi qiyinchilik to'liq kuzatilmaydi. Shuning uchun haqiqiy savol "model qoidadan qancha qo'shadi?", va bu 3-misolda o'lchanadi. Loyiha hujjatiga ikki raqam yoziladi: yuqori chegara (78.6) va joriy qoida (24.3). Qolgan hamma natija shu ikkisi orasida baholanadi.

Misol 2 — Model metrikasi va biznes metrikasi: AUC va tejalgan zarar

python
"""Model metrikasi va biznes metrikasi: AUC va tejalgan zarar bir xil narsani o'lchamaydi."""

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
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 oy_guruhlari(df):
    oy = df["oy"].to_numpy()
    return [np.flatnonzero(oy == t) for t in np.unique(oy)]


def tejam_k(qiymat, ball, guruhlar, k=K):
    """Har oyda ball bo'yicha top-k; oylik o'rtacha tejam (mln so'm)."""
    jami = 0.0
    for g in guruhlar:
        jami += qiymat[g[np.argsort(-ball[g], kind="stable")[:k]]].sum()
    return jami / len(guruhlar)


def bootstrap(y, qiymat, ballar, guruhlar, takror=300, seed=1):
    """Oy ichida qayta tanlash: har ball uchun AUC va tejam namunalari (juftlashgan)."""
    rng = np.random.default_rng(seed)
    auc = {k: [] for k in ballar}
    tej = {k: [] for k in ballar}
    for _ in range(takror):
        gb = [rng.choice(g, len(g)) for g in guruhlar]
        idx = np.concatenate(gb)
        yangi = []
        bosh = 0
        for g in gb:
            yangi.append(np.arange(bosh, bosh + len(g)))
            bosh += len(g)
        for k, b in ballar.items():
            auc[k].append(roc_auc_score(y[idx], b[idx]))
            tej[k].append(tejam_k(qiymat[idx], b[idx], yangi))
    return ({k: np.array(v) for k, v in auc.items()},
            {k: np.array(v) for k, v in tej.items()})


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

    lr = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
    p = lr.fit(tr[BELGILAR], tr["y60"]).predict_proba(va[BELGILAR])[:, 1]
    print("=== 1. LogReg (y60), o'quv: as-of 7-14, val: 16-18 ===")
    print(f"  AUC {roc_auc_score(y, p):.4f}, tejam (top-{K}, p bo'yicha) "
          f"{tejam_k(qiymat, p, guruhlar):.1f} mln so'm/oy")
    tp = fp = fn = 0
    for g in guruhlar:
        top = g[np.argsort(-p[g], kind="stable")[:K]]
        tp += y[top].sum()
        fp += K - y[top].sum()
        fn += y[g].sum() - y[top].sum()
    m = len(guruhlar)
    print(f"  oyiga: TP {tp / m:.1f}, FP {fp / m:.1f}, FN {fn / m:.1f}; "
          f"precision@{K} {tp / (tp + fp):.3f}, recall@{K} {tp / (tp + fn):.3f}")

    print("\n=== 2. Bir xil model, ikki reyting: p va p * zarar ===")
    pz = p * zarar
    ballar = {"p": p, "p*zarar": pz}
    for k, b in ballar.items():
        print(f"  {k:<8} AUC {roc_auc_score(y, b):.4f}   tejam "
              f"{tejam_k(qiymat, b, guruhlar):6.1f}")

    print("\n=== 3. AUC oshadi, tejam o'zgarmaydi: faqat top-K dan tashqari tartib ===")
    yangi = p.copy()
    for g in guruhlar:
        tartib = g[np.argsort(-p[g], kind="stable")]
        top, qolgan = tartib[:K], tartib[K:]
        kuchaytir = p[qolgan] * (1 + 4 * y[qolgan])      # musbatlarni yuqoriga
        daraja = np.argsort(np.argsort(kuchaytir)) / len(qolgan)
        yangi[qolgan] = p[top].min() * 0.999 * daraja
    ballar["p (tashqari tuzatilgan)"] = yangi
    for k in ["p", "p (tashqari tuzatilgan)"]:
        print(f"  {k:<24} AUC {roc_auc_score(y, ballar[k]):.4f}   tejam "
              f"{tejam_k(qiymat, ballar[k], guruhlar):6.1f}")

    print("\n=== 4. Juftlashgan bootstrap (oy ichida, 300 takror): AUC va tejam farqi ===")
    hgb = HistGradientBoostingClassifier(max_iter=200, learning_rate=0.05,
                                         max_leaf_nodes=15, min_samples_leaf=100,
                                         random_state=0)
    ph = hgb.fit(tr[BELGILAR], tr["y60"]).predict_proba(va[BELGILAR])[:, 1]
    ballar = {"p": p, "p*zarar": pz, "HGB p*zarar": ph * zarar}
    auc_b, tej_b = bootstrap(y, qiymat, ballar, guruhlar)
    xulosa = {}
    for a, b in [("p*zarar", "p"), ("HGB p*zarar", "p*zarar")]:
        da, dt = auc_b[a] - auc_b[b], tej_b[a] - tej_b[b]
        ta = roc_auc_score(y, ballar[a]) - roc_auc_score(y, ballar[b])
        tt = tejam_k(qiymat, ballar[a], guruhlar) - tejam_k(qiymat, ballar[b], guruhlar)
        print(f"  {a} - {b}:")
        print(f"    AUC farqi   {ta:+.4f} (SE {da.std(ddof=1):.4f}) "
              f"sezilarli: {abs(ta) > 2 * da.std(ddof=1)}")
        print(f"    tejam farqi {tt:+7.2f} (SE {dt.std(ddof=1):.2f}) "
              f"sezilarli: {abs(tt) > 2 * dt.std(ddof=1)}")
        xulosa[a] = (ta, abs(ta) > 2 * da.std(ddof=1), tt, abs(tt) > 2 * dt.std(ddof=1))

    print("\n=== 5. Xulosa (natijadan hisoblangan) ===")
    for nom, (ta, sa, tt, st) in xulosa.items():
        if sa and not st:
            print(f"  {nom}: AUC farqi sezilarli ({ta:+.4f}), tejam farqi yo'q "
                  f"({tt:+.2f}) -> AUC farqi qiymatga o'tmadi")
    a_p = roc_auc_score(y, p)
    a_y = roc_auc_score(y, yangi)
    if abs(tejam_k(qiymat, yangi, guruhlar) - tejam_k(qiymat, p, guruhlar)) < 1e-9:
        print(f"  top-K tashqarisi: AUC {a_y - a_p:+.4f}, tejam aynan o'zgarmadi")
    print("  qaror top-K ga tayansa, metrika ham top-K qiymatini o'lchasin")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. LogReg (y60), o'quv: as-of 7-14, val: 16-18 ===
  AUC 0.8482, tejam (top-200, p bo'yicha) 26.3 mln so'm/oy
  oyiga: TP 24.0, FP 176.0, FN 52.7; precision@200 0.120, recall@200 0.313

=== 2. Bir xil model, ikki reyting: p va p * zarar ===
  p        AUC 0.8482   tejam   26.3
  p*zarar  AUC 0.8314   tejam   28.8

=== 3. AUC oshadi, tejam o'zgarmaydi: faqat top-K dan tashqari tartib ===
  p                        AUC 0.8482   tejam   26.3
  p (tashqari tuzatilgan)  AUC 0.9536   tejam   26.3

=== 4. Juftlashgan bootstrap (oy ichida, 300 takror): AUC va tejam farqi ===
  p*zarar - p:
    AUC farqi   -0.0169 (SE 0.0046) sezilarli: True
    tejam farqi   +2.53 (SE 2.58) sezilarli: False
  HGB p*zarar - p*zarar:
    AUC farqi   -0.0127 (SE 0.0039) sezilarli: True
    tejam farqi   +0.18 (SE 1.94) sezilarli: False

=== 5. Xulosa (natijadan hisoblangan) ===
  p*zarar: AUC farqi sezilarli (-0.0169), tejam farqi yo'q (+2.53) -> AUC farqi qiymatga o'tmadi
  HGB p*zarar: AUC farqi sezilarli (-0.0127), tejam farqi yo'q (+0.18) -> AUC farqi qiymatga o'tmadi
  top-K tashqarisi: AUC +0.1054, tejam aynan o'zgarmadi
  qaror top-K ga tayansa, metrika ham top-K qiymatini o'lchasin

Natija tahlili.

1-bo'lim — oddiy LogReg (y60 da, as-of 7-14 da o'qitilgan). Val da AUC 0.8482. Ko'pchilik hisobotlar shu raqam bilan tugaydi. Qarorga tarjima qilsak, boshqacha rasm chiqadi. Ehtimol bo'yicha top-200 ga qo'ng'iroq oyiga 26.3 mln so'm tejaydi. 200 qo'ng'iroqdan o'rtacha faqat 24.0 tasi haqiqiy DPD60 (precision@200 0.120), va DPD60 larning 31.3% i ushlanadi (recall). "AUC 0.85" rahbarga yuqori raqamdek ko'rinadi. Lekin qo'ng'iroq markazi uchun uning ma'nosi: "har 8-9 qo'ng'iroqdan bittasi foydali". Bu raqam ham to'g'ri, ham kutilganlarni real darajaga tushiradi. Muvaffaqiyat mezoni aynan shunday tilda yozilishi kerak.

2-bo'lim — bir xil model, ikki reyting. p bo'yicha tartiblaganda AUC 0.8482, p * zarar bo'yicha esa 0.8314, ya'ni AUC pasaydi. Chunki kichik balansli, lekin ehtimoli yuqori mijozlar pastga tushdi, AUC uchun esa ular ham muhim. Tejam esa 26.3 dan 28.8 ga oshdi. Nazariy jihatdan bu kutilgan natija: qo'ng'iroqning kutilgan qiymati p * 0.30 * zarar - 0.02, shuning uchun p * zarar bo'yicha tartib — kalibrlangan p uchun optimal tartib.

3-bo'lim — AUC oshadi, tejam o'zgarmaydi. Faqat top-200 dan tashqaridagi mijozlar tartibini "tuzatdik": musbatlarni yuqoriroqqa, lekin chegaradan pastga surdik. AUC 0.8482 dan 0.9536 ga sakradi, tejam esa aynan 26.3 bo'lib qoldi, chunki qo'ng'iroq qilinadigan ro'yxat o'zgarmadi. Bu sun'iy tajriba, lekin amalda xuddi shu hol uchraydi: yangi belgi past xavfli mijozlarni yaxshiroq ajratadi, AUC o'sadi, biznes esa hech narsani sezmaydi.

4-bo'lim — juftlashgan bootstrap (oy ichida qayta tanlash, 300 takror). Ikkala taqqoslashda ham rasm bir xil:

  • p * zarar va p: AUC farqi -0.0169 (SE 0.0046), sezilarli. Tejam farqi +2.53 (SE 2.58), sezilarli emas.
  • HistGB va LogReg (ikkalasi p * zarar bilan): AUC farqi -0.0127 (SE 0.0039), sezilarli. Tejam farqi +0.18 (SE 1.94), sezilarli emas.

Halol xulosa: AUC bo'yicha "sezilarli" farqlar biznes qiymatida sezilarli farqqa aylanmadi. p * zarar yo'nalishi nazariya bilan mos, lekin 3 oylik val uni isbotlash uchun yetarli emas. Buni 29.3 da ko'proq oy bilan tekshiramiz. Qaror qanday metrika bilan qabul qilinsa, taqqoslash ham shu metrika bilan va uning noaniqligi bilan qilinishi kerak. AUC farqining SE si tejam farqinikidan ancha kichik. Buning sababi: AUC barcha qatorlardan, tejam esa faqat top-200 dan hisoblanadi. Biznes metrikasi shovqinliroq, shuning uchun uni baholash uchun ko'proq ma'lumot (oylar) kerak. Bu loyiha rejasiga ta'sir qiladi: 29.3 da baholash 7 oy bo'yicha qilinadi.

Misol 3 — Bazaviylar va "model kerakmi?": qoidalar, ekspert ball-kartasi va LogReg

python
"""Bazaviylar va "model kerakmi?": qoidalar, ekspert ball-kartasi va LogReg."""

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 oy_guruhlari(df):
    oy = df["oy"].to_numpy()
    return [np.flatnonzero(oy == t) for t in np.unique(oy)]


def tejam_k(qiymat, ball, guruhlar, k=K):
    jami = 0.0
    for g in guruhlar:
        jami += qiymat[g[np.argsort(-ball[g], kind="stable")[:k]]].sum()
    return jami / len(guruhlar)


def ekspert_ball(df):
    """Kredit bo'limi tajribasidan: oddiy ball-karta, teng ballda katta balans oldin."""
    ball = (2 * (df["util"] > 0.8) + 1 * (df["naqd_soni_3"] >= 2)
            + 2 * (df["tolov_nisbati_3"] < 0.15) + 1 * (df["util_ozg"] > 0.15))
    return (ball + df["zarar"] / 100).to_numpy()


def main() -> None:
    df = belgilar_jadvali(yarat_bank())
    tr, va = df[df.oy <= 14], df[df.oy.between(16, 18)].reset_index(drop=True)
    guruhlar = oy_guruhlari(va)
    qiymat = va["qiymat"].to_numpy()
    lr = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
    p = lr.fit(tr[BELGILAR], tr["y60"]).predict_proba(va[BELGILAR])[:, 1]
    rng = np.random.default_rng(0)
    nomzodlar = {                       # soddalik tartibida
        "0 hech kim": None,
        "1 tasodifiy": rng.random(len(va)),
        "2 qoida: util": va["util"].to_numpy(),
        "3 qoida: util*zarar": (va["util"] * va["zarar"]).to_numpy(),
        "4 ekspert ball-karta": ekspert_ball(va),
        "5 LogReg p*zarar": p * va["zarar"].to_numpy(),
    }

    print("=== 1. Val oylari 16-18: oylik tejam (mln so'm) ===")
    baho = {}
    for nom, b in nomzodlar.items():
        baho[nom] = 0.0 if b is None else tejam_k(qiymat, b, guruhlar)
        print(f"  {nom:<22} {baho[nom]:>7.2f}")

    print("\n=== 2. Juftlashgan bootstrap (oy ichida, 300): eng yaxshisidan farq ===")
    eng = max(baho, key=baho.get)
    bs = {nom: [] for nom in nomzodlar}
    rng = np.random.default_rng(1)
    for _ in range(300):
        gb = [rng.choice(g, len(g)) for g in guruhlar]
        idx = np.concatenate(gb)
        chegara = np.cumsum([0] + [len(g) for g in gb])
        yangi = [np.arange(chegara[i], chegara[i + 1]) for i in range(len(gb))]
        for nom, b in nomzodlar.items():
            bs[nom].append(0.0 if b is None else tejam_k(qiymat[idx], b[idx], yangi))
    tanlov = None
    print(f"  eng yaxshisi: {eng}")
    print(f"  {'nomzod':<22} {'farq':>7} {'SE':>6} {'sezilarli yomon':>16}")
    for nom in nomzodlar:
        d = np.array(bs[nom]) - np.array(bs[eng])
        farq, se = baho[nom] - baho[eng], d.std(ddof=1)
        yomon = farq < -2 * se
        if not yomon and tanlov is None:
            tanlov = nom
        print(f"  {nom:<22} {farq:>+7.2f} {se:>6.2f} {str(yomon):>16}")
    print(f"  qaror qoidasi -> eng sodda munosib: {tanlov}")

    print("\n=== 3. \"Model kerakmi?\" - pul bilan ===")
    qoidalar = [n for n in nomzodlar if n[0] in "234"]
    eng_qoida = max(qoidalar, key=baho.get)
    d = np.array(bs["5 LogReg p*zarar"]) - np.array(bs[eng_qoida])
    farq, se = baho["5 LogReg p*zarar"] - baho[eng_qoida], d.std(ddof=1)
    lo, hi = 12 * (farq - 2 * se), 12 * (farq + 2 * se)
    print(f"  eng yaxshi qoida: {eng_qoida}")
    print(f"  model - qoida: oyiga {farq:+.2f} (SE {se:.2f}) -> yiliga {12 * farq:+.0f} "
          f"mln so'm, +-2*SE: [{lo:+.0f}, {hi:+.0f}]")
    print(f"  qoida model qiymatining {baho[eng_qoida] / baho['5 LogReg p*zarar']:.0%} "
          f"ini beradi")

    print("\n=== 4. Sig'im o'zgarsa: qoida va model farqi (oyiga, mln so'm) ===")
    print(f"  {'K':>5} {'qoida':>8} {'model':>8} {'farq':>7} {'model/qoida':>12}")
    for k in [50, 200, 800, 2000]:
        a = tejam_k(qiymat, nomzodlar[eng_qoida], guruhlar, k)
        b = tejam_k(qiymat, nomzodlar["5 LogReg p*zarar"], guruhlar, k)
        print(f"  {k:>5} {a:>8.2f} {b:>8.2f} {b - a:>+7.2f} {b / a:>12.2f}")

    print("\n=== 5. Xulosa (natijadan hisoblangan) ===")
    if tanlov == "5 LogReg p*zarar":
        print("  eng sodda munosib - model: qoidalar sezilarli yomon")
    else:
        print(f"  eng sodda munosib - {tanlov}: model sezilarli ustun emas")
    if lo <= 0:
        print("  3 oylik val da model foydasi noaniq (interval 0 ni qamraydi):")
        print("  prototip qarori - qoida bazaviy va zaxira sifatida saqlanadi, model")
        print("  ko'proq oyda (vaqt bo'yicha CV) tekshiriladi")
    else:
        print("  model foydasi 2*SE dan katta - loyiha keyingi bosqichga o'tadi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Val oylari 16-18: oylik tejam (mln so'm) ===
  0 hech kim                0.00
  1 tasodifiy              -0.32
  2 qoida: util            24.26
  3 qoida: util*zarar      23.04
  4 ekspert ball-karta     19.36
  5 LogReg p*zarar         28.85

=== 2. Juftlashgan bootstrap (oy ichida, 300): eng yaxshisidan farq ===
  eng yaxshisi: 5 LogReg p*zarar
  nomzod                    farq     SE  sezilarli yomon
  0 hech kim              -28.85   4.10             True
  1 tasodifiy             -29.17   4.09             True
  2 qoida: util            -4.59   2.93            False
  3 qoida: util*zarar      -5.81   2.98            False
  4 ekspert ball-karta     -9.49   3.09             True
  5 LogReg p*zarar         +0.00   0.00            False
  qaror qoidasi -> eng sodda munosib: 2 qoida: util

=== 3. "Model kerakmi?" - pul bilan ===
  eng yaxshi qoida: 2 qoida: util
  model - qoida: oyiga +4.59 (SE 2.93) -> yiliga +55 mln so'm, +-2*SE: [-15, +125]
  qoida model qiymatining 84% ini beradi

=== 4. Sig'im o'zgarsa: qoida va model farqi (oyiga, mln so'm) ===
      K    qoida    model    farq  model/qoida
     50     9.68    14.29   +4.61         1.48
    200    24.26    28.85   +4.59         1.19
    800    38.98    48.44   +9.46         1.24
   2000    34.09    38.32   +4.24         1.12

=== 5. Xulosa (natijadan hisoblangan) ===
  eng sodda munosib - 2 qoida: util: model sezilarli ustun emas
  3 oylik val da model foydasi noaniq (interval 0 ni qamraydi):
  prototip qarori - qoida bazaviy va zaxira sifatida saqlanadi, model
  ko'proq oyda (vaqt bo'yicha CV) tekshiriladi

Natija tahlili.

1-bo'lim — oltita nomzod soddalik tartibida. Tasodifiy tanlov bu safar -0.32 — nolga yaqin, ammo manfiy: 1-misoldagi "deyarli nol" tasdiqlandi. Bitta ustunli qoida 24.26, util * zarar qoidasi 23.04. Ekspert ball-kartasi 19.36 va u oddiy qoidadan ham yomon. Sababi: ballar diskret, bir xil ballli yuzlab mijoz orasida tartib faqat balansga qoladi. Bundan tashqari ekspert tanlagan chegaralar (util > 0.8, tolov_nisbati_3 < 0.15) ma'lumotda tekshirilmagan. LogReg p * zarar eng yaxshisi: 28.85.

2-bo'lim — qaror qoidasi. Eng yaxshisidan farqlar juftlashgan bootstrap bilan baholandi. Qoida "util" -4.59 (SE 2.93) farq qildi. Bu 2*SE = 5.86 dan kichik, ya'ni sezilarli yomon emas. "util * zarar" ham shunday: -5.81, SE 2.98. Ekspert kartasi esa sezilarli yomon: -9.49, SE 3.09. Qoida bo'yicha "eng sodda munosib" — bitta ustunli qoida. Bu kutilmagan va noqulay natija: 1-misoldan keyin biz model qurishga tayyor edik. Lekin qoida aniq: 3 oy ma'lumotda modelning afzalligi shovqindan ajralmaydi.

3-bo'lim — pul bilan. Model qoidadan oyiga +4.59 ko'p tejaydi, yiliga taxminan +55 mln so'm. Lekin +-2*SE oralig'i [-15, +125]: nolni qamraydi. Qoida model qiymatining 84% ini beradi. Bu raqamlarni loyiha xarajati bilan solishtirish kerak: ishlab chiqish, ma'lumot quvuri, monitoring va qayta o'qitish (27-qism). Agar yillik xarajat oraliqning pastki qismidan katta bo'lsa, "model kerak emas" javobi to'liq mumkin.

4-bo'lim — sig'im o'zgarsa. K = 50 da model qoidadan 1.48 barobar ko'p tejaydi: resurs qanchalik tanqis bo'lsa, reyting sifati shunchalik qimmat. K = 800 da farq eng katta (+9.46 mln so'm). K = 2000 da ikkalasining tejami K = 800 dagidan kam (34.09 va 38.32): ortiqcha qo'ng'iroqlar zararsizlik chegarasidan past mijozlarga ketib, pul yo'qotadi. Bundan ikki amaliy saboq chiqadi. Birinchidan, "model kerakmi?" savoliga javob sig'imga bog'liq. Ikkinchidan, sig'imni oshirish har doim ham foydali emas, va optimal K ni 29.3-bob ham hisoblash kerak.

5-bo'lim — prototip bosqichi qarori raqamlardan chiqdi. Qoida bazaviy va zaxira sifatida saqlanadi (u darhol ishlatilishi mumkin). Model loyihasi to'xtatilmaydi, lekin 2.6 dagi darvoza "model ustunligini ko'proq oyda isbotlash" deb yoziladi. 29.3 da vaqt bo'yicha CV 7 oyda aynan shu savolga javob beradi.

Misol 4 — Loyiha xatarlari: belgi ta'rifi (30 yoki 60 kun) va ma'lumot mavjudligi

python
"""Loyiha xatarlari: belgi ta'rifi (30 yoki 60 kun) va ma'lumot mavjudligi."""

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 oy_guruhlari(df):
    oy = df["oy"].to_numpy()
    return [np.flatnonzero(oy == t) for t in np.unique(oy)]


def top_k(ball, guruhlar, k=K):
    return np.concatenate([g[np.argsort(-ball[g], kind="stable")[:k]] for g in guruhlar])


def lr_ball(tr, va, belgi):
    m = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
    return m.fit(tr[BELGILAR], tr[belgi]).predict_proba(va[BELGILAR])[:, 1]


def main() -> None:
    df = belgilar_jadvali(yarat_bank())
    print("=== 1. Ikki ta'rif (as-of 7-22, barcha qatorlar) ===")
    print(f"  y30 (60 kun ichida 30+ kun kechikish): {df.y30.mean():.3f}")
    print(f"  y60 (60 kun ichida 60+ kun kechikish): {df.y60.mean():.3f}")
    print(f"  y60 = 1 bo'lganlar ichida y30 = 1: {df.loc[df.y60 == 1, 'y30'].mean():.3f}")
    ozi = ((df.y30 == 1) & (df.y60 == 0)).sum() / df.y30.sum()
    print(f"  y30 = 1 lar ichida o'zi to'lagan (y60 = 0): {ozi:.1%}")
    print(f"  oldingi 6 oyda kechikkan (kechikish_6 > 0) ulushi: "
          f"y60 da {(df.loc[df.y60 == 1, 'kechikish_6'] > 0).mean():.3f}, "
          f"faqat y30 da {(df.loc[(df.y30 == 1) & (df.y60 == 0), 'kechikish_6'] > 0).mean():.3f}")
    print(f"  avto_tolov ulushi: y60 da {df.loc[df.y60 == 1, 'avto_tolov'].mean():.3f}, "
          f"faqat y30 da {df.loc[(df.y30 == 1) & (df.y60 == 0), 'avto_tolov'].mean():.3f}")

    print("\n=== 2. Qaysi belgida o'qitamiz? o'quv 7-14, val 16-18 ===")
    tr, va = df[df.oy <= 14], df[df.oy.between(16, 18)].reset_index(drop=True)
    guruhlar = oy_guruhlari(va)
    qiymat, zarar = va["qiymat"].to_numpy(), va["zarar"].to_numpy()
    ballar = {b: lr_ball(tr, va, b) for b in ["y30", "y60"]}
    print(f"  {'model':<10} {'AUC y30':>8} {'AUC y60':>8} {'tejam/oy':>9} "
          f"{'y60':>6} {'faqat y30':>10} {'toza':>6}")
    tejamlar = {}
    for b, p in ballar.items():
        top = top_k(p * zarar, guruhlar)
        tejamlar[b] = qiymat[top].sum() / len(guruhlar)
        y60, y30 = va["y60"].to_numpy()[top], va["y30"].to_numpy()[top]
        m = len(guruhlar)
        print(f"  {b + ' da':<10} {roc_auc_score(va.y30, p):>8.4f} "
              f"{roc_auc_score(va.y60, p):>8.4f} {tejamlar[b]:>9.2f} "
              f"{y60.sum() / m:>6.1f} {((y30 == 1) & (y60 == 0)).sum() / m:>10.1f} "
              f"{(y30 == 0).sum() / m:>6.1f}")
    print("  ko'proq dalil: har oy t = 12-18 uchun o'quv - as-of <= t-2 (7 juft):")
    farqlar = []
    for t in range(12, 19):
        o, s = df[df.oy <= t - 2], df[df.oy == t]
        q, z = s["qiymat"].to_numpy(), s["zarar"].to_numpy()
        tt = {b: q[np.argsort(-lr_ball(o, s, b) * z, kind="stable")[:K]].sum()
              for b in ["y30", "y60"]}
        farqlar.append(tt["y60"] - tt["y30"])
    farqlar = np.array(farqlar)
    farq, se = farqlar.mean(), farqlar.std(ddof=1) / np.sqrt(len(farqlar))
    print(f"  oylik farqlar (y60 - y30): {np.round(farqlar, 1).tolist()}")
    print(f"  o'rtacha {farq:+.2f} mln so'm/oy (SE {se:.2f}), sezilarli: "
          f"{abs(farq) > 2 * se}, y60 yaxshi oylar: {(farqlar > 0).sum()}/{len(farqlar)}")

    print("\n=== 3. Ma'lumot mavjudligi: belgi qachon yetiladi? ===")
    print("  as-of t uchun y30/y60 (t va t+1 hisobvaraqlari) (t+2)-oy oxirida ma'lum")
    for nom, ufq in [("y60 (60 kun)", 2), ("y90 (90 kun, faraziy)", 3)]:
        oxirgi = 16 - ufq
        print(f"  {nom:<22} 16-oy oxirida o'qitish uchun oxirgi as-of: {oxirgi} "
              f"-> {oxirgi - 6} oy tarix")
    sarlavha = "o'quv oylari"
    print(f"  {sarlavha:<14} {'qator':>6} {'y60 soni':>9} {'AUC':>7} {'tejam/oy':>9}")
    natija = {}
    for bosh in [13, 11, 7]:
        t2 = df[df.oy.between(bosh, 14)]
        p = lr_ball(t2, va, "y60")
        natija[bosh] = qiymat[top_k(p * zarar, guruhlar)].sum() / len(guruhlar)
        print(f"  {f'{bosh}-14':<14} {len(t2):>6} {t2.y60.sum():>9} "
              f"{roc_auc_score(va.y60, p):>7.4f} {natija[bosh]:>9.2f}")

    print("\n=== 4. Xulosa (natijadan hisoblangan) ===")
    if farq > 2 * se:
        print(f"  y30 da o'qitilgan model oyiga {farq:.1f} mln so'm kam tejaydi: u o'zi "
              f"to'laydigan 'unutuvchan'larni ham tanlaydi")
    print(f"  y30 lar ichida {ozi:.0%} i o'zi to'laydi -> ta'rif qaror bilan mos bo'lishi shart")
    qiymatlar = np.array(list(natija.values()))
    if qiymatlar.max() - qiymatlar.min() < 0.1 * qiymatlar.mean():
        print(f"  2, 4 va 8 oylik o'quv yaqin ({qiymatlar.min():.2f}-{qiymatlar.max():.2f})"
              f" -> bu vazifada tarix hajmi asosiy cheklov emas")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ikki ta'rif (as-of 7-22, barcha qatorlar) ===
  y30 (60 kun ichida 30+ kun kechikish): 0.109
  y60 (60 kun ichida 60+ kun kechikish): 0.020
  y60 = 1 bo'lganlar ichida y30 = 1: 1.000
  y30 = 1 lar ichida o'zi to'lagan (y60 = 0): 81.3%
  oldingi 6 oyda kechikkan (kechikish_6 > 0) ulushi: y60 da 0.223, faqat y30 da 0.323
  avto_tolov ulushi: y60 da 0.481, faqat y30 da 0.258

=== 2. Qaysi belgida o'qitamiz? o'quv 7-14, val 16-18 ===
  model       AUC y30  AUC y60  tejam/oy    y60  faqat y30   toza
  y30 da       0.7302   0.7369     22.41   16.3       47.3  136.3
  y60 da       0.6561   0.8482     28.85   20.7       35.0  144.3
  ko'proq dalil: har oy t = 12-18 uchun o'quv - as-of <= t-2 (7 juft):
  oylik farqlar (y60 - y30): [2.8, 6.3, 9.4, 8.5, 7.4, 6.8, 7.9]
  o'rtacha +7.00 mln so'm/oy (SE 0.81), sezilarli: True, y60 yaxshi oylar: 7/7

=== 3. Ma'lumot mavjudligi: belgi qachon yetiladi? ===
  as-of t uchun y30/y60 (t va t+1 hisobvaraqlari) (t+2)-oy oxirida ma'lum
  y60 (60 kun)           16-oy oxirida o'qitish uchun oxirgi as-of: 14 -> 8 oy tarix
  y90 (90 kun, faraziy)  16-oy oxirida o'qitish uchun oxirgi as-of: 13 -> 7 oy tarix
  o'quv oylari    qator  y60 soni     AUC  tejam/oy
  13-14            8488       192  0.8434     28.56
  11-14           16918       373  0.8405     30.47
  7-14            33358       697  0.8482     28.85

=== 4. Xulosa (natijadan hisoblangan) ===
  y30 da o'qitilgan model oyiga 7.0 mln so'm kam tejaydi: u o'zi to'laydigan 'unutuvchan'larni ham tanlaydi
  y30 lar ichida 81% i o'zi to'laydi -> ta'rif qaror bilan mos bo'lishi shart
  2, 4 va 8 oylik o'quv yaqin (28.56-30.47) -> bu vazifada tarix hajmi asosiy cheklov emas

Natija tahlili.

1-bo'lim — ikki ta'rif. y60 — y30 ning qism to'plami: DPD60 larning 1.000 qismida y30 ham 1, chunki 60 kunlik kechikish avval 30 kunlik bo'ladi. Lekin teskarisi to'g'ri emas: y30 larning 81.3% i ikkinchi hisobvaraqni o'zi to'lagan. Ular boshqa turdagi mijozlar. Faqat y30 bo'lganlarning 32.3% i oldin ham kechikkan (DPD60 larda 22.3%), avto to'lov esa ularda kamroq (0.258 va 0.481). Bu "unutuvchan" mijozlar portreti: vaqti-vaqti bilan kechikadi, lekin to'laydi. Qiziq tomoni: kechikish_6 belgisi DPD60 dan ko'ra ko'proq aynan ularni ko'rsatadi.

2-bo'lim — qaysi belgida o'qitish kerak? y30 da o'qitilgan model o'z belgisida yaxshiroq (AUC y30 0.7302 va 0.6561). Lekin biznes uchun muhim bo'lgan y60 ni yomon ajratadi (0.7369 va 0.8482). Top-200 tarkibi hammasini ko'rsatadi. y30-model ro'yxatida oyiga 16.3 DPD60 va 47.3 "o'zi to'laydigan" mijoz bor, y60-modelda esa 20.7 va 35.0. Tejam: 22.41 va 28.85. 3 oylik val da bu farq bitta son, shuning uchun 7 ta oyning har birida juftlashtirib tekshirdik (har oy t uchun o'quv — as-of <= t-2). y60 yetti oyning yettisida yaxshiroq, o'rtacha +7.00 mln so'm/oy (SE 0.81). Belgi ta'rifi bu loyihadagi eng katta "bepul" yaxshilanish bo'ldi. Hech qanday model murakkabligi bunday farq bermadi (2-misolda HistGB +0.18).

3-bo'lim — ma'lumot mavjudligi. As-of t uchun belgi t+2-oy oxirida ma'lum bo'ladi. Shuning uchun 16-oy oxirida model qayta o'qitilsa, eng oxirgi belgili as-of 14 bo'ladi. Faraziy 90 kunlik ta'rifda bu 13 bo'lardi: har qo'shimcha gorizont oyi modelni bir oy "eskiroq" qiladi. O'quv tarixi uzunligi bo'yicha egri chiziq: 2 oy (8488 qator, 192 musbat) 28.56, 4 oy 30.47, 8 oy 28.85. Farqlar kichik va tartibsiz, demak bu vazifada tarix hajmi cheklov emas. Bu foydali xabar. Yangi mahsulotda, masalan, faqat 3 oylik tarix bo'lganda ham loyihani boshlash mumkin edi. Bir ogohlantirish bor: bu xulosa faqat shu ma'lumot uchun o'lchandi. Kamyob hodisalarda (musbatlar yuztadan kam) tarix hajmi hal qiluvchi bo'lishi mumkin.

4-bo'lim — xulosa loyiha hujjatiga yoziladi: belgi y60, chunki qaror (qo'ng'iroq) zararni kamaytirish uchun. Belgi yetilishi 2 oy. Tarix hajmi cheklov emas.


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

Noto'g'ri fikr To'g'risi
"Loyiha model tanlashdan boshlanadi" Qaror, uning egasi, sig'im va metrikadan boshlanadi
"AUC yuqori — loyiha muvaffaqiyatli" Muvaffaqiyat — biznes metrikasi (tejam), top-K va zarar bilan
"AUC oshsa, tejam ham oshadi" Top-K dan tashqaridagi yaxshilanish qiymat bermaydi (2-misol: AUC +0.105, tejam 0)
"Ehtimol bo'yicha tartiblash yetarli" Kutilgan qiymat bo'yicha: p * zarar
"Model tasodifiydan yaxshi — demak foydali" Model qiymati = model - eng yaxshi qoida
"Qoida — ibtidoiy, model har doim yutadi" 3-misolda 3 oy val da qoida sezilarli yomon emas
"Ko'p musbat bo'lsin deb 30 kunlik belgi olamiz" Belgi qaror bilan mos bo'lsin: y30 oyiga 7 mln kam tejadi
"Sig'imni oshirish har doim foydali" K = 2000 da tejam K = 800 dagidan kam
"Faraziy parametrlar — tafsilot" Ular natijani belgilaydi; pilotda tekshiriladi
"Joriy etish — loyihaning oxiri" Monitoring, qaror halqasi va nazorat guruhi bilan davom etadi

6. Keng tarqalgan xatolar va yechimlari

1. Istak bilan boshlash

python
vazifa = "kechikishni bashorat qiluvchi model"                           # ⚠️
vazifa = {"qaror": "oyiga 200 qo'ng'iroq", "belgi": "y60", "metrika": "tejam/oy"}  # ✅

2. Faqat AUC

python
print(roc_auc_score(y, p))                                              # ⚠️
print(tejam_k(qiymat, p * zarar, guruhlar), roc_auc_score(y, p))        # ✅ ikkalasi

3. Zararsiz tartib

python
ro_yxat = df.nlargest(200, "p")                                         # ⚠️
ro_yxat = df.assign(k=df.p * df.zarar).nlargest(200, "k")               # ✅

4. Bazaviysiz model

python
print("model tasodifiydan 30 mln ko'p tejaydi")                         # ⚠️
print("model eng yaxshi qoidadan +4.6 (SE 2.9) ko'p tejaydi")           # ✅

5. Qulay belgi

python
y = df.y30          # musbatlar ko'p, o'qitish oson                     # ⚠️
y = df.y60          # qaror maqsadi: zararli kechikish                  # ✅

6. Belgi yetilishini unutish

python
oquv = df[df.oy <= joriy_oy]                                            # ⚠️ belgi hali yo'q
oquv = df[df.oy <= joriy_oy - 2]                                        # ✅ 60 kunlik gorizont

7. Nazorat guruhisiz joriy etish

python
qongiroq = top_k(hammasi)                                               # ⚠️ samara o'lchanmaydi
nazorat = rng.random(n) < 0.05; qongiroq = top_k(hammasi[~nazorat])     # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 8.2-dars (o'tilgan): Savol berish — EDA ham savoldan boshlanadi
  • 14.9, 14.10-darslar (o'tilgan): Nomutanosib sinflar va kalibrlash — p * zarar qarori uchun
  • 17.8-dars (o'tilgan): Leakage — 29.2 da as-of bilan o'lchanadi
  • 18.9, 18.10-darslar (o'tilgan): Metrika tanlash va modellarni juftlashgan taqqoslash
  • 27-qism (o'tilgan): Joriy etish, monitoring, qaror halqasi va A/B test
  • 28.11-dars (o'tilgan): Sababiy xulosa — qo'ng'iroq samarasi 0.30-bob aslida sababiy savol
  • 29.2-29.4-darslar: Shu loyihaning ma'lumot, model va taqdimot qismlari
  • 29.10-dars: Intervyudagi case study — aynan shu 2.2 dagi savollar bilan boshlanadi

8. Eng yaxshi amaliyotlar

  1. Loyiha qarordan boshlanadi: kim, qachon, qanday cheklov bilan, nima o'zgaradi.

  2. Biznes metrikasi loyiha boshida yoziladi; model metrikasi — yordamchi.

  3. Qiymat zinapoyasini o'lchang: hech narsa, tasodifiy, qoida, model, oracle.

  4. Model qiymati — eng yaxshi qoidaga nisbatan, noaniqligi bilan.

  5. Belgi ta'rifini qaror bilan moslang va bir necha ta'rifni biznes metrikasi bilan solishtiring.

  6. Belgi yetilishi, sizish va qaror halqasini reja bosqichida hisobga oling.

  7. Faraziy parametrlarni aniq belgilang va pilotda tekshirishni rejalashtiring.

  8. Har bosqich oxirida darvoza: davom etish, qayta ko'rish yoki to'xtatish.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # oyiga 87.6 DPD60, sig'im 200 - sig'im cheklovmi?
2.  # zararsizlik chegarasi 0.021, bazaviy ulush 0.020 - tasodifiy qo'ng'iroq qiymati?
3.  # p o'rniga p * zarar bo'yicha tartib - AUC va tejam qayoqqa?
4.  # top-K dan tashqaridagi tartibni yaxshilasak - AUC va tejam?
5.  # 3 oy val da qoida "util" modeldan -4.59 (SE 2.93) - qaysi tanlanadi?
6.  # K = 50 va K = 800 - model/qoida nisbati qayerda katta?
7.  # K = 2000 - tejam K = 800 dan ko'pmi?
8.  # y30 lar ichida o'zi to'laganlar ulushi taxminan?
9.  # y30 da o'qitilgan model - AUC y30 bo'yicha yaxshimi, tejam bo'yicha-chi?
10. # 90 kunlik belgi - o'quv uchun oxirgi as-of qanday o'zgaradi?
Javoblar
  1. Yo'q — sig'im DPD60 sonidan katta; muammo reytingda (1-misol)
  2. Deyarli nol (+0.01 va -0.32)
  3. AUC pasayadi (0.8482 → 0.8314), tejam oshadi (26.3 → 28.8), lekin 3 oyda sezilarli emas
  4. AUC oshadi 0.9536-bob, tejam aynan o'zgarmaydi
  5. Qoida — eng sodda munosib (farq 2*SE = 5.86 dan kichik)
  6. K = 50 da (1.48 barobar)
  7. Yo'q — 34.09 va 38.32 (K = 800 da 38.98 va 48.44)
  8. ~81%
  9. AUC y30 bo'yicha yaxshi 0.7302-bob, tejam bo'yicha yomon (oyiga ~7 mln kam)
  10. Bir oy oldinga suriladi (14 → 13) — model bir oy eskiroq

Vazifa 2: Xatolarni tuzating

python
1.  hisobot = f"Model AUC {auc:.2f} - loyiha muvaffaqiyatli"

2.  royxat = df[df.oy == t].nlargest(200, "p")

3.  foyda = tejam_k(qiymat, p * zarar, g) - tejam_k(qiymat, rng.random(n), g)
    print(f"model foydasi {foyda:.1f}")

4.  oquv = df[df.oy <= t]       # t - joriy as-of oy

5.  y = df["y30"]               # "kechikish" = 30 kun, ko'p musbat
Javoblar
python
1.  hisobot = f"tejam {tejam:.1f} mln/oy; qoidadan farq {farq:+.1f} (SE {se:.1f}); AUC {auc:.3f}"

2.  royxat = df[df.oy == t].assign(k=lambda d: d.p * d.zarar).nlargest(200, "k")

3.  foyda = tejam_k(qiymat, p * zarar, g) - tejam_k(qiymat, df.util.to_numpy(), g)
    # + juftlashgan SE; tasodifiy emas, eng yaxshi qoida bilan

4.  oquv = df[df.oy <= t - 2]   # belgi 2 oyda yetiladi

5.  y = df["y60"]               # qaror maqsadi bilan mos; y30 ni biznes metrikasida solishtiring

Vazifa 3: Qiymat zinapoyasi

Modellang (1- va 3-misollar asosida):

  1. Qo'ng'iroq samarasi 0.30 o'rniga 0.15 va 0.45 bo'lsin — oracle, qoida va model tejami qanday o'zgaradi? Qaysi qaror o'zgaradi?
  2. Qo'ng'iroq narxi 0.02 o'rniga 0.05 — tasodifiy qo'ng'iroq va K = 800 dagi tejam?
  3. Optimal K ni toping: K = 50, 100, ..., 2000 uchun model va qoida tejami egri chizig'i
  4. Ekspert ball-kartasining chegaralarini ma'lumotda tanlang (masalan, util kvantillari) — u qoidadan o'tadimi?

Vazifa 4: Metrikalar

Modellang (2-misol asosida):

  1. p ni 10 ga ko'paytiring (kalibrlashni buzing) — AUC, top-K tejam va "p * 0.30 * zarar > 0.02" siyosatining tejami qanday o'zgaradi?
  2. Precision@200 va tejam@200 ni oylar bo'yicha chizing — ular bir xil tartibdami?
  3. Log-loss va Brier ni ham hisoblang — qaysi model metrikasi tejam bilan eng yaxshi mos keladi?
  4. Top-K dan ichidagi tartibni o'zgartiring (top-200 ichida aralashtiring) — tejam o'zgaradimi? Nega?

Vazifa 5: Belgi ta'rifi

Modellang (4-misol asosida):

  1. "Kamida bitta kechikish (y30) va balansi 5 mln dan katta" degan gibrid belgi bilan o'qiting — tejam?
  2. y30 modeli va y60 modeli ehtimollarini o'rtachalang — natija?
  3. y30 dagi "o'zi to'laganlar" uchun alohida model quring — ularga qo'ng'iroq o'rniga SMS eslatma yuborilsa (narx 0.001), qo'shimcha qiymat qancha bo'lardi? (faraz: SMS kechikishni 20% kamaytiradi va kechikish to'lovi bankka zarar emas)

Vazifa 6: Loyiha hujjati

Boshqa vazifa uchun 2.7 dagi shablonni to'ldiring: telekom kompaniyasi mijoz ketishini kamaytirish uchun oyiga 500 ta chegirma taklifi bera oladi. Qaror, belgi, gorizont, biznes metrikasi (xarajat matritsasi bilan), bazaviylar, xatarlar va darvozalarni yozing. Qaysi raqamlar faraziy ekanini belgilang.

Vazifa 7: O'ylash

Kredit risk bo'limi boshlig'i: "Bizga murakkab model kerak emas. Hozir ham operatorlar limitdan foydalanish ulushi eng yuqori mijozlarga qo'ng'iroq qilyapti va bu yaxshi ishlayapti. Siz 3 oy ishlab AUC 0.85 li model qildingiz, lekin o'zingiz aytyapsizki, u qoidadan sezilarli yaxshi emas. Loyihani yopamiz."

Javob

Qisqa javob: Boshliq qisman haq: 3 oylik val da model qoidadan sezilarli yaxshi emas. Lekin loyiha faqat "model" emas edi, va ikki topilma qoidaning o'zini ham yaxshilaydi.

1. U nimada haq. 3-misolda qoida "util" modeldan -4.59 (SE 2.93) farq qildi, bu sezilarli emas. Qoida model qiymatining 84% ini beradi. Model xarajati (quvur, monitoring, qayta o'qitish) haqiqiy va uni e'tiborsiz qoldirib bo'lmaydi.

2. Loyiha allaqachon nima berdi.

python
# 1) qiymat chegarasi: oracle oyiga ~78.6 mln - joriy qoida uning 31% ini beradi
# 2) belgi ta'rifi: 30 kunlik ro'yxat oyiga ~7 mln kam tejaydi (7/7 oy) -
#    agar operatorlar "30 kun kechikkanlar"ga ham qo'ng'iroq qilsa, bu to'g'ridan-to'g'ri foyda
# 3) sig'im: K = 2000 da tejam K = 800 dagidan kam - "ko'proq qo'ng'iroq" har doim yaxshi emas
# 4) tasodifiy qo'ng'iroq ~0 - reyting shart; qoida buni allaqachon qilyapti

3. Nega hozir yopish erta. Farq noaniq, lekin yo'nalish modelga qarab: oyiga +4.59, yiliga ~+55 mln so'm, oraliq [-15, +125]. Bu savolni 3 oy emas, ko'proq oy bilan tekshirish arzon: yangi ma'lumot kerak emas, faqat vaqt bo'yicha CV kerak. K = 50 da model 1.48 barobar ustun: agar sig'im qisqarsa, model ahamiyati ortadi.

4. Taklif.

  • Qoida hozirdan ishlashda davom etadi — u bazaviy va zaxira.
  • 2 hafta: 7 oylik vaqt bo'yicha CV. Mezon: model - qoida > 2*SE bo'lsa, davom etamiz, aks holda loyiha yopiladi va hisobotda "qoida yetarli" deb yoziladi.
  • Mezon oldindan, hozir yoziladi, natija kelgach o'zgartirilmaydi.

Boshliqqa javob: "Siz qisman haqsiz: 3 oy ma'lumotda model qoidadan ishonchli yaxshi emas va biz buni yashirmaymiz. Lekin qoidaning o'zi oracle imkoniyatining uchdan birini beradi, va belgini to'g'ri tanlash allaqachon oyiga ~7 mln farq qiladi. Menga ikki hafta bering: mavjud ma'lumotning 7 oyida bitta oldindan yozilgan mezon bilan tekshiraman. Agar model qoidadan ishonchli yaxshi bo'lmasa, loyihani o'zim yopishni taklif qilaman."

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


Xulosa

Bu darsda DS loyihasining hayot siklini bank loyihasi misolida boshidan ko'rdik: istakni qarorga, qarorni DS vazifasiga va muvaffaqiyatni pulga aylantirdik.

Eng muhim uch fikr:

  1. Loyiha qarordan boshlanadi, muvaffaqiyat — biznes metrikasi bilan. 1-misolda tahlil birligi (mijoz, as-of oy), sig'im (oyiga 200 qo'ng'iroq) va xarajat matritsasi aniqlandi. Oracle oyiga 78.57 mln so'm tejashi mumkin, oddiy qoida 24.26, tasodifiy tanlov esa deyarli nol. 2-misolda AUC va tejam ikki marta turlicha gapirdi. p * zarar tartibi AUC ni 0.8482 dan 0.8314 ga tushirdi, tejamni esa 26.3 dan 28.8 ga oshirdi. Top-K dan tashqaridagi o'zgarish AUC ni 0.9536 ga ko'tardi, tejam esa aynan o'zgarmadi. AUC farqlari "sezilarli" chiqdi, tejam farqlari esa yo'q.

  2. Model qiymati — eng yaxshi qoidaga nisbatan, noaniqligi bilan. 3-misolda 3 oylik val da bitta ustunli qoida modeldan sezilarli yomon emas (-4.59, SE 2.93): model yiliga ~+55 mln so'm qo'shadi, lekin oraliq [-15, +125]. Prototip qarori halol yozildi: qoida zaxira sifatida qoladi, model ko'proq oyda tekshiriladi. Sig'im javobni o'zgartiradi: K = 50 da model 1.48 barobar ustun, K = 2000 da esa ortiqcha qo'ng'iroqlar tejamni kamaytiradi.

  3. Xatarlar loyiha boshida o'lchanadi. 4-misolda belgi ta'rifi eng katta farqni berdi: y30 larning 81.3% i o'zi to'laydi, y30 da o'qitilgan model oyiga 7.00 mln so'm (SE 0.81) kam tejaydi va y60 yetti oyning yettisida yutdi. Belgi yetilishi o'quv oylarini 2 oyga cheklaydi, tarix hajmi esa bu vazifada cheklov emas. Qaror halqasi uchun nazorat guruhi, faraziy parametrlar uchun esa pilot reja loyiha hujjatiga yozildi.

Keyingi darsda To'liq loyiha: muammo va ma'lumot: loyihaning birinchi amaliy qismi. To'rtta jadvalni sqlite ga yig'ib, belgilarni SQL bilan quramiz. Vaqt nuqtasi (as-of) qoidasini va sizishni "snapshot" testi bilan o'lchab ushlaymiz. Qarorga ta'sir qiladigan EDA, sifat tekshiruvlari, oylar bo'yicha train/val/test bo'lish va ma'lumot kartasi (datasheet) ham shu darsda.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
29.1-dars: Data Science loyihasining hayot sikli — IlmHamroh