{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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"},"colab":{"provenance":[]},"widgets":{"application/vnd.jupyter.widget-state+json":{"07aa6b9732934db4b441f6350adb17bc":{"model_module":"@jupyter-widgets/controls","model_name":"VBoxModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"VBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"VBoxView","box_style":"","children":["IPY_MODEL_cd688586002549b0acd8758fe5079c4e","IPY_MODEL_3bc90d86f49348019a03ff1b12be3afc","IPY_MODEL_bcc200077c6c4f3c921eb5d58f808a96","IPY_MODEL_2b732e9054f2438897544bfe7b87dc18"],"layout":"IPY_MODEL_2b326651921842059d6037fd8461c111"}},"cd688586002549b0acd8758fe5079c4e":{"model_module":"@jupyter-widgets/controls","model_name":"HTMLModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_feded04992ca4659bc5c49c24b75b9fe","placeholder":"​","style":"IPY_MODEL_67a521454ac04529840dc411f0dc1c60","value":"<center> <img\nsrc=https://www.kaggle.com/static/images/site-logo.png\nalt='Kaggle'> <br> Create an API token from <a\nhref=\"https://www.kaggle.com/settings/account\" target=\"_blank\">your Kaggle\nsettings page</a> and paste it below. <br> </center>"}},"3bc90d86f49348019a03ff1b12be3afc":{"model_module":"@jupyter-widgets/controls","model_name":"PasswordModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"PasswordModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"PasswordView","continuous_update":true,"description":"API Token:","description_tooltip":null,"disabled":false,"layout":"IPY_MODEL_716515caf730409e939167d6419efc43","placeholder":"​","style":"IPY_MODEL_28aee37a2c194239b2199fab0261c9e9","value":""}},"bcc200077c6c4f3c921eb5d58f808a96":{"model_module":"@jupyter-widgets/controls","model_name":"ButtonModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"ButtonModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"ButtonView","button_style":"","description":"Login","disabled":false,"icon":"","layout":"IPY_MODEL_0407b3851d754a52a0fb281d7bec4d96","style":"IPY_MODEL_4ff84ad4e2394c43baa7171184190fe7","tooltip":""}},"2b732e9054f2438897544bfe7b87dc18":{"model_module":"@jupyter-widgets/controls","model_name":"HTMLModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_b1e6657b8a04423a81223579900777ca","placeholder":"​","style":"IPY_MODEL_c188248f3b374dae9690e65d83934b7f","value":"\n<b>Thank You</b></center>"}},"2b326651921842059d6037fd8461c111":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":"center","align_self":null,"border":null,"bottom":null,"display":"flex","flex":null,"flex_flow":"column","grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":"50%"}},"feded04992ca4659bc5c49c24b75b9fe":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"67a521454ac04529840dc411f0dc1c60":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"716515caf730409e939167d6419efc43":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"28aee37a2c194239b2199fab0261c9e9":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"0407b3851d754a52a0fb281d7bec4d96":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"4ff84ad4e2394c43baa7171184190fe7":{"model_module":"@jupyter-widgets/controls","model_name":"ButtonStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"ButtonStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","button_color":null,"font_weight":""}},"b1e6657b8a04423a81223579900777ca":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"c188248f3b374dae9690e65d83934b7f":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}}}},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":3064,"databundleVersionId":44235}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import kagglehub\n# Es posible que debas volver a ejecutar esta celda después de acceder.\nkagglehub.login()\npath = kagglehub.competition_download('acm-sf-chapter-hackathon-big')","metadata":{"id":"fSqbHOM-yv9j","outputId":"53033907-89cc-430d-bfdb-1189bd8e2fab","trusted":true,"execution":{"iopub.status.busy":"2026-05-26T13:59:14.128775Z","iopub.execute_input":"2026-05-26T13:59:14.129283Z","iopub.status.idle":"2026-05-26T13:59:14.776103Z","shell.execute_reply.started":"2026-05-26T13:59:14.12925Z","shell.execute_reply":"2026-05-26T13:59:14.775162Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# POR ESTO:\nfrom collections import defaultdict, Counter\nimport csv\nfrom datetime import datetime\nimport os\n\n# ── Rutas Actualizadas ─────────────────────────────────────────────────────────\n# Usamos la variable 'path' que viene de kagglehub\ntrain_file = os.path.join(path, 'train.csv')\n\n# Carpeta de salida para los datos procesados\nout_buffer_path = '/content/output'\nout_dir = os.path.join(out_buffer_path, 'bert4rec_data')\n\nif not os.path.exists(out_dir):\n    os.makedirs(out_dir, exist_ok=True)\n# ──────────────────────────────────────────────────────────────────────────────\n\ndef parse_datetime(click_time):\n    click_time = click_time.strip().strip('\"')\n    try:\n        return datetime.strptime(click_time, '%Y-%m-%d %H:%M:%S.%f')\n    except ValueError:\n        return datetime.strptime(click_time, '%Y-%m-%d %H:%M:%S')\n\n\ndef normalize_row(row):\n    if not row:\n        return None\n    if len(row) == 1:\n        return next(csv.reader([row[0]], quotechar='\"', skipinitialspace=True))\n    return row\n\n\ndef build_user_sequences(\n    train_file,\n    min_interactions=5,\n    min_item_freq=5\n):\n    user_interactions = defaultdict(list)\n    sku_counter = Counter()\n\n    invalid_rows = 0\n\n    print(f\"\\n[INFO] Leyendo interacciones...\")\n\n    # =========================================================\n    # PRIMERA PASADA:\n    # contar frecuencia global de SKUs\n    # =========================================================\n\n    with open(train_file, 'r', encoding='utf-8') as f:\n\n        reader = csv.reader(\n            f,\n            quotechar='\"',\n            skipinitialspace=True\n        )\n\n        next(reader, None)\n\n        for row in reader:\n\n            row = normalize_row(row)\n\n            if not row or len(row) < 6:\n                invalid_rows += 1\n                continue\n\n            user, sku, category, query, click_time, query_time = row[:6]\n\n            user = user.strip()\n            sku = sku.strip()\n\n            if not user or not sku:\n                invalid_rows += 1\n                continue\n\n            sku_counter[sku] += 1\n\n    print(f\"[INFO] SKUs únicos originales: {len(sku_counter)}\")\n\n    # =========================================================\n    # SKUs válidos\n    # =========================================================\n\n    valid_skus = {\n        sku\n        for sku, freq in sku_counter.items()\n        if freq >= min_item_freq\n    }\n\n    print(\n        f\"[INFO] SKUs filtrados (freq >= {min_item_freq}): \"\n        f\"{len(valid_skus)}\"\n    )\n\n    # =========================================================\n    # SEGUNDA PASADA:\n    # construir secuencias filtradas\n    # =========================================================\n\n    with open(train_file, 'r', encoding='utf-8') as f:\n\n        reader = csv.reader(\n            f,\n            quotechar='\"',\n            skipinitialspace=True\n        )\n\n        next(reader, None)\n\n        for row in reader:\n\n            row = normalize_row(row)\n\n            if not row or len(row) < 6:\n                continue\n\n            user, sku, category, query, click_time, query_time = row[:6]\n\n            user = user.strip()\n            sku = sku.strip()\n\n            # =================================================\n            # IGNORAR ITEMS RAROS\n            # =================================================\n\n            if sku not in valid_skus:\n                continue\n\n            try:\n                t = parse_datetime(click_time)\n\n            except ValueError:\n                continue\n\n            user_interactions[user].append((t, sku))\n\n    # =========================================================\n    # Construir secuencias finales\n    # =========================================================\n\n    sequences = {}\n\n    skipped_users = 0\n\n    for user, interactions in user_interactions.items():\n\n        interactions.sort(key=lambda x: x[0])\n\n        skus = [sku for (_, sku) in interactions]\n\n        # =====================================================\n        # eliminar usuarios demasiado cortos\n        # =====================================================\n\n        if len(skus) < min_interactions:\n            skipped_users += 1\n            continue\n\n        sequences[user] = skus\n\n    print(f\"\\n[INFO] Usuarios válidos: {len(sequences)}\")\n    print(f\"[INFO] Usuarios eliminados: {skipped_users}\")\n\n    if invalid_rows:\n        print(f\"[INFO] Filas inválidas: {invalid_rows}\")\n\n    return sequences\n\n\ndef build_vocab(sequences, out_dir):\n    all_skus = set()\n    for skus in sequences.values():\n        all_skus.update(skus)\n\n    vocab = {sku: idx + 2 for idx, sku in enumerate(sorted(all_skus))}\n    vocab['[PAD]']  = 0\n    vocab['[MASK]'] = 1\n\n    vocab_file = os.path.join(out_dir, 'vocab.csv')\n    with open(vocab_file, 'w', newline='') as f:\n        writer = csv.writer(f)\n        writer.writerow(['sku', 'idx'])\n        for sku, idx in sorted(vocab.items(), key=lambda x: x[1]):\n            writer.writerow([sku, idx])\n\n    print(f\"  vocab.csv    → {len(all_skus)} SKUs únicos (+ PAD y MASK)\")\n    return vocab\n\n\ndef split_sequences(sequences, vocab, out_dir):\n\n    os.makedirs(out_dir, exist_ok=True)\n\n    f_train = os.path.join(out_dir, 'train.csv')\n    f_val   = os.path.join(out_dir, 'val.csv')\n    f_test  = os.path.join(out_dir, 'test.csv')\n\n    header = ['user', 'sequence', 'target']\n\n    with open(f_train, 'w', newline='') as ft, \\\n         open(f_val,   'w', newline='') as fv, \\\n         open(f_test,  'w', newline='') as fs:\n\n        train_writer = csv.writer(ft)\n        val_writer   = csv.writer(fv)\n        test_writer  = csv.writer(fs)\n\n        train_writer.writerow(header)\n        val_writer.writerow(header)\n        test_writer.writerow(header)\n\n        for user, skus in sequences.items():\n\n            indices = [vocab[sku] for sku in skus if sku in vocab]\n\n            if len(indices) < 5:\n                continue\n\n            # ---------- TRAIN ----------\n            # muchas subsecuencias\n            MIN_PREFIX_LEN = 3\n\n            for i in range(MIN_PREFIX_LEN, len(indices) - 2):\n\n                seq = indices[:i]\n                target = indices[i]\n\n                train_writer.writerow([\n                    user,\n                    ' '.join(map(str, seq)),\n                    target\n                ])\n\n            # ---------- VALIDATION ----------\n            val_seq = indices[:-2]\n            val_target = indices[-2]\n\n            val_writer.writerow([\n                user,\n                ' '.join(map(str, val_seq)),\n                val_target\n            ])\n\n            # ---------- TEST ----------\n            test_seq = indices[:-1]\n            test_target = indices[-1]\n\n            test_writer.writerow([\n                user,\n                ' '.join(map(str, test_seq)),\n                test_target\n            ])\n\n    print(\"train.csv generado\")\n    print(\"val.csv generado\")\n    print(\"test.csv generado\")\n\nif __name__ == '__main__':\n    print(f\"Input Kaggle: {train_file}\")\n    print(f\"Output Dir:   {out_dir}\")\n\n    print(\"1. Construyendo secuencias...\")\n    sequences = build_user_sequences(\n      train_file,\n      min_interactions=5,\n      min_item_freq=5\n    )\n\n    print(\"\\n2. Construyendo vocabulario...\")\n    vocab = build_vocab(sequences, out_dir)\n\n    print(\"\\n3. Generando splits...\")\n    split_sequences(sequences, vocab, out_dir)\n\n    print('\\ndone')\n","metadata":{"id":"tnznRUFVzeNN","outputId":"b789c245-2bfc-4b47-e6fa-1a83a4c8be44","trusted":true,"execution":{"iopub.status.busy":"2026-05-26T13:59:14.777859Z","iopub.execute_input":"2026-05-26T13:59:14.778349Z","iopub.status.idle":"2026-05-26T13:59:46.630842Z","shell.execute_reply.started":"2026-05-26T13:59:14.778318Z","shell.execute_reply":"2026-05-26T13:59:46.629715Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport csv\nimport math\nimport random\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport xgboost as xgb\nfrom collections import Counter\n\n# ── Rutas ──────────────────────────────────────────────────────────────────────\ndata_dir = '/content/output/bert4rec_data'\nout_buffer_path = '/content/output'\n\ntrain_file = os.path.join(data_dir, 'train.csv')\nval_file   = os.path.join(data_dir, 'val.csv')\ntest_file  = os.path.join(data_dir, 'test.csv')\nvocab_file = os.path.join(data_dir, 'vocab.csv')\n# ──────────────────────────────────────────────────────────────────────────────\n\n# ── Hiperparámetros ────────────────────────────────────────────────────────────\nMAX_SEQ_LEN   = 10      # longitud máxima de secuencia\nD_MODEL       = 32      # dimensión de embeddings\nN_HEADS       = 2       # cabezas de atención\nN_LAYERS      = 2       # capas transformer\nDROPOUT       = 0.1\nMASK_PROB     = 0.15    # probabilidad de enmascarar un ítem\nBATCH_SIZE    = 128\nLR            = 1e-3\nEPOCHS        = 50\nTOP_K         = 10      # métricas HR@10 y NDCG@10\n# ──────────────────────────────────────────────────────────────────────────────\n\ndef load_vocab(vocab_file):\n    sku2idx = {}\n    idx2sku = {}\n    with open(vocab_file, 'r') as f:\n        reader = csv.reader(f)\n        next(reader)\n        for row in reader:\n            if len(row) < 2: continue\n            sku, idx = row[0], int(row[1])\n            sku2idx[sku] = idx\n            idx2sku[idx] = sku\n    return sku2idx, idx2sku\n\ndef load_sequences(filepath):\n    data = []\n    with open(filepath, 'r') as f:\n        reader = csv.reader(f)\n        next(reader)\n        for row in reader:\n            if len(row) < 3: continue\n            sequence = list(map(int, row[1].strip().split()))\n            target = int(row[2])\n            data.append((sequence, target))\n    return data\n\ndef build_item_popularity(data):\n\n    counter = Counter()\n\n    for seq, target in data:\n\n        counter.update(seq)\n        counter[target] += 1\n\n    return counter\n\nclass Bert4RecDataset(Dataset):\n    def __init__(self, data, max_seq_len, mask_prob, vocab_size, mode='train'):\n        self.data = data\n        self.max_seq_len = max_seq_len\n        self.mask_prob = mask_prob\n        self.vocab_size = vocab_size\n        self.mode = mode\n        self.PAD = 0\n        self.MASK = 1\n\n    def __len__(self):\n        return len(self.data)\n\n    def __getitem__(self, idx):\n        sequence, target = self.data[idx]\n        sequence = sequence[-self.max_seq_len:]\n        if self.mode == 'train':\n            masked_seq, labels = self._mask_sequence(sequence)\n        else:\n            masked_seq = sequence + [self.MASK]\n            masked_seq = masked_seq[-self.max_seq_len:]\n            labels = [self.PAD] * (len(masked_seq) - 1) + [target]\n        pad_len = self.max_seq_len - len(masked_seq)\n        masked_seq = [self.PAD] * pad_len + masked_seq\n        labels = [self.PAD] * pad_len + labels\n        return torch.tensor(masked_seq, dtype=torch.long), torch.tensor(labels, dtype=torch.long)\n\n    def _mask_sequence(self, sequence):\n\n        masked_seq = []\n        labels = []\n\n        for item in sequence:\n\n            if random.random() < self.mask_prob:\n\n                prob = random.random()\n\n                labels.append(item)\n\n                # 80% MASK\n                if prob < 0.8:\n\n                    masked_seq.append(self.MASK)\n\n                # 10% random item\n                elif prob < 0.9:\n\n                    masked_seq.append(\n                        random.randint(2, self.vocab_size - 1)\n                    )\n\n                # 10% original\n                else:\n\n                    masked_seq.append(item)\n\n            else:\n\n                masked_seq.append(item)\n                labels.append(self.PAD)\n\n        return masked_seq, labels\n\nclass PositionalEncoding(nn.Module):\n    def __init__(self, d_model, max_seq_len, dropout):\n        super().__init__()\n        self.dropout = nn.Dropout(dropout)\n        pe = torch.zeros(max_seq_len, d_model)\n        position = torch.arange(0, max_seq_len).unsqueeze(1).float()\n        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))\n        pe[:, 0::2] = torch.sin(position * div_term)\n        pe[:, 1::2] = torch.cos(position * div_term)\n        self.register_buffer('pe', pe.unsqueeze(0))\n\n    def forward(self, x):\n        x = x + self.pe[:, :x.size(1), :]\n        return self.dropout(x)\n\nclass Bert4Rec(nn.Module):\n    def __init__(self, vocab_size, d_model, n_heads, n_layers, max_seq_len, dropout):\n        super().__init__()\n        self.item_embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)\n        self.pos_encoding = PositionalEncoding(d_model, max_seq_len, dropout)\n        encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=n_heads, dim_feedforward=d_model*4, dropout=dropout, batch_first=True)\n        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)\n        self.norm = nn.LayerNorm(d_model)\n        self.fc_out = nn.Linear(d_model, vocab_size)\n\n    def forward(self, x):\n        pad_mask = (x == 0)\n        emb = self.item_embedding(x)\n        emb = self.pos_encoding(emb)\n        out = self.transformer(emb, src_key_padding_mask=pad_mask)\n        return self.fc_out(self.norm(out))\n\ndef train_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    total_loss = 0\n    for seq, lab in loader:\n        seq, lab = seq.to(device), lab.to(device)\n        optimizer.zero_grad()\n        logits = model(seq).view(-1, model.fc_out.out_features)\n        loss = criterion(logits, lab.view(-1))\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    return total_loss / len(loader)\n\ndef evaluate(model, loader, device, top_k=10):\n    model.eval()\n    hits, ndcg, total = 0, 0, 0\n    with torch.no_grad():\n        for seq, lab in loader:\n            seq, lab = seq.to(device), lab.to(device)\n            logits = model(seq)[:, -1, :]\n            targets = lab[:, -1]\n            _, top_items = torch.topk(logits, top_k, dim=-1)\n            for i in range(targets.size(0)):\n                target = targets[i].item()\n                if target == 0: continue\n                total += 1\n                preds = top_items[i].tolist()\n                if target in preds:\n                    hits += 1\n                    ndcg += 1.0 / math.log2(preds.index(target) + 2)\n    return hits/total if total > 0 else 0, ndcg/total if total > 0 else 0\n\ndef build_xgb_dataset(model, data, item_popularity, device, top_k=20):\n    X = []\n    y = []\n    model.eval()\n\n    with torch.no_grad():\n        for sequence, target in data:\n            # 1. Añadir el token [MASK] (1) al final del historial original\n            seq = sequence + [1]\n\n            # 2. Recortar después para asegurar que NUNCA pase de MAX_SEQ_LEN (10)\n            seq = seq[-MAX_SEQ_LEN:]\n\n            # 3. Calcular el padding correcto (ahora pad_len siempre será >= 0)\n            pad_len = MAX_SEQ_LEN - len(seq)\n            seq = [0] * pad_len + seq\n\n            seq_tensor = torch.tensor(seq).unsqueeze(0).to(device)\n            logits = model(seq_tensor)[0, -1]\n\n            # Ignorar PAD y MASK en las predicciones\n            logits[0] = -1e9\n            logits[1] = -1e9\n\n            probs = torch.softmax(logits, dim=-1)\n            top_scores, top_items = torch.topk(probs, top_k)\n\n            top_scores = top_scores.cpu().numpy()\n            top_items = top_items.cpu().numpy()\n\n            # Cada candidato recuperado por BERT se convierte en una fila para XGBoost\n            for idx, item in enumerate(top_items):\n                bert_score = top_scores[idx]\n                rank = idx + 1  # Posición del 1 al top_k\n\n                # Características del candidato\n                X.append([\n                    bert_score,                # Score de BERT\n                    rank,                      # Ranking inicial de BERT\n                    item_popularity[item],     # Popularidad del ítem\n                    len(sequence),             # Historial del usuario\n                    int(item in sequence)      # ¿El usuario ya había interactuado con este ítem?\n                ])\n\n                # Si el candidato de BERT es justo el que el usuario eligió -> 1, si no -> 0\n                if item == target:\n                    y.append(1)\n                else:\n                    y.append(0)\n\n    return np.array(X), np.array(y)\n\nif __name__ == '__main__':\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    sku2idx, idx2sku = load_vocab(vocab_file)\n    vocab_size = len(sku2idx)\n\n    # 1. Cargar Datos\n    train_data = load_sequences(train_file)\n    val_data = load_sequences(val_file)\n    test_data = load_sequences(test_file)\n\n    train_loader = DataLoader(Bert4RecDataset(train_data, MAX_SEQ_LEN, MASK_PROB, vocab_size, 'train'), batch_size=BATCH_SIZE, shuffle=True)\n    val_loader = DataLoader(Bert4RecDataset(val_data, MAX_SEQ_LEN, MASK_PROB, vocab_size, 'eval'), batch_size=BATCH_SIZE)\n\n    # 2. Entrenar BERT4Rec\n    model = Bert4Rec(vocab_size, D_MODEL, N_HEADS, N_LAYERS, MAX_SEQ_LEN, DROPOUT).to(device)\n    optimizer = torch.optim.Adam(model.parameters(), lr=LR)\n    criterion = nn.CrossEntropyLoss(ignore_index=0)\n\n    best_hr = 0\n    for epoch in range(1, EPOCHS + 1):\n        loss = train_epoch(model, train_loader, optimizer, criterion, device)\n        hr, ndcg = evaluate(model, val_loader, device, TOP_K)\n        print(f\"Epoch {epoch:02d} | Loss: {loss:.4f} | Val HR@{TOP_K}: {hr:.4f} | Val NDCG@{TOP_K}: {ndcg:.4f}\")\n        if hr > best_hr:\n            best_hr = hr\n            torch.save(model.state_dict(), os.path.join(out_buffer_path, 'bert4rec_best.pt'))\n\n    # Cargar el mejor modelo de BERT\n    model.load_state_dict(torch.load(os.path.join(out_buffer_path, 'bert4rec_best.pt'), map_location=device))\n\n    # 3. CONSTRUIR DATASET PARA XGBOOST (Usando datos de VAL)\n    print(\"\\n[INFO] Construyendo dataset de XGBoost con datos de Validación...\")\n    item_popularity = build_item_popularity(train_data)\n\n    # Usamos val_data para entrenar XGBoost\n    X_xgb_train, y_xgb_train = build_xgb_dataset(model, val_data, item_popularity, device, top_k=20)\n\n    # 4. ENTRENAR XGBOOST\n    print(\"[INFO] Entrenando XGBoost Ranker/Classifier...\")\n    xgb_model = xgb.XGBClassifier(\n        n_estimators=100,\n        max_depth=5,\n        learning_rate=0.1,\n        objective='binary:logistic',\n        random_state=42\n    )\n    xgb_model.fit(X_xgb_train, y_xgb_train)\n    print(\"[OK] XGBoost entrenado con éxito.\")","metadata":{"id":"WdUCh6B-ysGo","outputId":"5e0f0d72-4881-4c1f-eb77-3e13563e006c","trusted":true,"execution":{"iopub.status.busy":"2026-05-26T13:59:46.632258Z","iopub.execute_input":"2026-05-26T13:59:46.633096Z","execution_failed":"2026-05-26T14:03:15.704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport csv\nimport math\nimport random\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport xgboost as xgb\nfrom collections import Counter\n\n# ── Rutas ──────────────────────────────────────────────────────────────────────\ndata_dir = '/content/output/bert4rec_data'\nout_buffer_path = '/content/output'\n\ntrain_file = os.path.join(data_dir, 'train.csv')\nval_file   = os.path.join(data_dir, 'val.csv')\ntest_file  = os.path.join(data_dir, 'test.csv')\nvocab_file = os.path.join(data_dir, 'vocab.csv')\n# ──────────────────────────────────────────────────────────────────────────────\n\n# ── Hiperparámetros Adaptados ──────────────────────────────────────────────────\nMAX_SEQ_LEN   = 10      # Longitud máxima ideal para tus secuencias cortas\nD_MODEL       = 32      # Dimensión de embeddings compacta\nN_HEADS       = 2       # Cabezas de atención\nN_LAYERS      = 2       # Capas Transformer (suficiente para secuencias de ~7 items)\nDROPOUT       = 0.1\nBATCH_SIZE    = 128\nLR            = 1e-3\nEPOCHS        = 50\nTOP_K         = 10      # Métricas HR@10 y NDCG@10\n# ──────────────────────────────────────────────────────────────────────────────\n\ndef load_vocab(vocab_file):\n    sku2idx = {}\n    idx2sku = {}\n    with open(vocab_file, 'r') as f:\n        reader = csv.reader(f)\n        next(reader)\n        for row in reader:\n            if len(row) < 2: continue\n            sku, idx = row[0], int(row[1])\n            sku2idx[sku] = idx\n            idx2sku[idx] = sku\n    return sku2idx, idx2sku\n\ndef load_sequences(filepath):\n    data = []\n    with open(filepath, 'r') as f:\n        reader = csv.reader(f)\n        next(reader)\n        for row in reader:\n            if len(row) < 3: continue\n            sequence = list(map(int, row[1].strip().split()))\n            target = int(row[2])\n            data.append((sequence, target))\n    return data\n\ndef build_item_popularity(data):\n    counter = Counter()\n    for seq, target in data:\n        counter.update(seq)\n        counter[target] += 1\n    return counter\n\n# ── Dataset para SASRec (Sin Máscaras Aleatorias) ──────────────────────────────\nclass SASRecDataset(Dataset):\n    def __init__(self, data, max_seq_len):\n        self.data = data\n        self.max_seq_len = max_seq_len\n        self.PAD = 0\n\n    def __len__(self):\n        return len(self.data)\n\n    def __getitem__(self, idx):\n        sequence, target = self.data[idx]\n\n        # Truncar la secuencia histórica si supera el límite máximo\n        sequence = sequence[-self.max_seq_len:]\n\n        # Rellenar con PAD (0) a la izquierda para mantener alineación temporal\n        pad_len = self.max_seq_len - len(sequence)\n        padded_seq = [self.PAD] * pad_len + sequence\n\n        return torch.tensor(padded_seq, dtype=torch.long), torch.tensor(target, dtype=torch.long)\n\nclass PositionalEncoding(nn.Module):\n    def __init__(self, d_model, max_seq_len, dropout):\n        super().__init__()\n        self.dropout = nn.Dropout(dropout)\n        pe = torch.zeros(max_seq_len, d_model)\n        position = torch.arange(0, max_seq_len).unsqueeze(1).float()\n        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))\n        pe[:, 0::2] = torch.sin(position * div_term)\n        pe[:, 1::2] = torch.cos(position * div_term)\n        self.register_buffer('pe', pe.unsqueeze(0))\n\n    def forward(self, x):\n        x = x + self.pe[:, :x.size(1), :]\n        return self.dropout(x)\n\n# ── Modelo SASRec (Transformer Causal Unidireccional) ──────────────────────────\nclass SASRec(nn.Module):\n    def __init__(self, vocab_size, d_model, n_heads, n_layers, max_seq_len, dropout):\n        super().__init__()\n        self.item_embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)\n        self.pos_encoding = PositionalEncoding(d_model, max_seq_len, dropout)\n\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=d_model,\n            nhead=n_heads,\n            dim_feedforward=d_model * 4,\n            dropout=dropout,\n            batch_first=True\n        )\n        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)\n        self.norm = nn.LayerNorm(d_model)\n        self.fc_out = nn.Linear(d_model, vocab_size)\n\n    def forward(self, x):\n        seq_len = x.size(1)\n        pad_mask = (x == 0)\n\n        # Máscara Causal: Triángulo superior en True para impedir mirar al futuro\n        causal_mask = torch.triu(torch.ones(seq_len, seq_len, device=x.device), diagonal=1).bool()\n\n        emb = self.item_embedding(x)\n        emb = self.pos_encoding(emb)\n\n        # Inyectamos simultáneamente la máscara causal y la máscara de padding\n        out = self.transformer(emb, mask=causal_mask, src_key_padding_mask=pad_mask)\n        return self.fc_out(self.norm(out))\n\n# ── Entrenamiento y Evaluación de SASRec ────────────────────────────────────────\ndef train_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    total_loss = 0\n    for seq, target in loader:\n        seq, target = seq.to(device), target.to(device)\n        optimizer.zero_grad()\n\n        # Tomamos los logits únicamente de la última posición (-1), que representa\n        # la predicción del siguiente ítem basándose en todo el contexto izquierdo.\n        logits = model(seq)[:, -1, :]\n\n        loss = criterion(logits, target)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    return total_loss / len(loader)\n\ndef evaluate(model, loader, device, top_k=10):\n    model.eval()\n    hits, ndcg, total = 0, 0, 0\n    with torch.no_grad():\n        for seq, target in loader:\n            seq, target = seq.to(device), target.to(device)\n\n            # Evaluación directa: Predecir el paso posterior usando el vector final\n            logits = model(seq)[:, -1, :]\n\n            _, top_items = torch.topk(logits, top_k, dim=-1)\n            for i in range(target.size(0)):\n                t_item = target[i].item()\n                if t_item == 0: continue\n                total += 1\n                preds = top_items[i].tolist()\n                if t_item in preds:\n                    hits += 1\n                    ndcg += 1.0 / math.log2(preds.index(t_item) + 2)\n    return hits / total if total > 0 else 0, ndcg / total if total > 0 else 0\n\n# ── Construcción de Dataset para XGBoost Re-ranker (Sin Token MASK) ─────────────\ndef build_xgb_dataset(model, data, item_popularity, device, top_k=20):\n    X = []\n    y = []\n    model.eval()\n\n    with torch.no_grad():\n        for sequence, target in data:\n            # En SASRec no añadimos un token [MASK]. La secuencia entra tal cual limpia.\n            seq = sequence[-MAX_SEQ_LEN:]\n            pad_len = MAX_SEQ_LEN - len(seq)\n            seq = [0] * pad_len + seq\n\n            seq_tensor = torch.tensor(seq).unsqueeze(0).to(device)\n            logits = model(seq_tensor)[0, -1] # Extraemos la última posición predictiva\n\n            # Forzar penalización estricta a PAD (0) y MASK (1) del vocabulario\n            logits[0] = -1e9\n            logits[1] = -1e9\n\n            probs = torch.softmax(logits, dim=-1)\n            top_scores, top_items = torch.topk(probs, top_k)\n\n            top_scores = top_scores.cpu().numpy()\n            top_items = top_items.cpu().numpy()\n\n            for idx, item in enumerate(top_items):\n                sasrec_score = top_scores[idx]\n                rank = idx + 1\n\n                X.append([\n                    sasrec_score,              # Score base de SASRec\n                    rank,                      # Ranking inicial del transformer\n                    item_popularity[item],     # Popularidad del ítem candidato\n                    len(sequence),             # Tamaño real del historial del usuario\n                    int(item in sequence)      # Feature de recencia: ¿Ya interactuó antes con él?\n                ])\n\n                if item == target:\n                    y.append(1)\n                else:\n                    y.append(0)\n\n    return np.array(X), np.array(y)\n\n# ── Bloque Principal de Ejecución ───────────────────────────────────────────────\nif __name__ == '__main__':\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"[INFO] Ejecutando pipeline híbrido SASRec + XGBoost en: {device}\")\n\n    sku2idx, idx2sku = load_vocab(vocab_file)\n    vocab_size = len(sku2idx)\n\n    # 1. Cargar Datos de las secuencias generadas\n    train_data = load_sequences(train_file)\n    val_data = load_sequences(val_file)\n    test_data = load_sequences(test_file)\n\n    train_loader = DataLoader(SASRecDataset(train_data, MAX_SEQ_LEN), batch_size=BATCH_SIZE, shuffle=True)\n    val_loader = DataLoader(SASRecDataset(val_data, MAX_SEQ_LEN), batch_size=BATCH_SIZE)\n\n    # 2. Inicializar y Entrenar SASRec\n    model = SASRec(vocab_size, D_MODEL, N_HEADS, N_LAYERS, MAX_SEQ_LEN, DROPOUT).to(device)\n    optimizer = torch.optim.Adam(model.parameters(), lr=LR)\n    criterion = nn.CrossEntropyLoss() # Pérdida directa sobre el target lineal\n\n    best_hr = 0\n    print(\"\\n=== Comenzando Entrenamiento de SASRec ===\")\n    for epoch in range(1, EPOCHS + 1):\n        loss = train_epoch(model, train_loader, optimizer, criterion, device)\n        hr, ndcg = evaluate(model, val_loader, device, TOP_K)\n        print(f\"Epoch {epoch:02d} | Loss: {loss:.4f} | Val HR@{TOP_K}: {hr:.4f} | Val NDCG@{TOP_K}: {ndcg:.4f}\")\n\n        if hr > best_hr:\n            best_hr = hr\n            torch.save(model.state_dict(), os.path.join(out_buffer_path, 'sasrec_best.pt'))\n\n    # Cargar el estado del mejor Transformer entrenado\n    model.load_state_dict(torch.load(os.path.join(out_buffer_path, 'sasrec_best.pt'), map_location=device))\n\n    # 3. Construir Dataset de Características para el Re-ranker (XGBoost)\n    print(\"\\n[INFO] Extrayendo candidatos de SASRec con datos de Validación...\")\n    item_popularity = build_item_popularity(train_data)\n    X_xgb_train, y_xgb_train = build_xgb_dataset(model, val_data, item_popularity, device, top_k=20)\n\n    # 4. Entrenar Segunda Etapa (XGBoost Classifier)\n    print(\"[INFO] Entrenando Clasificador XGBoost...\")\n    xgb_model = xgb.XGBClassifier(\n        n_estimators=100,\n        max_depth=5,\n        learning_rate=0.1,\n        objective='binary:logistic',\n        random_state=42\n    )\n    xgb_model.fit(X_xgb_train, y_xgb_train)\n    print(\"[OK] Pipeline de SASRec + XGBoost completado exitosamente.\")","metadata":{"id":"T-6rO6n4TBOe","outputId":"8cc9e048-67d5-43a1-ce18-fcacfd60a70c","trusted":true,"execution":{"execution_failed":"2026-05-26T14:03:15.704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n===== TEST FINAL DE BERT4REC (Solo etapa 1) =====\")\n\n# 1. Crear el Loader de Test con el modo 'eval' corregido\ntest_loader = DataLoader(\n    Bert4RecDataset(\n        load_sequences(test_file),\n        MAX_SEQ_LEN,\n        MASK_PROB,\n        vocab_size,\n        mode='eval'\n    ),\n    batch_size=BATCH_SIZE\n)\n\n# 2. Inicializar el modelo y cargar los mejores pesos guardados\nbest_model = Bert4Rec(\n    vocab_size,\n    D_MODEL,\n    N_HEADS,\n    N_LAYERS,\n    MAX_SEQ_LEN,\n    DROPOUT\n).to(device)\n\nbest_model.load_state_dict(\n    torch.load(\n        os.path.join(out_buffer_path, 'bert4rec_best.pt'),\n        map_location=device\n    )\n)\n\n# 3. Evaluar Etapa 1 usando la función de evaluación corregida\ntest_hr, test_ndcg = evaluate(\n    best_model,\n    test_loader,\n    device,\n    TOP_K\n)\n\nprint(f\"BERT4Rec Base -> TEST HR@{TOP_K}: {test_hr:.4f}\")\nprint(f\"BERT4Rec Base -> TEST NDCG@{TOP_K}: {test_ndcg:.4f}\")\n\n\nprint(\"\\n===== PREPARANDO DATOS PARA XGBOOST =====\")\n\ntrain_data = load_sequences(train_file)\nval_data = load_sequences(val_file)\ntest_data = load_sequences(test_file)\n\nitem_popularity = build_item_popularity(train_data)\n\n# NOTA: Para entrenar XGBoost usamos VAL_DATA para evitar overfitting\nprint(\"[1/1] Extrayendo características de Validación para entrenar XGBoost...\")\nX_xgb_train, y_xgb_train = build_xgb_dataset(\n    best_model,\n    val_data,\n    item_popularity,\n    device,\n    top_k=20\n)\n\nprint(f\"Dataset XGBoost Listo -> Filas de Entrenamiento: {X_xgb_train.shape[0]}\")\n\n\nprint(\"\\n===== ENTRENANDO XGBOOST (RE-RANKER) =====\")\n\n# Calcular factor de balanceo para obligar a XGBoost a aprender de los \"1\"\nnum_neg = np.sum(y_xgb_train == 0)\nnum_pos = np.sum(y_xgb_train == 1)\nfactor_balance = num_neg / num_pos if num_pos > 0 else 1.0\n\nxgb_model = xgb.XGBClassifier(\n    n_estimators=150,\n    max_depth=5,\n    learning_rate=0.05,\n    subsample=0.8,\n    colsample_bytree=0.8,\n    scale_pos_weight=factor_balance, # Manejo de desbalance\n    eval_metric='logloss',\n    random_state=42\n)\n\nxgb_model.fit(X_xgb_train, y_xgb_train)\nprint(\"[OK] XGBoost Entrenado.\")\n\n\nprint(\"\\n===== EVALUACIÓN FINAL DEL SISTEMA HÍBRIDO (BERT + XGBOOST) =====\")\n\n# Ejecutar la evaluación con la función externa sincronizada\nhybrid_hr = evaluar_sistema_completo(\n    best_model,\n    xgb_model,\n    test_data,\n    item_popularity,\n    device,\n    TOP_K\n)\n\nprint(f\"Resultado final en Test Set:\")\nprint(f\" -> Solo BERT4Rec HR@{TOP_K}: {test_hr:.4f}\")\nprint(f\" -> Híbrido (BERT + XGBoost Re-rank) HR@{TOP_K}: {hybrid_hr:.4f}\")","metadata":{"id":"gZnWOg9x0P16","outputId":"1851afc6-c0a7-438a-e1f4-22762cce72da","trusted":true,"execution":{"execution_failed":"2026-05-26T14:03:15.704Z"}},"outputs":[],"execution_count":null}]}