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

27.13-dars: Qayta o'qitish va A/B test

27-QISM — MLOPS VA DEPLOY · 13-dars


1. Kirish va motivatsiya

Oldingi darsda driftni aniqlashni o'rgandik va eng muhim xulosaga keldik: drift signali — savol, javob emas. Endi javob tomoniga o'tamiz. Model eskirdi — keyin nima? Uchta savol tug'iladi:

  1. Qachon qayta o'qitish kerak — jadval bo'yicha, drift signali bo'yicha yoki uzluksiz?
  2. Qaysi ma'lumotda — butun tarixdami, faqat oxirgi oylardami yoki eski ma'lumotga kamroq vazn berib?
  3. Yangi model haqiqatan yaxshiroqmi — va buni foydalanuvchilarga zarar yetkazmasdan qanday bilamiz?

Uchinchi savol eng nozigi. Oflayn metrika (validatsiyadagi AUC) yangi model yaxshiroq deydi. Lekin ishlab chiqarishda boshqa narsalar ham bor: modelning bashoratlari foydalanuvchi xulqini o'zgartiradi, biznes metrikasi (konversiya, daromad) AUC bilan to'g'ridan-to'g'ri bog'liq emas, yangi kod versiyasida kutilmagan xato bo'lishi mumkin. Shuning uchun almashtirish bosqichma-bosqich bo'ladi: avval shadow rejim (yangi model javob beradi, lekin javob foydalanuvchiga bormaydi), keyin oldindan yozilgan darvoza qoidalari, keyin canary (trafikning kichik qismi), va nihoyat onlayn A/B test — biznes metrikasi bo'yicha haqiqiy tajriba.

A/B test o'zi ham tuzoqlarga to'la. Eng mashhuri — peeking: tajribani har kuni ochib, "sezilarli bo'ldi — to'xtatamiz" deyish. Bu 11-qismda ko'rilgan edi; bu darsda uni simulyatsiya bilan o'lchaymiz va yolg'on musbat ulushi 5% dan qanchaga oshishini ko'ramiz. Oxirida A/B testning alternativasi — ko'p qo'lli banditlarni noldan yozamiz va ular qachon foydali, qachon zararli ekanini ko'ramiz.

Real vaziyat. Onlayn do'kon tavsiya modelini har hafta avtomatik qayta o'qitardi va yangi versiyani darhol ishga tushirardi — "yangi ma'lumot = yaxshiroq model" deb. Bir hafta ma'lumot yig'uvchi xizmatda xato bo'ldi: bir mintaqa buyurtmalari yozilmadi. Qayta o'qitilgan model shu mintaqa haqida hech narsa bilmay qoldi va ishga tushdi. Umumiy oflayn metrika hatto biroz yaxshilangan edi — chunki qolgan mintaqalarda model yangilangan. Muammo ikki hafta keyin, mintaqa bo'yicha savdo tushib ketganda aniqlandi. Oldindan yozilgan darvoza qoidasi — "har segmentda yomonlashuv chegaradan oshmasin" — bu versiyani birinchi kuniyoq to'xtatgan bo'lardi. Bu darsning 2-misoli aynan shu holatni raqam bilan ko'rsatadi.

Bu darsda modelni xavfsiz yangilash tartibini quramiz: qachon, qaysi ma'lumotda, qanday tekshiruv bilan — va xato bo'lsa, qanday orqaga qaytish.

Bu darsda:

  • Qayta o'qitish strategiyalari: jadval, trigger, uzluksiz
  • O'quv oynasi: butun tarix, oxirgi N oy, vaznli — drift turiga qarab
  • Champion/challenger va shadow rejim
  • Darvoza qoidalari — natijani ko'rishdan oldin
  • Canary, nazorat namunalari va avtomatik rollback
  • Onlayn A/B test: xesh bilan taqsimlash, quvvat, test, ishonch intervali
  • Peeking muammosi va guardrail metrikalar
  • Ko'p qo'lli bandit: epsilon-greedy va Thompson sampling
  • CT (continuous training) quvuri
  • Tuzoqlar

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


2. Nazariya — chuqur tushuntirish

2.1. Qachon qayta o'qitish?

text
1. JADVAL BO'YICHA (har hafta / har oy)
   + oddiy, bashorat qilinadigan, resurs rejalashtiriladi
   - drift bo'lmasa - bekor resurs; to'satdan driftda - kechikish
   - jadval "keyingi o'qitish" gacha zararni to'xtatmaydi

2. TRIGGER BO'YICHA
   trigger: sifat tushishi (belgilar kelgach) > chegara
            muhim ustun / bashorat drifti > chegara 27.12-bob
            yetarli yangi belgi to'plandi (masalan, 10 000 ta)
   + resurs faqat kerak bo'lganda
   - trigger chegaralarini sozlash kerak; belgilar kechikadi

