IlmHamroh
Data Science va sun'iy intellekt/Maxsus mavzular12/12-dars51 daqiqa
Mundarija (22)

28.12-dars: Amaliyot — do'konlar tarmog'i uchun talab bashorati va anomaliya monitoringi

28-QISM — MAXSUS MAVZULAR · 12-dars


1. Kirish va motivatsiya

28-qismda kursning asosiy yo'lidan tashqarida qolgan, lekin amalda tez-tez uchraydigan vazifalarni ko'rdik: vaqt qatorlari va ularning klassik va ML modellari, tavsiya tizimlari, ilg'or anomaliya aniqlash, reinforcement learning, geografik ma'lumot, katta ma'lumot va sababiy xulosa. Endi ularning bir nechtasini bitta real loyihaga yig'amiz — chakana savdoda eng ko'p uchraydigan ML vazifasiga.

Vazifa — 8 ta do'kondan iborat tarmoq (Toshkent, Samarqand, Buxoro, Namangan, Andijon) uchun kunlik talab bashorati: har do'kon va har mahsulot guruhi (non, sut, meva) bo'yicha keyingi 28 kunlik savdo. Bashorat omborga buyurtma berish uchun ishlatiladi: kam buyurtma — mahsulot tugaydi, xaridor ketadi; ko'p buyurtma — non va sut buziladi, meva chiriydi. Ma'lumotda hamma narsa bor: o'sish trendi, haftalik va yillik mavsum, bayram kunlari va ularning arafasi, aksiya kunlari — va, haqiqiy hayotdagidek, ma'lumot xatolari.

Loyiha to'rt qadamdan iborat, har biri alohida misol:

  1. Ma'lumot va sifat — takrorlar, noto'g'ri qiymatlar, yo'qolgan kunlar, anomaliyalar (28.6 g'oyasi: kutilgan qiymatdan robust og'ish), hujjatlashtirilgan yopilishlar; tozalash va to'ldirish siyosati.
  2. Modellar va backtest — seasonal naive, ETS va global HistGB (lag, sirpanuvchi, taqvim, bayram va aksiya belgilari — sizishsiz) rolling-origin backtest da; do'kon × oyna bo'yicha juftlashgan taqqoslash va "eng yaxshisidan sezilarli yomon bo'lmagan eng sodda" qoidasi; ufq bo'yicha xato.
  3. Noaniqlik va zaxira qarori — konformal va kvantil intervallar, ularning haqiqiy qamrovi; yetishmaslik va ortiqcha narxidan kelib chiqib buyurtma kvantilini tanlash.
  4. Yakun — test davri bir marta; aksiya ta'sirini sababiy baholash (aksiyalar tasodifiy emas — sust kunlarda o'tkaziladi); yakuniy model paketi va monitoring rejasi.

Real vaziyat. Supermarketlar tarmog'i talab bashoratini "o'tgan yilning shu haftasi + 5%" qoidasi bilan qilardi. Yangi model backtest da o'rtacha xatoni 20% ga kamaytirdi va ishga tushirildi. Ikki oydan keyin non bo'limida isrof kamaymadi, aksincha oshdi. Tahlil ko'rsatdiki: (1) model aksiya kunlarini "odatiy talab" deb o'rgangan — aksiya taqvimi modelga berilmagan edi; (2) omborga nuqtali bashorat (o'rtacha) yuborilgan, holbuki non uchun ortiqcha qolgan mahsulot narxi yetishmaslik narxidan yuqori — buyurtma o'rtachadan past kvantilda bo'lishi kerak edi; (3) bitta do'konda kassa tizimi uch kun nol yozgan, model buni "talab tushdi" deb o'rgangan. Bu darsdagi loyiha aynan shu uch xatoni oldini oladi.

Bu darsda talab bashorati tizimini ma'lumot sifatidan tortib zaxira qarori, sababiy tahlil va monitoringgacha quramiz — har qarorni oldindan yozilgan qoida va juftlashgan taqqoslash bilan.

Bu darsda:

  • Loyiha xaritasi va qaror qoidalari (oldindan)
  • Ma'lumot sifati: validatsiya, yo'q kunlar, anomaliyalar, kutilgan hodisalar
  • Rolling-origin backtest: seasonal naive, ETS, global HistGB — sizishsiz belgilar
  • Juftlashgan taqqoslash do'kon × oyna bo'yicha, ufq bo'yicha xato
  • Bashorat intervallari: konformal va kvantil, qamrov tekshiruvi
  • Zaxira qarori: kritik nisbat va buyurtma kvantili
  • Aksiya ta'siri: naiv va tuzatilgan baholash
  • Model paketi va monitoring rejasi
  • Tuzoqlar

ℹ Misollar real numpy/pandas/statsmodels/sklearn/joblib bilan (Python 3.14). Har misol mustaqil ishlaydi — ma'lumot generatori va umumiy funksiyalar har birida takrorlanadi. Bayram sanalari sintetik taqvim — o'quv maqsadida to'qilgan "bayram kunlari".


2. Nazariya — chuqur tushuntirish

2.1. Loyiha xaritasi

