IlmHamroh
Data Science va sun'iy intellekt/Maxsus mavzular4/12-dars53 daqiqa
Mundarija (26)

28.4-dars: Tavsiya tizimlari asoslari

28-QISM — MAXSUS MAVZULAR · 4-dars


1. Kirish va motivatsiya

Oldingi uch darsda vaqt qatorlari bilan ishladik: kuzatuvlar vaqt bo'yicha tartiblangan, vazifa esa kelajakni bashorat qilish edi. Endi butunlay boshqa shakldagi ma'lumotga o'tamiz — foydalanuvchilar va mahsulotlar orasidagi o'zaro ta'sirlar. Onlayn do'kon, kitob do'koni, musiqa yoki video xizmati, yangiliklar sayti — hammasida bitta savol bor: bu foydalanuvchiga nimani ko'rsatish kerak?

Bu savol oddiy klassifikatsiyaga o'xshamaydi. Belgilar jadvali yo'q — faqat "kim nimani ko'rdi, sotib oldi, baholadi" degan jurnal bor. Mahsulotlar minglab, foydalanuvchi esa ularning bir necha o'ntasini ko'rgan: matritsaning 97-99.99% i bo'sh. Bo'sh katak "yoqmadi" degani emas — ko'pincha "ko'rmagan" degani. Va eng muhimi: bizdan har bir katak uchun raqam emas, tartiblangan qisqa ro'yxat kutiladi — ekranda faqat 10 ta joy bor.

Real vaziyat. Toshkentdagi onlayn do'kon jamoasi bosh sahifadagi "Sizga yoqishi mumkin" bloki uchun murakkab kollaborativ model qurdi. Offline hisobotda natija ajoyib edi: recall@10 mashhurlik ro'yxatidan ikki barobar yuqori. Onlayn A/B testda esa farq deyarli sezilmadi. Tahlil uchta xatoni ko'rsatdi. Birinchi — ma'lumot tasodifiy bo'lingan edi: model "kelajakdagi" xaridlarni o'qitishda ko'rgan, mashhurlikni esa butun davrdan hisoblagan. Ikkinchi — bazaviy sifatida faqat "tasodifiy tavsiya" olingan, mashhurlik esa noto'g'ri sozlangan edi. Uchinchi — model tavsiyalarining 90% dan ortig'i baribir eng mashhur 10% mahsulotlardan iborat bo'lib chiqdi: u mashhurlik ro'yxatini biroz boshqacha tartibda qaytarayotgan edi.

Bu darsda tavsiya vazifasini to'g'ri qo'yish va — eng muhimi — halol baholashni o'rganamiz. Barcha usullarni noldan quramiz va bitta bo'lish, bitta metrikalar to'plami va foydalanuvchilar bo'yicha juftlashgan taqqoslash bilan solishtiramiz.

Bu darsda:

  • Vazifa: reyting bashorati va top-N ranjirlash
  • Explicit va implicit feedback; foydalanuvchi-mahsulot matritsasi va siyraklik
  • Uzun dum: mashhurlik juda notekis taqsimlangan
  • Bazaviylar: mashhurlik (ko'pincha yengish qiyin) va tasodifiy
  • Kontent-asosli tavsiya (TF-IDF, 23.4)
  • Kollaborativ filtrlash: user-kNN va item-kNN, kosinus o'xshashlik noldan
  • Baholash: vaqt bo'yicha va tasodifiy bo'lish; precision@k, recall@k, hit rate, MAP, nDCG@k noldan
  • Qamrov, xilma-xillik, yangilik va mashhurlik tarafkashligi
  • Tuzoqlar

ℹ Misollar real numpy/pandas/scipy/sklearn bilan (Python 3.14). Ma'lumot sintetik: urug' bilan yaratilgan onlayn do'kon — foydalanuvchilarning yashirin didlari, mahsulot kategoriyalari, keyinroq chiqqan yangi mahsulotlar va mashhurlikning uzun dumi bilan.


2. Nazariya — chuqur tushuntirish

2.1. Vazifa: reyting bashorati yoki top-N ranjirlash

Tavsiya tizimi haqida ikki xil savol berish mumkin va ular turli model va turli metrika talab qiladi.

text
1. REYTING BASHORATI (rating prediction)
   savol:   "foydalanuvchi u mahsulot i ga qanday baho qo'yadi?"
   ma'lumot: (u, i, baho) uchliklari, baho 1..5
   metrika: RMSE, MAE - faqat MA'LUM baholarda
   klassik misol: Netflix Prize (2006-2009)

2. TOP-N RANJIRLASH (top-N recommendation)
   savol:   "foydalanuvchi u ga qaysi N ta mahsulotni ko'rsatamiz?"
   ma'lumot: (u, i) juftliklari - ko'rdi/oldi (baho shart emas)
   metrika: precision@k, recall@k, hit rate, MAP, nDCG@k
   amaliyotda deyarli har doim SHU vazifa

NEGA FARQ QILADI:
   RMSE faqat baho qo'yilgan kataklarda o'lchanadi - bu foydalanuvchi
   allaqachon TANLAGAN mahsulotlar. Tavsiya esa KO'RMAGAN mahsulotlar
   orasidan tanlaydi. Baholarni yaxshi bashorat qiladigan model
   "nimani ko'rsatish kerak" savoliga yomon javob berishi mumkin.

Amalda tavsiya — ranjirlash vazifasi. Ekranda 10 ta joy bor; muhimi ularning birinchi o'ntaligi va tartibi, qolgan 790 ta mahsulotning aniq balli emas.

2.2. Explicit va implicit feedback

text
EXPLICIT (aniq) feedback: foydalanuvchi o'zi aytgan fikr
   yulduzcha baho, like/dislike, sharh
   + ma'nosi aniq (1 = yoqmadi, 5 = yoqdi)
   - KAM: 1-10% ta'sirlarda bor
   - TANLOV TARAFKASHLIGI: odamlar yoqqan narsani sotib oladi va baholaydi,
     baholar "tasodifiy yo'qolmagan" (missing not at random, MNAR)

IMPLICIT (yashirin) feedback: xulqdan kelib chiqadigan signal
   ko'rish, savatga qo'shish, xarid, tinglash vaqti, qayta xarid
   + KO'P: har harakat yoziladi
   - faqat IJOBIY signal: bo'sh katak = "yoqmadi" YOKI "ko'rmadi"
   - kuchi har xil: 1 ko'rish << 5 xarid  (28.5 da "ishonch" g'oyasi)
   - shovqin: tasodifiy bosish, sovg'a uchun xarid

Implicit ma'lumotda salbiy misol yo'q. Bo'sh katakni "0 = yoqmadi" deb o'qish noto'g'ri, lekin uni butunlay tashlab ketish ham noto'g'ri — model hamma narsani "yoqadi" deb o'rganadi. Bu nozik muvozanat 28.5-darsning asosiy mavzusi.

2.3. Foydalanuvchi-mahsulot matritsasi va siyraklik

text
             mahsulotlar ->
            i1  i2  i3  i4  i5  i6  ...
   u1        1   -   -   1   -   -
   u2        -   1   -   -   -   1          1 = o'zaro ta'sir bor
   u3        1   -   1   -   -   -          - = BO'SH (noma'lum!)
   ...

ZICHLIK = to'ldirilgan kataklar / (foydalanuvchilar x mahsulotlar)
   bizning sintetik do'kon:   ~2.8%
   real onlayn do'kon:        0.001% - 0.1%

SAQLASH: zich massiv o'rniga SIYRAK (CSR - compressed sparse row)
   data    - nol bo'lmagan qiymatlar
   indices - ularning ustun raqamlari
   indptr  - har qator qayerdan boshlanishi
   xotira ~ nnz * 12 bayt (float64 + int32), zich esa n_user * n_item * 8

