{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":31254,"databundleVersionId":3103714,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom tqdm.notebook import tqdm\nnp.random.seed(42)\n\nimport matplotlib.pyplot as plt\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:30:33.340932Z","iopub.execute_input":"2025-12-24T12:30:33.341221Z","iopub.status.idle":"2025-12-24T12:30:33.345292Z","shell.execute_reply.started":"2025-12-24T12:30:33.341197Z","shell.execute_reply":"2025-12-24T12:30:33.344525Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"###  SRC","metadata":{}},{"cell_type":"code","source":"def map12(pred_items, target_item):\n    \"\"\"\n    pred_items : список з 12 рекомендованих item_id (в порядку від 1 до 12)\n    target_item : правильний item_id\n    \"\"\"\n    score = 0.0\n    for k in range(1, 13):\n        if pred_items[k-1] == target_item:\n            score = 1.0 / k\n            break\n    return score\n\n\ndef map12_score(preds, targets):\n    total_score = 0\n    for p, t in zip(preds, targets):\n        if t in p:\n            # Знаходимо позицію (1-based index)\n            rank = np.where(p == t)[0][0] + 1\n            total_score += 1 / rank\n    return total_score / len(targets)\n\ndef plot_history(history):\n    plt.figure(figsize=(12, 5))\n\n    # Графік Loss (Втрат)\n    plt.subplot(1, 2, 1)\n    plt.plot(history.history['loss'], label='Train Loss', marker='o')\n    plt.plot(history.history['val_loss'], label='Val Loss', marker='o')\n    plt.title('Model Loss')\n    plt.xlabel('Epoch')\n    plt.ylabel('Loss')\n    plt.legend()\n    plt.grid(True)\n\n    # Графік Accuracy (Точності)\n    plt.subplot(1, 2, 2)\n    plt.plot(history.history['accuracy'], label='Train Acc', marker='o')\n    plt.plot(history.history['val_accuracy'], label='Val Acc', marker='o')\n    plt.title('Model Accuracy')\n    plt.xlabel('Epoch')\n    plt.ylabel('Accuracy')\n    plt.legend()\n    plt.grid(True)\n\n    plt.tight_layout()\n    plt.show()\n\n\ndef model_predict_last_item(history):\n    return history[-1]\n\ndef model_predict_last_12_item(history):\n    return history[-12:][::-1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:30:33.347107Z","iopub.execute_input":"2025-12-24T12:30:33.347338Z","iopub.status.idle":"2025-12-24T12:30:33.358711Z","shell.execute_reply.started":"2025-12-24T12:30:33.347318Z","shell.execute_reply":"2025-12-24T12:30:33.358042Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training","metadata":{}},{"cell_type":"code","source":"transactions_train = pd.read_csv('/kaggle/input/h-and-m-personalized-fashion-recommendations/transactions_train.csv',\n                                usecols=['t_dat', 'customer_id', 'article_id'])\n\nunique_transactions = transactions_train.drop_duplicates(subset=['t_dat', 'customer_id', 'article_id'])\ndel transactions_train\npurchase_counts = unique_transactions.groupby(\"customer_id\")[\"article_id\"].count()\n\ntop_users = purchase_counts.sort_values(ascending=False).head(10000).index.tolist()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:30:33.360192Z","iopub.execute_input":"2025-12-24T12:30:33.360428Z","iopub.status.idle":"2025-12-24T12:31:27.376565Z","shell.execute_reply.started":"2025-12-24T12:30:33.360409Z","shell.execute_reply":"2025-12-24T12:31:27.375952Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"unique_transactions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:27.377433Z","iopub.execute_input":"2025-12-24T12:31:27.377667Z","iopub.status.idle":"2025-12-24T12:31:27.385862Z","shell.execute_reply.started":"2025-12-24T12:31:27.377646Z","shell.execute_reply":"2025-12-24T12:31:27.385339Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"articles_df = pd.read_csv('/kaggle/input/h-and-m-personalized-fashion-recommendations/articles.csv',\n                         usecols=['article_id', 'product_group_name', 'product_type_name', 'colour_group_code'])\narticles_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:27.386635Z","iopub.execute_input":"2025-12-24T12:31:27.386841Z","iopub.status.idle":"2025-12-24T12:31:27.803518Z","shell.execute_reply.started":"2025-12-24T12:31:27.386822Z","shell.execute_reply":"2025-12-24T12:31:27.802933Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_top = unique_transactions[unique_transactions.customer_id.isin(top_users)].reset_index(drop=True)\n\ndf_top = df_top.merge(articles_df, how='left', on=['article_id'], validate='m:1', )\n\ndef encode_column(df, column_name):\n    unique_values = df[column_name].unique()\n    \n    val2idx = {val: i + 1 for i, val in enumerate(unique_values)}\n    idx2val = {i + 1: val for i, val in enumerate(unique_values)}\n    \n    encoded_series = df[column_name].map(val2idx)\n    \n    vocab_size = len(val2idx) + 1\n    \n    return encoded_series, val2idx, idx2val, vocab_size\n\ndf_top['article'], art2idx, idx2art, article_vocab_size = encode_column(df_top, 'article_id')\ndf_top['cust'], cust2idx, idx2cust, customer_vocab_size = encode_column(df_top, 'customer_id')\ndf_top['product_type'], prod_type2idx, idx2prod_type, prod_type_vocab_size = encode_column(df_top, 'product_type_name')\ndf_top['product_group'], prod_group2idx, idx2prod_group, prod_group_vocab_size = encode_column(df_top, 'product_group_name')\ndf_top['colour_group'], color2idx, idx2color, color_vocab_size = encode_column(df_top, 'colour_group_code')\n\ndf_top = df_top.drop(columns=['customer_id', 'article_id', 'product_type_name', 'product_group_name'], errors='ignore')\ndf_top","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:27.804311Z","iopub.execute_input":"2025-12-24T12:31:27.804512Z","iopub.status.idle":"2025-12-24T12:31:32.140235Z","shell.execute_reply.started":"2025-12-24T12:31:27.804492Z","shell.execute_reply":"2025-12-24T12:31:32.139421Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"top_users = np.array(top_users)\ntop_users_le = pd.Series(top_users).map(cust2idx).values\ntop_users_le[:2]\n\nnp.random.shuffle(top_users_le)\n\ntrain_size = int(0.9 * len(top_users_le))\n\ntrain_users = set(top_users_le[:train_size])\ntest_users  = set(top_users_le[train_size:])\n\ntrain_df = df_top[df_top['cust'].isin(train_users)].copy()\ntest_df  = df_top[df_top['cust'].isin(test_users)].copy()\n\nprint(train_df.shape, test_df.shape)\n\ntrain_df = train_df.sort_values(['cust', 't_dat'])\ntest_df  = test_df.sort_values(['cust', 't_dat'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:32.141299Z","iopub.execute_input":"2025-12-24T12:31:32.141611Z","iopub.status.idle":"2025-12-24T12:31:32.786386Z","shell.execute_reply.started":"2025-12-24T12:31:32.141586Z","shell.execute_reply":"2025-12-24T12:31:32.785593Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_cust_info = pd.read_csv('/kaggle/input/h-and-m-personalized-fashion-recommendations/customers.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T10:52:52.118201Z","iopub.execute_input":"2025-12-24T10:52:52.118812Z","iopub.status.idle":"2025-12-24T10:52:57.860460Z","shell.execute_reply.started":"2025-12-24T10:52:52.118782Z","shell.execute_reply":"2025-12-24T10:52:57.859864Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_cust_info","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T10:57:50.538055Z","iopub.execute_input":"2025-12-24T10:57:50.538838Z","iopub.status.idle":"2025-12-24T10:57:50.550454Z","shell.execute_reply.started":"2025-12-24T10:57:50.538806Z","shell.execute_reply":"2025-12-24T10:57:50.549793Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_cust_info['customer_id_mapped'] = df_cust_info['customer_id'].map(cust2idx)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T10:56:56.282823Z","iopub.execute_input":"2025-12-24T10:56:56.283632Z","iopub.status.idle":"2025-12-24T10:56:56.479092Z","shell.execute_reply.started":"2025-12-24T10:56:56.283578Z","shell.execute_reply":"2025-12-24T10:56:56.478380Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train = df_cust_info[df_cust_info[\"customer_id_mapped\"].isin(train_users)]\ndf_test  = df_cust_info[df_cust_info[\"customer_id_mapped\"].isin(test_users)]\n\ncat_cols = [\"Active\", \"club_member_status\", \"fashion_news_frequency\"]\n\nnum_cols = [\"age\"]\n\nfor col in cat_cols:\n    print(f\"\\n=== {col}: TRAIN ===\")\n    display(df_train[col].value_counts(normalize=True).round(3))\n\n    print(f\"\\n=== {col}: TEST ===\")\n    display(df_test[col].value_counts(normalize=True).round(3))\n\n\nprint(df_train[num_cols].describe())\nprint(df_test[num_cols].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T10:59:18.545121Z","iopub.execute_input":"2025-12-24T10:59:18.545871Z","iopub.status.idle":"2025-12-24T10:59:18.761962Z","shell.execute_reply.started":"2025-12-24T10:59:18.545842Z","shell.execute_reply":"2025-12-24T10:59:18.761359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MAX_LEN = 50\ndef prepare_multi_input_data(df, max_len, is_train=True, max_examples_per_user=30):\n    # Групуємо дані по юзерах один раз\n    # Порядок у списках відповідатиме t_dat, якщо df відсортовано\n    grouped = df.sort_values(['cust', 't_dat']).groupby('cust')\n    \n    X_art, X_type, X_group, X_col = [], [], [], []\n    y = []\n\n    for cust_id, group in tqdm(grouped, desc=\"Processing Users\"):\n        # Перетворюємо колонки групи в масиви для швидкості\n        articles = group['article'].values\n        types = group['product_type'].values\n        groups = group['product_group'].values\n        colors = group['colour_group'].values\n        \n        if len(articles) < 2:\n            continue\n            \n        if is_train:\n            # Логіка \"свіжості\": беремо лише хвіст історії для створення прикладів\n            # Нам потрібно (max_examples + 1) елементів, щоб отримати max_examples пар (X, y)\n            start_pos = max(1, len(articles) - max_examples_per_user)\n            \n            for i in range(start_pos, len(articles)):\n                # Історія — все до поточного індексу i\n                X_art.append(articles[:i][-max_len:])\n                X_type.append(types[:i][-max_len:])\n                X_group.append(groups[:i][-max_len:])\n                X_col.append(colors[:i][-max_len:])\n                # Ціль — елемент на індексі i\n                y.append(articles[i])\n        else:\n            # Для тесту/валідації — тільки один (найсвіжіший) приклад на юзера\n            X_art.append(articles[:-1][-max_len:])\n            X_type.append(types[:-1][-max_len:])\n            X_group.append(groups[:-1][-max_len:])\n            X_col.append(colors[:-1][-max_len:])\n            y.append(articles[-1])\n\n    # Спільні параметри паддінгу\n    pad_cfg = {'maxlen': max_len, 'padding': 'pre', 'dtype': 'int32', 'value': 0}\n    \n    X_dict = {\n        'input_art': pad_sequences(X_art, **pad_cfg),\n        'input_type': pad_sequences(X_type, **pad_cfg),\n        'input_group': pad_sequences(X_group, **pad_cfg),\n        'input_col': pad_sequences(X_col, **pad_cfg)\n    }\n    \n    return X_dict, np.array(y, dtype='int32')\n\n# Виклик прямо з DataFrame\nX_train, y_train = prepare_multi_input_data(train_df, MAX_LEN, is_train=True)\nX_test, y_test = prepare_multi_input_data(test_df, MAX_LEN, is_train=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:32.788837Z","iopub.execute_input":"2025-12-24T12:31:32.789185Z","iopub.status.idle":"2025-12-24T12:31:36.974295Z","shell.execute_reply.started":"2025-12-24T12:31:32.789163Z","shell.execute_reply":"2025-12-24T12:31:36.973482Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\n\nBATCH_SIZE = 1024*4\ndef get_dataloader(X, y, batch_size=1024, shuffle=True):\n    ds = tf.data.Dataset.from_tensor_slices((X, y))\n    if shuffle:\n        ds = ds.shuffle(100000)\n    return ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\ntrain_loader = get_dataloader(X_train, y_train, batch_size=BATCH_SIZE)\ntest_loader = get_dataloader(X_test, y_test, batch_size=BATCH_SIZE, shuffle=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:36.975309Z","iopub.execute_input":"2025-12-24T12:31:36.975624Z","iopub.status.idle":"2025-12-24T12:31:38.134828Z","shell.execute_reply.started":"2025-12-24T12:31:36.975594Z","shell.execute_reply":"2025-12-24T12:31:38.134117Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# next(iter(train_loader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:38.135785Z","iopub.execute_input":"2025-12-24T12:31:38.136163Z","iopub.status.idle":"2025-12-24T12:31:38.139337Z","shell.execute_reply.started":"2025-12-24T12:31:38.136127Z","shell.execute_reply":"2025-12-24T12:31:38.138558Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras.layers import Input, Embedding, LSTM, Dense, Dropout, BatchNormalization, Concatenate\nfrom tensorflow.keras.models import Model\n\ndef build_multi_input_model():\n    # 1. Визначаємо входи (назви мають збігатися з ключами у нашому X_dict)\n    input_art = Input(shape=(MAX_LEN,), name='input_art')\n    input_type = Input(shape=(MAX_LEN,), name='input_type')\n    input_group = Input(shape=(MAX_LEN,), name='input_group')\n    input_col = Input(shape=(MAX_LEN,), name='input_col')\n\n    # 2. Embedding шари для кожної фічі\n    # Для артикулів залишаємо більше векторів (64), для категорій — менше (16-32)\n    emb_art = Embedding(article_vocab_size, 64, name='emb_art')(input_art)\n    emb_type = Embedding(prod_type_vocab_size, 8, name='emb_type')(input_type)\n    emb_group = Embedding(prod_group_vocab_size, 8, name='emb_group')(input_group)\n    emb_col = Embedding(color_vocab_size, 8, name='emb_col')(input_col)\n\n    # 3. Конкатенація (об'єднуємо всі ознаки в один вектор для кожного кроку часу)\n    # Тепер на кожному з 50 кроків LSTM бачитиме вектор розмірністю 64+16+16+16 = 112\n    merged = Concatenate()([emb_art, emb_type, emb_group, emb_col])\n\n    # 4. Рекурентна частина\n    lstm_out = LSTM(128, dropout=0.4)(merged)\n    \n    # 5. Повнозв'язні шари\n    x = BatchNormalization()(lstm_out)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    \n    # 6. Вихідний шар (передбачаємо тільки ID наступного артикула)\n    output = Dense(article_vocab_size, activation='softmax', name='output')(x)\n\n    # Збираємо модель\n    model = Model(\n        inputs=[input_art, input_type, input_group, input_col], \n        outputs=output\n    )\n\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=0.001, use_ema=True),\n        loss='sparse_categorical_crossentropy',\n        metrics=['accuracy']\n    )\n    \n    return model\n\n# Створюємо модель\n# strategy = tf.distribute.MirroredStrategy()\n# with strategy.scope():\nmodel = build_multi_input_model() # Твоя функція для Functional API\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:38.140294Z","iopub.execute_input":"2025-12-24T12:31:38.140732Z","iopub.status.idle":"2025-12-24T12:31:39.680673Z","shell.execute_reply.started":"2025-12-24T12:31:38.140695Z","shell.execute_reply":"2025-12-24T12:31:39.680140Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Налаштування EarlyStopping, щоб не чекати всі 20 епох, якщо модель перестане вчитися\nearly_stop = tf.keras.callbacks.EarlyStopping(\n    monitor='val_loss', \n    patience=3,           # Чекаємо 3 епохи без покращень\n    restore_best_weights=True # Зберігаємо найкращу версію\n)\n\n# Навчання на розширеному (augmented) датасеті\n# Якщо використовуєш DataLoader (рекомендую):\nhistory = model.fit(\n    train_loader, # DataLoader сам розбереться з іменами входів\n    validation_data=test_loader,\n    epochs=20,\n    callbacks=[early_stop]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:31:39.681515Z","iopub.execute_input":"2025-12-24T12:31:39.681787Z","iopub.status.idle":"2025-12-24T12:39:57.634182Z","shell.execute_reply.started":"2025-12-24T12:31:39.681756Z","shell.execute_reply":"2025-12-24T12:39:57.633312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.save('hm_lstm_multi_input_model_use_ema.keras')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:39:57.636145Z","iopub.execute_input":"2025-12-24T12:39:57.636410Z","iopub.status.idle":"2025-12-24T12:39:59.375430Z","shell.execute_reply.started":"2025-12-24T12:39:57.636386Z","shell.execute_reply":"2025-12-24T12:39:59.374650Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import tensorflow as tf\n# from tensorflow.keras.models import load_model\n\n# # Вказуємо шлях до другого GPU (індексація починається з 0)\n\n# loaded_model = load_model('hm_lstm_multi_input_model_use_ema.keras')\n\nloaded_model = model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:41:12.129559Z","iopub.execute_input":"2025-12-24T12:41:12.130110Z","iopub.status.idle":"2025-12-24T12:41:12.133524Z","shell.execute_reply.started":"2025-12-24T12:41:12.130080Z","shell.execute_reply":"2025-12-24T12:41:12.132939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom tqdm import tqdm\nimport tensorflow as tf\n\nall_top_12 = []\n\n# Ітеруємося по батчах з DataLoader\nfor x_batch, _ in tqdm(test_loader, desc=\"Predicting batches\"):\n    # Отримуємо ймовірності тільки для поточного батчу (наприклад, 256 юзерів)\n    batch_preds = loaded_model.predict_on_batch(x_batch)\n    \n    # Одразу знаходимо ТОП-12 індексів (найбільші ймовірності)\n    # np.argsort дає індекси від найменшого до найбільшого\n    # [:, -12:] бере 12 найкращих, [:, ::-1] розвертає їх у правильному порядку\n    batch_top_12 = np.argsort(batch_preds, axis=1)[:, -12:][:, ::-1]\n    \n    # Зберігаємо лише ці 12 чисел на кожного юзера\n    all_top_12.append(batch_top_12)\n\n# 3. Об'єднуємо всі батчі в один масив\ntop_12_indices = np.vstack(all_top_12)\n\nprint(f\"\\nГотово! Отримано прогнозів для {top_12_indices.shape[0]} користувачів.\")\nprint(f\"Формат виходу: {top_12_indices.shape}\") # Має бути (кількість_юзерів, 12)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:41:12.318344Z","iopub.execute_input":"2025-12-24T12:41:12.319043Z","iopub.status.idle":"2025-12-24T12:42:34.445948Z","shell.execute_reply.started":"2025-12-24T12:41:12.319014Z","shell.execute_reply":"2025-12-24T12:42:34.443019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_history(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:42:34.451518Z","iopub.execute_input":"2025-12-24T12:42:34.451765Z","iopub.status.idle":"2025-12-24T12:42:34.941226Z","shell.execute_reply.started":"2025-12-24T12:42:34.451743Z","shell.execute_reply":"2025-12-24T12:42:34.940565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"lstm_score = map12_score(top_12_indices, y_test)\nprint(f\"LSTM Validation MAP@12: {lstm_score:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:42:34.942086Z","iopub.execute_input":"2025-12-24T12:42:34.942287Z","iopub.status.idle":"2025-12-24T12:42:35.067214Z","shell.execute_reply.started":"2025-12-24T12:42:34.942268Z","shell.execute_reply":"2025-12-24T12:42:35.066606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"map_scores = []\n\nnum_samples = len(X_test['input_art'])\n\nfor i in tqdm(range(num_samples), desc=\"Calculating MAP@12\"):\n    # Отримуємо історію покупок (тільки артикули) для Baseline\n    # Ми беремо i-й рядок з масиву 'input_art'\n    history_art = X_test['input_art'][i]\n    target = y_test[i]\n    \n    # Baseline функція: вона зазвичай дивиться на історію артикулів\n    # і вибирає останні 12 унікальних значень\n    pred = model_predict_last_12_item(history_art)\n    \n    # Рахуємо MAP@12 для цього юзера\n    map_scores.append(map12(pred, target))\n\nbaseline_score = np.mean(map_scores)\nprint(f\"\\nBaseline MAP@12 (Last 12 items): {baseline_score:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T12:42:35.068402Z","iopub.execute_input":"2025-12-24T12:42:35.068649Z","iopub.status.idle":"2025-12-24T12:42:35.156420Z","shell.execute_reply.started":"2025-12-24T12:42:35.068628Z","shell.execute_reply":"2025-12-24T12:42:35.155671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}