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

29.2-dars: To'liq loyiha: muammo va ma'lumot

29-QISM — LOYIHALAR VA KARYERA · 2-dars


1. Kirish va motivatsiya

Oldingi darsda loyihaning ramkasini qurdik. Qaror — oyiga 200 ta profilaktik qo'ng'iroq. Belgi — 60 kun ichida 60+ kunlik kechikish (y60). Biznes metrikasi — tejalgan zarar minus qo'ng'iroq xarajati. Bazaviy — "limitdan foydalanish ulushi eng yuqori" qoidasi. Loyiha hujjatiga "as-of qoidasi: faqat sanasi <= as-of bo'lgan yozuvlar" degan qator ham yozildi. Bu darsda o'sha qatorni amalga oshiramiz va shu qator eng oson buziladigan qoida ekanini ko'ramiz.

Real vaziyat. Bank tahlilchisi kechikish modeli uchun belgilarni SQL bilan yozdi: "mijozning oxirgi 6 hisobvarag'i bo'yicha kechikishlar soni va to'lov ulushi". So'rov mantiqan to'g'ri ko'rinadi. Validatsiyada AUC 0.97 chiqdi va jamoa buni bayram qildi. Ishlab chiqarishning birinchi oyida model ro'yxati deyarli tasodifiy bo'lib chiqdi. Sabab: "oxirgi 6 hisobvaraq" ga joriy hisobvaraq ham kirgan edi. Uning to'lovi esa as-of dan keyin, keyingi oyda qilinadi. Tarixiy omborda bu to'lov allaqachon bor edi, ishlab chiqarishda esa hali yo'q. Model o'qitishda kelajakni ko'rgan, ishlab chiqarishda esa bu ma'lumot unga yetib kelmagan. 2-misolda aynan shu xatoni quramiz, uning narxini o'lchaymiz va uni avtomatik ushlaydigan uchta tekshiruvni yozamiz.

Bu darsda loyihaning ma'lumot qismini to'liq qilamiz: manbalarni yig'ish, SQL bilan "nuqtaviy to'g'ri" belgilar, sizishni o'lchash va ushlash, sifat tekshiruvlari, qarorga yo'naltirilgan EDA, vaqt bo'yicha bo'lish va ma'lumot kartasi.

Bu darsda:

  • Ma'lumot inventari: jadvallar, kalitlar, hodisa vaqti
  • Vaqt nuqtasi (as-of) va nuqtaviy to'g'ri belgilar
  • SQL bilan belgilar: CTE, parametr, indeks, pandas bilan tenglik testi
  • Sizish: kelajak to'lovi, yetilmagan belgi; snapshot testi, sana auditi, "juda kuchli belgi" signali
  • Sifat tekshiruvlari: oldingi eksport bilan solishtirish, karantin, partiya darajasida tuzatish
  • Qarorga yo'naltirilgan EDA
  • Vaqt bo'yicha bo'lish: o'quv, bo'shliq, val, test; belgi yetilishi
  • Ma'lumot kartasi (datasheet)

ℹ Misollar real sqlite3/pandas/sklearn bilan (Python 3.14). Ombor jarayon ichida quriladi: fayl tempfile papkasida yoki :memory: da, ulanish contextlib.closing bilan. Generator 29.1 dagi bilan aynan bir xil (urug' 29).


2. Nazariya — chuqur tushuntirish

2.1. Ma'lumot inventari

Loyihaning birinchi ma'lumot artefakti — model emas, balki inventar: qanday jadvallar bor, ularning kaliti nima, har qator qachon sodir bo'lgan va omborga qachon yozilgan.

text
mijozlar (6000)              hisobvaraqlar (~111 ming)        tolovlar (~101 ming)
  mijoz_id  PK                 mijoz_id, oy  PK                  mijoz_id, hisob_oy  PK
  yosh, hudud, daromad         balans                             tolov_oy   <- HODISA VAQTI
  limit, avto_tolov            min_tolov                          tolov_kuni
  maosh_loyihasi               (oy oxirida yaratiladi)            summa
  ochilgan_oy                                                     (keyingi oyda keladi!)
        |                            |                                  |
        +-------- mijoz_id ----------+-------- (mijoz_id, oy) ----------+
        |
tranzaksiyalar (~587 ming)
  mijoz_id, oy, kun, turi (xarid / naqd / onlayn), summa
Savol Nega muhim
Kalit nima, u takrorlanmaydimi? JOIN lar qatorlarni ko'paytirib yubormasligi uchun
Hodisa vaqti qaysi ustun? As-of filtri aynan shu ustun bo'yicha
Qator keyin o'zgaradimi? "Hozirgi holat" jadvali (masalan, limit) o'tmishni buzadi
Qancha kechikib yoziladi? Ishlab chiqarishda as-of da hali yo'q ma'lumot
Kim egasi, kirish huquqi bormi? Ma'lumot mavjudligi xatari (29.1)

"Hozirgi holat" tuzog'i. mijozlar jadvalidagi limit — bugungi limit. Agar bank mijozning limitini kechikishdan keyin qisqartirgan bo'lsa, eski as-of lar uchun ham "qisqargan limit" ishlatiladi. Belgi kelajakdan xabar beradi. To'g'ri yechim — o'zgarishlar tarixini saqlash (SCD 2-tur: amal_boshi, amal_oxiri ustunlari) va as-of dagi qiymatni olish. Bizning generatorda limit o'zgarmaydi. Real loyihada esa bu savolni har bir "statik" ustun uchun berish kerak.

2.2. Vaqt nuqtasi (as-of) va nuqtaviy to'g'ri belgilar

text
                as-of t (t-oy oxiri)
                      |
 ...  t-2   t-1       |    t+1        t+2
------+-----+---------+-----+----------+------------> vaqt
      |     |  t-hisobvaraq yaratildi   |
      |     |         |     | t ning to'lovi (muddat: t+1 ning 25-kuni)
      |     |  t-1 ning to'lovi keldi   |     t+1 ning to'lovi
      |     |  (t-oyda)                 |     (t+2 da)
      |                                  |
  BELGILAR (X): faqat t gacha             NATIJA (y60): t va t+1 hisobvaraqlari
  - balans/limit: t gacha                 ikkalasi ham to'lanmagan
  - to'lov belgilari: muddati o'tgan      -> (t+2)-oy oxirida MA'LUM bo'ladi
    hisobvaraqlar, ya'ni t-1 gacha
  - tranzaksiyalar: t-2 dan t gacha

Nuqtaviy to'g'ri (point-in-time correct) belgi — as-of paytida ombor qanday bo'lsa, o'sha holatdan hisoblanadigan belgi. Tekshiruv savoli oddiy: "t-oy oxirida, ishlab chiqarishda, bu qiymatni aynan shunday hisoblay olarmidim?"

Bizning loyihadagi uchta nozik joy:

  1. Joriy hisobvaraq yaratilgan, to'lovi esa yo'q. t-oy hisobvarag'ining balansi ma'lum, to'lovi hali kelmagan. Shuning uchun to'lov belgilari faqat t-1 gacha bo'lgan hisobvaraqlardan olinadi.
  2. To'lov hisobvaraq oyi bilan saqlanadi. tolovlar.hisob_oy = t qatorining haqiqiy sanasi tolov_oy = t+1. Filtr hisob_oy bo'yicha emas, hodisa vaqti (tolov_oy <= t) bo'yicha qo'yiladi.
  3. Natija (belgi) alohida so'rov. Belgilar va natija bitta so'rovda aralashmaydi: belgilar "t gacha", natija "t dan keyin".

2.3. SQL bilan belgilar

Belgilarni SQL bilan yozishning afzalliklari: ma'lumot omborda qoladi, so'rovni DBA yoki boshqa jamoa o'qiy oladi, ishlab chiqarishda aynan shu so'rov ishga tushadi. Tuzilish:

sql
WITH nomzod AS (...),          -- kim baholanadi: as-of t da faol va kechikmagan
     muddati_otgan AS (...),   -- hisobvaraq + to'lov, faqat tolov_oy <= :t
     tolov_b AS (...),         -- kechikish_6, tolov_nisbati_3, tolov_kuni_3
     balans_b AS (...),        -- util, util_3, util_ozg
     tr_b AS (...)             -- naqd_soni_3, naqd_summa_3, xarid_soni_3
SELECT ... FROM nomzod JOIN ... LEFT JOIN tr_b ...   -- tranzaksiyasi yo'q -> 0

Qoidalar:

  • Parametr (:t) orqali, f-string bilan emas: SQL injeksiyasi va keshlash muammolarining oldini oladi.
  • LEFT JOIN + COALESCE: tranzaksiyasi yo'q mijoz yo'qolib qolmasligi kerak. JOIN uni jim o'chiradi.
  • Butun sonlarni bo'lish: SQLite da 3 / 2 = 1. Kasr natija uchun ustunlardan biri REAL bo'lishi kerak.
  • Zaxira so'z: limit ustun nomi SQL kalit so'zi, shuning uchun uni qo'shtirnoq ichida yozish kerak: m."limit".
  • Indekslar (mijoz_id, oy) va (mijoz_id, hisob_oy) bo'yicha. Ularsiz har JOIN butun jadvalni ko'rib chiqadi.
  • Tenglik testi: SQL natijasi mustaqil yozilgan pandas hisobi bilan bir xil bo'lishi kerak (1-misolda eng katta farq 8.88e-16). Ikki mustaqil amalga oshirish bir-birini tekshiradi.

2.4. Sizish: turlari va ushlash

text
LOYIHADAGI SIZISH TURLARI:
  1. KELAJAK HODISASI   "oxirgi 6 hisobvaraq" joriy hisobvaraq to'lovini ham oladi
                        (to'lov t+1 da, u y60 ning bir qismi!)
  2. YETILMAGAN BELGI   o'quvga as-of T-1 qo'shildi, uning natijasi hali to'liq emas
                        -> y60 = 0 ko'rinadi -> bashorat tizimli pasayadi
  3. HOZIRGI HOLAT      o'zgaruvchan atributning bugungi qiymati (limit, hudud, status)
  4. NATIJADAN HOSIL    "undiruv bo'limiga o'tkazilgan" bayrog'i - kechikishning natijasi

USHLASH (hammasi avtomatik, CI da):
  a) SNAPSHOT TESTI  as-of t dagi ombor nusxasidan va to'liq ombordan hisoblangan
                     belgilar BIR XIL bo'lishi shart (halolda 0 farq)
  b) SANA AUDITI     so'rov o'qigan har manba qatorining hodisa vaqti <= as-of
  c) KUCHLI BELGI    bitta belgining o'zi AUC > 0.9 -> qo'lda tekshiruv
                     (bu vazifada eng kuchli halol belgi ~0.8)
  d) ISHLAB CHIQARISH SIMULYATSIYASI  val oylarini snapshot dan qayta hisoblab baholash

Snapshot testi — eng kuchli himoya. U belgining ma'nosini tushunishni talab qilmaydi. Omborni as-of paytiga "orqaga o'rash" va natijani solishtirish kifoya. 2-misolda sizuvchi so'rovda u 3 ta ustunda 12286 ta farqli katak topdi. Halol so'rovda farq 0.

17.8-darsda sizishning umumiy nazariyasini ko'rgan edik. Bu yerdagi farq shundaki, sizish SQL ichida yashiringan va kod ko'rib chiqishda (code review) oson o'tib ketadi. Shuning uchun uni o'qish bilan emas, o'lchash bilan ushlash kerak.

2.5. Sifat tekshiruvlari

27.3-darsdagi validatsiya g'oyasi bu yerda eksport darajasida qo'llanadi. Har yangi eksport oldingi (tasdiqlangan) eksport bilan solishtiriladi. Mutlaq "nol" talab qilinmaydi: 1-misolda toza manbada ham 52 ta tabiiy "to'liq takror" tranzaksiya bor. Bir mijoz bir kunda bir xil summaga ikki marta xarid qilishi mumkin.

