{"cells":[{"cell_type":"markdown","metadata":{},"source":"# OTTO вЂ“ baseline recommender\n\nSession-based rec. Plan:\n1. Stream train.jsonl, build co-visitation counts\n2. For each test session pick the last N aids as seeds\n3. Look up co-visited items, aggregate weighted scores\n4. Top-20 per type + popularity fallback"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"import json\nimport pandas as pd\nimport numpy as np\nfrom collections import defaultdict\nimport gc\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\n\nDATA = Path('/kaggle/input/otto-recommender-system')\nprint('files:', list(DATA.iterdir()))"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# peek at a few train sessions to understand structure\nwith open(DATA / 'train.jsonl') as f:\n    for i, line in enumerate(f):\n        row = json.loads(line)\n        print(f\"session {row['session']}: {len(row['events'])} events\")\n        print('first event:', row['events'][0])\n        if i >= 2:\n            break"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# type mapping from string to int\nTYPE2ID = {'clicks': 0, 'carts': 1, 'orders': 2}"},{"cell_type":"markdown","metadata":{},"source":"## Step 1 вЂ“ build co-visitation matrix\n\nStream train.jsonl session by session (never load it all into RAM).\nFor each session: take last 30 events, generate all pairs within 24h,\nincrement covisit[a][b] counter."},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"SEED_N    = 30          # events per session to use\nTIME_WIN  = 24 * 3600   # seconds вЂ” pairs outside this are skipped\nMAX_KEEP  = 40          # top-N neighbors to keep per aid\n\ncovisit_click = defaultdict(lambda: defaultdict(int))\ncovisit_buy   = defaultdict(lambda: defaultdict(int))  # carts+orders transitions\n\nclick_pop  = defaultdict(int)\nbuy_pop    = defaultdict(int)\norder_pop  = defaultdict(int)\n\nn_sessions = 0\n\nwith open(DATA / 'train.jsonl') as f:\n    for line in tqdm(f, desc='building covisit'):\n        row = json.loads(line)\n        evs = row['events']\n        n_sessions += 1\n\n        # count popularity\n        for ev in evs:\n            t = TYPE2ID[ev['type']] if isinstance(ev['type'], str) else ev['type']\n            if t == 0:\n                click_pop[ev['aid']] += 1\n            if t in (1, 2):\n                buy_pop[ev['aid']] += 1\n            if t == 2:\n                order_pop[ev['aid']] += 1\n\n        # take last SEED_N events\n        evs = evs[-SEED_N:]\n        aids = [e['aid'] for e in evs]\n        tss  = [e['ts']  for e in evs]\n        types = [TYPE2ID[e['type']] if isinstance(e['type'], str) else e['type'] for e in evs]\n\n        for i in range(len(aids)):\n            for j in range(len(aids)):\n                if i == j:\n                    continue\n                if abs(tss[i] - tss[j]) > TIME_WIN:\n                    continue\n\n                covisit_click[aids[i]][aids[j]] += 1\n\n                # for buy matrix only care about pairs where j is cart or order\n                if types[j] in (1, 2):\n                    covisit_buy[aids[i]][aids[j]] += 1\n\nprint(f'done. sessions: {n_sessions:,}')\nprint(f'covisit_click aids: {len(covisit_click):,}')\nprint(f'covisit_buy   aids: {len(covisit_buy):,}')"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# trim to top-MAX_KEEP neighbors per aid (saves RAM)\nfor aid in list(covisit_click.keys()):\n    top = sorted(covisit_click[aid].items(), key=lambda x: -x[1])[:MAX_KEEP]\n    covisit_click[aid] = dict(top)\n\nfor aid in list(covisit_buy.keys()):\n    top = sorted(covisit_buy[aid].items(), key=lambda x: -x[1])[:MAX_KEEP]\n    covisit_buy[aid] = dict(top)\n\ngc.collect()\nprint('trimmed ok')"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# global popularity lists вЂ” used as fallback for cold sessions\ntop20_clicks = [aid for aid, _ in sorted(click_pop.items(),  key=lambda x: -x[1])[:20]]\ntop20_buys   = [aid for aid, _ in sorted(buy_pop.items(),    key=lambda x: -x[1])[:20]]\ntop20_orders = [aid for aid, _ in sorted(order_pop.items(),  key=lambda x: -x[1])[:20]]\n\nprint('top5 by clicks:', top20_clicks[:5])\nprint('top5 by orders:', top20_orders[:5])"},{"cell_type":"markdown","metadata":{},"source":"## Step 2 вЂ“ generate predictions for test sessions"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def recommend(session_aids, covisit, fallback, topk=20):\n    seeds = session_aids[-SEED_N:]\n    seen  = set(seeds)\n    scores = defaultdict(float)\n\n    for rank, aid in enumerate(reversed(seeds)):\n        w = 1.0 / (rank + 1)   # more recent = higher weight\n        for nbr, cnt in covisit.get(aid, {}).items():\n            if nbr not in seen:\n                scores[nbr] += w * cnt\n\n    recs = [aid for aid, _ in sorted(scores.items(), key=lambda x: -x[1])[:topk]]\n\n    # pad with popular if needed\n    for pop in fallback:\n        if len(recs) >= topk:\n            break\n        if pop not in seen and pop not in recs:\n            recs.append(pop)\n\n    return recs"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"rows = []\n\nwith open(DATA / 'test.jsonl') as f:\n    for line in tqdm(f, desc='predicting'):\n        row = json.loads(line)\n        sess = row['session']\n        evs  = row['events']\n\n        all_aids   = [e['aid'] for e in evs]\n        buy_aids   = [e['aid'] for e in evs if (TYPE2ID[e['type']] if isinstance(e['type'], str) else e['type']) in (1, 2)]\n        order_aids = [e['aid'] for e in evs if (TYPE2ID[e['type']] if isinstance(e['type'], str) else e['type']) == 2]\n\n        # clicks: use all events as context\n        recs_c = recommend(all_aids, covisit_click, top20_clicks)\n\n        # carts: prefer buy events as seeds\n        seed_cart = buy_aids if len(buy_aids) >= 3 else all_aids\n        recs_cart = recommend(seed_cart, covisit_buy, top20_buys)\n\n        # orders: prefer order events as seeds\n        seed_order = order_aids if len(order_aids) >= 2 else (buy_aids if len(buy_aids) >= 2 else all_aids)\n        recs_order = recommend(seed_order, covisit_buy, top20_orders)\n\n        rows.append({'session_type': f'{sess}_clicks', 'labels': ' '.join(map(str, recs_c))})\n        rows.append({'session_type': f'{sess}_carts',  'labels': ' '.join(map(str, recs_cart))})\n        rows.append({'session_type': f'{sess}_orders', 'labels': ' '.join(map(str, recs_order))})\n\nprint(f'generated {len(rows):,} rows')"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"sub = pd.DataFrame(rows)\nprint(sub.shape)\nsub.head(6)"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# sanity: check against sample_submission\nsample = pd.read_csv(DATA / 'sample_submission.csv')\nprint('expected rows:', len(sample))\nprint('our rows:     ', len(sub))\n\nmissing = set(sample['session_type']) - set(sub['session_type'])\nextra   = set(sub['session_type'])    - set(sample['session_type'])\nprint(f'missing: {len(missing)}, extra: {len(extra)}')"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# label count distribution\nlc = sub['labels'].str.split().str.len()\nprint(lc.describe())\nprint(f'sessions with <20 labels: {(lc < 20).sum()} ({(lc < 20).mean()*100:.1f}%)')"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"sub.to_csv('submission.csv', index=False)\nprint('saved submission.csv')"}],"cells_metadata":{},"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.0"}},"nbformat":4,"nbformat_minor":4}