{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":31254,"databundleVersionId":3103714}],"dockerImageVersionId":31400,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"%%bash\npip install faiss-cpu lightgbm fastapi uvicorn pyarrow -q\necho \"done\"","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:34:45.914471Z","iopub.execute_input":"2026-05-31T15:34:45.915343Z","iopub.status.idle":"2026-05-31T15:34:51.376728Z","shell.execute_reply.started":"2026-05-31T15:34:45.915300Z","shell.execute_reply":"2026-05-31T15:34:51.375806Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%bash\nls /kaggle/input/competitions/h-and-m-personalized-fashion-recommendations/","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:34:55.010074Z","iopub.execute_input":"2026-05-31T15:34:55.010625Z","iopub.status.idle":"2026-05-31T15:34:55.029885Z","shell.execute_reply.started":"2026-05-31T15:34:55.010592Z","shell.execute_reply":"2026-05-31T15:34:55.028932Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%bash\nmkdir -p /kaggle/working/hm-recsys/{data,models,retrieval,ranking,serving,utils,scripts}\nmkdir -p /kaggle/working/processed\nmkdir -p /kaggle/working/artifacts\necho \"✓ folders created\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:34:55.773627Z","iopub.execute_input":"2026-05-31T15:34:55.773894Z","iopub.status.idle":"2026-05-31T15:34:55.790070Z","shell.execute_reply.started":"2026-05-31T15:34:55.773873Z","shell.execute_reply":"2026-05-31T15:34:55.789231Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"config_code = '''\nfrom pathlib import Path\n\nROOT         = Path(\"/kaggle/working/hm-recsys\")\nDATA_DIR     = Path(\"/kaggle/input/competitions/h-and-m-personalized-fashion-recommendations\")\nPROCESSED_DIR = Path(\"/kaggle/working/processed\")\nARTIFACT_DIR  = Path(\"/kaggle/working/artifacts\")\n\nfor d in [PROCESSED_DIR, ARTIFACT_DIR]:\n    d.mkdir(parents=True, exist_ok=True)\n\nMIN_USER_INTERACTIONS = 5\nMIN_ITEM_INTERACTIONS = 3\nVAL_WEEKS  = 1\nTEST_WEEKS = 1\n\nEMBEDDING_DIM     = 128\nUSER_HIDDEN_DIMS  = [256, 128]\nITEM_HIDDEN_DIMS  = [256, 128]\nDROPOUT           = 0.2\nTEMPERATURE       = 0.07\n\nBATCH_SIZE    = 2048\nLEARNING_RATE = 1e-3\nWEIGHT_DECAY  = 1e-5\nEPOCHS        = 15\nPATIENCE      = 3\nNUM_WORKERS   = 2\nDEVICE        = \"cuda\"\n\nRETRIEVAL_TOP_K = 500\nFAISS_NLIST     = 100\nFAISS_NPROBE    = 10\n\nRANKER_TOP_N = 12\nLGBM_PARAMS  = {\n    \"objective\": \"lambdarank\",\n    \"metric\": \"ndcg\",\n    \"ndcg_eval_at\": [10],\n    \"learning_rate\": 0.05,\n    \"num_leaves\": 63,\n    \"min_child_samples\": 20,\n    \"n_estimators\": 300,\n    \"subsample\": 0.8,\n    \"colsample_bytree\": 0.8,\n    \"verbose\": -1,\n}\n\nEVAL_K_VALUES = [10, 20, 50, 100]\n'''\n\nwith open(\"/kaggle/working/hm-recsys/config.py\", \"w\") as f:\n    f.write(config_code)\nprint(\"✓ config.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:00.667315Z","iopub.execute_input":"2026-05-31T15:35:00.667694Z","iopub.status.idle":"2026-05-31T15:35:00.674551Z","shell.execute_reply.started":"2026-05-31T15:35:00.667662Z","shell.execute_reply":"2026-05-31T15:35:00.673739Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"logger_code = '''\nimport logging, sys\n\ndef get_logger(name, level=logging.INFO):\n    logger = logging.getLogger(name)\n    if not logger.handlers:\n        h = logging.StreamHandler(sys.stdout)\n        h.setFormatter(logging.Formatter(\"%(asctime)s | %(levelname)s | %(message)s\", datefmt=\"%H:%M:%S\"))\n        logger.addHandler(h)\n    logger.setLevel(level)\n    return logger\n'''\n\nwith open(\"/kaggle/working/hm-recsys/utils/__init__.py\", \"w\") as f: f.write(\"\")\nwith open(\"/kaggle/working/hm-recsys/utils/logger.py\", \"w\") as f:\n    f.write(logger_code)\nprint(\"✓ logger.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:07.389051Z","iopub.execute_input":"2026-05-31T15:35:07.390025Z","iopub.status.idle":"2026-05-31T15:35:07.395753Z","shell.execute_reply.started":"2026-05-31T15:35:07.389995Z","shell.execute_reply":"2026-05-31T15:35:07.394951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"metrics_code = '''\nimport numpy as np\n\ndef recall_at_k(positives, ranked, k):\n    if not positives: return 0.0\n    return len(set(ranked[:k]) & positives) / min(len(positives), k)\n\ndef dcg_at_k(positives, ranked, k):\n    return sum(1/np.log2(i+2) for i,item in enumerate(ranked[:k]) if item in positives)\n\ndef ndcg_at_k(positives, ranked, k):\n    ideal = dcg_at_k(positives, list(positives), k)\n    return 0.0 if ideal == 0 else dcg_at_k(positives, ranked, k) / ideal\n\ndef average_precision_at_k(positives, ranked, k):\n    if not positives: return 0.0\n    score, hits = 0.0, 0\n    for i, item in enumerate(ranked[:k]):\n        if item in positives:\n            hits += 1\n            score += hits / (i+1)\n    return score / min(len(positives), k)\n'''\n\nwith open(\"/kaggle/working/hm-recsys/utils/metrics.py\", \"w\") as f:\n    f.write(metrics_code)\nprint(\"✓ metrics.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:07.647742Z","iopub.execute_input":"2026-05-31T15:35:07.648463Z","iopub.status.idle":"2026-05-31T15:35:07.653805Z","shell.execute_reply.started":"2026-05-31T15:35:07.648432Z","shell.execute_reply":"2026-05-31T15:35:07.653073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"user_tower_code = '''\nimport torch, torch.nn as nn, torch.nn.functional as F\n\nclass UserTower(nn.Module):\n    def __init__(self, num_users, feature_dims, embedding_dim=128, hidden_dims=None, dropout=0.2):\n        super().__init__()\n        if hidden_dims is None: hidden_dims = [256, 128]\n        self.user_id_emb = nn.Embedding(num_users, 64, padding_idx=0)\n        self.feat_embeddings = nn.ModuleDict()\n        feat_emb_dim = 16\n        for name, n_cats in feature_dims.items():\n            self.feat_embeddings[name] = nn.Embedding(n_cats+1, feat_emb_dim, padding_idx=0)\n        input_dim = 64 + feat_emb_dim * len(feature_dims)\n        layers = []\n        prev = input_dim\n        for h in hidden_dims:\n            layers += [nn.Linear(prev,h), nn.LayerNorm(h), nn.ReLU(), nn.Dropout(dropout)]\n            prev = h\n        layers.append(nn.Linear(prev, embedding_dim))\n        self.mlp = nn.Sequential(*layers)\n\n    def forward(self, user_ids, features):\n        x = self.user_id_emb(user_ids)\n        for name, emb in self.feat_embeddings.items():\n            x = torch.cat([x, emb(features[name])], dim=-1)\n        return F.normalize(self.mlp(x), p=2, dim=-1)\n'''\n\nwith open(\"/kaggle/working/hm-recsys/models/__init__.py\", \"w\") as f: f.write(\"\")\nwith open(\"/kaggle/working/hm-recsys/models/user_tower.py\", \"w\") as f:\n    f.write(user_tower_code)\nprint(\"✓ user_tower.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:07.906832Z","iopub.execute_input":"2026-05-31T15:35:07.907531Z","iopub.status.idle":"2026-05-31T15:35:07.913560Z","shell.execute_reply.started":"2026-05-31T15:35:07.907499Z","shell.execute_reply":"2026-05-31T15:35:07.912764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"item_tower_code = '''\nimport torch, torch.nn as nn, torch.nn.functional as F\n\nclass ItemTower(nn.Module):\n    def __init__(self, num_items, feature_dims, embedding_dim=128, hidden_dims=None, dropout=0.2):\n        super().__init__()\n        if hidden_dims is None: hidden_dims = [256, 128]\n        self.item_id_emb = nn.Embedding(num_items, 64, padding_idx=0)\n        self.feat_embeddings = nn.ModuleDict()\n        feat_emb_dim = 16\n        for name, n_cats in feature_dims.items():\n            self.feat_embeddings[name] = nn.Embedding(n_cats+1, feat_emb_dim, padding_idx=0)\n        input_dim = 64 + feat_emb_dim * len(feature_dims)\n        layers = []\n        prev = input_dim\n        for h in hidden_dims:\n            layers += [nn.Linear(prev,h), nn.LayerNorm(h), nn.ReLU(), nn.Dropout(dropout)]\n            prev = h\n        layers.append(nn.Linear(prev, embedding_dim))\n        self.mlp = nn.Sequential(*layers)\n\n    def forward(self, item_ids, features):\n        x = self.item_id_emb(item_ids)\n        for name, emb in self.feat_embeddings.items():\n            x = torch.cat([x, emb(features[name])], dim=-1)\n        return F.normalize(self.mlp(x), p=2, dim=-1)\n'''\n\nwith open(\"/kaggle/working/hm-recsys/models/item_tower.py\", \"w\") as f:\n    f.write(item_tower_code)\nprint(\"✓ item_tower.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:12.066852Z","iopub.execute_input":"2026-05-31T15:35:12.067582Z","iopub.status.idle":"2026-05-31T15:35:12.073027Z","shell.execute_reply.started":"2026-05-31T15:35:12.067551Z","shell.execute_reply":"2026-05-31T15:35:12.072070Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"two_tower_code = '''\nimport torch, torch.nn as nn, torch.nn.functional as F\nfrom models.user_tower import UserTower\nfrom models.item_tower import ItemTower\n\nclass TwoTowerModel(nn.Module):\n    def __init__(self, user_tower, item_tower, temperature=0.07):\n        super().__init__()\n        self.user_tower = user_tower\n        self.item_tower = item_tower\n        self.temperature = temperature\n\n    def forward(self, user_ids, user_feats, item_ids, item_feats):\n        u = self.user_tower(user_ids, user_feats)\n        v = self.item_tower(item_ids, item_feats)\n        loss = self.contrastive_loss(u, v)\n        return loss, u, v\n\n    def contrastive_loss(self, u, v):\n        B = u.size(0)\n        logits = torch.matmul(u, v.T) / self.temperature\n        labels = torch.arange(B, device=u.device)\n        return (F.cross_entropy(logits, labels) + F.cross_entropy(logits.T, labels)) / 2\n\n    def get_user_embedding(self, user_ids, feats):\n        with torch.no_grad(): return self.user_tower(user_ids, feats)\n\n    def get_item_embedding(self, item_ids, feats):\n        with torch.no_grad(): return self.item_tower(item_ids, feats)\n'''\n\nwith open(\"/kaggle/working/hm-recsys/models/two_tower.py\", \"w\") as f:\n    f.write(two_tower_code)\nprint(\"✓ two_tower.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:12.313347Z","iopub.execute_input":"2026-05-31T15:35:12.314214Z","iopub.status.idle":"2026-05-31T15:35:12.319546Z","shell.execute_reply.started":"2026-05-31T15:35:12.314184Z","shell.execute_reply":"2026-05-31T15:35:12.318868Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preprocess_code = '''\nimport sys\nsys.path.insert(0, \"/kaggle/working/hm-recsys\")\n\nimport pandas as pd, numpy as np, pickle\nfrom pathlib import Path\nfrom sklearn.preprocessing import LabelEncoder\nfrom config import DATA_DIR, PROCESSED_DIR, MIN_USER_INTERACTIONS, MIN_ITEM_INTERACTIONS, VAL_WEEKS, TEST_WEEKS\nfrom utils.logger import get_logger\n\nlog = get_logger(__name__)\n\ndef encode_column(df, col, encoder=None):\n    df[col] = df[col].astype(str).fillna(\"unknown\")\n    if encoder is None:\n        encoder = LabelEncoder()\n        df[col] = encoder.fit_transform(df[col])\n    else:\n        known = set(encoder.classes_)\n        df[col] = df[col].apply(lambda x: x if x in known else \"unknown\")\n        df[col] = encoder.transform(df[col])\n    return df, encoder\n\ndef save(obj, path):\n    Path(path).parent.mkdir(parents=True, exist_ok=True)\n    with open(path, \"wb\") as f: pickle.dump(obj, f)\n\ndef main():\n    log.info(\"Loading CSVs...\")\n    tx = pd.read_csv(DATA_DIR / \"transactions_train.csv\", parse_dates=[\"t_dat\"])\n    customers = pd.read_csv(DATA_DIR / \"customers.csv\")\n    articles  = pd.read_csv(DATA_DIR / \"articles.csv\")\n    log.info(f\"Transactions: {len(tx):,}\")\n\n    # Filter sparse\n    for _ in range(3):\n        n = len(tx)\n        uc = tx[\"customer_id\"].value_counts()\n        ic = tx[\"article_id\"].value_counts()\n        tx = tx[tx[\"customer_id\"].isin(uc[uc>=MIN_USER_INTERACTIONS].index) &\n                tx[\"article_id\"].isin(ic[ic>=MIN_ITEM_INTERACTIONS].index)]\n        if len(tx)==n: break\n    log.info(f\"After filter: {len(tx):,} rows | {tx.customer_id.nunique():,} users | {tx.article_id.nunique():,} items\")\n\n    # Time split\n    max_date = tx[\"t_dat\"].max()\n    test_start = max_date - pd.Timedelta(weeks=TEST_WEEKS)\n    val_start  = test_start - pd.Timedelta(weeks=VAL_WEEKS)\n    train = tx[tx.t_dat < val_start]\n    val   = tx[(tx.t_dat >= val_start) & (tx.t_dat < test_start)]\n    test  = tx[tx.t_dat >= test_start]\n    train_users = set(train.customer_id); train_items = set(train.article_id)\n    val  = val[val.customer_id.isin(train_users) & val.article_id.isin(train_items)]\n    test = test[test.customer_id.isin(train_users) & test.article_id.isin(train_items)]\n    log.info(f\"Train:{len(train):,} Val:{len(val):,} Test:{len(test):,}\")\n\n    # Encode IDs\n    user_enc = LabelEncoder().fit(list(train.customer_id.unique()))\n    item_enc = LabelEncoder().fit(list(train.article_id.unique()))\n    for df in [train, val, test]:\n        df[\"user_idx\"] = user_enc.transform(df[\"customer_id\"])\n        df[\"item_idx\"] = item_enc.transform(df[\"article_id\"])\n\n    # User features\n    uf = customers[customers.customer_id.isin(set(train.customer_id))].copy()\n    uf[\"age\"] = uf[\"age\"].fillna(uf[\"age\"].median())\n    uf[\"age_bin\"] = pd.cut(uf[\"age\"], bins=[0,20,30,40,50,60,120], labels=[\"<20\",\"20s\",\"30s\",\"40s\",\"50s\",\"60+\"])\n    user_feat_enc = {}\n    for c in [\"club_member_status\",\"fashion_news_frequency\",\"age_bin\"]:\n        uf, enc = encode_column(uf, c)\n        user_feat_enc[c] = enc\n    uf[\"has_fn\"] = (uf[\"fashion_news_frequency\"]>0).astype(int)\n    uid_map = pd.DataFrame({\"customer_id\": user_enc.classes_, \"user_idx\": range(len(user_enc.classes_))})\n    uf = uf[[\"customer_id\",\"club_member_status\",\"fashion_news_frequency\",\"age_bin\",\"has_fn\"]].merge(uid_map, on=\"customer_id\", how=\"inner\")\n\n    # Item features\n    itf = articles[articles.article_id.isin(set(train.article_id))].copy()\n    item_feat_enc = {}\n    item_cat_cols = [\"product_type_name\",\"colour_group_name\",\"department_name\",\"index_name\",\"garment_group_name\"]\n    for c in item_cat_cols:\n        itf, enc = encode_column(itf, c)\n        item_feat_enc[c] = enc\n    iid_map = pd.DataFrame({\"article_id\": item_enc.classes_, \"item_idx\": range(len(item_enc.classes_))})\n    itf = itf[[\"article_id\"]+item_cat_cols].merge(iid_map, on=\"article_id\", how=\"inner\")\n\n    # Interaction dicts\n    train_int = train.groupby(\"user_idx\")[\"item_idx\"].apply(sorted).to_dict()\n    val_int   = val.groupby(\"user_idx\")[\"item_idx\"].apply(sorted).to_dict()\n    test_int  = test.groupby(\"user_idx\")[\"item_idx\"].apply(sorted).to_dict()\n\n    # Save\n    train.to_parquet(PROCESSED_DIR/\"train.parquet\", index=False)\n    val.to_parquet(PROCESSED_DIR/\"val.parquet\", index=False)\n    test.to_parquet(PROCESSED_DIR/\"test.parquet\", index=False)\n    uf.to_parquet(PROCESSED_DIR/\"user_features.parquet\", index=False)\n    itf.to_parquet(PROCESSED_DIR/\"item_features.parquet\", index=False)\n    for name, obj in [(\"user_enc\",user_enc),(\"item_enc\",item_enc),\n                       (\"user_feat_encoders\",user_feat_enc),(\"item_feat_encoders\",item_feat_enc),\n                       (\"train_interactions\",train_int),(\"val_interactions\",val_int),(\"test_interactions\",test_int)]:\n        save(obj, PROCESSED_DIR/f\"{name}.pkl\")\n    log.info(f\"Done. Users:{len(user_enc.classes_):,} Items:{len(item_enc.classes_):,}\")\n\nmain()\n'''\n\nwith open(\"/kaggle/working/hm-recsys/data/preprocess.py\", \"w\") as f:\n    f.write(preprocess_code)\nwith open(\"/kaggle/working/hm-recsys/data/__init__.py\", \"w\") as f: f.write(\"\")\nprint(\"✓ preprocess.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:12.538683Z","iopub.execute_input":"2026-05-31T15:35:12.538951Z","iopub.status.idle":"2026-05-31T15:35:12.547522Z","shell.execute_reply.started":"2026-05-31T15:35:12.538927Z","shell.execute_reply":"2026-05-31T15:35:12.546636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%bash\ncd /kaggle/working/hm-recsys\npython data/preprocess.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:35:16.866794Z","iopub.execute_input":"2026-05-31T15:35:16.867272Z","iopub.status.idle":"2026-05-31T15:38:50.584428Z","shell.execute_reply.started":"2026-05-31T15:35:16.867243Z","shell.execute_reply":"2026-05-31T15:38:50.583473Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_code = '''\nimport sys\nsys.path.insert(0, \"/kaggle/working/hm-recsys\")\n\nimport pickle, random\nimport numpy as np, pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim import Adam\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\n\nfrom config import *\nfrom models.user_tower import UserTower\nfrom models.item_tower import ItemTower\nfrom models.two_tower import TwoTowerModel\nfrom utils.metrics import recall_at_k\nfrom utils.logger import get_logger\n\nlog = get_logger(__name__)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nlog.info(f\"Device: {device}\")\n\nUSER_FEAT_COLS = [\"club_member_status\",\"fashion_news_frequency\",\"age_bin\",\"has_fn\"]\nITEM_FEAT_COLS = [\"product_type_name\",\"colour_group_name\",\"department_name\",\"index_name\",\"garment_group_name\"]\n\nclass InteractionDataset(Dataset):\n    def __init__(self, interactions, user_features, item_features):\n        self.pairs = [(u,i) for u,items in interactions.items() for i in items]\n        self.uf = user_features.set_index(\"user_idx\")\n        self.itf = item_features.set_index(\"item_idx\")\n\n    def __len__(self): return len(self.pairs)\n\n    def __getitem__(self, idx):\n        u, i = self.pairs[idx]\n        ur = self.uf.loc[u]\n        ir = self.itf.loc[i]\n        return {\"user_id\": u, \"item_id\": i,\n                \"user_feats\": {c: int(ur[c]) for c in USER_FEAT_COLS},\n                \"item_feats\": {c: int(ir[c]) for c in ITEM_FEAT_COLS}}\n\ndef collate_fn(batch):\n    return (\n        torch.tensor([b[\"user_id\"] for b in batch], dtype=torch.long),\n        torch.tensor([b[\"item_id\"] for b in batch], dtype=torch.long),\n        {k: torch.tensor([b[\"user_feats\"][k] for b in batch], dtype=torch.long) for k in USER_FEAT_COLS},\n        {k: torch.tensor([b[\"item_feats\"][k] for b in batch], dtype=torch.long) for k in ITEM_FEAT_COLS},\n    )\n\ndef build_model(uf, itf, n_users, n_items):\n    ufd = {c: int(uf[c].max())+1 for c in USER_FEAT_COLS}\n    ifd = {c: int(itf[c].max())+1 for c in ITEM_FEAT_COLS}\n    ut = UserTower(n_users, ufd, EMBEDDING_DIM, USER_HIDDEN_DIMS, DROPOUT)\n    it = ItemTower(n_items, ifd, EMBEDDING_DIM, ITEM_HIDDEN_DIMS, DROPOUT)\n    return TwoTowerModel(ut, it, TEMPERATURE).to(device)\n\n@torch.no_grad()\ndef evaluate(model, val_int, uf, itf, n_items, k=100):\n    model.eval()\n    all_ids = torch.arange(n_items, device=device)\n    itf_idx = itf.set_index(\"item_idx\").reindex(range(n_items))\n    ifeats = {c: torch.tensor(itf_idx[c].fillna(0).values, dtype=torch.long, device=device) for c in ITEM_FEAT_COLS}\n    all_item_emb = model.get_item_embedding(all_ids, ifeats)\n    uf_idx = uf.set_index(\"user_idx\")\n    recalls = []\n    for u in random.sample(list(val_int.keys()), min(500, len(val_int))):\n        if u not in uf_idx.index: continue\n        row = uf_idx.loc[u]\n        uid_t = torch.tensor([u], device=device)\n        uft   = {c: torch.tensor([int(row[c])], device=device) for c in USER_FEAT_COLS}\n        u_emb = model.get_user_embedding(uid_t, uft)\n        scores = (u_emb @ all_item_emb.T).squeeze(0)\n        top_k = scores.topk(k).indices.cpu().numpy().tolist()\n        recalls.append(recall_at_k(set(val_int[u]), top_k, k))\n    return float(np.mean(recalls))\n\ndef train():\n    log.info(\"Loading data...\")\n    uf  = pd.read_parquet(PROCESSED_DIR/\"user_features.parquet\")\n    itf = pd.read_parquet(PROCESSED_DIR/\"item_features.parquet\")\n    with open(PROCESSED_DIR/\"train_interactions.pkl\",\"rb\") as f: train_int = pickle.load(f)\n    with open(PROCESSED_DIR/\"val_interactions.pkl\",\"rb\") as f:   val_int   = pickle.load(f)\n    with open(PROCESSED_DIR/\"user_enc.pkl\",\"rb\") as f: user_enc = pickle.load(f)\n    with open(PROCESSED_DIR/\"item_enc.pkl\",\"rb\") as f: item_enc = pickle.load(f)\n\n    n_users, n_items = len(user_enc.classes_), len(item_enc.classes_)\n    dataset = InteractionDataset(train_int, uf, itf)\n    loader  = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True,\n                         num_workers=NUM_WORKERS, collate_fn=collate_fn, pin_memory=True)\n    model   = build_model(uf, itf, n_users, n_items)\n    opt     = Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)\n    sched   = CosineAnnealingLR(opt, T_max=EPOCHS)\n\n    log.info(f\"Training: {len(dataset):,} pairs | {n_users:,} users | {n_items:,} items\")\n    best, patience_ctr = 0.0, 0\n\n    for epoch in range(1, EPOCHS+1):\n        model.train()\n        total = 0.0\n        for step, (uids, iids, ufeats, ifeats) in enumerate(loader):\n            uids   = uids.to(device)\n            iids   = iids.to(device)\n            ufeats = {k:v.to(device) for k,v in ufeats.items()}\n            ifeats = {k:v.to(device) for k,v in ifeats.items()}\n            opt.zero_grad()\n            loss, _, _ = model(uids, ufeats, iids, ifeats)\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n            opt.step()\n            total += loss.item()\n            if (step+1) % 300 == 0:\n                log.info(f\"  Ep{epoch} step{step+1}/{len(loader)} loss={loss.item():.4f}\")\n        sched.step()\n        recall = evaluate(model, val_int, uf, itf, n_items)\n        log.info(f\"Epoch {epoch:02d} | loss={total/len(loader):.4f} | Recall@100={recall:.4f}\")\n        if recall > best:\n            best = recall; patience_ctr = 0\n            torch.save(model.state_dict(), ARTIFACT_DIR/\"two_tower.pt\")\n            log.info(f\"  ✓ Best saved (Recall@100={best:.4f})\")\n        else:\n            patience_ctr += 1\n            if patience_ctr >= PATIENCE:\n                log.info(\"Early stopping.\"); break\n\n    import json\n    meta = {\"user_feat_cols\": USER_FEAT_COLS, \"item_feat_cols\": ITEM_FEAT_COLS,\n            \"num_users\": n_users, \"num_items\": n_items}\n    with open(ARTIFACT_DIR/\"model_meta.json\",\"w\") as f: json.dump(meta, f)\n    log.info(f\"Done. Best Recall@100={best:.4f}\")\n\ntrain()\n'''\n\nwith open(\"/kaggle/working/hm-recsys/retrieval/__init__.py\", \"w\") as f: f.write(\"\")\nwith open(\"/kaggle/working/hm-recsys/retrieval/train.py\", \"w\") as f:\n    f.write(train_code)\nprint(\"✓ train.py written\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:45:54.399261Z","iopub.execute_input":"2026-05-31T15:45:54.400061Z","iopub.status.idle":"2026-05-31T15:45:54.409443Z","shell.execute_reply.started":"2026-05-31T15:45:54.400024Z","shell.execute_reply":"2026-05-31T15:45:54.408383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fast_preprocess = '''\nimport sys\nsys.path.insert(0, \"/kaggle/working/hm-recsys\")\n\nimport pandas as pd, numpy as np, pickle\nfrom pathlib import Path\nfrom sklearn.preprocessing import LabelEncoder\nfrom config import PROCESSED_DIR\nfrom utils.logger import get_logger\n\nlog = get_logger(__name__)\nDATA_DIR = Path(\"/kaggle/input/competitions/h-and-m-personalized-fashion-recommendations\")\n\nlog.info(\"Loading transactions...\")\ntx = pd.read_csv(DATA_DIR/\"transactions_train.csv\", parse_dates=[\"t_dat\"])\nlog.info(f\"Full size: {len(tx):,}\")\n\nmax_date = tx[\"t_dat\"].max()\ntx = tx[tx[\"t_dat\"] >= max_date - pd.Timedelta(weeks=10)]\nlog.info(f\"After 10-week filter: {len(tx):,}\")\n\nfor _ in range(3):\n    n = len(tx)\n    uc = tx[\"customer_id\"].value_counts()\n    ic = tx[\"article_id\"].value_counts()\n    tx = tx[tx[\"customer_id\"].isin(uc[uc>=5].index) & tx[\"article_id\"].isin(ic[ic>=3].index)]\n    if len(tx)==n: break\nlog.info(f\"After filter: {len(tx):,} | {tx.customer_id.nunique():,} users | {tx.article_id.nunique():,} items\")\n\ntest_start = max_date - pd.Timedelta(weeks=1)\nval_start  = test_start - pd.Timedelta(weeks=1)\ntrain = tx[tx.t_dat < val_start]\nval   = tx[(tx.t_dat >= val_start) & (tx.t_dat < test_start)]\ntest  = tx[tx.t_dat >= test_start]\ntrain_users = set(train.customer_id); train_items = set(train.article_id)\nval  = val[val.customer_id.isin(train_users) & val.article_id.isin(train_items)]\ntest = test[test.customer_id.isin(train_users) & test.article_id.isin(train_items)]\nlog.info(f\"Train:{len(train):,} Val:{len(val):,} Test:{len(test):,}\")\n\nuser_enc = LabelEncoder().fit(list(train.customer_id.unique()))\nitem_enc = LabelEncoder().fit(list(train.article_id.unique()))\nfor df in [train, val, test]:\n    df[\"user_idx\"] = user_enc.transform(df[\"customer_id\"])\n    df[\"item_idx\"] = item_enc.transform(df[\"article_id\"])\n\ncustomers = pd.read_csv(DATA_DIR/\"customers.csv\")\narticles  = pd.read_csv(DATA_DIR/\"articles.csv\")\n\nuf = customers[customers.customer_id.isin(set(train.customer_id))].copy()\nuf[\"age\"] = uf[\"age\"].fillna(uf[\"age\"].median())\nuf[\"age_bin\"] = pd.cut(uf[\"age\"], bins=[0,20,30,40,50,60,120], labels=[\"<20\",\"20s\",\"30s\",\"40s\",\"50s\",\"60+\"]).astype(str)\nuser_feat_enc = {}\nfor c in [\"club_member_status\",\"fashion_news_frequency\",\"age_bin\"]:\n    uf[c] = uf[c].astype(str).fillna(\"unknown\")\n    enc = LabelEncoder(); uf[c] = enc.fit_transform(uf[c]); user_feat_enc[c] = enc\nuf[\"has_fn\"] = (uf[\"fashion_news_frequency\"]>0).astype(int)\nuid_map = pd.DataFrame({\"customer_id\": user_enc.classes_, \"user_idx\": range(len(user_enc.classes_))})\nuf = uf[[\"customer_id\",\"club_member_status\",\"fashion_news_frequency\",\"age_bin\",\"has_fn\"]].merge(uid_map, on=\"customer_id\", how=\"inner\")\n\nitf = articles[articles.article_id.isin(set(train.article_id))].copy()\nitem_cat_cols = [\"product_type_name\",\"colour_group_name\",\"department_name\",\"index_name\",\"garment_group_name\"]\nitem_feat_enc = {}\nfor c in item_cat_cols:\n    itf[c] = itf[c].astype(str).fillna(\"unknown\")\n    enc = LabelEncoder(); itf[c] = enc.fit_transform(itf[c]); item_feat_enc[c] = enc\niid_map = pd.DataFrame({\"article_id\": item_enc.classes_, \"item_idx\": range(len(item_enc.classes_))})\nitf = itf[[\"article_id\"]+item_cat_cols].merge(iid_map, on=\"article_id\", how=\"inner\")\n\ntrain_int = train.groupby(\"user_idx\")[\"item_idx\"].apply(sorted).to_dict()\nval_int   = val.groupby(\"user_idx\")[\"item_idx\"].apply(sorted).to_dict()\ntest_int  = test.groupby(\"user_idx\")[\"item_idx\"].apply(sorted).to_dict()\n\nPROCESSED_DIR.mkdir(parents=True, exist_ok=True)\ntrain.to_parquet(PROCESSED_DIR/\"train.parquet\", index=False)\nval.to_parquet(PROCESSED_DIR/\"val.parquet\", index=False)\ntest.to_parquet(PROCESSED_DIR/\"test.parquet\", index=False)\nuf.to_parquet(PROCESSED_DIR/\"user_features.parquet\", index=False)\nitf.to_parquet(PROCESSED_DIR/\"item_features.parquet\", index=False)\n\ndef save(obj, path):\n    with open(path,\"wb\") as f: pickle.dump(obj,f)\n\nfor name, obj in [(\"user_enc\",user_enc),(\"item_enc\",item_enc),\n                   (\"user_feat_encoders\",user_feat_enc),(\"item_feat_encoders\",item_feat_enc),\n                   (\"train_interactions\",train_int),(\"val_interactions\",val_int),\n                   (\"test_interactions\",test_int)]:\n    save(obj, PROCESSED_DIR/f\"{name}.pkl\")\n\nlog.info(f\"Done. Users:{len(user_enc.classes_):,}  Items:{len(item_enc.classes_):,}\")\n'''\nexec(fast_preprocess)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:52:49.136198Z","iopub.execute_input":"2026-05-31T15:52:49.136954Z","iopub.status.idle":"2026-05-31T15:53:49.601966Z","shell.execute_reply.started":"2026-05-31T15:52:49.136920Z","shell.execute_reply":"2026-05-31T15:53:49.601184Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"config_path = \"/kaggle/working/hm-recsys/config.py\"\nwith open(config_path) as f:\n    c = f.read()\nc = c.replace(\"EPOCHS        = 15\", \"EPOCHS        = 5\")\nc = c.replace(\"BATCH_SIZE    = 2048\", \"BATCH_SIZE    = 4096\")\nwith open(config_path, \"w\") as f:\n    f.write(c)\nprint(\"✓ Config patched\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:57:03.084982Z","iopub.execute_input":"2026-05-31T15:57:03.085646Z","iopub.status.idle":"2026-05-31T15:57:03.091525Z","shell.execute_reply.started":"2026-05-31T15:57:03.085616Z","shell.execute_reply":"2026-05-31T15:57:03.090657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%bash\ncd /kaggle/working/hm-recsys\npython retrieval/train.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T15:57:04.296824Z","iopub.execute_input":"2026-05-31T15:57:04.298052Z","iopub.status.idle":"2026-05-31T16:04:43.950561Z","shell.execute_reply.started":"2026-05-31T15:57:04.298005Z","shell.execute_reply":"2026-05-31T16:04:43.949828Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"faiss_code = '''\nimport sys\nsys.path.insert(0, \"/kaggle/working/hm-recsys\")\n\nimport json, pickle\nimport numpy as np, pandas as pd\nimport torch, faiss\n\nfrom config import *\nfrom models.user_tower import UserTower\nfrom models.item_tower import ItemTower\nfrom models.two_tower import TwoTowerModel\nfrom utils.logger import get_logger\n\nlog = get_logger(__name__)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nUSER_FEAT_COLS = [\"club_member_status\",\"fashion_news_frequency\",\"age_bin\",\"has_fn\"]\nITEM_FEAT_COLS = [\"product_type_name\",\"colour_group_name\",\"department_name\",\"index_name\",\"garment_group_name\"]\n\ndef load_model(meta, uf, itf):\n    ufd = {c: int(uf[c].max())+1 for c in USER_FEAT_COLS}\n    ifd = {c: int(itf[c].max())+1 for c in ITEM_FEAT_COLS}\n    from models.user_tower import UserTower\n    from models.item_tower import ItemTower\n    ut = UserTower(meta[\"num_users\"], ufd, EMBEDDING_DIM, USER_HIDDEN_DIMS, DROPOUT)\n    it = ItemTower(meta[\"num_items\"], ifd, EMBEDDING_DIM, ITEM_HIDDEN_DIMS, DROPOUT)\n    model = TwoTowerModel(ut, it, TEMPERATURE).to(device)\n    model.load_state_dict(torch.load(ARTIFACT_DIR/\"two_tower.pt\", map_location=device))\n    model.eval()\n    return model\n\nwith open(ARTIFACT_DIR/\"model_meta.json\") as f: meta = json.load(f)\nuf  = pd.read_parquet(PROCESSED_DIR/\"user_features.parquet\")\nitf = pd.read_parquet(PROCESSED_DIR/\"item_features.parquet\")\nmodel = load_model(meta, uf, itf)\nn_items = meta[\"num_items\"]\n\nlog.info(f\"Computing embeddings for {n_items:,} items...\")\nitf_idx = itf.set_index(\"item_idx\").reindex(range(n_items))\nall_embs = []\nbatch = 4096\nfor start in range(0, n_items, batch):\n    end = min(start+batch, n_items)\n    ids = torch.arange(start, end, device=device)\n    feats = {c: torch.tensor(itf_idx[c].fillna(0).values[start:end], dtype=torch.long, device=device)\n             for c in ITEM_FEAT_COLS}\n    with torch.no_grad():\n        emb = model.get_item_embedding(ids, feats).cpu().numpy()\n    all_embs.append(emb)\n\nembeddings = np.vstack(all_embs).astype(\"float32\")\nnp.save(ARTIFACT_DIR/\"item_embeddings.npy\", embeddings)\nlog.info(f\"Embeddings shape: {embeddings.shape}\")\n\nd = embeddings.shape[1]\nquantizer = faiss.IndexFlatIP(d)\nindex = faiss.IndexIVFFlat(quantizer, d, FAISS_NLIST, faiss.METRIC_INNER_PRODUCT)\nfaiss.normalize_L2(embeddings)\nindex.train(embeddings)\nindex.add(embeddings)\nindex.nprobe = FAISS_NPROBE\nfaiss.write_index(index, str(ARTIFACT_DIR/\"faiss.index\"))\nlog.info(f\"FAISS index built: {index.ntotal:,} vectors. Saved.\")\n'''\n\nwith open(\"/kaggle/working/hm-recsys/retrieval/faiss_index.py\", \"w\") as f:\n    f.write(faiss_code)\n\n# Run it immediately\nexec(faiss_code)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T16:22:40.722430Z","iopub.execute_input":"2026-05-31T16:22:40.722906Z","iopub.status.idle":"2026-05-31T16:22:41.279935Z","shell.execute_reply.started":"2026-05-31T16:22:40.722872Z","shell.execute_reply":"2026-05-31T16:22:41.278984Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ranker_code = '''\nimport sys\nsys.path.insert(0, \"/kaggle/working/hm-recsys\")\n\nimport json, pickle, random\nimport numpy as np, pandas as pd\nimport lightgbm as lgb\nimport torch, faiss\n\nfrom config import *\nfrom retrieval.faiss_index import load_model\nfrom utils.metrics import ndcg_at_k, recall_at_k\nfrom utils.logger import get_logger\n\nlog = get_logger(__name__)\n\nUSER_FEAT_COLS = [\"club_member_status\",\"fashion_news_frequency\",\"age_bin\",\"has_fn\"]\nITEM_FEAT_COLS = [\"product_type_name\",\"colour_group_name\",\"department_name\",\"index_name\",\"garment_group_name\"]\n\ndef build_user_stats(tx):\n    s = tx.groupby(\"user_idx\").agg(\n        user_purchase_count=(\"article_id\",\"count\"),\n        user_avg_price=(\"price\",\"mean\"),\n        user_recency_days=(\"t_dat\",\"max\")\n    ).reset_index()\n    s[\"user_recency_days\"] = (tx[\"t_dat\"].max() - s[\"user_recency_days\"]).dt.days\n    return s\n\ndef build_item_stats(tx):\n    s = tx.groupby(\"item_idx\").agg(\n        item_purchase_count=(\"customer_id\",\"count\"),\n        item_avg_price=(\"price\",\"mean\"),\n    ).reset_index()\n    s[\"item_log_popularity\"] = np.log1p(s[\"item_purchase_count\"])\n    return s\n\ndef get_candidates_with_scores(model, user_idx, uf_idx, index, item_embeddings, n_candidates=RETRIEVAL_TOP_K):\n    device_t = next(model.parameters()).device\n    if user_idx not in uf_idx.index: return [], []\n    row = uf_idx.loc[user_idx]\n    uid_t = torch.tensor([user_idx], device=device_t)\n    uft   = {c: torch.tensor([int(row[c])], device=device_t) for c in USER_FEAT_COLS}\n    with torch.no_grad():\n        u_emb = model.get_user_embedding(uid_t, uft).cpu().numpy().astype(\"float32\")\n    faiss.normalize_L2(u_emb)\n    _, indices = index.search(u_emb, n_candidates)\n    candidates = indices[0].tolist()\n    item_emb_matrix = item_embeddings[candidates]\n    scores = (u_emb @ item_emb_matrix.T).squeeze().tolist()\n    if isinstance(scores, float): scores = [scores]\n    return candidates, scores\n\nwith open(ARTIFACT_DIR/\"model_meta.json\") as f: meta = json.load(f)\nwith open(PROCESSED_DIR/\"train_interactions.pkl\",\"rb\") as f: train_int = pickle.load(f)\nwith open(PROCESSED_DIR/\"val_interactions.pkl\",\"rb\") as f:   val_int   = pickle.load(f)\n\nuf   = pd.read_parquet(PROCESSED_DIR/\"user_features.parquet\")\nitf  = pd.read_parquet(PROCESSED_DIR/\"item_features.parquet\")\ntx   = pd.read_parquet(PROCESSED_DIR/\"train.parquet\")\ntx[\"t_dat\"] = pd.to_datetime(tx[\"t_dat\"])\n\nmodel   = load_model(meta, uf, itf)\nindex   = faiss.read_index(str(ARTIFACT_DIR/\"faiss.index\"))\nindex.nprobe = FAISS_NPROBE\nitem_emb = np.load(ARTIFACT_DIR/\"item_embeddings.npy\")\n\nuser_stats = build_user_stats(tx)\nitem_stats  = build_item_stats(tx)\nuf_idx = uf.set_index(\"user_idx\")\nitf_idx = itf.set_index(\"item_idx\")\n\nlog.info(\"Building ranker training pairs...\")\nval_users = random.sample(list(val_int.keys()), min(3000, len(val_int)))\nrows = []\nfor u in val_users:\n    positives = set(val_int.get(u, []))\n    if not positives: continue\n    seen = set(train_int.get(u, []))\n    candidates, scores = get_candidates_with_scores(model, u, uf_idx, index, item_emb)\n    candidates = [c for c in candidates if c not in seen][:RETRIEVAL_TOP_K]\n    scores = scores[:len(candidates)]\n    for cand, score in zip(candidates, scores):\n        rows.append({\"user_idx\":u, \"item_idx\":cand, \"retrieval_score\":score, \"label\":int(cand in positives)})\n\npairs = pd.DataFrame(rows)\nlog.info(f\"Pairs: {len(pairs):,} | positives: {pairs.label.sum():,}\")\n\npairs = pairs.merge(user_stats, on=\"user_idx\", how=\"left\")\npairs = pairs.merge(item_stats, on=\"item_idx\", how=\"left\")\npairs = pairs.merge(itf[[\"item_idx\"]+ITEM_FEAT_COLS], on=\"item_idx\", how=\"left\")\npairs = pairs.merge(uf[[\"user_idx\"]+USER_FEAT_COLS], on=\"user_idx\", how=\"left\")\npairs[\"price_affinity\"] = (pairs[\"user_avg_price\"] / (pairs[\"item_avg_price\"]+1e-6)).clip(0,5)\npairs.fillna(0, inplace=True)\n\nfeature_cols = [\"retrieval_score\",\"user_purchase_count\",\"user_avg_price\",\"user_recency_days\",\n                \"item_purchase_count\",\"item_avg_price\",\"item_log_popularity\",\"price_affinity\"] + ITEM_FEAT_COLS + USER_FEAT_COLS\n\nX = pairs[feature_cols].values\ny = pairs[\"label\"].values\ngroup = pairs.groupby(\"user_idx\").size().values\n\nlog.info(\"Training LightGBM ranker...\")\ndtrain = lgb.Dataset(X, label=y, group=group, feature_name=feature_cols)\nranker = lgb.train(LGBM_PARAMS, dtrain, num_boost_round=300, callbacks=[lgb.log_evaluation(50)])\nranker.save_model(str(ARTIFACT_DIR/\"ranker.lgb\"))\nwith open(ARTIFACT_DIR/\"ranker_meta.json\",\"w\") as f:\n    json.dump({\"feature_cols\": feature_cols}, f)\n\n# Evaluate\nlog.info(\"Evaluating...\")\neval_users = random.sample(list(val_int.keys()), min(300, len(val_int)))\nndcgs, recalls = [], []\nfor u in eval_users:\n    positives = set(val_int.get(u, []))\n    if not positives: continue\n    seen = set(train_int.get(u, []))\n    candidates, scores = get_candidates_with_scores(model, u, uf_idx, index, item_emb)\n    candidates = [c for c in candidates if c not in seen][:RETRIEVAL_TOP_K]\n    scores = scores[:len(candidates)]\n    ep = pd.DataFrame({\"user_idx\":u,\"item_idx\":candidates,\"retrieval_score\":scores,\"label\":0})\n    ep = ep.merge(user_stats,on=\"user_idx\",how=\"left\").merge(item_stats,on=\"item_idx\",how=\"left\")\n    ep = ep.merge(itf[[\"item_idx\"]+ITEM_FEAT_COLS],on=\"item_idx\",how=\"left\")\n    ep = ep.merge(uf[[\"user_idx\"]+USER_FEAT_COLS],on=\"user_idx\",how=\"left\")\n    ep[\"price_affinity\"] = (ep[\"user_avg_price\"]/(ep[\"item_avg_price\"]+1e-6)).clip(0,5)\n    ep.fillna(0,inplace=True)\n    pred = ranker.predict(ep[feature_cols].values)\n    ranked = [candidates[i] for i in np.argsort(-pred)]\n    ndcgs.append(ndcg_at_k(positives, ranked, RANKER_TOP_N))\n    recalls.append(recall_at_k(positives, ranked, RANKER_TOP_N))\n\nlog.info(f\"\\\\n{'='*40}\")\nlog.info(f\"FINAL RESULTS\")\nlog.info(f\"NDCG@{RANKER_TOP_N}:   {np.mean(ndcgs):.4f}\")\nlog.info(f\"Recall@{RANKER_TOP_N}: {np.mean(recalls):.4f}\")\nlog.info(f\"{'='*40}\")\n'''\n\nwith open(\"/kaggle/working/hm-recsys/ranking/ranker.py\", \"w\") as f:\n    f.write(ranker_code)\nwith open(\"/kaggle/working/hm-recsys/ranking/__init__.py\", \"w\") as f: f.write(\"\")\n\nexec(ranker_code)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-31T16:22:47.910295Z","iopub.execute_input":"2026-05-31T16:22:47.910751Z","iopub.status.idle":"2026-05-31T16:24:05.430854Z","shell.execute_reply.started":"2026-05-31T16:22:47.910717Z","shell.execute_reply":"2026-05-31T16:24:05.430210Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}