Tekshiruv Nimani ushlaydi Harakat
Kalit takrori ikki marta yuklangan qator partiyani qayta yuklash
NULL / bo'sh tizimdan kelmagan maydon imputatsiya + bayroq yoki so'rov
Diapazon (limit <= 0) nol limit, manfiy summa segmentni chiqarish yoki tuzatish
Birlik (to'lov > 10 * balans) so'm va mln so'm aralashgan birlikni qaytarish
Referensial yaxlitlik yetim to'lov (mijozi yo'q) karantin (o'chirilmaydi!)
Oylik hajm (robust z, 8.6) takroriy partiya, tushib qolgan oy partiya darajasida tuzatish

Partiya darajasida tuzatish. Takroriy partiyani drop_duplicates() bilan "tozalash" xato: u tabiiy takrorlarni ham o'chiradi. To'g'ri yo'l — nuqsonli partiyani (2025-03) aniqlash va faqat uni bitta nusxaga qaytarish (3-misol).

2.6. Qarorga yo'naltirilgan EDA

8-qismdagi EDA to'liq tadqiqot edi. Loyiha ichidagi EDA esa qarorga ta'sir qiladigan savollar bilan cheklanadi va test oylariga qaramaydi:

Savol Qaysi qarorga ta'sir qiladi
Natija ulushi oylar bo'yicha barqarormi? kalibrlash, monitoring chegarasi, bo'lish
Kuchli belgi monotonmi, sakrash bormi? chiziqli model yetadimi
Qaysi belgi "boshqa" turdagi hodisani ushlaydi? qoida bazaviysi, belgi ta'rifi
Kichik segmentlar bormi? segment bo'yicha xato tahlili (29.3)
Hududiy farqlar signalmi, shovqinmi? adolatlilik tekshiruvi (29.11)

Har EDA bandidan keyin "bu qaysi qarorni o'zgartiradi?" savolini bering. Javob "hech qaysini" bo'lsa, u hisobotga kirmaydi.

2.7. Vaqt bo'yicha bo'lish

text
as-of:   7 ........ 14 | 15 | 16 .. 18 | 19 | 20 .. 22 | 23-24
         O'QUV (8 oy)  |BO'S|  VAL (3)  |BO'S|  TEST (3) | belgi yetilmagan
                                                         (as-of emas)
NEGA BO'SHLIQ: as-of 14 ning natijasi 16-oy oxirida ma'lum -> 16-oyda o'qitilgan
  model uchun eng oxirgi belgili oy 14. Val ning birinchi oyi 16 -> o'quv <= 14.
  Test 20 dan -> yakuniy model <= 18 da o'qitiladi 29.3-bob. 15 va 19 - bo'shliq.

NEGA TASODIFIY EMAS:
  1. ishlab chiqarish = o'tmishda o'qib, kelajakni bashorat qilish -> shuni simulyatsiya
  2. tasodifiy bo'lishda o'quvda val dan KEYINGI oylar bor (32.6% qator)
  3. drift 27.12-bob faqat vaqt bo'yicha bo'lishda ko'rinadi
  4. belgi yetilishi va bo'shliqni faqat vaqt o'qida to'g'ri qo'yish mumkin

TEST QOIDASI: test oylari (20-22) belgilari 29.3 da BIR MARTA ochiladi.
  Hatto ulushiga ham hozir qaralmaydi (1-bo'lim jadvalida "?").

Mijoz bo'yicha bo'lish (GroupKFold, 18.2) boshqa savolga javob beradi: "yangi, hech ko'rilmagan mijozlarda qanday ishlaydi?". Bizning qarorimiz esa asosan o'sha mijozlarni kelajakda baholaydi, shuning uchun asosiy o'q — vaqt.

2.8. Ma'lumot kartasi (datasheet)

Ma'lumot kartasi — ma'lumot to'plamining "pasporti". Unda nima bor, qanday yig'ilgan, qaysi maqsadga yaroqli va qaysiga yaroqsiz ekani yoziladi. Model kartasi 29.3-bob modelni tasvirlaydi, ma'lumot kartasi esa ma'lumotni. Kartadagi raqamlar kod bilan to'ldiriladi (4-misol), qo'lda ko'chirilmaydi.

markdown
# Ma'lumot kartasi: kredit karta kechikishi (v1)

## Tarkib
- Manbalar: mijozlar, hisobvaraqlar, tolovlar, tranzaksiyalar (bank ombori, kunlik eksport)
- Davr: 2024-01 - 2025-12; as-of oylar 2024-07 - 2025-10
- Tahlil birligi: (mijoz, as-of oy); qatorlar: 68532; noyob mijozlar: 5463
- Nomzodlar: faol, kamida bitta hisobvarag'i bor, oxirgi hisobvarag'i to'langan

## Belgilar va natija
- 15 belgi, hammasi as-of gacha; snapshot testi: 0 farq
- Natija y60: t va t+1 hisobvaraqlari bo'yicha minimal to'lov qilinmagan
- Natija 2 oyda yetiladi; o'quvdagi ulush 0.0209

## Yig'ish va sifat
- Sifat tekshiruvlari: kalit, NULL, diapazon, birlik, yaxlitlik, oylik hajm
- Ma'lum nuqsonlar va tuzatishlar jurnali: (versiya bo'yicha)

## Bo'lish
- O'quv 7-14, val 16-18, test 20-22; bo'shliqlar 15 va 19; test bir marta ochiladi

## Maqsad va cheklovlar
- Yaroqli: profilaktik qo'ng'iroq ro'yxati uchun kechikish xavfini baholash
- Yaroqsiz: kredit berish qarori (boshqa populyatsiya), shaxsiy baho sifatida oshkor qilish
- Hudud va yosh - sezgir bo'lishi mumkin; segmentlar bo'yicha tahlil majburiy (29.11)
- Zarar parametri faraziy (0.45 * balans)

## Egasi va yangilanish
- Egasi: DS jamoasi + kredit risk; yangilanish: oylik; versiya: xesh bilan (27.5)

2.9. Tuzoqlar

Asosiy tuzoqlar: "oxirgi N hisobvaraq" oynasiga joriy davrni qo'shish; to'lovni hodisa vaqti o'rniga hisob oyi bo'yicha filtrlash; "hozirgi holat" jadvalidagi o'zgaruvchan atributlarni o'tmishga qo'llash; belgilar va natijani bitta so'rovda aralashtirish; JOIN bilan faolligi yo'q mijozlarni jim yo'qotish; SQL natijasini mustaqil hisob bilan solishtirmaslik; takroriy partiyani drop_duplicates() bilan "tozalash"; nuqsonli qatorlarni karantin o'rniga o'chirish; EDA da test oylariga qarash; tasodifiy train_test_split; bo'shliqsiz bo'lish va yetilmagan belgini 0 deb olish; ma'lumot kartasini qo'lda to'ldirish.


3. Tez ma'lumotnoma

python
import contextlib
import sqlite3

import pandas as pd

with contextlib.closing(sqlite3.connect(":memory:")) as con:
    for nom, df in bank.items():
        df.to_sql(nom, con, index=False)
    con.execute("CREATE INDEX ix_p ON tolovlar(mijoz_id, hisob_oy)")
    X = pd.read_sql(BELGI_SQL, con, params={"t": 16})        # faqat t gacha
    y = pd.read_sql(NATIJA_SQL, con, params={"t": 16})       # alohida: t dan keyin

# halol to'lov sharti: hodisa vaqti bo'yicha
# LEFT JOIN tolovlar p ON ... AND p.tolov_oy <= :t   WHERE h.oy BETWEEN :t - 6 AND :t - 1

# snapshot testi
snapshot(con, t)                                             # tolov_oy <= t, oy <= t ...
assert belgilar(con, t).equals(belgilar(con, t, p="snap_"))

# vaqt bo'yicha bo'lish
oquv, val, test = df[df.oy <= 14], df[df.oy.between(16, 18)], df[df.oy.between(20, 22)]

Ma'lumot bosqichi nazorat ro'yxati

Qadam Artefakt Tekshiruv
Inventar jadvallar, kalitlar, hodisa vaqti kalit takrori, yaxlitlik
Belgilar BELGI_SQL pandas bilan tenglik, snapshot testi
Natija NATIJA_SQL yetilish sanasi
Sifat tekshiruvlar jadvali oldingi eksport bilan solishtirish
EDA 5-6 ta qarorga oid savol test oylarisiz
Bo'lish o'quv / bo'shliq / val / bo'shliq / test test yopiq
Hujjat ma'lumot kartasi raqamlar koddan

Ma'lumot qismi xulosasi

inventar: kalit + hodisa vaqti + "hozirgi holat" ustunlari
belgilar: as-of gacha (to'lov: tolov_oy <= t, hisobvaraq <= t-1); natija - alohida
sizish: snapshot testi + sana auditi + kuchli belgi signali + ishlab chiqarish simulyatsiyasi
sifat: oldingi eksportga nisbatan; karantin; partiya darajasida tuzatish
bo'lish: vaqt bo'yicha, bo'shliq = belgi yetilishi; test bir marta

4. Batafsil misollar

Misollar real sqlite3/pandas/sklearn bilan (Python 3.14). Har misol mustaqil ishlaydi. Generator (yarat_bank) va pandas belgilar jadvali (belgilar_jadvali) 29.1 dagi bilan aynan bir xil. 1-misolda SQL va pandas natijasi bir xil ekani isbotlanadi, shuning uchun keyingi darslar tezroq pandas variantidan foydalanadi.

Misol 1 — Manbalarni sqlite ga yig'ish, yaxlitlik tekshiruvi va SQL bilan as-of belgilar

python
"""Manbalarni sqlite ga yig'ish, yaxlitlik tekshiruvi va SQL bilan as-of belgilar."""

import contextlib
import pathlib
import sqlite3
import tempfile

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

BELGI_SQL = """
WITH nomzod AS (            -- as-of t: faol, kamida bitta hisobvaraq, oxirgisi to'langan
    SELECT m.mijoz_id, m.yosh, m.daromad, m."limit" AS lim, m.avto_tolov,
           m.maosh_loyihasi, :t - m.ochilgan_oy AS staj
    FROM mijozlar m
    JOIN hisobvaraqlar h0 ON h0.mijoz_id = m.mijoz_id AND h0.oy = :t
    JOIN hisobvaraqlar h1 ON h1.mijoz_id = m.mijoz_id AND h1.oy = :t - 1
    LEFT JOIN tolovlar p1 ON p1.mijoz_id = m.mijoz_id AND p1.hisob_oy = :t - 1
                         AND p1.tolov_oy <= :t
    WHERE m.ochilgan_oy <= :t - 1
      AND COALESCE(p1.summa, 0) >= h1.min_tolov - 1e-9
),
muddati_otgan AS (          -- to'lov muddati as-of gacha o'tgan hisobvaraqlar
    SELECT h.mijoz_id, h.oy, h.balans, COALESCE(p.summa, 0) AS tolangan,
           p.tolov_kuni,
           CASE WHEN COALESCE(p.summa, 0) < h.min_tolov - 1e-9 THEN 1 ELSE 0 END
               AS kechikdi
    FROM hisobvaraqlar h
    LEFT JOIN tolovlar p ON p.mijoz_id = h.mijoz_id AND p.hisob_oy = h.oy
                        AND p.tolov_oy <= :t
    WHERE h.oy BETWEEN :t - 6 AND :t - 1
),
tolov_b AS (
    SELECT mijoz_id, SUM(kechikdi) AS kechikish_6,
           AVG(CASE WHEN oy >= :t - 3 THEN tolangan / balans END) AS tolov_nisbati_3,
           AVG(CASE WHEN oy >= :t - 3 THEN tolov_kuni END) AS tolov_kuni_3
    FROM muddati_otgan GROUP BY mijoz_id
),
balans_b AS (
    SELECT mijoz_id,
           MAX(CASE WHEN oy = :t THEN balans END) AS balans,
           MAX(CASE WHEN oy = :t - 3 THEN balans END) AS balans_3oy_oldin,
           AVG(CASE WHEN oy >= :t - 2 THEN balans END) AS ort_balans_3
    FROM hisobvaraqlar WHERE oy BETWEEN :t - 3 AND :t GROUP BY mijoz_id
),
tr_b AS (
    SELECT mijoz_id, SUM(turi = 'naqd') AS naqd_soni_3,
           SUM(CASE WHEN turi = 'naqd' THEN summa ELSE 0 END) AS naqd_summa_3,
           SUM(turi = 'xarid') AS xarid_soni_3
    FROM tranzaksiyalar WHERE oy BETWEEN :t - 2 AND :t GROUP BY mijoz_id
)
SELECT n.mijoz_id, :t AS oy, n.yosh, n.daromad, n.lim AS "limit", n.avto_tolov,
       n.maosh_loyihasi, n.staj,
       b.balans / n.lim AS util,
       b.ort_balans_3 / n.lim AS util_3,
       (b.balans - COALESCE(b.balans_3oy_oldin, b.balans)) / n.lim AS util_ozg,
       t.kechikish_6,
       COALESCE(t.tolov_nisbati_3, 0) AS tolov_nisbati_3,
       COALESCE(t.tolov_kuni_3, 25) AS tolov_kuni_3,
       COALESCE(r.naqd_soni_3, 0) AS naqd_soni_3,
       COALESCE(r.naqd_summa_3, 0) AS naqd_summa_3,
       COALESCE(r.xarid_soni_3, 0) AS xarid_soni_3,
       b.balans
FROM nomzod n
JOIN balans_b b USING (mijoz_id)
JOIN tolov_b t USING (mijoz_id)
LEFT JOIN tr_b r USING (mijoz_id)
ORDER BY n.mijoz_id
"""

NATIJA_SQL = """
SELECT h.mijoz_id,
       MAX(CASE WHEN COALESCE(p.summa, 0) < h.min_tolov - 1e-9 THEN 1 ELSE 0 END) AS y30,
       MIN(CASE WHEN COALESCE(p.summa, 0) < h.min_tolov - 1e-9 THEN 1 ELSE 0 END) AS y60
FROM hisobvaraqlar h
LEFT JOIN tolovlar p ON p.mijoz_id = h.mijoz_id AND p.hisob_oy = h.oy
WHERE h.oy IN (:t, :t + 1)
GROUP BY h.mijoz_id HAVING COUNT(*) = 2
"""


def yukla(con, bank):
    for nom, df in bank.items():
        df.to_sql(nom, con, index=False)
    con.executescript("""
        CREATE UNIQUE INDEX ix_m ON mijozlar(mijoz_id);
        CREATE UNIQUE INDEX ix_h ON hisobvaraqlar(mijoz_id, oy);
        CREATE UNIQUE INDEX ix_p ON tolovlar(mijoz_id, hisob_oy);
        CREATE INDEX ix_t ON tranzaksiyalar(oy, mijoz_id);
    """)


def main() -> None:
    bank = yarat_bank()
    with tempfile.TemporaryDirectory() as papka:
        yol = pathlib.Path(papka) / "bank.db"
        with contextlib.closing(sqlite3.connect(yol)) as con:
            yukla(con, bank)
            print("=== 1. Ombor: jadvallar va hajm ===")
            for nom in ["mijozlar", "hisobvaraqlar", "tolovlar", "tranzaksiyalar"]:
                n = con.execute(f"SELECT COUNT(*) FROM {nom}").fetchone()[0]
                print(f"  {nom:<15} {n:>8} qator")
            print(f"  fayl: {yol.name} (vaqtinchalik papkada), indekslar: 4")

            print("\n=== 2. Yaxlitlik tekshiruvlari (SQL) ===")
            tekshiruvlar = {
                "takror hisobvaraq (mijoz, oy)":
                    "SELECT COUNT(*) FROM (SELECT mijoz_id, oy FROM hisobvaraqlar "
                    "GROUP BY 1, 2 HAVING COUNT(*) > 1)",
                "yetim to'lov (hisobvarag'i yo'q)":
                    "SELECT COUNT(*) FROM tolovlar p LEFT JOIN hisobvaraqlar h "
                    "ON h.mijoz_id = p.mijoz_id AND h.oy = p.hisob_oy "
                    "WHERE h.mijoz_id IS NULL",
                "yetim tranzaksiya (mijozi yo'q)":
                    "SELECT COUNT(*) FROM tranzaksiyalar t LEFT JOIN mijozlar m "
                    "USING (mijoz_id) WHERE m.mijoz_id IS NULL",
                "to'lov hisobvaraqdan oldin":
                    "SELECT COUNT(*) FROM tolovlar WHERE tolov_oy <= hisob_oy",
                "ochilishdan oldingi hisobvaraq":
                    "SELECT COUNT(*) FROM hisobvaraqlar h JOIN mijozlar m "
                    "USING (mijoz_id) WHERE h.oy < m.ochilgan_oy",
            }
            for nom, sql in tekshiruvlar.items():
                n = con.execute(sql).fetchone()[0]
                print(f"  {nom:<34} {n:>5}  {'OK' if n == 0 else 'XATO'}")

            print("\n=== 3. SQL bilan belgilar: as-of t = 16 (2025-04) ===")
            t = 16
            sql_df = pd.read_sql(BELGI_SQL, con, params={"t": t})
            nat = pd.read_sql(NATIJA_SQL, con, params={"t": t})
            sql_df = sql_df.merge(nat, on="mijoz_id", how="left")
            print(f"  nomzodlar: {len(sql_df)}, y30 {sql_df.y30.mean():.3f}, "
                  f"y60 {sql_df.y60.mean():.3f}")
            print(sql_df[["mijoz_id", "util", "kechikish_6", "tolov_nisbati_3",
                          "naqd_soni_3", "y60"]].head(4).round(3).to_string(index=False))

        print("\n=== 4. SQL va pandas (belgilar_jadvali) bir xilmi? ===")
        pd_df = belgilar_jadvali(bank, [t]).sort_values("mijoz_id").reset_index(drop=True)
        print(f"  qatorlar: SQL {len(sql_df)}, pandas {len(pd_df)}, mijozlar bir xil: "
              f"{sql_df.mijoz_id.tolist() == pd_df.mijoz_id.tolist()}")
        farqlar = {b: float(np.abs(sql_df[b].to_numpy(float) - pd_df[b].to_numpy(float)).max())
                   for b in BELGILAR + ["balans", "y30", "y60"]}
        eng = max(farqlar, key=farqlar.get)
        print(f"  eng katta farq: {eng} = {farqlar[eng]:.2e}")
        print(f"  hamma belgi mos (< 1e-9): {all(v < 1e-9 for v in farqlar.values())}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ombor: jadvallar va hajm ===
  mijozlar            6000 qator
  hisobvaraqlar     111058 qator
  tolovlar          100780 qator
  tranzaksiyalar    586885 qator
  fayl: bank.db (vaqtinchalik papkada), indekslar: 4

=== 2. Yaxlitlik tekshiruvlari (SQL) ===
  takror hisobvaraq (mijoz, oy)          0  OK
  yetim to'lov (hisobvarag'i yo'q)       0  OK
  yetim tranzaksiya (mijozi yo'q)        0  OK
  to'lov hisobvaraqdan oldin             0  OK
  ochilishdan oldingi hisobvaraq         0  OK

=== 3. SQL bilan belgilar: as-of t = 16 (2025-04) ===
  nomzodlar: 4362, y30 0.105, y60 0.021
 mijoz_id  util  kechikish_6  tolov_nisbati_3  naqd_soni_3  y60
        3 0.459            1            0.915            0    0
        4 0.370            0            0.562            2    0
        6 0.594            1            0.688            0    0
        8 0.160            1            0.875            1    0

=== 4. SQL va pandas (belgilar_jadvali) bir xilmi? ===
  qatorlar: SQL 4362, pandas 4362, mijozlar bir xil: True
  eng katta farq: naqd_summa_3 = 8.88e-16
  hamma belgi mos (< 1e-9): True

Natija tahlili.

1-bo'lim — ombor: to'rtta jadval, jami 800 mingga yaqin qator. Indekslar kalit ustunlarda qurilgan. Ular noyob (UNIQUE) indeks, shuning uchun takroriy kalit yuklanishning o'zida xato beradi.

2-bo'lim — beshta yaxlitlik tekshiruvi. Hammasi 0: takroriy hisobvaraq yo'q, yetim to'lov va tranzaksiya yo'q. To'lov hisobvaraqdan oldin kelmagan, hisobvaraq karta ochilishidan oldin yaratilmagan. Oxirgi ikki tekshiruv vaqt mantig'ini tekshiradi. Aynan shunday tekshiruvlar keyinchalik as-of qoidasini himoya qiladi: agar to'lov sanasi hisobvaraqdan oldin bo'lsa, tolov_oy <= t filtrining ma'nosi yo'qoladi.

3-bo'lim — SQL bilan as-of 16 (2025-04) uchun belgilar. 4362 ta nomzod, y30 0.105, y60 0.021. Belgilar va natija ikkita alohida so'rovdan olinib, mijoz_id bo'yicha birlashtirildi. Namuna qatorlarda kechikish_6 = 1, lekin y60 = 0 bo'lgan mijozlar ko'p. Bu 29.1 dagi "unutuvchan" mijozlar: ular kechikadi, lekin to'laydi.

4-bo'lim — ikki mustaqil amalga oshirish solishtirildi: SQL so'rov va 29.1 dagi pandas belgilar_jadvali. Qatorlar soni (4362) va mijozlar ro'yxati bir xil. Eng katta farq naqd_summa_3 da 8.88e-16, bu faqat suzuvchi nuqtali qo'shish tartibidan kelgan farq. Hamma belgi 1e-9 aniqlikda mos. Bu test loyihada ikki ish qiladi. Birinchidan, SQL dagi mantiqiy xatoni ushlaydi: JOIN o'rniga LEFT JOIN, oyna chegarasi, COALESCE. Ikkinchidan, keyingi darslarda tezroq pandas variantidan foydalanishga ruxsat beradi, chunki ikkalasi bitta narsa ekani isbotlandi.

Misol 2 — Vaqt nuqtasi va sizish: "oxirgi 6 hisobvaraq" xatosini o'lchash va ushlash

python
"""Vaqt nuqtasi (as-of) va sizish: "oxirgi 6 hisobvaraq" xatosini o'lchash va ushlash."""

import contextlib
import sqlite3

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

# {p} - jadval prefiksi (snapshot uchun), {oyna} va {tolov_sharti} - halol/sizuvchi farqi
SHABLON = """
WITH nomzod AS (
    SELECT m.mijoz_id, m.yosh, m.daromad, m."limit" AS lim, m.avto_tolov,
           m.maosh_loyihasi, :t - m.ochilgan_oy AS staj
    FROM {p}mijozlar m
    JOIN {p}hisobvaraqlar h0 ON h0.mijoz_id = m.mijoz_id AND h0.oy = :t
    JOIN {p}hisobvaraqlar h1 ON h1.mijoz_id = m.mijoz_id AND h1.oy = :t - 1
    LEFT JOIN {p}tolovlar p1 ON p1.mijoz_id = m.mijoz_id AND p1.hisob_oy = :t - 1
                            AND p1.tolov_oy <= :t
    WHERE m.ochilgan_oy <= :t - 1 AND COALESCE(p1.summa, 0) >= h1.min_tolov - 1e-9
),
hisob AS (
    SELECT h.mijoz_id, h.oy, h.balans, COALESCE(p.summa, 0) AS tolangan, p.tolov_kuni,
           CASE WHEN COALESCE(p.summa, 0) < h.min_tolov - 1e-9 THEN 1 ELSE 0 END
               AS kechikdi
    FROM {p}hisobvaraqlar h
    LEFT JOIN {p}tolovlar p ON p.mijoz_id = h.mijoz_id AND p.hisob_oy = h.oy {tolov_sharti}
    WHERE h.oy BETWEEN {oyna}
),
tolov_b AS (
    SELECT mijoz_id, SUM(kechikdi) AS kechikish_6,
           AVG(CASE WHEN oy >= {oxirgi3} THEN tolangan / balans END) AS tolov_nisbati_3,
           AVG(CASE WHEN oy >= {oxirgi3} THEN tolov_kuni END) AS tolov_kuni_3
    FROM hisob GROUP BY mijoz_id
),
balans_b AS (
    SELECT mijoz_id, MAX(CASE WHEN oy = :t THEN balans END) AS balans,
           MAX(CASE WHEN oy = :t - 3 THEN balans END) AS b3,
           AVG(CASE WHEN oy >= :t - 2 THEN balans END) AS ort_b
    FROM {p}hisobvaraqlar WHERE oy BETWEEN :t - 3 AND :t GROUP BY mijoz_id
),
tr_b AS (
    SELECT mijoz_id, SUM(turi = 'naqd') AS naqd_soni_3,
           SUM(CASE WHEN turi = 'naqd' THEN summa ELSE 0 END) AS naqd_summa_3,
           SUM(turi = 'xarid') AS xarid_soni_3
    FROM {p}tranzaksiyalar WHERE oy BETWEEN :t - 2 AND :t GROUP BY mijoz_id
)
SELECT n.mijoz_id, :t AS oy, n.yosh, n.daromad, n.lim AS "limit", n.avto_tolov,
       n.maosh_loyihasi, n.staj, b.balans / n.lim AS util, b.ort_b / n.lim AS util_3,
       (b.balans - COALESCE(b.b3, b.balans)) / n.lim AS util_ozg, t.kechikish_6,
       COALESCE(t.tolov_nisbati_3, 0) AS tolov_nisbati_3,
       COALESCE(t.tolov_kuni_3, 25) AS tolov_kuni_3,
       COALESCE(r.naqd_soni_3, 0) AS naqd_soni_3,
       COALESCE(r.naqd_summa_3, 0) AS naqd_summa_3,
       COALESCE(r.xarid_soni_3, 0) AS xarid_soni_3, b.balans
FROM nomzod n JOIN balans_b b USING (mijoz_id) JOIN tolov_b t USING (mijoz_id)
LEFT JOIN tr_b r USING (mijoz_id)
ORDER BY n.mijoz_id
"""
VARIANT = {
    # to'lov muddati as-of gacha o'tgan hisobvaraqlar va faqat as-of gacha kelgan to'lovlar
    "halol": dict(oyna=":t - 6 AND :t - 1", oxirgi3=":t - 3",
                  tolov_sharti="AND p.tolov_oy <= :t"),
    # "oxirgi 6 hisobvaraq" - joriy (t) hisobvaraq ham, uning kelajakdagi to'lovi ham
    "sizuvchi": dict(oyna=":t - 5 AND :t", oxirgi3=":t - 2", tolov_sharti=""),
}
NATIJA_SQL = """
SELECT h.mijoz_id,
       MIN(CASE WHEN COALESCE(p.summa, 0) < h.min_tolov - 1e-9 THEN 1 ELSE 0 END) AS y60
FROM hisobvaraqlar h
LEFT JOIN tolovlar p ON p.mijoz_id = h.mijoz_id AND p.hisob_oy = h.oy
WHERE h.oy IN (:t, :t + 1) GROUP BY h.mijoz_id HAVING COUNT(*) = 2
"""


def belgilar(con, variant, t, p=""):
    sql = SHABLON.format(p=p, **VARIANT[variant])
    return pd.read_sql(sql, con, params={"t": t})


def snapshot(con, t):
    """t-oy oxiridagi ombor holati: faqat shu paytgacha yozilgan qatorlar."""
    con.executescript(f"""
        DROP TABLE IF EXISTS snap_mijozlar; DROP TABLE IF EXISTS snap_hisobvaraqlar;
        DROP TABLE IF EXISTS snap_tolovlar; DROP TABLE IF EXISTS snap_tranzaksiyalar;
        CREATE TEMP TABLE snap_mijozlar AS SELECT * FROM mijozlar WHERE ochilgan_oy <= {t};
        CREATE TEMP TABLE snap_hisobvaraqlar AS SELECT * FROM hisobvaraqlar WHERE oy <= {t};
        CREATE TEMP TABLE snap_tolovlar AS SELECT * FROM tolovlar WHERE tolov_oy <= {t};
        CREATE TEMP TABLE snap_tranzaksiyalar AS SELECT * FROM tranzaksiyalar WHERE oy <= {t};
        CREATE INDEX temp.sx_h ON snap_hisobvaraqlar(mijoz_id, oy);
        CREATE INDEX temp.sx_p ON snap_tolovlar(mijoz_id, hisob_oy);
        CREATE INDEX temp.sx_t ON snap_tranzaksiyalar(oy, mijoz_id);
    """)


def tejam(df, ball):
    df = df.assign(_b=ball, q=SAMARA * 0.45 * df["balans"] * df["y60"] - NARX)
    return df.groupby("oy").apply(lambda g: g.nlargest(K, "_b")["q"].sum(),
                                  include_groups=False).mean()


def main() -> None:
    bank = yarat_bank()
    with contextlib.closing(sqlite3.connect(":memory:")) as con:
        for nom, df in bank.items():
            df.to_sql(nom, con, index=False)
        con.executescript("""
            CREATE INDEX ix_h ON hisobvaraqlar(mijoz_id, oy);
            CREATE INDEX ix_p ON tolovlar(mijoz_id, hisob_oy);
            CREATE INDEX ix_t ON tranzaksiyalar(oy, mijoz_id);""")
        jad = {}
        for v in VARIANT:
            qism = []
            for t in list(range(7, 15)) + [16, 17, 18]:
                f = belgilar(con, v, t)
                qism.append(f.merge(pd.read_sql(NATIJA_SQL, con, params={"t": t}),
                                    on="mijoz_id"))
            jad[v] = pd.concat(qism, ignore_index=True)

        print("=== 1. Bitta belgining AUC si (y60, as-of 7-14) ===")
        print(f"  {'belgi':<16} {'halol':>7} {'sizuvchi':>9}")
        yakka = {}
        for b in ["kechikish_6", "tolov_nisbati_3", "tolov_kuni_3", "util"]:
            r = []
            for v in VARIANT:
                d = jad[v][jad[v].oy <= 14]
                a = roc_auc_score(d.y60, d[b])
                r.append(max(a, 1 - a))              # yo'nalishdan qat'i nazar
            yakka[b] = r
            print(f"  {b:<16} {r[0]:>7.4f} {r[1]:>9.4f}")

        print("\n=== 2. Hisobotdagi natija: o'quv 7-14, val 16-18 ===")
        modellar, hisobot = {}, {}
        for v in VARIANT:
            tr, va = jad[v][jad[v].oy <= 14], jad[v][jad[v].oy >= 16]
            m = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
            modellar[v] = m.fit(tr[BELGILAR], tr.y60)
            p = m.predict_proba(va[BELGILAR])[:, 1]
            hisobot[v] = (roc_auc_score(va.y60, p), tejam(va, p * va.balans))
            print(f"  {v:<9} AUC {hisobot[v][0]:.4f}, tejam {hisobot[v][1]:6.2f} mln so'm/oy")

        print("\n=== 3. Ishlab chiqarish: belgilar oy oxiridagi ombordan (snapshot) ===")
        prod = {v: [] for v in VARIANT}
        for t in [16, 17, 18]:
            snapshot(con, t)
            nat = pd.read_sql(NATIJA_SQL, con, params={"t": t})
            for v in VARIANT:
                prod[v].append(belgilar(con, v, t, p="snap_").merge(nat, on="mijoz_id"))
        haqiqiy = {}
        for v in VARIANT:
            d = pd.concat(prod[v], ignore_index=True)
            p = modellar[v].predict_proba(d[BELGILAR])[:, 1]
            haqiqiy[v] = (roc_auc_score(d.y60, p), tejam(d, p * d.balans))
            print(f"  {v:<9} AUC {haqiqiy[v][0]:.4f} (hisobotda {hisobot[v][0]:.4f}), "
                  f"tejam {haqiqiy[v][1]:6.2f} (hisobotda {hisobot[v][1]:.2f})")

        print("\n=== 4. Sizishni ushlash: uchta avtomatik tekshiruv (t = 16) ===")
        snapshot(con, 16)
        for v in VARIANT:
            toliq = belgilar(con, v, 16).set_index("mijoz_id")
            snap = belgilar(con, v, 16, p="snap_").set_index("mijoz_id")
            farq = ~np.isclose(toliq[BELGILAR], snap.loc[toliq.index, BELGILAR])
            ustunlar = [b for b, f in zip(BELGILAR, farq.any(axis=0)) if f] or "yo'q"
            print(f"  [snapshot testi] {v:<9} farqli kataklar {farq.sum():>5}, "
                  f"ustunlar: {ustunlar}")
        kelajak = con.execute("""
            SELECT COUNT(*) FROM hisobvaraqlar h JOIN tolovlar p
              ON p.mijoz_id = h.mijoz_id AND p.hisob_oy = h.oy
            WHERE h.oy BETWEEN 16 - 5 AND 16 AND p.tolov_oy > 16""").fetchone()[0]
        print(f"  [sana auditi] sizuvchi so'rov as-of dan keyingi {kelajak} ta to'lovni "
              f"o'qiydi (halolda 0 - shart bilan)")
        for b, (h, s) in yakka.items():
            if s > 0.9:
                print(f"  [shubhali kuchli belgi] {b}: bitta o'zi AUC {s:.3f} > 0.9")

    print("\n=== 5. Xulosa (natijadan hisoblangan) ===")
    d_auc = hisobot["sizuvchi"][0] - haqiqiy["sizuvchi"][0]
    print(f"  sizuvchi model: hisobotdagi AUC ishlab chiqarishda {d_auc:.3f} ga tushdi")
    if haqiqiy["sizuvchi"][1] < haqiqiy["halol"][1]:
        print(f"  ishlab chiqarishda halol model {haqiqiy['halol'][1] - haqiqiy['sizuvchi'][1]:.1f}"
              f" mln so'm/oy ko'p tejaydi")
    if abs(hisobot["halol"][0] - haqiqiy["halol"][0]) < 1e-9:
        print("  halol belgilar: hisobot va ishlab chiqarish aynan bir xil")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bitta belgining AUC si (y60, as-of 7-14) ===
  belgi              halol  sizuvchi
  kechikish_6       0.5282    0.9138
  tolov_nisbati_3   0.7967    0.9291
  tolov_kuni_3      0.6517    0.6593
  util              0.7844    0.7844

=== 2. Hisobotdagi natija: o'quv 7-14, val 16-18 ===
  halol     AUC 0.8482, tejam  28.85 mln so'm/oy
  sizuvchi  AUC 0.9692, tejam  60.07 mln so'm/oy

=== 3. Ishlab chiqarish: belgilar oy oxiridagi ombordan (snapshot) ===
  halol     AUC 0.8482 (hisobotda 0.8482), tejam  28.85 (hisobotda 28.85)
  sizuvchi  AUC 0.7991 (hisobotda 0.9692), tejam  22.56 (hisobotda 60.07)

=== 4. Sizishni ushlash: uchta avtomatik tekshiruv (t = 16) ===
  [snapshot testi] halol     farqli kataklar     0, ustunlar: yo'q
  [snapshot testi] sizuvchi  farqli kataklar 12286, ustunlar: ['kechikish_6', 'tolov_nisbati_3', 'tolov_kuni_3']
  [sana auditi] sizuvchi so'rov as-of dan keyingi 4575 ta to'lovni o'qiydi (halolda 0 - shart bilan)
  [shubhali kuchli belgi] kechikish_6: bitta o'zi AUC 0.914 > 0.9
  [shubhali kuchli belgi] tolov_nisbati_3: bitta o'zi AUC 0.929 > 0.9

=== 5. Xulosa (natijadan hisoblangan) ===
  sizuvchi model: hisobotdagi AUC ishlab chiqarishda 0.170 ga tushdi
  ishlab chiqarishda halol model 6.3 mln so'm/oy ko'p tejaydi
  halol belgilar: hisobot va ishlab chiqarish aynan bir xil

Natija tahlili.

Ikki so'rov faqat bitta qatorda farq qiladi. Halol so'rov WHERE h.oy BETWEEN :t - 6 AND :t - 1 va AND p.tolov_oy <= :t shartlarini ishlatadi. Sizuvchi so'rov esa BETWEEN :t - 5 AND :t ni ishlatadi va to'lov sharti yo'q. Kod ko'rib chiqishda ikkinchisi ham "oxirgi 6 hisobvaraq" kabi tabiiy ko'rinadi.

1-bo'lim — bitta belgining AUC si. Halol kechikish_6 deyarli foydasiz (0.5282), chunki o'tmishdagi kechikishlar asosan "unutuvchan"larni ko'rsatadi 29.1-bob. Sizuvchi variantda esa u 0.9138 ga sakraydi: joriy hisobvaraqning to'lovi y60 ning birinchi yarmi. tolov_nisbati_3 ham 0.7967 dan 0.9291 ga ko'tariladi. Sizish tegmagan util ikkalasida bir xil (0.7844). Bu ham foydali diagnostika: sizish faqat to'lov jadvaliga tegadigan belgilarda ko'rinadi.

2-bo'lim — hisobotdagi natija. Sizuvchi model val da AUC 0.9692 va oyiga 60.07 mln so'm tejam ko'rsatadi. Bu 29.1 dagi oracle ning (78.57) katta qismi. Halol model esa 0.8482 va 28.85 ko'rsatadi. Tajribasiz jamoa birinchisini tanlaydi.

3-bo'lim — ishlab chiqarish simulyatsiyasi. Val oylari uchun belgilar oy oxiridagi ombor nusxasidan (snapshot) qayta hisoblandi. Halol model natijasi o'zgarmadi: AUC va tejam aynan bir xil. Sizuvchi modelning AUC si 0.7991 ga, tejami 22.56 ga tushdi. Bu nafaqat halol modeldan (28.85), balki 29.1 dagi bitta ustunli qoidadan (24.26) ham yomon. Sabab: snapshot da joriy hisobvaraqning to'lovi yo'q, shuning uchun hamma mijozda kechikish_6 bittaga oshadi va tolov_nisbati_3 pasayadi. Model o'qitishda o'rgangan eng kuchli signal ishlab chiqarishda shovqinga aylanadi. Hisobot va haqiqat orasidagi farq: AUC 0.170, tejam oyiga 37.5 mln so'm.

4-bo'lim — uchta avtomatik tekshiruv. Ularning har biri sizishni mustaqil ushladi:

  • Snapshot testi. Halol so'rovda to'liq ombor va snapshot natijasi orasida 0 farq bor, sizuvchida esa aynan uchta to'lov belgisida 12286 ta farqli katak. Test belgilarning ma'nosini bilishni talab qilmaydi va CI ga qo'yiladi.
  • Sana auditi. Sizuvchi so'rov as-of dan keyin kelgan 4575 ta to'lovni o'qiydi.
  • Kuchli belgi signali. Ikki belgining o'zi AUC > 0.9 berdi. Kechikish bashoratida bitta belgi uchun bu juda yuqori, va bu qo'lda tekshirishga sabab bo'ladi.

5-bo'lim — xulosa raqamdan: sizuvchi modelning AUC si ishlab chiqarishda 0.170 ga tushdi, halol model esa oyiga 6.3 mln so'm ko'p tejaydi. Asosiy saboq: val natijasi "juda yaxshi" bo'lsa, bu bayram uchun emas, tekshiruv uchun signal.

Misol 3 — Sifat tekshiruvlari (xom eksportdagi nuqsonlar) va qarorga yo'naltirilgan EDA

python
"""Sifat tekshiruvlari (xom eksportdagi nuqsonlar) va qarorga yo'naltirilgan EDA."""

import contextlib
import sqlite3

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 xom_eksport(bank, seed=7):
    """Bank eksportida uchraydigan nuqsonlar: takroriy partiya, birlik, bo'sh qiymat."""
    rng = np.random.default_rng(seed)
    xom = {k: v.copy() for k, v in bank.items()}
    tr = xom["tranzaksiyalar"]
    xom["tranzaksiyalar"] = pd.concat([tr, tr[tr.oy == 15]], ignore_index=True)
    tl = xom["tolovlar"]
    i = rng.choice(len(tl), 25, replace=False)
    tl.loc[i, "summa"] = tl.loc[i, "summa"] * 1_000_000          # so'mda yozilgan
    yetim = tl.iloc[:15].assign(mijoz_id=np.arange(9001, 9016))
    xom["tolovlar"] = pd.concat([tl, yetim], ignore_index=True)
    mj = xom["mijozlar"]
    mj.loc[rng.choice(len(mj), 12, replace=False), "limit"] = 0.0
    mj["yosh"] = mj["yosh"].astype(float)
    mj.loc[rng.choice(len(mj), 40, replace=False), "yosh"] = np.nan
    return xom


TEKSHIRUVLAR = {
    "mijoz_id takrori": "SELECT COUNT(*) - COUNT(DISTINCT mijoz_id) FROM mijozlar",
    "yosh bo'sh (NULL)": "SELECT COUNT(*) FROM mijozlar WHERE yosh IS NULL",
    "limit <= 0": 'SELECT COUNT(*) FROM mijozlar WHERE "limit" <= 0',
    "yetim to'lov": "SELECT COUNT(*) FROM tolovlar p LEFT JOIN mijozlar m "
                    "USING (mijoz_id) WHERE m.mijoz_id IS NULL",
    "to'lov > 10 * balans": "SELECT COUNT(*) FROM tolovlar p JOIN hisobvaraqlar h "
                            "ON h.mijoz_id = p.mijoz_id AND h.oy = p.hisob_oy "
                            "WHERE p.summa > 10 * h.balans",
    "to'liq takror tranzaksiya": "SELECT COALESCE(SUM(n - 1), 0) FROM (SELECT COUNT(*) AS n "
                                 "FROM tranzaksiyalar GROUP BY mijoz_id, oy, kun, turi, "
                                 "summa HAVING n > 1)",
}


def tekshir(bank):
    with contextlib.closing(sqlite3.connect(":memory:")) as con:
        for nom, df in bank.items():
            df.to_sql(nom, con, index=False)
        natija = {nom: con.execute(sql).fetchone()[0] for nom, sql in TEKSHIRUVLAR.items()}
        hajm = pd.read_sql("SELECT oy, COUNT(*) AS n FROM tranzaksiyalar GROUP BY oy", con)
    med = hajm.n.median()
    mad = (hajm.n - med).abs().median()
    z = 0.6745 * (hajm.n - med) / mad                     # robust z (8.6)
    natija["oylik hajm |z| > 5"] = int((z.abs() > 5).sum())
    return natija, hajm.oy[z.abs() > 5].tolist()


def main() -> None:
    bank = yarat_bank()
    xom = xom_eksport(bank)
    print("=== 1. Sifat tekshiruvlari: oldingi (toza) eksport va yangi eksport ===")
    n_toza, _ = tekshir(bank)
    n_xom, oylar = tekshir(xom)
    print(f"  {'tekshiruv':<28} {'oldingi':>8} {'yangi':>7}  holat")
    for nom, n in n_xom.items():
        print(f"  {nom:<28} {n_toza[nom]:>8} {n:>7}  {'OK' if n <= n_toza[nom] else 'XATO'}")
    print(f"  hajmi g'ayrioddiy oylar: {[oy_nomi(m) for m in oylar]}")

    print("\n=== 2. Nuqsonlar belgilarga qanday o'tadi? (as-of 15-17) ===")
    toza = belgilar_jadvali(bank, [15, 16, 17]).set_index(["mijoz_id", "oy"])
    with np.errstate(divide="ignore", invalid="ignore"):
        buz = belgilar_jadvali(xom, [15, 16, 17]).set_index(["mijoz_id", "oy"])
    umumiy = toza.index.intersection(buz.index)
    print(f"  qatorlar: toza {len(toza)}, xom {len(buz)}")
    for b in ["naqd_soni_3", "xarid_soni_3", "tolov_nisbati_3", "util", "yosh"]:
        a, c = toza.loc[umumiy, b].to_numpy(float), buz.loc[umumiy, b].to_numpy(float)
        farq = ~np.isclose(a, c)
        print(f"  {b:<16} buzilgan {farq.sum():>6} qator ({farq.mean():6.2%}), "
              f"o'rtacha: toza {np.nanmean(a):.3f}, xom {np.nanmean(c[np.isfinite(c)]):.3f}")

    print("\n=== 3. Tuzatish va qayta tekshiruv ===")
    tuz = {k: v.copy() for k, v in xom.items()}
    t = tuz["tranzaksiyalar"]                  # 2025-03 partiyasi ikki marta yuklangan:
    ustun = list(t.columns)                    # har takror guruhning yarmi qoladi
    n = t.assign(_i=1).groupby(ustun)["_i"].transform("size")
    tuz["tranzaksiyalar"] = t[(t.oy != 15) | (t.groupby(ustun).cumcount() < n // 2)]
    tl = tuz["tolovlar"].merge(tuz["hisobvaraqlar"], left_on=["mijoz_id", "hisob_oy"],
                               right_on=["mijoz_id", "oy"], how="left")
    birlik = (tl["summa"] > 10 * tl["balans"]).to_numpy()
    tuz["tolovlar"].loc[birlik, "summa"] = np.round(tuz["tolovlar"].loc[birlik, "summa"] / 1e6, 3)
    karantin = tuz["tolovlar"][~tuz["tolovlar"].mijoz_id.isin(tuz["mijozlar"].mijoz_id)]
    tuz["tolovlar"] = tuz["tolovlar"].drop(karantin.index)
    nol_limit = tuz["mijozlar"].mijoz_id[tuz["mijozlar"]["limit"] <= 0]
    for k in ["mijozlar", "hisobvaraqlar", "tolovlar", "tranzaksiyalar"]:
        tuz[k] = tuz[k][~tuz[k].mijoz_id.isin(nol_limit)]
    print(f"  2025-03 partiyasi bitta nusxaga qaytarildi, {birlik.sum()} to'lov birligi tuzatildi, "
          f"{len(karantin)} yetim karantinga, {len(nol_limit)} mijoz (limit 0) chiqarildi")
    n_tuz, _ = tekshir(tuz)
    qoldi = {k: v for k, v in n_tuz.items() if v > n_toza[k]}
    print(f"  qolgan muammolar: {qoldi} (yosh - modelda imputatsiya + bayroq)")
    t2 = belgilar_jadvali(tuz, [15, 16, 17]).set_index(["mijoz_id", "oy"])
    u = t2.index.intersection(toza.index)
    ustunlar = [b for b in BELGILAR if b != "yosh"]
    mos = np.allclose(t2.loc[u, ustunlar].to_numpy(float), toza.loc[u, ustunlar].to_numpy(float))
    print(f"  tuzatilgan belgilar toza manba bilan mos (yoshdan tashqari): {mos}")

    print("\n=== 4. Qarorga yo'naltirilgan EDA (toza, as-of 7-18; test oylari yopiq) ===")
    df = belgilar_jadvali(bank, range(7, 19))
    oylik = df.groupby("oy")["y60"].mean()
    print(f"  y60 oylar bo'yicha: min {oylik.min():.4f}, max {oylik.max():.4f}, "
          f"o'rtacha {oylik.mean():.4f}")
    df["faqat_y30"] = ((df.y30 == 1) & (df.y60 == 0)).astype(int)
    bins = pd.cut(df.util, [0, 0.2, 0.4, 0.6, 0.85, 1.0], include_lowest=True)
    t = df.groupby(bins, observed=True).agg(n=("y60", "size"), y60=("y60", "mean"))
    print("  util oralig'i bo'yicha y60:")
    for oraliq, n_, y_ in zip(t.index.astype(str), t.n, t.y60):
        print(f"    {oraliq:<14} n={n_:>6}  y60 {y_:.4f}")
    k6 = df.assign(k=df.kechikish_6.clip(upper=2)).groupby("k")[["y60", "faqat_y30"]].mean()
    print("  kechikish_6 (0, 1, 2+) bo'yicha: y60 "
          f"{np.round(k6.y60.to_numpy(), 4).tolist()}, faqat y30 "
          f"{np.round(k6.faqat_y30.to_numpy(), 4).tolist()}")
    yangi = df.staj < 6
    print(f"  yangi mijozlar (staj < 6 oy): ulush {yangi.mean():.3f}, "
          f"y60 {df.y60[yangi].mean():.4f} va eskilar {df.y60[~yangi].mean():.4f}")
    h = df.groupby("hudud")["y60"].mean()
    print(f"  hudud bo'yicha y60: {h.min():.4f} ({h.idxmin()}) dan "
          f"{h.max():.4f} ({h.idxmax()}) gacha")

    print("\n=== 5. Xulosa: EDA dan chiqqan qarorlar ===")
    sakrash = t.y60.iloc[-1] / t.y60.iloc[-2]
    if sakrash > 2 and t.n.iloc[-1] < 100:
        print(f"  util > 0.85 da y60 {sakrash:.1f} barobar yuqori, lekin atigi "
              f"{t.n.iloc[-1]} qator -> alohida belgi shart emas; util monoton va kuchli")
    elif sakrash > 2:
        print(f"  util > 0.85 da y60 {sakrash:.1f} barobar sakraydi -> chiziqsiz model sinaladi")
    if k6.faqat_y30.iloc[-1] > k6.y60.iloc[-1]:
        print("  ko'p kechikkanlar orasida 'o'zi to'laydiganlar' DPD60 dan ko'p -> "
              "kechikish_6 yolg'iz qoida sifatida yaroqsiz")
    if yangi.mean() < 0.1:
        print(f"  yangi mijozlar kam ({yangi.mean():.1%}) -> segment xatosi alohida "
              f"tekshiriladi 29.3-bob")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Sifat tekshiruvlari: oldingi (toza) eksport va yangi eksport ===
  tekshiruv                     oldingi   yangi  holat
  mijoz_id takrori                    0       0  OK
  yosh bo'sh (NULL)                   0      40  XATO
  limit <= 0                          0      12  XATO
  yetim to'lov                        0      15  XATO
  to'lov > 10 * balans                0      25  XATO
  to'liq takror tranzaksiya          52   25487  XATO
  oylik hajm |z| > 5                  0       1  XATO
  hajmi g'ayrioddiy oylar: ['2025-03']

=== 2. Nuqsonlar belgilarga qanday o'tadi? (as-of 15-17) ===
  qatorlar: toza 13095, xom 13096
  naqd_soni_3      buzilgan   2012 qator (15.36%), o'rtacha: toza 0.525, xom 0.697
  xarid_soni_3     buzilgan  12594 qator (96.17%), o'rtacha: toza 10.620, xom 14.190
  tolov_nisbati_3  buzilgan      6 qator ( 0.05%), o'rtacha: toza 0.664, xom 85.000
  util             buzilgan     24 qator ( 0.18%), o'rtacha: toza 0.352, xom 0.352
  yosh             buzilgan     86 qator ( 0.66%), o'rtacha: toza 43.951, xom 43.994

=== 3. Tuzatish va qayta tekshiruv ===
  2025-03 partiyasi bitta nusxaga qaytarildi, 25 to'lov birligi tuzatildi, 15 yetim karantinga, 12 mijoz (limit 0) chiqarildi
  qolgan muammolar: {"yosh bo'sh (NULL)": 39} (yosh - modelda imputatsiya + bayroq)
  tuzatilgan belgilar toza manba bilan mos (yoshdan tashqari): True

=== 4. Qarorga yo'naltirilgan EDA (toza, as-of 7-18; test oylari yopiq) ===
  y60 oylar bo'yicha: min 0.0152, max 0.0259, o'rtacha 0.0195
  util oralig'i bo'yicha y60:
    (-0.001, 0.2]  n=  8776  y60 0.0013
    (0.2, 0.4]     n= 23232  y60 0.0083
    (0.4, 0.6]     n= 14990  y60 0.0306
    (0.6, 0.85]    n=  3874  y60 0.0826
    (0.85, 1.0]    n=    22  y60 0.4545
  kechikish_6 (0, 1, 2+) bo'yicha: y60 [0.0184, 0.0229, 0.0317], faqat y30 [0.0719, 0.1583, 0.1988]
  yangi mijozlar (staj < 6 oy): ulush 0.100, y60 0.0223 va eskilar 0.0192
  hudud bo'yicha y60: 0.0176 (Buxoro) dan 0.0226 (Qashqadaryo) gacha

=== 5. Xulosa: EDA dan chiqqan qarorlar ===
  util > 0.85 da y60 5.5 barobar yuqori, lekin atigi 22 qator -> alohida belgi shart emas; util monoton va kuchli
  ko'p kechikkanlar orasida 'o'zi to'laydiganlar' DPD60 dan ko'p -> kechikish_6 yolg'iz qoida sifatida yaroqsiz
  yangi mijozlar kam (10.0%) -> segment xatosi alohida tekshiriladi (29.3)

Natija tahlili.

1-bo'lim — yangi eksport oldingi (tasdiqlangan) eksport bilan solishtirildi. Beshta turdagi nuqson ushlandi: 40 ta bo'sh yosh, 12 ta nol limit, 15 ta yetim to'lov, 25 ta birlik xatosi (so'mda yozilgan to'lov) va 2025-03 partiyasining ikki marta yuklanishi. Oxirgisini ikki tekshiruv birga ko'rsatdi: to'liq takrorlar 52 dan 25487 ga oshdi va oylik hajm robust z bo'yicha g'ayrioddiy chiqdi. 52 — muhim raqam. Toza manbada ham shuncha tabiiy takror bor, shuning uchun "takror = 0" talabi yolg'on signal berar edi. Mezon mutlaq nol emas, oldingi eksport bilan solishtirish.

2-bo'lim — nuqsonlar belgilarga qanday o'tadi (as-of 15-17). Takroriy partiya xarid_soni_3 ni 96.17% qatorda buzdi: o'rtacha 10.620 dan 14.190 ga oshdi. naqd_soni_3 15.36% qatorda buzildi. Birlik xatosi faqat 6 qatorga tegdi, lekin tolov_nisbati_3 ning o'rtachasini 0.664 dan 85.000 ga ko'tardi. Bitta qator butun statistikani buzishi mumkin. Qiziq yon ta'sir ham bor: xom ma'lumotda qatorlar bittaga ko'p (13096 va 13095). Birlik xatosi bitta kechikkan mijozning to'lovini "katta to'lov" qilib ko'rsatdi va u nomzodga aylandi.

3-bo'lim — tuzatish. 2025-03 partiyasi partiya darajasida bitta nusxaga qaytarildi, shuning uchun 52 ta tabiiy takror saqlanib qoldi. 25 ta to'lov birligi tuzatildi. Yetim to'lovlar o'chirilmadi, karantinga olindi. Nol limitli 12 mijoz chiqarildi, chunki ular uchun util aniqlanmagan. Qayta tekshiruvdan keyin faqat 39 ta bo'sh yosh qoldi (bittasi nol limitli mijoz edi): bu model quvurida imputatsiya va bayroq bilan hal qilinadi. Tuzatilgan eksportdan hisoblangan belgilar toza manba bilan to'liq mos (True). Tuzatish to'g'ri ekani shu bilan isbotlandi.

4-bo'lim — qarorga yo'naltirilgan EDA (faqat as-of 7-18, test oylari yopiq):

  • y60 oylar bo'yicha 0.0152 dan 0.0259 gacha o'zgaradi. Tebranish katta, shuning uchun kalibrlash va monitoring chegarasi bitta oyga qarab qo'yilmaydi.
  • util bo'yicha y60 monoton va keskin o'sadi: 0.0013 dan 0.0826 gacha. 0.85 dan yuqorida y60 0.4545, lekin bu faqat 22 qator.
  • kechikish_6 o'sgan sari "faqat y30" ulushi (0.0719 dan 0.1988 gacha) y60 ga qaraganda (0.0184 dan 0.0317 gacha) ancha tez o'sadi.
  • Yangi mijozlar (staj < 6 oy) 10.0%, ularda y60 biroz yuqori (0.0223 va 0.0192).
  • Hududlar orasida y60 0.0176 (Buxoro) dan 0.0226 (Qashqadaryo) gacha. Ma'lumot sintetik va generatorda hudud ta'siri yo'q, demak bu farq to'liq shovqin. Bu EDA dagi har bir farq signal emasligini eslatadi.

5-bo'lim — EDA dan chiqqan qarorlar. util > 0.85 dagi sakrash kam uchraydi, shuning uchun alohida belgi shart emas: chiziqli model uchun util yetarli, HistGB esa 29.3 da baribir sinaladi. kechikish_6 yolg'iz qoida sifatida yaroqsiz, bu 29.1 dagi "kechikish + util" qoidasining yomon natijasini tushuntiradi. Yangi mijozlar segmenti kichik, shuning uchun model xatosi unda alohida tekshiriladi 29.3-bob.

Misol 4 — Vaqt bo'yicha bo'lish: bo'shliq, orqaga sinov, yetilmagan belgi va ma'lumot kartasi

python
"""Vaqt bo'yicha bo'lish: bo'shliq, orqaga sinov, yetilmagan belgi va ma'lumot kartasi."""

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import GroupShuffleSplit, train_test_split
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

BOLISH = {"o'quv": range(7, 15), "bo'shliq 1": [15], "val": range(16, 19),
          "bo'shliq 2": [19], "test": range(20, 23)}


def baholash(tr, va, ulush=K / 4300):
    """AUC, eng xavfli ulush ichida aniqlik va kalibrlash (o'rtacha p / haqiqiy ulush)."""
    m = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
    p = m.fit(tr[BELGILAR], tr["y60"]).predict_proba(va[BELGILAR])[:, 1]
    top = np.argsort(-p, kind="stable")[: int(ulush * len(va))]
    return (roc_auc_score(va["y60"], p), va["y60"].to_numpy()[top].mean(),
            p.mean() / va["y60"].mean())


def main() -> None:
    df = belgilar_jadvali(yarat_bank())
    print("=== 1. Bo'lish rejasi (as-of oylar; test belgilariga qaralmaydi) ===")
    print(f"  {'qism':<11} {'oylar':<18} {'qator':>6} {'y60':>5} {'ulush':>7}  belgi ma'lum")
    for nom, oylar in BOLISH.items():
        d = df[df.oy.isin(oylar)]
        oraliq = f"{oy_nomi(min(oylar))} - {oy_nomi(max(oylar))}"
        malum = oy_nomi(max(oylar) + 2)
        if nom == "test":
            print(f"  {nom:<11} {oraliq:<18} {len(d):>6} {'?':>5} {'?':>7}  {malum}")
        else:
            print(f"  {nom:<11} {oraliq:<18} {len(d):>6} {d.y60.sum():>5} "
                  f"{d.y60.mean():>7.4f}  {malum}")

    print("\n=== 2. Orqaga sinov: faqat 7-14 ma'lumoti bilan kelajakni baholash ===")
    tarix = df[df.oy <= 14].reset_index(drop=True)
    haqiqat = baholash(tarix, df[df.oy.between(16, 18)])
    sxemalar = {}
    a, b = train_test_split(tarix, test_size=0.2, random_state=0)
    sxemalar["tasodifiy qatorlar"] = baholash(a, b)
    i, j = next(GroupShuffleSplit(1, test_size=0.2, random_state=0)
                .split(tarix, groups=tarix.mijoz_id))
    sxemalar["mijoz bo'yicha"] = baholash(tarix.iloc[i], tarix.iloc[j])
    sxemalar["vaqt: 7-11 -> 13-14"] = baholash(tarix[tarix.oy <= 11], tarix[tarix.oy >= 13])
    print(f"  {'sxema':<22} {'AUC':>7} {'top aniqlik':>12} {'p/haqiqiy':>10}")
    for nom, (auc, prec, kal) in sxemalar.items():
        print(f"  {nom:<22} {auc:>7.4f} {prec:>12.4f} {kal:>10.3f}")
    print(f"  {'HAQIQAT: 7-14 -> 16-18':<22} {haqiqat[0]:>7.4f} {haqiqat[1]:>12.4f} "
          f"{haqiqat[2]:>10.3f}")
    xato = {nom: abs(v[0] - haqiqat[0]) for nom, v in sxemalar.items()}
    print(f"  AUC xatosi: " + ", ".join(f"{k} {v:.4f}" for k, v in xato.items()))
    oq_oylar = np.sort(a.oy.to_numpy())
    keyin = len(oq_oylar) - np.searchsorted(oq_oylar, b.oy.to_numpy() + 1, side="right")
    print(f"  tasodifiy bo'lishda val qatori uchun o'quvdagi 'kelajak' qatorlar "
          f"(belgisi undan keyin yetilgan): o'rtacha {keyin.mean() / len(oq_oylar):.1%}")

    print("\n=== 3. Yetilmagan belgi tuzog'i: T-oyda o'qitishga as-of T-1 ham qo'shilsa ===")
    print("  (as-of T-1 ning t+1 hisobvarag'i hali ma'lum emas -> ombordan y60 = 0 ko'rinadi)")
    natija = {"halol": [], "yetilmagan": []}
    for T in range(12, 19):
        va = df[df.oy == T]
        halol = df[df.oy <= T - 2]
        yet = df[df.oy <= T - 1].copy()
        yet.loc[yet.oy == T - 1, "y60"] = 0
        for nom, tr in [("halol", halol), ("yetilmagan", yet)]:
            natija[nom].append(baholash(tr, va))
    for nom, r in natija.items():
        r = np.array(r)
        kal = r[:, 2]
        print(f"  {nom:<11} AUC o'rtacha {r[:, 0].mean():.4f}, p/haqiqiy o'rtacha "
              f"{kal.mean():.3f} (SE {kal.std(ddof=1) / np.sqrt(len(kal)):.3f})")
    nisbat = np.array(natija["yetilmagan"])[:, 2] / np.array(natija["halol"])[:, 2]
    print(f"  yetilmagan / halol bashorat darajasi: har oyda {nisbat.min():.3f} - "
          f"{nisbat.max():.3f} (7/7 oyda past: {bool((nisbat < 1).all())})")

    print("\n=== 4. Ma'lumot kartasi (datasheet) - raqamlar natijadan ===")
    oq = df[df.oy.isin(BOLISH["o'quv"])]
    karta = [
        "## Ma'lumot kartasi: kredit karta kechikishi (v1)",
        f"- Davr: {oy_nomi(1)} - {oy_nomi(OYLAR)}; as-of oylar: "
        f"{oy_nomi(df.oy.min())} - {oy_nomi(df.oy.max())}",
        f"- Tahlil birligi: (mijoz, as-of oy); qatorlar: {len(df)}; "
        f"mijozlar: {df.mijoz_id.nunique()}",
        f"- Belgilar: {len(BELGILAR)} ta; hammasi as-of gacha (snapshot testi: 0 farq)",
        f"- Natija: y60, o'quvdagi ulush {oq.y60.mean():.4f}; yetilish 2 oy",
        "- Bo'lish: o'quv 7-14, val 16-18, test 20-22; bo'shliqlar 15 va 19",
        f"- Yangi mijozlar (staj < 6): {(df.staj < 6).mean():.1%}",
        "- Cheklovlar: sintetik; faraziy zarar (0.45 * balans); test oylari ochilmagan",
    ]
    for q in karta:
        print("  " + q)

    print("\n=== 5. Xulosa (natijadan hisoblangan) ===")
    for i, metrika in enumerate(["AUC", "top aniqlik", "p/haqiqiy"]):
        eng = min(sxemalar, key=lambda k: abs(sxemalar[k][i] - haqiqat[i]))
        print(f"  {metrika:<12} bo'yicha kelajakka eng yaqin: {eng}")
    if (nisbat < 1).all():
        print(f"  yetilmagan belgi bashoratni tizimli pasaytiradi (~{1 - nisbat.mean():.0%})")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bo'lish rejasi (as-of oylar; test belgilariga qaralmaydi) ===
  qism        oylar               qator   y60   ulush  belgi ma'lum
  o'quv       2024-07 - 2025-02   33358   697  0.0209  2025-04
  bo'shliq 1  2025-03 - 2025-03    4333    66  0.0152  2025-05
  val         2025-04 - 2025-06   13203   230  0.0174  2025-08
  bo'shliq 2  2025-07 - 2025-07    4475   102  0.0228  2025-09
  test        2025-08 - 2025-10   13163     ?       ?  2025-12

=== 2. Orqaga sinov: faqat 7-14 ma'lumoti bilan kelajakni baholash ===
  sxema                      AUC  top aniqlik  p/haqiqiy
  tasodifiy qatorlar      0.8177       0.1484      0.928
  mijoz bo'yicha          0.8306       0.1210      1.037
  vaqt: 7-11 -> 13-14     0.8372       0.1447      0.857
  HAQIQAT: 7-14 -> 16-18  0.8482       0.1156      1.088
  AUC xatosi: tasodifiy qatorlar 0.0305, mijoz bo'yicha 0.0176, vaqt: 7-11 -> 13-14 0.0110
  tasodifiy bo'lishda val qatori uchun o'quvdagi 'kelajak' qatorlar (belgisi undan keyin yetilgan): o'rtacha 32.6%

=== 3. Yetilmagan belgi tuzog'i: T-oyda o'qitishga as-of T-1 ham qo'shilsa ===
  (as-of T-1 ning t+1 hisobvarag'i hali ma'lum emas -> ombordan y60 = 0 ko'rinadi)
  halol       AUC o'rtacha 0.8380, p/haqiqiy o'rtacha 1.026 (SE 0.082)
  yetilmagan  AUC o'rtacha 0.8381, p/haqiqiy o'rtacha 0.893 (SE 0.080)
  yetilmagan / halol bashorat darajasi: har oyda 0.803 - 0.908 (7/7 oyda past: True)

=== 4. Ma'lumot kartasi (datasheet) - raqamlar natijadan ===
  ## Ma'lumot kartasi: kredit karta kechikishi (v1)
  - Davr: 2024-01 - 2025-12; as-of oylar: 2024-07 - 2025-10
  - Tahlil birligi: (mijoz, as-of oy); qatorlar: 68532; mijozlar: 5463
  - Belgilar: 15 ta; hammasi as-of gacha (snapshot testi: 0 farq)
  - Natija: y60, o'quvdagi ulush 0.0209; yetilish 2 oy
  - Bo'lish: o'quv 7-14, val 16-18, test 20-22; bo'shliqlar 15 va 19
  - Yangi mijozlar (staj < 6): 9.4%
  - Cheklovlar: sintetik; faraziy zarar (0.45 * balans); test oylari ochilmagan

=== 5. Xulosa (natijadan hisoblangan) ===
  AUC          bo'yicha kelajakka eng yaqin: vaqt: 7-11 -> 13-14
  top aniqlik  bo'yicha kelajakka eng yaqin: mijoz bo'yicha
  p/haqiqiy    bo'yicha kelajakka eng yaqin: mijoz bo'yicha
  yetilmagan belgi bashoratni tizimli pasaytiradi (~13%)

Natija tahlili.

1-bo'lim — bo'lish rejasi. O'quv 8 oy (33358 qator, 697 ta y60), val 3 oy (13203 qator, 230 ta y60), test 3 oy (13163 qator). Test uchun y60 soni va ulushi ataylab chiqarilmadi (?). Test belgilariga birinchi marta 29.3 da, model va siyosat to'liq tanlangandan keyin qaraladi. "Belgi ma'lum" ustuni bo'shliqlarning sababini ko'rsatadi: o'quvning oxirgi oyi (as-of 14) belgisi 2025-04 da yetiladi, val esa aynan 2025-04 dan boshlanadi. Bo'shliq oylarining o'zi bo'sh emas (4333 va 4475 qator), ular faqat bu bo'lishda ishlatilmaydi.

2-bo'lim — orqaga sinov. Faqat as-of 7-14 ma'lumoti bor deb faraz qildik va uchta sxema bilan "kelajakdagi" (16-18) sifatni bashorat qildik. Keyin haqiqatni o'lchadik: 7-14 da o'qitilgan model 16-18 da. Natija aralash chiqdi va uni halol yozamiz:

  • AUC bo'yicha vaqt sxemasi eng yaqin: xato 0.0110. Mijoz bo'yicha bo'lishda 0.0176, tasodifiy bo'lishda 0.0305. Uchalasi ham AUC ni past baholadi: val oylari tasodifan "osonroq" chiqdi.
  • Top aniqlik va kalibrlash bo'yicha mijoz bo'yicha sxema eng yaqin. Vaqt sxemasi esa kalibrlashni 0.857 deb ko'rsatdi, haqiqat 1.088: 13-14 va 16-18 oylarining bazaviy ulushlari har xil.

Bu ma'lumotda tasodifiy bo'lish keskin optimizm bermadi. Sabab: generatorda oylar orasidagi drift kichik va belgilarda mijozni "yodlab olish" imkoni kam. Lekin tasodifiy bo'lishda har val qatori uchun o'quvning o'rtacha 32.6% i undan keyingi oylardan olingan: model kelajakdan o'rganadi. Drift paydo bo'lganda, masalan test oylarida makroiqtisodiy yuklama kuchayganda 29.3-bob, aynan shu tafovut ko'rinadi. Vaqt bo'yicha bo'lish drift kuchli bo'lgani uchun emas, ishlab chiqarishni simulyatsiya qilgani uchun tanlanadi.

3-bo'lim — yetilmagan belgi tuzog'i. Tahlilchi "eng yangi ma'lumotni ham olaylik" deb T-oyda as-of T-1 ni ham o'quvga qo'shadi. Omborda as-of T-1 ning ikkinchi hisobvarag'i hali to'lanmagan, shuning uchun uning y60 si 0 ko'rinadi. 7 ta oy bo'yicha (T = 12-18) AUC deyarli o'zgarmadi (0.8380 va 0.8381), bashorat darajasi esa tizimli pasaydi. p/haqiqiy o'rtacha 1.026 dan 0.893 ga tushdi va 7 oyning yettisida past chiqdi (0.803-0.908 barobar). Bu tuzoqni AUC ko'rsatmaydi. Lekin 29.1 da ko'rganimizdek, qaror p * zarar > narx ko'rinishida bo'lsa, ehtimolning 13% ga past bo'lishi to'g'ridan-to'g'ri pul xatosi.

4-bo'lim — ma'lumot kartasi. Hamma raqam natijadan olindi: 68532 qator, 5463 noyob mijoz, 15 belgi, o'quvdagi ulush 0.0209, yangi mijozlar 9.4%. Karta 2.8 dagi shablonning qisqa versiyasi. To'liq kartada egasi, yangilanish chastotasi va yaroqsiz foydalanish holatlari ham bo'ladi.

5-bo'lim — xulosa har metrika uchun alohida chiqarildi. Yagona "g'olib" sxema yo'q, va buni yashirmaymiz. Vaqt bo'yicha bo'lishning asosiy dalili bu jadvalda emas: u yetilmagan belgi, bo'shliq va keyingi darsdagi test oylari driftida ko'rinadi.


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

Noto'g'ri fikr To'g'risi
"Oxirgi 6 hisobvaraq — tabiiy oyna" Joriy hisobvaraq to'lovi as-of dan keyin; oyna t-6 dan t-1 gacha
"To'lovni hisob oyi bo'yicha filtrlash yetadi" Hodisa vaqti (tolov_oy <= t) bo'yicha
"Val AUC 0.97 — ajoyib model" Snapshot testi: ishlab chiqarishda 0.80 va qoidadan yomon
"SQL to'g'ri ko'rinadi — to'g'ri" Mustaqil pandas hisobi va snapshot testi bilan isbot
"Takrorlarni drop_duplicates bilan tozalaymiz" Tabiiy takrorlar ham bor (52); partiya darajasida tuzatish
"Nuqsonli qatorlarni o'chiramiz" Karantin: sabab bilan saqlanadi
"EDA dagi har farq — topilma" Hudud farqi to'liq shovqin edi (generatorda ta'sir yo'q)
"Eng yangi oylarni ham o'quvga qo'shamiz" Yetilmagan belgi bashoratni ~13% pasaytiradi
"Tasodifiy bo'lish har doim optimistik" Bu ma'lumotda yo'q; lekin u kelajakdan o'rganadi (32.6%)
"Test ulushiga qarash — zararsiz" Test belgilari bir marta, tanlov tugagach ochiladi

6. Keng tarqalgan xatolar va yechimlari

1. Joriy davrni oynaga qo'shish

python
"WHERE h.oy BETWEEN :t - 5 AND :t"                                      # ⚠️
"WHERE h.oy BETWEEN :t - 6 AND :t - 1 ... AND p.tolov_oy <= :t"         # ✅

2. Belgi va natija bitta so'rovda

python
df = pd.read_sql("SELECT ..., kechikdi_keyin AS y FROM ...", con)       # ⚠️
X = pd.read_sql(BELGI_SQL, con, params=p); y = pd.read_sql(NATIJA_SQL, con, params=p)  # ✅

3. JOIN bilan qator yo'qotish

python
"FROM nomzod n JOIN tr_b r USING (mijoz_id)"                            # ⚠️ tranzaksiyasizlar yo'qoladi
"FROM nomzod n LEFT JOIN tr_b r USING (mijoz_id)" + "COALESCE(r.naqd_soni_3, 0)"  # ✅

4. Takrorni noto'g'ri tozalash

python
tr = tr.drop_duplicates()                                               # ⚠️ 52 tabiiy takror ham ketadi
tr = tr[(tr.oy != 15) | (tr.groupby(ustun).cumcount() < n // 2)]       # ✅ faqat nuqsonli partiya

5. Snapshot testisiz joriy etish

python
model = fit(belgilar(con, t))                                           # ⚠️
assert np.allclose(belgilar(con, t), belgilar(con, t, p="snap_"))       # ✅ CI da

6. Tasodifiy bo'lish

python
tr, va = train_test_split(df, test_size=0.2)                            # ⚠️
tr, va = df[df.oy <= 14], df[df.oy.between(16, 18)]                     # ✅ bo'shliq bilan

7. Yetilmagan belgi

python
oquv = df[df.oy <= joriy - 1]                                           # ⚠️ y60 hali 0 ko'rinadi
oquv = df[df.oy <= joriy - 2]                                           # ✅ gorizont = 2 oy

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 3-qism va 7-qism (o'tilgan): Pandas va ma'lumot yig'ish — SQL bilan birga
  • 8-qism (o'tilgan): EDA — bu yerda qarorga yo'naltirilgan qisqa variant; robust z (8.6)
  • 17.5, 17.8-darslar (o'tilgan): Sana belgilari va leakage — as-of ning nazariyasi
  • 18.2-dars (o'tilgan): CV turlari — vaqt va guruh bo'yicha bo'lish
  • 27.3-dars (o'tilgan): Ma'lumot quvuri, validatsiya va karantin
  • 27.12-dars (o'tilgan): Drift — vaqt bo'yicha bo'lish uni ko'rsatadi
  • 29.3-dars: Shu belgilar bilan modellashtirish va test
  • 29.6-dars: GitHub va loyiha tuzilishi — sql/, tests/test_snapshot.py

8. Eng yaxshi amaliyotlar

  1. Inventar: kalit, hodisa vaqti va "hozirgi holat" ustunlari har jadval uchun.

  2. Belgilar faqat as-of gacha, hodisa vaqti bo'yicha; natija alohida so'rovda.

  3. SQL ni mustaqil hisob bilan solishtiring.

  4. Snapshot testi va sana auditi CI da doimiy bo'lsin.

  5. Val natijasi "juda yaxshi" bo'lsa — tekshiruv signali.

  6. Sifat: oldingi eksportga nisbatan; karantin; partiya darajasida tuzatish.

  7. EDA faqat qarorga ta'sir qiladigan savollar bilan va test oylarisiz.

  8. Vaqt bo'yicha bo'lish, bo'shliq = belgi yetilishi; ma'lumot kartasi kod bilan to'ldiriladi.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # t-oy hisobvarag'ining to'lovi qaysi oyda keladi?
2.  # "oxirgi 6 hisobvaraq" sizuvchi oynada kechikish_6 ning AUC si qayoqqa?
3.  # sizuvchi model snapshot dan hisoblangan belgilarda - qoidadan yaxshimi?
4.  # halol so'rovda snapshot testi nechta farq topadi?
5.  # toza manbada to'liq takror tranzaksiyalar soni - nolmi?
6.  # birlik xatosi 6 qatorga tegdi - o'rtacha tolov_nisbati_3 ga ta'siri?
7.  # generatorda hudud ta'siri yo'q - hududlar orasidagi y60 farqi nima?
8.  # as-of 14 natijasi qachon ma'lum? val nega 16 dan boshlanadi?
9.  # yetilmagan as-of T-1 ni qo'shish AUC ga va kalibrlashga ta'siri?
10. # tasodifiy bo'lish bu ma'lumotda AUC ni oshirib ko'rsatdimi?
Javoblar
  1. t+1-oyda (muddat — 25-kun)
  2. Keskin oshadi: 0.5282 → 0.9138
  3. Yo'q — 22.56, qoida 24.26, halol model 28.85
  4. 0
  5. Yo'q — 52 ta tabiiy takror
  6. 0.664 → 85.000 (o'rtacha butunlay buziladi)
  7. Shovqin (0.0176 va 0.0226)
  8. 2025-04 (16-oy) oxirida; 16-oyda o'qitilgan modelning oxirgi belgili oyi 14
  9. AUC deyarli o'zgarmaydi (0.8380 va 0.8381), bashorat ~13% past (7/7 oy)
  10. Yo'q — 0.8177, haqiqat 0.8482; lekin o'quvning 32.6% i kelajakdan

Vazifa 2: Xatolarni tuzating

python
1.  sql = f"SELECT ... WHERE oy <= {t}"          # t - foydalanuvchidan

2.  "LEFT JOIN tolovlar p ON p.mijoz_id = h.mijoz_id AND p.hisob_oy = h.oy "
    "WHERE h.oy BETWEEN :t - 5 AND :t"

3.  "SELECT n.*, r.naqd_soni_3 FROM nomzod n JOIN tr_b r USING (mijoz_id)"

4.  xom = xom[xom.mijoz_id.isin(mijozlar.mijoz_id)]      # yetimlarni tashlaymiz

5.  eda = df.groupby("oy").y60.mean()                    # df - 7-22 hammasi
Javoblar
python
1.  pd.read_sql("SELECT ... WHERE oy <= :t", con, params={"t": t})

2.  "LEFT JOIN tolovlar p ON p.mijoz_id = h.mijoz_id AND p.hisob_oy = h.oy "
    "AND p.tolov_oy <= :t WHERE h.oy BETWEEN :t - 6 AND :t - 1"

3.  "SELECT n.*, COALESCE(r.naqd_soni_3, 0) AS naqd_soni_3 "
    "FROM nomzod n LEFT JOIN tr_b r USING (mijoz_id)"

4.  karantin = xom[~xom.mijoz_id.isin(mijozlar.mijoz_id)]    # sabab bilan saqlanadi
    xom = xom.drop(karantin.index)

5.  eda = df[df.oy <= 18].groupby("oy").y60.mean()          # test oylari yopiq

Vazifa 3: SQL

Modellang (1-misol asosida):

  1. util_3 va kechikish_6 ni SQLite oyna funksiyalari (AVG(...) OVER (PARTITION BY mijoz_id ORDER BY oy ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)) bilan qayta yozing — natija pandas bilan mosmi?
  2. Barcha as-of oylar (7-22) belgilarini bitta so'rov bilan oling (as-of oylar jadvali bilan JOIN) — vaqtni solishtiring
  3. Yangi belgi: "oxirgi 3 oyda naqd yechish summasining limitga nisbati" — SQL da qo'shing va snapshot testidan o'tkazing
  4. EXPLAIN QUERY PLAN bilan indekslar ishlatilayotganini tekshiring; indekslarni olib tashlab vaqtni o'lchang

Vazifa 4: Sizish

Modellang (2-misol asosida):

  1. "Hozirgi holat" sizishini yarating: DPD60 ga tushgan mijozlarning limitini keyin 50% ga qisqartiring va mijozlar.limit ni yangilang — util belgisi qanday siziydi? Snapshot testi buni ushlaydimi? (Ishora: mijozlar jadvali snapshot da o'zgarmaydi — nega test ojiz?)
  2. Snapshot testini barcha as-of oylar (7-18) uchun ishga tushiring va vaqtini o'lchang
  3. Sana auditini umumlashtiring: so'rov o'qigan har jadval uchun eng katta hodisa vaqtini qaytaradigan funksiya
  4. "Natijadan hosil" belgisi qo'shing (masalan, "undiruvga o'tkazilgan" bayrog'i t+1 da) — kuchli belgi signali ishlaydimi?

Vazifa 5: Sifat va EDA

Modellang (3-misol asosida):

  1. Nuqsonli eksport bilan (tuzatmasdan) LogReg o'qiting — val tejami qanchaga o'zgaradi?
  2. Oylik hajm tekshiruvini tolovlar va hisobvaraqlar uchun ham qo'shing; "tushib qolgan oy" nuqsonini yarating va ushlang
  3. Hududlar orasidagi y60 farqi uchun xi-kvadrat testi 11.5-bob — p-qiymat nima deydi? 6 hududdan eng yaxshi va eng yomonini tanlab test qilsak-chi?
  4. util bo'yicha y60 ni 20 ta kvantil bo'yicha chizing — logit shkalada chiziqlimi?

Vazifa 6: Bo'lish

Modellang (4-misol asosida):

  1. Orqaga sinovni har oy T = 12-18 uchun takrorlang (o'quv <= T-2) — sxemalarning AUC xatosi o'rtacha va SE bilan
  2. Generatorda makroiqtisodiy yuklamani kuchaytiring (0.08 o'rniga 0.2) — tasodifiy bo'lish optimizmi paydo bo'ladimi?
  3. Mijoz bo'yicha va vaqt bo'yicha birga bo'lish (yangi mijozlar + kelajak oylar) — qaysi savolga javob beradi?

Vazifa 7: O'ylash

Jamoa rahbari: "Snapshot testi har kecha 20 daqiqa ishlaydi va hozirgacha bironta ham xato topmadi. CI ni tezlashtirish uchun uni o'chiramiz. SQL ni tajribali tahlilchi yozgan, kod ko'rib chiqishdan o'tgan."

Javob

Qisqa javob: Xato topmagan test foydasiz degani emas. Bu sug'urta, va u faqat kerak bo'lgan kuni ishlaydi. Uni o'chirish o'rniga arzonlashtirish mumkin.

1. Nega kod ko'rib chiqish yetarli emas. 2-misoldagi ikki so'rov faqat bitta qatorda farq qiladi (BETWEEN :t - 5 AND :t va :t - 6 AND :t - 1) va ikkalasi ham "oxirgi 6 hisobvaraq" kabi tabiiy o'qiladi. Sizuvchi variant val da AUC 0.9692 va 60.07 mln so'm ko'rsatdi, ishlab chiqarishda esa 22.56 berdi, ya'ni oddiy qoidadan ham yomon. Bunday xato ko'rib chiqishdan o'tadi, snapshot testidan esa o'tmaydi (12286 farqli katak).

2. Test qachon kerak bo'ladi. SQL o'zgarganda (yangi belgi), manba o'zgarganda (to'lovlar jadvali boshqa tizimdan kela boshlaydi) va yangi xodim kelganda. Hozirgacha xato yo'qligi — aynan test bor bo'lgani uchun ham bo'lishi mumkin: xato birinchi PR da ushlanib, kechagi hisobotga yetib bormagan.

3. Arzonlashtirish yo'llari.

python
# 1) har kecha - faqat 1-2 tasodifiy as-of oy, to'liq 16 oy - haftada bir marta
# 2) faqat SQL fayllari o'zgargan PR larda ishga tushirish
# 3) snapshot ni to'liq nusxa emas, TEMP VIEW (WHERE tolov_oy <= t) bilan
# 4) mijozlarning 10% tasodifiy namunasida

4. Rahbarga javob: "Bu test 'eng qimmat' xatomizdan himoya qiladi: validatsiyada ajoyib, ishlab chiqarishda qoidadan ham yomon model. Kod ko'rib chiqish bunday xatoni ushlamaydi, biz buni ikki so'rovda o'lchadik. Testni o'chirmasdan, uni 20 daqiqadan 2 daqiqaga tushirishni taklif qilaman: kechasi bitta oy, to'liq tekshiruv faqat SQL o'zgarganda."

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


Xulosa

Bu darsda loyihaning ma'lumot qismini qildik: to'rtta jadvaldan nuqtaviy to'g'ri belgilar jadvaligacha, sifat, EDA va vaqt bo'yicha bo'lish bilan.

Eng muhim uch fikr:

  1. Belgilar faqat as-of gacha, hodisa vaqti bo'yicha, va bu isbotlanadi. 1-misolda SQL so'rov mustaqil pandas hisobi bilan 1e-9 aniqlikda mos keldi (eng katta farq 8.88e-16). Belgilar va natija alohida so'rovlarda, to'lovlar esa tolov_oy <= t bo'yicha filtrlangan.

  2. Sizish o'qish bilan emas, o'lchash bilan ushlanadi. 2-misolda bitta qatorlik farq ("oxirgi 6 hisobvaraq") val da AUC 0.9692 va 60.07 mln so'm ko'rsatdi. Ishlab chiqarish simulyatsiyasida esa 0.7991 va 22.56 chiqdi, bu oddiy qoidadan ham yomon. Snapshot testi (12286 farqli katak), sana auditi (4575 ta kelajak to'lovi) va kuchli belgi signali (AUC > 0.9) uni mustaqil ushladi. Halol so'rovda hisobot va ishlab chiqarish aynan bir xil.

  3. Sifat, EDA va bo'lish — qarorga xizmat qiladi. 3-misolda nuqsonlar oldingi eksport bilan solishtirib ushlandi, takroriy partiya esa partiya darajasida tuzatildi va tabiiy 52 takror saqlandi. Birlik xatosi faqat 6 qatorga tegib, tolov_nisbati_3 o'rtachasini 0.664 dan 85.000 ga ko'tardi. EDA dagi hududiy farq shovqin ekani ma'lum bo'ldi. 4-misolda tasodifiy bo'lish bu ma'lumotda AUC ni oshirmadi, lekin o'quvning 32.6% i kelajakdan olingan edi. Yetilmagan belgi esa bashoratni 7 oyning yettisida pasaytirdi (o'rtacha ~13%). Test oylari yopiq qoldi.

Keyingi darsda To'liq loyiha: modellashtirish va baholash: loyihaning ikkinchi qismi. Oldindan yozilgan baholash rejasi bilan qoida, logistik regressiya, HistGB va kichik tarmoqni 7 oylik vaqt bo'yicha CV da juftlashtirib solishtiramiz. 29.1 dagi ochiq savol ham shu yerda hal bo'ladi: model qoidadan ishonchli yaxshimi? Keyin ehtimollarni kalibrlaymiz, qo'ng'iroq markazi sig'imiga mos siyosat va uning kutilgan tejamini tanlaymiz, segmentlar bo'yicha xatolarni tahlil qilamiz, testni bir marta ochamiz va model kartasini yozamiz.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
29.2-dars: To'liq loyiha: muammo va ma'lumot — IlmHamroh