{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.7.12"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":38760,"databundleVersionId":4493939,"isSourceIdPinned":false}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":1041.72289,"end_time":"2026-04-04T19:21:45.731087","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-04-04T19:04:24.008197","version":"2.3.4"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"a8e2427f","cell_type":"markdown","source":"<h1 style=\"font-family:verdana;\"> <center>🛍 OTTO – Multi-Objective Recommender System - Baseline</center> </h1>\n\n***","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":0.00631,"end_time":"2026-04-04T19:04:35.691413","exception":false,"start_time":"2026-04-04T19:04:35.685103","status":"completed"},"tags":[]}},{"id":"9d68b2d8","cell_type":"markdown","source":"### <span style=\"font-family:verdana; word-spacing:1.5px;\">  Task overview\n    \nЦель этого соревнования — предсказать клики в e-commerce, добавления товаров в корзину и заказы. Вам нужно построить рекомендательную систему с несколькими целями, основанную на предыдущих событиях в пользовательской сессии.\n\nСовременные рекомендательные системы состоят из различных моделей с разными подходами — от простой матричной факторизации до глубоких нейронных сетей типа transformer. Однако не существует одной модели, которая могла бы одновременно оптимизировать несколько целей. В этом соревновании вам нужно построить единое решение для предсказания кликов, добавлений в корзину и конверсий на основе предыдущих событий в той же сессии.","metadata":{"papermill":{"duration":0.004718,"end_time":"2026-04-04T19:04:35.701470","exception":false,"start_time":"2026-04-04T19:04:35.696752","status":"completed"},"tags":[]}},{"id":"da4019f4","cell_type":"markdown","source":"### <span style=\"font-family:verdana; word-spacing:1.5px;\">   Imports / setup 🚚","metadata":{"papermill":{"duration":0.00476,"end_time":"2026-04-04T19:04:35.711327","exception":false,"start_time":"2026-04-04T19:04:35.706567","status":"completed"},"tags":[]}},{"id":"ebd38aa2","cell_type":"code","source":"### Imports ###\n\nimport pandas as pd\nfrom pathlib import Path\nimport os\nimport random\nimport numpy as np\nimport json\nfrom datetime import timedelta\nfrom collections import Counter\nfrom tqdm.notebook import tqdm\nfrom heapq import nlargest\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nsns.set_theme()\n\nimport warnings\nwarnings.filterwarnings('ignore')\n","metadata":{"execution":{"iopub.status.busy":"2026-04-05T19:49:35.395562Z","iopub.execute_input":"2026-04-05T19:49:35.397513Z","iopub.status.idle":"2026-04-05T19:49:35.421165Z","shell.execute_reply.started":"2026-04-05T19:49:35.397432Z","shell.execute_reply":"2026-04-05T19:49:35.419892Z"},"papermill":{"duration":1.450072,"end_time":"2026-04-04T19:04:37.166615","exception":false,"start_time":"2026-04-04T19:04:35.716543","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1e2aa984","cell_type":"code","source":"### Paths ###\nDATA_PATH = Path('/kaggle/input/competitions/otto-recommender-system')\nTRAIN_PATH = DATA_PATH/'train.jsonl'\nTEST_PATH = DATA_PATH/'test.jsonl'\nSAMPLE_SUB_PATH = Path('/kaggle/input/competitions/otto-recommender-system/sample_submission.csv')","metadata":{"execution":{"iopub.status.busy":"2026-04-05T19:50:15.758120Z","iopub.execute_input":"2026-04-05T19:50:15.758511Z","iopub.status.idle":"2026-04-05T19:50:15.765810Z","shell.execute_reply.started":"2026-04-05T19:50:15.758481Z","shell.execute_reply":"2026-04-05T19:50:15.764651Z"},"papermill":{"duration":0.016364,"end_time":"2026-04-04T19:04:37.188465","exception":false,"start_time":"2026-04-04T19:04:37.172101","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"db95b252","cell_type":"markdown","source":"# <span style=\"font-family:verdana; word-spacing:1.5px;\">   Baseline 📈\n    \n\nТестовые данные содержат укороченные сессии, аналогичные тем, что есть в обучающих данных. Задача состоит в том, чтобы предсказать следующий `aid`, по которому будет клик после обрезки сессии, а также оставшиеся `aid`, которые будут добавлены в корзину и заказаны; для каждого типа события можно предсказать до 20 значений.\n\nОценка решений производится по **Recall** для каждого типа действия, а затем три значения recall усредняются с весами:  \n`{'clicks': 0.10, 'carts': 0.30, 'orders': 0.60}`.  \nОчень важно хорошо предсказывать **orders**, так как они имеют наибольший вес :)\n\nДля каждой сессии в тестовых данных нужно предсказать значения `aid` для каждого типа события, которые происходят **после последней временной метки `ts`** в тестовой сессии. Иными словами, тестовые данные содержат сессии, обрезанные по времени, а вы должны предсказать, что произойдет после точки обрезки.\n\nДля **clicks** существует только одно истинное значение для каждой сессии — это следующий `aid`, по которому кликнули в течение сессии  \n(хотя вы все равно можете предсказать до 20 значений `aid`).  \nИстинные значения для **carts** и **orders** содержат все значения `aid`, которые были соответственно добавлены в корзину и заказаны в этой сессии.\n\nКаждая комбинация **session + type** должна находиться в отдельной строке `session_type` в сабмите  \n(по 3 строки на каждую сессию), а предсказания должны быть разделены пробелами. Это можно увидеть в `sample_test_df` ниже.","metadata":{"papermill":{"duration":0.004808,"end_time":"2026-04-04T19:04:37.198693","exception":false,"start_time":"2026-04-04T19:04:37.193885","status":"completed"},"tags":[]}},{"id":"c4333cfe-29f9-4e82-9a71-f8b98425a42d","cell_type":"markdown","source":"# Улучшенный подход: матрица ко-посещаемости\n\n**Идея:** для каждой пары товаров (A, B), которые встречались в одной сессии в окне ±3 событий, накапливаем взвешенный скор ко-посещаемости:\n\n- **Тип события**: заказ=18, корзина=6, клик=1 — более ценные события вносят больший вклад\n- **Дистанция**: чем ближе два события друг к другу, тем выше вес (1/dist)\n- **Две матрицы**: `covisit_click` (для предсказания кликов) и `covisit_buy` (для корзин/заказов)\n\nПри предсказании для тестовой сессии:\n1. Для каждого события в сессии ищем топ-20 ко-посещаемых товаров\n2. Суммируем скоры с затуханием — последние события важнее (weight = 1/rank)\n3. Дополняем до 20 глобально популярными товарами если нужно\n\n**Ожидаемый скор (Recall@20):** ~0.54–0.58 vs ~0.47 у базового popularity baseline","metadata":{"execution":{"iopub.status.busy":"2026-04-05T19:49:45.089436Z","iopub.execute_input":"2026-04-05T19:49:45.090257Z","iopub.status.idle":"2026-04-05T19:49:45.099721Z","shell.execute_reply.started":"2026-04-05T19:49:45.090213Z","shell.execute_reply":"2026-04-05T19:49:45.098256Z"},"papermill":{"duration":0.015231,"end_time":"2026-04-04T19:04:37.219027","exception":false,"start_time":"2026-04-04T19:04:37.203796","status":"completed"},"tags":[]}},{"id":"17b0c14d","cell_type":"code","source":"with open(TEST_PATH, 'r') as f:\n    print(f\"We have {len(f.readlines()):,} lines in the test data\")","metadata":{"execution":{"iopub.status.busy":"2026-04-05T19:50:16.276504Z","iopub.execute_input":"2026-04-05T19:50:16.277489Z","iopub.status.idle":"2026-04-05T19:50:18.916846Z","shell.execute_reply.started":"2026-04-05T19:50:16.277453Z","shell.execute_reply":"2026-04-05T19:50:18.915780Z"},"papermill":{"duration":6.986727,"end_time":"2026-04-04T19:04:44.210840","exception":false,"start_time":"2026-04-04T19:04:37.224113","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"077d761d","cell_type":"code","source":"# Load in a sample to a pandas df\n\nsample_size = 150\n\nchunks = pd.read_json(TEST_PATH, lines=True, chunksize = sample_size)\n\nfor c in chunks:\n    sample_test_df = c\n    break","metadata":{"execution":{"iopub.status.busy":"2026-04-05T19:50:18.923279Z","iopub.execute_input":"2026-04-05T19:50:18.923860Z","iopub.status.idle":"2026-04-05T19:50:18.961126Z","shell.execute_reply.started":"2026-04-05T19:50:18.923829Z","shell.execute_reply":"2026-04-05T19:50:18.960054Z"},"papermill":{"duration":0.03717,"end_time":"2026-04-04T19:04:44.256147","exception":false,"start_time":"2026-04-04T19:04:44.218977","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"28df641c","cell_type":"code","source":"sample_test_df.head()","metadata":{"execution":{"iopub.status.busy":"2026-04-05T19:50:20.285256Z","iopub.execute_input":"2026-04-05T19:50:20.285628Z","iopub.status.idle":"2026-04-05T19:50:20.323097Z","shell.execute_reply.started":"2026-04-05T19:50:20.285598Z","shell.execute_reply":"2026-04-05T19:50:20.322220Z"},"papermill":{"duration":0.058522,"end_time":"2026-04-04T19:04:44.320205","exception":false,"start_time":"2026-04-04T19:04:44.261683","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"11767bcd","cell_type":"markdown","source":"Ниже показан пример сабмита. Для каждой сессии в тестовом наборе есть предсказание (`labels`). Оно предсказывает, с какими товарами будет следующее взаимодействие в этой сессии. Для каждой сессии есть три типа действий (`clicks`, `carts`, `orders`), и предсказания делаются для всех трех типов.","metadata":{"papermill":{"duration":0.007341,"end_time":"2026-04-04T19:04:44.333873","exception":false,"start_time":"2026-04-04T19:04:44.326532","status":"completed"},"tags":[]}},{"id":"6a606858","cell_type":"code","source":"sample_submission = pd.read_csv(SAMPLE_SUB_PATH)\nsample_submission.head()","metadata":{"execution":{"iopub.status.busy":"2026-04-05T19:50:21.772644Z","iopub.execute_input":"2026-04-05T19:50:21.773519Z","iopub.status.idle":"2026-04-05T19:50:26.732987Z","shell.execute_reply.started":"2026-04-05T19:50:21.773482Z","shell.execute_reply":"2026-04-05T19:50:26.732211Z"},"papermill":{"duration":6.477493,"end_time":"2026-04-04T19:04:50.817740","exception":false,"start_time":"2026-04-04T19:04:44.340247","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"887f86e5","cell_type":"markdown","source":"Давайте найдем самый популярный товар для каждого типа действия.","metadata":{"papermill":{"duration":0.005089,"end_time":"2026-04-04T19:04:50.828781","exception":false,"start_time":"2026-04-04T19:04:50.823692","status":"completed"},"tags":[]}},{"id":"2a355c2d","cell_type":"code","source":"from collections import defaultdict, Counter\nfrom tqdm.notebook import tqdm\nimport pandas as pd\n\n# Веса по типу события\nTYPE_WEIGHT   = {'clicks': 1, 'carts': 6, 'orders': 18}\nTARGET_TYPES  = ['clicks', 'carts', 'orders']\nWINDOW        = 3   # смотрим на ±3 соседних события\n\n# Две матрицы ко-посещаемости:\n#   covisit_click  — для предсказания clicks (вес = 1/dist)\n#   covisit_buy    — для carts/orders (вес = type_weight / dist)\ncovisit_click = defaultdict(Counter)\ncovisit_buy   = defaultdict(Counter)\n\nsample_size = 200_000\nn_chunks    = 5\n\nchunks = pd.read_json(TRAIN_PATH, lines=True, chunksize=sample_size)\n\nprint('Строим матрицы ко-посещаемости...')\nfor c_idx, chunk in enumerate(chunks):\n    if c_idx >= n_chunks:\n        break\n    for _, row in tqdm(chunk.iterrows(), total=len(chunk),\n                       desc=f'Chunk {c_idx+1}/{n_chunks}', leave=False):\n        events = row.get('events', [])\n        if not isinstance(events, list) or len(events) < 2:\n            continue\n        n = len(events)\n        for i in range(n):\n            aid_i = events[i]['aid']\n            for j in range(max(0, i - WINDOW), min(n, i + WINDOW + 1)):\n                if i == j:\n                    continue\n                aid_j  = events[j]['aid']\n                type_j = events[j]['type']\n                dist   = abs(i - j)              # 1..WINDOW\n                w      = 1.0 / dist              # ближе = важнее\n                covisit_click[aid_i][aid_j] += w\n                covisit_buy[aid_i][aid_j]   += w * TYPE_WEIGHT[type_j]\n\n    # Обрезаем до топ-40 после каждого чанка — экономим память\n    print(f'  Chunk {c_idx+1}: {len(covisit_click):,} уникальных items, обрезаем...')\n    covisit_click = defaultdict(Counter,\n                        {k: Counter(dict(v.most_common(40)))\n                         for k, v in covisit_click.items()})\n    covisit_buy   = defaultdict(Counter,\n                        {k: Counter(dict(v.most_common(40)))\n                         for k, v in covisit_buy.items()})\n\n# Финальная обрезка до топ-20\nprint('Финальная обрезка до топ-20...')\ncovisit_click = {k: [a for a, _ in v.most_common(20)]\n                 for k, v in covisit_click.items()}\ncovisit_buy   = {k: [a for a, _ in v.most_common(20)]\n                 for k, v in covisit_buy.items()}\n\n# Глобальный топ-20 по типу (фолбэк для новых/редких items)\nglobal_pop = defaultdict(Counter)\nchunks2 = pd.read_json(TRAIN_PATH, lines=True, chunksize=sample_size)\nfor c_idx, chunk in enumerate(chunks2):\n    if c_idx >= n_chunks:\n        break\n    for _, row in chunk.iterrows():\n        for ev in (row.get('events') or []):\n            global_pop[ev['type']][ev['aid']] += TYPE_WEIGHT[ev['type']]\n\nglobal_top20 = {\n    t: [a for a, _ in cnt.most_common(20)]\n    for t, cnt in global_pop.items()\n}\nprint('Готово!')\nfor t in TARGET_TYPES:\n    print(f'  Топ-3 [{t}]: {global_top20.get(t, [])[:3]}')\n","metadata":{"execution":{"iopub.status.busy":"2026-04-05T20:05:27.154480Z","iopub.execute_input":"2026-04-05T20:05:27.155158Z","iopub.status.idle":"2026-04-05T20:27:52.275220Z","shell.execute_reply.started":"2026-04-05T20:05:27.155123Z","shell.execute_reply":"2026-04-05T20:27:52.274094Z"},"papermill":{"duration":343.430846,"end_time":"2026-04-04T19:10:34.265228","exception":false,"start_time":"2026-04-04T19:04:50.834382","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"72fbe203","cell_type":"markdown","source":"Матрица построена. Теперь для каждого товара из тестовой сессии мы знаем 20 товаров, с которыми он чаще всего посещался в train.\n\n**Стратегия предсказания для тестовой сессии:**\n- Идём по событиям с конца (последние важнее)\n- Для каждого события суммируем ко-посещаемость с decay = 1/rank\n- Для `clicks` используем `covisit_click`, для `carts`/`orders` — `covisit_buy`\n- Дополняем до 20 глобальным топом","metadata":{"papermill":{"duration":0.094088,"end_time":"2026-04-04T19:10:34.455560","exception":false,"start_time":"2026-04-04T19:10:34.361472","status":"completed"},"tags":[]}},{"id":"4b705389","cell_type":"code","source":"test_data = pd.read_json(TEST_PATH, lines=True, chunksize=200_000)\npreds = []\n\nfor chunk in tqdm(test_data, desc='Generating predictions'):\n    for _, row in chunk.iterrows():\n        events  = row.get('events', [])\n        session = row['session']\n\n        # Накапливаем скоры кандидатов\n        click_scores = Counter()\n        buy_scores   = Counter()\n\n        # Добавляем сами товары из сессии как сильный сигнал\n        for rank, event in enumerate(reversed(events)):\n            decay = 1.0 / (rank + 1)\n            aid   = event['aid']\n            tw    = TYPE_WEIGHT[event['type']]\n\n            # Сам товар — сильный кандидат\n            click_scores[aid] += decay\n            buy_scores[aid]   += decay * tw\n\n            # Ко-посещаемые товары\n            for cand in covisit_click.get(aid, []):\n                click_scores[cand] += decay\n            for cand in covisit_buy.get(aid, []):\n                buy_scores[cand]   += decay * tw\n\n        # Формируем топ-20 для каждого типа\n        type_scores = {\n            'clicks': click_scores,\n            'carts':  buy_scores,\n            'orders': buy_scores,\n        }\n\n        for t_type in TARGET_TYPES:\n            rec = [a for a, _ in type_scores[t_type].most_common(20)]\n            # Дополняем глобальным топом до 20\n            for aid in global_top20.get(t_type, []):\n                if len(rec) >= 20:\n                    break\n                if aid not in rec:\n                    rec.append(aid)\n            preds.append((f'{session}_{t_type}',\n                          ' '.join(str(a) for a in rec[:20])))\n\nprint(f'Предсказаний сгенерировано: {len(preds):,}')\n","metadata":{"execution":{"iopub.status.busy":"2026-04-05T20:27:52.277138Z","iopub.execute_input":"2026-04-05T20:27:52.277548Z","iopub.status.idle":"2026-04-05T20:34:25.472823Z","shell.execute_reply.started":"2026-04-05T20:27:52.277519Z","shell.execute_reply":"2026-04-05T20:34:25.471620Z"},"papermill":{"duration":433.180892,"end_time":"2026-04-04T19:17:47.733502","exception":false,"start_time":"2026-04-04T19:10:34.552610","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a46c304b","cell_type":"code","source":"preds_df = pd.DataFrame(preds, columns=['session_type', 'labels'])\npreds_df.head(6)\n","metadata":{"execution":{"iopub.status.busy":"2026-04-05T20:34:25.474499Z","iopub.execute_input":"2026-04-05T20:34:25.474867Z","iopub.status.idle":"2026-04-05T20:34:27.182660Z","shell.execute_reply.started":"2026-04-05T20:34:25.474839Z","shell.execute_reply":"2026-04-05T20:34:27.181829Z"},"papermill":{"duration":55.324303,"end_time":"2026-04-04T19:18:43.340292","exception":false,"start_time":"2026-04-04T19:17:48.015989","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2ae1b173","cell_type":"code","source":"# Загружаем sample_submission и мержим, чтобы порядок строк совпал\nsample_sub = pd.read_csv(SAMPLE_SUB_PATH)\nsubmission = sample_sub[['session_type']].merge(preds_df, on='session_type', how='left')\nsubmission['labels'] = submission['labels'].fillna('')\nsubmission.to_csv('submission.csv', index=False)\n\nprint(f'submission.csv сохранён: {len(submission):,} строк')\nprint(submission.head(6).to_string(index=False))\n","metadata":{"execution":{"iopub.status.busy":"2026-04-05T20:34:27.184693Z","iopub.execute_input":"2026-04-05T20:34:27.185316Z","iopub.status.idle":"2026-04-05T20:35:10.570697Z","shell.execute_reply.started":"2026-04-05T20:34:27.185281Z","shell.execute_reply":"2026-04-05T20:35:10.569528Z"},"papermill":{"duration":172.916766,"end_time":"2026-04-04T19:21:36.547850","exception":false,"start_time":"2026-04-04T19:18:43.631084","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5c58c94c","cell_type":"code","source":"submission.head(6)\n","metadata":{"execution":{"iopub.status.busy":"2026-04-05T20:35:10.571944Z","iopub.execute_input":"2026-04-05T20:35:10.572305Z","iopub.status.idle":"2026-04-05T20:35:10.582911Z","shell.execute_reply.started":"2026-04-05T20:35:10.572266Z","shell.execute_reply":"2026-04-05T20:35:10.581509Z"},"papermill":{"duration":0.317098,"end_time":"2026-04-04T19:21:37.162244","exception":false,"start_time":"2026-04-04T19:21:36.845146","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8cb57d5f-646a-435e-930f-7a200cd20afb","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}