IlmHamroh
Data Science va sun'iy intellekt/MLOps va deploy12/14-dars42 daqiqa
Mundarija (26)

27.12-dars: Drift aniqlash

27-QISM — MLOPS VA DEPLOY · 12-dars


1. Kirish va motivatsiya

Oldingi darsda monitoringni qurdik: xizmat sog'lig'i, kechikish, xatolar, bashoratlar va kirish ma'lumotlari loglarga yoziladi va chegaralar bilan kuzatiladi. Endi monitoringning eng nozik savoliga o'tamiz: ma'lumot o'zgardimi — va bu o'zgarish modelga zarar beradimi?

Model o'qitilgan paytdagi dunyo bilan ishlab chiqarishdagi dunyo hech qachon aynan bir xil emas. Mijozlar tarkibi o'zgaradi, narxlar oshadi, yangi mahsulot chiqadi, raqobatchi aksiya boshlaydi, ilova interfeysi yangilanadi, ma'lumot yig'uvchi xizmatda xato paydo bo'ladi. Bularning hammasi umumiy nom bilan drift (siljish) deyiladi. Muammo shundaki, drift turlari har xil, ularni aniqlash usullari har xil — va eng xavflisi, eng oson aniqlanadigan drift ko'pincha eng zararsizi bo'ladi.

Bu darsda drift turlarini ajratamiz va aniqlash usullarini noldan quramiz: PSI, KS testi, chi-kvadrat, Wasserstein masofa, domen klassifikatori, bashorat taqsimoti drifti va oqim ustida ishlaydigan Page-Hinkley detektori. Lekin darsning asosiy qismi — tuzoqlar. Ularni gapirib emas, o'lchab ko'rsatamiz: katta namunada KS testi arzimas o'zgarishni ham "sezilarli" deydi; 40 ta ustunni har kuni tekshirsangiz, drift bo'lmasa ham kunlarning ko'pchiligida qizil signal chiqadi; ahamiyatsiz ustundagi drift sifatga ta'sir qilmaydi, konsept drift esa kirish detektorlarining birortasiga ko'rinmasdan sifatni tushiradi.

Real vaziyat. Bank kredit skoring modeli uchun drift paneli qurdi: 60 ta ustunning har birida har kuni KS testi. Birinchi haftadanoq panelda har kuni 3-4 ta qizil ustun chiqdi — ko'pi arzimas o'zgarishlar edi. Jamoa signallarga qaramay qo'ydi. Uch oydan keyin defolt ulushi keskin oshdi. Tahlil ko'rsatdiki, kirish ustunlarining birortasi ham sezilarli o'zgarmagan: iqtisodiy vaziyat o'zgargani sababli bir xil daromad va tarixga ega mijozlarning to'lov xulqi o'zgargan edi. Bu — konsept drift, va uni faqat belgilar (haqiqiy natija) kelgandan keyin sifat metrikasi ko'rsatadi. Panel ham noto'g'ri narsani, ham noto'g'ri sezgirlik bilan o'lchagan.

Bu darsda driftni aniqlashni va — undan ham muhimi — signalni to'g'ri talqin qilishni o'rganamiz.

Bu darsda:

  • Drift turlari: kovariat drift P(X), konsept drift P(y|X), belgilar drifti P(y); vaqt bo'yicha shakllari
  • PSI, KS, chi-kvadrat, Wasserstein — noldan va scipy bilan
  • Bin tanlash tuzog'i
  • p-qiymat va effekt o'lchami; ko'p testlash (Bonferroni, BH)
  • Ko'p o'lchovli drift: domen klassifikatori
  • Bashorat taqsimoti drifti va belgilarsiz sifatni taxminlash
  • Drift va sifat: qaysi drift zarar beradi, qaysi biri ko'rinmaydi
  • Oqim ustida: Page-Hinkley, ADWIN g'oyasi, kechikish va yolg'on signal
  • Tuzoqlar

ℹ Misollar real numpy/scipy/sklearn bilan (Python 3.14). Ma'lumot sintetik, urug' bilan yaratiladi.


2. Nazariya — chuqur tushuntirish

2.1. Drift turlari: nima o'zgaradi?

Birgalikdagi taqsimotni ikki xil yoyish mumkin, va har bir ko'paytuvchi alohida o'zgarishi mumkin:

text
P(X, y) = P(y | X) * P(X)        = P(X | y) * P(y)

KOVARIAT DRIFT (ma'lumot drifti):   P(X) o'zgaradi, P(y|X) o'sha-o'sha
  misol: ilova yoshlar orasida mashhur bo'ldi -> yosh taqsimoti siljidi
  kirish detektorlari KO'RADI
  sifatga ta'siri: model o'sha hududda yaxshi bo'lsa - yo'q;
                   model yangi hududda xato bo'lsa - katta

KONSEPT DRIFT:                      P(y|X) o'zgaradi, P(X) o'sha-o'sha
  misol: iqtisodiy inqiroz - bir xil daromadli mijoz endi ko'proq
         kechiktiradi; firibgarlar yangi usul topdi
  kirish detektorlari KO'RMAYDI (X taqsimoti o'zgarmagan!)
  sifat albatta tushadi - model o'rgangan qoida endi noto'g'ri

BELGILAR DRIFTI (prior shift):      P(y) o'zgaradi, P(X|y) o'sha-o'sha
  misol: epidemiya mavsumi - kasallar ulushi oshdi
  X taqsimoti ham o'zgaradi (sinflar aralashmasi o'zgargani uchun)
  AUC deyarli o'zgarmaydi, lekin chegara va kalibrovka buziladi

Drift — bitta hodisa emas, uch xil hodisa. Ular turli detektor bilan ko'rinadi va turli yechim talab qiladi.

Amalda ular aralash keladi. Masalan, yangi marketing kampaniyasi boshqa mijozlarni olib keladi (kovariat), ularning xulqi ham boshqacha (konsept), ketish ulushi ham o'zgaradi (belgilar). Lekin fikrlashda ularni ajratish kerak, chunki savol har doim bitta: qaysi qism o'zgardi va model shu qismga bog'liqmi?

2.2. Vaqt bo'yicha shakllar

