IlmHamroh
Data Science va sun'iy intellekt/Maxsus mavzular6/12-dars50 daqiqa
Mundarija (25)

28.6-dars: Ilg'or anomaliya aniqlash

28-QISM — MAXSUS MAVZULAR · 6-dars


1. Kirish va motivatsiya

Oldingi ikki darsda tavsiya tizimlarini qurdik — ko'p foydalanuvchilarning odatiy xulqidan o'rganib, har biriga mos narsani topdik. Bu darsda teskari savolga qaytamiz: odatiy bo'lmagan narsani qanday topish mumkin?

Anomaliya aniqlash bilan ikki marta tanishganmiz. 08.6-darsda EDA doirasida IQR, z-score va "imkonsiz kombinatsiyalar" bilan g'ayrioddiy qatorlarni qidirdik. 16.10-darsda nazoratsiz usullarni — IsolationForest, LOF, One-Class SVM, PCA qayta tiklash xatosini — o'rgandik va anomaliyaning uch turini (nuqtali, kontekstli, jamoaviy) ajratdik; contamination faqat chegarani belgilashini, baholashda PR-AUC va precision@k kerakligini ko'rdik. Bu asoslarni qayta tushuntirmaymiz — ular ustiga uchta amaliy yo'nalishni quramiz:

  1. Vaqt qatoridagi anomaliya. Qiymat faqat o'zi emas, vaqt konteksti bilan baholanadi: juma kechqurun 40 MW — past, tunda 40 MW — baland. Sensor "qotib qolsa", har bir qiymat normal, lekin ketma-ketlik g'ayrioddiy.
  2. Firibgarlik. Tranzaksiyalarning 0.2-0.5% i firib; ROC-AUC 0.97 bo'lsa ham signallarning aksariyati yolg'on bo'lishi mumkin. Tekshiruvchilar kuniga faqat bir necha o'nta holatni ko'ra oladi, yorliqlar (chargeback) haftalar keyin keladi, xatoning narxi esa har xil.
  3. Avtoenkoder. Neyron tarmoq bilan qayta tiklash xatosi 26.2-bob — lekin u haqiqatan IsolationForest dan yaxshimi? Halol taqqoslaymiz.

Real vaziyat. Toshkentdagi bank firibgarlikka qarshi tizim uchun IsolationForest qurdi va hisobotda "ROC-AUC 0.97" deb yozdi. Ishga tushirilgach, operatorlar kuniga 600 dan ortiq signal oldi, ulardan faqat 20% ga yaqini haqiqiy firib edi. Operatorlar faqat birinchi 30 tasini ko'rishga ulgurdi va qolganini yopib qo'ydi. Keyingi oyda model qayta o'qitildi — lekin so'nggi ikki haftadagi firiblar hali tasdiqlanmagan edi va "normal" deb o'qitishga kirdi. Ikki xato: noto'g'ri metrika (ROC-AUC nomutanosiblikda aldaydi) va noto'g'ri yorliqlar (belgi kechikishi). Bu darsda ikkalasini ham raqam bilan ko'rsatamiz.

Bu darsda anomaliya aniqlashni "ball hisoblash" dan operatsion tizim ga aylantiramiz: kontekst, chegara, sig'im, narx va kechikish bilan.

Bu darsda:

  • Vaqt qatorida: nuqtali, kontekstli va kollektiv anomaliyalar
  • Rolling z-score, robust median/MAD, STL qoldig'i, bashorat qoldig'i
  • Hodisa darajasida baholash: recall, yolg'on signal/hafta, aniqlash kechikishi
  • Firibgarlik: shaxsiy bazaviy belgilar, nazoratsiz vs nazoratli vs yarim nazoratli
  • PR-AUC va ROC-AUC, kunlik sig'im (top-k), xarajatga asoslangan chegara
  • Vaqt bo'yicha bo'lish va belgi kechikishi
  • Avtoenkoder va IsolationForest — halol taqqoslash
  • Tuzoqlar

ℹ Misollar real numpy/pandas/statsmodels/sklearn/torch bilan (Python 3.14). Ma'lumot sintetik: tuman elektr iste'moli (soatlik, sintetik taqvimdagi bayram kunlari bilan), bank kartasi tranzaksiyalari (O'zbekiston shaharlari, akkaunt egallash epizodlari) va uskuna sensorlari.


2. Nazariya — chuqur tushuntirish

2.1. Qisqa eslatma va nima yangi

text
16.10 DA O'RGANILGAN (qayta tushuntirilmaydi):
  IsolationForest - tasodifiy bo'linishlarda tez izolyatsiya = anomaliya
  LOF - mahalliy zichlik; One-Class SVM; PCA qayta tiklash xatosi
  contamination - faqat chegara; baholash - PR-AUC, precision@k
  kontekstual anomaliya - belgi muhandisligi bilan nuqtaviyga aylanadi

BU DARSDA YANGI:
  vaqt konteksti (soat, hafta kuni, bayram) va KETMA-KETLIK
  sababiylik: detektor faqat O'TMISHNI ko'ra oladimi?
  hodisa darajasida baholash va aniqlash kechikishi
  juda nomutanosib yorliqli ma'lumot: nazoratli usullar, sig'im, narx
  yorliqlar kechikib keladi

2.2. Vaqt qatoridagi anomaliya turlari

text
NUQTALI (point):   bitta soatda keskin sakrash
   ---/\---        global darajada ham g'ayrioddiy bo'lishi MUMKIN,
                   lekin kun ichidagi tebranish katta bo'lsa - yo'q

KONTEKSTLI (contextual):  qiymat o'zi normal, VAQTIGA nisbatan g'ayrioddiy
   juma 20:00 da 39 MW - kunduzgi daraja; shu soatda odatda 57 MW
   global statistika uni KO'RMAYDI (39 - umumiy o'rtachadan past ham emas)
   kontekst = soat, hafta kuni, bayram, harorat

KOLLEKTIV (collective):   har nuqta normal, KETMA-KETLIK g'ayrioddiy
   sensor qotib qoldi: 10 soat bir xil qiymat
   ---_____________---   darajasi normal, lekin kunlik profil yo'qoldi
   aniqlash uchun bir necha nuqta kerak -> KECHIKISH muqarrar

ANOMALIYA EMAS (lekin detektorni aldaydi):
   bayram kunlari, dam olish kunlari, mavsum - KUTILGAN o'zgarish
   ular modelda kontekst sifatida bo'lishi kerak, aks holda yolg'on signal

2.3. Detektorlar

text
1. GLOBAL z:   |x - mean| / std  (tarixdan)
   kontekstsiz; kun ichidagi katta tebranish std ni shishiradi

2. ROLLING z:  oxirgi w soatning o'rtachasi va std si bilan  (sababiy)
   o'tgan 24 soat - kun profilining HAMMASI -> "kechki cho'qqi" normal
   anomaliyaning o'zi oynaga kirib, std ni shishiradi (niqoblanish)

3. ROBUST: median va MAD (median absolute deviation)
   z_robust = |x - median| / (1.4826 * MAD)     1.4826 - normal uchun std ga moslash
   bitta sakrash median va MAD ni deyarli o'zgartirmaydi

4. STL QOLDIG'I:  x = trend + mavsum + qoldiq   (statsmodels STL)
   qoldiqda robust z; mavsumiy kontekstni o'zi hisobga oladi
   ⚠️ STL BUTUN qatorni ko'radi (oldingi va KEYINGI qiymatlar) -
      oflayn tahlil uchun zo'r, real vaqtda bu shaklda ishlamaydi

5. BASHORAT QOLDIG'I:  e_t = x_t - x_hat_t,  x_hat - faqat o'tmishdan
   eng sodda: o'tgan 4 haftaning SHU SOATI mediana (mavsumiy naive, 28.1-28.2)
   yaxshiroq: 28.2-28.3 dagi modellar (bayram belgisi bilan)
   qoldiqda robust z; sababiy -> real vaqtda ishlaydi

statsmodels ba'zi hollarda (chastotasiz indeks, yaqinlashish) ogohlantirishlarni stderr ga chiqaradi. Misollarda indeksga freq="h" beramiz va STL chaqiruvini warnings.catch_warnings() + simplefilter("ignore") bilan o'raymiz — bu faqat ekranni toza saqlash uchun, natijaga ta'sir qilmaydi.

Kontekstli anomaliya uchun "normal" ning o'zi kontekstga bog'liq bo'lishi kerak. Global va rolling statistikalar "odatda qancha" degan savolga soatni hisobga olmay javob beradi — shuning uchun kontekstli anomaliyani deyarli ko'rmaydi (1-misol).

2.4. Vaqt qatorida baholash

text
NUQTA darajasi emas, HODISA darajasi:
   hodisa topildi = hodisa oynasida (+2 soat) kamida bitta signal
   recall = topilgan hodisalar / hamma hodisalar  (tur bo'yicha alohida)
   yolg'on signal / hafta = hech bir hodisaga tegishli bo'lmagan signallar
   KECHIKISH = birinchi signal soati - hodisa boshlanishi

CHEGARA - oldindan belgilangan byudjet bilan (27.11 alerting):
   "haftasiga ~1 yolg'on signal" -> toza KALIBRLASH davrida
   ball taqsimotining 1 - 1/168 kvantili
   har detektorga O'Z chegarasi, lekin BIR XIL yolg'on signal byudjeti
   -> detektorlarni teng sharoitda solishtirish mumkin

KALIBRLASH davri toza bo'lishi kerak; bo'lmasa (real hayotda) -
   robust kvantil yoki qo'lda tozalangan tarix

2.5. Firibgarlik: juda nomutanosib ma'lumot

text
ulush 0.3%:  60 000 tranzaksiyada ~200 firib
ROC-AUC:     "tasodifiy firib tasodifiy normaldan yuqorimi" - normal
             ko'p bo'lgani uchun FPR = 1% ham 600 yolg'on signal!
PR-AUC:      signallar ichida firib ulushi (precision) - recall bo'yicha
             tasodifiy model: PR-AUC = ulush 0.003-bob, ROC-AUC = 0.5

AMALIY METRIKALAR:
   kunlik sig'im n: tekshiruvchilar kuniga n ta ko'radi
     -> har kuni eng shubhali n ta: kunlik precision, topilgan firib ulushi
   xarajat: FN narxi (yo'qolgan summa) va FP narxi (tekshirish, mijoz
     noqulayligi) - chegara shu yig'indini minimallashtiradi

2.6. Belgilar: shaxsiy bazaviy

text
XOM BELGILAR:   summa, soat, kategoriya
   500 ming so'm - bir mijoz uchun oddiy, boshqasi uchun g'ayrioddiy

SHAXSIY BAZAVIY (har mijoz O'Z O'TMISHI bilan):
   summa_z   = (log summa - o'tmish o'rtachasi) / o'tmish std
   soat_farq = |soat - o'tmishdagi o'rtacha soat|
   yangi_shahar, yangi_qurilma - o'tmishda ko'rilmaganmi
   soni_24s  = oxirgi 24 soatdagi tranzaksiyalar soni
   oraliq    = oldingi tranzaksiyadan beri o'tgan vaqt
   tarix_soni - o'tmish qancha uzun (bazaviy qanchalik ishonchli)

   ⚠️ FAQAT O'TMISH: cumsum - joriy qiymat; groupby().shift()
      16.10-4-misoldagi groupby().transform("median") butun davrni
      (KELAJAKNI ham) ishlatadi - tahlil uchun mayli, ishlab chiqarishda
      bunday belgi yo'q (17.8 leakage)

2.7. Nazoratsiz, nazoratli, yarim nazoratli

text
NAZORATSIZ (IsolationForest, LOF):   yorliq kerak emas
   "g'ayrioddiy" != "firib": sayohatchi, katta xarid ham g'ayrioddiy
   yangi turdagi firibni ham ushlashi mumkin

NAZORATLI (HistGB, logistik regressiya):  tarixiy yorliqlar bilan
   "firib qanday ko'rinadi" ni o'rganadi - odatda ANCHA aniqroq
   nomutanosiblik: class_weight="balanced" (sinf og'irligi)
   faqat MA'LUM turdagi firiblarni yaxshi taniydi

YARIM NAZORATLI:
   a) faqat tasdiqlangan NORMAL misollarda o'qitilgan nazoratsiz model
      (novelty detection, 16.10 dagi novelty=True g'oyasi)
   b) nazoratsiz ball -> nazoratli modelga belgi sifatida
   c) kam yorliq + ko'p yorliqsiz ma'lumot (self-training)

AMALDA: ikkalasi birga - nazoratli model asosiy ball, nazoratsiz model
        "yangi narsa" uchun qo'shimcha signal

2.8. Xarajatga asoslangan chegara

text
signal (tekshirish yoki bloklash):  narx FP (tekshirish, mijoz noqulayligi)
o'tkazib yuborilgan firib:          narx = firib summasi

jami(c) = sum_{firib, ball < c} summa  +  (ball >= c lar soni) * narx_FP

