{"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":[],"gpuType":"T4"},"accelerator":"GPU","widgets":{"application/vnd.jupyter.widget-state+json":{"c13396351f8a437ab65b0444d9c28a6a":{"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_ecc738fe2b204fd39c4ab7b03c1bf72a","IPY_MODEL_5bc09a97b9e947f3abe748f0d10f0d30","IPY_MODEL_821262fbf11247779118dd57bbe6d177","IPY_MODEL_e3ccc600d9bd49f5987a98f70c6104eb"],"layout":"IPY_MODEL_d4196cd01ea1465ea918beaf3ee81743"}},"ecc738fe2b204fd39c4ab7b03c1bf72a":{"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_557e51aef1cb42a3abe9037a3088a704","placeholder":"​","style":"IPY_MODEL_76075541a9ae48f0ba7301cc12585303","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>"}},"5bc09a97b9e947f3abe748f0d10f0d30":{"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_0176e17561114ef6b4c475a1edeb72d5","placeholder":"​","style":"IPY_MODEL_010a3138bf194a939742bf702ddc284e","value":""}},"821262fbf11247779118dd57bbe6d177":{"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_6cb8f341cd42483d8e0d10f4edab59ab","style":"IPY_MODEL_95f1398b18e84427a29d73162ce81ad1","tooltip":""}},"e3ccc600d9bd49f5987a98f70c6104eb":{"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_bc58eb92ba6b4d9eb0668dcdc2e854c1","placeholder":"​","style":"IPY_MODEL_538f26ff4062494e94c0c08557864d5d","value":"\n<b>Thank You</b></center>"}},"d4196cd01ea1465ea918beaf3ee81743":{"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%"}},"557e51aef1cb42a3abe9037a3088a704":{"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}},"76075541a9ae48f0ba7301cc12585303":{"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":""}},"0176e17561114ef6b4c475a1edeb72d5":{"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}},"010a3138bf194a939742bf702ddc284e":{"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":""}},"6cb8f341cd42483d8e0d10f4edab59ab":{"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}},"95f1398b18e84427a29d73162ce81ad1":{"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":""}},"bc58eb92ba6b4d9eb0668dcdc2e854c1":{"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}},"538f26ff4062494e94c0c08557864d5d":{"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":31329,"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":"F5MuhlHsbH_D","outputId":"a884e26a-29de-47a2-ca2b-665957ed9794","trusted":true,"execution":{"iopub.status.busy":"2026-05-26T16:29:21.538414Z","iopub.execute_input":"2026-05-26T16:29:21.539067Z","iopub.status.idle":"2026-05-26T16:29:21.968486Z","shell.execute_reply.started":"2026-05-26T16:29:21.539038Z","shell.execute_reply":"2026-05-26T16:29:21.967858Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport math\nimport pickle\nimport string\nimport pandas as pd\nimport numpy as np\nimport xgboost as xgb\nfrom collections import defaultdict\nfrom sklearn.preprocessing import LabelEncoder\nfrom nltk.stem import WordNetLemmatizer\nimport nltk\nnltk.download('wordnet')\nnltk.download('omw-1.4')\n\n# Tabla nativa en C para remover puntuación velozmente en el notebook\nPUNCT_TRANSLATOR = str.maketrans({ch: ' ' for ch in string.punctuation})\n\nstopwords = set([\"a\", \"an\", \"and\", \"are\", \"as\", \"at\", \"be\", \"but\", \"by\",\n      \"for\", \"if\", \"in\", \"into\", \"is\", \"it\", \"no\", \"not\", \"of\", \"on\", \"or\", \"such\",\n      \"that\", \"the\", \"their\", \"then\", \"there\", \"these\", \"they\", \"this\", \"to\", \"was\",\n      \"will\", \"with\"])\n\ndef get_words(raw):\n    \"\"\"Tu función de util optimizada para no trabar el Notebook\"\"\"\n    if not raw or pd.isna(raw):\n        return []\n    raw = str(raw).strip().lower()\n    clean_text = raw.translate(PUNCT_TRANSLATOR)\n    return [w for w in clean_text.split() if w not in stopwords and len(w) > 0]\n\ndef writefile(f):\n    \"\"\"Escribe el submission.csv usando Pandas de forma segura\"\"\"\n    df_header = pd.DataFrame(columns=[\"sku\"])\n    df_header.to_csv(f, index=False, encoding='utf-8')\n    return lambda preds_list: pd.DataFrame({\"sku\": preds_list}).to_csv(f, mode='a', header=False, index=False)","metadata":{"id":"LYQVN8Ecbd0K","outputId":"eff2504b-9414-4918-9990-7ace48cfcd07","trusted":true,"execution":{"iopub.status.busy":"2026-05-26T16:29:21.969701Z","iopub.execute_input":"2026-05-26T16:29:21.970014Z","iopub.status.idle":"2026-05-26T16:29:22.030211Z","shell.execute_reply.started":"2026-05-26T16:29:21.969979Z","shell.execute_reply":"2026-05-26T16:29:22.029290Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport string\nimport types\nimport numpy as np\nimport pandas as pd\nfrom sklearn.preprocessing import LabelEncoder\nfrom nltk.stem import WordNetLemmatizer\n\n# =========================================================================\n# 1. PROCESADOR CORREGIDO (Con soporte para formatos de fecha mezclados)\n# =========================================================================\nPUNCT_TRANSLATOR = str.maketrans({ch: ' ' for ch in string.punctuation})\nstopwords = set([\"a\", \"an\", \"and\", \"are\", \"as\", \"at\", \"be\", \"but\", \"by\",\n                 \"for\", \"if\", \"in\", \"into\", \"is\", \"it\", \"no\", \"not\", \"of\", \"on\", \"or\", \"such\",\n                 \"that\", \"the\", \"their\", \"then\", \"there\", \"these\", \"they\", \"this\", \"to\", \"was\", \n                 \"will\", \"with\"])\n\ndef get_words(raw):\n    if not raw or pd.isna(raw):\n        return []\n    raw = str(raw).strip().lower()\n    clean_text = raw.translate(PUNCT_TRANSLATOR)\n    return [w for w in clean_text.split() if w not in stopwords and len(w) > 0]\n\nclass PandasOnTheFlyProcessor:\n    def __init__(self, st_date='2011-08-01'):\n        self.lemmatizer = WordNetLemmatizer()\n        self.local_cache = {}\n        self.st_date = st_date\n\n    def _split_word_num(self, word):\n        if len(word) <= 1 or word.isdigit() or word[-1].isalpha():\n            return word\n        idx = len(word) - 1\n        for idx in range(len(word) - 1, 0, -1):\n            if not (word[idx].isdigit() and word[idx - 1].isdigit()):\n                break\n        if idx <= 1:\n            return word\n        return word[:idx], word[idx:]\n\n    def _get_split(self, w):\n        if not w.isdigit() and not w.isalpha():\n            return self._split_word_num(w)\n        return w\n\n    def _clean_single_query(self, raw_query):\n        raw_query = str(raw_query).lower().strip()\n        if raw_query in self.local_cache:\n            return self.local_cache[raw_query]\n\n        words = get_words(raw_query)\n        new_words = []\n        for w in words:\n            split = self._get_split(w)\n            if isinstance(split, tuple):\n                new_words.extend(list(split))\n            else:\n                new_words.append(split)\n                \n        new_query = ''\n        for w in new_words:\n            lemma = self.lemmatizer.lemmatize(w)\n            if len(lemma) >= 4 and not lemma.isdigit() and not lemma.isalpha():\n                split = self._split_word_num(w)\n                if isinstance(split, tuple):\n                    w, num = split\n                    lemma = ' '.join([w, num])\n            new_query += lemma + ' '\n            \n        final_query = new_query.strip()\n        self.local_cache[raw_query] = final_query\n        return final_query\n\n    def stream_clean_rows(self, file_path, mode='train', chunk_size=50000, allowed_skus=None, min_days=0, max_days=None):\n            if not os.path.exists(file_path):\n                print(f\"[ERROR] Archivo no encontrado: {file_path}\")\n                return\n    \n            for chunk in pd.read_csv(file_path, delimiter=\",\", quotechar='\"', encoding='utf-8', chunksize=chunk_size):\n                # SOLUCIÓN AQUÍ: format='mixed' para que no se rompa ni genere NaT con los milisegundos\n                dt_series = pd.to_datetime(chunk['click_time'].astype(str).str.strip('\"'), errors='coerce', format='mixed')\n                st_dt = pd.to_datetime(self.st_date)\n                chunk['clean_time_int'] = ((dt_series - st_dt).dt.days).fillna(0).astype(int)\n                \n                # Filtros temporales por bloques\n                chunk = chunk[chunk['clean_time_int'] >= min_days].copy()\n                if max_days is not None:\n                    chunk = chunk[chunk['clean_time_int'] < max_days].copy()\n                    \n                if chunk.empty:\n                    continue\n    \n                if mode in ['train', 'val'] and allowed_skus is not None:\n                    chunk['sku_str'] = chunk['sku'].astype(str)\n                    chunk['cat_str'] = chunk['category'].astype(str)\n                    mask = chunk.apply(lambda r: r['cat_str'] in allowed_skus and r['sku_str'] in allowed_skus[r['cat_str']], axis=1)\n                    chunk = chunk[mask].copy()\n                    if chunk.empty:\n                        continue\n    \n                chunk['clean_query'] = chunk['query'].apply(self._clean_single_query)\n                chunk['clean_time'] = chunk['clean_time_int'].astype(str)\n                \n                if mode in ['train', 'val']:\n                    for _, row in chunk.iterrows():\n                        yield str(row['user']), str(row['sku']), str(row['category']), str(row['clean_query']), str(row['clean_time'])\n                else:\n                    for _, row in chunk.iterrows():\n                        yield str(row['user']), str(row['category']), str(row['clean_query']), str(row['clean_time'])","metadata":{"id":"ve0GnEMveRv2","trusted":true,"execution":{"iopub.status.busy":"2026-05-26T16:29:22.031355Z","iopub.execute_input":"2026-05-26T16:29:22.031998Z","iopub.status.idle":"2026-05-26T16:29:22.048269Z","shell.execute_reply.started":"2026-05-26T16:29:22.031964Z","shell.execute_reply":"2026-05-26T16:29:22.047484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ProductRecommenderML:\n\n    def __init__(self, st_date='2011-08-01'):\n        self.category_models = {}\n        self.top_words = []\n        self.top_bigrams = []\n        self.st_date = st_date\n\n    # =========================================================\n    # BIGRAMAS CONSECUTIVOS (MUCHO MÁS RÁPIDO)\n    # =========================================================\n    def _get_bigrams(self, words):\n\n        return [\n            f\"{words[i]}_{words[i+1]}\"\n            for i in range(len(words) - 1)\n        ]\n\n    # =========================================================\n    # FEATURE EXTRACTION\n    # =========================================================\n    def extract_features(\n        self,\n        words,\n        bigrams,\n        click_time,\n        word_freq,\n        bigram_freq\n    ):\n\n        word_set = set(words)\n        bigram_set = set(bigrams)\n\n        features = []\n\n        # Longitud query\n        features.append(len(words))\n\n        # Top words\n        features.extend(\n            1 if word in word_set else 0\n            for word in self.top_words\n        )\n\n        # Top bigrams\n        features.extend(\n            1 if bigram in bigram_set else 0\n            for bigram in self.top_bigrams\n        )\n\n        # Click time\n        try:\n            features.append(int(click_time))\n        except:\n            features.append(0)\n\n        # Promedio frecuencia palabras\n        if words:\n            features.append(\n                sum(word_freq.get(w, 0) for w in words)\n                / len(words)\n            )\n        else:\n            features.append(0)\n\n        # Promedio frecuencia bigrams\n        if bigrams:\n            features.append(\n                sum(bigram_freq.get(b, 0) for b in bigrams)\n                / len(bigrams)\n            )\n        else:\n            features.append(0)\n\n        return features\n\n    # =========================================================\n    # TRAIN\n    # =========================================================\n    def train(self, raw_train_filepath):\n\n        processor = PandasOnTheFlyProcessor(\n            st_date=self.st_date\n        )\n\n        # =====================================================\n        # PASADA 1\n        # =====================================================\n\n        print(\"[INFO] Pasada 1: estadísticas globales...\")\n\n        reader1 = processor.stream_clean_rows(\n            raw_train_filepath,\n            mode='train'\n        )\n\n        word_freq_global = defaultdict(int)\n        bigram_freq_global = defaultdict(int)\n\n        sku_counts_per_cat = defaultdict(\n            lambda: defaultdict(int)\n        )\n\n        for (\n            user,\n            sku,\n            category,\n            query,\n            click_time\n        ) in reader1:\n\n            words = get_words(query)\n            bigrams = self._get_bigrams(words)\n\n            for word in words:\n                word_freq_global[word] += 1\n\n            for bigram in bigrams:\n                bigram_freq_global[bigram] += 1\n\n            sku_counts_per_cat[category][sku] += 1\n\n        # =====================================================\n        # TOP FEATURES\n        # =====================================================\n\n        self.top_words = [\n            w for w, _\n            in sorted(\n                word_freq_global.items(),\n                key=lambda x: x[1],\n                reverse=True\n            )[:50]\n        ]\n\n        self.top_bigrams = [\n            b for b, _\n            in sorted(\n                bigram_freq_global.items(),\n                key=lambda x: x[1],\n                reverse=True\n            )[:30]\n        ]\n\n        # =====================================================\n        # TOP SKUS\n        # =====================================================\n\n        top_skus_by_cat = {}\n\n        for cat, skus in sku_counts_per_cat.items():\n\n            top_skus_by_cat[cat] = set(\n                s for s, c\n                in sorted(\n                    skus.items(),\n                    key=lambda x: x[1],\n                    reverse=True\n                )[:100]\n            )\n\n        del sku_counts_per_cat\n\n        # =====================================================\n        # PASADA 2\n        # =====================================================\n\n        print(\"[INFO] Pasada 2: cargando registros...\")\n\n        data_by_category = defaultdict(list)\n\n        reader2 = processor.stream_clean_rows(\n            raw_train_filepath,\n            mode='train',\n            allowed_skus=top_skus_by_cat\n        )\n\n        for (\n            user,\n            sku,\n            category,\n            query,\n            click_time\n        ) in reader2:\n\n            words = get_words(query)\n            bigrams = self._get_bigrams(words)\n\n            data_by_category[category].append(\n                (\n                    sku,\n                    words,\n                    bigrams,\n                    click_time\n                )\n            )\n\n        # =====================================================\n        # ENTRENAMIENTO\n        # =====================================================\n\n        print(\"[INFO] Entrenando modelos XGBoost...\")\n\n        trained_categories = 0\n\n        for category, records in data_by_category.items():\n\n            # -------------------------------------------------\n            # VALIDACIÓN INICIAL\n            # -------------------------------------------------\n\n            if not records or len(records) < 10:\n                continue\n\n            print(\n                f\"[INFO] Categoría: {category} \"\n                f\"| registros={len(records)}\"\n            )\n\n            # -------------------------------------------------\n            # FRECUENCIAS LOCALES\n            # -------------------------------------------------\n\n            word_freq = defaultdict(int)\n            bigram_freq = defaultdict(int)\n\n            for (_, words, bigrams, _) in records:\n\n                for word in words:\n                    word_freq[word] += 1\n\n                for bigram in bigrams:\n                    bigram_freq[bigram] += 1\n\n            # -------------------------------------------------\n            # FEATURES\n            # -------------------------------------------------\n\n            X = []\n            y = []\n\n            for (\n                sku,\n                words,\n                bigrams,\n                click_time\n            ) in records:\n\n                features = self.extract_features(\n                    words,\n                    bigrams,\n                    click_time,\n                    word_freq,\n                    bigram_freq\n                )\n\n                X.append(features)\n                y.append(sku)\n\n            # -------------------------------------------------\n            # LIMPIEZA ROBUSTA\n            # -------------------------------------------------\n\n            clean_X = []\n            clean_y = []\n\n            for xi, yi in zip(X, y):\n\n                if yi is None:\n                    continue\n\n                yi = str(yi).strip()\n\n                if yi == '' or yi.lower() == 'nan':\n                    continue\n\n                clean_X.append(xi)\n                clean_y.append(yi)\n\n            X = clean_X\n            y = clean_y\n\n            # -------------------------------------------------\n            # VALIDACIONES\n            # -------------------------------------------------\n\n            if len(y) == 0:\n\n                print(\n                    f\"[AVISO] {category} \"\n                    f\"sin labels válidos\"\n                )\n\n                continue\n\n            unique_classes = set(y)\n\n            if len(unique_classes) < 2:\n\n                print(\n                    f\"[AVISO] Saltando '{category}' \"\n                    f\"porque tiene \"\n                    f\"{len(unique_classes)} clase(s)\"\n                )\n\n                continue\n\n            print(\n                f\"[INFO] {category} | \"\n                f\"samples={len(y)} | \"\n                f\"classes={len(unique_classes)}\"\n            )\n\n            # -------------------------------------------------\n            # LABEL ENCODER\n            # -------------------------------------------------\n\n            label_encoder = LabelEncoder()\n\n            y_encoded = label_encoder.fit_transform(y)\n\n            if len(np.unique(y_encoded)) < 2:\n\n                print(\n                    f\"[AVISO] Encoding inválido \"\n                    f\"en '{category}'\"\n                )\n\n                continue\n\n            # -------------------------------------------------\n            # NUMPY OPTIMIZADO\n            # -------------------------------------------------\n\n            X = np.asarray(\n                X,\n                dtype=np.float32\n            )\n\n            y_encoded = np.asarray(\n                y_encoded,\n                dtype=np.int32\n            )\n\n            # -------------------------------------------------\n            # MODELO GPU\n            # -------------------------------------------------\n\n            model = xgb.XGBClassifier(\n                n_estimators=40,\n                max_depth=5,\n                learning_rate=0.1,\n                objective='multi:softprob',\n                num_class=len(unique_classes),\n                random_state=42,\n                eval_metric='mlogloss',\n                device='cuda',\n                tree_method='hist'\n            )\n\n            # -------------------------------------------------\n            # TRAIN\n            # -------------------------------------------------\n\n            try:\n\n                model.fit(X, y_encoded)\n\n                self.category_models[category] = (\n                    model,\n                    label_encoder,\n                    word_freq,\n                    bigram_freq\n                )\n\n                trained_categories += 1\n\n                print(\n                    f\"[OK] {category} entrenada\"\n                )\n\n            except Exception as e:\n\n                print(\n                    f\"[ERROR] {category}: {e}\"\n                )\n\n                continue\n\n        print(\n            f\"[OK] Pipeline completado. \"\n            f\"{trained_categories} modelos listos.\"\n        )\n\n        return self","metadata":{"id":"HpSm20XgeV0p","trusted":true,"execution":{"iopub.status.busy":"2026-05-26T16:29:22.050576Z","iopub.execute_input":"2026-05-26T16:29:22.051061Z","iopub.status.idle":"2026-05-26T16:29:22.074595Z","shell.execute_reply.started":"2026-05-26T16:29:22.051039Z","shell.execute_reply":"2026-05-26T16:29:22.073736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# CELDA 1: FASE DE ENTRENAMIENTO EXCLUSIVO\n# ==========================================\nimport os\n\ntrain_csv_path = os.path.join(path, 'train.csv')\nst_date_config = '2011-08-01'\n\nprint(\"Configurando el procesador para entrenar solo con el pasado...\")\n# Activamos el tope máximo en la clase para congelar el entrenamiento en el día 110\nPandasOnTheFlyProcessor.max_days = 110 \n\nprint(\"Iniciando fase de entrenamiento (Ventana: Días 30 al 109)...\")\nrecommender = ProductRecommenderML(st_date=st_date_config)\nrecommender.train(train_csv_path)\n\nprint(\"\\n[OK] Entrenamiento finalizado. El modelo se entrenó sin ver los datos de validación.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-26T16:29:22.075474Z","iopub.execute_input":"2026-05-26T16:29:22.075882Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!nvidia-smi","metadata":{"id":"Qmk-65gVcC5I","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================================\n# 2. EJECUCIÓN DE VALIDACIÓN LOCAL Y SUBMISSION\n# =========================================================================\ntest_csv_path = os.path.join(path, 'test.csv')\ntrain_csv_path = os.path.join(path, 'train.csv')\noutput_submission = 'submission.csv'\nst_date_config = '2011-08-01'\n\n# Parcheamos el método predict en el modelo activo\ndef predict_fixed(self, query, category, click_time, top_k=5):\n    if category not in self.category_models:\n        return []\n    model, label_encoder, word_freq, bigram_freq = self.category_models[category]\n    words = str(query).split()\n    bigrams = self._get_bigrams(words)\n    features = self.extract_features(words, bigrams, click_time, word_freq, bigram_freq)\n    X = np.asarray([features], dtype=np.float32)\n    try:\n        preds_proba = model.predict_proba(X)[0]\n        top_indices = np.argsort(preds_proba)[::-1][:top_k]\n        top_skus = label_encoder.inverse_transform(top_indices)\n        return [str(sku) for sku in top_skus]\n    except:\n        return []\n\nrecommender.predict = types.MethodType(predict_fixed, recommender)\n\n# --- EVALUACIÓN MAP@5 ---\nprint(\"--- Evaluando Rendimiento del Modelo (MAP@5) en Validación ---\")\nprocessor = PandasOnTheFlyProcessor(st_date=st_date_config)\n\n# Nota: Si el dataset de Best Buy resulta ser más corto de 110 días, cambia el min_days a 60 o 70\nval_stream = processor.stream_clean_rows(train_csv_path, mode='train', min_days=110)\n\ntotal_rows = 0\nmap_acumulado = 0.0\nmax_eval_rows = 2000 \n\nfor (user, true_sku, category, query, click_time) in val_stream:\n    preds = recommender.predict(query, category, click_time, top_k=5)\n    row_score = 0.0\n    if true_sku in preds:\n        posicion = preds.index(true_sku)\n        row_score = 1.0 / (posicion + 1)\n    map_acumulado += row_score\n    total_rows += 1\n    if total_rows >= max_eval_rows:\n        break\n\nscore_final = map_acumulado / total_rows if total_rows > 0 else 0\nprint(\"-\" * 90)\nprint(f\"Filas de validación evaluadas con éxito: {total_rows}\")\nprint(f\"SCORE MAP@5 EN VENTANA DE VALIDACIÓN: {score_final:.5f}\")\nprint(\"-\" * 90)","metadata":{"id":"_BCNatZSb2DR","outputId":"5d2279c7-b288-4482-a4f8-f26d636b58ce","trusted":true},"outputs":[],"execution_count":null}]}