scipy.sparse bilan matritsa ko'paytmasi (X.T @ X) faqat nol bo'lmagan kataklar ustida ishlaydi — kNN va keyingi darsdagi ALS shunga tayanadi (23.4-darsdagi siyrak TF-IDF matritsasi bilan bir xil g'oya).

2.4. Uzun dum va mashhurlik

text
  ta'sirlar soni
   |#
   |##
   |####
   |#######__
   |          ------------------------------------  <- UZUN DUM
   +---------------------------------------------->  mahsulotlar (mashhurlik tartibida)
     BOSH: oz sonli mashhur       DUM: ko'p sonli kam uchraydigan
     mahsulot, ko'p ta'sir        mahsulot, har biri oz ta'sir

Oqibatlari:

  • Mashhurlik bazaviysi kuchli — tasodifiy foydalanuvchi bilan tasodifiy mahsulotning uchrashish ehtimoli mahsulot mashhurligiga proporsional.
  • Dumdagi mahsulotlar haqida ma'lumot kam — kollaborativ usullar ularni yomon o'rganadi va kam tavsiya qiladi.
  • O'zini kuchaytiruvchi halqa: tavsiya qilingan mahsulot ko'proq ko'riladi → mashhurroq bo'ladi → yana tavsiya qilinadi. Bu "boy yanada boyiydi" effekti; uni o'lchash kerak 2.10-bob.

2.5. Bazaviylar

text
TASODIFIY:    har foydalanuvchiga tasodifiy k ta (ko'rmagan) mahsulot
              pastki chegara - "model umuman biror narsa o'rgandimi?"

MASHHURLIK:   hamma foydalanuvchiga eng ko'p ta'sirli k ta mahsulot
              (u ko'rganlari chiqarib tashlanadi)
              shaxsiylashtirilmagan, lekin KO'PINCHA YENGISH QIYIN

VAQT OYNASIDAGI MASHHURLIK: faqat oxirgi 30 kundagi ta'sirlar
              katalog va moda o'zgarganda muhim
              ⚠️ "oxirgi" - test boshlanishiga nisbatan, kelajakdan emas

Mashhurlik — majburiy bazaviy. Tavsiya bo'yicha ilmiy maqolalar tahlillarida murakkab modellarning sezilarli qismi yaxshi sozlangan mashhurlik yoki oddiy kNN ni yengolmagani ko'rsatilgan. Tasodifiy bazaviyni yengish — yutuq emas.

2.6. Kontent-asosli tavsiya

Kontent-asosli usul boshqa foydalanuvchilarga qaramaydi — faqat mahsulot atributlari va foydalanuvchining o'z tarixiga qaraydi.

text
1. Har mahsulot -> vektor (tavsif TF-IDF, kategoriya, narx, brend)    [23.4]
2. Foydalanuvchi profili = ko'rgan mahsulotlari vektorlarining yig'indisi
3. ball(u, i) = kosinus(profil_u, vektor_i)

+ yangi mahsulotni birinchi kundanoq tavsiya qila oladi (sovuq start, 28.5)
+ tushuntirish oson: "siz 'qahva' ko'rgansiz"
- "tor": faqat allaqachon ko'rganlariga o'xshashlarni beradi (xilma-xillik past)
- mashhurlik va sifatni bilmaydi - tavsifi o'xshash yomon mahsulot ham chiqadi
- atributlar sifati hal qiladi

2.7. Kollaborativ filtrlash: user-kNN va item-kNN

Kollaborativ filtrlash (CF) mahsulot mazmuniga qaramaydi — faqat kim nimani olganiga qaraydi: "sizga o'xshash odamlar buni olgan".

text
KOSINUS O'XSHASHLIK (ikki vektor orasida):
   cos(a, b) = (a . b) / (|a| * |b|)
   binar ma'lumotda: umumiy foydalanuvchilar / sqrt(n_a * n_b)

ITEM-kNN (mahsulotga asoslangan):
   S = mahsulot-mahsulot o'xshashlik:  X ustunlari normallanadi, S = Xn^T Xn
   har mahsulotda faqat eng yaqin K qo'shni qoladi
   ball(u, i) = sum_{j u ko'rgan} S[i, j]
   "siz X ni olgansiz - X ni olganlar Y ni ham olgan"

USER-kNN (foydalanuvchiga asoslangan):
   W = foydalanuvchi-foydalanuvchi o'xshashlik: X qatorlari normallanadi
   har foydalanuvchida eng yaqin K qo'shni
   ball(u, i) = sum_{v qo'shni} W[u, v] * X[v, i]

QAYSI BIRI:
   item-kNN - mahsulotlar kamroq va barqarorroq -> S ni kamdan-kam qayta
              hisoblash yetadi; tushuntirish oson; sanoatda keng tarqalgan
   user-kNN - foydalanuvchi didi tez o'zgaradi, foydalanuvchilar ko'p ->
              W katta va tez eskiradi
   ikkalasi ham: yangi foydalanuvchi/mahsulot uchun ishlamaydi (sovuq start)

kNN — sodda, lekin kuchli. U hech narsani "o'qitmaydi" — faqat o'xshashlikni hisoblaydi. Shunga qaramay ko'p amaliy vaziyatlarda murakkab modellar uni kam farq bilan yengadi yoki umuman yengolmaydi.

2.8. Baholash dizayni: qanday bo'lish kerak

text
TASODIFIY BO'LISH: har foydalanuvchining 20% ta'sirlari tasodifan test
   ⚠️ train ga KELAJAK kiradi: foydalanuvchi keyinroq olganlari train da,
      oldin olganlari test da; mashhurlik keyingi oylardan hisoblanadi
   natija ODATDA oshirib ko'rsatiladi

HAR FOYDALANUVCHINING OXIRGISI (leave-last-out, vaqt bo'yicha):
   har foydalanuvchining OXIRGI 20% ta'sirlari test
   real vazifaga yaqin: "tarixiga qarab keyingisini top"
   ⚠️ boshqa foydalanuvchilarning keyingi ta'sirlari train da qoladi
      (kichik sizish) - kerak bo'lsa, mashhurlikni test boshlanishigacha
      bo'lgan oynadan hisoblang

GLOBAL VAQT CHEGARASI: sana T gacha - train, T dan keyin - test
   eng halol (ishlab chiqarishga aynan mos), lekin test da yangi
   mahsulotlar va yangi foydalanuvchilar ko'proq - raqamlar pastroq

Bo'lish usuli raqamni o'zgartiradi — va usullar tartibini ham o'zgartirishi mumkin. Bu 17.8 (leakage) va 18-qismdagi vaqt bo'yicha CV ning tavsiya tizimlaridagi ko'rinishi.

Yana ikki qoida:

  • Train da ko'rilgan mahsulotlar tavsiyadan chiqarib tashlanadi (qayta xarid vazifasi bo'lmasa).
  • Barcha usullar bitta bo'lish va bitta foydalanuvchilar to'plamida baholanadi — shunda juftlashgan taqqoslash mumkin.

2.9. Ranjirlash metrikalari

Bitta foydalanuvchi uchun: top — tavsiya qilingan k ta mahsulot (tartib bilan), T — testda haqiqatan olganlari, rel_j = 1 agar j-o'rindagi mahsulot T da bo'lsa.

text
precision@k = (top ichidagi to'g'rilar) / k
recall@k    = (top ichidagi to'g'rilar) / |T|
hit@k       = 1, agar kamida bitta to'g'ri bo'lsa (hit rate - o'rtachasi)

AP@k  = (1 / min(|T|, k)) * sum_{j=1..k} precision@j * rel_j
MAP@k = foydalanuvchilar bo'yicha AP@k o'rtachasi

DCG@k  = sum_{j=1..k} rel_j / log2(j + 1)          (yuqoridagi o'rin qimmatroq)
IDCG@k = eng yaxshi tartibdagi DCG = sum_{j=1..min(|T|,k)} 1 / log2(j + 1)
nDCG@k = DCG@k / IDCG@k                            ([0, 1] oralig'ida)

MISOL: top = [5, 3, 9, 1, 7], T = {3, 7, 8}  ->  rel = [0, 1, 0, 0, 1]
   precision@5 = 2/5 = 0.4        recall@5 = 2/3 = 0.667     hit = 1
   AP@5  = (1/2 + 2/5) / 3 = 0.3
   nDCG@5 = (1/log2(3) + 1/log2(6)) / (1 + 1/log2(3) + 1/log2(4)) = 0.478
Metrika Tartibni ko'radimi Nimaga mos
precision@k yo'q "ekrandagi k ta joyning qanchasi foydali"
recall@k yo'q "foydalanuvchi olganlarining qanchasini topdik"
hit@k yo'q "kamida bittasi to'g'rimi" — test da 1 ta mahsulot bo'lsa
MAP@k ha to'g'rilar qanchalik yuqorida
nDCG@k ha (log chegirma) eng ko'p ishlatiladigan umumiy ranjirlash metrikasi

Metrikalar foydalanuvchi bo'yicha hisoblanadi, keyin o'rtachalanadi. Shuning uchun har usul uchun har foydalanuvchining bitta raqami bor — va ikki usulni bir xil foydalanuvchilar ustida juftlashgan farq bilan solishtirish mumkin.

2.10. Aniqlikdan tashqari: qamrov, xilma-xillik, yangilik

text
QAMROV (catalog coverage):
   kamida bir marta tavsiya qilingan mahsulotlar / katalog hajmi
   mashhurlik ro'yxati: k / n_item  (hammaga bir xil 10 ta)

XILMA-XILLIK (intra-list diversity, ILD):
   ro'yxat ichidagi juftliklarning qanchasi TURLI kategoriyadan
   (yoki 1 - o'rtacha o'xshashlik)

YANGILIK (novelty):
   o'rtacha -log2(pop_i / n_user) - kam uchraydigan mahsulot "yangiroq"

MASHHURLIK TARAFKASHLIGI (popularity bias):
   tavsiyalarda eng mashhur 10% mahsulotlar ulushi
   VS testdagi haqiqiy ta'sirlarda shu ulush
   tavsiya >> test  ->  model mashhurlarga yig'ilyapti

Yuqori nDCG va past qamrov birga bo'lishi mumkin. Model "hammaga bir xil mashhur mahsulotlar" ni biroz shaxsiylashtirib, yaxshi aniqlik oladi — lekin katalogning uchdan ikki qismi hech kimga ko'rsatilmaydi va sotuvchilar norozi. Bu kompromissni o'lchash va boshqarish mumkin: ballga mashhurlik jarimasini qo'shib, qayta ranjirlash (4-misol).

2.11. Juftlashgan taqqoslash foydalanuvchilar bo'yicha

text
har foydalanuvchi u uchun: d_u = nDCG_A(u) - nDCG_B(u)
o'rtacha farq  = mean(d)
SE             = std(d) / sqrt(n_user)
|mean(d)| > 2 * SE  ->  farq sezilarli

QAROR QOIDASI: eng yaxshisidan sezilarli yomon bo'lmagan ENG SODDA usul
   soddalik tartibi: mashhurlik < kontent < item-kNN < user-kNN

Foydalanuvchilar mustaqil birlik: bir foydalanuvchi ichidagi metrikalar bir-biriga bog'liq, foydalanuvchilar esa (taxminan) mustaqil. Shuning uchun SE foydalanuvchilar bo'yicha olinadi, alohida ta'sirlar bo'yicha emas.

2.12. Tuzoqlar

Asosiy tuzoqlar: tasodifiy bo'lish bilan baholash (kelajak train ga kiradi); mashhurlik bazaviysini tashlab ketish yoki uni butun davrdan (kelajakni ham qo'shib) hisoblash; train da ko'rilgan mahsulotlarni tavsiyadan chiqarmaslik; bo'sh katakni "yoqmadi" deb o'qish; o'rtacha baho bo'yicha "eng yaxshi" ro'yxat (2 ta 5 li baho bilan mahsulot birinchi o'rinda); faqat aniqlikka qarash — qamrov va mashhurlik tarafkashligini o'lchamaslik; farqni juftlashgan SE siz e'lon qilish; giperparametrlarni (K, jarima) test da tanlash.


3. Tez ma'lumotnoma

python
import numpy as np
from scipy import sparse

X = sparse.csr_matrix((np.ones(len(df)), (df["user"], df["item"])),
                      shape=(n_user, n_item))              # foydalanuvchi x mahsulot

# mashhurlik
pop = np.asarray(X.sum(0)).ravel()

# item-kNN: kosinus = normallangan ustunlar ko'paytmasi
Xn = X @ sparse.diags(1 / np.sqrt(np.maximum(pop, 1)))
S = (Xn.T @ Xn).toarray()
np.fill_diagonal(S, 0)
ball = X[users] @ S                                        # (n, n_item)

# kontent: TF-IDF profil (23.4)
V = TfidfVectorizer().fit_transform(items["tavsif"])
ball = cosine_similarity(X[users] @ V, V)

# baholash: ko'rilganlarni chiqarish, top-k, metrikalar
ball[X[users].toarray() > 0] = -np.inf
top = np.argsort(-ball, axis=1, kind="stable")[:, :10]
rel = np.take_along_axis(T, top, axis=1)                   # T - test (bool)
recall = rel.sum(1) / T.sum(1)
ndcg = (rel / np.log2(np.arange(2, 12))).sum(1) / idcg

# juftlashgan taqqoslash
d = ndcg_A - ndcg_B
sezilarli = abs(d.mean()) > 2 * d.std(ddof=1) / np.sqrt(len(d))

Qaysi vaziyatda nima

Vaziyat Yondashuv
Birinchi versiya, har qanday loyiha mashhurlik (vaqt oynasi bilan) — bazaviy
Tarix yetarli, mahsulotlar barqaror item-kNN
Yangi mahsulotlar ko'p, atributlar yaxshi kontent-asosli (yoki gibrid, 28.5)
Katta ma'lumot, yuqori aniqlik kerak matritsa faktorizatsiyasi (28.5)
Baholash vaqt bo'yicha bo'lish, nDCG@k + recall@k + qamrov
Taqqoslash foydalanuvchilar bo'yicha juftlashgan farq va SE

Tavsiya tizimi xulosasi

vazifa: top-N ranjirlash (reyting bashorati - kamdan-kam)
implicit: bo'sh katak = noma'lum, "yoqmadi" emas
bazaviy: mashhurlik - majburiy va kuchli
bo'lish: vaqt bo'yicha; tasodifiy - oshirib ko'rsatadi
metrikalar: precision/recall/hit/MAP/nDCG @k - har foydalanuvchida
aniqlikdan tashqari: qamrov, xilma-xillik, yangilik, mashhurlik ulushi

4. Batafsil misollar

Misollar real numpy/pandas/scipy/sklearn bilan (Python 3.14). Har misol mustaqil ishlaydi va bir xil sintetik do'konni (dokon(), urug' 0) yaratadi.

Misol 1 — Onlayn do'kon ma'lumoti: matritsa, siyraklik, uzun dum

Generator har foydalanuvchiga yashirin did beradi (kategoriya, kichik guruh, narx darajasi bo'yicha), har mahsulotga esa "sifat" (mashhurlik manbai) va chiqish kuni. Foydalanuvchi har safar 75% ehtimol bilan o'z didiga, 25% ehtimol bilan umumiy mashhurlikka qarab tanlaydi; yangi chiqqan mahsulot bir necha hafta "trendda" bo'ladi.

python
"""Onlayn do'kon ma'lumoti: matritsa, siyraklik, uzun dum, explicit va implicit."""

import numpy as np
import pandas as pd
from scipy import sparse

KATEGORIYA = {
    "elektronika": "telefon smartfon quloqchin zaryadlovchi noutbuk planshet "
                   "sichqoncha klaviatura kolonka televizor kamera soat",
    "kiyim": "koylak shim kurtka poyabzal krossovka shapka sharf palto "
             "kostyum futbolka yubka sviter",
    "kitob": "roman darslik detektiv sher tarix fantastika lugat ertak "
             "biografiya psixologiya biznes dasturlash",
    "oziq": "choy qahva asal yongoq shokolad guruch yog un makaron sharbat "
            "pechene ziravor",
    "gozallik": "krem atir shampun labbuyoq boyoq niqob sovun losyon taroq "
                "lak gel upa",
    "uy": "idish qozon choynak gilam parda yostiq chiroq sochiq pichoq tova "
          "vaza javon",
    "sport": "koptok gantel velosiped raketka forma gilamcha skakalka shlem "
             "sumka butsa trenajyor suzish",
    "bolalar": "oyinchoq konstruktor qogirchoq kolyaska taglik mashinka pazl "
               "albom rangli kiyimcha kitobcha samokat",
}
NARX = ["arzon", "orta", "premium"]
UMUMIY = "sifatli yangi chegirma original mashhur qulay kafolat tez yetkazish".split()


def dokon(seed=0, n_user=2000, n_item=800, kunlar=180):
    """Sintetik onlayn do'kon: yashirin didlar, mashhurlik dumi, yangi mahsulotlar."""
    rng = np.random.default_rng(seed)
    katlar = list(KATEGORIYA)
    n_kat, n_sub = len(katlar), 4
    sub = rng.integers(0, n_kat * n_sub, n_item)          # har mahsulot kichik guruhi
    kat = sub // n_sub
    narx = rng.choice(3, n_item, p=[0.5, 0.35, 0.15])
    sifat = rng.lognormal(0, 1.4, n_item)                  # mashhurlik dumi manbai
    chiqish = np.where(rng.random(n_item) < 0.7, 0,
                       rng.integers(0, kunlar - 20, n_item))
    tavsif = []
    for i in range(n_item):
        sozlar = KATEGORIYA[katlar[kat[i]]].split()
        guruh = sozlar[(sub[i] % n_sub) * 3:(sub[i] % n_sub) * 3 + 3]
        tanlov = list(rng.choice(guruh, 2, replace=False))
        tanlov.append(rng.choice([s for s in sozlar if s not in tanlov]))
        tanlov += [katlar[kat[i]], NARX[narx[i]], f"brend{rng.integers(0, 40)}"]
        tanlov += list(rng.choice(UMUMIY, 2, replace=False))
        tavsif.append(" ".join(tanlov))
    # foydalanuvchi didi: kategoriya x kichik guruh x narx
    kat_pref = rng.dirichlet(np.full(n_kat, 0.3), n_user)
    sub_pref = rng.dirichlet(np.full(n_sub, 0.5), (n_user, n_kat))
    narx_pref = rng.dirichlet(np.full(3, 1.0), n_user)
    did = (kat_pref[:, kat] * sub_pref[:, kat, sub % n_sub]
           * narx_pref[:, narx])                           # (user, item)
    did /= did.sum(1, keepdims=True)
    hafta = np.arange(0, kunlar, 7)
    yosh = np.clip((hafta[:, None] - chiqish[None, :]) / 7, 0, None)
    jalb = sifat * (0.6 + 1.2 * np.exp(-yosh / 6)) * (hafta[:, None] >= chiqish)
    qatorlar = []
    for u in range(n_user):
        n_u = int(np.clip(rng.lognormal(2.8, 0.8), 3, 150))
        vaqtlar = np.sort(rng.uniform(0, kunlar, n_u))
        korgan = np.zeros(n_item, dtype=bool)
        for t in vaqtlar:
            a = jalb[int(t) // 7] * ~korgan
            p = 0.75 * did[u] * a / (did[u] @ a) + 0.25 * a / a.sum()
            i = int(np.searchsorted(np.cumsum(p), rng.random() * p.sum()))
            korgan[i] = True
            mos = did[u, i] / did[u].max()
            xarid = rng.random() < 0.15 + 0.35 * mos
            baho = 0
            if xarid and rng.random() < 0.3:
                baho = int(np.clip(round(2.4 + 2.2 * mos + 0.3 * np.log(sifat[i])
                                         + rng.normal(0, 0.7)), 1, 5))
            qatorlar.append((u, i, t, int(xarid), baho))
    df = pd.DataFrame(qatorlar, columns=["user", "item", "vaqt", "xarid", "baho"])
    items = pd.DataFrame({"item": np.arange(n_item), "kategoriya": np.array(katlar)[kat],
                          "sub": sub, "narx": narx, "chiqish": chiqish,
                          "tavsif": tavsif})
    return df, items


def main() -> None:
    df, items = dokon()
    n_user, n_item = df["user"].nunique(), len(items)

    print("=== 1. O'zaro ta'sirlar jurnali ===")
    print(f"  foydalanuvchi {n_user}, mahsulot {n_item}, "
          f"ko'rish {len(df)}, xarid {int(df['xarid'].sum())}, "
          f"baho {int((df['baho'] > 0).sum())}")
    faol = df.groupby("user").size()
    q = np.percentile(faol, [10, 50, 90, 99]).astype(int).tolist()
    print(f"  bir foydalanuvchi: min {faol.min()}, persentillar 10/50/90/99 = {q},"
          f" max {faol.max()}")
    print(df.head(3).round(2).to_string(index=False))

    print("\n=== 2. Foydalanuvchi-mahsulot matritsasi ===")
    X = sparse.csr_matrix((np.ones(len(df)), (df["user"], df["item"])),
                          shape=(n_user, n_item))
    zichlik = X.nnz / (n_user * n_item)
    zich_bayt = n_user * n_item * 8
    csr_bayt = X.data.nbytes + X.indices.nbytes + X.indptr.nbytes
    print(f"  to'ldirilgan kataklar: {X.nnz} ({zichlik:.2%}), bo'sh {1 - zichlik:.2%}")
    print(f"  zich float64: {zich_bayt:,} bayt;  CSR: {csr_bayt:,} bayt "
          f"({zich_bayt / csr_bayt:.0f} marta kam)")
    u_real, i_real, nnz_real = 10_000_000, 1_000_000, 500_000_000
    print(f"  real miqyos (10 mln x 1 mln, 500 mln ta'sir): zich "
          f"{u_real * i_real * 8 / 1e12:.0f} TB, CSR ~{nnz_real * 12 / 1e9:.0f} GB, "
          f"zichlik {nnz_real / (u_real * i_real):.3%}")

    print("\n=== 3. Uzun dum: mashhurlik juda notekis ===")
    pop = np.sort(np.asarray(X.sum(0)).ravel())[::-1]
    for ulush in [0.01, 0.1, 0.2]:
        k = int(n_item * ulush)
        print(f"  eng mashhur {ulush:>4.0%} mahsulot ({k:>3} ta): "
              f"ta'sirlarning {pop[:k].sum() / pop.sum():.1%} i")
    print(f"  5 tadan kam ta'sirli mahsulotlar: {np.mean(pop < 5):.1%}")
    print("  o'nlik guruhlar (mashhur -> kam), ta'sirlar ulushi:")
    for d in range(10):
        s = pop[d * n_item // 10:(d + 1) * n_item // 10].sum() / pop.sum()
        print(f"   {d + 1:>2}  {'#' * int(round(s * 100))} {s:.1%}")

    print("\n=== 4. Explicit va implicit ===")
    baholi = df[df["baho"] > 0]
    print(f"  bahoga ega ta'sirlar: {len(baholi) / len(df):.1%}; "
          f"baho qo'ygan foydalanuvchilar {baholi['user'].nunique() / n_user:.1%}")
    taqs = baholi["baho"].value_counts(normalize=True).sort_index()
    print("  baholar taqsimoti: " + ", ".join(f"{k}: {v:.2f}" for k, v in taqs.items()))
    print(f"  o'rtacha baho {baholi['baho'].mean():.2f} - baho faqat XARID "
          f"qilinganlarga, xarid esa ko'proq yoqqanlarga")

    print("\n=== 5. O'rtacha baho bo'yicha top: kichik namuna tuzog'i ===")
    g = baholi.groupby("item")["baho"].agg(["mean", "count"])
    mu, m = baholi["baho"].mean(), 10
    g["silliq"] = (g["count"] * g["mean"] + m * mu) / (g["count"] + m)
    xom = g.sort_values(["mean", "count"], ascending=False).head(5)
    sil = g.sort_values("silliq", ascending=False).head(5)
    print(f"  xom o'rtacha top-5: baholar soni {xom['count'].tolist()}, "
          f"o'rtacha {xom['mean'].round(2).tolist()}")
    print(f"  silliqlangan (m={m}) top-5: baholar soni {sil['count'].tolist()}, "
          f"o'rtacha {sil['mean'].round(2).tolist()}")

    print("\n=== 6. Vaqt: katalog va mashhurlik o'zgaradi ===")
    yangi = items["chiqish"].to_numpy() > 0
    for a, b in [(0, 30), (75, 105), (150, 180)]:
        oyna = df[(df["vaqt"] >= a) & (df["vaqt"] < b)]
        print(f"  {a:>3}-{b:<3} kun: keyin chiqqan mahsulotlar ulushi "
              f"{yangi[oyna['item']].mean():.1%}")
    top_bosh = df[df["vaqt"] < 60]["item"].value_counts().head(20).index
    top_oxir = df[df["vaqt"] >= 120]["item"].value_counts().head(20).index
    print(f"  birinchi va oxirgi 60 kun top-20 lari kesishmasi: "
          f"{len(set(top_bosh) & set(top_oxir))} ta")
    print("  ⭐ Mashhurlik vaqtga bog'liq - baholashda vaqt tartibini saqlang")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. O'zaro ta'sirlar jurnali ===
  foydalanuvchi 2000, mahsulot 800, ko'rish 45019, xarid 11197, baho 3327
  bir foydalanuvchi: min 3, persentillar 10/50/90/99 = [6, 16, 47, 118], max 150
 user  item  vaqt  xarid  baho
    0   718  4.25      1     0
    0   249 15.36      0     0
    0   135 20.71      0     0

=== 2. Foydalanuvchi-mahsulot matritsasi ===
  to'ldirilgan kataklar: 45019 (2.81%), bo'sh 97.19%
  zich float64: 12,800,000 bayt;  CSR: 548,232 bayt (23 marta kam)
  real miqyos (10 mln x 1 mln, 500 mln ta'sir): zich 80 TB, CSR ~6 GB, zichlik 0.005%

=== 3. Uzun dum: mashhurlik juda notekis ===
  eng mashhur   1% mahsulot (  8 ta): ta'sirlarning 7.8% i
  eng mashhur  10% mahsulot ( 80 ta): ta'sirlarning 41.5% i
  eng mashhur  20% mahsulot (160 ta): ta'sirlarning 60.9% i
  5 tadan kam ta'sirli mahsulotlar: 9.6%
  o'nlik guruhlar (mashhur -> kam), ta'sirlar ulushi:
    1  ########################################## 41.5%
    2  ################### 19.4%
    3  ############ 12.5%
    4  ######### 8.9%
    5  ###### 6.4%
    6  ##### 4.6%
    7  ### 3.1%
    8  ## 2.1%
    9  # 1.2%
   10   0.4%

=== 4. Explicit va implicit ===
  bahoga ega ta'sirlar: 7.4%; baho qo'ygan foydalanuvchilar 69.2%
  baholar taqsimoti: 1: 0.01, 2: 0.12, 3: 0.30, 4: 0.32, 5: 0.25
  o'rtacha baho 3.68 - baho faqat XARID qilinganlarga, xarid esa ko'proq yoqqanlarga

=== 5. O'rtacha baho bo'yicha top: kichik namuna tuzog'i ===
  xom o'rtacha top-5: baholar soni [3, 2, 2, 2, 2], o'rtacha [5.0, 5.0, 5.0, 5.0, 5.0]
  silliqlangan (m=10) top-5: baholar soni [7, 30, 17, 33, 10], o'rtacha [4.71, 4.23, 4.29, 4.18, 4.4]

=== 6. Vaqt: katalog va mashhurlik o'zgaradi ===
    0-30  kun: keyin chiqqan mahsulotlar ulushi 3.1%
   75-105 kun: keyin chiqqan mahsulotlar ulushi 27.0%
  150-180 kun: keyin chiqqan mahsulotlar ulushi 39.9%
  birinchi va oxirgi 60 kun top-20 lari kesishmasi: 6 ta
  ⭐ Mashhurlik vaqtga bog'liq - baholashda vaqt tartibini saqlang

Natija tahlili.

1-bo'lim — 2000 foydalanuvchi, 800 mahsulot, 45019 ko'rish. Ulardan 11197 tasi xarid, atigi 3327 tasiga baho qo'yilgan. Faollik juda notekis: median foydalanuvchi 16 ta mahsulot ko'rgan, eng faol 1% esa 118 tadan ortiq — bu ham real hayotdagidek.

2-bo'lim — matritsaning 97.19% i bo'sh. Siyrak CSR shakli zich massivdan 23 marta kam joy oladi. Real miqyosda bu farq hal qiluvchi: 10 mln foydalanuvchi va 1 mln mahsulot uchun zich matritsa 80 TB, siyrak esa bir necha GB — va zichlik bizning sintetik do'kondagidan ancha past (0.005%).

3-bo'lim — uzun dum. Eng mashhur 10% mahsulot ta'sirlarning 41.5% ini, 20% i esa 60.9% ini oladi; oxirgi o'nlik guruh atigi 0.4%. Bu mashhurlik bazaviysining kuchli bo'lishining sababi.

4-bo'lim — explicit feedback kam: ta'sirlarning faqat 7.4% ida baho bor. O'rtacha baho 3.68 va 1 ball deyarli yo'q (0.01). Bu "mahsulotlar yaxshi" degani emas — baho faqat sotib olganlar tomonidan qo'yiladi, sotib olish esa ko'proq didga mos kelganda bo'ladi. Baholangan kataklar tasodifiy namuna emas (MNAR).

5-bo'lim — kichik namuna tuzog'i. O'rtacha baho bo'yicha top-5 ning hammasi 5.0 — lekin ularda atigi 2-3 tadan baho bor. Silliqlangan o'rtacha (n * o'rtacha + m * mu) / (n + m) (bu 11-qismdagi "qisqartirish" g'oyasi, Bayes o'rtachasi) kam bahoni umumiy o'rtachaga tortadi: endi ro'yxatda 30, 33 tagacha bahoga ega mahsulotlar bor.

6-bo'lim — vaqt. Keyinroq chiqqan mahsulotlar birinchi oyda ta'sirlarning 3.1% ini, oxirgi oyda 39.9% ini tashkil qiladi; birinchi va oxirgi 60 kunning top-20 lari atigi 6 ta mahsulotda kesishadi. Katalog va moda o'zgaradi — shuning uchun baholashda vaqt tartibini saqlash kerak (2-misol).

Misol 2 — Metrikalar noldan, bazaviylar va uch xil bo'lish

python
"""Ranjirlash metrikalari noldan, bazaviylar va uch xil bo'lish."""

import numpy as np
import pandas as pd
from scipy import sparse
from sklearn.metrics import ndcg_score

KATEGORIYA = {
    "elektronika": "telefon smartfon quloqchin zaryadlovchi noutbuk planshet "
                   "sichqoncha klaviatura kolonka televizor kamera soat",
    "kiyim": "koylak shim kurtka poyabzal krossovka shapka sharf palto "
             "kostyum futbolka yubka sviter",
    "kitob": "roman darslik detektiv sher tarix fantastika lugat ertak "
             "biografiya psixologiya biznes dasturlash",
    "oziq": "choy qahva asal yongoq shokolad guruch yog un makaron sharbat "
            "pechene ziravor",
    "gozallik": "krem atir shampun labbuyoq boyoq niqob sovun losyon taroq "
                "lak gel upa",
    "uy": "idish qozon choynak gilam parda yostiq chiroq sochiq pichoq tova "
          "vaza javon",
    "sport": "koptok gantel velosiped raketka forma gilamcha skakalka shlem "
             "sumka butsa trenajyor suzish",
    "bolalar": "oyinchoq konstruktor qogirchoq kolyaska taglik mashinka pazl "
               "albom rangli kiyimcha kitobcha samokat",
}
NARX = ["arzon", "orta", "premium"]
UMUMIY = "sifatli yangi chegirma original mashhur qulay kafolat tez yetkazish".split()


def dokon(seed=0, n_user=2000, n_item=800, kunlar=180):
    """Sintetik onlayn do'kon: yashirin didlar, mashhurlik dumi, yangi mahsulotlar."""
    rng = np.random.default_rng(seed)
    katlar = list(KATEGORIYA)
    n_kat, n_sub = len(katlar), 4
    sub = rng.integers(0, n_kat * n_sub, n_item)          # har mahsulot kichik guruhi
    kat = sub // n_sub
    narx = rng.choice(3, n_item, p=[0.5, 0.35, 0.15])
    sifat = rng.lognormal(0, 1.4, n_item)                  # mashhurlik dumi manbai
    chiqish = np.where(rng.random(n_item) < 0.7, 0,
                       rng.integers(0, kunlar - 20, n_item))
    tavsif = []
    for i in range(n_item):
        sozlar = KATEGORIYA[katlar[kat[i]]].split()
        guruh = sozlar[(sub[i] % n_sub) * 3:(sub[i] % n_sub) * 3 + 3]
        tanlov = list(rng.choice(guruh, 2, replace=False))
        tanlov.append(rng.choice([s for s in sozlar if s not in tanlov]))
        tanlov += [katlar[kat[i]], NARX[narx[i]], f"brend{rng.integers(0, 40)}"]
        tanlov += list(rng.choice(UMUMIY, 2, replace=False))
        tavsif.append(" ".join(tanlov))
    # foydalanuvchi didi: kategoriya x kichik guruh x narx
    kat_pref = rng.dirichlet(np.full(n_kat, 0.3), n_user)
    sub_pref = rng.dirichlet(np.full(n_sub, 0.5), (n_user, n_kat))
    narx_pref = rng.dirichlet(np.full(3, 1.0), n_user)
    did = (kat_pref[:, kat] * sub_pref[:, kat, sub % n_sub]
           * narx_pref[:, narx])                           # (user, item)
    did /= did.sum(1, keepdims=True)
    hafta = np.arange(0, kunlar, 7)
    yosh = np.clip((hafta[:, None] - chiqish[None, :]) / 7, 0, None)
    jalb = sifat * (0.6 + 1.2 * np.exp(-yosh / 6)) * (hafta[:, None] >= chiqish)
    qatorlar = []
    for u in range(n_user):
        n_u = int(np.clip(rng.lognormal(2.8, 0.8), 3, 150))
        vaqtlar = np.sort(rng.uniform(0, kunlar, n_u))
        korgan = np.zeros(n_item, dtype=bool)
        for t in vaqtlar:
            a = jalb[int(t) // 7] * ~korgan
            p = 0.75 * did[u] * a / (did[u] @ a) + 0.25 * a / a.sum()
            i = int(np.searchsorted(np.cumsum(p), rng.random() * p.sum()))
            korgan[i] = True
            mos = did[u, i] / did[u].max()
            xarid = rng.random() < 0.15 + 0.35 * mos
            baho = 0
            if xarid and rng.random() < 0.3:
                baho = int(np.clip(round(2.4 + 2.2 * mos + 0.3 * np.log(sifat[i])
                                         + rng.normal(0, 0.7)), 1, 5))
            qatorlar.append((u, i, t, int(xarid), baho))
    df = pd.DataFrame(qatorlar, columns=["user", "item", "vaqt", "xarid", "baho"])
    items = pd.DataFrame({"item": np.arange(n_item), "kategoriya": np.array(katlar)[kat],
                          "sub": sub, "narx": narx, "chiqish": chiqish,
                          "tavsif": tavsif})
    return df, items


def bolish(df, usul="vaqt", ulush=0.2, seed=0, chegara=150.0):
    """'vaqt' - har foydalanuvchining oxirgi 20% i test; 'tasodifiy'; 'global'."""
    df = df.sort_values(["user", "vaqt"]).reset_index(drop=True)
    if usul == "global":
        tarix = df[df["vaqt"] < chegara].groupby("user").size()
        yetarli = df["user"].map(tarix).fillna(0).to_numpy() >= 3
        test = (df["vaqt"] >= chegara).to_numpy() & yetarli
        return df[df["vaqt"] < chegara].copy(), df[test].copy()
    n = df.groupby("user")["item"].transform("size").to_numpy()
    if usul == "tasodifiy":
        kalit = np.random.default_rng(seed).random(len(df))
        tartib = (df.assign(k=kalit).groupby("user")["k"]
                  .rank(method="first").to_numpy() - 1)
    else:
        tartib = df.groupby("user").cumcount().to_numpy()
    n_test = np.maximum(1, np.round(ulush * n)).astype(int)
    test = (tartib >= n - n_test) & (n >= 5)
    return df[~test].copy(), df[test].copy()


def matritsa(qism, n_user, n_item):
    return sparse.csr_matrix((np.ones(len(qism)), (qism["user"], qism["item"])),
                             shape=(n_user, n_item))


def metrikalar(top, T):
    """top: (n, k) tavsiya indekslari; T: (n, n_item) test to'plami (bool)."""
    k = top.shape[1]
    rel = np.take_along_axis(T, top, axis=1)
    n_rel = T.sum(1)
    pozitsiya = np.arange(1, k + 1)
    ap = (np.cumsum(rel, 1) / pozitsiya * rel).sum(1) / np.minimum(n_rel, k)
    chegirma = 1 / np.log2(pozitsiya + 1)
    idcg = np.array([chegirma[:min(r, k)].sum() for r in n_rel])
    return {"precision": rel.sum(1) / k, "recall": rel.sum(1) / n_rel,
            "hit": rel.any(1).astype(float), "MAP": ap,
            "nDCG": (rel * chegirma).sum(1) / idcg}


def baholash(ball, X_train, test, users, k=10):
    """Train da ko'rilgan mahsulotlar tavsiya qilinmaydi; har foydalanuvchi metrikasi."""
    ball = np.array(ball, dtype=float)
    ball[X_train[users].toarray() > 0] = -np.inf
    top = np.argsort(-ball, axis=1, kind="stable")[:, :k]
    qator = pd.Series(np.arange(len(users)), index=users)
    tt = test[test["user"].isin(users)]
    T = np.zeros(ball.shape, dtype=bool)
    T[qator[tt["user"]].to_numpy(), tt["item"].to_numpy()] = True
    return metrikalar(top, T), top


def mashhurlik_oynada(train, t_bosh, n_item, oyna=30.0):
    """Har foydalanuvchi uchun: test boshlanishidan OLDINGI 'oyna' kundagi ko'rishlar."""
    vaqt, item = train["vaqt"].to_numpy(), train["item"].to_numpy()
    ball = np.zeros((len(t_bosh), n_item))
    for r, t in enumerate(t_bosh):
        m = (vaqt >= t - oyna) & (vaqt < t)
        ball[r] = np.bincount(item[m], minlength=n_item)
    return ball


def main() -> None:
    print("=== 1. Metrikalar noldan: bitta foydalanuvchi ===")
    top = np.array([[5, 3, 9, 1, 7]])                  # tavsiya tartibi
    T = np.zeros((1, 10), dtype=bool)
    T[0, [3, 7, 8]] = True                             # haqiqatda olganlari
    m = metrikalar(top, T)
    print("  tavsiya [5, 3, 9, 1, 7], haqiqiy {3, 7, 8}; relevantlik [0 1 0 0 1]")
    print("  " + ", ".join(f"{k}@5 {v[0]:.4f}" for k, v in m.items()))
    print(f"  qo'lda: AP = (1/2 + 2/5) / 3 = {(1 / 2 + 2 / 5) / 3:.4f};  "
          f"nDCG = (1/log2(3) + 1/log2(6)) / (1 + 1/log2(3) + 1/log2(4)) = "
          f"{(1 / np.log2(3) + 1 / np.log2(6)) / (1 + 1 / np.log2(3) + 0.5):.4f}")
    ball = np.zeros((1, 10))
    ball[0, top[0]] = [5, 4, 3, 2, 1]
    print(f"  sklearn ndcg_score(k=5): {ndcg_score(T.astype(float), ball, k=5):.4f}")

    df, items = dokon()
    n_user, n_item = 2000, len(items)
    rng = np.random.default_rng(1)

    print("\n=== 2. Uch xil bo'lish ===")
    bolishlar = {u: bolish(df, u) for u in ["tasodifiy", "vaqt", "global"]}
    for nom, (tr, te) in bolishlar.items():
        kechroq = np.mean(te["item"].map(items.set_index("item")["chiqish"]) > 0)
        yangi = ~te["item"].isin(tr["item"].unique())
        print(f"  {nom:<10} train {len(tr):>6}, test {len(te):>5} "
              f"({te['user'].nunique()} foyd.), test o'rtacha kuni "
              f"{te['vaqt'].mean():5.1f}, trainda yo'q mahsulot {yangi.mean():.1%}, "
              f"kech chiqqan {kechroq:.1%}")

    print("\n=== 3. Bazaviylar ('vaqt' bo'lishi, k=10) ===")
    tr, te = bolishlar["vaqt"]
    X = matritsa(tr, n_user, n_item)
    users = np.sort(te["user"].unique())
    t_bosh = te.groupby("user")["vaqt"].min().loc[users].to_numpy()
    pop = np.asarray(X.sum(0)).ravel()
    usullar = {
        "tasodifiy": rng.random((len(users), n_item)),
        "mashhurlik (butun)": np.tile(pop, (len(users), 1)),
        "mashhurlik (30 kun)": mashhurlik_oynada(tr, t_bosh, n_item),
    }
    natija = {}
    print(f"  {'usul':<22}" + "".join(f"{k:>10}" for k in
                                     ["precision", "recall", "hit", "MAP", "nDCG"]))
    for nom, b in usullar.items():
        natija[nom], _ = baholash(b, X, te, users)
        print(f"  {nom:<22}" + "".join(f"{v.mean():>10.4f}"
                                        for v in natija[nom].values()))

    print("\n=== 4. Juftlashgan taqqoslash: foydalanuvchilar bo'yicha nDCG@10 ===")
    for a, b in [("mashhurlik (butun)", "tasodifiy"),
                 ("mashhurlik (30 kun)", "mashhurlik (butun)")]:
        d = natija[a]["nDCG"] - natija[b]["nDCG"]
        se = d.std(ddof=1) / np.sqrt(len(d))
        print(f"  {a} - {b}: {d.mean():+.4f}, SE {se:.4f}, "
              f"sezilarli: {abs(d.mean()) > 2 * se}")

    print("\n=== 5. Bo'lish usuli natijani o'zgartiradi (mashhurlik, nDCG@10) ===")
    for nom, (tr, te) in bolishlar.items():
        X = matritsa(tr, n_user, n_item)
        us = np.sort(te["user"].unique())
        pop = np.asarray(X.sum(0)).ravel()
        r, _ = baholash(np.tile(pop, (len(us), 1)), X, te, us)
        se = r["nDCG"].std(ddof=1) / np.sqrt(len(us))
        print(f"  {nom:<10} nDCG@10 {r['nDCG'].mean():.4f} (SE {se:.4f}), "
              f"recall@10 {r['recall'].mean():.4f}")
    print("  ⭐ Tasodifiy bo'lish kelajakni o'qitishga qo'shadi - raqamlar yolg'on")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Metrikalar noldan: bitta foydalanuvchi ===
  tavsiya [5, 3, 9, 1, 7], haqiqiy {3, 7, 8}; relevantlik [0 1 0 0 1]
  precision@5 0.4000, recall@5 0.6667, hit@5 1.0000, MAP@5 0.3000, nDCG@5 0.4776
  qo'lda: AP = (1/2 + 2/5) / 3 = 0.3000;  nDCG = (1/log2(3) + 1/log2(6)) / (1 + 1/log2(3) + 1/log2(4)) = 0.4776
  sklearn ndcg_score(k=5): 0.4776

=== 2. Uch xil bo'lish ===
  tasodifiy  train  36124, test  8895 (1864 foyd.), test o'rtacha kuni  89.8, trainda yo'q mahsulot 0.1%, kech chiqqan 24.9%
  vaqt       train  36124, test  8895 (1864 foyd.), test o'rtacha kuni 159.0, trainda yo'q mahsulot 1.2%, kech chiqqan 40.0%
  global     train  37610, test  7332 (1713 foyd.), test o'rtacha kuni 165.1, trainda yo'q mahsulot 4.6%, kech chiqqan 39.8%

=== 3. Bazaviylar ('vaqt' bo'lishi, k=10) ===
  usul                   precision    recall       hit       MAP      nDCG
  tasodifiy                 0.0058    0.0120    0.0536    0.0041    0.0094
  mashhurlik (butun)        0.0383    0.0924    0.3203    0.0351    0.0706
  mashhurlik (30 kun)       0.0407    0.0972    0.3353    0.0381    0.0767

=== 4. Juftlashgan taqqoslash: foydalanuvchilar bo'yicha nDCG@10 ===
  mashhurlik (butun) - tasodifiy: +0.0612, SE 0.0033, sezilarli: True
  mashhurlik (30 kun) - mashhurlik (butun): +0.0061, SE 0.0041, sezilarli: False

=== 5. Bo'lish usuli natijani o'zgartiradi (mashhurlik, nDCG@10) ===
  tasodifiy  nDCG@10 0.0959 (SE 0.0035), recall@10 0.1187
  vaqt       nDCG@10 0.0706 (SE 0.0031), recall@10 0.0924
  global     nDCG@10 0.0624 (SE 0.0032), recall@10 0.0823
  ⭐ Tasodifiy bo'lish kelajakni o'qitishga qo'shadi - raqamlar yolg'on

Natija tahlili.

1-bo'lim — metrikalar 2.9-bo'limdagi qo'lda hisob bilan aynan mos: precision@5 0.4, recall@5 0.6667, AP 0.3, nDCG@5 0.4776. nDCG ni sklearn.metrics.ndcg_score ham 0.4776 deb tasdiqladi. Noldan yozilgan funksiya vektorlashtirilgan — bitta chaqiruv 1864 foydalanuvchining hammasini hisoblaydi.

2-bo'lim — uch bo'lish. Tasodifiy va "vaqt" bo'lishida train/test hajmi bir xil (36124 / 8895), farq faqat qaysi ta'sirlar test ga tushganida: tasodifiy bo'lishda test o'rtacha 89.8-kunda, vaqt bo'yicha bo'lishda 159.0-kunda. Vaqt bo'yicha test da keyinroq chiqqan mahsulotlar ulushi 40.0% (tasodifiyda 24.9%), global chegarada esa test mahsulotlarining 4.6% i train da umuman yo'q — ularni hech bir CF usuli topa olmaydi.

3-bo'lim — bazaviylar. Tasodifiy tavsiya nDCG@10 0.0094 — 10 ta tavsiyadan birortasi to'g'ri chiqqan foydalanuvchilar atigi 5.4%. Mashhurlik yetti barobar yaxshi: hit 0.3203, nDCG 0.0706. Test boshlanishidan oldingi 30 kunlik mashhurlik (0.0767) biroz yuqori.

4-bo'lim — juftlashgan taqqoslash. Mashhurlik tasodifiydan +0.0612 yaxshi, SE 0.0033 — farq SE dan 18 barobar katta. 30 kunlik oyna esa butun davrdagidan +0.0061 yaxshi, lekin SE 0.0041 — sezilarli emas. Qaror qoidasi bo'yicha oddiy mashhurlikni qoldiramiz; farq katalog tezroq o'zgaradigan do'konda sezilarli bo'lishi mumkin — buni har safar o'lchash kerak.

5-bo'lim — bir xil usul, bir xil ma'lumot, uch xil bo'lish: nDCG@10 tasodifiy bo'lishda 0.0959, vaqt bo'yicha 0.0706, global chegarada 0.0624. Tasodifiy bo'lish natijani taxminan 36% ga oshirib ko'rsatdi: mashhurlik "kelajak" ta'sirlaridan ham hisoblangan va test da eski, allaqachon mashhur mahsulotlar ko'proq. Kirishdagi jamoa aynan shu tuzoqqa tushgan edi.

Misol 3 — Kontent-asosli va kollaborativ filtrlash

python
"""Kontent-asosli va kollaborativ filtrlash (user/item kNN) - bazaviylarga qarshi."""

import numpy as np
import pandas as pd
from scipy import sparse
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

KATEGORIYA = {
    "elektronika": "telefon smartfon quloqchin zaryadlovchi noutbuk planshet "
                   "sichqoncha klaviatura kolonka televizor kamera soat",
    "kiyim": "koylak shim kurtka poyabzal krossovka shapka sharf palto "
             "kostyum futbolka yubka sviter",
    "kitob": "roman darslik detektiv sher tarix fantastika lugat ertak "
             "biografiya psixologiya biznes dasturlash",
    "oziq": "choy qahva asal yongoq shokolad guruch yog un makaron sharbat "
            "pechene ziravor",
    "gozallik": "krem atir shampun labbuyoq boyoq niqob sovun losyon taroq "
                "lak gel upa",
    "uy": "idish qozon choynak gilam parda yostiq chiroq sochiq pichoq tova "
          "vaza javon",
    "sport": "koptok gantel velosiped raketka forma gilamcha skakalka shlem "
             "sumka butsa trenajyor suzish",
    "bolalar": "oyinchoq konstruktor qogirchoq kolyaska taglik mashinka pazl "
               "albom rangli kiyimcha kitobcha samokat",
}
NARX = ["arzon", "orta", "premium"]
UMUMIY = "sifatli yangi chegirma original mashhur qulay kafolat tez yetkazish".split()


def dokon(seed=0, n_user=2000, n_item=800, kunlar=180):
    """Sintetik onlayn do'kon: yashirin didlar, mashhurlik dumi, yangi mahsulotlar."""
    rng = np.random.default_rng(seed)
    katlar = list(KATEGORIYA)
    n_kat, n_sub = len(katlar), 4
    sub = rng.integers(0, n_kat * n_sub, n_item)          # har mahsulot kichik guruhi
    kat = sub // n_sub
    narx = rng.choice(3, n_item, p=[0.5, 0.35, 0.15])
    sifat = rng.lognormal(0, 1.4, n_item)                  # mashhurlik dumi manbai
    chiqish = np.where(rng.random(n_item) < 0.7, 0,
                       rng.integers(0, kunlar - 20, n_item))
    tavsif = []
    for i in range(n_item):
        sozlar = KATEGORIYA[katlar[kat[i]]].split()
        guruh = sozlar[(sub[i] % n_sub) * 3:(sub[i] % n_sub) * 3 + 3]
        tanlov = list(rng.choice(guruh, 2, replace=False))
        tanlov.append(rng.choice([s for s in sozlar if s not in tanlov]))
        tanlov += [katlar[kat[i]], NARX[narx[i]], f"brend{rng.integers(0, 40)}"]
        tanlov += list(rng.choice(UMUMIY, 2, replace=False))
        tavsif.append(" ".join(tanlov))
    # foydalanuvchi didi: kategoriya x kichik guruh x narx
    kat_pref = rng.dirichlet(np.full(n_kat, 0.3), n_user)
    sub_pref = rng.dirichlet(np.full(n_sub, 0.5), (n_user, n_kat))
    narx_pref = rng.dirichlet(np.full(3, 1.0), n_user)
    did = (kat_pref[:, kat] * sub_pref[:, kat, sub % n_sub]
           * narx_pref[:, narx])                           # (user, item)
    did /= did.sum(1, keepdims=True)
    hafta = np.arange(0, kunlar, 7)
    yosh = np.clip((hafta[:, None] - chiqish[None, :]) / 7, 0, None)
    jalb = sifat * (0.6 + 1.2 * np.exp(-yosh / 6)) * (hafta[:, None] >= chiqish)
    qatorlar = []
    for u in range(n_user):
        n_u = int(np.clip(rng.lognormal(2.8, 0.8), 3, 150))
        vaqtlar = np.sort(rng.uniform(0, kunlar, n_u))
        korgan = np.zeros(n_item, dtype=bool)
        for t in vaqtlar:
            a = jalb[int(t) // 7] * ~korgan
            p = 0.75 * did[u] * a / (did[u] @ a) + 0.25 * a / a.sum()
            i = int(np.searchsorted(np.cumsum(p), rng.random() * p.sum()))
            korgan[i] = True
            mos = did[u, i] / did[u].max()
            xarid = rng.random() < 0.15 + 0.35 * mos
            baho = 0
            if xarid and rng.random() < 0.3:
                baho = int(np.clip(round(2.4 + 2.2 * mos + 0.3 * np.log(sifat[i])
                                         + rng.normal(0, 0.7)), 1, 5))
            qatorlar.append((u, i, t, int(xarid), baho))
    df = pd.DataFrame(qatorlar, columns=["user", "item", "vaqt", "xarid", "baho"])
    items = pd.DataFrame({"item": np.arange(n_item), "kategoriya": np.array(katlar)[kat],
                          "sub": sub, "narx": narx, "chiqish": chiqish,
                          "tavsif": tavsif})
    return df, items


def bolish(df, usul="vaqt", ulush=0.2, seed=0, chegara=150.0):
    """'vaqt' - har foydalanuvchining oxirgi 20% i test; 'tasodifiy'; 'global'."""
    df = df.sort_values(["user", "vaqt"]).reset_index(drop=True)
    if usul == "global":
        tarix = df[df["vaqt"] < chegara].groupby("user").size()
        yetarli = df["user"].map(tarix).fillna(0).to_numpy() >= 3
        test = (df["vaqt"] >= chegara).to_numpy() & yetarli
        return df[df["vaqt"] < chegara].copy(), df[test].copy()
    n = df.groupby("user")["item"].transform("size").to_numpy()
    if usul == "tasodifiy":
        kalit = np.random.default_rng(seed).random(len(df))
        tartib = (df.assign(k=kalit).groupby("user")["k"]
                  .rank(method="first").to_numpy() - 1)
    else:
        tartib = df.groupby("user").cumcount().to_numpy()
    n_test = np.maximum(1, np.round(ulush * n)).astype(int)
    test = (tartib >= n - n_test) & (n >= 5)
    return df[~test].copy(), df[test].copy()


def matritsa(qism, n_user, n_item):
    return sparse.csr_matrix((np.ones(len(qism)), (qism["user"], qism["item"])),
                             shape=(n_user, n_item))


def metrikalar(top, T):
    """top: (n, k) tavsiya indekslari; T: (n, n_item) test to'plami (bool)."""
    k = top.shape[1]
    rel = np.take_along_axis(T, top, axis=1)
    n_rel = T.sum(1)
    pozitsiya = np.arange(1, k + 1)
    ap = (np.cumsum(rel, 1) / pozitsiya * rel).sum(1) / np.minimum(n_rel, k)
    chegirma = 1 / np.log2(pozitsiya + 1)
    idcg = np.array([chegirma[:min(r, k)].sum() for r in n_rel])
    return {"precision": rel.sum(1) / k, "recall": rel.sum(1) / n_rel,
            "hit": rel.any(1).astype(float), "MAP": ap,
            "nDCG": (rel * chegirma).sum(1) / idcg}


def baholash(ball, X_train, test, users, k=10):
    """Train da ko'rilgan mahsulotlar tavsiya qilinmaydi; har foydalanuvchi metrikasi."""
    ball = np.array(ball, dtype=float)
    ball[X_train[users].toarray() > 0] = -np.inf
    top = np.argsort(-ball, axis=1, kind="stable")[:, :k]
    qator = pd.Series(np.arange(len(users)), index=users)
    tt = test[test["user"].isin(users)]
    T = np.zeros(ball.shape, dtype=bool)
    T[qator[tt["user"]].to_numpy(), tt["item"].to_numpy()] = True
    return metrikalar(top, T), top


def kosinus_ustun(X):
    """Mahsulotlar orasidagi kosinus o'xshashlik: ustunlarni normallab, X^T X."""
    norma = np.sqrt(np.asarray(X.multiply(X).sum(0)).ravel())
    Xn = X @ sparse.diags(1 / np.maximum(norma, 1e-12))
    S = (Xn.T @ Xn).toarray()
    np.fill_diagonal(S, 0)
    return S


def eng_yaqin(S, k):
    """Har qatorda faqat eng katta k ta qiymat qoladi."""
    S = S.copy()
    kesish = np.argpartition(-S, k, axis=1)[:, k:]
    np.put_along_axis(S, kesish, 0, axis=1)
    return S


def main() -> None:
    df, items = dokon()
    n_user, n_item = 2000, len(items)
    tr, te = bolish(df, "vaqt")
    X = matritsa(tr, n_user, n_item)
    users = np.sort(te["user"].unique())
    Xu = X[users]

    print("=== 1. Kosinus o'xshashlik noldan (siyrak matritsada) ===")
    S_item = kosinus_ustun(X)
    tekshir = cosine_similarity(X.T[:5])
    np.fill_diagonal(tekshir, 0)
    print(f"  noldan va sklearn bir xilmi (5x5, 1e-12 aniqlikda): "
          f"{np.allclose(S_item[:5, :5], tekshir, atol=1e-12)}")
    print(f"  mahsulot-mahsulot matritsa {S_item.shape}, nolmas ulushi "
          f"{np.mean(S_item > 0):.1%}")
    j = int(np.argmax(np.asarray(X.sum(0)).ravel()))
    qoshni = np.argsort(-S_item[j])[:4]
    print(f"  eng mashhur mahsulot: '{items.at[j, 'tavsif']}'")
    for q in qoshni:
        print(f"    o'xshash {S_item[j, q]:.3f}: '{items.at[q, 'tavsif']}'")

    print("\n=== 2. Usullar ('vaqt' bo'lishi, k=10) ===")
    pop = np.asarray(X.sum(0)).ravel()
    tfidf = TfidfVectorizer().fit_transform(items["tavsif"])
    profil = Xu @ tfidf
    kontent = cosine_similarity(profil, tfidf)
    item_knn = (Xu @ eng_yaqin(S_item, 50)).astype(float)
    Xr = sparse.diags(1 / np.sqrt(np.asarray(X.sum(1)).ravel() + 1e-12)) @ X
    S_user = (Xr[users] @ Xr.T).toarray()
    S_user[np.arange(len(users)), users] = 0          # o'zi bilan o'xshashlik
    user_knn = eng_yaqin(S_user, 50) @ X.toarray()
    usullar = {"tasodifiy": np.random.default_rng(1).random((len(users), n_item)),
               "mashhurlik": np.tile(pop, (len(users), 1)),
               "kontent (TF-IDF)": kontent, "item-kNN": item_knn,
               "user-kNN": user_knn}
    natija = {}
    print(f"  {'usul':<18}" + "".join(f"{k:>10}" for k in
                                     ["precision", "recall", "hit", "MAP", "nDCG"]))
    for nom, b in usullar.items():
        natija[nom], _ = baholash(b, X, te, users)
        print(f"  {nom:<18}" + "".join(f"{v.mean():>10.4f}"
                                        for v in natija[nom].values()))

    print("\n=== 3. Juftlashgan taqqoslash va qaror (nDCG@10) ===")
    nomzod = ["mashhurlik", "kontent (TF-IDF)", "item-kNN", "user-kNN"]  # soddadan
    eng = max(nomzod, key=lambda n: natija[n]["nDCG"].mean())
    print(f"  eng yaxshi: {eng}")
    tanlov = None
    for n in nomzod:
        if n == eng:
            tanlov = tanlov or n
            continue
        d = natija[n]["nDCG"] - natija[eng]["nDCG"]
        se = d.std(ddof=1) / np.sqrt(len(d))
        yomon = d.mean() < -2 * se
        print(f"  {n:<18} - {eng}: {d.mean():+.4f}, SE {se:.4f}, "
              f"sezilarli yomon: {yomon}")
        if tanlov is None and not yomon:
            tanlov = n
    print(f"  qaror (eng yaxshisidan sezilarli yomon bo'lmagan eng sodda): {tanlov}")

    print("\n=== 4. Faollik bo'yicha: tarix qisqa bo'lsa ===")
    tarix = np.asarray(Xu.sum(1)).ravel()
    guruhlar = [(0, 6, "1-5"), (6, 16, "6-15"), (16, 10_000, "16+")]
    print(f"  {'train tarixi':<14} {'foyd.':>6}" + "".join(
        f"{n:>18}" for n in ["mashhurlik", "kontent (TF-IDF)", "item-kNN"]))
    for a, b, nom in guruhlar:
        m = (tarix >= a) & (tarix < b)
        print(f"  {nom:<14} {m.sum():>6}" + "".join(
            f"{natija[n]['nDCG'][m].mean():>18.4f}"
            for n in ["mashhurlik", "kontent (TF-IDF)", "item-kNN"]))

    print("\n=== 5. Tushuntirish: nega shu tavsiya? (item-kNN) ===")
    u = int(np.argmax(tarix == 8))
    tarixi = Xu[u].indices
    _, top = baholash(item_knn[[u]], X, te, users[[u]], k=3)
    print(f"  foydalanuvchi {users[u]}, tarix kategoriyalari: "
          f"{sorted(items.loc[tarixi, 'kategoriya'].value_counts().to_dict().items())}")
    for i in top[0]:
        sabab = tarixi[np.argmax(S_item[i, tarixi])]
        print(f"   tavsiya '{items.at[i, 'tavsif'][:34]}'  <- "
              f"'{items.at[sabab, 'tavsif'][:34]}' ({S_item[i, sabab]:.2f})")
    print("  ⭐ Item-kNN ni tushuntirish oson: 'siz X ni olgansiz'")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Kosinus o'xshashlik noldan (siyrak matritsada) ===
  noldan va sklearn bir xilmi (5x5, 1e-12 aniqlikda): True
  mahsulot-mahsulot matritsa (800, 800), nolmas ulushi 45.8%
  eng mashhur mahsulot: 'asal qahva ziravor oziq arzon brend34 kafolat sifatli'
    o'xshash 0.385: 'qahva asal ziravor oziq arzon brend20 sifatli original'
    o'xshash 0.337: 'qahva asal yog oziq orta brend24 sifatli mashhur'
    o'xshash 0.336: 'shim kurtka sviter kiyim arzon brend22 original qulay'
    o'xshash 0.330: 'makaron un qahva oziq orta brend30 tez yangi'

=== 2. Usullar ('vaqt' bo'lishi, k=10) ===
  usul               precision    recall       hit       MAP      nDCG
  tasodifiy             0.0058    0.0120    0.0536    0.0041    0.0094
  mashhurlik            0.0383    0.0924    0.3203    0.0351    0.0706
  kontent (TF-IDF)      0.0335    0.0643    0.2591    0.0236    0.0524
  item-kNN              0.0508    0.1234    0.3954    0.0501    0.0961
  user-kNN              0.0569    0.1288    0.4211    0.0565    0.1070

=== 3. Juftlashgan taqqoslash va qaror (nDCG@10) ===
  eng yaxshi: user-kNN
  mashhurlik         - user-kNN: -0.0364, SE 0.0036, sezilarli yomon: True
  kontent (TF-IDF)   - user-kNN: -0.0546, SE 0.0042, sezilarli yomon: True
  item-kNN           - user-kNN: -0.0108, SE 0.0028, sezilarli yomon: True
  qaror (eng yaxshisidan sezilarli yomon bo'lmagan eng sodda): user-kNN

=== 4. Faollik bo'yicha: tarix qisqa bo'lsa ===
  train tarixi    foyd.        mashhurlik  kontent (TF-IDF)          item-kNN
  1-5               149            0.0527            0.0124            0.0782
  6-15              916            0.0678            0.0407            0.0940
  16+               799            0.0771            0.0732            0.1019

=== 5. Tushuntirish: nega shu tavsiya? (item-kNN) ===
  foydalanuvchi 5, tarix kategoriyalari: [('bolalar', 1), ('gozallik', 2), ('kitob', 1), ('kiyim', 1), ('sport', 2), ('uy', 1)]
   tavsiya 'losyon sovun niqob gozallik orta b'  <- 'sovun losyon taroq gozallik orta b' 0.45-bob
   tavsiya 'shim kurtka sviter kiyim arzon bre'  <- 'krossovka shapka palto kiyim premi' 0.29-bob
   tavsiya 'suzish butsa gantel sport arzon br'  <- 'krossovka shapka palto kiyim premi' 0.24-bob
  ⭐ Item-kNN ni tushuntirish oson: 'siz X ni olgansiz'

Natija tahlili.

1-bo'lim — noldan yozilgan kosinus (X ustunlarini normallab, Xn^T Xn) sklearn bilan mashina aniqligida mos (1e-12 aniqlikda bir xil). Eng mashhur mahsulotning (asal qahva ziravor) eng yaqin qo'shnilari asosan xuddi shu kichik guruhdan — qahva asal. Lekin uchinchi qo'shni shim kurtka sviter — butunlay boshqa kategoriya. Bu mashhurlik izi: ikkala mahsulot ham juda mashhur, shuning uchun ularni birga ko'rganlar tasodifan ko'p. Kosinus bu effektni qisman kamaytiradi, lekin yo'qotmaydi.

2-bo'lim — beshta usul bitta bo'lishda. Kollaborativ usullar mashhurlikdan yaxshi: item-kNN nDCG 0.0961, user-kNN 0.1070 (mashhurlik 0.0706). Kontent-asosli usul esa mashhurlikdan yomon (0.0524): u faqat tavsif o'xshashligiga qaraydi va mahsulotning mashhur yoki kam uchrashini bilmaydi — dumdagi, kam ko'riladigan, lekin tavsifi o'xshash mahsulotlarni ham yuqoriga chiqaradi.

3-bo'lim — qaror. Eng yaxshisi user-kNN. Item-kNN undan -0.0108 farq qiladi, SE 0.0028 — farq kichik, lekin sezilarli. Qaror qoidasi bo'yicha user-kNN tanlanadi. Amaliyotda item-kNN ning afzalliklari (barqaror, oldindan hisoblanadi, tushuntirish oson) bu kichik farqdan ustun bo'lishi mumkin — lekin bu biznes qarori, uni raqam bilan ochiq aytish kerak: "nDCG da 10% yo'qotamiz, evaziga ...".

4-bo'lim — faollik bo'yicha. Tarix qisqa (1-5 ta) bo'lgan foydalanuvchilarda kontent-asosli usul juda zaif (0.0124): profil 1-2 mahsulotdan tuzilgan. Item-kNN esa hatto qisqa tarixda ham mashhurlikdan yaxshi (0.0782 va 0.0527). Umuman yangi foydalanuvchi (tarix 0) uchun esa CF ham, kontent ham ishlamaydi — 28.5-darsdagi sovuq start muammosi.

5-bo'lim — item-kNN tavsiyasini bitta jumla bilan tushuntirish mumkin: har tavsiya uchun foydalanuvchi tarixidagi eng o'xshash mahsulot va o'xshashlik darajasi. Bu foydalanuvchi ishonchi uchun ham, xatolarni qidirish uchun ham qimmatli.

Misol 4 — Qamrov, xilma-xillik, yangilik va mashhurlik tarafkashligi

python
"""Aniqlikdan tashqari: qamrov, xilma-xillik, yangilik va mashhurlik tarafkashligi."""

import numpy as np
import pandas as pd
from scipy import sparse
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

KATEGORIYA = {
    "elektronika": "telefon smartfon quloqchin zaryadlovchi noutbuk planshet "
                   "sichqoncha klaviatura kolonka televizor kamera soat",
    "kiyim": "koylak shim kurtka poyabzal krossovka shapka sharf palto "
             "kostyum futbolka yubka sviter",
    "kitob": "roman darslik detektiv sher tarix fantastika lugat ertak "
             "biografiya psixologiya biznes dasturlash",
    "oziq": "choy qahva asal yongoq shokolad guruch yog un makaron sharbat "
            "pechene ziravor",
    "gozallik": "krem atir shampun labbuyoq boyoq niqob sovun losyon taroq "
                "lak gel upa",
    "uy": "idish qozon choynak gilam parda yostiq chiroq sochiq pichoq tova "
          "vaza javon",
    "sport": "koptok gantel velosiped raketka forma gilamcha skakalka shlem "
             "sumka butsa trenajyor suzish",
    "bolalar": "oyinchoq konstruktor qogirchoq kolyaska taglik mashinka pazl "
               "albom rangli kiyimcha kitobcha samokat",
}
NARX = ["arzon", "orta", "premium"]
UMUMIY = "sifatli yangi chegirma original mashhur qulay kafolat tez yetkazish".split()


def dokon(seed=0, n_user=2000, n_item=800, kunlar=180):
    """Sintetik onlayn do'kon: yashirin didlar, mashhurlik dumi, yangi mahsulotlar."""
    rng = np.random.default_rng(seed)
    katlar = list(KATEGORIYA)
    n_kat, n_sub = len(katlar), 4
    sub = rng.integers(0, n_kat * n_sub, n_item)          # har mahsulot kichik guruhi
    kat = sub // n_sub
    narx = rng.choice(3, n_item, p=[0.5, 0.35, 0.15])
    sifat = rng.lognormal(0, 1.4, n_item)                  # mashhurlik dumi manbai
    chiqish = np.where(rng.random(n_item) < 0.7, 0,
                       rng.integers(0, kunlar - 20, n_item))
    tavsif = []
    for i in range(n_item):
        sozlar = KATEGORIYA[katlar[kat[i]]].split()
        guruh = sozlar[(sub[i] % n_sub) * 3:(sub[i] % n_sub) * 3 + 3]
        tanlov = list(rng.choice(guruh, 2, replace=False))
        tanlov.append(rng.choice([s for s in sozlar if s not in tanlov]))
        tanlov += [katlar[kat[i]], NARX[narx[i]], f"brend{rng.integers(0, 40)}"]
        tanlov += list(rng.choice(UMUMIY, 2, replace=False))
        tavsif.append(" ".join(tanlov))
    # foydalanuvchi didi: kategoriya x kichik guruh x narx
    kat_pref = rng.dirichlet(np.full(n_kat, 0.3), n_user)
    sub_pref = rng.dirichlet(np.full(n_sub, 0.5), (n_user, n_kat))
    narx_pref = rng.dirichlet(np.full(3, 1.0), n_user)
    did = (kat_pref[:, kat] * sub_pref[:, kat, sub % n_sub]
           * narx_pref[:, narx])                           # (user, item)
    did /= did.sum(1, keepdims=True)
    hafta = np.arange(0, kunlar, 7)
    yosh = np.clip((hafta[:, None] - chiqish[None, :]) / 7, 0, None)
    jalb = sifat * (0.6 + 1.2 * np.exp(-yosh / 6)) * (hafta[:, None] >= chiqish)
    qatorlar = []
    for u in range(n_user):
        n_u = int(np.clip(rng.lognormal(2.8, 0.8), 3, 150))
        vaqtlar = np.sort(rng.uniform(0, kunlar, n_u))
        korgan = np.zeros(n_item, dtype=bool)
        for t in vaqtlar:
            a = jalb[int(t) // 7] * ~korgan
            p = 0.75 * did[u] * a / (did[u] @ a) + 0.25 * a / a.sum()
            i = int(np.searchsorted(np.cumsum(p), rng.random() * p.sum()))
            korgan[i] = True
            mos = did[u, i] / did[u].max()
            xarid = rng.random() < 0.15 + 0.35 * mos
            baho = 0
            if xarid and rng.random() < 0.3:
                baho = int(np.clip(round(2.4 + 2.2 * mos + 0.3 * np.log(sifat[i])
                                         + rng.normal(0, 0.7)), 1, 5))
            qatorlar.append((u, i, t, int(xarid), baho))
    df = pd.DataFrame(qatorlar, columns=["user", "item", "vaqt", "xarid", "baho"])
    items = pd.DataFrame({"item": np.arange(n_item), "kategoriya": np.array(katlar)[kat],
                          "sub": sub, "narx": narx, "chiqish": chiqish,
                          "tavsif": tavsif})
    return df, items


def bolish(df, usul="vaqt", ulush=0.2, seed=0, chegara=150.0):
    """'vaqt' - har foydalanuvchining oxirgi 20% i test; 'tasodifiy'; 'global'."""
    df = df.sort_values(["user", "vaqt"]).reset_index(drop=True)
    if usul == "global":
        tarix = df[df["vaqt"] < chegara].groupby("user").size()
        yetarli = df["user"].map(tarix).fillna(0).to_numpy() >= 3
        test = (df["vaqt"] >= chegara).to_numpy() & yetarli
        return df[df["vaqt"] < chegara].copy(), df[test].copy()
    n = df.groupby("user")["item"].transform("size").to_numpy()
    if usul == "tasodifiy":
        kalit = np.random.default_rng(seed).random(len(df))
        tartib = (df.assign(k=kalit).groupby("user")["k"]
                  .rank(method="first").to_numpy() - 1)
    else:
        tartib = df.groupby("user").cumcount().to_numpy()
    n_test = np.maximum(1, np.round(ulush * n)).astype(int)
    test = (tartib >= n - n_test) & (n >= 5)
    return df[~test].copy(), df[test].copy()


def matritsa(qism, n_user, n_item):
    return sparse.csr_matrix((np.ones(len(qism)), (qism["user"], qism["item"])),
                             shape=(n_user, n_item))


def metrikalar(top, T):
    """top: (n, k) tavsiya indekslari; T: (n, n_item) test to'plami (bool)."""
    k = top.shape[1]
    rel = np.take_along_axis(T, top, axis=1)
    n_rel = T.sum(1)
    pozitsiya = np.arange(1, k + 1)
    ap = (np.cumsum(rel, 1) / pozitsiya * rel).sum(1) / np.minimum(n_rel, k)
    chegirma = 1 / np.log2(pozitsiya + 1)
    idcg = np.array([chegirma[:min(r, k)].sum() for r in n_rel])
    return {"precision": rel.sum(1) / k, "recall": rel.sum(1) / n_rel,
            "hit": rel.any(1).astype(float), "MAP": ap,
            "nDCG": (rel * chegirma).sum(1) / idcg}


def baholash(ball, X_train, test, users, k=10):
    """Train da ko'rilgan mahsulotlar tavsiya qilinmaydi; har foydalanuvchi metrikasi."""
    ball = np.array(ball, dtype=float)
    ball[X_train[users].toarray() > 0] = -np.inf
    top = np.argsort(-ball, axis=1, kind="stable")[:, :k]
    qator = pd.Series(np.arange(len(users)), index=users)
    tt = test[test["user"].isin(users)]
    T = np.zeros(ball.shape, dtype=bool)
    T[qator[tt["user"]].to_numpy(), tt["item"].to_numpy()] = True
    return metrikalar(top, T), top


def user_knn(X, users, k=50):
    Xr = sparse.diags(1 / np.sqrt(np.asarray(X.sum(1)).ravel() + 1e-12)) @ X
    S = (Xr[users] @ Xr.T).toarray()
    S[np.arange(len(users)), users] = 0
    kesish = np.argpartition(-S, k, axis=1)[:, k:]
    np.put_along_axis(S, kesish, 0, axis=1)
    return S @ X.toarray()


def item_knn(X, users, k=50):
    norma = np.sqrt(np.asarray(X.sum(0)).ravel())
    Xn = X @ sparse.diags(1 / np.maximum(norma, 1e-12))
    S = (Xn.T @ Xn).toarray()
    np.fill_diagonal(S, 0)
    kesish = np.argpartition(-S, k, axis=1)[:, k:]
    np.put_along_axis(S, kesish, 0, axis=1)
    return X[users] @ S


def tavsifla(top, pop, kat, n_train_user):
    """Qamrov, ro'yxat ichidagi xilma-xillik, yangilik, mashhurlar ulushi."""
    k = top.shape[1]
    qamrov = len(np.unique(top)) / len(pop)
    turli = (kat[top][:, :, None] != kat[top][:, None, :]).sum((1, 2)) / (k * (k - 1))
    yangilik = -np.log2((pop[top] + 1) / n_train_user).mean()
    bosh = pop >= np.quantile(pop, 0.9)
    return qamrov, turli.mean(), yangilik, bosh[top].mean()


def qayta_ranjir(ball, pop, beta):
    """ball ni foydalanuvchi ichida [0, 1] ga keltirib, mashhurlik uchun jarima."""
    b = ball / np.maximum(ball.max(1, keepdims=True), 1e-12)
    p = np.log1p(pop) / np.log1p(pop).max()
    return b - beta * p


def main() -> None:
    df, items = dokon()
    n_user, n_item = 2000, len(items)
    kat = pd.factorize(items["kategoriya"])[0]
    tr, te = bolish(df, "vaqt")
    X = matritsa(tr, n_user, n_item)
    users = np.sort(te["user"].unique())
    pop = np.asarray(X.sum(0)).ravel()
    bosh = pop >= np.quantile(pop, 0.9)

    print("=== 1. Test ta'sirlarining o'zi qanchalik 'mashhur'? ===")
    print(f"  train: eng mashhur 10% mahsulot ta'sirlarning "
          f"{pop[bosh].sum() / pop.sum():.1%} ini oladi")
    print(f"  test:  shu mahsulotlar test ta'sirlarining "
          f"{bosh[te['item']].mean():.1%} i")

    print("\n=== 2. Aniqlikdan tashqari metrikalar (k=10) ===")
    tfidf = TfidfVectorizer().fit_transform(items["tavsif"])
    usullar = {"tasodifiy": np.random.default_rng(1).random((len(users), n_item)),
               "mashhurlik": np.tile(pop, (len(users), 1)).astype(float),
               "kontent (TF-IDF)": cosine_similarity(X[users] @ tfidf, tfidf),
               "item-kNN": item_knn(X, users), "user-kNN": user_knn(X, users)}
    print(f"  {'usul':<18} {'nDCG':>7} {'qamrov':>7} {'xilma':>6} {'yangilik':>9} "
          f"{'top10% ulushi':>14}")
    natija = {}
    for nom, b in usullar.items():
        natija[nom], top = baholash(b, X, te, users)
        q, x, y, ul = tavsifla(top, pop, kat, n_user)
        print(f"  {nom:<18} {natija[nom]['nDCG'].mean():>7.4f} {q:>7.1%} {x:>6.3f} "
              f"{y:>9.2f} {ul:>14.1%}")
    print(f"  (test ta'sirlarida top10% ulushi: {bosh[te['item']].mean():.1%})")

    print("\n=== 3. Qaysi mahsulotlar topiladi: bosh va dum ===")
    dum_te = te[~bosh[te["item"]]]
    bosh_te = te[bosh[te["item"]]]
    print(f"  {'usul':<18} {'bosh recall':>12} {'dum recall':>11}")
    ub, ud = np.sort(bosh_te["user"].unique()), np.sort(dum_te["user"].unique())
    ib, id_ = np.searchsorted(users, ub), np.searchsorted(users, ud)
    for nom in ["mashhurlik", "kontent (TF-IDF)", "item-kNN", "user-kNN"]:
        r_b, _ = baholash(usullar[nom][ib], X, bosh_te, ub)
        r_d, _ = baholash(usullar[nom][id_], X, dum_te, ud)
        print(f"  {nom:<18} {r_b['recall'].mean():>12.4f} {r_d['recall'].mean():>11.4f}")

    print("\n=== 4. Mashhurlik jarimasi: beta VALIDATSIYADA tanlanadi ===")
    tr2, va = bolish(tr, "vaqt")
    X2 = matritsa(tr2, n_user, n_item)
    u2 = np.sort(va["user"].unique())
    pop2 = np.asarray(X2.sum(0)).ravel()
    b2 = user_knn(X2, u2)
    asos, _ = baholash(qayta_ranjir(b2, pop2, 0.0), X2, va, u2)
    tanlov = 0.0
    print(f"  {'beta':>5} {'val nDCG':>9} {'farq':>8} {'SE':>7} {'qamrov':>7} "
          f"{'top10%':>7}")
    for beta in [0.0, 0.1, 0.2, 0.4, 0.8]:
        r, top = baholash(qayta_ranjir(b2, pop2, beta), X2, va, u2)
        d = r["nDCG"] - asos["nDCG"]
        se = d.std(ddof=1) / np.sqrt(len(d))
        q, _, _, ul = tavsifla(top, pop2, kat, n_user)
        print(f"  {beta:>5.1f} {r['nDCG'].mean():>9.4f} {d.mean():>+8.4f} {se:>7.4f} "
              f"{q:>7.1%} {ul:>7.1%}")
        if d.mean() >= -2 * se:
            tanlov = beta
    print(f"  tanlov: nDCG sezilarli tushmagan eng katta beta = {tanlov}")
    test = {}
    for beta in [0.0, tanlov]:
        r, top = baholash(qayta_ranjir(usullar["user-kNN"], pop, beta), X, te, users)
        q, _, _, ul = tavsifla(top, pop, kat, n_user)
        test[beta] = r["nDCG"]
        print(f"  TEST beta={beta}: nDCG {r['nDCG'].mean():.4f}, qamrov {q:.1%}, "
              f"top10% ulushi {ul:.1%}")
    d = test[tanlov] - test[0.0]
    print(f"  TEST juftlashgan farq: {d.mean():+.4f}, "
          f"SE {d.std(ddof=1) / np.sqrt(len(d)):.4f}")
    print("  ⭐ Aniqlik yagona maqsad emas: qamrov va dum ham o'lchanadi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Test ta'sirlarining o'zi qanchalik 'mashhur'? ===
  train: eng mashhur 10% mahsulot ta'sirlarning 43.7% ini oladi
  test:  shu mahsulotlar test ta'sirlarining 32.0% i

=== 2. Aniqlikdan tashqari metrikalar (k=10) ===
  usul                  nDCG  qamrov  xilma  yangilik  top10% ulushi
  tasodifiy           0.0094  100.0%  0.875      6.59           9.4%
  mashhurlik          0.0706    3.8%  0.829      2.54         100.0%
  kontent (TF-IDF)    0.0524   99.5%  0.168      6.93           5.7%
  item-kNN            0.0961   31.6%  0.818      2.82          95.8%
  user-kNN            0.1070   31.6%  0.749      3.08          90.4%
  (test ta'sirlarida top10% ulushi: 32.0%)

=== 3. Qaysi mahsulotlar topiladi: bosh va dum ===
  usul                bosh recall  dum recall
  mashhurlik               0.2517      0.0000
  kontent (TF-IDF)         0.0357      0.0791
  item-kNN                 0.3148      0.0130
  user-kNN                 0.3170      0.0263

=== 4. Mashhurlik jarimasi: beta VALIDATSIYADA tanlanadi ===
   beta  val nDCG     farq      SE  qamrov  top10%
    0.0    0.0944  +0.0000  0.0000   31.5%   90.6%
    0.1    0.0955  +0.0011  0.0008   41.5%   86.1%
    0.2    0.0947  +0.0003  0.0012   63.4%   73.5%
    0.4    0.0511  -0.0433  0.0029   45.5%   22.3%
    0.8    0.0019  -0.0925  0.0039    4.5%    0.3%
  tanlov: nDCG sezilarli tushmagan eng katta beta = 0.2
  TEST beta=0.0: nDCG 0.1070, qamrov 31.6%, top10% ulushi 90.4%
  TEST beta=0.2: nDCG 0.1070, qamrov 52.1%, top10% ulushi 79.4%
  TEST juftlashgan farq: +0.0000, SE 0.0010
  ⭐ Aniqlik yagona maqsad emas: qamrov va dum ham o'lchanadi

Natija tahlili.

1-bo'lim — boshlang'ich nuqta: eng mashhur 10% mahsulot train ta'sirlarining 43.7% ini, test ta'sirlarining 32.0% ini oladi (test keyinroq — yangi mahsulotlar ko'proq).

2-bo'lim — aniqlikdan tashqari metrikalar. Mashhurlik katalogning atigi 3.8% ini qamraydi va uning 100% tavsiyalari "bosh" dan. Item-kNN ning 95.8% tavsiyalari, user-kNN ning 90.4% i eng mashhur 10% mahsulotlardan — holbuki foydalanuvchilar haqiqatda ularga 32.0% hollardagina murojaat qilgan. Bu mashhurlik tarafkashligi: kollaborativ usullar mashhurlarga yig'iladi, chunki ular hamma bilan "birga ko'rilgan". Kontent-asosli usul teskari: qamrov 99.5%, yangilik eng yuqori (6.93), lekin xilma-xillik juda past (0.168) — ro'yxat deyarli butunlay bitta kategoriyadan.

3-bo'lim — bosh va dum alohida. Mashhurlik dumdagi mahsulotlarni hech qachon topmaydi (dum recall 0.0000). User-kNN dumda 0.0263, item-kNN 0.0130 — boshdagidan o'n barobardan ko'proq past. Dumda eng yaxshisi kontent-asosli usul (0.0791) — bosh bo'yicha eng zaif bo'lishiga qaramay. Usullar bir-birini to'ldiradi: bu gibrid tizimlar g'oyasining asosi.

4-bo'lim — mashhurlik jarimasi. Ball foydalanuvchi ichida [0, 1] ga keltirilib, beta * log(1 + pop) (normallangan) ayriladi. beta validatsiyada tanlandi (train ichidan xuddi shu qoidadagi ikkinchi bo'lish): beta = 0.2 gacha nDCG sezilarli tushmadi (+0.0003, SE 0.0012), beta = 0.4 da esa keskin tushdi (-0.0433). Test da beta = 0.2: nDCG o'zgarmadi (0.1070, juftlashgan farq +0.0000, SE 0.0010), qamrov esa 31.6% dan 52.1% ga oshdi, mashhurlar ulushi 90.4% dan 79.4% ga tushdi. Aniqlikni yo'qotmasdan katalogning qo'shimcha beshdan bir qismi tavsiyalarga kirdi. Bu offline natija — foydalanuvchilarning bunga munosabatini faqat onlayn A/B test ko'rsatadi 27.13-bob.


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

Noto'g'ri fikr To'g'risi
"Bo'sh katak — foydalanuvchiga yoqmagan" Ko'pincha shunchaki ko'rmagan; implicit ma'lumotda salbiy signal yo'q
"RMSE past bo'lsa, tavsiya yaxshi" RMSE faqat baholangan (tanlangan) kataklarda; tavsiya — ko'rilmaganlar orasida ranjirlash
"Tasodifiy bazaviyni yengdik — model yaxshi" Asosiy bazaviy mashhurlik; u tasodifiydan 7 barobar kuchli (2-misol)
"Tasodifiy bo'lish ham yetarli" Kelajak train ga kiradi; mashhurlik natijasi 36% oshib ketdi (2-misol)
"O'rtacha bahosi eng yuqori mahsulot — eng yaxshisi" 2 ta 5 li baho tasodif; silliqlangan o'rtacha kerak
"Kontent-asosli usul doim zaif" Umumiy aniqlikda zaif, lekin dumda eng yaxshi va yangi mahsulot uchun yagona yo'l
"nDCG yuqori — hammasi joyida" Tavsiyalarning 90% i eng mashhur 10% dan bo'lishi mumkin (4-misol)
"Farq 0.01 — demak A yaxshi" Foydalanuvchilar bo'yicha juftlashgan SE bilan tekshiring

6. Keng tarqalgan xatolar va yechimlari

1. Tasodifiy bo'lish

python
test = df.sample(frac=0.2, random_state=0)                           # ⚠️
df = df.sort_values(["user", "vaqt"])                                # ✅ vaqt bo'yicha
test = df[df.groupby("user").cumcount() >= n_u - n_test]

2. Ko'rilgan mahsulotlarni qayta tavsiya qilish

python
top = np.argsort(-ball, axis=1)[:, :10]                              # ⚠️
ball[X_train[users].toarray() > 0] = -np.inf                         # ✅
top = np.argsort(-ball, axis=1, kind="stable")[:, :10]

3. Mashhurlikni kelajakdan hisoblash

python
pop = df["item"].value_counts()                                      # ⚠️ butun jurnal
pop = train["item"].value_counts()                                   # ✅ faqat train

4. Kam bahoga ishonish

python
top = g.sort_values("mean").tail(10)                                 # ⚠️
g["silliq"] = (g["count"] * g["mean"] + m * mu) / (g["count"] + m)   # ✅

5. Faqat aniqlik

python
print(ndcg.mean())                                                   # ⚠️ yolg'iz
print(ndcg.mean(), qamrov, bosh_ulushi)                              # ✅

6. Farq SE siz

python
if ndcg_A.mean() > ndcg_B.mean(): tanla(A)                           # ⚠️
d = ndcg_A - ndcg_B                                                  # ✅ bir xil foydalanuvchilar
sezilarli = abs(d.mean()) > 2 * d.std(ddof=1) / np.sqrt(len(d))

7. Giperparametrni test da tanlash

python
beta = max(betalar, key=lambda b: ndcg_test(b))                      # ⚠️
tr2, va = bolish(train, "vaqt")                                      # ✅ train ichidan validatsiya

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 10-qism (o'tilgan): Vektorlar, skalyar ko'paytma va normalar — kosinus o'xshashlik
  • 11-qism (o'tilgan): Standart xato va farqning sezilarliligi; qisqartirish (silliqlangan o'rtacha)
  • 17.8, 18-qism (o'tilgan): Leakage va vaqt bo'yicha bo'lish — bu yerda tasodifiy bo'lish tuzog'i
  • 23.4-dars (o'tilgan): TF-IDF, siyrak matritsa va kosinus — kontent-asosli tavsiya
  • 27.13-dars (o'tilgan): Onlayn A/B test — offline metrika onlayn natijani kafolatlamaydi
  • 28.5-dars: Matritsa faktorizatsiyasi, implicit feedback uchun ALS, sovuq start
  • 28.11-dars: Sababiy xulosa — tavsiyaning ta'sirini (u bo'lmasa ham olarmidi?) o'lchash

8. Eng yaxshi amaliyotlar

  1. Vazifani top-N ranjirlash deb qo'ying; metrikani biznes savolidan tanlang.

  2. Vaqt bo'yicha bo'ling; train da ko'rilganlarni tavsiyadan chiqaring.

  3. Mashhurlikni har doim bazaviy qiling — test boshlanishigacha bo'lgan ma'lumotdan.

  4. Bir nechta metrika: nDCG@k, recall@k, hit rate — va qamrov, mashhurlar ulushi.

  5. Taqqoslashni foydalanuvchilar bo'yicha juftlashgan SE bilan qiling.

  6. Sodda usuldan boshlang: mashhurlik → item-kNN → murakkab model.

  7. Segmentlar bo'yicha tekshiring: yangi/faol foydalanuvchilar, bosh/dum mahsulotlar.

  8. Giperparametrlarni validatsiyada tanlang; yakuniy so'z — onlayn A/B test.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # implicit ma'lumotda bo'sh katak nimani bildiradi?
2.  # top = [4, 2, 8], T = {2} - precision@3, recall@3, hit@3?
3.  # xuddi shu misolda nDCG@3?
4.  # T = {4, 8}, top = [4, 2, 8] - AP@3?
5.  # mashhurlik ro'yxatining qamrovi (800 mahsulot, k=10)?
6.  # tasodifiy bo'lish odatda natijani qanday o'zgartiradi?
7.  # item-kNN da ikkita juda mashhur mahsulot nega "o'xshash" chiqadi?
8.  # kontent-asosli usulning xilma-xilligi nega past?
9.  # user-kNN ni yangi foydalanuvchiga qo'llash mumkinmi?
10. # 5 ta baho bilan 4.9 va 300 ta baho bilan 4.6 - qaysi biri yuqori?
11. # SE qaysi birlik bo'yicha olinadi?
12. # mashhurlik jarimasi beta oshsa, qamrov va nDCG?
Javoblar
  1. Noma'lum — ko'pincha "ko'rmagan", "yoqmadi" emas
  2. precision@3 = 1/3, recall@3 = 1, hit@3 = 1
  3. 1 / log2(3) = 0.631 (IDCG = 1)
  4. (1/1 + 2/3) / 2 = 0.833
  5. 10 / 800 = 1.25% — hammaga bir xil ro'yxat
  6. Oshirib ko'rsatadi — kelajak train ga kiradi
  7. Ularni birga ko'rgan odamlar tasodifan ko'p — mashhurlik izi
  8. Profilga eng o'xshash mahsulotlar — o'sha kategoriyadan
  9. Yo'q — o'xshashlikni hisoblash uchun tarix kerak (sovuq start)
  10. Silliqlangan o'rtachada odatda 300 bahoniki; 5 ta baho ishonchsiz
  11. Foydalanuvchilar bo'yicha — ular mustaqil birlik
  12. Qamrov avval oshadi, nDCG bir nuqtagacha saqlanadi, keyin keskin tushadi

Vazifa 2: Xatolarni tuzating

python
1.  train, test = train_test_split(df, test_size=0.2)

2.  pop = df["item"].value_counts().index[:10]

3.  top = np.argsort(-ball, axis=1)[:, :10]      # train mahsulotlari ham bor

4.  eng_yaxshi = g.sort_values("mean").tail(5)   # 2-3 ta baho bilan

5.  print("item-kNN yaxshi", ndcg_knn.mean() > ndcg_pop.mean())
Javoblar
python
1.  df = df.sort_values(["user", "vaqt"])
    test = df.groupby("user").tail(n_test)      # har foydalanuvchining oxirgilari

2.  pop = train["item"].value_counts().index[:10]

3.  ball[X_train[users].toarray() > 0] = -np.inf
    top = np.argsort(-ball, axis=1, kind="stable")[:, :10]

4.  g["silliq"] = (g["count"] * g["mean"] + m * mu) / (g["count"] + m)
    eng_yaxshi = g.sort_values("silliq").tail(5)

5.  d = ndcg_knn - ndcg_pop
    print(d.mean(), d.std(ddof=1) / np.sqrt(len(d)))

Vazifa 3: Ma'lumot

Modellang (1-misol asosida):

  1. dokon() da sifat ning lognormal sigmasini 0.7 va 2.0 qiling — uzun dum va mashhurlik bazaviysi nDCG si qanday o'zgaradi?
  2. Foydalanuvchilarning mashhurlikka qarab tanlash ulushini (25%) 0% va 60% qiling — mashhurlik va item-kNN farqi qanday o'zgaradi?
  3. Silliqlangan o'rtachada m ni 1, 10, 50 qilib, top-5 ro'yxat qanday o'zgarishini ko'ring
  4. Faqat xaridlarni (ko'rishsiz) ta'sir deb olib, zichlik va uzun dumni qayta hisoblang

Vazifa 4: Bo'lish

Modellang (2-misol asosida):

  1. Test ulushini 10%, 20%, 40% qiling — nDCG@10 va hit@10 qanday o'zgaradi va nega?
  2. k = 5, 10, 20 uchun precision@k va recall@k ni hisoblang; ular qarama-qarshi yo'nalishda harakatlanadimi?
  3. Global chegara bo'lishida item-kNN va mashhurlikni solishtiring — "vaqt" bo'lishidagi tartib saqlanadimi?
  4. Vaqt oynasidagi mashhurlik uchun oyna uzunligini 7, 14, 30, 60 kun qilib, juftlashgan SE bilan solishtiring

Vazifa 5: kNN

Modellang (3-misol asosida):

  1. item-kNN da qo'shnilar sonini K = 10, 50, 200, hammasi qiling — natijani validatsiyada tanlang
  2. Kosinus o'rniga "shrinkage" qo'shing: S_ij * n_ij / (n_ij + 20) (n_ij — umumiy foydalanuvchilar) — mashhurlik izi kamayadimi?
  3. item-kNN va kontent ballarini alfa * knn + (1 - alfa) * kontent qilib aralashtiring (ikkalasini ham foydalanuvchi ichida normallang) — dum recall va umumiy nDCG
  4. Foydalanuvchi faolligi bo'yicha segmentlarda (1-5, 6-15, 16+) user-kNN va item-kNN ni juftlashgan SE bilan solishtiring

Vazifa 6: Mashhurlik tarafkashligi

Modellang (4-misol asosida):

  1. Item-kNN uchun ham mashhurlik jarimasini qo'llang va beta ni validatsiyada tanlang
  2. Xilma-xillikni kategoriya o'rniga TF-IDF kosinus o'xshashligi bilan hisoblang (1 - o'rtacha o'xshashlik)
  3. MMR (maximal marginal relevance) qayta ranjirlashni yozing: har qadamda lambda * ball - (1 - lambda) * max o'xshashlik (tanlanganlar bilan)
  4. Qamrov va nDCG uchun "Pareto chizig'i" ni jadval qilib chiqaring

Vazifa 7: O'ylash

Onlayn do'kon mahsulot menejeri: "Yangi tavsiya modelimiz recall@10 bo'yicha mashhurlikdan 40% yaxshi. Uni ertaga barcha foydalanuvchilarga chiqaramiz." Siz baholash hisobotini ko'rdingiz: ma'lumot tasodifiy bo'lingan, metrika faqat recall@10, taqqoslash faqat "tasodifiy tavsiya" va yangi model bilan. Nima deysiz?

Javob

Qisqa javob: hozircha "40%" raqamiga ishonib bo'lmaydi — uni to'g'ri dizayn bilan qayta o'lchash, keyin kichik A/B test bilan chiqarish kerak.

1. Bo'lish. Tasodifiy bo'lishda train ga kelajak kiradi. 2-misolda aynan bir xil mashhurlik usuli tasodifiy bo'lishda nDCG@10 0.0959, vaqt bo'yicha 0.0706 berdi — 36% farq faqat bo'lish tufayli. Model kelajakdagi o'zaro ta'sirlardan foydalansa, uning ustunligi bundan ham ko'proq oshib ko'rinishi mumkin. Birinchi qadam: har foydalanuvchining oxirgi ta'sirlarini (yoki global sana chegarasidan keyingisini) test qilib, ikkala usulni qayta o'lchash.

2. Bazaviy. Mashhurlik to'g'ri hisoblanganmi — faqat train dan, ko'rilganlar chiqarilgan holda? 3-misolda mashhurlik kontent-asosli usuldan yaxshi chiqdi; noto'g'ri sozlangan bazaviy har qanday modelni yaxshi ko'rsatadi.

3. Juftlashgan taqqoslash. Bir xil foydalanuvchilarda farq va SE: d = recall_yangi - recall_pop, SE = std(d) / sqrt(n). Farq 2 SE dan katta bo'lmasa — "40%" shovqin bo'lishi mumkin.

4. Boshqa metrikalar. nDCG@10 (tartib), qamrov va mashhurlar ulushi. 4-misolda kollaborativ usullar tavsiyalarining 90% dan ortig'i eng mashhur 10% mahsulotdan edi — model "qayta tartiblangan mashhurlik ro'yxati" bo'lishi mumkin; unda onlayn foyda kichik bo'ladi.

5. Segmentlar. Yangi foydalanuvchilar (qisqa tarix) va dum mahsulotlar alohida — 3-misolda qisqa tarixda kontent-asosli usul deyarli ishlamadi.

6. Chiqarish. Offline metrika onlayn natijani kafolatlamaydi: tavsiya foydalanuvchi xulqini o'zgartiradi. Kichik trafik ulushida A/B test 27.13-bob, oldindan belgilangan asosiy metrika (masalan, sessiyadagi xaridlar) va guardrail lar (qaytarishlar, sahifa tezligi) bilan.

Menejerga javob: "Modelni chiqarishga qarshi emasman, lekin '40%' raqami tasodifiy bo'lishdan kelgan — u oshirib ko'rsatilgan bo'lishi aniq. Bir kunda vaqt bo'yicha bo'lish va mashhurlik bazaviysi bilan qayta o'lchayman, farqni SE bilan beraman. Farq saqlansa, 10% trafikda A/B test boshlaymiz; ikki hafta ichida onlayn natija bo'ladi."

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


Xulosa

Bu darsda tavsiya vazifasini qo'yish, oddiy usullarni noldan qurish va ularni halol baholashni o'rgandik.

Eng muhim uch fikr:

  1. Tavsiya — ranjirlash vazifasi va uning ma'lumoti o'ziga xos. Foydalanuvchi-mahsulot matritsasi deyarli bo'sh (1-misolda 97.19%), bo'sh katak "yoqmadi" emas, baholar kam (7.4%) va tanlangan (MNAR), mashhurlik esa juda notekis — eng mashhur 10% mahsulot ta'sirlarning 41.5% ini oladi. Shuning uchun asosiy vazifa — ko'rilmagan mahsulotlar orasidan top-N ro'yxatni tartiblash, asosiy bazaviy — mashhurlik.

  2. Baholash dizayni natijani belgilaydi. Bir xil mashhurlik usuli tasodifiy bo'lishda nDCG@10 0.0959, vaqt bo'yicha bo'lishda 0.0706 berdi. Vaqt bo'yicha bo'lish, train da ko'rilganlarni chiqarish, noldan yozilgan precision/recall/hit/MAP/nDCG va foydalanuvchilar bo'yicha juftlashgan SE bilan user-kNN (0.1070) item-kNN dan (0.0961) kichik, lekin sezilarli farq bilan yaxshi chiqdi; kontent-asosli usul (0.0524) esa mashhurlikni ham yengolmadi.

  3. Aniqlik yagona o'lchov emas. Kollaborativ usullar tavsiyalarining 90-96% i eng mashhur 10% mahsulotlardan edi, holbuki foydalanuvchilar ularga 32% hollarda murojaat qilgan; dumdagi mahsulotlarni kontent-asosli usul yaxshiroq topdi. Validatsiyada tanlangan mashhurlik jarimasi (beta = 0.2) nDCG ni saqlab, qamrovni 31.6% dan 52.1% ga oshirdi.

Keyingi darsda Matritsa faktorizatsiyasi va implicit feedback: foydalanuvchi va mahsulotlarni yashirin (latent) faktorlar bilan ifodalash — explicit baholar uchun bias li SGD, implicit ma'lumot uchun "ishonch" g'oyasiga asoslangan ALS, juftlik bo'yicha ranjirlash (BPR), sovuq start va ikki bosqichli tavsiya tizimi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
28.4-dars: Tavsiya tizimlari asoslari — IlmHamroh