c ni VALIDATSIYA davrida tanlaymiz (test - bir marta)
0.5 - ehtimol uchun "tabiiy" chegara, lekin narxlar teng bo'lmasa
      optimal chegara undan ancha farq qilishi mumkin
narxlar biznes bilan kelishiladi; bu darsdagi raqamlar FARAZIY

2.9. Vaqt bo'yicha bo'lish va belgi kechikishi

text
FIRIB YORLIG'I qachon ma'lum bo'ladi?
   mijoz shikoyati / chargeback - kunlar yoki HAFTALAR keyin
   o'qitish kuni T da oxirgi haftalardagi firiblarning ko'pi hali "normal"

SODDA YO'L (xato):   noma'lum = 0 -> firiblar "normal" deb o'qitiladi
YETILGAN YO'L:       faqat T - L gacha bo'lgan davr (L - odatiy kechikish)
                     yorliqlar ishonchli, lekin ma'lumot eskiroq va kamroq
BAHOLASH:            test davri yorliqlari ham yetilishi kerak -
                     aks holda precision past ko'rinadi

BO'LISH: har doim vaqt bo'yicha (train - o'tmish, test - kelajak);
         tasodifiy bo'lish bitta epizodning firiblarini ikkala tomonga
         taqsimlaydi va natijani oshirib ko'rsatadi

2.10. Avtoenkoder bilan anomaliya

text
26.2 dagi avtoenkoder:  x -> enkoder -> z (kichik) -> dekoder -> x_hat
   faqat (asosan) normal ma'lumotda o'qitiladi
   anomaliya bali = |x - x_hat|^2  (qayta tiklash xatosi)
   PCA qayta tiklash xatosining NOCHIZIQLI umumlashmasi

QACHON FOYDALI:
   ustunlar orasida murakkab (nochiziqli) bog'liqlik bor
   anomaliya har ustunda alohida normal, lekin BIRGALIKDA mos emas
   yuqori o'lchamli ma'lumot (rasm, spektr, ko'p sensor)

QACHON ORTIQCHA:
   kichik jadval, anomaliya bitta-ikkita ustunda chetga chiqishda
   -> IsolationForest oddiyroq, tezroq, masshtablash kerak emas
   AE: masshtablash, arxitektura, epoxalar - sozlanadigan narsa ko'p

2.11. Tuzoqlar

Asosiy tuzoqlar: kontekstsiz (global) statistika bilan kontekstli anomaliyani qidirish; STL kabi kelajakni ko'radigan usulni real vaqt detektori deb baholash; nuqta darajasida baholab, kechikish va hodisa recall ini o'lchamaslik; detektorlarni turli yolg'on signal byudjetida solishtirish; bayram va dam olish kunlarini kontekstga qo'shmaslik; nomutanosib ma'lumotda ROC-AUC ga tayanish; shaxsiy bazaviyni butun davrdan (kelajakdan) hisoblash; tasodifiy bo'lish; yetilmagan yorliqlarni "normal" deb olish; chegarani 0.5 yoki test ga qarab tanlash; tekshiruvchilar sig'imini hisobga olmaslik; "chuqur o'rganish yaxshiroq" deb IsolationForest bilan solishtirmaslik.


3. Tez ma'lumotnoma

python
import numpy as np
import pandas as pd

# vaqt qatori: rolling va robust (faqat o'tmish)
oldin = x.shift(1)
z_roll = (x - oldin.rolling(24).mean()) / oldin.rolling(24).std()
med = oldin.rolling(24).median()
mad = (oldin - med).abs().rolling(24).median()
z_rob = (x - med) / (1.4826 * mad)

# mavsumiy naive bashorat qoldig'i: o'tgan 4 haftaning shu soati
bashorat = np.median([x.shift(168 * k) for k in range(1, 5)], axis=0)
qoldiq = x - bashorat

# chegara: toza kalibrlash davrida haftasiga ~1 yolg'on signal
chegara = np.nanquantile(ball[kalibrlash], 1 - 1 / 168)

# firibgarlik: shaxsiy bazaviy (o'tmish o'rtachasi, joriy qator kirmaydi)
n_old = df.groupby("mijoz").cumcount()
yig = df["ls"].groupby(df["mijoz"]).cumsum() - df["ls"]
df["summa_z"] = (df["ls"] - yig / n_old) / std_otmish

# nazoratli, nomutanosib
m = HistGradientBoostingClassifier(class_weight="balanced", early_stopping=False)
pr_auc = average_precision_score(y_te, m.fit(X_tr, y_tr).predict_proba(X_te)[:, 1])

# kunlik sig'im: har kuni eng shubhali n ta
top = te.assign(b=ball).groupby("kun", group_keys=False).apply(
    lambda g: g.nlargest(20, "b"))

# xarajatli chegara (validatsiyada)
jami = summa[(y == 1) & (ball < c)].sum() + (ball >= c).sum() * narx_fp

Qaysi vaziyatda nima

Vaziyat Yondashuv
Vaqt qatori, real vaqt bashorat qoldig'i (mavsum + bayram konteksti) + robust z
Vaqt qatori, oflayn tekshiruv STL qoldig'i
Sensor qotishi, darajaning siljishi ketma-ketlik belgilari (o'zgarmaslik, CUSUM), kechikishni o'lchang
Firib, yorliqlar bor HistGB + class_weight, shaxsiy bazaviy belgilar
Yorliq yo'q yoki yangi tur IsolationForest (shaxsiy belgilar bilan)
Baholash PR-AUC, kunlik top-k, xarajat; vaqt bo'yicha bo'lish
Murakkab ko'p o'lchovli bog'liqlik avtoenkoder (IF va PCA bilan solishtirib)

Anomaliya tizimi xulosasi

kontekst: soat/kun/bayram - "normal" ning o'zi kontekstga bog'liq
sababiylik: real vaqt detektori faqat o'tmishni ko'radi
baholash: hodisa recall, yolg'on signal/hafta, kechikish - bir xil byudjetda
firib: shaxsiy bazaviy, PR-AUC, top-k sig'im, narx bo'yicha chegara
yorliqlar kechikadi: yetilmagan davrni ehtiyot bilan
AE - murakkab bog'liqlikda; oddiy jadvalda IF ko'pincha yetarli

4. Batafsil misollar

Misollar real numpy/pandas/statsmodels/sklearn/torch bilan (Python 3.14). Har misol mustaqil ishlaydi. 2-4-misollar bir xil tranzaksiya generatorini (tranzaksiyalar()) va shaxsiy belgilarni (belgilar()) ishlatadi.

Misol 1 — Vaqt qatorida anomaliyalar: besh detektor

Tuman elektr iste'moli, soatlik, 20 hafta: kunlik profil (ertalabki va kechki cho'qqi), dam olish kunlari past, sekin trend va AR(1) shovqin. 13-haftada sintetik taqvimdagi ikki bayram kuni (iste'mol 20% past — anomaliya emas). 9-20-haftalarga 8 ta nuqtali, 6 ta kontekstli (ish kuni 19-21 soatda kunduzgi daraja) va 4 ta kollektiv (sensor 10 soat qotib qoladi) anomaliya qo'shilgan. 1-4-haftalar — tarix, 5-8 — toza kalibrlash.

python
"""Vaqt qatorida anomaliyalar: nuqtali, kontekstli, kollektiv - besh detektor."""

import warnings

import numpy as np
import pandas as pd
from statsmodels.tsa.seasonal import STL

HAFTA = 168


def qator(seed):
    """Tuman elektr iste'moli (MW), soatlik, 20 hafta; 9-20-haftalarga anomaliyalar."""
    rng = np.random.default_rng(seed)
    n = 20 * HAFTA
    t = np.arange(n)
    soat, kun = t % 24, (t // 24) % 7
    profil = (40 + 12 * np.exp(-(soat - 9) ** 2 / 8) + 20 * np.exp(-(soat - 20) ** 2 / 6)
              - 8 * (soat < 6))
    x = profil * np.where(kun >= 5, 0.85, 1.0) + 0.02 * t / 24 \
        + 3 * np.sin(2 * np.pi * t / (24 * 30))
    shovqin = np.zeros(n)
    e = rng.normal(0, 1.5, n)
    for i in range(1, n):
        shovqin[i] = 0.6 * shovqin[i - 1] + e[i]
    x = x + shovqin
    bayram = np.zeros(n, dtype=bool)                   # sintetik taqvimda bayram kunlari
    b0 = 12 * HAFTA + 24                               # 13-hafta seshanba-chorshanba
    bayram[b0:b0 + 48] = True
    x[bayram] *= 0.8
    hodisalar, band = [], np.zeros(n, dtype=bool)

    def joy(uzunlik, shart):
        while True:
            s = int(rng.integers(8 * HAFTA + 24, n - uzunlik - 24))
            if shart(s) and not band[s - 24:s + uzunlik + 24].any() and not bayram[s - 24:s + uzunlik + 24].any():
                band[s:s + uzunlik] = True
                return s
    for _ in range(8):                                  # nuqtali: bitta soatda sakrash
        s = joy(1, lambda s: True)
        x[s] += rng.choice([-1, 1]) * rng.uniform(12, 20)
        hodisalar.append(("nuqtali", s, s + 1))
    for _ in range(6):                                  # kontekstli: kechki cho'qqida kunduzgi daraja
        s = joy(3, lambda s: s % 24 == 19 and (s // 24) % 7 < 5)
        x[s:s + 3] = x[s - 5:s - 2].mean() - 3          # 14-16 soatdagi daraja - umumiy normal
        hodisalar.append(("kontekstli", s, s + 3))
    for _ in range(4):                                  # kollektiv: sensor qotib qoldi
        s = joy(10, lambda s: True)
        x[s:s + 10] = x[s]
        hodisalar.append(("kollektiv", s, s + 10))
    return pd.Series(x, index=pd.date_range("2025-01-06", periods=n, freq="h")), hodisalar, bayram


def ballar(x):
    """Har detektor: har soat uchun 'g'ayrioddiylik' bali (katta = shubhali)."""
    v = x.to_numpy()
    tarix = v[:8 * HAFTA]
    b = {"global z": np.abs(v - tarix.mean()) / tarix.std()}
    oldin = x.shift(1)
    o, s = oldin.rolling(24).mean(), oldin.rolling(24).std()
    b["rolling z (24s)"] = (np.abs(x - o) / s).to_numpy()
    med = oldin.rolling(24).median()
    mad = (oldin - med).abs().rolling(24).median()
    b["robust (median/MAD)"] = (np.abs(x - med) / (1.4826 * mad)).to_numpy()
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        q = STL(x, period=24, robust=True).fit().resid.to_numpy()
    b["STL qoldig'i"] = np.abs(q - np.median(q[:8 * HAFTA])) / (
        1.4826 * np.median(np.abs(q[:8 * HAFTA] - np.median(q[:8 * HAFTA]))))
    haftalar = np.stack([x.shift(HAFTA * k).to_numpy() for k in range(1, 5)])
    bashorat = np.median(haftalar, axis=0)             # o'tgan 4 haftaning shu soati
    qold = pd.Series(v - bashorat, index=x.index)
    qmad = (qold - qold.shift(1).rolling(HAFTA).median()).abs().shift(1).rolling(HAFTA).median()
    b["bashorat qoldig'i"] = (np.abs(qold - qold.shift(1).rolling(HAFTA).median())
                              / (1.4826 * qmad)).to_numpy()
    return b


def baholash(ball, hodisalar, bayram, n):
    """Chegara: toza kalibrlash haftalarida (5-8) haftasiga ~1 yolg'on signal."""
    kal = ball[4 * HAFTA:8 * HAFTA]
    chegara = np.nanquantile(kal, 1 - 1 / HAFTA)
    signal = np.nan_to_num(ball[8 * HAFTA:], nan=0.0) > chegara
    ind = np.flatnonzero(signal) + 8 * HAFTA
    oyna = np.zeros(n, dtype=bool)
    topildi, kechikish = {}, []
    for tur, s, e in hodisalar:
        oyna[s:e + 3] = True
        hit = ind[(ind >= s) & (ind < e + 3)]
        topildi.setdefault(tur, []).append(len(hit) > 0)
        if tur == "kollektiv" and len(hit):
            kechikish.append(hit[0] - s)
    yolgon = [i for i in ind if not oyna[i] and not bayram[i]]
    return ({k: np.mean(v) for k, v in topildi.items()},
            np.mean([t for v in topildi.values() for t in v]),
            len(yolgon) / 12, int(bayram[ind].sum()), kechikish)


def main() -> None:
    x, hodisalar, bayram = qator(0)
    print("=== 1. Qator va anomaliyalar (urug' 0) ===")
    print(f"  {len(x)} soat (20 hafta): 1-4 tarix, 5-8 kalibrlash (toza), 9-20 baholash")
    print(f"  o'rtacha {x.mean():.1f} MW, std {x.std():.1f}; kun ichida 3-soat "
          f"{x[x.index.hour == 3].mean():.1f}, 20-soat {x[x.index.hour == 20].mean():.1f}")
    for tur in ["nuqtali", "kontekstli", "kollektiv"]:
        s = [h for h in hodisalar if h[0] == tur]
        print(f"  {tur:<11} {len(s)} ta hodisa, masalan soat {s[0][1]}: "
              f"qiymat {x.iloc[s[0][1]]:.1f} (shu soatda odatda "
              f"{np.median([x.iloc[s[0][1] - HAFTA * k] for k in (1, 2, 3)]):.1f})")
    print(f"  bayram kunlari (anomaliya EMAS, 48 soat): o'rtacha "
          f"{x[bayram].mean():.1f}, oldingi haftaning shu soatlari "
          f"{x.shift(HAFTA)[bayram].mean():.1f}")

    print("\n=== 2. Besh detektor, 6 ta qator (urug'lar) - o'rtacha ===")
    jadval = {}
    for seed in range(6):
        x, hodisalar, bayram = qator(seed)
        for nom, b in ballar(x).items():
            jadval.setdefault(nom, []).append(baholash(b, hodisalar, bayram, len(x)))
    print(f"  {'detektor':<20} {'nuqtali':>8} {'kontekst':>9} {'kollektiv':>10} "
          f"{'hammasi':>8} {'yolg_on/hafta':>14} {'bayramda':>9}")
    for nom, r in jadval.items():
        tur = {k: np.mean([q[0][k] for q in r]) for k in ["nuqtali", "kontekstli", "kollektiv"]}
        print(f"  {nom:<20} {tur['nuqtali']:>8.2f} {tur['kontekstli']:>9.2f} "
              f"{tur['kollektiv']:>10.2f} {np.mean([q[1] for q in r]):>8.2f} "
              f"{np.mean([q[2] for q in r]):>14.2f} {np.mean([q[3] for q in r]):>9.1f}")

    print("\n=== 3. Kollektiv anomaliya: aniqlash kechikishi (soat) ===")
    for nom, r in jadval.items():
        k = [d for q in r for d in q[4]]
        print(f"  {nom:<20} topildi {len(k):>2}/24, kechikish mediana "
              f"{np.median(k) if k else float('nan'):>4.1f}")

    print("\n=== 4. Qaror: faqat REAL VAQTDA ishlay oladigan detektorlar ichida ===")
    rec = {nom: np.array([q[1] for q in r]) for nom, r in jadval.items()}
    tartib = ["global z", "rolling z (24s)", "robust (median/MAD)",
              "bashorat qoldig'i"]                      # soddadan murakkabga, sababiy
    eng = max(tartib, key=lambda n: rec[n].mean())
    tanlov = None
    for nom in tartib:
        d = rec[nom] - rec[eng]
        se = d.std(ddof=1) / np.sqrt(len(d))
        yomon = d.mean() < -2 * se
        if nom != eng:
            print(f"  {nom:<20} - {eng}: {d.mean():+.3f}, SE {se:.3f}, "
                  f"sezilarli yomon: {yomon}")
        if tanlov is None and not yomon:
            tanlov = nom
    print(f"  qaror (eng sodda munosib): {tanlov}")
    d = rec["STL qoldig'i"] - rec[eng]
    print(f"  STL (butun qatorni, ya'ni KELAJAKNI ham ko'radi) - {eng}: {d.mean():+.3f}, "
          f"SE {d.std(ddof=1) / np.sqrt(len(d)):.3f} - faqat oflayn tahlil uchun")
    print("  ⭐ Kontekst (soat, hafta kuni) bilan solishtirmaguncha kontekstli anomaliya ko'rinmaydi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Qator va anomaliyalar (urug' 0) ===
  3360 soat (20 hafta): 1-4 tarix, 5-8 kalibrlash (toza), 9-20 baholash
  o'rtacha 43.4 MW, std 9.1; kun ichida 3-soat 32.5, 20-soat 57.9
  nuqtali     8 ta hodisa, masalan soat 1485: qiymat 34.5 (shu soatda odatda 48.6)
  kontekstli  6 ta hodisa, masalan soat 2803: qiymat 39.1 (shu soatda odatda 57.2)
  kollektiv   4 ta hodisa, masalan soat 2471: qiymat 47.4 (shu soatda odatda 45.1)
  bayram kunlari (anomaliya EMAS, 48 soat): o'rtacha 34.9, oldingi haftaning shu soatlari 42.3

=== 2. Besh detektor, 6 ta qator (urug'lar) - o'rtacha ===
  detektor              nuqtali  kontekst  kollektiv  hammasi  yolg_on/hafta  bayramda
  global z                 0.33      0.00       0.08     0.17           2.06       0.5
  rolling z (24s)          0.33      0.06       0.04     0.18           2.36       0.2
  robust (median/MAD)      0.27      0.08       0.29     0.21           2.33       0.5
  STL qoldig'i             0.98      1.00       0.96     0.98           1.28       1.5
  bashorat qoldig'i        0.88      0.94       0.92     0.91           1.35       8.5

=== 3. Kollektiv anomaliya: aniqlash kechikishi (soat) ===
  global z             topildi  2/24, kechikish mediana  5.0
  rolling z (24s)      topildi  1/24, kechikish mediana 10.0
  robust (median/MAD)  topildi  7/24, kechikish mediana 10.0
  STL qoldig'i         topildi 23/24, kechikish mediana  4.0
  bashorat qoldig'i    topildi 22/24, kechikish mediana  4.0

=== 4. Qaror: faqat REAL VAQTDA ishlay oladigan detektorlar ichida ===
  global z             - bashorat qoldig'i: -0.741, SE 0.062, sezilarli yomon: True
  rolling z (24s)      - bashorat qoldig'i: -0.731, SE 0.056, sezilarli yomon: True
  robust (median/MAD)  - bashorat qoldig'i: -0.694, SE 0.059, sezilarli yomon: True
  qaror (eng sodda munosib): bashorat qoldig'i
  STL (butun qatorni, ya'ni KELAJAKNI ham ko'radi) - bashorat qoldig'i: +0.074, SE 0.031 - faqat oflayn tahlil uchun
  ⭐ Kontekst (soat, hafta kuni) bilan solishtirmaguncha kontekstli anomaliya ko'rinmaydi

Natija tahlili.

1-bo'lim — kontekst raqamda: kun ichida 3-soatda o'rtacha 32.5 MW, 20-soatda 57.9 MW, umumiy std 9.1. Kontekstli anomaliya misolida qiymat 39.1 — umumiy o'rtachadan (43.4) ham past emas, lekin shu soatda odatda 57.2. Bayram kunlari iste'mol 34.9, oldingi haftaning shu soatlarida 42.3 — sezilarli farq, lekin bu kutilgan hodisa.

2-bo'lim — 6 ta qator, har detektorga bir xil yolg'on signal byudjeti (kalibrlashda haftasiga ~1). Kontekstsiz detektorlar deyarli ko'r: global z kontekstli anomaliyalarning birortasini topmadi (0.00), nuqtali sakrashlarning faqat 0.33 qismini — kun ichidagi tebranish (std 9.1) 12-20 MW lik sakrashni "yashiradi". Rolling va robust variantlar ham oxirgi 24 soat bilan solishtiradi — bu oynada kechki cho'qqi ham, tungi pastlik ham bor, shuning uchun ular ham kontekstni bilmaydi (hammasi 0.21 gacha). Kontekstni biladigan ikki detektor boshqa sinfda: bashorat qoldig'i 0.91, STL qoldig'i 0.98. Bayram ustuni muhim: bashorat qoldig'i bayram kunlarida o'rtacha 8.5 ta signal berdi — "o'tgan 4 hafta" bayramni bilmaydi. Real tizimda bayram taqvimi modelga belgi sifatida kiradi (28.2-28.3).

3-bo'lim — kollektiv anomaliya (sensor qotishi). Qotgan qiymat dastlab normal — farq faqat kunlik profil qiymatni "tark etganda" paydo bo'ladi, shuning uchun eng yaxshi detektorlar ham mediana 4 soat kechikish bilan topadi. Kontekstsiz detektorlar 24 hodisadan faqat 1-7 tasini topdi. Kechikish — kollektiv anomaliyaning tabiiy narxi; uni ogohlantirish qoidasini loyihalashda 27.11-bob hisobga olish kerak.

4-bo'lim — qaror. STL butun qatorni — ya'ni kelajakni ham — ko'radi, shuning uchun real vaqt detektori sifatida solishtirilmaydi. Sababiy detektorlar ichida bashorat qoldig'i qolgan uchtasidan 0.69-0.74 ga yaxshi (SE ~0.06) va qaror qoidasi bo'yicha tanlandi. STL undan yana +0.074 (SE 0.031) yaxshi — kelajakni ko'rishning "bonusi"; u oflayn tahlil (masalan, o'tgan oy hisobotida anomaliyalarni belgilash) uchun ajoyib vosita.

Misol 2 — Firibgarlik: shaxsiy bazaviy, nazoratsiz va nazoratli usullar

Generator 1200 mijoz uchun 180 kunlik karta tranzaksiyalarini yaratadi: har mijozning odatiy summasi, soati, shahri, kategoriyalari; 3% sayohat, 1.5% yangi qurilma — normal xulqda ham. Firibgarlik — akkaunt egallash epizodlari: bir necha soat ichida 1-5 ta tranzaksiya, ko'pincha katta summa, tunda, yangi qurilma yoki boshqa shahardan, lekin har doim emas. Har firibga tasdiqlanish kechikishi (chargeback, o'rtacha ~23 kun) ham beriladi.

python
"""Firibgarlik: shaxsiy bazaviy belgilar, vaqt bo'yicha bo'lish, nazoratsiz vs nazoratli."""

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier, IsolationForest
from sklearn.metrics import average_precision_score, roc_auc_score, roc_curve

KATEG = ["oziq", "transport", "restoran", "kiyim", "elektronika", "kommunal",
         "otkazma", "onlayn"]


def tranzaksiyalar(seed=0, n_mijoz=1200, kunlar=180):
    """Bank kartasi tranzaksiyalari; firibgarlik - akkaunt egallash epizodlari (~0.3%)."""
    rng = np.random.default_rng(seed)
    tezlik = rng.lognormal(np.log(0.45), 0.5, n_mijoz)          # kuniga tranzaksiya
    odat_summa = rng.lognormal(11.3, 0.6, n_mijoz)               # ~80 ming so'm
    odat_soat = rng.uniform(9, 20, n_mijoz)
    uy = rng.integers(0, 6, n_mijoz)
    kat_pref = rng.dirichlet(np.full(len(KATEG), 0.7), n_mijoz)
    kat_pref[:, 4] *= 0.3                                        # elektronika kam
    kat_pref /= kat_pref.sum(1, keepdims=True)
    soni = rng.poisson(tezlik * kunlar)
    m = np.repeat(np.arange(n_mijoz), soni)
    n = len(m)
    vaqt = rng.uniform(0, kunlar, n)
    kat = (rng.random(n)[:, None] > np.cumsum(kat_pref[m], 1)).sum(1)
    kat_koef = np.array([0.6, 0.3, 1.0, 2.0, 4.0, 1.2, 2.5, 1.5])
    summa = odat_summa[m] * kat_koef[kat] * rng.lognormal(0, 0.6, n)
    soat = np.mod(odat_soat[m] + rng.normal(0, 3, n), 24)
    safar = rng.random(n) < 0.03
    shahar = np.where(safar, (uy[m] + rng.integers(1, 6, n)) % 6, uy[m])
    qurilma = np.where(rng.random(n) < 0.015, rng.integers(1, 1000, n), 0)
    firib = np.zeros(n, dtype=bool)
    # firibgarlik epizodlari: bir necha soat ichida 1-5 ta tranzaksiya
    ep = int(n * 0.003 / 2.6)
    em = rng.integers(0, n_mijoz, ep)
    et = rng.uniform(0, kunlar - 1, ep)
    ek = rng.integers(1, 6, ep)
    fm = np.repeat(em, ek)
    fv = np.repeat(et, ek) + rng.uniform(0, 0.25, ek.sum())
    nf = len(fm)
    fk = rng.choice(len(KATEG), nf, p=[0.05, 0.02, 0.05, 0.1, 0.35, 0.03, 0.25, 0.15])
    fs = odat_summa[fm] * kat_koef[fk] * rng.lognormal(np.log(1.8), 0.8, nf)
    fsoat = np.where(rng.random(nf) < 0.45, rng.uniform(0, 6, nf),
                     np.mod(odat_soat[fm] + rng.normal(0, 5, nf), 24))
    fsh = np.where(rng.random(nf) < 0.5, (uy[fm] + rng.integers(1, 6, nf)) % 6, uy[fm])
    fq = np.where(rng.random(nf) < 0.7, rng.integers(1000, 2000, nf), 0)
    df = pd.DataFrame({
        "mijoz": np.r_[m, fm], "vaqt": np.r_[vaqt, fv], "summa": np.r_[summa, fs],
        "soat": np.r_[soat, fsoat], "kategoriya": np.r_[kat, fk],
        "shahar": np.r_[shahar, fsh], "qurilma": np.r_[qurilma, fq],
        "firib": np.r_[firib, np.ones(nf, dtype=bool)].astype(int)})
    kechikish = 3 + rng.exponential(20, len(df))                # chargeback kelishi (kun)
    df["belgi_kuni"] = np.where(df["firib"] == 1, df["vaqt"] + kechikish, df["vaqt"])
    return df.sort_values(["mijoz", "vaqt"]).reset_index(drop=True)


def belgilar(df, sizish=False):
    """Shaxsiy bazaviy: har tranzaksiya mijozning FAQAT O'TMISHI bilan solishtiriladi."""
    df = df.copy()
    g = df.groupby("mijoz")
    ls = np.log(df["summa"])
    df["log_summa"] = ls
    if sizish:                                  # XATO: butun davr (kelajak ham) ishlatiladi
        o = ls.groupby(df["mijoz"]).transform("mean")
        s = ls.groupby(df["mijoz"]).transform("std")
        os_ = g["soat"].transform("mean")
    else:
        n_old = g.cumcount()
        yig = ls.groupby(df["mijoz"]).cumsum() - ls
        yig2 = (ls ** 2).groupby(df["mijoz"]).cumsum() - ls ** 2
        o = yig / n_old.replace(0, np.nan)
        s = np.sqrt((yig2 / n_old.replace(0, np.nan) - o ** 2).clip(lower=0.01))
        os_ = (df["soat"].groupby(df["mijoz"]).cumsum() - df["soat"]) / n_old.replace(0, np.nan)
    df["summa_z"] = ((ls - o) / s).fillna(0)
    df["soat_farq"] = (df["soat"] - os_).abs().fillna(0)
    df["yangi_shahar"] = (df.groupby(["mijoz", "shahar"]).cumcount() == 0).astype(int)
    df["yangi_qurilma"] = (df.groupby(["mijoz", "qurilma"]).cumcount() == 0).astype(int)
    birinchi = g.cumcount() == 0
    df.loc[birinchi, ["yangi_shahar", "yangi_qurilma"]] = 0
    kalit = df["mijoz"].to_numpy() * 1000.0 + df["vaqt"].to_numpy()
    df["soni_24s"] = np.arange(len(df)) - np.searchsorted(kalit, kalit - 1.0)
    df["oraliq"] = np.log1p(g["vaqt"].diff().fillna(30).to_numpy() * 24)
    df["tarix_soni"] = g.cumcount()
    return df


XOM = ["log_summa", "soat", "kategoriya"]
SHAXSIY = ["log_summa", "soat", "kategoriya", "summa_z", "soat_farq", "yangi_shahar",
           "yangi_qurilma", "soni_24s", "oraliq", "tarix_soni"]


def ap_vazn(y, w, tartib):
    """Og'irlikli average precision: bootstrap da kun necha marta tanlangan bo'lsa."""
    y, w = y[tartib], w[tartib].astype(float)
    tp = np.cumsum(w * y)
    precision = tp / np.maximum(np.cumsum(w), 1e-12)
    return float(np.sum(w * y * precision) / max(tp[-1], 1e-12))


def main() -> None:
    xom = tranzaksiyalar()
    df = belgilar(xom)
    print("=== 1. Ma'lumot ===")
    print(f"  {len(df)} tranzaksiya, {df['mijoz'].nunique()} mijoz, 180 kun; "
          f"firibgarlik {int(df['firib'].sum())} ta ({df['firib'].mean():.2%})")
    f, n = df[df["firib"] == 1], df[df["firib"] == 0]
    print(f"  summa mediana: normal {n['summa'].median():,.0f}, firib {f['summa'].median():,.0f} so'm")
    print(f"  firib ichida: tungi (0-6) {np.mean(f['soat'] < 6):.0%}, yangi qurilma "
          f"{f['yangi_qurilma'].mean():.0%}, yangi shahar {f['yangi_shahar'].mean():.0%}")
    print(f"  normal ichida: tungi {np.mean(n['soat'] < 6):.0%}, yangi qurilma "
          f"{n['yangi_qurilma'].mean():.1%}, yangi shahar {n['yangi_shahar'].mean():.1%}")

    tr, te = df[df["vaqt"] < 120], df[df["vaqt"] >= 120]
    y_tr, y_te = tr["firib"].to_numpy(), te["firib"].to_numpy()
    print(f"  vaqt bo'yicha: train 0-119 kun ({len(tr)}, firib {y_tr.sum()}), "
          f"test 120-179 ({len(te)}, firib {y_te.sum()})")

    print("\n=== 2. Usullar (test, 60 kun) ===")
    ballar = {}
    for nom, cols in [("IF xom belgilar", XOM), ("IF shaxsiy belgilar", SHAXSIY)]:
        m = IsolationForest(n_estimators=100, random_state=0, n_jobs=1).fit(tr[cols])
        ballar[nom] = -m.score_samples(te[cols])
    m = IsolationForest(n_estimators=100, random_state=0, n_jobs=1).fit(
        tr.loc[tr["firib"] == 0, SHAXSIY])
    ballar["IF faqat normal (yarim)"] = -m.score_samples(te[SHAXSIY])
    for nom, cols, vazn in [("HistGB xom, balanced", XOM, "balanced"),
                            ("HistGB", SHAXSIY, None),
                            ("HistGB balanced", SHAXSIY, "balanced")]:
        m = HistGradientBoostingClassifier(max_iter=80, learning_rate=0.1, max_leaf_nodes=15,
                                           early_stopping=False, class_weight=vazn,
                                           random_state=0)
        ballar[nom] = m.fit(tr[cols], y_tr).predict_proba(te[cols])[:, 1]
    print(f"  {'usul':<24} {'ROC-AUC':>8} {'PR-AUC':>7} {'P@100':>6}")
    print(f"  {'tasodifiy (nazariy)':<24} {0.5:>8.4f} {y_te.mean():>7.4f} {y_te.mean():>6.3f}")
    for nom, b in ballar.items():
        top = np.argsort(-b, kind="stable")[:100]
        print(f"  {nom:<24} {roc_auc_score(y_te, b):>8.4f} "
              f"{average_precision_score(y_te, b):>7.4f} {y_te[top].mean():>6.3f}")
    print("\n=== 3. ROC-AUC nega aldaydi (IF shaxsiy belgilar) ===")
    b = ballar["IF shaxsiy belgilar"]
    fpr, tpr, chegara = roc_curve(y_te, b)
    for maqsad in [0.01, 0.05]:
        i = np.searchsorted(fpr, maqsad)
        tp, fp = tpr[i] * y_te.sum(), fpr[i] * (len(y_te) - y_te.sum())
        print(f"  FPR {maqsad:.0%}: firiblarning {tpr[i]:.0%} i topildi ({tp:.0f} ta), "
              f"yolg'on signal {fp:.0f} ta -> precision {tp / (tp + fp):.1%}")
    print(f"  ROC-AUC {roc_auc_score(y_te, b):.3f} 'yaxshi' ko'rinadi, lekin signallarning "
          f"aksariyati yolg'on - PR-AUC {average_precision_score(y_te, b):.3f}")

    print("\n=== 4. Juftlashgan bootstrap (kunlar bo'yicha, 200 marta), PR-AUC ===")
    kun = te["vaqt"].astype(int).to_numpy() - 120
    rng = np.random.default_rng(0)
    juftlar = [("HistGB balanced", "HistGB"), ("HistGB balanced", "IF shaxsiy belgilar"),
               ("IF shaxsiy belgilar", "IF faqat normal (yarim)")]
    tartib = {nom: np.argsort(-b, kind="stable") for nom, b in ballar.items()}
    for a, c in juftlar:
        d = []
        for _ in range(200):
            w = np.bincount(rng.integers(0, 60, 60), minlength=60)[kun]   # kun vazni
            d.append(ap_vazn(y_te, w, tartib[a]) - ap_vazn(y_te, w, tartib[c]))
        nuqta = (average_precision_score(y_te, ballar[a])
                 - average_precision_score(y_te, ballar[c]))
        se = np.std(d, ddof=1)
        print(f"  {a} - {c}: {nuqta:+.4f}, SE {se:.4f}, sezilarli: {abs(nuqta) > 2 * se}")
    print("  ⭐ Yorliq bo'lsa - nazoratli model; baholash - PR-AUC va top-k")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ma'lumot ===
  107685 tranzaksiya, 1200 mijoz, 180 kun; firibgarlik 388 ta (0.36%)
  summa mediana: normal 93,548, firib 335,491 so'm
  firib ichida: tungi (0-6) 48%, yangi qurilma 68%, yangi shahar 36%
  normal ichida: tungi 3%, yangi qurilma 1.5%, yangi shahar 2.2%
  vaqt bo'yicha: train 0-119 kun (71885, firib 244), test 120-179 (35800, firib 144)

=== 2. Usullar (test, 60 kun) ===
  usul                      ROC-AUC  PR-AUC  P@100
  tasodifiy (nazariy)        0.5000  0.0040  0.004
  IF xom belgilar            0.7779  0.0707  0.150
  IF shaxsiy belgilar        0.9670  0.4908  0.620
  IF faqat normal (yarim)    0.9661  0.4877  0.590
  HistGB xom, balanced       0.8503  0.1761  0.300
  HistGB                     0.8597  0.2162  0.310
  HistGB balanced            0.9847  0.7152  0.830

=== 3. ROC-AUC nega aldaydi (IF shaxsiy belgilar) ===
  FPR 1%: firiblarning 68% i topildi (98 ta), yolg'on signal 367 ta -> precision 21.1%
  FPR 5%: firiblarning 85% i topildi (123 ta), yolg'on signal 1787 ta -> precision 6.4%
  ROC-AUC 0.967 'yaxshi' ko'rinadi, lekin signallarning aksariyati yolg'on - PR-AUC 0.491

=== 4. Juftlashgan bootstrap (kunlar bo'yicha, 200 marta), PR-AUC ===
  HistGB balanced - HistGB: +0.4990, SE 0.0375, sezilarli: True
  HistGB balanced - IF shaxsiy belgilar: +0.2245, SE 0.0347, sezilarli: True
  IF shaxsiy belgilar - IF faqat normal (yarim): +0.0031, SE 0.0138, sezilarli: False
  ⭐ Yorliq bo'lsa - nazoratli model; baholash - PR-AUC va top-k

Natija tahlili.

1-bo'lim — 107685 tranzaksiya, firib 388 ta (0.36%). Firiblar o'rtacha kattaroq (mediana 335,491 va 93,548 so'm), ko'pincha tunda (48% va 3%) va yangi qurilmadan (68% va 1.5%) — lekin har bir belgi alohida olinganda aniq ajratmaydi: normal tranzaksiyalar orasida ham tungi, sayohatdagi va katta xaridlar bor. Bo'lish vaqt bo'yicha: 0-119 kun train, 120-179 test.

2-bo'lim — belgilar sifati hal qiladi. IsolationForest xom belgilarda (summa, soat, kategoriya) PR-AUC 0.0707 — "g'ayrioddiy summa" ko'p mijoz uchun oddiy. Shaxsiy bazaviy belgilar bilan xuddi shu model 0.4908 — yetti barobar yaxshi. Nazoratli model ham xuddi shunday: xom belgilarda 0.1761, shaxsiy belgilarda 0.7152. Sinf og'irligi muhim chiqdi: og'irliksiz HistGB atigi 0.2162 — 0.36% musbat sinfda boosting beqaror va noto'g'ri tomonga moslashadi; class_weight="balanced" bilan 0.7152 va top-100 da 0.83 precision. Yarim nazoratli variant (IF faqat normal tranzaksiyalarda o'qitilgan) oddiy IF bilan deyarli bir xil (0.4877 va 0.4908): train dagi firiblar ulushi shunchalik kichikki, ular IF ni "ifloslantirmaydi".

3-bo'lim — ROC-AUC nega aldaydi. IF (shaxsiy) ROC-AUC 0.967 — "a'lo" ko'rinadi. Lekin FPR 1% da, ya'ni normal tranzaksiyalarning atigi 1% ida signal bersa ham: 98 ta firib topiladi, yolg'on signal esa 367 ta — precision 21.1%. FPR 5% da precision 6.4%. ROC-AUC normal sinfning kattaligini "ko'rmaydi"; PR-AUC (0.491) haqiqiy manzarani beradi.

4-bo'lim — juftlashgan bootstrap kunlar bo'yicha (bir kun ichidagi tranzaksiyalar bog'liq — bitta epizod bir kunda). HistGB balanced og'irliksiz variantdan +0.4990 (SE 0.0375) va IF dan +0.2245 (SE 0.0347) yaxshi — ikkalasi ham sezilarli. IF va yarim nazoratli IF farqi +0.0031 (SE 0.0138) — sezilarli emas. Xulosa: yorliqlar bo'lsa, nazoratli model; nazoratsiz model esa yorliq yo'q joyda va "yangi tur" signali sifatida.

Misol 3 — Amaliyotda: belgi kechikishi, kunlik sig'im va xarajatli chegara

python
"""Firibgarlik amaliyotda: belgi kechikishi, kunlik sig'im (top-k) va xarajatli chegara."""

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier, IsolationForest
from sklearn.metrics import average_precision_score

KATEG = ["oziq", "transport", "restoran", "kiyim", "elektronika", "kommunal",
         "otkazma", "onlayn"]


def tranzaksiyalar(seed=0, n_mijoz=1200, kunlar=180):
    """Bank kartasi tranzaksiyalari; firibgarlik - akkaunt egallash epizodlari (~0.3%)."""
    rng = np.random.default_rng(seed)
    tezlik = rng.lognormal(np.log(0.45), 0.5, n_mijoz)          # kuniga tranzaksiya
    odat_summa = rng.lognormal(11.3, 0.6, n_mijoz)               # ~80 ming so'm
    odat_soat = rng.uniform(9, 20, n_mijoz)
    uy = rng.integers(0, 6, n_mijoz)
    kat_pref = rng.dirichlet(np.full(len(KATEG), 0.7), n_mijoz)
    kat_pref[:, 4] *= 0.3                                        # elektronika kam
    kat_pref /= kat_pref.sum(1, keepdims=True)
    soni = rng.poisson(tezlik * kunlar)
    m = np.repeat(np.arange(n_mijoz), soni)
    n = len(m)
    vaqt = rng.uniform(0, kunlar, n)
    kat = (rng.random(n)[:, None] > np.cumsum(kat_pref[m], 1)).sum(1)
    kat_koef = np.array([0.6, 0.3, 1.0, 2.0, 4.0, 1.2, 2.5, 1.5])
    summa = odat_summa[m] * kat_koef[kat] * rng.lognormal(0, 0.6, n)
    soat = np.mod(odat_soat[m] + rng.normal(0, 3, n), 24)
    safar = rng.random(n) < 0.03
    shahar = np.where(safar, (uy[m] + rng.integers(1, 6, n)) % 6, uy[m])
    qurilma = np.where(rng.random(n) < 0.015, rng.integers(1, 1000, n), 0)
    firib = np.zeros(n, dtype=bool)
    # firibgarlik epizodlari: bir necha soat ichida 1-5 ta tranzaksiya
    ep = int(n * 0.003 / 2.6)
    em = rng.integers(0, n_mijoz, ep)
    et = rng.uniform(0, kunlar - 1, ep)
    ek = rng.integers(1, 6, ep)
    fm = np.repeat(em, ek)
    fv = np.repeat(et, ek) + rng.uniform(0, 0.25, ek.sum())
    nf = len(fm)
    fk = rng.choice(len(KATEG), nf, p=[0.05, 0.02, 0.05, 0.1, 0.35, 0.03, 0.25, 0.15])
    fs = odat_summa[fm] * kat_koef[fk] * rng.lognormal(np.log(1.8), 0.8, nf)
    fsoat = np.where(rng.random(nf) < 0.45, rng.uniform(0, 6, nf),
                     np.mod(odat_soat[fm] + rng.normal(0, 5, nf), 24))
    fsh = np.where(rng.random(nf) < 0.5, (uy[fm] + rng.integers(1, 6, nf)) % 6, uy[fm])
    fq = np.where(rng.random(nf) < 0.7, rng.integers(1000, 2000, nf), 0)
    df = pd.DataFrame({
        "mijoz": np.r_[m, fm], "vaqt": np.r_[vaqt, fv], "summa": np.r_[summa, fs],
        "soat": np.r_[soat, fsoat], "kategoriya": np.r_[kat, fk],
        "shahar": np.r_[shahar, fsh], "qurilma": np.r_[qurilma, fq],
        "firib": np.r_[firib, np.ones(nf, dtype=bool)].astype(int)})
    kechikish = 3 + rng.exponential(20, len(df))                # chargeback kelishi (kun)
    df["belgi_kuni"] = np.where(df["firib"] == 1, df["vaqt"] + kechikish, df["vaqt"])
    return df.sort_values(["mijoz", "vaqt"]).reset_index(drop=True)


def belgilar(df, sizish=False):
    """Shaxsiy bazaviy: har tranzaksiya mijozning FAQAT O'TMISHI bilan solishtiriladi."""
    df = df.copy()
    g = df.groupby("mijoz")
    ls = np.log(df["summa"])
    df["log_summa"] = ls
    if sizish:                                  # XATO: butun davr (kelajak ham) ishlatiladi
        o = ls.groupby(df["mijoz"]).transform("mean")
        s = ls.groupby(df["mijoz"]).transform("std")
        os_ = g["soat"].transform("mean")
    else:
        n_old = g.cumcount()
        yig = ls.groupby(df["mijoz"]).cumsum() - ls
        yig2 = (ls ** 2).groupby(df["mijoz"]).cumsum() - ls ** 2
        o = yig / n_old.replace(0, np.nan)
        s = np.sqrt((yig2 / n_old.replace(0, np.nan) - o ** 2).clip(lower=0.01))
        os_ = (df["soat"].groupby(df["mijoz"]).cumsum() - df["soat"]) / n_old.replace(0, np.nan)
    df["summa_z"] = ((ls - o) / s).fillna(0)
    df["soat_farq"] = (df["soat"] - os_).abs().fillna(0)
    df["yangi_shahar"] = (df.groupby(["mijoz", "shahar"]).cumcount() == 0).astype(int)
    df["yangi_qurilma"] = (df.groupby(["mijoz", "qurilma"]).cumcount() == 0).astype(int)
    birinchi = g.cumcount() == 0
    df.loc[birinchi, ["yangi_shahar", "yangi_qurilma"]] = 0
    kalit = df["mijoz"].to_numpy() * 1000.0 + df["vaqt"].to_numpy()
    df["soni_24s"] = np.arange(len(df)) - np.searchsorted(kalit, kalit - 1.0)
    df["oraliq"] = np.log1p(g["vaqt"].diff().fillna(30).to_numpy() * 24)
    df["tarix_soni"] = g.cumcount()
    return df


XOM = ["log_summa", "soat", "kategoriya"]
SHAXSIY = ["log_summa", "soat", "kategoriya", "summa_z", "soat_farq", "yangi_shahar",
           "yangi_qurilma", "soni_24s", "oraliq", "tarix_soni"]


def hgb(X, y):
    return HistGradientBoostingClassifier(max_iter=80, learning_rate=0.1, max_leaf_nodes=15,
                                          early_stopping=False, class_weight="balanced",
                                          random_state=0).fit(X, y)


def ap_vazn(y, w, tartib):
    """Og'irlikli average precision (kunlar bo'yicha bootstrap uchun)."""
    y, w = y[tartib], w[tartib].astype(float)
    tp = np.cumsum(w * y)
    return float(np.sum(w * y * tp / np.maximum(np.cumsum(w), 1e-12)) / max(tp[-1], 1e-12))


def kunlik_top(ball, kun, y, n):
    """Har kuni eng shubhali n ta tekshiriladi: kunlik precision va topilganlar."""
    prec, top_soni = [], []
    for d in np.unique(kun):
        i = np.flatnonzero(kun == d)
        tanlandi = i[np.argsort(-ball[i], kind="stable")[:n]]
        prec.append(y[tanlandi].mean())
        top_soni.append(y[tanlandi].sum())
    return np.array(prec), np.array(top_soni)


def xarajat(ball, chegara, y, summa, fp_narx=20_000):
    """FN - firib summasi yo'qotiladi; har signal (TP yoki FP) - tekshirish narxi."""
    signal = ball >= chegara
    return float(summa[(y == 1) & ~signal].sum() + signal.sum() * fp_narx)


def main() -> None:
    df = belgilar(tranzaksiyalar())
    T = 120                                          # o'qitish kuni
    tr, te = df[df["vaqt"] < T], df[df["vaqt"] >= T]
    y_te = te["firib"].to_numpy()
    kun = te["vaqt"].astype(int).to_numpy()

    print("=== 1. Belgi kechikishi: 120-kunda qaysi firiblar MA'LUM? ===")
    malum = tr["belgi_kuni"] < T
    f = tr["firib"] == 1
    print(f"  train dagi firib: {int(f.sum())}, 120-kungacha tasdiqlangani: "
          f"{int((f & malum).sum())}")
    for a, b in [(0, 60), (60, 90), (90, 105), (105, 120)]:
        m = f & (tr["vaqt"] >= a) & (tr["vaqt"] < b)
        print(f"   {a:>3}-{b:<3} kun: firib {int(m.sum()):>3}, ma'lum {(m & malum).sum() / m.sum():.0%}")
    strategiyalar = {
        "ideal (hamma yorliq)": (tr, tr["firib"]),
        "sodda (noma'lum = 0)": (tr, (tr["firib"] == 1) & malum),
        "faqat yetilgan (<75)": (tr[tr["vaqt"] < 75], tr.loc[tr["vaqt"] < 75, "firib"]),
    }
    ballar = {}
    for nom, (qism, y) in strategiyalar.items():
        ballar[nom] = hgb(qism[SHAXSIY], y.astype(int)).predict_proba(te[SHAXSIY])[:, 1]
        print(f"  {nom:<22} o'qitish firiblari {int(y.sum()):>3}, test PR-AUC "
              f"{average_precision_score(y_te, ballar[nom]):.4f}")
    rng = np.random.default_rng(0)
    tartib = {n: np.argsort(-b, kind="stable") for n, b in ballar.items()}
    nomlar = list(ballar)
    for a, c in [(nomlar[1], nomlar[0]), (nomlar[2], nomlar[1])]:
        d = []
        for _ in range(200):
            w = np.bincount(rng.integers(0, 60, 60), minlength=60)[kun - T]
            d.append(ap_vazn(y_te, w, tartib[a]) - ap_vazn(y_te, w, tartib[c]))
        nuqta = average_precision_score(y_te, ballar[a]) - average_precision_score(y_te, ballar[c])
        print(f"  {a} - {c}: {nuqta:+.4f}, SE {np.std(d, ddof=1):.4f}")

    print("\n=== 2. Kunlik sig'im: tekshiruvchilar kuniga n ta ko'radi ===")
    ifm = IsolationForest(n_estimators=100, random_state=0, n_jobs=1).fit(tr[SHAXSIY])
    usul = {"qoida (summa_z>2 va yangi qurilma)":
            (te["summa_z"] > 2).to_numpy() * 2.0 + te["yangi_qurilma"].to_numpy()
            + 0.01 * te["summa_z"].to_numpy(),
            "IF shaxsiy": -ifm.score_samples(te[SHAXSIY]),
            "HistGB (yetilgan)": ballar["faqat yetilgan (<75)"]}
    print(f"  test: 60 kun, kuniga o'rtacha {len(te) / 60:.0f} tranzaksiya, "
          f"{y_te.sum() / 60:.1f} firib")
    print(f"  {'usul':<36}" + "".join(f"{f'n={n}':>14}" for n in [5, 10, 20]))
    kunlik = {}
    for nom, b in usul.items():
        qator = []
        for n in [5, 10, 20]:
            p, s = kunlik_top(b, kun, y_te, n)
            kunlik[(nom, n)] = s
            qator.append(f"{p.mean():.2f}/{s.sum() / y_te.sum():.0%}")
        print(f"  {nom:<36}" + "".join(f"{q:>14}" for q in qator))
    print("  (kunlik precision / topilgan firiblar ulushi)")
    for a in ["qoida (summa_z>2 va yangi qurilma)", "IF shaxsiy"]:
        d = kunlik[("HistGB (yetilgan)", 10)] - kunlik[(a, 10)]
        se = d.std(ddof=1) / np.sqrt(len(d))
        print(f"  n=10: HistGB - {a}: kuniga {d.mean():+.2f} firib, SE {se:.2f}, "
              f"sezilarli: {abs(d.mean()) > 2 * se}")

    print("\n=== 3. Xarajatga asoslangan chegara (faraziy narxlar) ===")
    print("  FN: firib summasi yo'qoladi; har signal: 20 000 so'm (tekshirish, mijoz noqulayligi)")
    trv, va = df[df["vaqt"] < 90], df[(df["vaqt"] >= 90) & (df["vaqt"] < T)]
    b_va = hgb(trv[SHAXSIY], trv["firib"]).predict_proba(va[SHAXSIY])[:, 1]
    y_va, s_va = va["firib"].to_numpy(), va["summa"].to_numpy()
    nomzod = np.quantile(b_va, [0.9, 0.97, 0.99, 0.995, 0.998, 0.999])
    xv = [xarajat(b_va, c, y_va, s_va) for c in nomzod]
    eng = nomzod[int(np.argmin(xv))]
    print("  validatsiya (90-119 kun): " + ", ".join(
        f"{c:.3f}: {x / 1e6:.1f}" for c, x in zip(nomzod, xv)) + " (mln so'm)")
    print(f"  tanlangan chegara: {eng:.3f}")
    b = ballar["faqat yetilgan (<75)"]
    s_te = te["summa"].to_numpy()
    variantlar = {"hech narsa tekshirmaslik": np.inf, "chegara 0.5": 0.5,
                  "xarajat bo'yicha (val)": eng}
    for nom, c in variantlar.items():
        signal = b >= c
        print(f"  TEST {nom:<24} signal {int(signal.sum()):>5}, topildi "
              f"{int(y_te[signal].sum()):>3}/{int(y_te.sum())}, xarajat "
              f"{xarajat(b, c, y_te, s_te) / 1e6:>6.1f} mln")
    print("  ⭐ Chegara - biznes qarori: narxlar va sig'im bilan, validatsiyada")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Belgi kechikishi: 120-kunda qaysi firiblar MA'LUM? ===
  train dagi firib: 244, 120-kungacha tasdiqlangani: 178
     0-60  kun: firib 116, ma'lum 99%
    60-90  kun: firib  62, ma'lum 79%
    90-105 kun: firib  13, ma'lum 46%
   105-120 kun: firib  53, ma'lum 15%
  ideal (hamma yorliq)   o'qitish firiblari 244, test PR-AUC 0.7152
  sodda (noma'lum = 0)   o'qitish firiblari 178, test PR-AUC 0.7002
  faqat yetilgan (<75)   o'qitish firiblari 148, test PR-AUC 0.6931
  sodda (noma'lum = 0) - ideal (hamma yorliq): -0.0150, SE 0.0108
  faqat yetilgan (<75) - sodda (noma'lum = 0): -0.0071, SE 0.0156

=== 2. Kunlik sig'im: tekshiruvchilar kuniga n ta ko'radi ===
  test: 60 kun, kuniga o'rtacha 597 tranzaksiya, 2.4 firib
  usul                                           n=5          n=10          n=20
  qoida (summa_z>2 va yangi qurilma)        0.13/27%      0.08/34%      0.07/62%
  IF shaxsiy                                0.25/52%      0.17/69%      0.09/78%
  HistGB (yetilgan)                         0.30/62%      0.20/83%      0.10/87%
  (kunlik precision / topilgan firiblar ulushi)
  n=10: HistGB - qoida (summa_z>2 va yangi qurilma): kuniga +1.18 firib, SE 0.21, sezilarli: True
  n=10: HistGB - IF shaxsiy: kuniga +0.33 firib, SE 0.10, sezilarli: True

=== 3. Xarajatga asoslangan chegara (faraziy narxlar) ===
  FN: firib summasi yo'qoladi; har signal: 20 000 so'm (tekshirish, mijoz noqulayligi)
  validatsiya (90-119 kun): 0.011: 38.0, 0.081: 15.8, 0.334: 9.6, 0.697: 8.7, 0.983: 15.6, 0.994: 24.1 (mln so'm)
  tanlangan chegara: 0.697
  TEST hech narsa tekshirmaslik signal     0, topildi   0/144, xarajat   81.2 mln
  TEST chegara 0.5              signal   206, topildi 103/144, xarajat   25.9 mln
  TEST xarajat bo'yicha (val)   signal   169, topildi 100/144, xarajat   26.6 mln
  ⭐ Chegara - biznes qarori: narxlar va sig'im bilan, validatsiyada

Natija tahlili.

1-bo'lim — 120-kunda train dagi 244 firibdan faqat 178 tasi tasdiqlangan. Kechikish oxirgi haftalarda eng og'ir: 105-120 kunlardagi firiblarning faqat 15% i ma'lum. Uch strategiya: "ideal" (hamma yorliq — amalda imkonsiz, faqat taqqoslash uchun) PR-AUC 0.7152; "sodda" (noma'lum = normal) 0.7002; "faqat yetilgan" (75-kungacha) 0.6931. Farqlar kichik va bootstrap SE ichida (-0.0150, SE 0.0108; -0.0071, SE 0.0156). Bu ma'lumotda belgi kechikishi kichik zarar berdi: yetilmagan 66 ta firib class_weight bilan og'irligi oshirilgan 178 ta haqiqiy firib oldida kam. Lekin bu kafolat emas — firib turi tez o'zgaradigan davrda aynan eng yangi firiblar eng qimmatli va aynan ular yetilmagan. Qaysi strategiya yaxshi ekanini har safar o'lchash kerak; biz keyingi bo'limlarda xavfsizroq "faqat yetilgan" modelni ishlatamiz.

2-bo'lim — kunlik sig'im. Testda kuniga ~`597tranzaksiya va2.4firib. Tekshiruvchilar kunigan = 10ta ko'rsa: HistGB firiblarning83%ini topadi (kunlik precision0.20), IF — 69%, oddiy qoida ("summa_z > 2 va yangi qurilma") — 34%. Juftlashgan farq kunlar bo'yicha: HistGB qoidadan kuniga +1.18 firib (SE 0.21), IF dan +0.33 (SE 0.10) ko'proq topadi. nni 20 ga oshirish HistGB da qamrovni83%dan87%` ga oshiradi, precision esa yarmiga tushadi — sig'im va samaradorlik o'rtasidagi murosa.

3-bo'lim — xarajat (faraziy narxlar: o'tkazib yuborilgan firib — uning summasi, har signal — 20 000 so'm). Validatsiyada (90-119 kun; soddalik uchun yorliqlar yetilgan deb olingan) jami xarajat chegara 0.697 da eng kichik (8.7 mln). Testda hech narsa tekshirmaslik 81.2 mln yo'qotish beradi; chegara 0.5 — 25.9 mln, validatsiyada tanlangan 0.697 — 26.6 mln. Halol natija: tanlangan chegara testda 0.5 dan biroz yomon chiqdi — xarajat egri chizig'i minimum atrofida tekis, va val/test farqi shovqin ichida. Asosiy yutuq — chegaraning o'zida emas, tizimda: xarajat 81.2 mln dan ~26 mln ga tushdi. Chegarani narxlar asosida tanlash "0.5" ni tasodifan to'g'ri bo'lib chiqqanini ham tekshirish imkonini beradi.

Misol 4 — Avtoenkoder va IsolationForest: halol taqqoslash

Ikki ma'lumot: (1) uskuna sensorlari — 8 ustun 2 ta yashirin holatdan nochiziqli hosil qilingan, anomaliyada 2 ta ustun boshqa qatordan olinadi (har biri alohida normal, birgalikda mos emas); (2) 2-misoldagi tranzaksiyalar (shaxsiy belgilar, train dan 4000 qator). Har birida 4 urug', uchta nazoratsiz usul: IsolationForest, PCA (3 komponent) qayta tiklash xatosi va avtoenkoder (8 → 16 → 3 → 16 → 8, torch).

python
"""Avtoenkoder bilan anomaliya: IsolationForest va PCA bilan halol taqqoslash."""

import numpy as np
import pandas as pd
import torch
from sklearn.decomposition import PCA
from sklearn.ensemble import IsolationForest
from sklearn.metrics import average_precision_score
from sklearn.preprocessing import StandardScaler

KATEG = ["oziq", "transport", "restoran", "kiyim", "elektronika", "kommunal",
         "otkazma", "onlayn"]


def tranzaksiyalar(seed=0, n_mijoz=1200, kunlar=180):
    """Bank kartasi tranzaksiyalari; firibgarlik - akkaunt egallash epizodlari (~0.3%)."""
    rng = np.random.default_rng(seed)
    tezlik = rng.lognormal(np.log(0.45), 0.5, n_mijoz)          # kuniga tranzaksiya
    odat_summa = rng.lognormal(11.3, 0.6, n_mijoz)               # ~80 ming so'm
    odat_soat = rng.uniform(9, 20, n_mijoz)
    uy = rng.integers(0, 6, n_mijoz)
    kat_pref = rng.dirichlet(np.full(len(KATEG), 0.7), n_mijoz)
    kat_pref[:, 4] *= 0.3                                        # elektronika kam
    kat_pref /= kat_pref.sum(1, keepdims=True)
    soni = rng.poisson(tezlik * kunlar)
    m = np.repeat(np.arange(n_mijoz), soni)
    n = len(m)
    vaqt = rng.uniform(0, kunlar, n)
    kat = (rng.random(n)[:, None] > np.cumsum(kat_pref[m], 1)).sum(1)
    kat_koef = np.array([0.6, 0.3, 1.0, 2.0, 4.0, 1.2, 2.5, 1.5])
    summa = odat_summa[m] * kat_koef[kat] * rng.lognormal(0, 0.6, n)
    soat = np.mod(odat_soat[m] + rng.normal(0, 3, n), 24)
    safar = rng.random(n) < 0.03
    shahar = np.where(safar, (uy[m] + rng.integers(1, 6, n)) % 6, uy[m])
    qurilma = np.where(rng.random(n) < 0.015, rng.integers(1, 1000, n), 0)
    firib = np.zeros(n, dtype=bool)
    # firibgarlik epizodlari: bir necha soat ichida 1-5 ta tranzaksiya
    ep = int(n * 0.003 / 2.6)
    em = rng.integers(0, n_mijoz, ep)
    et = rng.uniform(0, kunlar - 1, ep)
    ek = rng.integers(1, 6, ep)
    fm = np.repeat(em, ek)
    fv = np.repeat(et, ek) + rng.uniform(0, 0.25, ek.sum())
    nf = len(fm)
    fk = rng.choice(len(KATEG), nf, p=[0.05, 0.02, 0.05, 0.1, 0.35, 0.03, 0.25, 0.15])
    fs = odat_summa[fm] * kat_koef[fk] * rng.lognormal(np.log(1.8), 0.8, nf)
    fsoat = np.where(rng.random(nf) < 0.45, rng.uniform(0, 6, nf),
                     np.mod(odat_soat[fm] + rng.normal(0, 5, nf), 24))
    fsh = np.where(rng.random(nf) < 0.5, (uy[fm] + rng.integers(1, 6, nf)) % 6, uy[fm])
    fq = np.where(rng.random(nf) < 0.7, rng.integers(1000, 2000, nf), 0)
    df = pd.DataFrame({
        "mijoz": np.r_[m, fm], "vaqt": np.r_[vaqt, fv], "summa": np.r_[summa, fs],
        "soat": np.r_[soat, fsoat], "kategoriya": np.r_[kat, fk],
        "shahar": np.r_[shahar, fsh], "qurilma": np.r_[qurilma, fq],
        "firib": np.r_[firib, np.ones(nf, dtype=bool)].astype(int)})
    kechikish = 3 + rng.exponential(20, len(df))                # chargeback kelishi (kun)
    df["belgi_kuni"] = np.where(df["firib"] == 1, df["vaqt"] + kechikish, df["vaqt"])
    return df.sort_values(["mijoz", "vaqt"]).reset_index(drop=True)


def belgilar(df, sizish=False):
    """Shaxsiy bazaviy: har tranzaksiya mijozning FAQAT O'TMISHI bilan solishtiriladi."""
    df = df.copy()
    g = df.groupby("mijoz")
    ls = np.log(df["summa"])
    df["log_summa"] = ls
    if sizish:                                  # XATO: butun davr (kelajak ham) ishlatiladi
        o = ls.groupby(df["mijoz"]).transform("mean")
        s = ls.groupby(df["mijoz"]).transform("std")
        os_ = g["soat"].transform("mean")
    else:
        n_old = g.cumcount()
        yig = ls.groupby(df["mijoz"]).cumsum() - ls
        yig2 = (ls ** 2).groupby(df["mijoz"]).cumsum() - ls ** 2
        o = yig / n_old.replace(0, np.nan)
        s = np.sqrt((yig2 / n_old.replace(0, np.nan) - o ** 2).clip(lower=0.01))
        os_ = (df["soat"].groupby(df["mijoz"]).cumsum() - df["soat"]) / n_old.replace(0, np.nan)
    df["summa_z"] = ((ls - o) / s).fillna(0)
    df["soat_farq"] = (df["soat"] - os_).abs().fillna(0)
    df["yangi_shahar"] = (df.groupby(["mijoz", "shahar"]).cumcount() == 0).astype(int)
    df["yangi_qurilma"] = (df.groupby(["mijoz", "qurilma"]).cumcount() == 0).astype(int)
    birinchi = g.cumcount() == 0
    df.loc[birinchi, ["yangi_shahar", "yangi_qurilma"]] = 0
    kalit = df["mijoz"].to_numpy() * 1000.0 + df["vaqt"].to_numpy()
    df["soni_24s"] = np.arange(len(df)) - np.searchsorted(kalit, kalit - 1.0)
    df["oraliq"] = np.log1p(g["vaqt"].diff().fillna(30).to_numpy() * 24)
    df["tarix_soni"] = g.cumcount()
    return df


XOM = ["log_summa", "soat", "kategoriya"]
SHAXSIY = ["log_summa", "soat", "kategoriya", "summa_z", "soat_farq", "yangi_shahar",
           "yangi_qurilma", "soni_24s", "oraliq", "tarix_soni"]


def sensor(seed, n=5000, ulush=0.02):
    """Uskuna sensorlari: 8 ustun 2 ta yashirin holatdan (burchak t, yuklama s) NOCHIZIQLI."""
    rng = np.random.default_rng(seed)
    t, s = rng.uniform(0, 2 * np.pi, n), rng.normal(0, 1, n)
    X = np.column_stack([np.cos(t), np.sin(t), np.cos(2 * t), np.sin(t) * s,
                         s, np.tanh(2 * s), 0.5 * np.cos(t) + 0.5 * s, np.exp(0.5 * s)])
    X += rng.normal(0, 0.05, X.shape)
    y = np.zeros(n, dtype=int)
    a = rng.choice(n, int(n * ulush), replace=False)
    for i in a:                                  # 2 ta ustun boshqa qatordan: har biri
        j = rng.choice(8, 2, replace=False)      # alohida normal, birga - g'ayrioddiy
        X[i, j] = X[rng.integers(0, n), j]
    y[a] = 1
    return X, y


def avtoenkoder(Xtr, Xte, seed, yashirin=3, qadam=600):
    torch.manual_seed(seed)
    torch.set_flush_denormal(True)
    d = Xtr.shape[1]
    model = torch.nn.Sequential(torch.nn.Linear(d, 16), torch.nn.Tanh(),
                                torch.nn.Linear(16, yashirin), torch.nn.Tanh(),
                                torch.nn.Linear(yashirin, 16), torch.nn.Tanh(),
                                torch.nn.Linear(16, d))
    opt = torch.optim.Adam(model.parameters(), lr=0.01)
    A = torch.tensor(Xtr, dtype=torch.float32)
    for _ in range(qadam):                       # to'liq partiya, MSE
        loss = ((model(A) - A) ** 2).mean()
        opt.zero_grad()
        loss.backward()
        opt.step()
    with torch.no_grad():
        B = torch.tensor(Xte, dtype=torch.float32)
        return ((model(B) - B) ** 2).mean(1).numpy(), float(loss)


def uch_usul(Xtr, Xte, seed):
    sk = StandardScaler().fit(Xtr)
    A, B = sk.transform(Xtr), sk.transform(Xte)
    b = {"IsolationForest": -IsolationForest(n_estimators=100, random_state=seed, n_jobs=1)
         .fit(Xtr).score_samples(Xte)}
    p = PCA(3, random_state=seed).fit(A)
    b["PCA (3) qayta tiklash"] = ((B - p.inverse_transform(p.transform(B))) ** 2).mean(1)
    b["avtoenkoder (3)"], _ = avtoenkoder(A, B, seed)
    return b


def main() -> None:
    print("=== 1. Avtoenkoder ichkarisi: qayta tiklash xatosi (sensor, urug' 0) ===")
    X, y = sensor(0)
    Xtr, Xte, yte = X[:3000], X[3000:], y[3000:]
    sk = StandardScaler().fit(Xtr)
    xato, loss = avtoenkoder(sk.transform(Xtr), sk.transform(Xte), 0)
    print(f"  8 ustun -> 3 yashirin -> 8 ustun; oxirgi train MSE {loss:.4f}")
    print(f"  test qayta tiklash xatosi: normal mediana {np.median(xato[yte == 0]):.4f}, "
          f"anomaliya mediana {np.median(xato[yte == 1]):.4f}")
    z = np.abs(StandardScaler().fit(Xtr).transform(Xte))
    print(f"  alohida ustunlarda |z| > 3 bo'lgan anomaliyalar: "
          f"{np.mean(z[yte == 1].max(1) > 3):.0%} (normal: {np.mean(z[yte == 0].max(1) > 3):.0%})")

    print("\n=== 2. Ikki ma'lumot, 4 urug', PR-AUC ===")
    natija = {}
    for seed in range(4):
        X, y = sensor(seed)
        natija.setdefault("sensor (nochiziqli bog'liqlik)", []).append(
            (uch_usul(X[:3000], X[3000:], seed), y[3000:]))
        df = belgilar(tranzaksiyalar(seed=seed, n_mijoz=400))
        tr, te = df[df["vaqt"] < 120], df[df["vaqt"] >= 120]
        tr = tr.sample(4000, random_state=seed)          # kichik jadval: 4000 qator
        natija.setdefault("tranzaksiyalar (shaxsiy belgilar)", []).append(
            (uch_usul(tr[SHAXSIY].to_numpy(float), te[SHAXSIY].to_numpy(float), seed),
             te["firib"].to_numpy()))
    tartib = ["IsolationForest", "PCA (3) qayta tiklash", "avtoenkoder (3)"]
    for nom, qatorlar in natija.items():
        pr = {u: np.array([average_precision_score(yy, b[u]) for b, yy in qatorlar])
              for u in tartib}
        print(f"  {nom}: anomaliya ulushi {np.mean([yy.mean() for _, yy in qatorlar]):.2%}")
        for u in tartib:
            print(f"    {u:<24} PR-AUC {pr[u].mean():.4f}  (urug'lar: "
                  f"{', '.join(f'{v:.3f}' for v in pr[u])})")
        eng = max(tartib, key=lambda u: pr[u].mean())
        tanlov = None
        for u in tartib:
            d = pr[u] - pr[eng]
            se = d.std(ddof=1) / np.sqrt(len(d))
            if u != eng:
                print(f"    {u} - {eng}: {d.mean():+.4f}, SE {se:.4f}")
            if tanlov is None and d.mean() >= -2 * se:
                tanlov = u
        print(f"    qaror (eng sodda munosib): {tanlov}")
    print("  ⭐ Avtoenkoder - murakkab bog'liqlik bo'lsa; oddiy jadvalda IF ko'pincha yetarli")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Avtoenkoder ichkarisi: qayta tiklash xatosi (sensor, urug' 0) ===
  8 ustun -> 3 yashirin -> 8 ustun; oxirgi train MSE 0.0226
  test qayta tiklash xatosi: normal mediana 0.0123, anomaliya mediana 0.2606
  alohida ustunlarda |z| > 3 bo'lgan anomaliyalar: 3% (normal: 2%)

=== 2. Ikki ma'lumot, 4 urug', PR-AUC ===
  sensor (nochiziqli bog'liqlik): anomaliya ulushi 1.98%
    IsolationForest          PR-AUC 0.0365  (urug'lar: 0.031, 0.034, 0.047, 0.035)
    PCA (3) qayta tiklash    PR-AUC 0.0487  (urug'lar: 0.028, 0.047, 0.083, 0.035)
    avtoenkoder (3)          PR-AUC 0.6414  (urug'lar: 0.647, 0.699, 0.488, 0.732)
    IsolationForest - avtoenkoder (3): -0.6049, SE 0.0570
    PCA (3) qayta tiklash - avtoenkoder (3): -0.5928, SE 0.0647
    qaror (eng sodda munosib): avtoenkoder (3)
  tranzaksiyalar (shaxsiy belgilar): anomaliya ulushi 0.34%
    IsolationForest          PR-AUC 0.5288  (urug'lar: 0.620, 0.641, 0.355, 0.499)
    PCA (3) qayta tiklash    PR-AUC 0.1339  (urug'lar: 0.040, 0.060, 0.237, 0.199)
    avtoenkoder (3)          PR-AUC 0.1514  (urug'lar: 0.175, 0.021, 0.094, 0.315)
    PCA (3) qayta tiklash - IsolationForest: -0.3949, SE 0.1135
    avtoenkoder (3) - IsolationForest: -0.3774, SE 0.0978
    qaror (eng sodda munosib): IsolationForest
  ⭐ Avtoenkoder - murakkab bog'liqlik bo'lsa; oddiy jadvalda IF ko'pincha yetarli

Natija tahlili.

1-bo'lim — avtoenkoder sensor ma'lumotining nochiziqli tuzilmasini 3 o'lchamli yashirin fazoga siqdi: test da normal nuqtalarning qayta tiklash xatosi mediana 0.0123, anomaliyalarniki 0.2606 — yigirma barobardan ko'proq. Holbuki alohida ustunlar bo'yicha anomaliyalar deyarli ko'rinmaydi: |z| > 3 anomaliyalarning 3% ida, normallarning 2% ida.

2-bo'lim, sensor — aynan avtoenkoder uchun yaratilgan vazifa. IF (0.0365) va PCA (0.0487) deyarli tasodifiy darajada (anomaliya ulushi 1.98%): IF har ustunni alohida bo'ladi va marginal taqsimotlar normal; PCA chiziqli — aylana (cos t, sin t) va eksponenta kabi nochiziqli bog'liqlikni tiklay olmaydi. Avtoenkoder 0.6414 — IF dan +0.60 (SE 0.057). Qaror qoidasi avtoenkoderni tanladi — bu yerda murakkablik o'zini oqlaydi.

2-bo'lim, tranzaksiyalar — manzara teskari. IF 0.5288, PCA 0.1339, avtoenkoder 0.1514; avtoenkoder IF dan -0.3774 (SE 0.0978) — sezilarli yomon. Sabab: firib bir-ikki belgida kuchli chetga chiqish (katta summa_z, yangi qurilma, tungi soat) — IF buni bir necha bo'linishda ajratadi. Qayta tiklash xatosi esa hamma ustunlarning xatosini qo'shadi va tarix_soni, oraliq kabi "shovqinli", lekin firib bilan bog'liq bo'lmagan ustunlar xatoni to'ldiradi. Urug'lar orasidagi tarqoqlik ham avtoenkoderda kattaroq (0.021 dan 0.315 gacha) — u sozlamalarga (masshtab, epoxa, arxitektura) sezgir.

Darsning halol xulosasi: avtoenkoder "IF ning yangi, yaxshiroq versiyasi" emas. U ko'p o'lchovli nochiziqli bog'liqlik buzilganda kuchli; oddiy jadvalda, anomaliya alohida belgilarda ko'rinsa, IF sodda, tez va yaxshiroq. Ikkalasini har doim bir xil bo'lish va bir xil metrikada solishtiring.


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

Noto'g'ri fikr To'g'risi
"z-score > 3 — anomaliya" Kontekstsiz z kontekstli anomaliyani ko'rmaydi (1-misol: 0.00)
"STL eng yaxshi natija berdi — real vaqtda ishlatamiz" STL kelajakni ko'radi; real vaqtda sababiy bashorat qoldig'i
"Bayramdagi pasayish — anomaliya" Kutilgan o'zgarish; kontekst sifatida modelga kiradi
"Nuqta bo'yicha precision/recall yetarli" Hodisa recall, yolg'on signal/hafta va kechikish kerak
"ROC-AUC 0.97 — a'lo detektor" FPR 1% da precision 21% (2-misol); PR-AUC qarang
"Summa katta — firib" Shaxsiy bazaviysiz IF PR-AUC 0.07, bilan — 0.49
"Nomutanosiblikda nazoratli model ham avtomatik ishlaydi" Og'irliksiz HistGB 0.22, balanced bilan 0.72
"Chegara — 0.5" Chegara narxlar va sig'imdan kelib chiqadi
"Hozirgacha tasdiqlanmagan — demak normal" Belgi kechikishi: oxirgi haftadagi firiblarning 85% i hali noma'lum
"Avtoenkoder — IF dan zamonaviyroq, demak yaxshiroq" Oddiy jadvalda IF sezilarli yaxshi chiqdi (4-misol)

6. Keng tarqalgan xatolar va yechimlari

1. Kontekstsiz statistika

python
z = (x - x.mean()) / x.std()                                          # ⚠️
qoldiq = x - np.median([x.shift(168 * k) for k in range(1, 5)], axis=0)   # ✅ shu soat

2. Kelajakni ko'radigan detektor

python
q = STL(x, period=24).fit().resid           # ⚠️ real vaqt detektori sifatida
# real vaqtda: faqat o'tmishdan bashorat qoldig'i                   # ✅

3. Chegara "ko'z bilan"

python
signal = z > 3                                                        # ⚠️
chegara = np.nanquantile(ball[kalibrlash], 1 - 1 / 168)              # ✅ byudjet

4. Kelajakdan shaxsiy bazaviy

python
df["nisbat"] = df["summa"] / df.groupby("mijoz")["summa"].transform("median")  # ⚠️
yig = ls.groupby(df["mijoz"]).cumsum() - ls                          # ✅ faqat o'tmish

5. ROC-AUC bilan tanlash

python
eng = max(modellar, key=lambda m: roc_auc_score(y, m))                # ⚠️
eng = max(modellar, key=lambda m: average_precision_score(y, m))      # ✅ + top-k

6. Yetilmagan yorliqlar

python
y = df["firib_malum"].fillna(0)                                       # ⚠️ firib = 0
tr = df[df["vaqt"] < T - L]                                           # ✅ yetilgan davr

7. Sinf og'irligisiz nomutanosib boosting

python
HistGradientBoostingClassifier()                                      # ⚠️
HistGradientBoostingClassifier(class_weight="balanced")               # ✅ va PR-AUC da tekshiring

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 08.6, 16.10-darslar (o'tilgan): Anomaliya turlari, IsolationForest, LOF, PCA qayta tiklash, PR-AUC va precision@k
  • 14-qism, 18-qism (o'tilgan): Nomutanosib klassifikatsiya, PR egri chizig'i, chegara tanlash
  • 17.8-dars (o'tilgan): Leakage — shaxsiy bazaviy faqat o'tmishdan
  • 26.2-dars (o'tilgan): Avtoenkoder — qayta tiklash xatosi
  • 27.11-27.12-darslar (o'tilgan): Alerting byudjeti, drift va Page-Hinkley — oqimdagi o'zgarish aniqlash
  • 28.1-28.3-darslar (o'tilgan): Mavsum, bayram belgilari va bashorat modellari — bashorat qoldig'i shular ustida
  • 28.11-dars: Sababiy xulosa — "bu choralar firibni kamaytirdimi?" savoliga javob

8. Eng yaxshi amaliyotlar

  1. "Normal" ni kontekst bilan aniqlang: soat, kun, bayram, mijozning o'z tarixi.

  2. Real vaqt detektori faqat o'tmishni ko'rsin; oflayn tahlil bilan aralashtirmang.

  3. Hodisa darajasida baholang: recall, yolg'on signal/hafta, kechikish.

  4. Detektorlarni bir xil yolg'on signal byudjetida solishtiring; chegarani toza davrda kalibrlang.

  5. Nomutanosib ma'lumotda PR-AUC, kunlik top-k va xarajat; ROC-AUC — faqat qo'shimcha.

  6. Yorliqlar bo'lsa — nazoratli model (class_weight); nazoratsiz — yangi tur signali.

  7. Vaqt bo'yicha bo'ling va belgi kechikishini hisobga oling.

  8. Murakkab usulni (avtoenkoder) har doim IsolationForest bilan juftlashgan taqqoslang.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # 20:00 da 39 MW (odatda 57), umumiy o'rtacha 43 - global z qanday?
2.  # rolling 24 soatlik oyna kontekstli anomaliyani nega ko'rmaydi?
3.  # MAD ni std ga aylantirish koeffitsienti?
4.  # STL qoldig'i real vaqt detektori bo'la oladimi?
5.  # sensor qotib qolsa, detektor nega kechikadi?
6.  # firib ulushi 0.4% - tasodifiy modelning PR-AUC i?
7.  # 36000 normal, FPR 1% - nechta yolg'on signal?
8.  # 16.10 dagi groupby().transform("median") nima uchun xavfli?
9.  # nomutanosib boosting ga nima qo'shish kerak?
10. # o'qitish kunida oxirgi 2 haftadagi firiblar qanday belgilangan bo'ladi?
11. # FN narxi katta, FP narxi kichik - chegara qaysi tomonga siljiydi?
12. # anomaliya bitta ustunda katta chetlanish - IF yoki avtoenkoder?
Javoblar
  1. Kichik (~0.4) — globalda normal, faqat soat kontekstida g'ayrioddiy
  2. Oynada kun profilining hammasi bor — "odatiy" daraja soatni bilmaydi
  3. 1.4826 (normal taqsimotda std = 1.4826 * MAD)
  4. Yo'q — u keyingi qiymatlarni ham ishlatadi; faqat oflayn tahlil uchun
  5. Qotgan qiymat dastlab normal; farq profil undan uzoqlashganda paydo bo'ladi
  6. 0.004 (ulushga teng)
  7. 360 ta — firiblar esa bor-yo'g'i yuzga yaqin
  8. Kelajakdagi tranzaksiyalarni ham ishlatadi — ishlab chiqarishda bunday belgi yo'q
  9. class_weight="balanced" (yoki sample_weight) va PR-AUC bilan tekshirish
  10. Ko'pchiligi hali "normal" — chargeback hali kelmagan
  11. Pastga — ko'proq signal, kamroq o'tkazib yuborish
  12. IsolationForest — sodda va odatda yaxshiroq (4-misol, tranzaksiyalar)

Vazifa 2: Xatolarni tuzating

python
1.  signal = np.abs(x - x.mean()) / x.std() > 3            # soatlik elektr

2.  df["nisbat"] = df["summa"] / df.groupby("mijoz")["summa"].transform("mean")

3.  eng = max(modellar, key=lambda m: roc_auc_score(y_te, ball[m]))

4.  y_train = df_train["firib_tasdiqlangan"].fillna(0)      # oxirgi haftalar ham

5.  chegara = 0.5                                          # FN = summa, FP = 20 000
Javoblar
python
1.  bashorat = np.median([x.shift(168 * k) for k in range(1, 5)], axis=0)
    ball = np.abs(x - bashorat) / (1.4826 * mad_qoldiq)
    signal = ball > np.nanquantile(ball[kalibrlash], 1 - 1 / 168)

2.  n_old = df.groupby("mijoz").cumcount()
    df["nisbat"] = df["summa"] / ((df.groupby("mijoz")["summa"].cumsum()
                                   - df["summa"]) / n_old)

3.  eng = max(modellar, key=lambda m: average_precision_score(y_te, ball[m]))

4.  df_train = df_train[df_train["vaqt"] < T - L]           # faqat yetilgan davr

5.  chegara = min(nomzodlar, key=lambda c: xarajat(ball_val, c, y_val, summa_val))

Vazifa 3: Vaqt qatori

Modellang (1-misol asosida):

  1. Bashorat qoldig'iga bayram belgisini qo'shing (bayram kuni — o'tgan haftaning yakshanbasi bilan solishtirish) va bayramdagi yolg'on signallarni sanang
  2. Kollektiv anomaliya uchun "o'zgarmaslik" detektorini yozing: oxirgi 4 soatda qiymat o'zgarishlari std si juda kichik — kechikish qanchaga qisqaradi?
  3. Yolg'on signal byudjetini haftasiga 0.5, 1, 3 qilib, har detektorning hodisa recall ini jadval qiling
  4. STL(period=168) ni sinab ko'ring — dam olish kunlari effekti qoldiqdan chiqadimi?

Vazifa 4: Firibgarlik belgilari

Modellang (2-misol asosida):

  1. Tasodifiy bo'lish bilan xuddi shu modellarni baholang — PR-AUC qanchaga oshib ketadi va nega?
  2. Har bir shaxsiy belgini navbat bilan olib tashlab (ablation), HistGB PR-AUC ga hissasini o'lchang
  3. IF ballini HistGB ga belgi sifatida qo'shing (yarim nazoratli b) — sezilarli yutuq bormi?
  4. Firibning yangi turini yarating (masalan, kichik summalar seriyasi) va faqat test ga qo'shing — HistGB va IF qanday ushlaydi?

Vazifa 5: Amaliyot

Modellang (3-misol asosida):

  1. Kechikish o'rtachasini 20 kundan 45 kunga oshiring — "sodda" va "yetilgan" strategiyalar farqi qanday o'zgaradi?
  2. FP narxini 5 000, 20 000, 100 000 so'm qiling — optimal chegara va signal soni
  3. Kunlik sig'im n ni xarajat funksiyasiga qo'shing: n dan ortiq signal tekshirilmaydi
  4. Test davrini ikki qismga bo'lib, validatsiyada tanlangan chegaraning barqarorligini tekshiring

Vazifa 6: Avtoenkoder

Modellang (4-misol asosida):

  1. Yashirin o'lchamni 2, 3, 6 qiling — sensor ma'lumotida PR-AUC qanday o'zgaradi?
  2. Avtoenkoderni faqat normal train qatorlarida o'qiting — ifloslangan train bilan farq bormi?
  3. Tranzaksiyalarda avtoenkoderga faqat 4 ta eng informativ belgini bering — IF ga yetib oladimi?
  4. PCA komponentlar sonini 2-6 qilib, sensor ma'lumotida qaysi nuqtada AE ga yaqinlashishini toping

Vazifa 7: O'ylash

Bankning firibgarlikka qarshi bo'limi boshlig'i: "Bizda IsolationForest bor, ROC-AUC 0.97. Operatorlar esa kuniga yuzlab yolg'on signaldan charchagan va ishonmay qo'ygan. Chuqur o'rganish — avtoenkoderga o'tsak, muammo hal bo'ladimi?" Nima deysiz?

Javob

Qisqa javob: yo'q — muammo algoritmda emas, metrika, chegara va yorliqlardan foydalanmaslikda. Avtoenkoder bu turdagi jadval ma'lumotida IF dan yaxshi bo'lishi kutilmaydi (4-misol: tranzaksiyalarda sezilarli yomon).

1. Metrika. ROC-AUC 0.97 nomutanosiblikda "a'lo" ko'rinadi, lekin 2-misolda xuddi shunday ROC-AUC (0.967) FPR 1% da ham precision atigi 21% berdi — 36 ming normalning 1% i 360 ta yolg'on signal. Asosiy metrika — PR-AUC va kunlik sig'imdagi topilgan firiblar.

2. Sig'im va chegara. Operatorlar kuniga ko'ra oladigan sonni (masalan, 10) chegara sifatida belgilang: har kuni faqat eng shubhali 10 ta. 3-misolda HistGB bilan bu kunlik precision 0.20 va firiblarning 83% ini beradi — "yuzlab" o'rniga 10 ta, har beshinchisi haqiqiy. Narxlar ma'lum bo'lsa, chegara xarajat bo'yicha tanlanadi.

3. Yorliqlardan foydalanish. Bankda tasdiqlangan firiblar tarixi bor — bu nazoratli model uchun oltin. 2-misolda HistGB (class_weight="balanced") IF dan PR-AUC da +0.22 sezilarli yaxshi. IF ni tashlamaymiz — u yangi turdagi firiblar uchun qo'shimcha signal.

4. Belgilar. Shaxsiy bazaviy belgilar (mijozning o'z o'tmishiga nisbatan) PR-AUC ni yetti barobar oshirdi (0.07 → 0.49) — algoritm almashtirishdan ko'ra ancha katta yutuq.

5. Yorliqlar kechikishi. Qayta o'qitishda oxirgi haftalardagi hali tasdiqlanmagan firiblar "normal" bo'lib qolmasligi kerak — yetilgan davr yoki buni o'lchab tanlangan strategiya.

Boshliqqa javob: "Avtoenkoder bu muammoni hal qilmaydi — bizning sinovimizda oddiy jadvalda u IsolationForest dan yomon chiqdi. Muammo shundaki, biz signallarni ROC-AUC bo'yicha baholab, chegarani operatorlar sig'imidan qat'i nazar qo'yganmiz va tasdiqlangan firiblar tarixidan foydalanmayapmiz. Taklif: shaxsiy belgilar bilan nazoratli model, har kuni faqat top-10 signal, IF — yangi turlar uchun qo'shimcha. Kutilgan natija: kuniga 10 ta signal, har beshinchisi haqiqiy firib, firiblarning ~80% i topiladi — buni test davrida raqam bilan ko'rsataman."

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


Xulosa

Bu darsda anomaliya aniqlashni kontekst, vaqt, narx va halol taqqoslash bilan kengaytirdik.

Eng muhim uch fikr:

  1. "Normal" kontekstga bog'liq. 1-misolda global, rolling va robust z-score kontekstli anomaliyalarni deyarli ko'rmadi (global z — 0.00), mavsumiy kontekstni biladigan bashorat qoldig'i esa bir xil yolg'on signal byudjetida hodisalarning 0.91 qismini topdi. STL undan ham yaxshi (0.98), lekin kelajakni ko'radi — faqat oflayn tahlil uchun. Kollektiv anomaliya kechikish bilan (~4 soat) topiladi, bayram esa kontekstsiz modelni aldaydi (bayramda 8.5 yolg'on signal).

  2. Firibgarlik — nomutanosiblik, sig'im va narx masalasi. ROC-AUC 0.967 bo'lgan detektor FPR 1% da 21% precision berdi. Shaxsiy bazaviy belgilar IF ning PR-AUC ini 0.07 dan 0.49 ga oshirdi; tasdiqlangan yorliqlar bilan sinf og'irlikli HistGB 0.72 ga yetdi va kuniga 10 ta tekshiruvda firiblarning 83% ini topdi. Belgi kechikishi oxirgi haftalardagi firiblarning 85% ini yashirdi — bu ma'lumotda zarari kichik chiqdi, lekin uni har safar o'lchash kerak. Chegara narxlardan kelib chiqadi va validatsiyada tanlanadi.

  3. Murakkab usul — faqat o'lchab. Avtoenkoder nochiziqli bog'liqlik buzilgan sensor ma'lumotida IF va PCA ni katta farq bilan yengdi (0.64 va 0.04), oddiy tranzaksiya jadvalida esa IF dan sezilarli yomon chiqdi (0.15 va 0.53). Qoida o'zgarmaydi: bir xil bo'lish, bir xil metrika, juftlashgan taqqoslash va eng sodda munosib usul.

Keyingi darsda Reinforcement learning asoslari: model endi faqat "bu g'alati" demaydi, balki o'zi qaror qabul qiladi, uning oqibatini ko'radi va ketma-ket qarorlardan o'rganadi — agent, muhit, mukofot, Markov qaror jarayoni, qiymat funksiyalari va Q-learning.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
28.6-dars: Ilg'or anomaliya aniqlash — IlmHamroh