text
MANBA: kassa tizimlaridan kunlik savdo (do'kon x guruh x kun) ---- (1-misol)
  |      + aksiya taqvimi (oldindan rejalashtiriladi)
  |      + bayram taqvimi, hujjatlashtirilgan yopilishlar (ta'mir)
  v
SIFAT: takror -> olib tashlash; manfiy -> NaN; to'liq panjara (yo'q kunlar)
  anomaliya: robust qoldiq (o'tgan 4 haftaning shu kuni medianasi)
  kutilgan hodisa (bayram, arafa, aksiya, yopilish) -> anomaliya EMAS
  anomaliya va yo'q kun -> to'ldirish (hafta kuni medianasi) + belgi
  |
  v
BACKTEST: 4 ta 28 kunlik oyna, oxirgi 28 kun - TEST (tegilmaydi) -- (2-misol)
  seasonal naive  <  ETS (har qator)  <  global HistGB   (soddalik tartibi)
  metrika: WAPE (do'kon x oyna birligida), juftlashgan farq + SE
  QOIDA: eng yaxshisidan sezilarli yomon bo'lmagan eng sodda
  |
  v
NOANIQLIK: konformal (backtest qoldiqlari) va kvantil HistGB -------- (3-misol)
  qamrov: nominal 80% - haqiqatda nechchi?
  ZAXIRA: kritik nisbat = c_yetishmaslik / (c_yetishmaslik + c_ortiqcha)
  |
  v
YAKUN: test bir marta -> hisobot                                   (4-misol)
  aksiya ta'siri: naiv / regressiya / kontrfaktual model / moslashtirish
  paket (joblib): model + belgilar + metadata + nazorat namunalari
  monitoring rejasi: ma'lumot, sifat, qamrov, trigger (27.11-27.13)

Qaror qoidalari 2-qadam boshida, natijalarni ko'rishdan oldin yoziladi. Test davri faqat oxirida, bir marta ochiladi — u tanlov uchun emas, hisobot uchun.

2.2. Ma'lumot sifati

text
VALIDATSIYA 27.3-bob:
  kalit (do'kon, guruh, sana) noyob     -> takrorlar olib tashlanadi
  savdo >= 0                           -> manfiy (qaytarish yozuvi) -> NaN
  to'liq panjara: har kun uchun qator   -> yo'q qator = NaN (0 EMAS!)

ANOMALIYA (28.6 g'oyasi - kutilgan qiymat + robust og'ish):
  kutilgan b_t = median( y_{t-7}, y_{t-14}, y_{t-21}, y_{t-28} )  (log1p da)
  qoldiq  r_t = log1p(y_t) - b_t
  robust z = (r - median(r)) / (1.4826 * MAD(r))     (qator bo'yicha)
  |z| > chegara -> shubhali
  KUTILGAN HODISALAR (bayram, arafa, aksiya, hujjatlashtirilgan yopilish):
     anomaliya ro'yxatidan chiqariladi - ular modelga BELGI sifatida beriladi

TO'LDIRISH SIYOSATI:
  yo'q kun, kassa noli, birlik xatosi  -> NaN -> hafta kuni medianasi (+ belgi)
  hujjatlashtirilgan yopilish          -> 0 qoladi, lekin o'quvdan chiqariladi
  hamma tuzatish jurnalga yoziladi (nima, qayerda, qanday)

Nega 0 bilan to'ldirish xavfli: model "talab tushdi" deb o'rganadi — keyingi haftalarda past bashorat, omborda yetishmaslik. Nega anomaliyani shunchaki tashlab bo'lmaydi: lag belgilari (o'tgan hafta shu kuni) bo'sh qoladi. To'ldirish — kichik yomonlik; lekin to'ldirilgan qiymat belgilanadi, va baholashda (xato hisobida) ishlatilmaydi.

2.3. Backtest dizayni va qaror qoidalari

text
UFQ: h = 1..28 kun (ombor 4 haftalik reja bo'yicha buyurtma beradi)
ORIGIN: har 28 kunda, bir xil hafta kuni (masalan yakshanba kechqurun)
OYNALAR: oxirgi 28 kun - TEST; undan oldingi 4 ta 28 kunlik oyna - backtest
  oynalar qoplanmaydi -> xatolar mustaqilroq, SE halolroq 28.1-bob

NOMZODLAR (soddalik tartibida):
  1. seasonal naive:  f(o+h) = y(o + h - 7*ceil(h/7))   "o'tgan haftaning shu kuni"
  2. ETS (A,N,A), log1p shkalada, har qator alohida, oxirgi 182 kun   28.2-bob
  3. global HistGB: BITTA model hamma 24 qator uchun                  28.3-bob

METRIKA: WAPE = sum|y - f| / sum y   (nollarga chidamli, birliksiz)
BIRLIK: do'kon x oyna (8 x 4 = 32) - har do'konning 3 guruhi x 28 kuni
TAQQOSLASH: d = WAPE_model - WAPE_eng_yaxshi, SE = std(d)/sqrt(32)
QOIDA: |d| <= 2*SE bo'lgan (sezilarli yomon bo'lmagan) eng sodda model

2.4. Global model belgilari va sizish

Global HistGB — "direct" strategiya: origin o da ma'lum bo'lgan narsalardan o + h kunini bashorat qiladi. Belgilar faqat o gacha bo'lgan qiymatlardan yoki oldindan ma'lum narsalardan olinadi.

text
ORIGIN DA MA'LUM (o gacha):
  daraja28 = log1p(o'rtacha y[o-27 .. o])             - qator darajasi
  daraja7 - daraja28                                  - oxirgi haftaning siljishi
  sn = log1p(y[o + h - 7*ceil(h/7)]) - daraja28       - seasonal naive nisbiy
  otgan_yil = log1p(y[t-367 .. t-361]) - log1p(y[o-391 .. o-364])
                                                      - o'tgan yilning shakli
OLDINDAN MA'LUM (kelajak kuni t = o + h uchun):
  h, hafta kuni, yil kuni (sin, cos), bayram, arafa, AKSIYA (reja!)
IDENTIFIKATOR: do'kon, guruh (kategoriya)
MAQSAD: log1p(y_t) - daraja28  (nisbiy - do'konlar hajmi turlicha)

SIZISH TUZOQLARI 17.8-bob:
  y[o+1 ..] dan har qanday statistika (sirpanuvchi o'rtacha "markazlashgan")
  o'quv misollarining maqsad kuni t > backtest origin O  ->  o'quv faqat
    o <= O - 28 originlardan (maqsad kunlari O dan oshmaydi)
  aksiya belgisi - faqat REJADA bo'lsa (kechikib kiritiladigan aksiya - sizish)

2.5. Noaniqlik va zaxira qarori

text
KONFORMAL (split, qoldiq kvantillari):
  kalibrovka oynalarida qoldiq r = log1p(y) - log1p(f)  (ufq haftasi bo'yicha)
  interval: expm1( log1p(f) + [q_0.1(r), q_0.9(r)] )    -> nominal 80%
  kafolat: yangi ma'lumot kalibrovkaga "o'xshash" bo'lsa (almashinuvchanlik)
  bayram/aksiya kunlari kam -> ular uchun qamrov alohida tekshiriladi

KVANTIL HistGB: loss="quantile", quantile=0.1 va 0.9 - alohida modellar
  kafolat yo'q, lekin sharoitga (belgilarga) moslashadi

ZAXIRA (newsvendor, 27-qism oldidagi biznes misollar):
  c_u - 1 dona yetishmaslik narxi (yo'qotilgan marja, xaridor noroziligi)
  c_o - 1 dona ortiqcha narxi (buzilish, chegirma bilan sotish)
  optimal buyurtma = talabning  c_u / (c_u + c_o)  kvantili
  c_u = 3, c_o = 1  ->  0.75 kvantil;   c_u = c_o  ->  mediana
  NUQTALI bashorat (o'rtacha/mediana) buyurtma sifatida - odatda NOTO'G'RI kvantil

2.6. Aksiya ta'siri — sababiy savol

Marketing so'raydi: "aksiya savdoni necha foizga oshiradi?" Aksiyalar tasodifiy emas — ular sust kunlarda (hafta boshi, past mavsum) o'tkaziladi. Bu — 28.11 dagi chalkashtiruvchi.

text
NAIV:  mean(y | aksiya) / mean(y | aksiyasiz) - 1
       aksiya kunlari o'zi sust -> ta'sir KAM ko'rinadi (hatto manfiy)

TUZATILGAN 28.11-bob:
  regressiya: log1p(y) ~ aksiya + qator + hafta_kuni x guruh + oy x guruh
              + bayram + arafa        (SE - qator bo'yicha klaster)
  kontrfaktual model (g-hisoblash): aksiyasiz kunlarda o'qitilgan model
              aksiya kunlari uchun "aksiya bo'lmaganda" savdoni bashorat qiladi
              ta'sir = mean( log1p(y) - log1p(f_aksiyasiz) )  -> ATT
  moslashtirish: aksiya kuni <-> shu qator, shu hafta kuni, +-1-2 hafta,
              aksiyasiz va bayramsiz kunlar
FARAZ: aksiya tanlovi taqvim va mavsum orqali tushuntiriladi
       (menejer "bugun sust bo'ladi" degan boshqa ma'lumotdan foydalansa -
        o'lchanmagan chalkashtiruvchi, 28.11 3-misol)

2.7. Paket va monitoring

text
PAKET 27.6-bob:
  model, belgilar ro'yxati va tartibi, kategoriya ustunlari
  metadata: ma'lumot davri, oynalar, backtest WAPE, tanlov qoidasi natijasi,
            interval kalibrovkasi (q_0.1, q_0.9 ufq haftasi bo'yicha), versiya
  nazorat namunalari: 50 ta kirish + kutilgan chiqish -> yuklashda tekshiriladi

MONITORING (27.11, 27.12):
  ma'lumot:  yo'q qatorlar, anomaliya ulushi, kassa nollari (har kuni)
  sifat:     haftalik do'kon WAPE > chegara (backtest dan) 2 hafta ketma-ket
  intervallar: 4 haftalik 80% interval qamrovi < 70%
  drift:     aksiya ulushi, yangi do'kon, assortiment o'zgarishi
  TRIGGER -> qayta o'qitish 27.13-bob: jadval (har 4 hafta) + sifat triggeri

2.8. Tuzoqlar

Asosiy tuzoqlar: yo'q kunlarni va kassa nollarini 0 deb o'qitish; kutilgan hodisalarni (bayram, aksiya) anomaliya deb tozalab tashlash; aksiya taqvimini modelga bermaslik; sirpanuvchi belgilarni origin dan keyingi qiymatlardan hisoblash; o'quv misollarida maqsad kuni backtest origin idan keyin bo'lishi; tasodifiy bo'lish bilan baholash; bitta backtest oynasi bilan qaror; bazaviysiz "yaxshi natija"; test davrini model tanlashda ishlatish; nuqtali bashoratni buyurtma sifatida yuborish; intervallarning qamrovini tekshirmaslik (ayniqsa bayram va aksiya kunlarida); aksiya ta'sirini naiv farq bilan baholash; paketsiz (belgilar tartibisiz) model saqlash; monitoring chegaralarini taxminan qo'yish.


3. Tez ma'lumotnoma

python
# 1) sifat
xom = xom.drop_duplicates(["qator", "kun"])
W = xom.pivot(index="qator", columns="kun", values="savdo").reindex(
    index=range(S), columns=range(T)).to_numpy()          # yo'q kun -> NaN
b = np.nanmedian([siljit(np.log1p(W), 7 * k) for k in range(1, 5)], axis=0)
r = np.log1p(W) - b
z = (r - med) / (1.4826 * mad)
anomaliya = (np.abs(z) > 4) & ~kutilgan                   # bayram/arafa/aksiya/yopilish

# 2) backtest
sn = Y[s, O + h - 7 * np.ceil(h / 7).astype(int)]         # seasonal naive
ets = ExponentialSmoothing(qator, trend=None, seasonal="add",
                           seasonal_periods=7).fit().forecast(28)
model = HistGradientBoostingRegressor(categorical_features=[0, 1]).fit(X, y)
wape = np.abs(y - f).sum() / y.sum()
d = wape_a - wape_b; se = d.std(ddof=1) / np.sqrt(len(d))  # do'kon x oyna

# 3) noaniqlik va zaxira
q_lo, q_hi = np.quantile(qoldiq_kal, [0.1, 0.9])
past, yuqori = np.expm1(np.log1p(f) + q_lo), np.expm1(np.log1p(f) + q_hi)
kvantil = c_u / (c_u + c_o)                                 # 3 / (3 + 1) = 0.75
buyurtma = np.expm1(np.log1p(f) + np.quantile(qoldiq_kal, kvantil))

# 4) aksiya ta'siri va paket
tasir = np.mean(np.log1p(y_aksiya) - np.log1p(f_aksiyasiz))  # kontrfaktual
joblib.dump({"model": model, "belgilar": BELGILAR, "metadata": meta,
             "nazorat": {"X": X[:50], "bashorat": model.predict(X[:50])}}, yol)

Loyiha tuzilishi (haqiqiy repoda)

text
talab_bashorati/
  konfig/qoidalar.yaml    ufq, oynalar, tanlov qoidasi, c_u/c_o, monitoring chegaralari
  src/talab/
    manba.py        kassa, aksiya va bayram taqvimlari
    sifat.py        validatsiya, anomaliya, to'ldirish, jurnal
    belgilar.py     origin ga asoslangan belgilar (sizish testlari bilan)
    modellar.py     seasonal naive, ETS, global HistGB
    backtest.py     oynalar, metrikalar, juftlashgan taqqoslash
    noaniqlik.py    konformal kalibrovka, kvantillar, buyurtma
    sababiy.py      aksiya ta'siri
    paket.py        yasash, yuklash, nazorat
    monitoring.py   kunlik va haftalik tekshiruvlar
  tests/            har modul uchun; belgilar uchun "kelajakdan sizish" testi

Amaliyot xulosasi

xom -> takror/manfiy/yo'q -> anomaliya (kutilgan hodisalar - anomaliya emas) -> to'ldirish + belgi
backtest: 4 oyna, SN < ETS < HistGB, WAPE do'kon x oyna, juftlashgan qaror
interval: konformal (qoldiq kvantillari) / kvantil model -> qamrovni tekshir
buyurtma = c_u/(c_u+c_o) kvantili; aksiya ta'siri - tuzatilgan
test bir marta; paket + monitoring rejasi

4. Batafsil misollar

Misollar real numpy/pandas/statsmodels/sklearn/joblib bilan (Python 3.14). Har misol mustaqil ishlaydi, shuning uchun umumiy kod (ma'lumot generatori, belgilar, modellar) har birida takrorlanadi. Haqiqiy loyihada bu kod 3-bo'limdagi modullarda bir marta yoziladi. statsmodels ning chastota va yaqinlashish ogohlantirishlari warnings.catch_warnings() bilan o'chirilgan — indeksga freq berilgan, lekin optimizator xabarlari o'quv misolida shovqin.

Misol 1 — Ma'lumot sifati: validatsiya, anomaliyalar va to'ldirish

python
"""1-qadam: xom savdo ma'lumoti - validatsiya, anomaliyalar, to'ldirish siyosati."""

import warnings

import numpy as np
import pandas as pd

DOKONLAR = ["Toshkent-Chilonzor", "Toshkent-Yunusobod", "Toshkent-Sergeli",
            "Samarqand-Markaz", "Samarqand-Siyob", "Buxoro-Markaz",
            "Namangan-Markaz", "Andijon-Markaz"]
GURUHLAR = ["non", "sut", "meva"]
# sintetik taqvim: "bayram kunlari" (sanalar o'quv maqsadida to'qilgan)
BAYRAMLAR = ["2023-01-01", "2023-03-21", "2023-04-21", "2023-06-28",
             "2023-09-01", "2024-01-01", "2024-03-21", "2024-04-10",
             "2024-06-16", "2024-09-01", "2025-01-01", "2025-03-21",
             "2025-03-30", "2025-06-06", "2025-09-01"]
HAFTALIK = {"non": [1.0, 0.95, 0.95, 1.0, 1.05, 1.2, 1.1],
            "sut": [1.0, 1.0, 0.95, 1.0, 1.05, 1.15, 1.1],
            "meva": [0.9, 0.9, 0.9, 0.95, 1.05, 1.25, 1.3]}
YILLIK = {"non": (0.05, 15), "sut": (0.08, 30), "meva": (0.35, 200)}
ASOS = {"non": 120, "sut": 80, "meva": 60}


def malumot(seed=0):
    """Toza kunlik savdo: Y (24 qator x kun), aksiya A, taqvim, kutilgan mu."""
    rng = np.random.default_rng(seed)
    sana = pd.date_range("2023-01-01", "2025-12-31", freq="D")
    t = np.arange(len(sana))
    dow, doy = sana.dayofweek.to_numpy(), sana.dayofyear.to_numpy()
    bayram = sana.isin(pd.to_datetime(BAYRAMLAR))
    arafa = (np.roll(bayram, -1) | np.roll(bayram, -2)) & ~bayram
    Y, A, MU = [], [], []
    for _ in DOKONLAR:
        olcham, osish = rng.uniform(0.6, 1.6), rng.uniform(0.03, 0.15)
        for g in GURUHLAR:
            amp, choqqi = YILLIK[g]
            mu = (ASOS[g] * olcham * (1 + osish) ** (t / 365.25)
                  * np.array(HAFTALIK[g])[dow]
                  * (1 + amp * np.cos(2 * np.pi * (doy - choqqi) / 365.25))
                  * np.where(bayram, 0.4, 1.0) * np.where(arafa, 1.8, 1.0))
            nisbiy = mu / pd.Series(mu).rolling(29, center=True,
                                                min_periods=1).mean().to_numpy()
            p = np.clip(0.05 * nisbiy ** -6.0, 0, 0.35) * ~bayram * ~arafa
            aksiya = rng.random(len(t)) < p               # sust kunlarda ko'proq
            Y.append(rng.poisson(mu * np.where(aksiya, 1.25, 1.0)
                                 * rng.gamma(20, 1 / 20, len(t))))
            A.append(aksiya)
            MU.append(mu)
    kal = {"sana": sana, "dow": dow, "doy": doy, "bayram": bayram,
           "arafa": arafa}
    return np.array(Y, dtype=float), np.array(A), kal, np.array(MU)


def xom_jadval(Y, kal, rng):
    """Toza ma'lumotdan kassa tizimi 'xom' eksporti: xatolar bilan."""
    S, T = Y.shape
    xom = pd.DataFrame({"qator": np.repeat(np.arange(S), T),
                        "kun": np.tile(np.arange(T), S),
                        "savdo": Y.ravel(), "tur": "toza"})
    t0 = kal["sana"].get_loc(pd.Timestamp("2024-08-05"))
    yop = (xom.qator // 3 == 5) & xom.kun.between(t0, t0 + 6)   # ta'mir
    xom.loc[yop, "savdo"] = 0.0
    xom.loc[yop, "tur"] = "yopilish"
    toza = np.flatnonzero((xom.tur == "toza").to_numpy() & (xom.kun >= 60).to_numpy())
    pos_kun = rng.choice(np.arange(60, T), 12, replace=False)
    pos_dokon = rng.integers(0, len(DOKONLAR), 12)
    for d, k in zip(pos_dokon, pos_kun):                        # kassa noli
        m = (xom.qator // 3 == d) & (xom.kun == k) & (xom.tur == "toza")
        xom.loc[m, "savdo"] = 0.0
        xom.loc[m, "tur"] = "kassa_nol"
    toza = np.flatnonzero((xom.tur == "toza").to_numpy() & (xom.kun >= 60).to_numpy())
    tanlov = rng.choice(toza, 50, replace=False)
    xom.loc[tanlov[:30], "savdo"] *= 10                         # birlik xatosi
    xom.loc[tanlov[:30], "tur"] = "birlik"
    xom.loc[tanlov[30:], "savdo"] *= -1                         # qaytarish yozuvi
    xom.loc[tanlov[30:], "tur"] = "manfiy"
    toza = np.flatnonzero((xom.tur == "toza").to_numpy())
    yoqolgan = rng.choice(toza, int(0.015 * len(xom)), replace=False)
    xom = xom.drop(index=yoqolgan)
    takror = xom.sample(40, random_state=1)
    return pd.concat([xom, takror], ignore_index=True), len(yoqolgan)


def siljit(M, k):
    out = np.full(M.shape, np.nan)
    out[:, k:] = M[:, :-k]
    return out


def main() -> None:
    Y, A, kal, _ = malumot(0)
    S, T = Y.shape
    xom, n_yoq = xom_jadval(Y, kal, np.random.default_rng(1))

    print("=== 1. Xom ma'lumot ===")
    print(f"  qatorlar: {len(DOKONLAR)} do'kon x {len(GURUHLAR)} guruh = {S}, "
          f"kunlar {T} ({kal['sana'][0].date()} - {kal['sana'][-1].date()})")
    print(f"  kutilgan yozuvlar {S * T:,}, xom eksportda {len(xom):,}")
    print(f"  aksiya kunlari ulushi {A.mean():.3f}, bayram kunlari "
          f"{int(kal['bayram'].sum())}, arafa kunlari {int(kal['arafa'].sum())}")

    print("\n=== 2. Validatsiya: takror, manfiy, yo'q kunlar ===")
    takror = int(xom.duplicated(["qator", "kun"]).sum())
    xom = xom.drop_duplicates(["qator", "kun"])
    manfiy = int((xom.savdo < 0).sum())
    xom.loc[xom.savdo < 0, "savdo"] = np.nan
    W = (xom.pivot(index="qator", columns="kun", values="savdo")
         .reindex(index=range(S), columns=range(T)).to_numpy())
    tur = (xom.pivot(index="qator", columns="kun", values="tur")
           .reindex(index=range(S), columns=range(T)).to_numpy())
    yoq = np.isnan(W) & pd.isna(tur)
    print(f"  takror yozuvlar: {takror} -> olib tashlandi")
    print(f"  manfiy savdo: {manfiy} -> NaN")
    print(f"  yo'q kunlar (panjarada NaN): {int(yoq.sum())} "
          f"(yashirilgan: {n_yoq}), ulushi {yoq.mean():.4f}")
    eng = np.argsort(-yoq.sum(1), kind="stable")[:3]
    print(f"  eng ko'p yo'q: " + ", ".join(
        f"{DOKONLAR[s // 3]}/{GURUHLAR[s % 3]} {int(yoq[s].sum())}" for s in eng))

    print("\n=== 3. Anomaliyalar: robust qoldiq (o'tgan 4 haftaning shu kuni) ===")
    L = np.log1p(W)
    with warnings.catch_warnings():          # birinchi 4 hafta: hamma qiymat NaN
        warnings.simplefilter("ignore", RuntimeWarning)
        baza = np.nanmedian(np.stack([siljit(L, 7 * k) for k in range(1, 5)]),
                            axis=0)
    r = L - baza
    med = np.nanmedian(r, axis=1, keepdims=True)
    mad = np.nanmedian(np.abs(r - med), axis=1, keepdims=True)
    z = (r - med) / (1.4826 * mad)
    haqiqiy = np.isin(tur, ["birlik", "kassa_nol"])
    kutilgan = (kal["bayram"] | kal["arafa"])[None, :] | A | (tur == "yopilish")
    baholanadi = ~np.isnan(z)
    print(f"  haqiqiy anomaliyalar: {int(haqiqiy.sum())} "
          f"(birlik {int((tur == 'birlik').sum())}, "
          f"kassa noli {int((tur == 'kassa_nol').sum())})")
    print(f"  {'chegara':>7} {'rejim':<10} {'belgilandi':>10} {'aniqlik':>8} "
          f"{'topildi':>8} {'kutilgan hodisa':>16}")
    for chegara in [3.0, 4.0, 5.0]:
        for rejim in ["taqvimsiz", "taqvimli"]:
            b = baholanadi & (np.abs(np.nan_to_num(z)) > chegara)
            if rejim == "taqvimli":
                b &= ~kutilgan
            tp = int((b & haqiqiy).sum())
            print(f"  {chegara:>7.1f} {rejim:<10} {int(b.sum()):>10} "
                  f"{tp / max(int(b.sum()), 1):>8.3f} "
                  f"{tp / int(haqiqiy.sum()):>8.3f} {int((b & kutilgan).sum()):>16}")
    bayroq = baholanadi & (np.abs(np.nan_to_num(z)) > 4.0) & ~kutilgan
    for nom in ["birlik", "kassa_nol"]:
        m = tur == nom
        print(f"  chegara 4, taqvimli: {nom} topildi {int((bayroq & m).sum())}"
              f"/{int(m.sum())}")
    otdi = haqiqiy & ~bayroq
    print(f"  o'tkazib yuborilgan {int(otdi.sum())} tadan kutilgan hodisa kunida: "
          f"{int((otdi & kutilgan).sum())}, ularning |z| minimumi "
          f"{np.abs(z[otdi & kutilgan]).min():.1f}")

    print("\n=== 4. To'ldirish siyosati va uning xatosi ===")
    tuzat = yoq | bayroq | np.isnan(W) & ~yoq
    medianasi = np.expm1(baza)
    ok = tuzat & ~np.isnan(medianasi)
    for nom, qiymat in [("0 bilan", np.zeros_like(W)),
                        ("hafta kuni medianasi", medianasi)]:
        wape = np.abs(qiymat[ok] - Y[ok]).sum() / Y[ok].sum()
        print(f"  {nom:<22} to'ldirilgan {int(ok.sum())} katak, WAPE "
              f"(haqiqiy qiymatga nisbatan) {wape:.3f}")
    toza = np.where(ok, medianasi, W)
    yopiq = tur == "yopilish"
    print(f"  yopilish kunlari: {int(yopiq.sum())} katak -> 0 qoladi, "
          f"o'quvdan chiqariladi")
    qoldi = int(np.isnan(toza).sum())
    print(f"  tozalangan panjarada qolgan NaN: {qoldi} (o'tgan 4 haftada "
          f"qiymat yo'q - baza hisoblanmadi)")
    xato_ulush = float(np.abs(np.nan_to_num(toza) - Y)[~yopiq].sum()
                       / Y[~yopiq].sum())
    print(f"  tozalangan va haqiqiy ma'lumot farqi (WAPE): {xato_ulush:.4f}")
    print("  ⭐ Kutilgan hodisa - anomaliya emas; to'ldirilgan qiymat - belgilanadi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Xom ma'lumot ===
  qatorlar: 8 do'kon x 3 guruh = 24, kunlar 1096 (2023-01-01 - 2025-12-31)
  kutilgan yozuvlar 26,304, xom eksportda 25,950
  aksiya kunlari ulushi 0.061, bayram kunlari 15, arafa kunlari 30

=== 2. Validatsiya: takror, manfiy, yo'q kunlar ===
  takror yozuvlar: 40 -> olib tashlandi
  manfiy savdo: 20 -> NaN
  yo'q kunlar (panjarada NaN): 394 (yashirilgan: 394), ulushi 0.0150
  eng ko'p yo'q: Toshkent-Yunusobod/non 25, Namangan-Markaz/non 22, Toshkent-Chilonzor/non 20

=== 3. Anomaliyalar: robust qoldiq (o'tgan 4 haftaning shu kuni) ===
  haqiqiy anomaliyalar: 66 (birlik 30, kassa noli 36)
  chegara rejim      belgilandi  aniqlik  topildi  kutilgan hodisa
      3.0 taqvimsiz         371    0.178    1.000              255
      3.0 taqvimli          116    0.491    0.864                0
      4.0 taqvimsiz         148    0.439    0.985               88
      4.0 taqvimli           60    0.933    0.848                0
      5.0 taqvimsiz          96    0.667    0.970               41
      5.0 taqvimli           55    1.000    0.833                0
  chegara 4, taqvimli: birlik topildi 23/30
  chegara 4, taqvimli: kassa_nol topildi 33/36
  o'tkazib yuborilgan 10 tadan kutilgan hodisa kunida: 9, ularning |z| minimumi 5.4

=== 4. To'ldirish siyosati va uning xatosi ===
  0 bilan                to'ldirilgan 473 katak, WAPE (haqiqiy qiymatga nisbatan) 1.000
  hafta kuni medianasi   to'ldirilgan 473 katak, WAPE (haqiqiy qiymatga nisbatan) 0.260
  yopilish kunlari: 21 katak -> 0 qoladi, o'quvdan chiqariladi
  tozalangan panjarada qolgan NaN: 1 (o'tgan 4 haftada qiymat yo'q - baza hisoblanmadi)
  tozalangan va haqiqiy ma'lumot farqi (WAPE): 0.0070
  ⭐ Kutilgan hodisa - anomaliya emas; to'ldirilgan qiymat - belgilanadi

Natija tahlili.

1-bo'lim — 24 qator (8 do'kon x 3 guruh), 1 096 kun: kutilgan 26,304 yozuv, xom eksportda 25,950. Aksiya kunlari 6.1%, sintetik taqvimda 15 bayram va 30 arafa kuni.

2-bo'lim — validatsiya. 40 ta takror yozuv (bir kun ikki marta eksport qilingan) olib tashlandi, 20 ta manfiy qiymat (qaytarish yozuvi savdo ustuniga tushgan) NaN qilindi. To'liq panjara 394 ta yo'q kunni ko'rsatdi (1.5%) — aynan biz yashirgan son. Bu kunlar NaN bo'lib qoladi: 0 emas, chunki savdo bo'lgan, faqat yozilmagan.

3-bo'lim — anomaliyalar. 66 ta haqiqiy anomaliya: 30 ta birlik xatosi (qiymat 10 barobar) va 36 ta kassa noli (12 do'kon-kun x 3 guruh). Taqvimsiz rejimda chegara 3 da 371 ta belgi, ulardan 255 tasi bayram, arafa, aksiya yoki ta'mir kunlari — aniqlik atigi 0.178: ma'lumot egasi har kuni o'nlab "anomaliya" oladi va ularning ko'pi bayram. Taqvimli rejim (kutilgan hodisalar ro'yxatdan chiqariladi) chegara 4 da 60 ta belgi beradi, aniqlik 0.933, topildi 0.848. Chegara 5 da aniqlik 1.000, lekin topilgani 0.833 ga tushadi — chegara aniqlik va to'liqlik orasidagi murosa 28.6-bob. Diqqat: o'tkazib yuborilgan 10 ta anomaliyadan 9 tasi kutilgan hodisa kunida, ularning |z| si kamida 5.4 — ya'ni aniq anomaliya, faqat biz bu kunlarni butunlay tekshiruvdan chiqarib yubordik. Yaxshiroq qoida — kutilgan kunlarda chegarani kengaytirish yoki kutilgan ta'sirni bazaga qo'shish (Vazifa 3).

4-bo'lim — to'ldirish. 473 ta katak (yo'q kunlar, manfiylar, belgilangan anomaliyalar) to'ldirildi. 0 bilan to'ldirish ularda haqiqiy qiymatdan 100% farq qiladi (WAPE 1.000), o'tgan 4 haftaning shu kuni medianasi — 0.260. Mukammal emas (to'ldirilgan kunlarning bir qismi aksiya kuni), lekin model uchun "talab nolga tushdi" degan yolg'on signal yo'q. Ta'mir kunlari (21 katak) 0 qoladi — do'kon haqiqatan yopiq edi — lekin o'quvdan chiqariladi. Tozalangan panjara haqiqiy ma'lumotdan umumiy hisobda 0.7% farq qiladi. Keyingi misollar tozalangan ma'lumot bilan ishlaydi (soddalik uchun generatorning toza chiqishi olinadi).

Misol 2 — Seasonal naive, ETS va global HistGB: rolling-origin backtest

python
"""2-qadam: rolling-origin backtest - seasonal naive, ETS va global HistGB."""

import warnings

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
from statsmodels.tsa.holtwinters import ExponentialSmoothing

DOKONLAR = ["Toshkent-Chilonzor", "Toshkent-Yunusobod", "Toshkent-Sergeli",
            "Samarqand-Markaz", "Samarqand-Siyob", "Buxoro-Markaz",
            "Namangan-Markaz", "Andijon-Markaz"]
GURUHLAR = ["non", "sut", "meva"]
# sintetik taqvim: "bayram kunlari" (sanalar o'quv maqsadida to'qilgan)
BAYRAMLAR = ["2023-01-01", "2023-03-21", "2023-04-21", "2023-06-28",
             "2023-09-01", "2024-01-01", "2024-03-21", "2024-04-10",
             "2024-06-16", "2024-09-01", "2025-01-01", "2025-03-21",
             "2025-03-30", "2025-06-06", "2025-09-01"]
HAFTALIK = {"non": [1.0, 0.95, 0.95, 1.0, 1.05, 1.2, 1.1],
            "sut": [1.0, 1.0, 0.95, 1.0, 1.05, 1.15, 1.1],
            "meva": [0.9, 0.9, 0.9, 0.95, 1.05, 1.25, 1.3]}
YILLIK = {"non": (0.05, 15), "sut": (0.08, 30), "meva": (0.35, 200)}
ASOS = {"non": 120, "sut": 80, "meva": 60}
H = 28


def malumot(seed=0):
    """Toza kunlik savdo: Y (24 qator x kun), aksiya A, taqvim, kutilgan mu."""
    rng = np.random.default_rng(seed)
    sana = pd.date_range("2023-01-01", "2025-12-31", freq="D")
    t = np.arange(len(sana))
    dow, doy = sana.dayofweek.to_numpy(), sana.dayofyear.to_numpy()
    bayram = sana.isin(pd.to_datetime(BAYRAMLAR))
    arafa = (np.roll(bayram, -1) | np.roll(bayram, -2)) & ~bayram
    Y, A, MU = [], [], []
    for _ in DOKONLAR:
        olcham, osish = rng.uniform(0.6, 1.6), rng.uniform(0.03, 0.15)
        for g in GURUHLAR:
            amp, choqqi = YILLIK[g]
            mu = (ASOS[g] * olcham * (1 + osish) ** (t / 365.25)
                  * np.array(HAFTALIK[g])[dow]
                  * (1 + amp * np.cos(2 * np.pi * (doy - choqqi) / 365.25))
                  * np.where(bayram, 0.4, 1.0) * np.where(arafa, 1.8, 1.0))
            nisbiy = mu / pd.Series(mu).rolling(29, center=True,
                                                min_periods=1).mean().to_numpy()
            p = np.clip(0.05 * nisbiy ** -6.0, 0, 0.35) * ~bayram * ~arafa
            aksiya = rng.random(len(t)) < p               # sust kunlarda ko'proq
            Y.append(rng.poisson(mu * np.where(aksiya, 1.25, 1.0)
                                 * rng.gamma(20, 1 / 20, len(t))))
            A.append(aksiya)
            MU.append(mu)
    kal = {"sana": sana, "dow": dow, "doy": doy, "bayram": bayram,
           "arafa": arafa}
    return np.array(Y, dtype=float), np.array(A), kal, np.array(MU)


def jadval(Y, A, kal, originlar):
    """Direct strategiya belgilari: faqat origin gacha ma'lum yoki rejadagi."""
    S = Y.shape[0]
    s, o, h = (a.ravel() for a in np.meshgrid(np.arange(S), originlar,
                                              np.arange(1, H + 1), indexing="ij"))
    t = o + h
    cs = np.concatenate([np.zeros((S, 1)), np.cumsum(Y, axis=1)], axis=1)

    def orta(a, b):                                   # Y[s, a..b] o'rtachasi
        return (cs[s, b + 1] - cs[s, a]) / (b - a + 1)

    d28 = np.log1p(orta(o - 27, o))
    X = np.column_stack([
        s // 3, s % 3, h, kal["dow"][t],
        np.sin(2 * np.pi * kal["doy"][t] / 365.25),
        np.cos(2 * np.pi * kal["doy"][t] / 365.25),
        kal["bayram"][t], kal["arafa"][t], A[s, t],
        np.log1p(orta(o - 6, o)) - d28,
        np.log1p(Y[s, t - 7 * np.ceil(h / 7).astype(int)]) - d28,
        np.log1p(orta(t - 367, t - 361)) - np.log1p(orta(o - 391, o - 364)),
    ])
    return X, s, t, d28


def hgb(Y, A, kal, O, **kw):
    """O gacha bo'lgan ma'lumotda o'qitish: maqsad kunlari O dan oshmaydi."""
    originlar = np.arange(O - H, 399, -14)[::-1]
    X, s, t, d28 = jadval(Y, A, kal, originlar)
    param = dict(max_iter=120, learning_rate=0.1, max_leaf_nodes=15,
                 min_samples_leaf=40, early_stopping=False,
                 categorical_features=[0, 1], random_state=0)
    param.update(kw)
    return HistGradientBoostingRegressor(**param).fit(
        X, np.log1p(Y[s, t]) - d28), len(X)


def ets(Y, kal, O):
    f = np.empty((Y.shape[0], H))
    indeks = pd.date_range(kal["sana"][O - 181], periods=182, freq="D")
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        for s in range(Y.shape[0]):
            qator = pd.Series(np.log1p(Y[s, O - 181:O + 1]), index=indeks)
            fit = ExponentialSmoothing(qator, trend=None, seasonal="add",
                                       seasonal_periods=7).fit(use_brute=False)
            f[s] = np.expm1(fit.forecast(H).to_numpy())
    return f.clip(0)


def main() -> None:
    Y, A, kal, _ = malumot(0)
    S, T = Y.shape
    O_test = T - 1 - H
    oynalar = [O_test - H * k for k in range(4, 0, -1)]
    modellar = ["seasonal naive", "ETS", "HistGB"]

    print("=== 1. Backtest dizayni ===")
    print(f"  {S} qator, {T} kun; test (tegilmaydi): "
          f"{kal['sana'][O_test + 1].date()} - {kal['sana'][-1].date()}")
    print(f"  4 oyna originlari: {kal['sana'][oynalar[0]].date()} - "
          f"{kal['sana'][oynalar[-1]].date()} (har 28 kun, hafta kuni "
          f"{int(kal['dow'][oynalar[0]])}: 0 - dushanba)")

    yozuv = []                   # (model, oyna, qator, t, y, f)
    for w, O in enumerate(oynalar):
        m, n_orq = hgb(Y, A, kal, O)
        X, s, t, d28 = jadval(Y, A, kal, np.array([O]))
        f_h = np.expm1(m.predict(X) + d28).clip(0)
        h = t - O
        f_sn = Y[s, t - 7 * np.ceil(h / 7).astype(int)]
        f_e = ets(Y, kal, O)[s, h - 1]
        for nom, f in zip(modellar, [f_sn, f_e, f_h]):
            yozuv.append(pd.DataFrame({"model": nom, "oyna": w, "qator": s,
                                       "t": t, "y": Y[s, t], "f": f}))
        if w == 0:
            print(f"  HistGB o'quv misollari (1-oyna): {n_orq:,}")
    df = pd.concat(yozuv, ignore_index=True)
    df["dokon"] = df.qator // 3
    df["xato"] = (df.y - df.f).abs()
    df["hafta"] = (df.t - df.oyna.map(dict(enumerate(oynalar))) - 1) // 7 + 1
    df["kun_turi"] = np.select(
        [kal["bayram"][df.t] | kal["arafa"][df.t], A[df.qator, df.t]],
        ["bayram/arafa", "aksiya"], "oddiy")

    birlik = (df.groupby(["model", "dokon", "oyna"])[["xato", "y"]].sum()
              .assign(wape=lambda d: d.xato / d.y)["wape"].unstack("model"))
    print(f"\n=== 2. WAPE: {len(birlik)} ta do'kon x oyna birligi ===")
    print(f"  {'model':<15} {'o_rtacha':>9} {'std':>7}   " + "  ".join(
        f"{k:>12}" for k in ["oddiy", "bayram/arafa", "aksiya"]))
    for nom in modellar:
        q = df[df.model == nom]
        turlar = [q[q.kun_turi == k] for k in ["oddiy", "bayram/arafa",
                                              "aksiya"]]
        print(f"  {nom:<15} {birlik[nom].mean():>9.4f} {birlik[nom].std():>7.4f}"
              f"   " + "  ".join(f"{x.xato.sum() / x.y.sum():>12.4f}"
                                 for x in turlar))

    print("\n=== 3. Juftlashgan taqqoslash va qaror ===")
    eng = birlik.mean().idxmin()
    print(f"  eng yaxshi (o'rtacha WAPE): {eng}")
    tanlov = None
    for nom in modellar:                                    # soddalik tartibi
        if nom == eng:
            yomon = False
            print(f"  {nom:<15} - eng yaxshi")
        else:
            d = birlik[nom] - birlik[eng]
            se = d.std(ddof=1) / np.sqrt(len(d))
            yomon = d.mean() > 2 * se
            print(f"  {nom:<15} - {eng}: {d.mean():+.4f} (SE {se:.4f}), "
                  f"sezilarli yomon: {yomon}")
        if tanlov is None and not yomon:
            tanlov = nom
    print(f"  QOIDA -> tanlov: {tanlov}")
    yutgan = (birlik["HistGB"] < birlik["seasonal naive"]).mean()
    print(f"  HistGB seasonal naive dan yaxshi bo'lgan birliklar: {yutgan:.2f}")

    print("\n=== 4. Ufq bo'yicha WAPE (hafta) ===")
    print(f"  {'model':<15} " + " ".join(f"{f'{k}-hafta':>9}" for k in range(1, 5)))
    for nom in modellar:
        q = df[df.model == nom].groupby("hafta")[["xato", "y"]].sum()
        print(f"  {nom:<15} " + " ".join(f"{v:>9.4f}"
                                         for v in (q.xato / q.y).to_numpy()))
    print("  ⭐ Qaror - juftlashgan farq va oldindan yozilgan qoida bilan")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Backtest dizayni ===
  24 qator, 1096 kun; test (tegilmaydi): 2025-12-04 - 2025-12-31
  4 oyna originlari: 2025-08-13 - 2025-11-05 (har 28 kun, hafta kuni 2: 0 - dushanba)
  HistGB o'quv misollari (1-oyna): 25,536

=== 2. WAPE: 32 ta do'kon x oyna birligi ===
  model            o_rtacha     std          oddiy  bayram/arafa        aksiya
  seasonal naive     0.2905  0.0373         0.2804        0.5391        0.2934
  ETS                0.2272  0.0288         0.2074        0.5409        0.2645
  HistGB             0.1994  0.0216         0.1973        0.1819        0.1822

=== 3. Juftlashgan taqqoslash va qaror ===
  eng yaxshi (o'rtacha WAPE): HistGB
  seasonal naive  - HistGB: +0.0911 (SE 0.0071), sezilarli yomon: True
  ETS             - HistGB: +0.0278 (SE 0.0046), sezilarli yomon: True
  HistGB          - eng yaxshi
  QOIDA -> tanlov: HistGB
  HistGB seasonal naive dan yaxshi bo'lgan birliklar: 1.00

=== 4. Ufq bo'yicha WAPE (hafta) ===
  model             1-hafta   2-hafta   3-hafta   4-hafta
  seasonal naive     0.2796    0.2780    0.3086    0.2977
  ETS                0.2104    0.2096    0.2615    0.2131
  HistGB             0.1990    0.1943    0.1947    0.1945
  ⭐ Qaror - juftlashgan farq va oldindan yozilgan qoida bilan

Natija tahlili.

1-bo'lim — test davri (2025-12-04 - 2025-12-31) umuman ochilmadi. To'rtta backtest oynasining originlari — 2025-08-13 dan 2025-11-05 gacha, har 28 kunda, bir xil hafta kuni (chorshanba). HistGB birinchi oynada 25,536 ta o'quv misolida o'qitildi — hammasining maqsad kuni origin dan oldin.

2-bo'lim — 32 ta do'kon x oyna birligida o'rtacha WAPE: seasonal naive 0.2905, ETS 0.2272, HistGB 0.1994. Kun turlari bo'yicha farq ayniqsa yorqin: bayram va arafa kunlarida seasonal naive va ETS xatosi 0.54 — ular o'tgan haftani yoki haftalik mavsumni takrorlaydi va bayramni "ko'rmaydi"; HistGB esa taqvim belgisi tufayli 0.1819 — oddiy kunlardagidan ham past. Aksiya kunlarida ham xuddi shunday: 0.29 va 0.26 ga qarshi 0.18. Bu — 1-bo'limdagi "real vaziyat"ning birinchi saboqi: aksiya va bayram taqvimi modelga belgi sifatida berilishi kerak.

3-bo'lim — qaror. Eng yaxshi — HistGB. Seasonal naive undan +0.0911 (SE 0.0071), ETS +0.0278 (SE 0.0046) ga yomon — ikkalasi ham sezilarli. Qoida soddalik tartibida birinchi "sezilarli yomon bo'lmagan" modelni oladi — bu HistGB. U 32 birlikning hammasida seasonal naive dan yaxshi.

4-bo'lim — ufq. HistGB xatosi 4 hafta davomida deyarli o'zgarmaydi (0.199 → 0.195): uning belgilarining ko'pi (taqvim, daraja, o'tgan yil shakli) ufqqa bog'liq emas. ETS 3-haftada 0.2615 ga sakraydi — bu oynalarning biriga tushgan 1-sentyabr bayrami. Umuman olganda bu ma'lumotda ufq xatosi kuchli o'smaydi, chunki shovqin katta va trend sekin; 28.1 dagi qoida — ufq bo'yicha xatoni alohida ko'rsatish — baribir amal qiladi: ombor 4-hafta buyurtmasini 1-hafta aniqligi bilan rejalashtirmasligi kerak.

Misol 3 — Bashorat intervallari va zaxira qarori

python
"""3-qadam: konformal va kvantil intervallar, qamrov, buyurtma kvantilini tanlash."""

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor

DOKONLAR = ["Toshkent-Chilonzor", "Toshkent-Yunusobod", "Toshkent-Sergeli",
            "Samarqand-Markaz", "Samarqand-Siyob", "Buxoro-Markaz",
            "Namangan-Markaz", "Andijon-Markaz"]
GURUHLAR = ["non", "sut", "meva"]
# sintetik taqvim: "bayram kunlari" (sanalar o'quv maqsadida to'qilgan)
BAYRAMLAR = ["2023-01-01", "2023-03-21", "2023-04-21", "2023-06-28",
             "2023-09-01", "2024-01-01", "2024-03-21", "2024-04-10",
             "2024-06-16", "2024-09-01", "2025-01-01", "2025-03-21",
             "2025-03-30", "2025-06-06", "2025-09-01"]
HAFTALIK = {"non": [1.0, 0.95, 0.95, 1.0, 1.05, 1.2, 1.1],
            "sut": [1.0, 1.0, 0.95, 1.0, 1.05, 1.15, 1.1],
            "meva": [0.9, 0.9, 0.9, 0.95, 1.05, 1.25, 1.3]}
YILLIK = {"non": (0.05, 15), "sut": (0.08, 30), "meva": (0.35, 200)}
ASOS = {"non": 120, "sut": 80, "meva": 60}
H = 28
C_U, C_O = 3.0, 1.0            # faraziy: yetishmaslik va ortiqcha narxi (1 dona)


def malumot(seed=0):
    """Toza kunlik savdo: Y (24 qator x kun), aksiya A, taqvim, kutilgan mu."""
    rng = np.random.default_rng(seed)
    sana = pd.date_range("2023-01-01", "2025-12-31", freq="D")
    t = np.arange(len(sana))
    dow, doy = sana.dayofweek.to_numpy(), sana.dayofyear.to_numpy()
    bayram = sana.isin(pd.to_datetime(BAYRAMLAR))
    arafa = (np.roll(bayram, -1) | np.roll(bayram, -2)) & ~bayram
    Y, A, MU = [], [], []
    for _ in DOKONLAR:
        olcham, osish = rng.uniform(0.6, 1.6), rng.uniform(0.03, 0.15)
        for g in GURUHLAR:
            amp, choqqi = YILLIK[g]
            mu = (ASOS[g] * olcham * (1 + osish) ** (t / 365.25)
                  * np.array(HAFTALIK[g])[dow]
                  * (1 + amp * np.cos(2 * np.pi * (doy - choqqi) / 365.25))
                  * np.where(bayram, 0.4, 1.0) * np.where(arafa, 1.8, 1.0))
            nisbiy = mu / pd.Series(mu).rolling(29, center=True,
                                                min_periods=1).mean().to_numpy()
            p = np.clip(0.05 * nisbiy ** -6.0, 0, 0.35) * ~bayram * ~arafa
            aksiya = rng.random(len(t)) < p               # sust kunlarda ko'proq
            Y.append(rng.poisson(mu * np.where(aksiya, 1.25, 1.0)
                                 * rng.gamma(20, 1 / 20, len(t))))
            A.append(aksiya)
            MU.append(mu)
    kal = {"sana": sana, "dow": dow, "doy": doy, "bayram": bayram,
           "arafa": arafa}
    return np.array(Y, dtype=float), np.array(A), kal, np.array(MU)


def jadval(Y, A, kal, originlar):
    """Direct strategiya belgilari: faqat origin gacha ma'lum yoki rejadagi."""
    S = Y.shape[0]
    s, o, h = (a.ravel() for a in np.meshgrid(np.arange(S), originlar,
                                              np.arange(1, H + 1), indexing="ij"))
    t = o + h
    cs = np.concatenate([np.zeros((S, 1)), np.cumsum(Y, axis=1)], axis=1)

    def orta(a, b):                                   # Y[s, a..b] o'rtachasi
        return (cs[s, b + 1] - cs[s, a]) / (b - a + 1)

    d28 = np.log1p(orta(o - 27, o))
    X = np.column_stack([
        s // 3, s % 3, h, kal["dow"][t],
        np.sin(2 * np.pi * kal["doy"][t] / 365.25),
        np.cos(2 * np.pi * kal["doy"][t] / 365.25),
        kal["bayram"][t], kal["arafa"][t], A[s, t],
        np.log1p(orta(o - 6, o)) - d28,
        np.log1p(Y[s, t - 7 * np.ceil(h / 7).astype(int)]) - d28,
        np.log1p(orta(t - 367, t - 361)) - np.log1p(orta(o - 391, o - 364)),
    ])
    return X, s, t, d28


def hgb(Y, A, kal, O, **kw):
    """O gacha bo'lgan ma'lumotda o'qitish: maqsad kunlari O dan oshmaydi."""
    originlar = np.arange(O - H, 399, -14)[::-1]
    X, s, t, d28 = jadval(Y, A, kal, originlar)
    param = dict(max_iter=120, learning_rate=0.1, max_leaf_nodes=15,
                 min_samples_leaf=40, early_stopping=False,
                 categorical_features=[0, 1], random_state=0)
    param.update(kw)
    return HistGradientBoostingRegressor(**param).fit(
        X, np.log1p(Y[s, t]) - d28)


def narx(y, q):
    return C_U * np.maximum(y - q, 0) + C_O * np.maximum(q - y, 0)


def main() -> None:
    Y, A, kal, _ = malumot(0)
    S, T = Y.shape
    O_test = T - 1 - H
    oynalar = [O_test - H * k for k in range(4, 0, -1)]
    kalibr, baho = oynalar[:2], oynalar[2:]

    qoldiq = {k: [] for k in range(1, 5)}
    qatorlar = []
    n_bayram_kal = 0
    for O in oynalar:
        X, s, t, d28 = jadval(Y, A, kal, np.array([O]))
        f = np.expm1(hgb(Y, A, kal, O).predict(X) + d28).clip(0)
        hafta = (t - O - 1) // 7 + 1
        r = np.log1p(Y[s, t]) - np.log1p(f)
        if O in kalibr:
            for k in range(1, 5):
                qoldiq[k].extend(r[hafta == k].tolist())
            n_bayram_kal = n_bayram_kal + int(
                (kal["bayram"][t] | kal["arafa"][t]).sum())
            continue
        q = {a: np.expm1(hgb(Y, A, kal, O, loss="quantile", quantile=a)
                         .predict(X) + d28).clip(0) for a in (0.1, 0.9)}
        qatorlar.append(pd.DataFrame({
            "O": O, "dokon": s // 3, "t": t, "hafta": hafta, "y": Y[s, t],
            "f": f, "kq10": q[0.1], "kq90": q[0.9],
            "tur": np.select([kal["bayram"][t] | kal["arafa"][t], A[s, t]],
                             ["bayram/arafa", "aksiya"], "oddiy")}))
    df = pd.concat(qatorlar, ignore_index=True)
    kq = {k: np.quantile(qoldiq[k], [0.1, 0.9]) for k in qoldiq}
    lf = np.log1p(df.f)
    df["cq10"] = np.expm1(lf + df.hafta.map(lambda k: kq[k][0]))
    df["cq90"] = np.expm1(lf + df.hafta.map(lambda k: kq[k][1]))

    print("=== 1. Konformal kalibrovka (2 oyna qoldiqlari, log shkala) ===")
    for k in range(1, 5):
        print(f"  {k}-hafta: n={len(qoldiq[k])}, q0.1 {kq[k][0]:+.3f}, "
              f"q0.9 {kq[k][1]:+.3f}")

    print("\n=== 2. Nominal 80% interval: haqiqiy qamrov (2 baholash oynasi) ===")
    print(f"  {'usul':<10} {'umumiy':>7} {'oddiy':>7} {'aksiya':>7} "
          f"{'nisbiy kenglik':>15}")
    for nom, lo, hi in [("konformal", "cq10", "cq90"),
                        ("kvantil", "kq10", "kq90")]:
        ichida = (df.y >= df[lo]) & (df.y <= df[hi])
        kenglik = ((df[hi] - df[lo]) / df.f.clip(1)).mean()
        turlar = [ichida[df.tur == k].mean() for k in ["oddiy", "aksiya"]]
        print(f"  {nom:<10} {ichida.mean():>7.3f} {turlar[0]:>7.3f} "
              f"{turlar[1]:>7.3f} {kenglik:>15.3f}")
    n_tur = df.tur.value_counts()
    print(f"  baholash kunlari: oddiy {n_tur.get('oddiy', 0)}, aksiya "
          f"{n_tur.get('aksiya', 0)}, bayram/arafa {n_tur.get('bayram/arafa', 0)}"
          f"; kalibrovkada bayram/arafa qoldiqlari {n_bayram_kal}")
    for nom, lo, hi in [("konformal", "cq10", "cq90"), ("kvantil", "kq10", "kq90")]:
        haf = [((df.y >= df[lo]) & (df.y <= df[hi]))[df.hafta == k].mean()
               for k in range(1, 5)]
        print(f"  {nom:<10} hafta bo'yicha qamrov: "
              + " ".join(f"{v:.3f}" for v in haf))

    print(f"\n=== 3. Zaxira: c_u={C_U:.0f}, c_o={C_O:.0f} -> kritik nisbat "
          f"{C_U / (C_U + C_O):.2f} ===")
    print(f"  {'buyurtma kvantili':<18} {'o_rtacha narx':>13} {'y <= buyurtma':>14}")
    natija = {}
    for a in [0.5, 0.6, 0.75, 0.9]:
        qa = {k: np.quantile(qoldiq[k], a) for k in qoldiq}
        buyurtma = np.expm1(lf + df.hafta.map(qa))
        natija[a] = narx(df.y, buyurtma)
        print(f"  {a:<18} {natija[a].mean():>13.3f} "
              f"{(df.y <= buyurtma).mean():>14.3f}")
    natija["nuqta"] = narx(df.y, df.f)
    print(f"  {'nuqtali bashorat':<18} {natija['nuqta'].mean():>13.3f} "
          f"{(df.y <= df.f).mean():>14.3f}")
    eng = min((natija[a].mean(), a) for a in [0.5, 0.6, 0.75, 0.9])[1]
    print(f"  eng arzon kvantil: {eng}")

    print("\n=== 4. Juftlashgan: 0.75 kvantil - nuqtali bashorat (do'kon x oyna) ===")
    df["n75"], df["nq"] = natija[0.75], natija["nuqta"]
    birlik = df.groupby(["dokon", "O"])[["n75", "nq"]].sum()
    d = (birlik.n75 - birlik.nq) / birlik.nq
    se = d.std(ddof=1) / np.sqrt(len(d))
    print(f"  {len(d)} birlik: nisbiy narx o'zgarishi {d.mean():+.3f} "
          f"(SE {se:.3f}), sezilarli: {abs(d.mean()) > 2 * se}")
    print(f"  0.75 kvantil arzonroq bo'lgan birliklar: {(d < 0).mean():.2f}")
    print("  ⭐ Buyurtma - nuqtali bashorat emas, narxlar nisbatidagi kvantil")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Konformal kalibrovka (2 oyna qoldiqlari, log shkala) ===
  1-hafta: n=336, q0.1 -0.323, q0.9 +0.324
  2-hafta: n=336, q0.1 -0.313, q0.9 +0.298
  3-hafta: n=336, q0.1 -0.353, q0.9 +0.316
  4-hafta: n=336, q0.1 -0.314, q0.9 +0.264

=== 2. Nominal 80% interval: haqiqiy qamrov (2 baholash oynasi) ===
  usul        umumiy   oddiy  aksiya  nisbiy kenglik
  konformal    0.749   0.749   0.747           0.635
  kvantil      0.745   0.746   0.723           0.650
  baholash kunlari: oddiy 1261, aksiya 83, bayram/arafa 0; kalibrovkada bayram/arafa qoldiqlari 72
  konformal  hafta bo'yicha qamrov: 0.762 0.738 0.801 0.693
  kvantil    hafta bo'yicha qamrov: 0.741 0.744 0.774 0.720

=== 3. Zaxira: c_u=3, c_o=1 -> kritik nisbat 0.75 ===
  buyurtma kvantili  o_rtacha narx  y <= buyurtma
  0.5                       58.522          0.493
  0.6                       50.661          0.620
  0.75                      47.707          0.751
  0.9                       54.706          0.876
  nuqtali bashorat          59.625          0.486
  eng arzon kvantil: 0.75

=== 4. Juftlashgan: 0.75 kvantil - nuqtali bashorat (do'kon x oyna) ===
  16 birlik: nisbiy narx o'zgarishi -0.200 (SE 0.022), sezilarli: True
  0.75 kvantil arzonroq bo'lgan birliklar: 0.94
  ⭐ Buyurtma - nuqtali bashorat emas, narxlar nisbatidagi kvantil

Natija tahlili.

1-bo'lim — ikki kalibrovka oynasining qoldiqlari (log shkalada, har ufq haftasi uchun 336 ta): q0.1 ~ -0.32, q0.9 ~ +0.30. Ya'ni 80% interval taxminan bashoratning 0.73 dan 1.35 barobarigacha — kunlik savdo shovqini katta. Ufq bo'yicha qoldiqlar deyarli bir xil, 2-misoldagi tekis ufq xatosiga mos.

2-bo'lim — halol natija: nominal 80% interval haqiqatda 74.9% (konformal) va 74.5% (kvantil HistGB) qamradi — ikkalasi ham biroz tor. Sabab — almashinuvchanlik farazi to'liq bajarilmaydi: kalibrovka oynalari avgust-oktabr, baholash oynalari oktabr-dekabr; mavsum va daraja o'zgargan, kalibrovka esa atigi 2 oyna. Hafta bo'yicha qamrov 0.69 dan 0.80 gacha tebranadi. Aksiya kunlarida konformal qamrov o'zgarmadi (0.747), kvantil model esa pastroq (0.723). Yana bir cheklov ochiq ko'rinadi: baholash oynalarida bitta ham bayram yoki arafa kuni yo'q, kalibrovkada esa atigi 72 ta bayram qoldig'i — bayram kunlari uchun interval sifatini bu backtest tekshira olmaydi. Amaliy xulosa: intervallar monitoringda kuzatiladi (qamrov 4 haftada 0.70 dan past bo'lsa — qayta kalibrlash), kalibrovka uchun ko'proq oyna ishlatiladi, va 80% o'rniga 85% nominal bilan boshlash mumkin.

3-bo'lim — zaxira qarori. Yetishmaslik narxi ortiqchadan 3 barobar (c_u = 3, c_o = 1) — kritik nisbat 0.75. Buyurtmani turli kvantillarda berib, haqiqiy narxni hisobladik: mediana (0.5) — 58.52, 0.6 — 50.66, 0.75 — 47.71 (eng arzon), 0.9 — 54.71. Nuqtali bashorat (59.63) mediana atrofida — u ham noto'g'ri kvantil. 0.75 kvantilda talab kunlarning 75.1% ida qoplandi — nazariya aytganidek. Nazariya va o'lchov bir nuqtada uchrashdi.

4-bo'lim — juftlashgan: 16 ta do'kon x oyna birligida 0.75 kvantil nuqtali bashoratga nisbatan narxni o'rtacha 20.0% ga kamaytirdi (SE 0.022, sezilarli), birliklarning 94% ida arzonroq. Bu — "real vaziyat"ning ikkinchi saboqi: omborga nuqtali bashorat emas, narxlar nisbatidagi kvantil yuboriladi. Haqiqiy loyihada c_u va c_o har guruh uchun alohida (non tez buziladi — c_o katta; ichimlik buzilmaydi — c_o kichik) va ular moliya bo'limi bilan kelishiladi.

Misol 4 — Test davri, aksiya ta'siri, model paketi va monitoring

python
"""4-qadam: test bir marta, aksiya ta'siri (sababiy), paket va monitoring rejasi."""

import tempfile
from pathlib import Path

import joblib
import numpy as np
import pandas as pd
import statsmodels.api as sm
from sklearn.ensemble import HistGradientBoostingRegressor

DOKONLAR = ["Toshkent-Chilonzor", "Toshkent-Yunusobod", "Toshkent-Sergeli",
            "Samarqand-Markaz", "Samarqand-Siyob", "Buxoro-Markaz",
            "Namangan-Markaz", "Andijon-Markaz"]
GURUHLAR = ["non", "sut", "meva"]
# sintetik taqvim: "bayram kunlari" (sanalar o'quv maqsadida to'qilgan)
BAYRAMLAR = ["2023-01-01", "2023-03-21", "2023-04-21", "2023-06-28",
             "2023-09-01", "2024-01-01", "2024-03-21", "2024-04-10",
             "2024-06-16", "2024-09-01", "2025-01-01", "2025-03-21",
             "2025-03-30", "2025-06-06", "2025-09-01"]
HAFTALIK = {"non": [1.0, 0.95, 0.95, 1.0, 1.05, 1.2, 1.1],
            "sut": [1.0, 1.0, 0.95, 1.0, 1.05, 1.15, 1.1],
            "meva": [0.9, 0.9, 0.9, 0.95, 1.05, 1.25, 1.3]}
YILLIK = {"non": (0.05, 15), "sut": (0.08, 30), "meva": (0.35, 200)}
ASOS = {"non": 120, "sut": 80, "meva": 60}
H = 28
BELGILAR = ["dokon", "guruh", "h", "hafta_kuni", "yil_sin", "yil_cos",
            "bayram", "arafa", "aksiya", "daraja7_28", "sn_nisbiy", "otgan_yil"]


def malumot(seed=0):
    """Toza kunlik savdo: Y (24 qator x kun), aksiya A, taqvim, kutilgan mu."""
    rng = np.random.default_rng(seed)
    sana = pd.date_range("2023-01-01", "2025-12-31", freq="D")
    t = np.arange(len(sana))
    dow, doy = sana.dayofweek.to_numpy(), sana.dayofyear.to_numpy()
    bayram = sana.isin(pd.to_datetime(BAYRAMLAR))
    arafa = (np.roll(bayram, -1) | np.roll(bayram, -2)) & ~bayram
    Y, A, MU = [], [], []
    for _ in DOKONLAR:
        olcham, osish = rng.uniform(0.6, 1.6), rng.uniform(0.03, 0.15)
        for g in GURUHLAR:
            amp, choqqi = YILLIK[g]
            mu = (ASOS[g] * olcham * (1 + osish) ** (t / 365.25)
                  * np.array(HAFTALIK[g])[dow]
                  * (1 + amp * np.cos(2 * np.pi * (doy - choqqi) / 365.25))
                  * np.where(bayram, 0.4, 1.0) * np.where(arafa, 1.8, 1.0))
            nisbiy = mu / pd.Series(mu).rolling(29, center=True,
                                                min_periods=1).mean().to_numpy()
            p = np.clip(0.05 * nisbiy ** -6.0, 0, 0.35) * ~bayram * ~arafa
            aksiya = rng.random(len(t)) < p               # sust kunlarda ko'proq
            Y.append(rng.poisson(mu * np.where(aksiya, 1.25, 1.0)
                                 * rng.gamma(20, 1 / 20, len(t))))
            A.append(aksiya)
            MU.append(mu)
    kal = {"sana": sana, "dow": dow, "doy": doy, "bayram": bayram,
           "arafa": arafa}
    return np.array(Y, dtype=float), np.array(A), kal, np.array(MU)


def jadval(Y, A, kal, originlar):
    """Direct strategiya belgilari: faqat origin gacha ma'lum yoki rejadagi."""
    S = Y.shape[0]
    s, o, h = (a.ravel() for a in np.meshgrid(np.arange(S), originlar,
                                              np.arange(1, H + 1), indexing="ij"))
    t = o + h
    cs = np.concatenate([np.zeros((S, 1)), np.cumsum(Y, axis=1)], axis=1)

    def orta(a, b):                                   # Y[s, a..b] o'rtachasi
        return (cs[s, b + 1] - cs[s, a]) / (b - a + 1)

    d28 = np.log1p(orta(o - 27, o))
    X = np.column_stack([
        s // 3, s % 3, h, kal["dow"][t],
        np.sin(2 * np.pi * kal["doy"][t] / 365.25),
        np.cos(2 * np.pi * kal["doy"][t] / 365.25),
        kal["bayram"][t], kal["arafa"][t], A[s, t],
        np.log1p(orta(o - 6, o)) - d28,
        np.log1p(Y[s, t - 7 * np.ceil(h / 7).astype(int)]) - d28,
        np.log1p(orta(t - 367, t - 361)) - np.log1p(orta(o - 391, o - 364)),
    ])
    return X, s, t, d28


def hgb(Y, A, kal, O, **kw):
    """O gacha bo'lgan ma'lumotda o'qitish: maqsad kunlari O dan oshmaydi."""
    originlar = np.arange(O - H, 399, -14)[::-1]
    X, s, t, d28 = jadval(Y, A, kal, originlar)
    param = dict(max_iter=120, learning_rate=0.1, max_leaf_nodes=15,
                 min_samples_leaf=40, early_stopping=False,
                 categorical_features=[0, 1], random_state=0)
    param.update(kw)
    return HistGradientBoostingRegressor(**param).fit(
        X, np.log1p(Y[s, t]) - d28)


def prognoz(m, Y, A, kal, O):
    X, s, t, d28 = jadval(Y, A, kal, np.array([O]))
    return np.expm1(m.predict(X) + d28).clip(0), s, t, X


def main() -> None:
    Y, A, kal, MU = malumot(0)
    S, T = Y.shape
    O_test = T - 1 - H

    print("=== 1. Yakuniy test: oxirgi 28 kun, BIR MARTA ===")
    model = hgb(Y, A, kal, O_test)
    f, s, t, X = prognoz(model, Y, A, kal, O_test)
    h = t - O_test
    sn = Y[s, t - 7 * np.ceil(h / 7).astype(int)]
    y = Y[s, t]
    dk = s // 3
    w_m = np.array([np.abs(y - f)[dk == d].sum() / y[dk == d].sum() for d in range(8)])
    w_s = np.array([np.abs(y - sn)[dk == d].sum() / y[dk == d].sum() for d in range(8)])
    d = w_m - w_s
    print(f"  WAPE: HistGB {np.abs(y - f).sum() / y.sum():.4f}, seasonal naive "
          f"{np.abs(y - sn).sum() / y.sum():.4f}")
    print(f"  do'konlar bo'yicha farq {d.mean():+.4f} (SE "
          f"{d.std(ddof=1) / np.sqrt(8):.4f}); HistGB yaxshi do'konlar "
          f"{int((d < 0).sum())}/8")
    print(f"  eng yomon do'kon: {DOKONLAR[int(np.argmax(w_m))]} "
          f"(WAPE {w_m.max():.4f})")

    print("\n=== 2. Aksiya ta'siri (o'quv davri; haqiqiy +25.0%) ===")
    davr = np.arange(400, O_test + 1)
    sq, tq = (a.ravel() for a in np.meshgrid(np.arange(S), davr, indexing="ij"))
    ak, yq = A[sq, tq], Y[sq, tq]
    bayramsiz = ~(kal["bayram"][tq] | kal["arafa"][tq])
    dow = kal["dow"][tq]
    print(f"  aksiya ulushi: dush-chor {ak[dow <= 2].mean():.3f}, "
          f"shan-yak {ak[dow >= 5].mean():.3f}")
    natija = {}
    naiv = [yq[(sq == i) & ak & bayramsiz].mean()
            / yq[(sq == i) & ~ak & bayramsiz].mean() - 1 for i in range(S)]
    natija["naiv farq"] = (np.mean(naiv), np.std(naiv, ddof=1) / np.sqrt(S))
    oy = kal["sana"][tq].month.to_numpy()
    g = sq % 3
    nazorat = ([(dow == k) & (g == j) for k in range(1, 7) for j in range(3)]
               + [(oy == k) & (g == j) for k in range(2, 13) for j in range(3)]
               + [sq == i for i in range(1, S)]
               + [tq / 365.25 * (sq // 3 == k) for k in range(8)])  # o'sish
    Xr = sm.add_constant(np.column_stack([ak, kal["bayram"][tq],
                                          kal["arafa"][tq]] + nazorat)
                         .astype(float))
    r = sm.OLS(np.log1p(yq), Xr).fit(cov_type="cluster",
                                     cov_kwds={"groups": sq})
    natija["regressiya"] = (np.expm1(r.params[1]), r.bse[1])
    Xb = np.column_stack([sq // 3, sq % 3, dow,
                          np.sin(2 * np.pi * kal["doy"][tq] / 365.25),
                          np.cos(2 * np.pi * kal["doy"][tq] / 365.25),
                          kal["bayram"][tq], kal["arafa"][tq], tq])
    baza = HistGradientBoostingRegressor(
        max_iter=200, learning_rate=0.1, max_leaf_nodes=31, min_samples_leaf=20,
        early_stopping=False, categorical_features=[0, 1], random_state=0,
    ).fit(Xb[~ak], np.log1p(yq[~ak]))
    kontr = np.log1p(yq[ak]) - baza.predict(Xb[ak])
    per = [kontr[sq[ak] == i].mean() for i in range(S)]
    natija["kontrfaktual model"] = (np.expm1(np.mean(per)),
                                    np.std(per, ddof=1) / np.sqrt(S))
    per = []
    for i in range(S):
        farqlar = []
        for tt in davr[A[i, davr] & ~(kal["bayram"] | kal["arafa"])[davr]]:
            nomzod = [tt + k for k in (-14, -7, 7, 14)
                      if 400 <= tt + k <= O_test and not A[i, tt + k]
                      and not (kal["bayram"] | kal["arafa"])[tt + k]]
            if nomzod:
                farqlar.append(np.log1p(Y[i, tt]) - np.log1p(Y[i, nomzod]).mean())
        per.append(np.mean(farqlar))
    natija["moslashtirish"] = (np.expm1(np.mean(per)),
                               np.std(per, ddof=1) / np.sqrt(S))
    for nom, (v, se) in natija.items():
        print(f"  {nom:<19} {v:+.3f} (SE {se:.3f})  2*SE ichida: "
              f"{abs(v - 0.25) <= 2 * se}")

    print("\n=== 3. Model paketi (joblib + metadata + nazorat namunalari) ===")
    paket = {
        "format": 1, "model": model, "belgilar": BELGILAR,
        "metadata": {"model": "HistGB global, direct, h=1..28",
                     "oqitish_davri": f"{kal['sana'][400].date()} - "
                                      f"{kal['sana'][O_test].date()}",
                     "qatorlar": S, "maqsad": "log1p(y) - daraja28",
                     "tanlov": "backtest, juftlashgan qoida (2-qadam)",
                     "zaxira_kvantili": 0.75},
        "nazorat": {"X": X[:50], "bashorat": model.predict(X[:50])},
    }
    with tempfile.TemporaryDirectory() as papka:
        yol = Path(papka) / "talab_modeli.joblib"
        joblib.dump(paket, yol)
        p2 = joblib.load(yol)
    mos = np.allclose(p2["model"].predict(p2["nazorat"]["X"]),
                      p2["nazorat"]["bashorat"], rtol=0, atol=1e-12)
    print(f"  kalitlar: {sorted(p2)}")
    print(f"  belgilar ({len(p2['belgilar'])}): {p2['belgilar'][:4]} (+8 ta)")
    for k in sorted(p2["metadata"]):
        print(f"  {k}: {p2['metadata'][k]}")
    print(f"  nazorat namunalari (50) yuklangach mos: {mos}")

    print("\n=== 4. Monitoring rejasi: chegaralar backtestdan ===")
    haftalik = []
    for k in (3, 2, 1):
        O = O_test - H * k
        fb, sb, tb, _ = prognoz(hgb(Y, A, kal, O), Y, A, kal, O)
        yb = Y[sb, tb]
        for dd in range(8):
            for w in range(4):
                m = (sb // 3 == dd) & ((tb - O - 1) // 7 == w)
                haftalik.append(np.abs(yb - fb)[m].sum() / yb[m].sum())
    haftalik = np.array(haftalik)
    chegara = float(np.quantile(haftalik, 0.95))
    print(f"  backtest: do'kon-hafta WAPE mediana {np.median(haftalik):.3f}, "
          f"95-persentil {chegara:.3f} ({len(haftalik)} ta)")
    test_h = np.array([[np.abs(y - f)[(dk == dd) & ((t - O_test - 1) // 7 == w)].sum()
                        / y[(dk == dd) & ((t - O_test - 1) // 7 == w)].sum()
                        for w in range(4)] for dd in range(8)])
    oshgan = test_h > chegara
    ketma = int(np.sum(oshgan[:, 1:] & oshgan[:, :-1]))
    print(f"  test davri: chegaradan oshgan do'kon-haftalar {int(oshgan.sum())}/32,"
          f" 2 hafta ketma-ket: {ketma}")
    reja = [
        ("har kuni", "yo'q qatorlar > 1% yoki kassa noli", "insident, to'ldirish"),
        ("har kuni", "anomaliya ulushi > 0.5%", "ma'lumot egasiga xabar"),
        ("har hafta", f"do'kon WAPE > {chegara:.3f}, 2 hafta ketma-ket",
         "tekshiruv, trigger"),
        ("har 4 hafta", "80% interval qamrovi < 0.70", "qayta kalibrlash"),
        ("har 4 hafta", "jadval bo'yicha qayta o'qitish", "backtest darvozasi"),
    ]
    for davriy, shart, harakat in reja:
        print(f"  {davriy:<12} {shart:<40} -> {harakat}")
    print("  ⭐ Test - hisobot uchun; aksiya ta'siri - tuzatilgan; paket o'zini tekshiradi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Yakuniy test: oxirgi 28 kun, BIR MARTA ===
  WAPE: HistGB 0.2092, seasonal naive 0.3245
  do'konlar bo'yicha farq -0.1170 (SE 0.0123); HistGB yaxshi do'konlar 8/8
  eng yomon do'kon: Andijon-Markaz (WAPE 0.2297)

=== 2. Aksiya ta'siri (o'quv davri; haqiqiy +25.0%) ===
  aksiya ulushi: dush-chor 0.084, shan-yak 0.023
  naiv farq           +0.191 (SE 0.015)  2*SE ichida: False
  regressiya          +0.259 (SE 0.010)  2*SE ichida: True
  kontrfaktual model  +0.257 (SE 0.010)  2*SE ichida: True
  moslashtirish       +0.254 (SE 0.011)  2*SE ichida: True

=== 3. Model paketi (joblib + metadata + nazorat namunalari) ===
  kalitlar: ['belgilar', 'format', 'metadata', 'model', 'nazorat']
  belgilar (12): ['dokon', 'guruh', 'h', 'hafta_kuni'] (+8 ta)
  maqsad: log1p(y) - daraja28
  model: HistGB global, direct, h=1..28
  oqitish_davri: 2024-02-05 - 2025-12-03
  qatorlar: 24
  tanlov: backtest, juftlashgan qoida (2-qadam)
  zaxira_kvantili: 0.75
  nazorat namunalari (50) yuklangach mos: True

=== 4. Monitoring rejasi: chegaralar backtestdan ===
  backtest: do'kon-hafta WAPE mediana 0.203, 95-persentil 0.254 (96 ta)
  test davri: chegaradan oshgan do'kon-haftalar 3/32, 2 hafta ketma-ket: 0
  har kuni     yo'q qatorlar > 1% yoki kassa noli       -> insident, to'ldirish
  har kuni     anomaliya ulushi > 0.5%                  -> ma'lumot egasiga xabar
  har hafta    do'kon WAPE > 0.254, 2 hafta ketma-ket   -> tekshiruv, trigger
  har 4 hafta  80% interval qamrovi < 0.70              -> qayta kalibrlash
  har 4 hafta  jadval bo'yicha qayta o'qitish           -> backtest darvozasi
  ⭐ Test - hisobot uchun; aksiya ta'siri - tuzatilgan; paket o'zini tekshiradi

Natija tahlili.

1-bo'lim — test davri bir marta ochildi. HistGB WAPE 0.2092, seasonal naive 0.3245; 8 do'konning hammasida HistGB yaxshi (farq -0.1170, SE 0.0123). Test xatosi backtestdagidan (0.1994) biroz yuqori — dekabr boshqa oylardan farq qiladi, va bitta 28 kunlik oyna shovqinli — lekin backtest do'kon-oyna tarqoqligi (std 0.0216) ichida. Muhim: test natijasi tanlovni o'zgartirmaydi — u faqat hisobotga yoziladi. Agar test yomon chiqsa, bu backtest dizaynini qayta ko'rib chiqish uchun signal, "boshqa modelni sinab ko'raylik" uchun emas.

2-bo'lim — aksiya ta'siri. Aksiyalar dushanba-chorshanba kunlari 8.4%, shanba-yakshanba 2.3% — sust kunlarda o'tkaziladi. Naiv baho +19.1% — haqiqiy +25% dan sezilarli past (2*SE ichida emas): aksiya kunlarining o'zi sust, va bu ta'sirni "yeb" qo'yadi. Uchta tuzatilgan usul — regressiya (hafta kuni x guruh, oy x guruh, bayram, o'sish; SE qator bo'yicha klaster) +25.9%, aksiyasiz kunlarda o'qitilgan kontrfaktual model +25.7%, shu qatorning ±1-2 hafta oldingi va keyingi aksiyasiz kunlari bilan moslashtirish +25.4% — hammasi haqiqiy qiymatni 2*SE ichida qamradi. Uch xil faraz bilan ishlaydigan uch usulning mos kelishi ishonchni oshiradi 28.11-bob. Lekin bu ishonch bitta farazga bog'liq: aksiya tanlovi taqvim va mavsum bilan tushuntiriladi. Menejer "ertaga yomg'ir, sust bo'ladi" degan ma'lumot bilan aksiya qo'ysa, bu o'lchanmagan chalkashtiruvchi bo'ladi.

3-bo'lim — paket. Model, 12 ta belgi nomi (tartibi bilan), metadata (o'qitish davri, maqsad o'zgartirishi, tanlov asosi, zaxira kvantili) va 50 ta nazorat namunasi bitta joblib faylda. Yuklangandan keyin nazorat namunalarining bashorati aynan mos (True) — boshqa muhitda kutubxona versiyasi farq qilsa, bu tekshiruv birinchi bo'lib xabar beradi 27.6-bob.

4-bo'lim — monitoring chegaralari taxmin bilan emas, backtestdan: oxirgi 3 oynaning 96 ta do'kon-hafta WAPE sining 95-persentili 0.254. Test davrida 32 ta do'kon-haftadan 3 tasi chegaradan oshdi (95-persentilda kutilgan ~1.6 ta), lekin ikki hafta ketma-ket oshgani yo'q — trigger yonmadi. Reja jadvali har bir tekshiruvni davriylik, shart va harakat bilan beradi — bu 27.11-27.13 dagi tizimning talab bashoratiga moslashtirilgani.


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

Noto'g'ri fikr To'g'risi
"Yo'q kun — savdo bo'lmagan kun" Ko'pincha yozilmagan kun; 0 bilan to'ldirish modelga "talab tushdi" deydi
"Bayram kunidagi keskin o'zgarish — anomaliya" Kutilgan hodisa; u modelga belgi sifatida beriladi
"Kutilgan kunlarni tekshirmasa ham bo'ladi" 1-misolda o'tkazib yuborilgan anomaliyalarning 9/10 i shunday kunlarda
"ETS/seasonal naive yetarli — aksiya taqvimi shart emas" Bayram va aksiya kunlarida xato 2-3 barobar katta
"Bitta backtest oynasi yetarli" Do'kon x oyna birliklari, juftlashgan farq va SE
"Testda yomon chiqsa, boshqa model tanlaymiz" Test — hisobot; tanlov faqat backtestda
"80% interval — 80% qamraydi" Faqat almashinuvchanlik bajarilsa; qamrovni o'lchang (misolda 75%)
"Omborga eng aniq bashoratni yuboramiz" Buyurtma — c_u/(c_u+c_o) kvantili; nuqtali bashoratdan 20% qimmatroq
"Aksiya kunlari 19% ko'p sotadi — ta'siri 19%" Aksiyalar sust kunlarda; tuzatilgan baho ~25%
"Model faylini saqlasak bo'ldi" Belgilar tartibi, metadata, nazorat namunalari bilan paket

6. Keng tarqalgan xatolar va yechimlari

1. Yo'q kunlarni 0 deb o'qish

python
W = df.pivot(...).fillna(0)                                     # ⚠️
W = df.pivot(...).reindex(columns=range(T))                     # ✅ NaN, keyin siyosat bilan

2. Kutilgan hodisalarni tozalab tashlash

python
df.loc[np.abs(z) > 3, "savdo"] = np.nan                         # ⚠️ bayramlar ham ketadi
anomaliya = (np.abs(z) > 4) & ~kutilgan                         # ✅ bayram/aksiya - belgi

3. Kelajakdan sizuvchi belgi

python
df["orta7"] = df.savdo.rolling(7, center=True).mean()           # ⚠️ kelajak qiymatlari
d7 = orta(o - 6, o)                                             # ✅ faqat origin gacha

4. Tasodifiy bo'lish yoki bitta oyna

python
train_test_split(X, y, test_size=0.2)                           # ⚠️
oynalar = [O_test - 28 * k for k in range(4, 0, -1)]            # ✅ rolling-origin

5. Nuqtali bashorat buyurtma sifatida

python
buyurtma = f                                                    # ⚠️ mediana atrofida
buyurtma = np.expm1(np.log1p(f) + np.quantile(qoldiq, C_U / (C_U + C_O)))  # ✅

6. Aksiya ta'sirini naiv baholash

python
tasir = y[aksiya].mean() / y[~aksiya].mean() - 1                # ⚠️ sust kunlar
tasir = np.expm1(np.mean(np.log1p(y[aksiya]) - f_aksiyasiz))    # ✅ kontrfaktual

7. Chegarani taxmin bilan qo'yish

python
if wape_hafta > 0.3: ogohlantir()                               # ⚠️ qayerdan 0.3?
chegara = np.quantile(backtest_haftalik_wape, 0.95)             # ✅ + 2 hafta ketma-ket

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 28.1-28.3-darslar (o'tilgan): Vaqt qatorlari asoslari, klassik va ML bashorat modellari — backtest, bazaviylar, ETS, global model
  • 28.6-dars (o'tilgan): Ilg'or anomaliya aniqlash — kutilgan qiymat va robust og'ish
  • 28.10-dars (o'tilgan): Katta ma'lumot — minglab do'kon va mahsulotda xuddi shu quvur chunk va bo'limlar bilan
  • 28.11-dars (o'tilgan): Sababiy xulosa — aksiya ta'siri
  • 17.5, 17.8-darslar (o'tilgan): Sana belgilari va sizish
  • 27.6, 27.11-27.13-darslar (o'tilgan): Paket, monitoring, drift, qayta o'qitish
  • Loyihalar va karyera qismi: Portfolio loyihasi sifatida — talab bashorati biznesga eng tushunarli ML loyihalaridan biri

8. Eng yaxshi amaliyotlar

  1. Qaror qoidalari va test davri — natijalarni ko'rishdan oldin.

  2. Yo'q kun NaN; kutilgan hodisa — belgi; anomaliya — to'ldirish va jurnal.

  3. Belgilar faqat origin gacha yoki rejadan; buni test bilan tekshiring.

  4. Bazaviy har doim; do'kon x oyna bo'yicha juftlashgan qaror; eng sodda munosib model.

  5. Xatoni kun turi va ufq bo'yicha ko'rsating — o'rtacha hamma narsani yashiradi.

  6. Intervallarning haqiqiy qamrovini o'lchang va monitoringga qo'shing.

  7. Buyurtma — narxlar nisbatidagi kvantil; c_u va c_o biznes bilan kelishiladi.

  8. Aksiya ta'siri — tuzatilgan usullar bilan, bir necha usul mos kelsa ishonch ortadi.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # yo'q kunni 0 bilan to'ldirsak, keyingi haftalar bashorati?
2.  # taqvimsiz anomaliya detektori bayram kunlarini nima qiladi?
3.  # kutilgan kunlarni butunlay chiqarib tashlashning narxi?
4.  # bayram kunlarida seasonal naive xatosi qanday?
5.  # nega o'quv originlari O - 28 dan oshmaydi?
6.  # HistGB xatosi ufq bo'yicha qanday o'zgaradi?
7.  # nominal 80% konformal interval qamrovi nimaga bog'liq?
8.  # c_u = 3, c_o = 1 - buyurtma kvantili?
9.  # nuqtali bashoratni buyurtma qilsak, talab qoplanadigan kunlar ulushi?
10. # aksiyalar sust kunlarda - naiv baho qaysi tomonga siljiydi?
11. # test davri yomon chiqsa, model almashtiramizmi?
12. # monitoring chegarasi qayerdan olinadi?
Javoblar
  1. Past chiqadi — model "talab tushdi" deb o'rganadi
  2. Anomaliya deb belgilaydi (chegara 3 da 255 ta yolg'on belgi)
  3. O'sha kunlardagi haqiqiy anomaliyalar o'tkazib yuboriladi (9/10)
  4. Juda katta (0.54, oddiy kunlarda 0.28)
  5. Maqsad kunlari backtest origin idan oshmasligi uchun — sizish yo'q
  6. Deyarli o'zgarmaydi (0.199 → 0.195)
  7. Kalibrovka va yangi ma'lumotning o'xshashligiga (almashinuvchanlik); misolda 75%
  8. 3 / (3 + 1) = 0.75
  9. ~50% (misolda 0.486)
  10. Pastga (misolda +19.1%, haqiqiy +25%)
  11. Yo'q — test hisobot uchun; backtest dizaynini qayta ko'rib chiqish signali
  12. Backtest dagi do'kon-hafta WAPE taqsimotidan (95-persentil)

Vazifa 2: Xatolarni tuzating

python
1.  W = xom.pivot(index="qator", columns="kun", values="savdo").fillna(0)

2.  df["orta7"] = df.groupby("qator").savdo.transform(lambda s: s.rolling(7, center=True).mean())

3.  X_tr, X_te = train_test_split(X, test_size=0.2, random_state=0)

4.  buyurtma = model.predict(X_kelajak)

5.  tasir = df[df.aksiya].savdo.mean() / df[~df.aksiya].savdo.mean() - 1
Javoblar
python
1.  W = xom.pivot(...).reindex(index=range(S), columns=range(T))   # NaN + to'ldirish siyosati

2.  d7 = orta(o - 6, o)          # faqat origin gacha; yoki shift(1).rolling(7)

3.  oynalar = [O_test - 28 * k for k in range(4, 0, -1)]   # rolling-origin backtest

4.  buyurtma = np.expm1(np.log1p(f) + np.quantile(qoldiq, 0.75))

5.  # kontrfaktual model / regressiya / moslashtirish (28.11)

Vazifa 3: Ma'lumot sifati

Modellang (1-misol asosida):

  1. Kutilgan kunlarni chiqarib tashlash o'rniga ular uchun chegarani 8 qiling — topilgan va yolg'on belgilar
  2. Kutilgan ta'sirni bazaga qo'shing (bayram x0.4, arafa x1.8, aksiya x1.25 — tarixdan baholab) va oddiy chegarani qo'llang
  3. Do'kon darajasidagi tekshiruv: bir kunda do'konning 3 guruhi ham nol bo'lsa — kassa noli ehtimoli
  4. To'ldirishni "o'tgan 4 hafta medianasi x aksiya ta'siri" bilan yaxshilang — WAPE qanchaga kamayadi?

Vazifa 4: Modellar

Modellang (2-misol asosida):

  1. HistGB dan aksiya belgisini olib tashlang — aksiya kunlaridagi WAPE qanchaga o'sadi?
  2. ETS ga bayram tuzatishi qo'shing (bayram kunlari prognozini tarixiy nisbatga ko'paytirish)
  3. Oynalar sonini 8 ga oshiring — SE qanday o'zgaradi va qaror o'zgaradimi?
  4. "Sizish testi": Y[:, O+1:] ni tasodifiy sonlar bilan almashtiring — HistGB bashorati o'zgarmasligi kerak

Vazifa 5: Noaniqlik va zaxira

Modellang (3-misol asosida):

  1. Kalibrovka oynalarini 2 dan 6 ga oshiring — qamrov 80% ga yaqinlashadimi?
  2. Guruhlar uchun turli narx: non c_o = 2, sut c_o = 1.5, meva c_o = 1 — har guruh uchun kvantil va narx
  3. Kvantil HistGB bilan 0.75 buyurtma — konformal bilan juftlashgan taqqoslang
  4. Qamrovni do'kon bo'yicha chiqaring — qaysi do'konda intervallar ishonchsiz?

Vazifa 6: Yakun va monitoring

Modellang (4-misol asosida):

  1. Aksiya tanlovida o'lchanmagan chalkashtiruvchi qo'shing (masalan "ob-havo": aksiyani ham, savdoni ham pasaytiradi) — tuzatilgan usullar qanchaga siljiydi?
  2. Aksiya ta'sirini guruh bo'yicha baholang (meva uchun kattaroq bo'lsin) — heterogen ta'sir
  3. Paketga konformal kalibrovka kvantillarini qo'shing va yuklangach interval bering
  4. Test davriga sun'iy drift qo'shing (bir do'konda savdo 30% tushadi) — monitoring qoidasi necha haftada ushlaydi?

Vazifa 7: O'ylash

Tarmoq direktori: "Yangi model backtestda WAPE ni 0.29 dan 0.20 ga tushirdi. Ajoyib! Endi uni hamma 300 ta do'konimizga va 40 ta mahsulot guruhiga birdan joriy qilamiz, omborga esa model bashoratini to'g'ridan-to'g'ri buyurtma sifatida yuboramiz. Monitoring keyin — avval natijani ko'raylik."

Javob

Qisqa javob: model yaxshi, lekin rejada uchta xavfli qadam bor: birdan hammaga, nuqtali bashorat buyurtma sifatida va monitoringsiz.

1. Buyurtma kvantili. 3-misolda nuqtali bashorat buyurtma sifatida 0.75 kvantildan 20% qimmat chiqdi (juftlashgan, SE 0.022). Nuqtali bashorat mediana atrofida — kunlarning yarmida mahsulot yetmaydi. Har guruh uchun c_u va c_o ni moliya bilan kelishib, kvantil buyurtma yuborish kerak.

2. Birdan hammaga emas. Backtest 8 do'kon va 3 guruhda o'tkazilgan. 300 do'kon va 40 guruhda boshqa narsalar bor: kam sotiladigan mahsulotlar (nollar ko'p — WAPE va log shkala boshqacha ishlaydi), yangi do'konlar (tarix yo'q — o'tgan yil belgisi bo'sh), boshqa aksiya amaliyotlari. Bosqichma-bosqich: avval 20-30 do'konda shadow rejim (model bashorat qiladi, lekin buyurtma eski usulda) — 27.13 dagi kabi — keyin canary.

3. Monitoring — boshidan. 1-misol ko'rsatdiki, ma'lumot xatolari (kassa noli, birlik xatosi) har kuni uchraydi; ular modelga jim kiradi. 3-misolda intervallar nominal 80% o'rniga 75% qamradi — bu faqat o'lchanganda ko'rinadi. 4-misoldagi reja (kunlik ma'lumot tekshiruvi, haftalik WAPE chegarasi backtestdan, 4 haftalik qamrov) ishga tushirish bilan birga yoqiladi, keyin emas.

python
# 1-2 hafta: 25 do'konda shadow, monitoring yoqilgan
# 3-6 hafta: shu do'konlarda canary - kvantil buyurtma, isrof va yetishmaslik o'lchanadi
# darvoza: isrof + yetishmaslik narxi eski usuldan sezilarli past (juftlashgan)
# keyin: 100 -> 300 do'kon, har bosqichda monitoring hisoboti

Direktorga javob: "Model haqiqatan yaxshi — backtestda xato uchdan biriga kamaydi. Lekin foyda xatodan emas, buyurtmadan keladi: model bashoratini to'g'ridan-to'g'ri yuborsak, har ikkinchi kun mahsulot yetmaydi; kvantil buyurtma bizning sinovimizda xarajatni 20% kamaytirdi. Men 25 do'konda 6 haftalik bosqichli joriy qilishni taklif qilaman — monitoring birinchi kundan ishlaydi, va 300 do'konga o'tish qarori isrof va yetishmaslik bo'yicha raqam bilan qabul qilinadi."

Nimani mustahkamlaydi: 2.1, 2.3, 2.5, 2.6-bo'limlar.


Xulosa

Bu darsda do'konlar tarmog'i uchun talab bashorati tizimini qurdik: xom kassa ma'lumotidan tortib buyurtma qarori, aksiya tahlili va monitoringgacha.

Eng muhim uch fikr:

  1. Ma'lumot sifati va kutilgan hodisalar — modeldan oldin. 1-misolda validatsiya 40 ta takror, 20 ta manfiy qiymat va 394 ta yo'q kunni topdi. Taqvimsiz anomaliya detektori bayram va aksiya kunlarini ham "anomaliya" dedi (aniqlik 0.178), taqvimli detektor chegara 4 da aniqlik 0.933 va to'liqlik 0.848 berdi — o'tkazib yuborilganlar esa asosan kutilgan kunlardagi haqiqiy xatolar edi. Hafta kuni medianasi bilan to'ldirish 0 bilan to'ldirishdan ancha yaxshi (WAPE 0.260 va 1.000).

  2. Qaror — backtest, juftlashgan farq va oldindan yozilgan qoida bilan. 2-misolda global HistGB 32 ta do'kon x oyna birligining hammasida seasonal naive dan yaxshi chiqdi (WAPE 0.1994, 0.2905 ga qarshi; ETS 0.2272), farqlar sezilarli; eng katta yutuq bayram va aksiya kunlarida (0.18 ga qarshi 0.54). Test davri bir marta ochildi va natija (0.2092) backtest bilan mos keldi.

  3. Bashorat — qaror emas: noaniqlik, kvantil va sabab. 3-misolda nominal 80% intervallar haqiqatda 75% qamradi — halol o'lchov monitoringga qo'shildi; c_u/(c_u+c_o) = 0.75 kvantilidagi buyurtma nuqtali bashoratdan 20% arzon chiqdi va o'lchangan eng arzon kvantil nazariyaga mos keldi. 4-misolda aksiya ta'siri naiv usulda +19.1%, uchta tuzatilgan usulda +25.4% dan +25.9% gacha (haqiqiy +25%); model paketi o'zini tekshiradi, monitoring chegaralari backtestdan olindi.

28-qism xulosasi. Bu qismda kursning asosiy yo'lidan tashqaridagi, lekin amalda juda ko'p uchraydigan vazifalarni ko'rdik. Vaqt qatorlari uchta darsda: komponentlar va halol baholash (rolling-origin backtest, bazaviylar), klassik modellar (ETS, ARIMA) va global ML modellari (lag belgilari, sizishsiz dizayn). Tavsiya tizimlari — mashhurlik bazaviysidan matritsa faktorizatsiyasi va implicit feedback gacha, ranjirlash metrikalari bilan. Ilg'or anomaliya aniqlash — kutilgan qiymat va robust og'ish, belgilarsiz baholash tuzoqlari. Reinforcement learning — ko'p qo'lli banditlardan chuqur RL va policy gradient gacha. Geografik ma'lumot — masofalar, hududlar va fazoviy sizish. Katta ma'lumot — dtype, chunk, oqimli algoritmlar, sketch lar, map-reduce va "qachon klaster kerak emas". Sababiy xulosa — korrelyatsiyadan ta'sirga: DAG lar, propensity score, AIPW, uplift va DiD. Har bir mavzu turli ko'rinsa ham, bitta skelet takrorlandi: bazaviy, halol baholash dizayni (vaqt, foydalanuvchi yoki guruh bo'yicha), juftlashgan taqqoslash va "eng sodda munosib yondashuv" qoidasi, farazlarni o'lchab tekshirish va natijani halol yozish. Bu skelet yangi turdagi har qanday vazifaga ko'chadi.

Keyingi qism — Loyihalar va karyera: kursning yakuniy qismi. Unda butun kurs davomida o'rganilganlar to'liq loyihaga yig'iladi — savolni qo'yishdan tortib ma'lumot, model, xizmat va hisobotgacha; portfolio qanday tuziladi va ko'rsatiladi; Data Science intervyulari — texnik savollar, holat tahlili (case study) va loyihani tushuntirish.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
28.12-dars: Amaliyot — do'konlar tarmog'i uchun talab bashorati va anomaliya monitoringi — IlmHamroh