text
TO'SATDAN (abrupt)          ASTA-SEKIN (gradual/incremental)
  ______                          ____/
        |______                  /
  (yangi versiya, qonun,       (inflyatsiya, auditoriya
   ma'lumot manbai buzildi)     asta o'zgaradi)

MAVSUMIY (seasonal)         QAYTUVCHI (recurring)
  /\  /\  /\  /\               ___    ___    ___
 /  \/  \/  \/  \                 |__|   |__|
  (hafta kuni, oy, bayram)     (aksiya boshlanadi va tugaydi,
                                eski rejim qaytadi)

Har shakl detektorga boshqa talab qo'yadi:

  • To'satdan — tez aniqlanadi, lekin tez javob kerak (odatda ma'lumot xatosi yoki tizim o'zgarishi).
  • Asta-sekin — har kuni o'zgarish kichik, shuning uchun "kechagi kun bilan solishtirish" uni ko'rmaydi; uzoqroq reference bilan solishtirish kerak.
  • Mavsumiy — drift emas, kutilgan o'zgarish. Reference oyna mavsumni qamramasa, detektor har mavsumda yolg'on signal beradi. Yechim: o'tgan yilning o'sha davri bilan solishtirish yoki mavsumni modelga belgi sifatida berish.
  • Qaytuvchi — eski rejim qaytadi; eski modellarni arxivda saqlash va qayta ishlatish foydali (27.5 registr).

2.3. Bir o'lchovli o'lchovlar

Bitta ustun uchun reference (masalan, o'quv davri) va current (oxirgi hafta) taqsimotlarini solishtiramiz.

text
PSI (Population Stability Index):
  1. chegaralarni REFERENCE kvantillaridan olamiz (10 bin -> har birida ~10%)
  2. p_i = reference ulushi, q_i = current ulushi har binda
  3. PSI = sum (q_i - p_i) * ln(q_i / p_i)       (bo'sh bin -> eps)
  an'anaviy qoida (qonun emas!):  < 0.1 barqaror
                                  0.1-0.25 o'rtacha siljish
                                  > 0.25 katta siljish

KS (Kolmogorov-Smirnov), ikki namunali:
  D = max_x |F_ref(x) - F_cur(x)|                 (empirik CDF lar)
  p-qiymat: sqrt(n*m/(n+m)) * D  ~  Kolmogorov taqsimoti (asimptotik)
  D - effekt o'lchami [0, 1];  p - "tasodifmi?" savoliga javob

CHI-KVADRAT (kategoriyalar):
  2 x K jadval (reference / current  x  toifalar)
  X^2 = sum (kuzatilgan - kutilgan)^2 / kutilgan,  df = K - 1

WASSERSTEIN (W1, "tuproq ko'chirish" masofasi):
  W1 = integral |F_ref(x) - F_cur(x)| dx
  ustun BIRLIGIDA (so'm, yosh, kun) -> reference std ga bo'lib taqqoslang
  PSI va KS dan farqli: siljish QANCHA uzoqqa ekanini ham hisobga oladi
O'lchov Tur Nimani o'lchaydi Kuchli tomoni Zaif tomoni
PSI son (bin) / kategoriya ulushlar farqi talqini oson, sanoatda keng tarqalgan bin va eps ga bog'liq, kichik n da shovqinli
KS D son CDF lar orasidagi maksimal farq binsiz, [0, 1] shkala dumdagi o'zgarishga kam sezgir
chi-kvadrat kategoriya toifa ulushlari klassik test, p-qiymat katta n da har doim sezilarli
W1 son massani ko'chirish narxi masofa ma'nosi bor birlikka bog'liq, normallash kerak

Chegaralar faqat reference dan olinadi — ular current ga moslashsa, siljish "yutilib" ketadi.

2.4. Bin tanlash tuzog'i

PSI oddiy ko'rinadi, lekin natija ikki tanlovga kuchli bog'liq:

text
1. BIN SONI
   drift YO'Q bo'lsa ham, kichik n da har bin ulushi tasodifan tebranadi
   PSI ning "shovqin tagligi" ~ (bin soni - 1) * (1/n_ref + 1/n_cur)
   100 bin + 500 qator  ->  drift bo'lmasa ham PSI > 0.1

2. BIN TURI
   teng kenglik (min..max ni teng bo'lish) QIYSHIQ ustunda:
     ma'lumotning ko'pi 2-3 binda, qolganlari deyarli bo'sh
   bitta xato yozuv (masalan 1e6) max ni cho'zadi ->
     HAMMA ma'lumot bitta binga tushadi -> PSI ~ 0, drift ko'rinmaydi
   KVANTIL chegaralar: har binda ~teng ulush, chetlanishga chidamli

3. EPS (bo'sh bin)
   reference da yo'q toifa/bin paydo bo'lsa: ln(q / eps) - eps ga bog'liq
   yangi toifani ALOHIDA metrika sifatida sanang ("ko'rilmagan ulush")

2.5. p-qiymat va effekt o'lchami

text
p-qiymat savoli:  "bu farq tasodifiy tebranishmi?"
biznes savoli:    "bu farq modelga/foydalanuvchiga TA'SIR qiladimi?"

n oshgan sari test har qanday nolga teng bo'lmagan farqni ushlaydi:
  0.02 std siljish, n = 500       -> p katta, "drift yo'q"
  0.02 std siljish, n = 500 000   -> p ~ 1e-18, "sezilarli drift!"
  effekt (D, W1, PSI) esa ikkala holatda ham ARZIMAS

AMALIY QOIDA: signal = statistik sezilarli VA effekt chegaradan katta
  masalan: p < alfa (tuzatilgan)  VA  (D >= 0.1 yoki PSI >= 0.1)
  effekt chegarasini biznes bilan kelishing, natijani ko'rishdan OLDIN

Katta ma'lumotda p-qiymat drift o'lchovi emas — u faqat "tasodif emas" deydi. Qaror uchun effekt o'lchami kerak (11-qismdagi "statistik va amaliy ahamiyat" farqi aynan shu).

2.6. Ko'p ustun — ko'p testlash

text
m = 40 ustun, har biri alfa = 0.05, drift YO'Q:
  kutilgan yolg'on signal / kun = 40 * 0.05 = 2
  P(kamida bitta signal) = 1 - 0.95^40 = 0.87
  -> deyarli HAR KUNI qizil chiroq, lekin hech narsa bo'lmagan

TUZATISHLAR 11.9-bob:
  Bonferroni: p < alfa / m        FWER <= alfa   (qat'iy, quvvat past)
  BH (Benjamini-Hochberg):        FDR <= alfa    (yumshoqroq, quvvat yuqori)
    p larni saralang: p_(1) <= ... <= p_(m)
    eng katta k: p_(k) <= alfa * k / m  ->  1..k rad etiladi

FWER - kamida bitta yolg'on signal ehtimoli
FDR  - signallar ichidagi yolg'onlar ULUSHI (kutilgan)

Monitoringda qaysi biri? Agar har signal odam vaqtini oladigan tekshiruv bo'lsa — FDR nazorati (BH) ko'pincha yaxshi murosa: haqiqiy driftlarning ko'pini topadi, signallar ichidagi yolg'onlar ulushini past tutadi.

2.7. Ko'p o'lchovli drift: domen klassifikatori

Bir o'lchovli testlar ustunlarni alohida ko'radi. Ikki ustun orasidagi korrelyatsiya o'zgarsa (masalan, avval yosh va daromad birga o'sardi, endi yo'q), har ustunning o'z taqsimoti o'zgarmasligi mumkin — bir o'lchovli testlar jim turadi.

text
DOMEN KLASSIFIKATORI (adversarial validation):
  reference qatorlari -> belgi 0,  current qatorlari -> belgi 1
  klassifikator (masalan HistGB) cross-validation bilan o'qitiladi
  AUC ~ 0.5  -> ikki to'plamni ajratib bo'lmaydi -> drift yo'q
  AUC >> 0.5 -> farq bor; belgi ahamiyati - QAYSI ustunlar

AFZALLIGI:  barcha ustunlar va ularning o'zaro bog'liqligi birga
            bitta raqam; aralash turdagi ustunlar
KAMCHILIGI: model o'qitish kerak (sekinroq)
            AUC ham "drift bor" deydi, "zararli" demaydi

2.8. Bashorat drifti va belgilarsiz sifat taxmini

Belgilar (haqiqiy natija) ko'pincha kechikib keladi: mijoz ketdimi — bir oydan keyin ma'lum, kredit qaytdimi — yildan keyin. Shu vaqt ichida qo'lda bor narsa — kirishlar va bashoratlar.

text
BASHORAT TAQSIMOTI DRIFTI:
  PSI(bashorat_ref, bashorat_cur)
  model "ko'radigan" o'zgarishni ko'rsatadi: ahamiyatsiz ustundagi
  drift bashoratni o'zgartirmaydi -> signal yo'q (bu yaxshi!)

ISHONCH ASOSIDA SIFAT TAXMINI (belgilarsiz):
  kalibrlangan model uchun: kutilgan aniqlik = mean(max(p, 1 - p))
  g'oya: "model 0.9 ishonch bilan aytgan bo'lsa, 90% hollarda to'g'ri"

CHEKLOVI (juda muhim):
  bu faqat P(y|X) o'zgarmagan va model yangi hududda ham
  KALIBRLANGAN bo'lsa to'g'ri
  konsept drift -> model o'zini xuddi avvalgidek ishonchli his qiladi,
                   taxmin "hammasi joyida" deydi, haqiqiy aniqlik tushgan
  yangi hududga siljish -> model ekstrapolyatsiyada ORTIQCHA ishonchli

Belgilarsiz taxmin — erta ogohlantirish, dalil emas. Uni belgilar kelgach haqiqiy sifat bilan albatta tekshiring.

2.9. Drift va sifat — to'rt holat

text
                      sifat O'ZGARMADI          sifat TUSHDI
drift KO'RINDI     ahamiyatsiz ustunda      muhim ustunda kovariat drift
                   drift (yolg'on xavotir)  (model yangi hududda xato),
                                            belgilar drifti (chegara)
drift KO'RINMADI   hammasi joyida           KONSEPT DRIFT - eng xavflisi

Shuning uchun monitoring ikki qavatli bo'ladi:

  1. Kirish va bashorat drifti — tez, belgisiz, erta ogohlantirish. Muhim ustunlarga (belgi ahamiyati bo'yicha) ko'proq vazn bering.
  2. Sifat metrikasi — belgilar kelgach, kechikib, lekin haqiqiy javob. Konsept driftni faqat shu ushlaydi.

2.10. Oqim ustida drift aniqlash

Partiyalar (kun, hafta) o'rniga har bir hodisa kelib turadigan oqimda detektor har qadamda qaror qabul qiladi.

text
OYNALAR:
  qat'iy reference oyna  vs  sirpanuvchi oxirgi-N oyna  -> test (z, KS)
  oyna kichik -> tez, lekin shovqinli; katta -> barqaror, lekin sekin

PAGE-HINKLEY (o'rtachaning OSHISHI, masalan xato darajasi):
  x_t      - kuzatuv (1 = model xato qildi, 0 = to'g'ri)
  xbar_t   - shu paytgacha o'rtacha
  m_t      = sum_{i<=t} (x_i - xbar_i - delta)     (delta - chidam)
  M_t      = min_{i<=t} m_i
  SIGNAL:  m_t - M_t > lambda
  lambda katta -> kam yolg'on signal, lekin KECHROQ aniqlaydi
  xotira O(1): faqat 4 ta son saqlanadi

ADWIN (ADaptive WINdowing) g'oyasi:
  oyna o'zgaruvchan uzunlikda; har qadamda oynani ikki qismga
  bo'lib ko'radi - o'rtachalar Hoeffding chegarasidan ko'proq farq
  qilsa, ESKI qismni tashlaydi. Oyna uzunligi o'zi "barqaror davr"
  uzunligiga moslashadi. (river kutubxonasida tayyor; bu darsda g'oya.)

ASOSIY MUROSA:
  aniqlash KECHIKISHI  <->  YOLG'ON SIGNAL chastotasi
  chegarani drift bo'lmagan tarixiy oqimda kalibrlang:
  "yiliga ko'pi bilan N ta yolg'on signal" -> lambda

2.11. Reference oyna va signaldan keyingi harakat

Har qanday drift o'lchovi "nimaga nisbatan?" degan savolga bog'liq. Reference tanlovi natijani o'lchov turidan kam bo'lmagan darajada belgilaydi.

text
REFERENCE VARIANTLARI:
  o'quv to'plami          model "bilgan" dunyo; eng keng tarqalgan
  validatsiya/test davri  model tekshirilgan dunyo
  sirpanuvchi (o'tgan hafta)  to'satdan o'zgarishni ko'radi,
                              asta-sekin driftni KO'RMAYDI
  o'tgan yilning o'sha davri  mavsumiy ma'lumot uchun

TANLOV QOIDASI:
  reference = "model yaxshi ishlagan va biz ishonadigan davr"
  hajmi yetarli (bin ulushlari barqaror bo'lishi uchun)
  mavsum bo'lsa - mavsumni qamrasin yoki o'sha davr bilan solishtiring
  model qayta o'qitilganda reference ham YANGILANADI

Signal chiqdi. Keyin nima?

text
1. TEKSHIRISH: ma'lumot xatosimi? (sxema, birlik, bo'sh qiymatlar,
   yangi manba) -> ha bo'lsa, bu drift emas, INSIDENT (27.3, 27.11)
2. TA'SIR: bashorat drifti bormi? muhim ustunmi? belgilar kelganmi?
3. QAROR (oldindan yozilgan runbook bo'yicha):
   ta'sir yo'q          -> kuzatishda davom, reference ni ko'rib chiqish
   ma'lumot xatosi      -> manbani tuzatish, zarar ko'rgan bashoratlar
   haqiqiy drift, sifat tushgan -> qayta o'qitish triggeri 27.13-bob
   konsept drift, belgilar kam  -> qoida/chegara bilan vaqtinchalik
                                   himoya, belgilar to'planishini kutish
4. HUJJAT: signal, sabab, qaror, natija - keyingi safar uchun

Drift signali — savol, javob emas. Uni ma'lumot xatosi, ahamiyatsiz o'zgarish va haqiqiy muammoga ajratish uchun oldindan yozilgan tartib kerak.

2.12. Tuzoqlar

Asosiy tuzoqlar: katta namunada p-qiymatni drift o'lchovi deb olish; har ustunni tuzatishsiz testlash va "signal charchoqligi"; PSI ni teng kenglikdagi binlarda yoki juda ko'p binda hisoblash; chegaralarni current dan olish; yangi toifani eps ichida yashirish; har qanday driftni "qayta o'qitish kerak" deb talqin qilish; kirish detektorlari jim bo'lsa "model yaxshi" deb xulosa qilish (konsept drift); belgilarsiz taxminni haqiqiy sifat o'rniga ishlatish; mavsumiy o'zgarishni drift deb qabul qilish; oqim detektorining chegarasini drift bo'lmagan ma'lumotda kalibrlamaslik.


3. Tez ma'lumotnoma

python
import numpy as np
from scipy import stats


def psi(ref, cur, bins=10, eps=1e-4):
    ichki = np.unique(np.quantile(ref, np.linspace(0, 1, bins + 1)[1:-1]))
    k = len(ichki) + 1
    p = np.bincount(np.searchsorted(ichki, ref, side="right"), minlength=k) / len(ref)
    q = np.bincount(np.searchsorted(ichki, cur, side="right"), minlength=k) / len(cur)
    p, q = np.clip(p, eps, None), np.clip(q, eps, None)
    return float(np.sum((q - p) * np.log(q / p)))


r = stats.ks_2samp(ref, cur)                       # r.statistic (D), r.pvalue
w = stats.wasserstein_distance(ref, cur) / ref.std()
chi = stats.chi2_contingency(jadval_2xK, correction=False)


def bh(p, alfa=0.05):                              # Benjamini-Hochberg
    m = len(p)
    tartib = np.argsort(p)
    otdi = p[tartib] <= alfa * np.arange(1, m + 1) / m
    rad = np.zeros(m, dtype=bool)
    if otdi.any():
        rad[tartib[:np.max(np.nonzero(otdi)[0]) + 1]] = True
    return rad


signal = (p_tuzatilgan < 0.05) & (D >= 0.1)        # sezilarli VA katta

# domen klassifikatori
X = np.vstack([X_ref, X_cur]); d = np.r_[np.zeros(len(X_ref)), np.ones(len(X_cur))]
s = cross_val_predict(HistGradientBoostingClassifier(), X, d, cv=3,
                      method="predict_proba")[:, 1]
domen_auc = roc_auc_score(d, s)                    # ~0.5 -> drift yo'q

taxminiy_aniqlik = np.mean(np.maximum(p, 1 - p))   # faqat kalibrlangan va
                                                   # konsept drift yo'q bo'lsa

Qaysi vaziyatda nima

Savol Vosita
Son ustun siljidimi? KS D + p, W1/std, PSI (kvantil binlar)
Kategoriya ulushlari? chi-kvadrat, PSI, "ko'rilmagan toifa ulushi"
Ustunlar birgalikda? domen klassifikatori AUC
Model buni sezdimi? bashorat taqsimoti PSI
Sifat tushdimi? belgilar kelgach — haqiqiy metrika (yagona ishonchli javob)
Ko'p ustun BH yoki Bonferroni + effekt chegarasi
Oqim Page-Hinkley / ADWIN, chegara drift yo'q tarixda kalibrlanadi

Drift xulosasi

drift turi: P(X) / P(y|X) / P(y) - qaysi qism o'zgardi?
signal = sezilarli (tuzatilgan p) VA katta (effekt chegarasi)
PSI: kvantil chegaralar reference dan; bin soni n ga mos
domen AUC - ko'p o'lchovli; bashorat PSI - model ko'rgan drift
konsept drift - faqat belgilar bilan; belgisiz taxmin - faqat ogohlantirish
oqim: kechikish <-> yolg'on signal

4. Batafsil misollar

Misollar real numpy/scipy/sklearn bilan (Python 3.14). Har misol mustaqil ishlaydi.

Misol 1 — PSI, KS, chi-kvadrat va Wasserstein noldan; bin tanlash tuzog'i

python
"""Drift o'lchovlari noldan: PSI, KS, chi-kvadrat, Wasserstein (scipy bilan solishtirib)."""

import numpy as np
from scipy import stats


def psi(ref, cur, bins=10, usul="kvantil", eps=1e-4):
    """Population Stability Index; chegaralar FAQAT reference dan olinadi."""
    if usul == "kvantil":
        ichki = np.quantile(ref, np.linspace(0, 1, bins + 1)[1:-1])
    else:                                   # teng kenglik
        ichki = np.linspace(ref.min(), ref.max(), bins + 1)[1:-1]
    ichki = np.unique(ichki)
    k = len(ichki) + 1                      # chetlar -inf va +inf gacha
    p = np.bincount(np.searchsorted(ichki, ref, side="right"),
                    minlength=k) / len(ref)
    q = np.bincount(np.searchsorted(ichki, cur, side="right"),
                    minlength=k) / len(cur)
    p, q = np.clip(p, eps, None), np.clip(q, eps, None)
    return float(np.sum((q - p) * np.log(q / p)))


def psi_kat(ref, cur, eps=1e-4):
    toifalar = sorted(set(ref) | set(cur))
    p = np.array([np.mean(ref == t) for t in toifalar])
    q = np.array([np.mean(cur == t) for t in toifalar])
    p, q = np.clip(p, eps, None), np.clip(q, eps, None)
    return float(np.sum((q - p) * np.log(q / p)))


def kolmogorov_sf(x, hadlar=100):
    """P(K > x) - Kolmogorov taqsimotining dumi (asimptotik)."""
    if x < 0.2:
        return 1.0
    k = np.arange(1, hadlar + 1)
    return float(min(1.0, 2 * np.sum((-1) ** (k - 1) * np.exp(-2 * k**2 * x**2))))


def ks_noldan(a, b):
    a, b = np.sort(a), np.sort(b)
    nuqtalar = np.concatenate([a, b])
    fa = np.searchsorted(a, nuqtalar, side="right") / len(a)
    fb = np.searchsorted(b, nuqtalar, side="right") / len(b)
    d = float(np.max(np.abs(fa - fb)))
    en = np.sqrt(len(a) * len(b) / (len(a) + len(b)))
    return d, kolmogorov_sf(en * d)


def chi2_noldan(ref, cur):
    toifalar = sorted(set(ref) | set(cur))
    jadval = np.array([[np.sum(ref == t) for t in toifalar],
                       [np.sum(cur == t) for t in toifalar]], dtype=float)
    kutilgan = jadval.sum(1, keepdims=True) * jadval.sum(0) / jadval.sum()
    x2 = float(np.sum((jadval - kutilgan) ** 2 / kutilgan))
    df = len(toifalar) - 1
    return x2, df, float(stats.chi2.sf(x2, df))


def wasserstein_noldan(a, b):
    """W1 = integral |F_a(x) - F_b(x)| dx (1 o'lchovli)."""
    a, b = np.sort(a), np.sort(b)
    x = np.sort(np.concatenate([a, b]))
    fa = np.searchsorted(a, x[:-1], side="right") / len(a)
    fb = np.searchsorted(b, x[:-1], side="right") / len(b)
    return float(np.sum(np.abs(fa - fb) * np.diff(x)))


def main() -> None:
    rng = np.random.default_rng(0)
    ref = rng.lognormal(4.0, 0.5, 5000)          # "oylik to'lov" - qiyshiq
    std = ref.std()

    print("=== 1. Noldan va scipy: bir xil javob ===")
    cur = rng.lognormal(4.1, 0.5, 2000)
    d, p = ks_noldan(ref, cur)
    r = stats.ks_2samp(ref, cur, method="asymp")
    print(f"  KS D:   noldan {d:.5f}   scipy {r.statistic:.5f}")
    print(f"  KS p:   noldan {p:.3e}   scipy {r.pvalue:.3e}")
    w = wasserstein_noldan(ref, cur)
    print(f"  W1:     noldan {w:.5f}   scipy "
          f"{stats.wasserstein_distance(ref, cur):.5f}")
    t_ref = rng.choice(["A", "B", "C", "D"], 5000, p=[0.4, 0.3, 0.2, 0.1])
    t_cur = rng.choice(["A", "B", "C", "D"], 2000, p=[0.35, 0.3, 0.22, 0.13])
    x2, df, p2 = chi2_noldan(t_ref, t_cur)
    jad = np.array([[np.sum(t_ref == t) for t in "ABCD"],
                    [np.sum(t_cur == t) for t in "ABCD"]])
    s = stats.chi2_contingency(jad, correction=False)
    print(f"  chi^2:  noldan {x2:.4f} (df={df}, p={p2:.4f})   "
          f"scipy {s.statistic:.4f} (p={s.pvalue:.4f})")

    print("\n=== 2. Ssenariylar: bir ustun, to'rt o'lchov ===")
    ssenariy = {
        "drift yo'q": rng.lognormal(4.0, 0.5, 2000),
        "o'rtacha +2%": rng.lognormal(4.0, 0.5, 2000) * 1.02,
        "o'rtacha +10%": rng.lognormal(4.0, 0.5, 2000) * 1.10,
        "o'rtacha +30%": rng.lognormal(4.0, 0.5, 2000) * 1.30,
        "tarqoqlik x1.5": np.exp(4.0 + 0.75 * rng.normal(0, 1, 2000)),
    }
    print(f"  {'ssenariy':<16} {'PSI':>7} {'KS D':>7} {'KS p':>10} "
          f"{'W1/std':>7}")
    for nom, c in ssenariy.items():
        d, p = ks_noldan(ref, c)
        print(f"  {nom:<16} {psi(ref, c):>7.4f} {d:>7.4f} {p:>10.2e} "
              f"{wasserstein_noldan(ref, c) / std:>7.4f}")

    print("\n=== 3. Bin tanlash tuzog'i (1): bin soni va shovqin ===")
    print("  drift YO'Q, current n=500; 200 takror o'rtachasi")
    print(f"  {'bin':>5} {'PSI o_rt':>9} {'PSI>0.1 ulushi':>15}")
    for b in [5, 10, 20, 50, 100]:
        qiymatlar = [psi(ref, rng.lognormal(4.0, 0.5, 500), bins=b)
                     for _ in range(200)]
        qiymatlar = np.array(qiymatlar)
        print(f"  {b:>5} {qiymatlar.mean():>9.4f} "
              f"{np.mean(qiymatlar > 0.1):>15.3f}")

    print("\n=== 4. Bin tanlash tuzog'i (2): teng kenglik va bitta xato ===")
    ichki = np.linspace(ref.min(), ref.max(), 11)[1:-1]
    ulush = np.bincount(np.searchsorted(ichki, ref, side="right"),
                        minlength=10) / len(ref)
    print(f"  teng kenglik, 10 bin ulushlari: {np.round(ulush, 3).tolist()}")
    ref_xato = np.append(ref, 1e6)                   # bitta xato yozuv
    print(f"  {'ssenariy':<16} {'kvantil':>8} {'teng':>8} {'teng+xato':>10}")
    for nom in ["o'rtacha +10%", "o'rtacha +30%"]:
        c = ssenariy[nom]
        print(f"  {nom:<16} {psi(ref, c):>8.4f} "
              f"{psi(ref, c, usul='teng'):>8.4f} "
              f"{psi(ref_xato, c, usul='teng'):>10.4f}")

    print("\n=== 5. Kategoriya: yangi qiymat paydo bo'ldi ===")
    t_yangi = t_cur.copy()
    t_yangi[:100] = "E"                              # reference da yo'q
    for eps in [1e-4, 1e-6]:
        print(f"  eps={eps:.0e}: PSI {psi_kat(t_ref, t_yangi, eps):.4f}")
    x2, df, p2 = chi2_noldan(t_ref, t_yangi)
    print(f"  chi^2 {x2:.1f} (df={df}), p={p2:.2e}")
    print(f"  reference da yo'q toifa ulushi: {np.mean(t_yangi == 'E'):.3f}")
    print("  ⭐ Yangi toifani alohida sanang - PSI qiymati eps ga bog'liq")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Noldan va scipy: bir xil javob ===
  KS D:   noldan 0.10840   scipy 0.10840
  KS p:   noldan 5.254e-15   scipy 4.452e-15
  W1:     noldan 7.23445   scipy 7.23445
  chi^2:  noldan 35.6664 (df=3, p=0.0000)   scipy 35.6664 (p=0.0000)

=== 2. Ssenariylar: bir ustun, to'rt o'lchov ===
  ssenariy             PSI    KS D       KS p  W1/std
  drift yo'q        0.0037  0.0250   3.34e-01  0.0279
  o'rtacha +2%      0.0101  0.0338   7.65e-02  0.0463
  o'rtacha +10%     0.0427  0.0912   9.56e-11  0.2058
  o'rtacha +30%     0.2521  0.2150   8.77e-58  0.5568
  tarqoqlik x1.5    0.1837  0.1002   6.97e-13  0.4733

=== 3. Bin tanlash tuzog'i (1): bin soni va shovqin ===
  drift YO'Q, current n=500; 200 takror o'rtachasi
    bin  PSI o_rt  PSI>0.1 ulushi
      5    0.0088           0.000
     10    0.0195           0.000
     20    0.0419           0.000
     50    0.1110           0.695
    100    0.2755           1.000

=== 4. Bin tanlash tuzog'i (2): teng kenglik va bitta xato ===
  teng kenglik, 10 bin ulushlari: [0.186, 0.413, 0.234, 0.101, 0.039, 0.017, 0.007, 0.002, 0.001, 0.0]
  ssenariy          kvantil     teng  teng+xato
  o'rtacha +10%      0.0427   0.0388     0.0001
  o'rtacha +30%      0.2521   0.2432     0.0001

=== 5. Kategoriya: yangi qiymat paydo bo'ldi ===
  eps=1e-04: PSI 0.3350
  eps=1e-06: PSI 0.5659
  chi^2 284.4 (df=4), p=2.53e-60
  reference da yo'q toifa ulushi: 0.050
  ⭐ Yangi toifani alohida sanang - PSI qiymati eps ga bog'liq

Natija tahlili.

1-bo'lim — noldan yozilgan funksiyalar scipy bilan mos: KS statistikasi D = 0.10840, Wasserstein 7.23445, chi-kvadrat 35.6664 — raqamma-raqam bir xil. KS p-qiymatida kichik farq bor (5.3e-15 va 4.5e-15): biz sof asimptotik Kolmogorov qatorini ishlatdik, scipy esa chekli namuna uchun kichik tuzatish qo'shadi. Bunday kichik p-qiymatlarda bu farq qarorga ta'sir qilmaydi.

2-bo'lim — bir ustunda to'rt o'lchov. "Drift yo'q" da hammasi kichik (PSI 0.0037, p 0.33). +2% siljishni hech bir o'lchov sezilarli demadi (p 0.077). +10% da KS p-qiymati juda kichik (9.6e-11), lekin PSI atigi 0.0427 — an'anaviy 0.1 chegarasidan ancha past. +30% da PSI 0.2521 — "katta siljish" chegarasidan o'tdi. E'tibor bering: "tarqoqlik x1.5" da o'rtacha deyarli o'zgarmagan, lekin PSI 0.1837 va W1/std 0.4733 — o'lchovlar faqat o'rtachani emas, taqsimot shaklini ham ko'radi.

3-bo'lim — birinchi bin tuzog'i. Drift yo'q, current da 500 qator. 10 binda PSI o'rtachasi 0.0195, 50 binda 0.1110 — 200 takrorning 69.5% ida 0.1 chegarasidan oshdi; 100 binda esa har safar (1.000). Ya'ni ko'p bin + kichik n = yolg'on drift. Bin sonini n ga moslang (odatda 10).

4-bo'lim — ikkinchi bin tuzog'i. Qiyshiq ustunda teng kenglikdagi 10 bin ulushlari juda notekis: oxirgi uch binda jami 0.3% ma'lumot. Shunga qaramay toza ma'lumotda natija kvantil binlarga yaqin. Lekin reference ga bitta xato yozuv (1e6) qo'shildi — va teng kenglikdagi PSI +30% siljishda ham 0.0001 ga tushdi: maksimum cho'zildi, butun ma'lumot bitta binga to'plandi, drift butunlay ko'rinmay qoldi. Kvantil chegaralar bitta chetlanishga chidamli.

5-bo'lim — reference da yo'q toifa (E, 5%). PSI eps ga bog'liq: 1e-4 da 0.3350, 1e-6 da 0.5659 — bir xil ma'lumot, ikki xil "drift kattaligi". Shuning uchun yangi toifa alohida, tushunarli metrika bilan sanaladi: "ko'rilmagan toifa ulushi 0.050".

Misol 2 — Katta namuna va ko'p testlash tuzoqlari

python
"""Ikki tuzoq: katta namunada p-qiymat va ko'p ustunda ko'p testlash."""

import numpy as np
from scipy import stats


def ks_p(ref_sorted, cur):
    """Tez KS (asimptotik p): ref oldindan saralangan."""
    cur = np.sort(cur)
    x = np.concatenate([ref_sorted, cur])
    d = np.max(np.abs(np.searchsorted(ref_sorted, x, side="right") / len(ref_sorted)
                      - np.searchsorted(cur, x, side="right") / len(cur)))
    en = np.sqrt(len(ref_sorted) * len(cur) / (len(ref_sorted) + len(cur)))
    return float(stats.kstwobign.sf(en * d))


def bonferroni(p, alfa):
    return p < alfa / len(p)


def bh(p, alfa):
    """Benjamini-Hochberg: FDR <= alfa."""
    m = len(p)
    tartib = np.argsort(p)
    chegara = alfa * np.arange(1, m + 1) / m
    otdi = p[tartib] <= chegara
    rad = np.zeros(m, dtype=bool)
    if otdi.any():
        k = np.max(np.nonzero(otdi)[0])
        rad[tartib[:k + 1]] = True
    return rad


def main() -> None:
    rng = np.random.default_rng(1)

    print("=== 1. Bir xil KICHIK siljish (0.02 std), turli n ===")
    d_nazariy = stats.norm.cdf(0.01) - stats.norm.cdf(-0.01)
    print(f"  haqiqiy (populyatsiya) D = {d_nazariy:.4f}, W1 = 0.0200")
    print(f"  {'n':>8} {'KS D':>7} {'W1':>7} {'KS p':>10} "
          f"{'p<0.05':>7} {'D>=0.1':>7}")
    for n in [500, 5000, 50_000, 500_000]:
        ref = rng.normal(0, 1, n)
        cur = rng.normal(0.02, 1, n)
        r = stats.ks_2samp(ref, cur, method="asymp")
        w = stats.wasserstein_distance(ref, cur)
        print(f"  {n:>8} {r.statistic:>7.4f} {w:>7.4f} {r.pvalue:>10.2e} "
              f"{str(r.pvalue < 0.05):>7} {str(r.statistic >= 0.1):>7}")
    print("  KATTA siljish (0.3 std), n=500 - solishtirish uchun:")
    ref, cur = rng.normal(0, 1, 500), rng.normal(0.3, 1, 500)
    r = stats.ks_2samp(ref, cur, method="asymp")
    w = stats.wasserstein_distance(ref, cur)
    print(f"  {500:>8} {r.statistic:>7.4f} {w:>7.4f} {r.pvalue:>10.2e} "
          f"{str(r.pvalue < 0.05):>7} {str(r.statistic >= 0.1):>7}")

    print("\n=== 2. 40 ustun, 200 kun, HECH QAYERDA drift yo'q ===")
    ustun, kun, n, alfa = 40, 200, 400, 0.05
    ref = np.sort(rng.normal(0, 1, (ustun, 2000)), axis=1)
    pq = np.empty((kun, ustun))
    for t in range(kun):
        cur = rng.normal(0, 1, (ustun, n))
        pq[t] = [ks_p(ref[j], cur[j])
                 for j in range(ustun)]
    usullar = {"tuzatishsiz": lambda p: p < alfa,
               "Bonferroni": lambda p: bonferroni(p, alfa),
               "BH (FDR)": lambda p: bh(p, alfa)}
    print(f"  {'usul':<12} {'signal/kun':>11} {'>=1 signal kunlar':>18}")
    for nom, f in usullar.items():
        rad = np.array([f(pq[t]) for t in range(kun)])
        print(f"  {nom:<12} {rad.sum(1).mean():>11.2f} "
              f"{rad.any(1).mean():>18.3f}")
    print(f"  nazariya (tuzatishsiz): 1 - 0.95^40 = {1 - 0.95 ** 40:.3f}")

    print("\n=== 3. 40 ustundan 4 tasida haqiqiy drift (0.25 std) ===")
    haqiqiy = np.zeros(ustun, dtype=bool)
    haqiqiy[:4] = True
    for t in range(kun):
        cur = rng.normal(0, 1, (ustun, n))
        cur[haqiqiy] += 0.25
        pq[t] = [ks_p(ref[j], cur[j])
                 for j in range(ustun)]
    print(f"  {'usul':<12} {'topildi (4 dan)':>16} {'yolg_on/kun':>12} "
          f"{'yolg_on ulush':>14}")
    for nom, f in usullar.items():
        rad = np.array([f(pq[t]) for t in range(kun)])
        top = rad[:, haqiqiy].sum(1).mean()
        yol = rad[:, ~haqiqiy].sum(1).mean()
        ulush = np.mean([rad[t, ~haqiqiy].sum() / max(rad[t].sum(), 1)
                         for t in range(kun)])
        print(f"  {nom:<12} {top:>16.2f} {yol:>12.2f} {ulush:>14.3f}")
    print("  ⭐ Signal = statistik sezilarli VA amaliy katta (effekt chegarasi)")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bir xil KICHIK siljish (0.02 std), turli n ===
  haqiqiy (populyatsiya) D = 0.0080, W1 = 0.0200
         n    KS D      W1       KS p  p<0.05  D>=0.1
       500  0.0700  0.1423   1.65e-01   False   False
      5000  0.0100  0.0154   9.62e-01   False   False
     50000  0.0129  0.0289   4.95e-04    True   False
    500000  0.0091  0.0218   2.00e-18    True   False
  KATTA siljish (0.3 std), n=500 - solishtirish uchun:
       500  0.1440  0.2706   5.49e-05    True    True

=== 2. 40 ustun, 200 kun, HECH QAYERDA drift yo'q ===
  usul          signal/kun  >=1 signal kunlar
  tuzatishsiz         1.74              0.810
  Bonferroni          0.06              0.050
  BH (FDR)            0.06              0.050
  nazariya (tuzatishsiz): 1 - 0.95^40 = 0.871

=== 3. 40 ustundan 4 tasida haqiqiy drift (0.25 std) ===
  usul          topildi (4 dan)  yolg_on/kun  yolg_on ulush
  tuzatishsiz              3.92         1.54          0.246
  Bonferroni               2.88         0.02          0.004
  BH (FDR)                 3.42         0.17          0.035
  ⭐ Signal = statistik sezilarli VA amaliy katta (effekt chegarasi)

Natija tahlili.

1-bo'lim — bir xil, arzimas siljish (0.02 std, populyatsiyadagi haqiqiy D = 0.0080). n = 500 va 5 000 da test "farq yo'q" dedi. n = 50 000 da p 4.95e-04, n = 500 000 da 2.00e-18 — "juda sezilarli drift". Lekin effekt o'lchami o'zgarmadi: D 0.0091, W1 0.0218 — haqiqiy qiymatga yaqinlashdi, xolos. Test "farq tasodif emas" deyapti va to'g'ri aytyapti — lekin bu farq amaliy jihatdan ahamiyatsiz. Solishtirish uchun haqiqiy katta siljish (0.3 std) atigi 500 qatorda ham ikkala mezondan o'tdi (D 0.1440). "Sezilarli VA katta" qoidasi ikkala holatni to'g'ri ajratadi.

2-bo'lim — 40 ustun, 200 kun, drift umuman yo'q. Tuzatishsiz testda kuniga o'rtacha 1.74 ta yolg'on signal va kunlarning 81% ida kamida bitta qizil ustun. Nazariy qiymat 0.871; biroz pastroq chiqqani asimptotik KS p-qiymatining kichik namunada biroz konservativ ekanidan. Bonferroni ham, BH ham yolg'on signalli kunlarni 5% ga tushirdi — to'liq nol gipotezada BH ham FWER ni nazorat qiladi.

3-bo'lim — 40 ustundan 4 tasida haqiqiy drift (0.25 std). Tuzatishsiz: 4 tadan 3.92 ta topildi, lekin kuniga 1.54 ta yolg'on — signallarning 24.6% i yolg'on. Bonferroni yolg'onni deyarli yo'q qildi (0.004), lekin haqiqiy driftlardan faqat 2.88 tasini topdi. BH o'rtada: 3.42 ta topildi, yolg'on ulush 3.5% — belgilangan 5% FDR dan past. Monitoring uchun bu odatda eng yaxshi murosa.

Misol 3 — Drift va model sifati: nima ko'rinadi, nima ko'rinmaydi

python
"""Drift va model sifati: kirish detektorlari nimani ko'radi, nimani ko'rmaydi."""

import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.model_selection import StratifiedKFold, cross_val_predict
from scipy import stats

USTUNLAR = ["x1", "x2", "x3", "x4"]


def haqiqiy_p(X, x2_koef=-1.0):
    x1, x2, x3 = X[:, 0], X[:, 1], X[:, 2]
    f = -0.3 + 1.6 * x1 - 0.5 * x1**2 + x2_koef * x2 + 0.3 * x3
    return 1 / (1 + np.exp(-f))


def yarat(rng, n, siljish=None, x2_koef=-1.0, musbat=None):
    if musbat is not None:                   # belgilar drifti: P(y) o'zgaradi,
        X, y, p = yarat(rng, 8 * n)          # P(X|y) esa o'sha-o'sha
        i1 = rng.choice(np.flatnonzero(y == 1), int(musbat * n), replace=False)
        i0 = rng.choice(np.flatnonzero(y == 0), n - len(i1), replace=False)
        i = rng.permutation(np.r_[i1, i0])
        return X[i], y[i], p[i]
    X = rng.normal(0, 1, (n, 4))
    if siljish:
        j, qiymat = siljish
        X[:, j] += qiymat
    p = haqiqiy_p(X, x2_koef)
    y = (rng.random(n) < p).astype(int)
    return X, y, p


def psi(ref, cur, bins=10, eps=1e-4):
    ichki = np.unique(np.quantile(ref, np.linspace(0, 1, bins + 1)[1:-1]))
    k = len(ichki) + 1
    p = np.bincount(np.searchsorted(ichki, ref, side="right"),
                    minlength=k) / len(ref)
    q = np.bincount(np.searchsorted(ichki, cur, side="right"),
                    minlength=k) / len(cur)
    p, q = np.clip(p, eps, None), np.clip(q, eps, None)
    return float(np.sum((q - p) * np.log(q / p)))


def domen_auc(X_ref, X_cur):
    """Reference (0) va current (1) ni ajrata oladigan klassifikator AUC i."""
    X = np.vstack([X_ref, X_cur])
    d = np.r_[np.zeros(len(X_ref)), np.ones(len(X_cur))]
    clf = HistGradientBoostingClassifier(max_iter=60, random_state=0)
    s = cross_val_predict(clf, X, d, method="predict_proba",
                          cv=StratifiedKFold(3, shuffle=True,
                                             random_state=0))[:, 1]
    return roc_auc_score(d, s)


def main() -> None:
    rng = np.random.default_rng(3)
    X_tr, y_tr, _ = yarat(rng, 6000)
    model = LogisticRegression(max_iter=1000).fit(X_tr, y_tr)
    X_ref, y_ref, _ = yarat(rng, 3000)
    p_ref = model.predict_proba(X_ref)[:, 1]
    print("=== 1. Model va reference oyna ===")
    print(f"  koeffitsientlar: "
          f"{dict(zip(USTUNLAR, np.round(model.coef_[0], 3).tolist()))}")
    print(f"  reference: P(y=1) {y_ref.mean():.3f}, AUC {roc_auc_score(y_ref, p_ref):.4f}, "
          f"aniqlik {accuracy_score(y_ref, p_ref > 0.5):.4f}")
    print("  x1, x2 - muhim; x4 - model uchun ahamiyatsiz")

    ssenariylar = {
        "A drift yo'q": dict(),
        "B x4 +1.0 (muhim emas)": dict(siljish=(3, 1.0)),
        "C x1 +1.5 (muhim)": dict(siljish=(0, 1.5)),
        "D konsept: x2 belgisi": dict(x2_koef=+1.0),
        "E belgilar: P(y)=0.7": dict(musbat=0.7),
    }
    natija = {}
    for nom, kw in ssenariylar.items():
        X, y, p_haq = yarat(rng, 3000, **kw)
        p = model.predict_proba(X)[:, 1]
        ks = [stats.ks_2samp(X_ref[:, j], X[:, j], method="asymp")
              for j in range(4)]
        signal = [USTUNLAR[j] for j in range(4)
                  if ks[j].pvalue < 0.05 / 4 and ks[j].statistic >= 0.1]
        natija[nom] = {
            "signal": ",".join(signal) if signal else "-",
            "domen": domen_auc(X_ref, X),
            "psi_p": psi(p_ref, p),
            "taxmin": float(np.mean(np.maximum(p, 1 - p))),
            "aniqlik": accuracy_score(y, p > 0.5),
            "auc": roc_auc_score(y, p),
            "ideal": roc_auc_score(y, p_haq),
        }

    print("\n=== 2. Kirish detektorlari ===")
    print(f"  {'ssenariy':<24} {'KS signal':>10} {'domen AUC':>10} "
          f"{'bashorat PSI':>13}")
    for nom, r in natija.items():
        print(f"  {nom:<24} {r['signal']:>10} {r['domen']:>10.3f} "
              f"{r['psi_p']:>13.4f}")

    print("\n=== 3. Haqiqiy sifat (belgilar kelgach) ===")
    print(f"  {'ssenariy':<24} {'taxmin':>7} {'aniqlik':>8} {'AUC':>7} "
          f"{'ideal AUC':>10}")
    for nom, r in natija.items():
        print(f"  {nom:<24} {r['taxmin']:>7.3f} {r['aniqlik']:>8.3f} "
              f"{r['auc']:>7.3f} {r['ideal']:>10.3f}")

    print("\n=== 4. Xulosa (natijadan hisoblangan) ===")
    asos = natija["A drift yo'q"]["auc"]
    for nom, r in natija.items():
        korindi = r["signal"] != "-" or r["domen"] > 0.6
        tushdi = (asos - r["auc"] > 0.02
                  or natija["A drift yo'q"]["aniqlik"] - r["aniqlik"] > 0.02)
        xato = abs(r["taxmin"] - r["aniqlik"])
        print(f"  {nom:<24} drift ko'rindi: {str(korindi):<5} "
              f"sifat tushdi: {str(tushdi):<5} |taxmin-haqiqiy| {xato:.3f}")
    print("  ⭐ Kirish drifti != sifat tushishi; konsept drift - ko'rinmas")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Model va reference oyna ===
  koeffitsientlar: {'x1': 1.298, 'x2': -1.01, 'x3': 0.294, 'x4': -0.015}
  reference: P(y=1) 0.401, AUC 0.8313, aniqlik 0.7533
  x1, x2 - muhim; x4 - model uchun ahamiyatsiz

=== 2. Kirish detektorlari ===
  ssenariy                  KS signal  domen AUC  bashorat PSI
  A drift yo'q                      -      0.508        0.0092
  B x4 +1.0 (muhim emas)           x4      0.732        0.0110
  C x1 +1.5 (muhim)                x1      0.847        1.2674
  D konsept: x2 belgisi             -      0.501        0.0089
  E belgilar: P(y)=0.7             x1      0.582        0.1650

=== 3. Haqiqiy sifat (belgilar kelgach) ===
  ssenariy                  taxmin  aniqlik     AUC  ideal AUC
  A drift yo'q               0.763    0.758   0.837      0.851
  B x4 +1.0 (muhim emas)     0.755    0.755   0.829      0.838
  C x1 +1.5 (muhim)          0.800    0.661   0.662      0.785
  D konsept: x2 belgisi      0.758    0.579   0.594      0.842
  E belgilar: P(y)=0.7       0.739    0.707   0.825      0.833

=== 4. Xulosa (natijadan hisoblangan) ===
  A drift yo'q             drift ko'rindi: False sifat tushdi: False |taxmin-haqiqiy| 0.005
  B x4 +1.0 (muhim emas)   drift ko'rindi: True  sifat tushdi: False |taxmin-haqiqiy| 0.000
  C x1 +1.5 (muhim)        drift ko'rindi: True  sifat tushdi: True  |taxmin-haqiqiy| 0.139
  D konsept: x2 belgisi    drift ko'rindi: False sifat tushdi: True  |taxmin-haqiqiy| 0.179
  E belgilar: P(y)=0.7     drift ko'rindi: True  sifat tushdi: True  |taxmin-haqiqiy| 0.032
  ⭐ Kirish drifti != sifat tushishi; konsept drift - ko'rinmas

Natija tahlili.

Haqiqiy bog'liqlik x1 bo'yicha nochiziqli (parabola: x1 ~ 1.6 atrofida xavf eng yuqori), model esa logistik regressiya — reference hududida (x1 ~ N(0, 1)) bu yaqinlashish yaxshi ishlaydi (AUC 0.8313). x4 ga model deyarli nol vazn bergan (-0.015).

  • A — drift yo'q. Detektorlar jim (domen AUC 0.508), ishonch asosidagi taxmin (0.763) haqiqiy aniqlikka (0.758) juda yaqin — kalibrlangan model uchun g'oya ishlaydi.
  • B — ahamiyatsiz ustunda drift. KS x4 ni ushladi, domen AUC 0.732 — "katta drift". Lekin bashorat PSI 0.0110 (o'zgarmadi) va AUC 0.829 — A dagi 0.837 dan farq 0.02 chegarasidan kichik. Bu yolg'on xavotir; uni bashorat drifti to'g'ri ajratdi.
  • C — muhim ustunda drift. x1 ma'lumotining katta qismi parabola cho'qqisi atrofiga va undan keyingi hududga o'tdi — u yerda xavf x1 bilan endi o'smaydi, balki kamayadi, chiziqli model esa buni bilmaydi. AUC 0.662 ga tushdi, garchi ideal (haqiqiy ehtimollar bilan) AUC 0.785 bo'lsa ham — ya'ni vazifa qiyinlashgani emas, model yomonlashgani. Bashorat PSI 1.2674 — juda katta. Diqqat: ishonch asosidagi taxmin 0.800, haqiqiy aniqlik 0.661 — model yangi hududda ortiqcha ishonchli, taxmin aldanadi.
  • D — konsept drift. x2 ning ta'siri teskarisiga o'zgardi, X taqsimoti o'sha-o'sha. KS signal yo'q, domen AUC 0.501, bashorat PSI 0.0089 — hamma kirish detektorlari jim. Taxmin 0.758 — "hammasi joyida". Haqiqiy aniqlik esa 0.579, AUC 0.594. Bu drift faqat belgilar kelgach ko'rinadi.
  • E — belgilar drifti. Ketuvchilar ulushi 0.40 dan 0.70 ga oshdi. X ham o'zgardi (KS x1 ni ushladi, bashorat PSI 0.1650). AUC deyarli o'zgarmadi (0.825) — tartiblash sifati saqlanadi; lekin 0.5 chegarasidagi aniqlik 0.758 dan 0.707 ga tushdi, chunki model eski ulushga kalibrlangan.

4-bo'lim jadvali darsning asosiy fikrini raqam bilan beradi: "drift ko'rindi" va "sifat tushdi" ustunlari bir-biriga mos kelmaydi (B va D).

Misol 4 — Oqim ustida: Page-Hinkley va oynali test

python
"""Oqim ustida drift: Page-Hinkley noldan, oynali test bilan solishtirish."""

import numpy as np
from scipy import stats


class PageHinkley:
    """O'rtachaning OSHISHINI kuzatadi (masalan, xato darajasi)."""

    def __init__(self, delta=0.01, lam=20.0, min_n=30):
        self.delta, self.lam, self.min_n = delta, lam, min_n
        self.n, self.orta, self.m, self.m_min = 0, 0.0, 0.0, 0.0

    def yangila(self, x):
        self.n += 1
        self.orta += (x - self.orta) / self.n
        self.m += x - self.orta - self.delta
        self.m_min = min(self.m_min, self.m)
        return self.n >= self.min_n and self.m - self.m_min > self.lam


def oqim(rng, turi, uzunlik=4000, ozgarish=2000, p0=0.20, p1=0.30):
    """Har qadamda model xato qildimi (1) yoki yo'q (0)."""
    p = np.full(uzunlik, p0)
    if turi == "to'satdan":
        p[ozgarish:] = p1
    elif turi == "asta-sekin":
        p[ozgarish:] = np.minimum(
            p1, p0 + (p1 - p0) * np.arange(uzunlik - ozgarish) / 1000)
    elif turi == "mavsumiy":
        p = p0 + 0.05 * np.sin(2 * np.pi * np.arange(uzunlik) / 1000)
    return (rng.random(uzunlik) < p).astype(float)


def birinchi_signal(detektor, xlar):
    for t, x in enumerate(xlar):
        if detektor.yangila(x):
            return t
    return None


def oynali_signal(xlar, ref=1000, oyna=500, qadam=100, alfa=0.001):
    """Birinchi 1000 qadam - reference; har 100 qadamda oxirgi 500 ni z-test."""
    p_ref = xlar[:ref].mean()
    for t in range(ref + oyna, len(xlar) + 1, qadam):
        p_cur = xlar[t - oyna:t].mean()
        p_bir = (p_ref * ref + p_cur * oyna) / (ref + oyna)
        se = np.sqrt(p_bir * (1 - p_bir) * (1 / ref + 1 / oyna))
        if se > 0 and stats.norm.sf((p_cur - p_ref) / se) < alfa:
            return t - 1
    return None

def baho(signal_ol, turi, seedlar, ozgarish=2000):
    """(yolg'on signal ulushi, {seed: kechikish}) - faqat o'zgarishdan keyin."""
    yolgon, kechikish = 0, {}
    for s in seedlar:
        t = signal_ol(oqim(np.random.default_rng(s), turi))
        if turi in ("yo'q", "mavsumiy"):
            yolgon += t is not None
        elif t is not None and t < ozgarish:
            yolgon += 1
        elif t is not None:
            kechikish[s] = t - ozgarish
    return yolgon / len(seedlar), kechikish


def main() -> None:
    seedlar = range(100)

    print("=== 1. Bitta oqim: xato darajasi 0.20 -> 0.30 (t=2000) ===")
    xlar = oqim(np.random.default_rng(7), "to'satdan")
    t = birinchi_signal(PageHinkley(lam=40), xlar)
    print(f"  Page-Hinkley (lambda=40) signal: t={t}, kechikish {t - 2000}")
    print(f"  oynali z-test signal:            t={oynali_signal(xlar)}")
    for a in [1000, 1900, 2100, 2500]:
        print(f"  t={a}: oxirgi 200 qadamdagi xato "
              f"{xlar[a - 200:a].mean():.3f}")

    print("\n=== 2. Lambda: yolg'on signal va kechikish (100 oqim) ===")
    print(f"  {'detektor':<16} {'yolg_on':>8} {'to_satdan':>10} "
          f"{'topildi':>8} {'asta-sekin':>11} {'mavsumiy':>9}")
    detektorlar = [(f"PH lambda={lam}",
                    lambda x, lam=lam: birinchi_signal(PageHinkley(lam=lam), x))
                   for lam in [10, 20, 40, 60, 80]]
    detektorlar.append(("oynali z-test", oynali_signal))
    jadval = {}
    for nom, f in detektorlar:
        yolgon, _ = baho(f, "yo'q", seedlar)
        _, k_tos = baho(f, "to'satdan", seedlar)
        _, k_ast = baho(f, "asta-sekin", seedlar)
        mavs, _ = baho(f, "mavsumiy", seedlar)
        jadval[nom] = (yolgon, k_tos)
        med = lambda k: np.median(list(k.values())) if k else float("nan")
        print(f"  {nom:<16} {yolgon:>8.2f} {med(k_tos):>10.0f} "
              f"{len(k_tos) / len(seedlar):>8.2f} {med(k_ast):>11.0f} "
              f"{mavs:>9.2f}")
    print("  kechikish - mediana (qadam); topildi - o'zgarishdan KEYIN")
    print("  birinchi signal bergan oqimlar ulushi")

    print("\n=== 3. Qaror: yolg'on signal <= 0.05 bo'lganlar ichida ===")
    nomzod = sorted((np.median(list(k.values())), nom)
                    for nom, (yo, k) in jadval.items() if yo <= 0.05 and k)
    (m1, a), (m2, b) = nomzod[0], nomzod[1]
    print(f"  eng tez ikkitasi: {a} ({m1:.0f}), {b} ({m2:.0f})")
    umumiy = sorted(set(jadval[a][1]) & set(jadval[b][1]))
    d = np.array([jadval[b][1][s] - jadval[a][1][s] for s in umumiy])
    se = d.std(ddof=1) / np.sqrt(len(d))
    print(f"  juftlashgan farq ({b} - {a}): {d.mean():+.1f} qadam, "
          f"SE {se:.1f}, sezilarli: {abs(d.mean()) > 2 * se}")
    if abs(d.mean()) > 2 * se:
        print(f"  tanlov: {a if d.mean() > 0 else b}")
    else:
        print("  tezlikda farq yo'q -> boshqa mezon: mavsumiy yolg'on signal")
        print("  va xotira (PH: O(1), oyna: 1500 qiymat)")
    print("  ⭐ Tezlik va yolg'on signal - bitta tugmaning ikki uchi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bitta oqim: xato darajasi 0.20 -> 0.30 (t=2000) ===
  Page-Hinkley (lambda=40) signal: t=2376, kechikish 376
  oynali z-test signal:            t=2499
  t=1000: oxirgi 200 qadamdagi xato 0.225
  t=1900: oxirgi 200 qadamdagi xato 0.150
  t=2100: oxirgi 200 qadamdagi xato 0.240
  t=2500: oxirgi 200 qadamdagi xato 0.310

=== 2. Lambda: yolg'on signal va kechikish (100 oqim) ===
  detektor          yolg_on  to_satdan  topildi  asta-sekin  mavsumiy
  PH lambda=10         0.99         76     0.16         344      1.00
  PH lambda=20         0.39        150     0.81         604      0.79
  PH lambda=40         0.01        412     1.00         932      0.02
  PH lambda=60         0.00        697     1.00        1193      0.00
  PH lambda=80         0.00        996     0.99        1518      0.00
  oynali z-test        0.02        399     1.00         999      0.10
  kechikish - mediana (qadam); topildi - o'zgarishdan KEYIN
  birinchi signal bergan oqimlar ulushi

=== 3. Qaror: yolg'on signal <= 0.05 bo'lganlar ichida ===
  eng tez ikkitasi: oynali z-test (399), PH lambda=40 (412)
  juftlashgan farq (PH lambda=40 - oynali z-test): -2.5 qadam, SE 10.0, sezilarli: False
  tezlikda farq yo'q -> boshqa mezon: mavsumiy yolg'on signal
  va xotira (PH: O(1), oyna: 1500 qiymat)
  ⭐ Tezlik va yolg'on signal - bitta tugmaning ikki uchi

Natija tahlili.

1-bo'lim — bitta oqimda xato darajasi t = 2000 da 0.20 dan 0.30 ga oshdi. Page-Hinkley (lambda = 40) t = 2376 da signal berdi — kechikish 376 qadam. Oynali z-test t = 2499 da. Oxirgi 200 qadam xato ulushiga qarang: o'zgarishdan oldin ham u 0.150 dan 0.225 gacha tebranadi — shuning uchun detektor bir necha yuz qadam "dalil to'plashi" kerak.

2-bo'lim — 100 oqimda murosa. lambda = 10 juda sezgir: drift bo'lmagan oqimlarning 99% ida yolg'on signal, to'satdan driftning ko'pida signal o'zgarishdan oldin chiqib ketgan (o'zgarishdan keyingi birinchi signal faqat 16% oqimda). lambda oshgan sari yolg'on signal kamayadi (0.39 → 0.01 → 0.00), kechikish esa oshadi (150 → 412 → 697 → 996 qadam). Asta-sekin driftda kechikish ancha katta (lambda = 40 da 932) — o'zgarish kichik qadamlar bilan yig'iladi. Mavsumiy oqimda (drift emas) sezgir detektorlar muntazam yolg'on signal beradi (lambda = 20 da 0.79).

3-bo'lim — qaror oldindan belgilangan mezon bilan: yolg'on signal <= 0.05 bo'lganlar ichida eng tezi. Oynali z-test (399) va PH lambda = 40 (412) — bir xil oqimlarda juftlashgan farq -2.5 qadam, SE 10.0 — sezilarli emas. Tezlik bir xil ekan, boshqa mezonlar hal qiladi: mavsumiy oqimda PH 0.02, oynali test 0.10 yolg'on signal berdi, PH esa O(1) xotira bilan ishlaydi. Bu natija faqat shu sozlamalar (oyna 500, qadam 100) uchun — ularni o'zgartirsangiz, taqqoslashni qayta o'tkazing.


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

Noto'g'ri fikr To'g'risi
"p < 0.05 — demak drift bor va muhim" Katta n da arzimas farq ham sezilarli; effekt o'lchami kerak
"Drift bor — qayta o'qitish kerak" Ahamiyatsiz ustundagi drift sifatga ta'sir qilmaydi (B)
"Kirish detektorlari jim — model yaxshi" Konsept drift X ni o'zgartirmaydi (D)
"PSI — bitta aniq raqam" Bin soni, bin turi va eps ga bog'liq
"Har ustunni alohida tekshirish yetarli" Ko'p testlash + korrelyatsiya o'zgarishi ko'rinmaydi
"Ishonch asosidagi taxmin sifatni o'lchaydi" Faqat kalibrlangan va konsept drift yo'q holda
"Belgilar drifti AUC ni tushiradi" AUC deyarli saqlanadi, chegara va kalibrovka buziladi
"Sezgir detektor — yaxshi detektor" Sezgirlik = ko'p yolg'on signal; murosa kerak
"Mavsumiy o'zgarish ham drift" Kutilgan o'zgarish; reference mavsumni qamrasin

6. Keng tarqalgan xatolar va yechimlari

1. Faqat p-qiymat

python
drift = stats.ks_2samp(ref, cur).pvalue < 0.05                    # ⚠️
r = stats.ks_2samp(ref, cur)
drift = (r.pvalue < 0.05 / m) and (r.statistic >= 0.1)             # ✅

2. Tuzatishsiz ko'p test

python
signallar = [c for c in ustunlar if p[c] < 0.05]                    # ⚠️
signallar = [c for c, r in zip(ustunlar, bh(p_massiv)) if r]        # ✅

3. Teng kenglikdagi binlar

python
chegaralar = np.linspace(ref.min(), ref.max(), 11)                  # ⚠️
chegaralar = np.quantile(ref, np.linspace(0, 1, 11))                # ✅

4. Chegaralarni current dan olish

python
chegaralar = np.quantile(np.r_[ref, cur], q)                        # ⚠️
chegaralar = np.quantile(ref, q)                                    # ✅ faqat reference

5. Yangi toifani eps ga yashirish

python
psi_kat(ref, cur, eps=1e-4)                                         # ⚠️ yolg'iz
korilmagan = np.mean(~np.isin(cur, np.unique(ref)))                 # ✅ alohida metrika

6. Faqat kirish drifti

python
if max_psi < 0.1: print("model sog'lom")                           # ⚠️
# belgilar kelgach: haqiqiy AUC/aniqlik reference bilan              # ✅

7. Kalibrlanmagan oqim detektori

python
ph = PageHinkley(lam=10)                                            # ⚠️ taxminiy
# lambda ni drift yo'q tarixiy oqimda yolg'on signal darajasi bo'yicha tanlang  # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 11.7, 11.9-darslar (o'tilgan): Noparametrik testlar (KS) va ko'p taqqoslash
  • 14-qism, 18.9-dars (o'tilgan): Kalibrovka va metrika tanlash
  • 27.3-dars (o'tilgan): Ma'lumot quvuridagi validatsiya — sxema buzilishi drift emas, xato
  • 27.11-dars (o'tilgan): Monitoring — drift metrikalari shu tizimga yoziladi
  • 27.13-dars: Qayta o'qitish triggeri va o'quv oynasi drift turiga qarab
  • 27.14-dars: Amaliyot — oylik partiyalarda drift, kechikkan belgilar va trigger

8. Eng yaxshi amaliyotlar

  1. Drift turini aniqlang: P(X), P(y|X) yoki P(y).

  2. Signal = sezilarli VA katta; effekt chegarasini oldindan kelishing.

  3. Ko'p ustunda BH yoki Bonferroni.

  4. PSI uchun kvantil binlar, reference dan, n ga mos bin soni.

  5. Muhim ustunlarga va bashorat driftiga ustuvorlik bering.

  6. Domen klassifikatori bilan ko'p o'lchovli tekshiruv.

  7. Sifatni belgilar bilan albatta o'lchang — konsept drift faqat shunda ko'rinadi.

  8. Oqim detektorini drift yo'q tarixda kalibrlang; mavsumni hisobga oling.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # P(y|X) o'zgarsa, X taqsimoti o'zgaradimi?
2.  # PSI chegaralari qaysi to'plamdan olinadi?
3.  # drift yo'q, n=500, 100 bin - PSI qanday bo'ladi?
4.  # bitta xato yozuv teng kenglikdagi PSI ga nima qiladi?
5.  # 0.02 std siljish, n=500000 - KS p-qiymat?
6.  # 40 ustun, alfa=0.05, drift yo'q - kamida bitta signal ehtimoli?
7.  # BH nimani nazorat qiladi?
8.  # domen AUC 0.5 nimani bildiradi?
9.  # ahamiyatsiz ustunda drift - bashorat PSI?
10. # belgilar driftida AUC?
11. # konsept driftda ishonch asosidagi taxmin?
12. # Page-Hinkley lambda oshsa?
Javoblar
  1. Yo'q — konsept drift X ni o'zgartirmasligi mumkin
  2. Faqat reference
  3. Shovqin tufayli katta (> 0.1) — yolg'on drift
  4. Hamma ma'lumot bitta binga tushadi, PSI ~ 0
  5. Juda kichik ("sezilarli"), effekt esa arzimas
  6. 1 - 0.95^40 ~ 0.87
  7. FDR — signallar ichidagi yolg'onlar ulushini
  8. Ikki to'plamni ajratib bo'lmaydi — drift yo'q
  9. Deyarli o'zgarmaydi
  10. Deyarli o'zgarmaydi; aniqlik va kalibrovka buziladi
  11. "Hammasi joyida" deydi — aldanadi
  12. Yolg'on signal kamayadi, kechikish oshadi

Vazifa 2: Xatolarni tuzating

python
1.  drift = stats.ks_2samp(ref, cur).pvalue < 0.05

2.  chegaralar = np.linspace(ref.min(), ref.max(), 11)

3.  signallar = [c for c in ustunlar if p[c] < 0.05]   # 60 ustun

4.  chegaralar = np.quantile(np.r_[ref, cur], q)

5.  if domen_auc < 0.6 and max_psi < 0.1: print("model sog'lom")
Javoblar
python
1.  r = stats.ks_2samp(ref, cur); drift = r.pvalue < 0.05 / m and r.statistic >= 0.1

2.  chegaralar = np.quantile(ref, np.linspace(0, 1, 11))

3.  signallar = [c for c, rad in zip(ustunlar, bh(p_massiv)) if rad]

4.  chegaralar = np.quantile(ref, q)

5.  # kirish jim bo'lsa ham: belgilar kelgach haqiqiy sifatni reference bilan solishtiring

Vazifa 3: O'lchovlar

Modellang (1-misol asosida):

  1. psi ni kategoriyali ustun uchun yozing va chi2_noldan bilan bir xil ma'lumotda solishtiring
  2. Bin soni 10 va current n = 200, 1000, 5000 da drift yo'q PSI shovqin tagligini o'lchang
  3. (bins - 1) * (1/n_ref + 1/n_cur) formulasi bilan solishtiring
  4. W1 ni turli birlikdagi ikki ustunda hisoblang va std ga bo'lish nega kerakligini ko'rsating

Vazifa 4: Tuzatishlar

Modellang (2-misol asosida):

  1. Ustunlar sonini 10, 40, 100 qiling — tuzatishsiz yolg'on kunlar ulushi
  2. Haqiqiy drift kattaligini 0.1, 0.25, 0.5 std qiling — BH va Bonferroni quvvati
  3. "BH + D >= 0.1" birlashgan qoidani qo'shing
  4. Natijani jadval qilib, monitoring uchun qoida taklif qiling

Vazifa 5: Drift va sifat

Modellang (3-misol asosida):

  1. Logistik regressiya o'rniga HistGradientBoosting — C ssenariysida sifat qancha tushadi?
  2. Korrelyatsiya drifti: x1 va x2 ni o'zaro bog'lang (marginallari o'zgarmasin) — KS va domen AUC
  3. Domen klassifikatorining belgi ahamiyatini chiqarib, qaysi ustun siljiganini toping
  4. E ssenariysi uchun chegarani yangi ulushga moslab (belgilar kelgach) aniqlikni tiklang

Vazifa 6: Oqim

Modellang (4-misol asosida):

  1. Page-Hinkley ga delta ni 0.005, 0.01, 0.02 qilib, kechikishga ta'sirini o'lchang
  2. Signaldan keyin detektorni qayta boshlab (reset), bitta oqimdagi signallar sonini sanang
  3. Mavsumiy oqim uchun reference ni "o'tgan davr" qilib, yolg'on signalni kamaytiring
  4. Oddiy ADWIN g'oyasini yozing: oynani ikki qismga bo'lib, o'rtachalar farqini Hoeffding chegarasi bilan solishtiring

Vazifa 7: O'ylash

Drift panelida 60 ta ustun. Har kuni 3-4 tasi qizil. Jamoa signallarga qaramay qo'ygan. Rahbar: "Panel ishlamayapti, uni o'chiramiz — oyiga bir marta qayta o'qitamiz, bo'ldi" dedi. Nima deysiz?

Javob

Qisqa javob: panelni o'chirish emas, qayta sozlash kerak. Hozirgi panel noto'g'ri savolga javob beryapti, lekin driftni kuzatish zaruratini bekor qilmaydi.

1. Nega har kuni qizil. 60 ta ustun, tuzatishsiz alfa = 0.05: drift bo'lmasa ham kuniga o'rtacha 3 ta yolg'on signal (2-misol: 40 ustunda 1.74, kunlarning 81% ida kamida bitta). Katta hajmli ma'lumotda esa har qanday arzimas o'zgarish "sezilarli" (1-bo'lim: p = 2e-18 da D = 0.009). Panel o'z vazifasini "to'g'ri" bajaryapti — faqat vazifa noto'g'ri qo'yilgan.

2. Qanday tuzatiladi.

python
# 1) ko'p testlash: BH (FDR 5%)
# 2) effekt chegarasi: D >= 0.1 yoki PSI >= 0.1 (biznes bilan kelishilgan)
# 3) ustuvorlik: belgi ahamiyati yuqori ustunlar + bashorat PSI
# 4) ko'p o'lchovli: haftada bir domen klassifikatori AUC
# 5) sifat: belgilar kelgach haqiqiy AUC - bu ASOSIY signal

3-misolda aynan shu ko'rindi: ahamiyatsiz ustundagi drift (B) sifatga ta'sir qilmadi, bashorat PSI esa uni to'g'ri e'tiborsiz qoldirdi.

3. "Oyiga bir marta qayta o'qitamiz" nega yetarli emas.

  • To'satdan drift (ma'lumot manbai buzildi, yangi versiya) oy oxirini kutmaydi — zarar shu kuniyoq boshlanadi.
  • Konsept drift paytida qayta o'qitish yordam beradi, lekin faqat yangi belgilar yetarli to'plangandan keyin; qachon ekanini bilish uchun sifat monitoringi kerak.
  • Qayta o'qitish ham xato qilishi mumkin (buzilgan ma'lumotda o'qitish). Drift va sifat monitoringi yangi modelni tekshirish uchun ham kerak (27.13 — darvoza).

Rahbarga javob: "Panel shovqin chiqaryapti, chunki 60 ta testni tuzatishsiz va effekt chegarasisiz ishlatyapmiz. Uni FDR nazorati, amaliy chegara va muhim ustunlarga ustuvorlik bilan qayta sozlaymiz — kutilgan yolg'on signal haftasiga bir martadan kam bo'ladi. Asosiy signal esa belgilar kelgach haqiqiy sifat bo'ladi. Muntazam qayta o'qitish yaxshi g'oya, lekin u monitoringning o'rnini bosmaydi."

Nimani mustahkamlaydi: 2.5, 2.6, 2.9-bo'limlar.


Xulosa

Bu darsda driftni aniqlash va talqin qilishni o'rgandik.

Eng muhim uch fikr:

  1. Drift uch xil va ular turli detektorga ko'rinadi. Kovariat drift P(X), konsept drift P(y|X), belgilar drifti P(y). 3-misolda ahamiyatsiz ustundagi drift barcha kirish detektorlarini yoqdi (domen AUC 0.732), lekin sifatni o'zgartirmadi; muhim ustundagi drift AUC ni 0.662 ga tushirdi; konsept drift esa kirish detektorlarining birortasiga ko'rinmasdan (domen AUC 0.501) AUC ni 0.594 ga tushirdi. Belgilarsiz ishonch taxmini aynan xavfli holatlarda aldandi (0.758 taxmin, 0.579 haqiqiy). Shuning uchun monitoring ikki qavatli: tez kirish/bashorat drifti va belgilar kelgach haqiqiy sifat.

  2. Signal = sezilarli VA katta, ko'p testlash tuzatilgan holda. 2-misolda arzimas siljish (D = 0.009) 500 000 qatorda p = 2e-18 berdi; 40 ta ustunni tuzatishsiz tekshirish drift bo'lmagan kunlarning 81% ida qizil chiroq yoqdi. BH yolg'on signal ulushini 3.5% ga tushirib, haqiqiy driftlarning ko'pini (3.42/4) saqladi. PSI esa bin tanlashiga sezgir: ko'p bin + kichik n yolg'on drift beradi, teng kenglik va bitta xato yozuv driftni butunlay yashiradi.

  3. Oqimda kechikish va yolg'on signal — bitta murosa. Page-Hinkley noldan bir necha qatorda yoziladi va O(1) xotira bilan ishlaydi. lambda oshsa, yolg'on signal 0.99 dan 0.00 gacha kamaydi, kechikish esa 76 dan 996 qadamgacha oshdi. Chegarani drift yo'q tarixda kalibrlab, oldindan belgilangan mezon bilan tanlaymiz; mavsumiy o'zgarish drift emas — reference uni qamrashi kerak.

Keyingi darsda Qayta o'qitish va A/B test: drift aniqlangandan keyin nima qilish kerak — qachon va qanday oynada qayta o'qitish, yangi modelni shadow rejimda sinash, darvoza qoidalari, onlayn A/B test va uning "peeking" tuzog'i, ko'p qo'lli banditlar va avtomatik rollback.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
27.12-dars: Drift aniqlash — IlmHamroh