3. UZLUKSIZ (onlayn o'rganish, har hodisadan keyin)
   + eng tez moslashadi
   - barqarorlik xavfi, zaharlanish (poisoning), sinash qiyin
   - faqat ba'zi vazifalar uchun (reklama, tavsiya)

AMALDA: jadval + trigger aralashmasi
  "har oy, yoki sifat 0.03 dan ko'p tushsa - darhol"
  har holda: qayta o'qitilgan model DARVOZA dan o'tadi, avtomatik emas

Qayta o'qitish — yangi model yaratish, uni ishga tushirish emas. Ishga tushirish — alohida, tekshiriladigan qaror.

2.2. O'quv oynasi

text
BUTUN TARIX:   [=========================]  hamma ma'lumot, teng vazn
OXIRGI N OY:            [======]            faqat yangi ma'lumot
VAZNLI:        [.:::::|||||||||||]          vazn = 0.5^(yosh / yarim_umr)

                barqaror   to'satdan   asta-sekin   qaytuvchi
butun tarix       yaxshi      sekin       sekin       o'rtacha
oxirgi N oy      biroz shov.  tez         tez         har almashishda
                                                      kechikadi
vaznli           yaxshi       o'rtacha    o'rtacha    o'rtacha
statik (hech)    yaxshi      yomon       yomon       ?

Nazariy murosa — dispersiya va siljish (bias). Ko'p ma'lumot dispersiyani kamaytiradi, lekin eski rejim model ichiga "siljish" olib kiradi. Qaysi biri og'irroq — drift turiga va har oydagi ma'lumot hajmiga bog'liq. Shuning uchun tanlov jadvaldan emas, vaqt bo'yicha simulyatsiyadan (backtest) keladi: o'tmishni oyma-oy "qayta o'ynab", har strategiya keyingi oyda qanday ishlaganini o'lchaymiz.

text
VAQT BO'YICHA BACKTEST:
  for t in baholash_oylari:
      o'quv = strategiya(0..t-1 oylar)     # kelajakka qarash YO'Q
      model = orgat(o'quv)
      ball[t] = metrika(model, t-oy)       # keyingi oy
  taqqoslash: bir xil oylar va urug'lar bo'yicha JUFTLASHGAN farq

2.3. Champion/challenger va shadow rejim

text
                  so'rov
                    |
          +---------+----------+
          v                    v
     CHAMPION (v1)        CHALLENGER (v2)       <- shadow
          |                    |
     javob -> foydalanuvchi   javob -> faqat LOG
                               (xato bo'lsa ham foydalanuvchi sezmaydi)

  belgilar kelgach: BIR XIL so'rovlarda juftlashgan taqqoslash

Shadow rejimning afzalliklari:

  • Bir xil so'rovlar — taqqoslash juftlashgan, shuning uchun sezgir (so'rovlar orasidagi farq yo'qoladi).
  • Xavfsiz — challenger xato qilsa yoki yiqilsa, foydalanuvchi champion javobini oladi.
  • Haqiqiy trafik — test to'plamida yo'q holatlar (buzuq so'rovlar, yangi toifalar, kechikish) ko'rinadi.

Cheklovi: shadow rejim modelning ta'sirini o'lchamaydi. Agar model qarori foydalanuvchi xulqini o'zgartirsa (masalan, chegirma taklifi), bu faqat A/B testda ko'rinadi.

2.4. Darvoza qoidalari

text
DARVOZA (oldindan yoziladi, kodda, versiyalanadi):
  1. asosiy metrika: challenger - champion  sezilarli yaxshi
     (juftlashgan farq, |d| > 2*SE)
  2. hech bir muhim metrika yomonlashmadi (AUC, kalibrovka)
  3. SEGMENTLAR: har segmentda yomonlashuv <= chegara
     (mintaqa, qurilma, yangi/eski mijoz)
  4. operatsion: shadow xatolari <= 0.1%, kechikish p99 <= SLA
  5. hujjat: model kartasi, ma'lumot versiyasi, konfig

QOIDA: natijani ko'rgandan keyin qoidani o'zgartirish - p-hacking
       qoida yomon bo'lsa - KEYINGI tajriba uchun o'zgartiriladi

Darvoza natijani ko'rishdan oldin yoziladi. Aks holda har doim "bu safar istisno" topiladi.

2.5. Canary, nazorat namunalari va rollback

Darvozadan o'tgan model ham darhol 100% trafikka chiqmaydi.

text
CANARY:  1% -> 10% -> 50% -> 100% trafik, har bosqichda guardrail
  guardrail (belgisiz, TEZ):
    xizmat xatolari, kechikish
    bashorat taqsimoti - O'Z shadow bashoratiga nisbatan (PSI)
    NAZORAT NAMUNALARI: registrda saqlangan kirish -> kutilgan chiqish
      (paketlash darsidagi g'oya, 27.6) - maks farq ~ 0 bo'lishi shart

AVTOMATIK ROLLBACK:
  guardrail buzildi -> registrda production = oldingi versiya
  oldingi versiya ARTEFAKTI saqlangan bo'lishi shart 27.5-bob
  rollback - bitta buyruq, sinab ko'rilgan, qo'lda emas

Nega nazorat namunalari? Taqsimot guardraili (PSI) ko'p nosozlikni ushlaydi, lekin hammasini emas. Masalan, ikkita bir xil taqsimotli ustunning o'rni almashib qolsa, bashoratlar taqsimoti deyarli o'zgarmaydi — har bir bashorat esa noto'g'ri. Buni faqat "aynan shu kirishga aynan shu chiqish" tekshiruvi ushlaydi. 2-misolda aynan shunday bo'ladi.

2.6. Onlayn A/B test

text
1. GIPOTEZA va METRIKA (oldindan): asosiy - konversiya; guardrail -
   qaytarish, shikoyat, kechikish; MDE - eng kichik muhim farq
2. NAMUNA HAJMI (quvvat tahlili, 11.2):
   n = (z_{1-a/2} * sqrt(2 p(1-p)) + z_{1-b} * sqrt(p1 q1 + p2 q2))^2 / d^2
   MDE 2 marta kichik -> n ~4 marta katta
3. TAQSIMLASH: foydalanuvchi ID si XESHI bilan
   guruh = hash(tajriba_nomi + ":" + user_id) -> [0, 1) -> < ulush ? B : A
   BARQAROR: bir foydalanuvchi har doim bir guruhda
   MUSTAQIL: har tajribaga o'z "tuzi" (salt)
   Python ning hash() - YO'Q: har jarayonda tasodifiy (PYTHONHASHSEED)
4. TAHLIL (belgilangan kunda, BIR MARTA):
   farq = p_B - p_A,  z-test,  95% CI = farq +- 1.96 * SE
5. QAROR: asosiy sezilarli yaxshi VA guardraillar chegaradan yomon emas

2.7. Peeking va guardrail

text
PEEKING: har kuni p-qiymatga qarab, p < 0.05 bo'lsa to'xtatish
  har qarash - yangi imkoniyat tasodifan 0.05 dan pastga tushish uchun
  30 kun har kuni qarash -> yolg'on musbat ~5% emas, ancha yuqori

YECHIMLAR:
  a) hajm va sanani oldindan belgilash, faqat oxirida qarash
  b) ketma-ket testlar: alfa ni qarashlarga taqsimlash
     (O'Brien-Fleming, Pocock), mSPRT / "har doim yaroqli" p-qiymatlar
  c) oddiy, lekin qo'pol: har qarashda alfa / qarashlar_soni (Bonferroni)
     -> yolg'on musbat nazoratda, lekin quvvat keskin tushadi

GUARDRAIL: asosiy metrika yaxshilandi, lekin nimaning hisobiga?
  konversiya +1.1 p.p., qaytarish ham +0.55 p.p. -> sof foyda?
  qoida: guardrail CI ning yomon cheti oldindan kelishilgan chegaradan oshmasin

A/B testda qaror bir marta, oldindan belgilangan kunda qabul qilinadi — erta qarash faqat xavfsizlik (guardrail) uchun.

2.8. Ko'p qo'lli bandit

A/B test — bilim olish vositasi: trafik teng bo'linadi, oxirida qaysi variant yaxshi ekanini ishonch bilan aytamiz. Lekin sinov davomida trafikning yarmi yomonroq variantga ketadi — bu "o'rganish narxi". Bandit bu narxni kamaytiradi: yaxshi ko'ringan variantga ko'proq trafik beradi.

text
REGRET = sum_t (eng yaxshi variant konversiyasi - tanlangan variant konversiyasi)
        "yomon tanlov tufayli yo'qotilgan konversiyalar"

EPSILON-GREEDY:
  ehtimol eps bilan - tasodifiy variant (o'rganish)
  aks holda - hozirgacha o'rtachasi eng yuqori variant (foydalanish)
  oddiy; lekin eps doimiy -> yomon variantlarga abadiy eps/K trafik

THOMPSON SAMPLING (Beta-Bernoulli):
  har variant uchun posterior: Beta(1 + muvaffaqiyat, 1 + muvaffaqiyatsizlik)
  har qadamda har posteriordan bitta namuna -> eng kattasini tanlash
  noaniq variant ba'zan katta namuna beradi -> o'zi sinaladi
  ishonch oshgan sari o'rganish tabiiy kamayadi

CHEKLOVLAR:
  kuchsiz variantlar haqida ma'lumot kam -> ishonch intervali keng
  kechikkan mukofot (konversiya 7 kundan keyin) murakkablashtiradi
  NOSTATSIONAR dunyo: eski dalillar "unutilmaydi" -> sekin moslashadi
    yechim: diskont (n, s ni har qadamda gamma ga ko'paytirish) yoki oyna

2.9. CT quvuri (continuous training)

text
     trigger (jadval / drift / sifat / yangi belgilar)
        |
        v
  [ma'lumot] -> validatsiya 27.3-bob -> versiya/xesh
        |
        v
  [o'qitish] -> eksperiment jurnali 27.4-bob -> kandidat artefakt
        |
        v
  [oflayn baholash] -> champion bilan juftlashgan, segmentlar
        |
        v
  [darvoza 1: oflayn] --RAD--> hisobot, champion qoladi
        | OK
        v
  [registr: Staging] 27.5-bob -> [shadow N kun]
        |
        v
  [darvoza 2: shadow] --RAD--> hisobot
        | OK
        v
  [canary 1% -> 10% -> 100%] --guardrail buzildi--> AVTOMATIK ROLLBACK
        | OK
        v
  [registr: Production], eski versiya Archived (o'chirilmaydi!)
        |
        v
  monitoring (27.11, 27.12) --> yangi trigger ...

Har qadam idempotent va jurnallanadi: quvur yarim yo'lda to'xtasa, qayta ishga tushirish xavfsiz; har qarorning sababi keyin tiklanadi. Haqiqiy loyihalarda bu sxema Airflow, Prefect, Kubeflow yoki GitHub Actions bilan orkestratsiya qilinadi, registr uchun MLflow ishlatiladi — lekin mantiq aynan shu.

2.10. Belgilar kechikishi va trigger

Qayta o'qitish triggeri ko'pincha sifatga bog'lanadi — lekin sifatni o'lchash uchun belgilar kerak, belgilar esa kechikadi. Bu kechikish butun tizim dizayniga ta'sir qiladi.

text
VAQT CHIZIG'I (mijoz ketishi, belgi 1 oydan keyin):
  oy t:    bashoratlar beriladi, kirishlar loglanadi
  oy t+1:  t-oy belgilari keladi -> t-oy sifati ma'lum
  oy t+1:  qayta o'qitish (t-oygacha belgili ma'lumot bilan)
  oy t+2:  shadow (t+2 so'rovlarida), belgilar t+3 da
  => drift boshlanganidan yangi model production ga chiqquncha
     kamida 2-3 oy

TEZLASHTIRISH YO'LLARI:
  erta signal: kirish/bashorat drifti 27.12-bob -> tayyorgarlik
  proksi belgi: tezroq keladigan o'rinbosar (masalan, 7 kunlik
                faollik -> 30 kunlik ketish uchun)
  shadow ni tarixiy so'rovlarda ham o'tkazish (replay)

TRIGGER QOIDASI (misol, oldindan yoziladi):
  sifat: AUC(oxirgi belgili oy) < AUC(reference) - 0.03
  YOKI  muhim ustunda PSI > 0.25 ikki oy ketma-ket
  VA    yangi belgili qatorlar >= 5000 (o'qitishga yetarli)

Trigger — qayta o'qitishni boshlaydi, qaror emas. Yangi model baribir darvoza, shadow va canary orqali o'tadi.

2.11. Tuzoqlar

Asosiy tuzoqlar: qayta o'qitilgan modelni darvozasiz avtomatik ishga tushirish; o'quv oynasini sinamasdan "butun tarix" yoki "oxirgi oy" deb tanlash; backtestda kelajak ma'lumotidan foydalanish; faqat umumiy metrikaga qarab segmentdagi yomonlashuvni ko'rmaslik; darvoza qoidalarini natijadan keyin yozish; shadow va canaryda faqat taqsimotga qarash (nazorat namunalarisiz); eski versiya artefaktini o'chirib yuborish (rollback imkonsiz); A/B guruhlarini hash() yoki random bilan har so'rovda qayta taqsimlash; namuna hajmini hisoblamasdan boshlash; har kuni qarab to'xtatish (peeking); guardrail metrikalarsiz qaror; banditni nostatsionar muhitda diskontsiz ishlatish.


3. Tez ma'lumotnoma

python
import hashlib

import numpy as np
from scipy import stats


def guruh(user_id, tajriba, ulush_b=0.5):
    h = hashlib.sha256(f"{tajriba}:{user_id}".encode()).hexdigest()
    return "B" if int(h[:8], 16) / 16**8 < ulush_b else "A"


def namuna_hajmi(p1, p2, alfa=0.05, quvvat=0.8):
    za, zb = stats.norm.ppf(1 - alfa / 2), stats.norm.ppf(quvvat)
    pb = (p1 + p2) / 2
    return int(np.ceil((za * np.sqrt(2 * pb * (1 - pb))
                        + zb * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2
                       / (p2 - p1) ** 2))


def z_test(x_a, n_a, x_b, n_b):
    pa, pb = x_a / n_a, x_b / n_b
    p = (x_a + x_b) / (n_a + n_b)
    z = (pb - pa) / np.sqrt(p * (1 - p) * (1 / n_a + 1 / n_b))
    se = np.sqrt(pa * (1 - pa) / n_a + pb * (1 - pb) / n_b)
    return pb - pa, 2 * stats.norm.sf(abs(z)), (pb - pa - 1.96 * se, pb - pa + 1.96 * se)


# vaznli o'quv oynasi: yosh - necha oy oldin
vazn = 0.5 ** (yosh / yarim_umr)
model.fit(X, y, sample_weight=vazn)

# juftlashgan shadow taqqoslash (bir xil so'rovlar)
d = log_loss_har_biri(y, p_challenger) - log_loss_har_biri(y, p_champion)
yaxshi = d.mean() < -2 * d.std(ddof=1) / np.sqrt(len(d))

# Thompson sampling (bitta qadam)
theta = rng.beta(1 + muvaffaqiyat, 1 + urinish - muvaffaqiyat)
tanlov = int(np.argmax(theta))

Qaror jadvali

Vaziyat Nima qilish
Barqaror dunyo Jadval bo'yicha, kam-kam; statik ham yetishi mumkin
To'satdan drift Trigger + qisqa oyna (oxirgi N oy)
Asta-sekin drift Muntazam + qisqa yoki vaznli oyna
Qaytuvchi rejim Rejimni belgi sifatida berish yoki rejim bo'yicha modellar
Yangi model tayyor Shadow → darvoza → canary → Production
Biznes ta'siri kerak A/B test, hajm oldindan, bir marta qarash
Ko'p variant, daromad muhim Bandit (Thompson), nostatsionar bo'lsa diskont

Xulosa

qayta o'qitish != ishga tushirish; darvoza majburiy
oyna - backtest bilan, drift turiga qarab
shadow: bir xil so'rovlar, juftlashgan; segmentlar
canary: PSI + nazorat namunalari; rollback avtomatik
A/B: xesh, quvvat, bir marta qarash, guardrail
bandit: regret kam, bilim kam; nostatsionarda diskont

4. Batafsil misollar

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

Misol 1 — O'quv oynasi: vaqt bo'yicha backtest, to'rt drift turi

python
"""O'quv oynasi: statik, butun tarix, oxirgi 3 oy, vaznli - drift turiga qarab."""

import warnings

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss

OYLAR, OY_N, BOSH = 20, 800, 6          # 20 oy, har oyda 800 mijoz
W0 = np.array([1.2, -1.0, 0.6, 0.0])
W1 = np.array([0.2, 0.8, 0.6, 1.0])       # yangi rejim


def koef(ssenariy, t):
    if ssenariy == "barqaror":
        return W0
    if ssenariy == "to'satdan":
        return W0 if t < 10 else W1
    if ssenariy == "asta-sekin":
        a = min(max((t - 4) / 14, 0.0), 1.0)
        return (1 - a) * W0 + a * W1
    if ssenariy == "qaytuvchi":            # 4 oy eski, 4 oy yangi, ...
        return W0 if (t // 4) % 2 == 0 else W1
    raise ValueError(ssenariy)


def oy_partiyasi(rng, ssenariy, t):
    X = rng.normal(0, 1, (OY_N, 4))
    f = -0.8 + X @ koef(ssenariy, t)
    y = (rng.random(OY_N) < 1 / (1 + np.exp(-f))).astype(int)
    return X, y


def orgat(X, y, w=None):
    return LogisticRegression(max_iter=500).fit(X, y, sample_weight=w)


def yurish(ssenariy, seed):
    rng = np.random.default_rng(seed)
    data = [oy_partiyasi(rng, ssenariy, t) for t in range(OYLAR)]
    statik = orgat(np.vstack([d[0] for d in data[:BOSH]]),
                   np.concatenate([d[1] for d in data[:BOSH]]))
    natija = {k: [] for k in ["statik", "butun tarix", "oxirgi 3 oy",
                              "vaznli"]}
    for t in range(BOSH, OYLAR):             # t-oy uchun bashorat
        X_t, y_t = data[t]
        Xs = np.vstack([d[0] for d in data[:t]])
        ys = np.concatenate([d[1] for d in data[:t]])
        yosh = np.repeat(np.arange(t - 1, -1, -1), OY_N)   # necha oy oldin
        modellar = {
            "statik": statik,
            "butun tarix": orgat(Xs, ys),
            "oxirgi 3 oy": orgat(Xs[yosh < 3], ys[yosh < 3]),
            "vaznli": orgat(Xs, ys, w=0.5 ** (yosh / 3)),   # yarim umr 3 oy
        }
        for nom, m in modellar.items():
            natija[nom].append(log_loss(y_t, m.predict_proba(X_t)[:, 1]))
    return {k: np.array(v) for k, v in natija.items()}


def main() -> None:
    warnings.simplefilter("ignore")
    seedlar = [0, 1, 2]
    tartib = ["statik", "butun tarix", "oxirgi 3 oy", "vaznli"]  # soddadan
    print("=== 1. Sozlama ===")
    print(f"  {OYLAR} oy x {OY_N} mijoz; {BOSH}-oydan boshlab har oy qayta "
          f"o'qitiladi va KEYINGI oy baholanadi")
    print(f"  metrika: log loss (kichik - yaxshi); {len(seedlar)} urug' x "
          f"{OYLAR - BOSH} oy = {len(seedlar) * (OYLAR - BOSH)} juft")

    print("\n=== 2. O'rtacha log loss ===")
    print(f"  {'ssenariy':<11}" + "".join(f"{s:>13}" for s in tartib))
    hammasi = {}
    for ss in ["barqaror", "to'satdan", "asta-sekin", "qaytuvchi"]:
        yig = {k: [] for k in tartib}
        for s in seedlar:
            r = yurish(ss, s)
            for k in tartib:
                yig[k].append(r[k])
        hammasi[ss] = {k: np.concatenate(v) for k, v in yig.items()}
        print(f"  {ss:<11}" + "".join(
            f"{hammasi[ss][k].mean():>13.4f}" for k in tartib))

    print("\n=== 3. Juftlashgan farq eng yaxshisidan va qaror ===")
    for ss, r in hammasi.items():
        eng = min(tartib, key=lambda k: r[k].mean())
        tanlov = None
        qatorlar = []
        for k in tartib:
            d = r[k] - r[eng]
            se = d.std(ddof=1) / np.sqrt(len(d)) if k != eng else 0.0
            yomon = d.mean() > 2 * se and k != eng
            qatorlar.append(f"{k} {d.mean():+.4f}"
                            + (f" (SE {se:.4f})" if k != eng else ""))
            if tanlov is None and not yomon:
                tanlov = k
        print(f"  {ss}: eng yaxshi {eng}")
        for q in qatorlar:
            print(f"    {q}")
        print(f"    TANLOV: {tanlov}")
    print("  ⭐ Oyna drift turiga mos tanlanadi; barqarorda statik ham yetadi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Sozlama ===
  20 oy x 800 mijoz; 6-oydan boshlab har oy qayta o'qitiladi va KEYINGI oy baholanadi
  metrika: log loss (kichik - yaxshi); 3 urug' x 14 oy = 42 juft

=== 2. O'rtacha log loss ===
  ssenariy          statik  butun tarix  oxirgi 3 oy       vaznli
  barqaror          0.4751       0.4750       0.4759       0.4752
  to'satdan         0.8261       0.6506       0.5445       0.5716
  asta-sekin        0.7349       0.6112       0.5386       0.5586
  qaytuvchi         0.5863       0.5918       0.6289       0.5948

=== 3. Juftlashgan farq eng yaxshisidan va qaror ===
  barqaror: eng yaxshi butun tarix
    statik +0.0000 (SE 0.0001)
    butun tarix +0.0000
    oxirgi 3 oy +0.0008 (SE 0.0002)
    vaznli +0.0002 (SE 0.0001)
    TANLOV: statik
  to'satdan: eng yaxshi oxirgi 3 oy
    statik +0.2816 (SE 0.0339)
    butun tarix +0.1061 (SE 0.0138)
    oxirgi 3 oy +0.0000
    vaznli +0.0271 (SE 0.0052)
    TANLOV: oxirgi 3 oy
  asta-sekin: eng yaxshi oxirgi 3 oy
    statik +0.1963 (SE 0.0248)
    butun tarix +0.0726 (SE 0.0081)
    oxirgi 3 oy +0.0000
    vaznli +0.0200 (SE 0.0019)
    TANLOV: oxirgi 3 oy
  qaytuvchi: eng yaxshi statik
    statik +0.0000
    butun tarix +0.0055 (SE 0.0057)
    oxirgi 3 oy +0.0426 (SE 0.0290)
    vaznli +0.0084 (SE 0.0132)
    TANLOV: statik
  ⭐ Oyna drift turiga mos tanlanadi; barqarorda statik ham yetadi

Natija tahlili.

Har ssenariyda 20 oy, har oyda 800 mijoz. 6-oydan boshlab har oy model qayta o'qitiladi va keyingi oy ma'lumotida baholanadi — kelajak ma'lumoti o'quvga tushmaydi. Uch urug' x 14 oy = 42 ta juft bo'yicha juftlashgan farq hisoblanadi. Qaror qoidasi oldindan: eng yaxshisidan sezilarli yomon bo'lmagan eng sodda strategiya (soddalik tartibi: statik < butun tarix < oxirgi 3 oy < vaznli).

  • Barqaror dunyo. To'rtala strategiya deyarli bir xil (0.4750-0.4759). Statik model butun tarixdan atigi +0.0000 farq qiladi — qoida statikni tanladi. Ya'ni drift bo'lmasa, qayta o'qitish foyda bermaydi — faqat resurs va xavf. "Oxirgi 3 oy" esa kamroq ma'lumot tufayli biroz yomon (+0.0008, SE 0.0002 — kichik, lekin sezilarli).
  • To'satdan drift (10-oy). Statik model halokatli (0.8261). Butun tarix ham sekin moslashadi (+0.1061) — eski rejim ma'lumoti uzoq vaqt og'irlik qiladi. Eng yaxshisi oxirgi 3 oy; vaznli (+0.0271) ham sezilarli yomonroq, chunki yarim umri 3 oy bo'lgan vazn eski rejimni sekinroq unutadi.
  • Asta-sekin drift. Xuddi shu tartib: oxirgi 3 oy eng yaxshi, vaznli +0.0200, butun tarix +0.0726, statik +0.1963.
  • Qaytuvchi rejim (har 4 oyda almashadi). Kutilmagan, lekin muhim natija: statik eng yaxshi (0.5863), oxirgi 3 oy esa eng yomon (0.6289). Sabab — qisqa oyna har almashishdan keyin 1-3 oy davomida eski rejimda o'qitilgan bo'lib qoladi, ya'ni doimo "bir qadam orqada". Statik model esa ikki rejim aralashmasida o'qitilgan (6 oyning 4 tasi eski, 2 tasi yangi rejim) va har ikkalasida o'rtacha ishlaydi. Lekin juftlashgan farqlarning hech biri sezilarli emas: butun tarix +0.0055 (SE 0.0057), vaznli +0.0084 (SE 0.0132), hatto oxirgi 3 oy ham +0.0426 (SE 0.0290). Qisqa oyna almashishdan keyingi oylarda katta xato qiladi, rejim o'zgarmagan oylarda esa yaxshi ishlaydi — shuning uchun uning farqlari juda tarqoq. Qoida bo'yicha tanlov — eng sodda, ya'ni statik. Qaytuvchi rejimda hech bir oyna yaxshi yechim emas — to'g'ri yechim rejimni (masalan, "aksiya davri" belgisini) modelga kirish sifatida berish.

Halol eslatma: bir urug' ichidagi oylar mustaqil emas (qo'shni oylar o'xshash), shuning uchun SE biroz optimistik. Katta farqlar (to'satdan va asta-sekin driftdagi) bu eslatmadan ancha katta; kichiklari (barqaror dunyodagi +0.0008) esa ehtiyotkorlik bilan o'qiladi.

Misol 2 — Shadow rejim, darvoza, canary va avtomatik rollback

python
"""Champion/challenger: shadow rejim, oldindan yozilgan darvoza, canary va rollback."""

import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score

USTUNLAR = ["x1", "x2", "x3", "x4", "hudud"]
DARVOZA = {                                   # natijani ko'rishdan OLDIN
    "log loss sezilarli yaxshi": "mean(d) < -2*SE",
    "AUC pasaymadi": "AUC_ch >= AUC_champ - 0.005",
    "har hududda yomonlashuv <= 0.02": "mean(d_hudud) <= 0.02",
    "shadow xatolari <= 0.1%": "xato / jami <= 0.001",
}


def partiya(rng, n, rejim):
    X = rng.normal(0, 1, (n, 5))
    X[:, 4] = rng.integers(0, 3, n)                      # hudud 0/1/2
    b1 = 1.2 if rejim == "eski" else 0.2                 # drift: x1 ta'siri
    b3 = np.where(X[:, 4] == 2, -0.9, 0.6)               # 2-hududda teskari
    f = -0.8 + b1 * X[:, 0] - 0.9 * X[:, 1] + b3 * X[:, 2] + 0.7 * X[:, 3]
    y = (rng.random(n) < 1 / (1 + np.exp(-f))).astype(int)
    return X, y


def orgat(X, y):
    return HistGradientBoostingClassifier(
        max_iter=120, learning_rate=0.08, categorical_features=[4],
        random_state=0).fit(X, y)


class Model:
    """Model + kirish tayyorlash (versiyaga xos xatti-harakat bilan)."""

    def __init__(self, nom, clf, qatiy=False, tartib_xato=False):
        self.nom, self.clf = nom, clf
        self.qatiy, self.tartib_xato = qatiy, tartib_xato

    def bashorat(self, X):
        X = X.copy()
        if np.isnan(X).any():
            if self.qatiy:
                raise ValueError("bo'sh qiymat")
            X = np.nan_to_num(X, nan=0.0)                 # o'quvdagi median ~ 0
        if self.tartib_xato:
            X[:, [0, 1]] = X[:, [1, 0]]                   # ustun tartibi xatosi
        return self.clf.predict_proba(X)[:, 1]


def shadow(champion, challenger, X):
    """Foydalanuvchiga champion javobi; challenger faqat logga."""
    javob = champion.bashorat(X)
    log, xato = np.full(len(X), np.nan), 0
    for i in range(0, len(X), 100):                      # 100 talik partiya
        try:
            log[i:i + 100] = challenger.bashorat(X[i:i + 100])
        except ValueError:                               # partiya yiqildi ->
            for j in range(i, min(i + 100, len(X))):     # bittalab qayta
                try:
                    log[j] = challenger.bashorat(X[j:j + 1])[0]
                except ValueError:
                    xato += 1
    return javob, log, xato / len(X)


def ll(y, p):
    p = np.clip(p, 1e-6, 1 - 1e-6)
    return -(y * np.log(p) + (1 - y) * np.log(1 - 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 main() -> None:
    rng = np.random.default_rng(5)
    X_eski, y_eski = partiya(rng, 6000, "eski")
    X_yangi, y_yangi = partiya(rng, 4000, "yangi")        # drift dan keyin
    champion = Model("v1", orgat(X_eski, y_eski))
    ch_a = Model("A", orgat(X_yangi, y_yangi))
    bor = X_yangi[:, 4] != 2                             # quvur 2-hududni tashladi
    ch_b = Model("B", orgat(X_yangi[bor], y_yangi[bor]), qatiy=True)

    print("=== 1. Shadow rejim: 3000 so'rov (keyingi oy) ===")
    X_s, y_s = partiya(rng, 3000, "yangi")
    X_s[rng.choice(3000, 6, replace=False), 3] = np.nan  # 6 ta buzuq so'rov
    loglar = {}
    for ch in [ch_a, ch_b]:
        javob, log, xato = shadow(champion, ch, X_s)
        loglar[ch.nom] = (log, xato)
        bir_xil = np.allclose(javob, champion.bashorat(X_s))
        print(f"  {ch.nom}: foydalanuvchi javobi champion bilan bir xil: "
              f"{bir_xil}; challenger xato so'rovlari {xato:.2%}")

    print("\n=== 2. Belgilar keldi: juftlashgan taqqoslash ===")
    p_ch = champion.bashorat(X_s)
    print(f"  champion {champion.nom}: AUC {roc_auc_score(y_s, p_ch):.4f}")
    qarorlar = {}
    for nom, (log, xato) in loglar.items():
        ok = ~np.isnan(log)
        d = ll(y_s[ok], log[ok]) - ll(y_s[ok], p_ch[ok])
        se = d.std(ddof=1) / np.sqrt(len(d))
        auc_c = roc_auc_score(y_s[ok], log[ok])
        auc_0 = roc_auc_score(y_s[ok], p_ch[ok])
        hudud_d = [float(d[X_s[ok, 4] == h].mean()) for h in range(3)]
        print(f"  {nom}: d = {d.mean():+.4f} (SE {se:.4f}), AUC {auc_c:.4f}, "
              f"hududlar {[round(v, 4) for v in hudud_d]}")
        qarorlar[nom] = {
            "log loss sezilarli yaxshi": d.mean() < -2 * se,
            "AUC pasaymadi": auc_c >= auc_0 - 0.005,
            "har hududda yomonlashuv <= 0.02": max(hudud_d) <= 0.02,
            "shadow xatolari <= 0.1%": xato <= 0.001,
        }

    print("\n=== 3. Darvoza (qoidalar oldindan yozilgan) ===")
    for qoida in DARVOZA:
        print(f"  {qoida:<34} " + "  ".join(
            f"{n}: {'OK' if q[qoida] else 'RAD'}" for n, q in qarorlar.items()))
    otdi = [n for n, q in qarorlar.items() if all(q.values())]
    print(f"  darvozadan o'tdi: {otdi}")

    print("\n=== 4. Registr, canary (10% trafik) va avtomatik rollback ===")
    modellar = {"v1": champion, "v2": Model("v2", ch_a.clf),
                "v3": Model("v3", ch_a.clf, tartib_xato=True)}  # tezlik patchi
    shadow_ref = {"v2": loglar["A"][0], "v3": loglar["A"][0]}   # o'sha model
    registr = {"production": "v1", "tarix": ["v1"], "rad": []}
    for yangi in ["v2", "v3"]:
        eski = registr["production"]
        X_c, _ = partiya(rng, 1000, "yangi")                    # canary kuni
        p_c = modellar[yangi].bashorat(X_c)
        ref = shadow_ref[yangi][~np.isnan(shadow_ref[yangi])]
        k = psi(ref, p_c)
        nazorat = ~np.isnan(shadow_ref[yangi])
        nazorat[200:] = False                                   # 200 ta namuna
        farq = np.max(np.abs(modellar[yangi].bashorat(X_s[nazorat])
                             - shadow_ref[yangi][nazorat]))
        buzildi = k > 0.1 or farq > 1e-9
        print(f"  {eski} -> {yangi}: bashorat PSI (o'z shadow iga nisbatan) "
              f"{k:.3f}; nazorat namunalari maks farq {farq:.3g}")
        if buzildi:
            registr["rad"].append(yangi)
            sabab = [n for n, b in [("PSI", k > 0.1),
                                    ("nazorat namunalari", farq > 1e-9)] if b]
            print(f"    guardrail BUZILDI ({', '.join(sabab)}) -> avtomatik "
                  f"ROLLBACK, production = {eski}")
        else:
            registr["production"] = yangi
            registr["tarix"].append(yangi)
            print(f"    guardrail OK -> 100% trafik, production = {yangi}")
    print(f"  yakuniy: production {registr['production']}, "
          f"tarix {registr['tarix']}, rad {registr['rad']}")
    print("  ⭐ Qaror - oldindan yozilgan qoidalar bilan, rollback - avtomatik")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Shadow rejim: 3000 so'rov (keyingi oy) ===
  A: foydalanuvchi javobi champion bilan bir xil: True; challenger xato so'rovlari 0.00%
  B: foydalanuvchi javobi champion bilan bir xil: True; challenger xato so'rovlari 0.20%

=== 2. Belgilar keldi: juftlashgan taqqoslash ===
  champion v1: AUC 0.7292
  A: d = -0.0711 (SE 0.0113), AUC 0.7593, hududlar [-0.0637, -0.0719, -0.0776]
  B: d = +0.0289 (SE 0.0147), AUC 0.7021, hududlar [-0.0333, -0.0417, 0.162]

=== 3. Darvoza (qoidalar oldindan yozilgan) ===
  log loss sezilarli yaxshi          A: OK  B: RAD
  AUC pasaymadi                      A: OK  B: RAD
  har hududda yomonlashuv <= 0.02    A: OK  B: RAD
  shadow xatolari <= 0.1%            A: OK  B: RAD
  darvozadan o'tdi: ['A']

=== 4. Registr, canary (10% trafik) va avtomatik rollback ===
  v1 -> v2: bashorat PSI (o'z shadow iga nisbatan) 0.009; nazorat namunalari maks farq 0
    guardrail OK -> 100% trafik, production = v2
  v2 -> v3: bashorat PSI (o'z shadow iga nisbatan) 0.016; nazorat namunalari maks farq 0.872
    guardrail BUZILDI (nazorat namunalari) -> avtomatik ROLLBACK, production = v2
  yakuniy: production v2, tarix ['v1', 'v2'], rad ['v3']
  ⭐ Qaror - oldindan yozilgan qoidalar bilan, rollback - avtomatik

Natija tahlili.

Champion (v1) eski rejimda o'qitilgan; drift dan keyin ikki challenger tayyorlandi: A — yangi ma'lumotning hammasida, B — ham yangi ma'lumotda, lekin quvur xatosi tufayli 2-hudud yozuvlari tushib qolgan va kirish tekshiruvi qat'iy (bo'sh qiymatda xato beradi).

1-bo'lim — shadow. Ikkala holatda ham foydalanuvchi javobi champion javobi bilan aynan bir xil (True) — shadow rejim foydalanuvchiga ta'sir qilmaydi. B 6 ta buzuq so'rovda (0.20%) yiqildi; partiya yiqilganda xizmat so'rovlarni bittalab qayta hisoblab, faqat haqiqatan buzuq so'rovlarni xato deb sanadi.

2-bo'lim — belgilar kelgach bir xil 3000 so'rovda juftlashgan taqqoslash. A log loss ni -0.0711 ga kamaytirdi (SE 0.0113), AUC 0.7292 → 0.7593, uchala hududda ham yaxshilanish. B umumiy hisobda ham yomon (+0.0289), lekin asosiy muammo 2-hududda: +0.162. Real vaziyatdagidek, agar 2-hudud ulushi kichikroq bo'lganida, B umumiy metrikada yaxshi ko'rinishi mumkin edi — segment qoidasi aynan shunday holat uchun.

3-bo'lim — oldindan yozilgan to'rt qoida. A hammasidan o'tdi, B to'rttasidan ham o'tmadi. Qoidalar DARVOZA lug'atida natijadan oldin yozilgan va kodda saqlanadi — keyin "istisno" qilishning iloji yo'q.

4-bo'lim — canary. v2 (A modeli) canary da o'z shadow bashoratlariga nisbatan PSI 0.009, nazorat namunalarida farq 0 → 100% trafik. Keyin "tezlik patchi" bilan v3 chiqarildi — model o'sha, lekin xizmat kodida ikki ustun o'rni almashgan. Bashorat PSI atigi 0.016 — taqsimot guardraili buni ko'rmadi, chunki almashgan ustunlarning taqsimoti bir xil. Nazorat namunalari esa maks farq 0.872 ni ko'rsatdi → avtomatik rollback, production v2 da qoldi, v3 rad etilganlar ro'yxatida.

Misol 3 — Onlayn A/B test: taqsimlash, quvvat, peeking va guardrail

python
"""Onlayn A/B test: xesh bilan taqsimlash, quvvat, test, peeking va guardrail."""

import hashlib

import numpy as np
from scipy import stats


def guruh(user_id, tajriba, ulush_b=0.5):
    """Barqaror taqsimlash: bir foydalanuvchi har doim bir guruhda."""
    h = hashlib.sha256(f"{tajriba}:{user_id}".encode()).hexdigest()
    return "B" if int(h[:8], 16) / 16**8 < ulush_b else "A"


def namuna_hajmi(p1, p2, alfa=0.05, quvvat=0.8):
    """Ikki proporsiya, ikki tomonlama test - har guruhga 11.2-bob."""
    za, zb = stats.norm.ppf(1 - alfa / 2), stats.norm.ppf(quvvat)
    p_bar = (p1 + p2) / 2
    n = (za * np.sqrt(2 * p_bar * (1 - p_bar))
         + zb * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2 / (p2 - p1) ** 2
    return int(np.ceil(n))


def z_test(x_a, n_a, x_b, n_b):
    """Proporsiyalar farqi: p-qiymat va 95% ishonch intervali 11.4-bob."""
    pa, pb = x_a / n_a, x_b / n_b
    p = (x_a + x_b) / (n_a + n_b)
    z = (pb - pa) / np.sqrt(p * (1 - p) * (1 / n_a + 1 / n_b))
    se = np.sqrt(pa * (1 - pa) / n_a + pb * (1 - pb) / n_b)
    return pb - pa, 2 * stats.norm.sf(abs(z)), (pb - pa - 1.96 * se,
                                                pb - pa + 1.96 * se)


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

    print("=== 1. Xesh bilan taqsimlash ===")
    idlar = [f"u{i}" for i in range(20000)]
    g1 = [guruh(u, "tajriba_tugma") for u in idlar]
    g2 = [guruh(u, "tajriba_narx") for u in idlar]
    print(f"  B ulushi: {np.mean([g == 'B' for g in g1]):.4f}")
    print(f"  barqarorlik (qayta hisoblash bir xil): "
          f"{all(guruh(u, 'tajriba_tugma') == g for u, g in zip(idlar[:500], g1))}")
    for a in "AB":
        for b in "AB":
            ulush = np.mean([(x == a) and (y == b) for x, y in zip(g1, g2)])
            print(f"  tugma={a}, narx={b}: {ulush:.4f}")
    print("  ikki tajriba mustaqil (har katak ~0.25) - boshqa tuz (salt)")

    print("\n=== 2. Quvvat tahlili: konversiya 10% -> 11% ===")
    n = namuna_hajmi(0.10, 0.11)
    print(f"  har guruhga n = {n} (alfa 0.05, quvvat 0.8)")
    print(f"  kuniga 2000 foydalanuvchi -> {int(np.ceil(2 * n / 2000))} kun")
    for mde in [0.005, 0.02]:
        print(f"  MDE {mde:.3f} bo'lsa: n = {namuna_hajmi(0.10, 0.10 + mde)}")

    print("\n=== 3. Bitta tajriba (haqiqiy farq +1 p.p.) ===")
    x_a, x_b = rng.binomial(n, 0.10), rng.binomial(n, 0.11)
    farq, p, (q, y) = z_test(x_a, n, x_b, n)
    print(f"  A {x_a / n:.4f}, B {x_b / n:.4f}; farq {farq:+.4f}, "
          f"95% CI [{q:+.4f}, {y:+.4f}], p = {p:.4f}")
    print(f"  qaror: {'B ni chiqarish' if p < 0.05 and farq > 0 else 'farq isbotlanmadi'}")
    T = 4000
    xa, xb = rng.binomial(n, 0.10, T), rng.binomial(n, 0.11, T)
    xa0 = rng.binomial(n, 0.10, T)
    kuch = np.mean([z_test(a, n, b, n)[1] < 0.05 for a, b in zip(xa, xb)])
    alfa = np.mean([z_test(a, n, b, n)[1] < 0.05 for a, b in zip(xa, xa0)])
    print(f"  {T} simulyatsiya: quvvat {kuch:.3f}, A/A da yolg'on musbat "
          f"{alfa:.3f}")

    print("\n=== 4. Peeking: har kuni qarab p<0.05 da to'xtatish ===")
    kun, kunlik, T = 30, 500, 4000                       # har guruhga kuniga
    nn = kunlik * np.arange(1, kun + 1)

    def kunlik_p(p_b):
        a = rng.binomial(kunlik, 0.10, (T, kun)).cumsum(1)
        b = rng.binomial(kunlik, p_b, (T, kun)).cumsum(1)
        pool = (a + b) / (2 * nn)
        z = (b - a) / nn / np.sqrt(pool * (1 - pool) * 2 / nn)
        return 2 * stats.norm.sf(np.abs(z))

    p_aa, p_ab = kunlik_p(0.10), kunlik_p(0.11)          # A/A va haqiqiy +1pp
    usullar = [("faqat 30-kuni (oldindan belgilangan)",
                lambda p: p[:, -1] < 0.05)]
    for d in [7, 14, 30]:
        usullar.append((f"dastlabki {d} kun har kuni qarash",
                        lambda p, d=d: (p[:, :d] < 0.05).any(1)))
    usullar.append(("har kuni, lekin alfa/30 (Bonferroni)",
                    lambda p: (p < 0.05 / 30).any(1)))
    print(f"  {'usul':<37} {'yolg_on musbat':>14} {'quvvat':>7}")
    for nom, f in usullar:
        print(f"  {nom:<37} {np.mean(f(p_aa)):>14.3f} "
              f"{np.mean(f(p_ab)):>7.3f}")

    print("\n=== 5. Guardrail: asosiy metrika yaxshi - qaytarishlar-chi? ===")
    n5 = 30000
    kon_a, kon_b = rng.binomial(n5, 0.100), rng.binomial(n5, 0.108)
    qay_a, qay_b = rng.binomial(n5, 0.020), rng.binomial(n5, 0.026)
    f1, p1, ci1 = z_test(kon_a, n5, kon_b, n5)
    f2, p2, ci2 = z_test(qay_a, n5, qay_b, n5)
    print(f"  konversiya (asosiy):  {f1:+.4f}, p={p1:.4f}, "
          f"CI [{ci1[0]:+.4f}, {ci1[1]:+.4f}]")
    print(f"  qaytarish (guardrail): {f2:+.4f}, p={p2:.4f}, "
          f"CI [{ci2[0]:+.4f}, {ci2[1]:+.4f}]")
    chegara = 0.002                                     # oldindan kelishilgan
    asosiy_ok = p1 < 0.05 and f1 > 0
    guard_ok = ci2[1] <= chegara                        # yomonlashuv <= 0.2 p.p.
    print(f"  asosiy OK: {asosiy_ok}; guardrail (CI yuqori <= +{chegara}) OK: "
          f"{guard_ok}")
    print(f"  QAROR: {'chiqarish' if asosiy_ok and guard_ok else 'CHIQARMASLIK'}")
    print("  ⭐ Hajm oldindan, qarash bir marta, guardrail - majburiy")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Xesh bilan taqsimlash ===
  B ulushi: 0.4931
  barqarorlik (qayta hisoblash bir xil): True
  tugma=A, narx=A: 0.2510
  tugma=A, narx=B: 0.2558
  tugma=B, narx=A: 0.2477
  tugma=B, narx=B: 0.2455
  ikki tajriba mustaqil (har katak ~0.25) - boshqa tuz (salt)

=== 2. Quvvat tahlili: konversiya 10% -> 11% ===
  har guruhga n = 14751 (alfa 0.05, quvvat 0.8)
  kuniga 2000 foydalanuvchi -> 15 kun
  MDE 0.005 bo'lsa: n = 57763
  MDE 0.020 bo'lsa: n = 3841

=== 3. Bitta tajriba (haqiqiy farq +1 p.p.) ===
  A 0.0984, B 0.1142; farq +0.0159, 95% CI [+0.0088, +0.0229], p = 0.0000
  qaror: B ni chiqarish
  4000 simulyatsiya: quvvat 0.809, A/A da yolg'on musbat 0.054

=== 4. Peeking: har kuni qarab p<0.05 da to'xtatish ===
  usul                                  yolg_on musbat  quvvat
  faqat 30-kuni (oldindan belgilangan)           0.057   0.809
  dastlabki 7 kun har kuni qarash                0.172   0.404
  dastlabki 14 kun har kuni qarash               0.231   0.640
  dastlabki 30 kun har kuni qarash               0.293   0.892
  har kuni, lekin alfa/30 (Bonferroni)           0.017   0.481

=== 5. Guardrail: asosiy metrika yaxshi - qaytarishlar-chi? ===
  konversiya (asosiy):  +0.0110, p=0.0000, CI [+0.0061, +0.0158]
  qaytarish (guardrail): +0.0055, p=0.0000, CI [+0.0031, +0.0079]
  asosiy OK: True; guardrail (CI yuqori <= +0.002) OK: False
  QAROR: CHIQARMASLIK
  ⭐ Hajm oldindan, qarash bir marta, guardrail - majburiy

Natija tahlili.

1-bo'lim — sha256(tajriba:user_id) bilan taqsimlash. B ulushi 0.4931. 20 000 foydalanuvchida 0.5 dan farq taxminan 1.95 SE — chegarada, lekin 5% darajada sezilarli emas; ishlab chiqarishda bu tekshiruv har tajribada avtomatik bajariladi (SRM — namuna nisbati testi, Vazifa 5). Qayta hisoblash har doim bir xil guruh beradi. Ikki tajriba turli nom (tuz) bilan mustaqil: to'rt katakning har biri ~0.25. Agar ikkala tajriba bir xil tuzdan foydalanganida, "tugma B" va "narx B" guruhlari aynan bir xil odamlar bo'lib qolardi va ta'sirlarni ajratib bo'lmasdi.

2-bo'lim — 10% dan 11% ga (+1 p.p.) o'sishni 80% quvvat bilan ko'rish uchun har guruhga 14751 foydalanuvchi kerak — kuniga 2000 foydalanuvchida 15 kun. MDE ikki marta kichraysa (0.005), n taxminan to'rt marta oshadi (57763); ikki marta kattalashsa — to'rt marta kamayadi (3841).

3-bo'lim — bitta tajriba: farq +0.0159, 95% CI [+0.0088, +0.0229], p < 0.0001 → B chiqariladi. E'tibor bering, haqiqiy farq +0.01, baholangani esa kattaroq — bitta tajriba natijasi tasodifan yuqori chiqishi mumkin, CI esa haqiqiy qiymatni o'z ichiga oladi. 4000 simulyatsiya formulani tasdiqladi: quvvat 0.809 (rejalashtirilgan 0.80), A/A da yolg'on musbat 0.054 (rejalashtirilgan 0.05).

4-bo'lim — peeking (har guruhga kuniga 500, 30 kun ≈ rejalashtirilgan hajm). Faqat oxirida qarash: yolg'on musbat 0.057. Har kuni qarab p < 0.05 da to'xtatish: 7 kunda 0.172, 14 kunda 0.231, 30 kunda 0.293 — deyarli olti barobar. Quvvat ham "yaxshilangandek" (0.892), lekin bu yolg'on musbatlar hisobiga. Har qarashda alfa/30 yolg'on musbatni 0.017 ga tushiradi, lekin quvvat 0.481 ga tushadi — juda qimmat. To'g'ri yo'l — oldindan belgilangan hajm yoki maxsus ketma-ket testlar 2.7-bob.

5-bo'lim — guardrail. Konversiya +0.0110 ga oshdi (p < 0.0001, CI [+0.0061, +0.0158]) — asosiy metrika bo'yicha aniq "g'alaba". Lekin qaytarishlar ham +0.0055 ga oshdi va CI ning yuqori cheti (+0.0079) oldindan kelishilgan +0.002 chegaradan katta. Qaror: chiqarmaslik. Har ikki qo'shimcha sotuvga taxminan bitta qo'shimcha qaytarish to'g'ri kelmoqda — sof foyda asosiy metrika ko'rsatganidan ancha kam, balki manfiy. Bu yerda qaror biznes bilan kelishilgan chegaradan keladi: agar qaytarish narxi past bo'lsa, chegara boshqacha qo'yilgan bo'lardi — lekin tajribadan oldin.

Misol 4 — Ko'p qo'lli bandit: epsilon-greedy va Thompson sampling

python
"""Ko'p qo'lli bandit: teng taqsimot (A/B/n), epsilon-greedy, Thompson sampling."""

import numpy as np

HAQIQIY = np.array([0.050, 0.055, 0.070])    # 3 variant konversiyasi (noma'lum)
QADAM, SEEDLAR = 20000, 200
STRATEGIYALAR = ["teng (A/B/n)", "epsilon-greedy", "Thompson",
                 "Thompson unutuvchi"]


def simulyatsiya(strategiya, seed, eps=0.1, keyin=None):
    """Hamma urug'lar parallel: har qadamda har urug' bitta variant tanlaydi."""
    rng = np.random.default_rng(seed)
    K, S = len(HAQIQIY), SEEDLAR
    n = np.zeros((S, K))
    s = np.zeros((S, K))
    regret = np.zeros(S)
    tanlov_soni = np.zeros((S, K))
    satr = np.arange(S)
    haqiqiy = HAQIQIY
    for t in range(QADAM):
        if keyin is not None and t == QADAM // 2:
            haqiqiy = keyin                                     # dunyo o'zgardi
        if strategiya == "teng (A/B/n)":
            a = np.full(S, t % K)
        elif strategiya == "epsilon-greedy":
            ortacha = np.where(n > 0, s / np.maximum(n, 1), 1.0)  # avval sinash
            a = np.argmax(ortacha + 1e-9 * rng.random((S, K)), axis=1)
            tasodif = rng.random(S) < eps
            a[tasodif] = rng.integers(0, K, tasodif.sum())
        else:                                                   # Thompson
            theta = rng.beta(1 + s, 1 + n - s)
            a = np.argmax(theta, axis=1)
        if strategiya == "Thompson unutuvchi":
            n *= 0.9998                                         # eski dalil
            s *= 0.9998                                         # so'nadi
        mukofot = rng.random(S) < haqiqiy[a]
        n[satr, a] += 1
        s[satr, a] += mukofot
        tanlov_soni[satr, a] += 1
        regret += haqiqiy.max() - haqiqiy[a]
    topdi = np.argmax(s / np.maximum(n, 1), axis=1) == np.argmax(haqiqiy)
    return regret, tanlov_soni / QADAM, topdi


def main() -> None:
    print("=== 1. Sozlama ===")
    print(f"  haqiqiy konversiyalar: {HAQIQIY.tolist()} (algoritmga noma'lum)")
    print(f"  {QADAM} tashrif, {SEEDLAR} takror; regret = yo'qotilgan "
          f"konversiyalar (eng yaxshisiga nisbatan)")

    print("\n=== 2. Natijalar ===")
    natija = {}
    print(f"  {'strategiya':<18} {'regret':>6} {'eng yaxshiga trafik':>20} "
          f"{'eng yaxshini topdi':>19}")
    for nom in STRATEGIYALAR:
        r, ulush, topdi = simulyatsiya(nom, seed=1)
        natija[nom] = r
        print(f"  {nom:<18} {r.mean():>6.1f} {ulush[:, -1].mean():>20.3f} "
              f"{topdi.mean():>19.3f}")

    print("\n=== 3. Juftlashgan farq (bir xil urug' indekslari) ===")
    for a, b in [("epsilon-greedy", "teng (A/B/n)"),
                 ("Thompson", "teng (A/B/n)"),
                 ("Thompson", "epsilon-greedy"),
                 ("Thompson unutuvchi", "Thompson")]:
        d = natija[a] - natija[b]
        se = d.std(ddof=1) / np.sqrt(len(d))
        print(f"  {a} - {b}: {d.mean():+.1f} (SE {se:.1f}), "
              f"sezilarli: {abs(d.mean()) > 2 * se}")
    eng = min(natija, key=lambda k: natija[k].mean())
    print(f"  eng kam regret: {eng}")

    print("\n=== 4. Nostatsionar dunyo: 10000-qadamda 1-variant 0.080 bo'ldi ===")
    keyin = np.array([0.080, 0.055, 0.070])
    print(f"  {'strategiya':<18} {'regret':>6} {'yangi eng yaxshini topdi':>25}")
    ns = {}
    for nom in STRATEGIYALAR:
        r, _, topdi = simulyatsiya(nom, seed=2, keyin=keyin)
        ns[nom] = r
        print(f"  {nom:<18} {r.mean():>6.1f} {topdi.mean():>25.3f}")
    d = ns["Thompson unutuvchi"] - ns["Thompson"]
    se = d.std(ddof=1) / np.sqrt(len(d))
    print(f"  unutuvchi - Thompson: {d.mean():+.1f} (SE {se:.1f}), "
          f"sezilarli: {abs(d.mean()) > 2 * se}")
    print("  oddiy bandit eski dalilni unutmaydi -> drift da sekin moslashadi")

    print("\n=== 5. Qachon qaysi biri ===")
    tejam = natija["teng (A/B/n)"].mean() - natija[eng].mean()
    print(f"  {eng} teng taqsimotga nisbatan ~{tejam:.0f} konversiya tejadi")
    print("  lekin: bandit kuchsiz variantlarga kam trafik beradi ->")
    print("  ular haqida ishonch intervali keng, 'nega yutdi' tahlili qiyin")
    print("  ⭐ Bilim kerak - A/B test; daromad kerak - bandit")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Sozlama ===
  haqiqiy konversiyalar: [0.05, 0.055, 0.07] (algoritmga noma'lum)
  20000 tashrif, 200 takror; regret = yo'qotilgan konversiyalar (eng yaxshisiga nisbatan)

=== 2. Natijalar ===
  strategiya         regret  eng yaxshiga trafik  eng yaxshini topdi
  teng (A/B/n)        233.3                0.333               1.000
  epsilon-greedy       62.5                0.815               0.965
  Thompson             39.6                0.882               0.995
  Thompson unutuvchi   74.9                0.780               0.985

=== 3. Juftlashgan farq (bir xil urug' indekslari) ===
  epsilon-greedy - teng (A/B/n): -170.9 (SE 4.9), sezilarli: True
  Thompson - teng (A/B/n): -193.7 (SE 1.9), sezilarli: True
  Thompson - epsilon-greedy: -22.9 (SE 5.4), sezilarli: True
  Thompson unutuvchi - Thompson: +35.3 (SE 2.3), sezilarli: True
  eng kam regret: Thompson

=== 4. Nostatsionar dunyo: 10000-qadamda 1-variant 0.080 bo'ldi ===
  strategiya         regret  yangi eng yaxshini topdi
  teng (A/B/n)        233.4                     0.130
  epsilon-greedy      143.2                     0.145
  Thompson            120.5                     0.325
  Thompson unutuvchi  100.4                     0.950
  unutuvchi - Thompson: -20.1 (SE 3.0), sezilarli: True
  oddiy bandit eski dalilni unutmaydi -> drift da sekin moslashadi

=== 5. Qachon qaysi biri ===
  Thompson teng taqsimotga nisbatan ~194 konversiya tejadi
  lekin: bandit kuchsiz variantlarga kam trafik beradi ->
  ular haqida ishonch intervali keng, 'nega yutdi' tahlili qiyin
  ⭐ Bilim kerak - A/B test; daromad kerak - bandit

Natija tahlili.

2-bo'lim — uch variant (5.0%, 5.5%, 7.0%), 20 000 tashrif, 200 takror. Teng taqsimot (A/B/n) har variantga 1/3 trafik berdi va 233.3 konversiya yo'qotdi — bu "o'rganish narxi". Epsilon-greedy regretni 62.5 ga, Thompson 39.6 ga tushirdi; Thompson trafikning 88.2% ini eng yaxshi variantga yo'naltirdi va 200 takrorning 99.5% ida uni to'g'ri topdi.

3-bo'lim — farqlar juftlashgan va hammasi sezilarli: Thompson epsilon-greedy dan 22.9 konversiya kam yo'qotadi (SE 5.4). Epsilon-greedy doimiy eps = 0.1 tufayli yomon variantlarga abadiy trafik beradi; Thompson esa ishonch oshgan sari o'rganishni o'zi kamaytiradi. Unutuvchi Thompson barqaror dunyoda +35.3 yomonroq — u eski dalillarni tashlab, keraksiz qayta o'rganadi.

4-bo'lim — dunyo o'zgardi: 10 000-qadamda 1-variant 0.080 ga ko'tarildi (endi eng yaxshisi). Oddiy Thompson oxirida yangi eng yaxshisini faqat 32.5% hollarda topdi — 10 000 qadamlik eski dalillar "7% li variant yaxshiroq" deb turibdi. Diskontli (unutuvchi, gamma = 0.9998) Thompson 95.0% hollarda topdi va regretni 20.1 ga kamaytirdi (SE 3.0). Bu 1-misoldagi oyna tanlovi bilan bir xil murosa: xotira uzun — barqarorlikda yaxshi, driftda sekin; xotira qisqa — aksincha.

5-bo'lim — xulosa. Bandit daromadni oshiradi, lekin bilimni kamaytiradi: kuchsiz variantlar kam trafik oladi, ular haqida ishonchli xulosa chiqmaydi. Agar savol "qaysi variant nega yaxshi va qanchaga" bo'lsa — A/B test; agar savol "sinov davomida iloji boricha kam yo'qotish" bo'lsa (sarlavha variantlari, reklama bannerlari) — bandit.


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

Noto'g'ri fikr To'g'risi
"Qayta o'qitish har doim foydali" Barqaror dunyoda foyda yo'q (1-misol)
"Yangi ma'lumot ko'p bo'lsa — yaxshi" Oyna drift turiga bog'liq; qaytuvchida qisqa oyna eng yomon
"Oflayn metrika yaxshi — ishga tushiramiz" Shadow, darvoza, canary, kerak bo'lsa A/B
"Umumiy metrika yaxshilandi — hammasi joyida" Segmentlarni alohida tekshiring
"Taqsimot guardraili nosozlikni ushlaydi" Ustun almashuvi taqsimotni o'zgartirmaydi — nazorat namunalari kerak
"Har kuni qarash zararsiz" Yolg'on musbat 5% dan ~30% gacha oshadi
"Asosiy metrika sezilarli — chiqaramiz" Guardrail ham o'tishi kerak
"hash(user_id) % 2 yetarli" Python hash() jarayonga bog'liq; sha256 + tuz
"Bandit A/B testdan har doim yaxshi" Regret kam, lekin bilim kam; driftda diskont kerak
"Rollback qo'lda qilinadi" Avtomatik va oldindan sinalgan bo'lishi kerak

6. Keng tarqalgan xatolar va yechimlari

1. Darvozasiz avtomatik ishga tushirish

python
model = orgat(yangi_malumot); registr.production = model            # ⚠️
kandidat = orgat(yangi_malumot)
if darvoza(kandidat, champion, shadow_log): registr.promote(kandidat)  # ✅

2. Backtestda kelajakka qarash

python
model = orgat(hamma_oylar); baho = metrika(model, oy[t])            # ⚠️
model = orgat(oylar[:t]);   baho = metrika(model, oy[t])            # ✅

3. Faqat umumiy metrika

python
ok = d.mean() < -2 * se                                             # ⚠️
ok = d.mean() < -2 * se and max(d[seg == s].mean() for s in segmentlar) <= 0.02  # ✅

4. Beqaror taqsimlash

python
guruh = "B" if random.random() < 0.5 else "A"          # ⚠️ har so'rovda boshqa
guruh = "B" if hash(user_id) % 2 else "A"              # ⚠️ jarayonga bog'liq
guruh = xesh_guruh(user_id, "tajriba_nomi")            # ✅ sha256 + tuz

5. Peeking

python
if p_qiymat(bugun) < 0.05: toxtat_va_chiqar()          # ⚠️ har kuni
if kun == REJA_KUNI: qaror(p_qiymat(kun))              # ✅ bir marta

6. Guardrailsiz qaror

python
if p_konversiya < 0.05: chiqar()                       # ⚠️
if p_konversiya < 0.05 and qaytarish_ci[1] <= CHEGARA: chiqar()   # ✅

7. Eski artefaktni o'chirish

python
os.remove("modellar/v1.joblib")                        # ⚠️ rollback imkonsiz
registr.arxivla("v1")                                  # ✅ saqlanadi

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 11.2, 11.4, 11.10-darslar (o'tilgan): Quvvat, proporsiyalar testi, A/B test loyihasi
  • 18.10-dars (o'tilgan): Modellarni juftlashgan taqqoslash
  • 27.4, 27.5-darslar (o'tilgan): Eksperiment jurnali va model registri — CT quvurining xotirasi
  • 27.10-dars (o'tilgan): Deploy strategiyalari — canary, blue-green
  • 27.12-dars (o'tilgan): Drift — qayta o'qitish triggeri
  • 27.14-dars: Amaliyot — trigger, shadow, darvoza, registr va xizmatda versiya almashuvi

8. Eng yaxshi amaliyotlar

  1. Qayta o'qitish va ishga tushirishni ajrating.

  2. O'quv oynasini vaqt bo'yicha backtest bilan tanlang.

  3. Darvoza qoidalarini oldindan, kodda yozing — segmentlar bilan.

  4. Shadow rejimda bir xil so'rovlarda juftlashgan taqqoslang.

  5. Canaryda nazorat namunalari va avtomatik rollback.

  6. A/B: xesh + tuz, hajm oldindan, bir marta qarash.

  7. Guardrail metrikalarsiz qaror yo'q.

  8. Eski versiyalarni arxivlang, o'chirmang.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # barqaror dunyoda statik va qayta o'qitilgan model?
2.  # to'satdan driftda qaysi oyna yaxshi?
3.  # qaytuvchi rejimda qisqa oyna?
4.  # shadow rejimda foydalanuvchi qaysi javobni oladi?
5.  # darvoza qoidalari qachon yoziladi?
6.  # ustunlar almashsa bashorat PSI?
7.  # A/B uchun Python hash() yaraydimi?
8.  # MDE ikki marta kichraysa n?
9.  # 30 kun har kuni qarash - yolg'on musbat?
10. # asosiy metrika sezilarli, guardrail yomon - qaror?
11. # Thompson vs teng taqsimot - regret?
12. # nostatsionar dunyoda oddiy bandit?
Javoblar
  1. Deyarli bir xil — qayta o'qitish foyda bermaydi
  2. Qisqa (oxirgi N oy)
  3. Eng yomoni bo'lishi mumkin — doim bir qadam orqada
  4. Champion javobini
  5. Natijani ko'rishdan oldin
  6. Deyarli o'zgarmaydi — nazorat namunalari kerak
  7. Yo'q — jarayonga bog'liq; sha256 + tuz
  8. ~4 marta oshadi
  9. ~0.29 (5% emas)
  10. Chiqarmaslik
  11. Ancha kam
  12. Sekin moslashadi — diskont kerak

Vazifa 2: Xatolarni tuzating

python
1.  model = orgat(yangi); registr.production = model

2.  guruh = "B" if hash(user_id) % 2 else "A"

3.  if p_qiymat(bugun) < 0.05: toxtat()

4.  ok = d.mean() < -2 * se          # faqat umumiy

5.  os.remove("modellar/v1.joblib")
Javoblar
python
1.  kandidat = orgat(yangi); registr.promote(kandidat) if darvoza(kandidat) else None

2.  guruh = xesh_guruh(user_id, "tajriba_nomi")

3.  if kun == REJA_KUNI: qaror(p_qiymat(kun))

4.  ok = d.mean() < -2 * se and all(d[seg == s].mean() <= 0.02 for s in segmentlar)

5.  registr.arxivla("v1")

Vazifa 3: O'quv oynasi

Modellang (1-misol asosida):

  1. Oyna uzunligini 1, 3, 6 oy qiling — to'satdan driftda qaysi biri yaxshi?
  2. Vaznli oynaning yarim umrini 1, 3, 6 oy qiling
  3. Qaytuvchi rejimda modelga "rejim" belgisini qo'shing — log loss qanchaga yaxshilanadi?
  4. Har oydagi mijozlar sonini 200 ga tushiring — barqaror dunyoda qisqa oyna qanchalik yomonlashadi?

Vazifa 4: Darvoza

Modellang (2-misol asosida):

  1. 2-hudud ulushini 5% ga tushiring — B umumiy metrikada yaxshi ko'rinadimi, segment qoidasi uni ushlaydimi?
  2. Kalibrovka qoidasini qo'shing: |mean(p) - mean(y)| <= 0.03
  3. Kechikish qoidasini qo'shing (operatsiyalar sonini kechikish o'rnida sanang)
  4. Canary ni uch bosqichli (1%, 10%, 50%) qiling

Vazifa 5: A/B test

Modellang (3-misol asosida):

  1. Bazaviy konversiya 2% bo'lsa, +0.2 p.p. uchun n qancha?
  2. 5 marta qarash uchun O'Brien-Fleming chegaralari bilan yolg'on musbat va quvvatni o'lchang (chegaralar: 4.56, 3.23, 2.63, 2.28, 2.04)
  3. A/A testni har kuni tekshiruvchi "salomatlik" testi sifatida ishlating (namuna nisbati: SRM testi)
  4. Guardrail uchun non-inferiority chegarasini o'zgartirib, qaror qanday o'zgarishini ko'ring

Vazifa 6: Bandit

Modellang (4-misol asosida):

  1. eps ni 0.01, 0.05, 0.2 qiling
  2. UCB1 ni yozing va Thompson bilan solishtiring
  3. Kechikkan mukofot: konversiya 500 qadamdan keyin ma'lum bo'lsin
  4. Diskont gamma ni 0.999, 0.9995, 0.9998 qilib, barqaror va nostatsionar regret murosasini jadval qiling

Vazifa 7: O'ylash

Jamoa taklif qildi: "CT quvuri tayyor. Har hafta avtomatik qayta o'qitamiz va yangi model oflayn AUC da champion dan yaxshi bo'lsa, darhol production ga chiqaramiz. Shadow va canary — vaqtni behuda sarflash." Nima deysiz?

Javob

Qisqa javob: avtomatik qayta o'qitish — yaxshi; avtomatik ishga tushirish faqat oflayn AUC bilan — xavfli. Shadow va canary vaqtni tejaydi, chunki xatoni foydalanuvchilarga yetmasdan ushlaydi.

1. Oflayn AUC nimani ko'rmaydi (2-misol).

  • Segmentlar: quvur xatosi bilan bir hudud ma'lumoti tushib qolsa, umumiy metrika hatto yaxshilanishi mumkin, o'sha hudud esa keskin yomonlashadi (B: 2-hududda +0.162).
  • Operatsion xatolar: yangi versiyaning kirish tekshiruvi buzuq so'rovlarda yiqiladi (B: 0.20%) — test to'plamida bunday so'rov yo'q edi.
  • Xizmat kodi xatolari: model to'g'ri, lekin xizmat kodida ustunlar almashgan (v3). Oflayn baholash modelni tekshiradi, xizmatni emas. Buni faqat canarydagi nazorat namunalari ushladi (maks farq 0.872), bashorat taqsimoti esa deyarli o'zgarmadi (PSI 0.016).

2. Har hafta qayta o'qitish ham har doim kerak emas (1-misol). Barqaror dunyoda qayta o'qitilgan model statikdan farq qilmadi — lekin har yangi versiya yangi xavf. Trigger (drift yoki sifat) bilan birga ishlatilsa, bekor almashtirishlar kamayadi.

3. Taklif qilinadigan tartib.

python
# har hafta: qayta o'qitish -> kandidat (avtomatik)
# darvoza 1 (oflayn, avtomatik): juftlashgan farq + segmentlar + kalibrovka
# shadow 3-7 kun (avtomatik): xatolar, bir xil so'rovlarda juftlashgan
# darvoza 2 (avtomatik): belgilar kelgach
# canary 1% -> 10% -> 100% (avtomatik): PSI + nazorat namunalari
# rollback (avtomatik), eski versiya arxivda

Hammasi avtomatik bo'lishi mumkin — "avtomatik" va "tekshiruvsiz" bir xil narsa emas.

4. Qachon A/B test kerak. Agar model qarori foydalanuvchi xulqiga ta'sir qilsa (tavsiya, narx, chegirma), shadow buni ko'rmaydi — biznes metrikasi bo'yicha A/B test kerak, oldindan hisoblangan hajm va guardraillar bilan (3-misol).

Jamoaga javob: "Avtomatik qayta o'qitishni qo'llab-quvvatlayman. Lekin ishga tushirish oldidan uchta avtomatik tekshiruv qo'shamiz: segmentli darvoza, qisqa shadow va nazorat namunali canary. Bular kuniga daqiqalar oladi, bitta yomon versiyaning narxi esa haftalar."

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


Xulosa

Bu darsda modelni xavfsiz yangilash tartibini qurdik.

Eng muhim uch fikr:

  1. Qachon va qaysi ma'lumotda — backtest hal qiladi. 1-misolda to'rt drift turida to'rt strategiya taqqoslandi. Barqaror dunyoda statik model qayta o'qitilganlardan farq qilmadi — qayta o'qitish foyda bermadi. To'satdan va asta-sekin driftda oxirgi 3 oy oynasi eng yaxshi bo'ldi (statik 0.8261 ga qarshi 0.5445). Qaytuvchi rejimda esa qisqa oyna o'rtacha bo'yicha eng yomoni chiqdi (farqi juda tarqoq bo'lgani uchun sezilarli emas) — almashishdan keyin doim bir qadam orqada; u yerda rejimni modelga belgi sifatida berish kerak. Bandit misolida ham xuddi shu murosa takrorlandi: xotira uzun — barqarorlikda yaxshi, driftda sekin.

  2. Yangi model shadow, darvoza va canary orqali o'tadi. 2-misolda shadow rejim foydalanuvchiga ta'sir qilmadi, bir xil so'rovlarda juftlashgan taqqoslash A ni sezilarli yaxshi (-0.0711, SE 0.0113), B ni esa segmentda halokatli (+0.162) deb ko'rsatdi. Oldindan yozilgan darvoza B ni to'xtatdi. Canaryda taqsimot guardraili ustunlar almashuvini ko'rmadi (PSI 0.016), nazorat namunalari ko'rdi (0.872) — va rollback avtomatik bajarildi.

  3. A/B testda qaror bir marta, guardrail bilan. Xesh + tuz barqaror va mustaqil taqsimlashni berdi; quvvat tahlili 10% → 11% uchun har guruhga 14751 foydalanuvchi talab qildi va simulyatsiya uni tasdiqladi (quvvat 0.809). Har kuni qarab to'xtatish yolg'on musbatni 0.057 dan 0.293 ga oshirdi. Asosiy metrika sezilarli yaxshilangan tajriba guardrail (qaytarishlar) tufayli to'xtatildi. Bandit esa sinov narxini kamaytiradi (Thompson regret 39.6, teng taqsimot 233.3), lekin bilimni ham kamaytiradi.

Keyingi darsda 27-qism amaliyoti: mijoz ketishini bashorat qilish xizmatini oxiridan oxirigacha quramiz — ma'lumot quvuri, eksperiment kuzatuvi va registr, paket, FastAPI xizmati, monitoring, drift, qayta o'qitish triggeri, shadow baholash, darvoza va xizmatda versiya almashuvi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
27.13-dars: Qayta o'qitish va A/B test — IlmHamroh