{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":31254,"databundleVersionId":3103714}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:05:24.751427Z","iopub.execute_input":"2026-04-29T20:05:24.751654Z","iopub.status.idle":"2026-04-29T20:05:25.821163Z","shell.execute_reply.started":"2026-04-29T20:05:24.751631Z","shell.execute_reply":"2026-04-29T20:05:25.820354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install implicit","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:05:25.822671Z","iopub.execute_input":"2026-04-29T20:05:25.823084Z","iopub.status.idle":"2026-04-29T20:12:00.578704Z","shell.execute_reply.started":"2026-04-29T20:05:25.823046Z","shell.execute_reply":"2026-04-29T20:12:00.577768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom scipy.sparse import csr_matrix\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\nfrom sklearn.compose import ColumnTransformer\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.neighbors import NearestNeighbors\nimport implicit\nfrom implicit.nearest_neighbours import bm25_weight\n\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\nDATA_DIR = \"/kaggle/input/competitions/h-and-m-personalized-fashion-recommendations\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:12:00.580174Z","iopub.execute_input":"2026-04-29T20:12:00.580768Z","iopub.status.idle":"2026-04-29T20:12:02.631710Z","shell.execute_reply.started":"2026-04-29T20:12:00.580739Z","shell.execute_reply":"2026-04-29T20:12:02.631135Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load only recent transactions to fit in memory\n# Full file is ~3.5GB; last 3 months is ~500MB and trains in reasonable time\nCUTOFF = pd.Timestamp(\"2020-06-22\")  # last ~3 months of data\n\ntransactions = pd.read_csv(\n    f\"{DATA_DIR}/transactions_train.csv\",\n    parse_dates=[\"t_dat\"],\n    dtype={\"article_id\": str, \"customer_id\": str}, #article_id is string so that it doesn't cut off IDs starting with 0\n)\ntransactions = transactions[transactions[\"t_dat\"] >= CUTOFF].reset_index(drop=True)\n\ncustomers = pd.read_csv(f\"{DATA_DIR}/customers.csv\")\nprint(f\"Transactions: {len(transactions):,}\")\nprint(f\"Customers: {len(customers):,}\")\nprint(f\"Date range: {transactions['t_dat'].min()} → {transactions['t_dat'].max()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:12:02.632553Z","iopub.execute_input":"2026-04-29T20:12:02.632972Z","iopub.status.idle":"2026-04-29T20:13:09.925637Z","shell.execute_reply.started":"2026-04-29T20:12:02.632949Z","shell.execute_reply":"2026-04-29T20:13:09.924756Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TEST_DAYS = 7\nsplit_date = transactions[\"t_dat\"].max() - pd.Timedelta(days=TEST_DAYS)\n\ntrain_tx = transactions[transactions[\"t_dat\"] <= split_date].reset_index(drop=True)\ntest_tx  = transactions[transactions[\"t_dat\"] >  split_date].reset_index(drop=True)\n\nprint(f\"Train: {len(train_tx):,} | Test: {len(test_tx):,}\")\nprint(f\"Split at: {split_date.date()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:09.928046Z","iopub.execute_input":"2026-04-29T20:13:09.928415Z","iopub.status.idle":"2026-04-29T20:13:10.316514Z","shell.execute_reply.started":"2026-04-29T20:13:09.928391Z","shell.execute_reply":"2026-04-29T20:13:10.315731Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Test users: {test_tx['customer_id'].nunique():,}\")\nprint(f\"Test items: {test_tx['article_id'].nunique():,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:10.317453Z","iopub.execute_input":"2026-04-29T20:13:10.317830Z","iopub.status.idle":"2026-04-29T20:13:10.373136Z","shell.execute_reply.started":"2026-04-29T20:13:10.317807Z","shell.execute_reply":"2026-04-29T20:13:10.372457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class InteractionPreprocessor:\n    \"\"\"\n    Builds time-decayed implicit feedback matrix from transactions.\n\n    Formula:\n        days_since      = (t_max - t_dat).days\n        time_weight     = exp(-lambda * days_since)       (lambda = ln2 / half_life)\n        r_ui            = sum over transactions (time_weight)\n        confidence      = 1 + alpha * r_ui    (applied implicitly by ALS via alpha)\n    \"\"\"\n\n    def __init__(self, half_life_days: int = 30, use_bm25: bool = True,\n                 bm25_K1: float = 100.0, bm25_B: float = 0.8):\n        self.half_life_days = half_life_days\n        self.use_bm25 = use_bm25\n        self.bm25_K1 = bm25_K1\n        self.bm25_B = bm25_B\n        self.t_max_ = None\n        self.n_users_ = 0\n        self.n_items_ = 0\n        self.user_categories_ = None\n        self.item_categories_ = None\n        self._user_idx_map = None\n        self._item_idx_map = None\n\n    def fit_transform(self, tx: pd.DataFrame) -> csr_matrix:\n        df = tx[[\"customer_id\", \"article_id\", \"t_dat\"]].copy()\n        df[\"t_dat\"] = pd.to_datetime(df[\"t_dat\"])\n        self.t_max_ = df[\"t_dat\"].max()\n\n        # time decay\n        lam = np.log(2) / self.half_life_days\n        days = (self.t_max_ - df[\"t_dat\"]).dt.days.values\n        days = np.maximum(days, 0)  # guard against any future-dated rows\n        df[\"w\"] = np.exp(-lam * days).astype(np.float32)\n\n        # encode ids using categorical codes (faster + lighter than LabelEncoder)\n        df[\"customer_id\"] = df[\"customer_id\"].astype(\"category\")\n        df[\"article_id\"]  = df[\"article_id\"].astype(\"category\")\n        self.user_categories_ = df[\"customer_id\"].cat.categories\n        self.item_categories_ = df[\"article_id\"].cat.categories\n        df[\"u\"] = df[\"customer_id\"].cat.codes.astype(np.int32)\n        df[\"i\"] = df[\"article_id\"].cat.codes.astype(np.int32)\n        self.n_users_ = len(self.user_categories_)\n        self.n_items_ = len(self.item_categories_)\n\n        # aggregate (user, item) -> sum of weights\n        agg = df.groupby([\"u\", \"i\"], sort=False)[\"w\"].sum().reset_index()\n        mat = csr_matrix(\n            (agg[\"w\"].values.astype(np.float32),\n             (agg[\"u\"].values, agg[\"i\"].values)),\n            shape=(self.n_users_, self.n_items_),\n        )\n\n        # BM25 reweighting\n        if self.use_bm25:\n            mat = bm25_weight(mat, K1=self.bm25_K1, B=self.bm25_B).tocsr()\n\n        return mat\n\n    def user_idx(self, customer_id):\n        \"\"\"Look up internal index for a customer_id, or None if unknown.\"\"\"\n        if self._user_idx_map is None:\n            self._user_idx_map = {c: i for i, c in enumerate(self.user_categories_)}\n        return self._user_idx_map.get(customer_id)\n\n    def item_idx(self, article_id):\n        \"\"\"Look up internal index for an article_id, or None if unknown.\"\"\"\n        if self._item_idx_map is None:\n            self._item_idx_map = {c: i for i, c in enumerate(self.item_categories_)}\n        return self._item_idx_map.get(article_id)\n\n    def item_ids(self, idx_array: np.ndarray) -> np.ndarray:\n        \"\"\"Convert internal item indices back to original article_ids.\"\"\"\n        return self.item_categories_.values[idx_array]\n\n    def user_ids(self, idx_array: np.ndarray) -> np.ndarray:\n        \"\"\"Convert internal user indices back to original customer_ids.\"\"\"\n        return self.user_categories_.values[idx_array]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:10.374124Z","iopub.execute_input":"2026-04-29T20:13:10.374496Z","iopub.status.idle":"2026-04-29T20:13:10.459196Z","shell.execute_reply.started":"2026-04-29T20:13:10.374473Z","shell.execute_reply":"2026-04-29T20:13:10.458346Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"prep = InteractionPreprocessor(\n    half_life_days=30,\n    use_bm25=True,\n    bm25_K1=100.0,\n    bm25_B=0.8,\n)\nuser_item = prep.fit_transform(train_tx)\n\nprint(f\"Matrix shape: {user_item.shape}\")\nprint(f\"NNZ: {user_item.nnz:,}\")\ndensity = user_item.nnz / (user_item.shape[0] * user_item.shape[1]) * 100\nprint(f\"Density: {density:.4f}%\")\nprint(f\"Value distribution after preprocessing:\")\nprint(f\"  min:    {user_item.data.min():.3f}\")\nprint(f\"  max:    {user_item.data.max():.3f}\")\nprint(f\"  mean:   {user_item.data.mean():.3f}\")\nprint(f\"  median: {np.median(user_item.data):.3f}\")\nprint(f\"  p99:    {np.percentile(user_item.data, 99):.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:10.460257Z","iopub.execute_input":"2026-04-29T20:13:10.460578Z","iopub.status.idle":"2026-04-29T20:13:13.868100Z","shell.execute_reply.started":"2026-04-29T20:13:10.460547Z","shell.execute_reply":"2026-04-29T20:13:13.867454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_als(user_item: csr_matrix,\n              factors: int = 64,\n              regularization: float = 0.05,\n              iterations: int = 20,\n              alpha: float = 10.0,\n              use_gpu: bool | None = None,\n              show_progress: bool = True,\n              random_state: int = 42):\n    \"\"\"\n    Trains Implicit ALS (Hu, Koren, Volinsky 2008).\n\n    Note on alpha: defaults assume BM25 + time-decay preprocessing where matrix\n    values are roughly in [0, 5]. With raw counts, use alpha in [40, 100].\n    \"\"\"\n    if use_gpu is None:\n        try:\n            import implicit.gpu\n            use_gpu = implicit.gpu.HAS_CUDA\n        except (ImportError, AttributeError):\n            use_gpu = False\n\n    print(f\"Training ALS: factors={factors}, reg={regularization}, \"\n          f\"iter={iterations}, alpha={alpha}, gpu={use_gpu}\")\n\n    model = implicit.als.AlternatingLeastSquares(\n        factors=factors,\n        regularization=regularization,\n        iterations=iterations,\n        alpha=alpha,\n        use_gpu=use_gpu,\n        random_state=random_state,\n    )\n    model.fit(user_item, show_progress=show_progress)\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T22:17:02.449378Z","iopub.execute_input":"2026-04-29T22:17:02.449791Z","iopub.status.idle":"2026-04-29T22:17:02.455790Z","shell.execute_reply.started":"2026-04-29T22:17:02.449763Z","shell.execute_reply":"2026-04-29T22:17:02.455053Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = train_als(\n    user_item,\n    factors=64,\n    regularization=0.05,\n    iterations=20,\n    alpha=10.0,  # tuned for BM25-weighted input; bump if not using BM25\n)\n\nprint(f\"User factors: {model.user_factors.shape}\")\nprint(f\"Item factors: {model.item_factors.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:13.876230Z","iopub.execute_input":"2026-04-29T20:13:13.876541Z","iopub.status.idle":"2026-04-29T20:13:16.157106Z","shell.execute_reply.started":"2026-04-29T20:13:13.876512Z","shell.execute_reply":"2026-04-29T20:13:16.156420Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def recommend_warm(model, user_item: csr_matrix, user_idx: int, N: int = 12,\n                   filter_seen: bool = True):\n    \"\"\"Single-user recommendation. Returns (item_indices, scores) as numpy arrays.\"\"\"\n    ids, scores = model.recommend(\n        userid=user_idx,\n        user_items=user_item[user_idx],\n        N=N,\n        filter_already_liked_items=filter_seen,\n    )\n    # Convert from cupy to numpy if running on GPU\n    return np.asarray(ids), np.asarray(scores)\n\n\ndef recommend_warm_batch(model, user_item: csr_matrix, user_indices: np.ndarray,\n                         N: int = 12, filter_seen: bool = True, batch_size: int = 10_000):\n    \"\"\"\n    Batch recommendation for many users. Returns (ids_matrix, scores_matrix)\n    of shape (len(user_indices), N).\n\n    Processes in chunks to avoid memory blowup on large user sets.\n    \"\"\"\n    all_ids = np.empty((len(user_indices), N), dtype=np.int64)\n    all_scores = np.empty((len(user_indices), N), dtype=np.float32)\n\n    for start in range(0, len(user_indices), batch_size):\n        end = min(start + batch_size, len(user_indices))\n        chunk = user_indices[start:end]\n        ids, scores = model.recommend(\n            userid=chunk,\n            user_items=user_item[chunk],\n            N=N,\n            filter_already_liked_items=filter_seen,\n        )\n        all_ids[start:end] = np.asarray(ids)\n        all_scores[start:end] = np.asarray(scores)\n\n    return all_ids, all_scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:16.158369Z","iopub.execute_input":"2026-04-29T20:13:16.158753Z","iopub.status.idle":"2026-04-29T20:13:16.165516Z","shell.execute_reply.started":"2026-04-29T20:13:16.158729Z","shell.execute_reply":"2026-04-29T20:13:16.164935Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def _to_numpy(arr):\n    \"\"\"Convert cupy array to numpy if needed; passthrough for numpy.\"\"\"\n    if hasattr(arr, \"to_numpy\"):\n        return arr.to_numpy()\n    if hasattr(arr, \"get\"):\n        return arr.get()\n    return np.asarray(arr)\n\n\nclass DemographicColdStart:\n    \"\"\"\n    For new users, find k-nearest warm users by demographics, then average their\n    ALS user-factors (direction-normalized) to synthesize a latent vector.\n    Optionally blends with popularity baseline via beta parameter.\n    \"\"\"\n\n    def __init__(self, k_neighbors: int = 20,\n                 numeric_cols=(\"age\",),\n                 categorical_cols=(\"club_member_status\", \"fashion_news_frequency\")):\n        self.k = k_neighbors\n        self.numeric_cols = list(numeric_cols)\n        self.categorical_cols = list(categorical_cols)\n        self.pipe = None\n        self.nn = None\n        self.warm_user_factors_ = None\n        self.warm_user_idx_ = None\n        self.item_factors_ = None\n        self.preproc_ = None\n        self.popularity_scores_ = None  # YENİ\n\n    def fit(self, customers: pd.DataFrame, preproc, model,\n            train_tx: pd.DataFrame = None, popularity_window_days: int = 7):\n        \"\"\"\n        train_tx, popularity_window_days: popularity skorlarını hesaplamak için.\n        train_tx None ise popularity-mix kullanılamaz.\n        \"\"\"\n        # keep only warm users (present in ALS)\n        warm_ids = set(preproc.user_categories_.tolist())\n        c = customers[customers[\"customer_id\"].isin(warm_ids)].copy()\n\n        # align rows to ALS user indices\n        cid_to_u = {cid: i for i, cid in enumerate(preproc.user_categories_)}\n        c[\"u\"] = c[\"customer_id\"].map(cid_to_u).astype(np.int32)\n        c = c.sort_values(\"u\").reset_index(drop=True)\n\n        # feature pipeline\n        num_pipe = Pipeline([(\"imp\", SimpleImputer(strategy=\"median\")),\n                             (\"sc\",  StandardScaler())])\n        cat_pipe = Pipeline([(\"imp\", SimpleImputer(strategy=\"constant\", fill_value=\"unknown\")),\n                             (\"oh\",  OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False))])\n        self.pipe = ColumnTransformer([\n            (\"num\", num_pipe, self.numeric_cols),\n            (\"cat\", cat_pipe, self.categorical_cols),\n        ])\n        X = self.pipe.fit_transform(c[self.numeric_cols + self.categorical_cols])\n\n        self.nn = NearestNeighbors(n_neighbors=self.k, metric=\"cosine\")\n        self.nn.fit(X)\n\n        # cache factors as numpy (handles GPU case)\n        uf = _to_numpy(model.user_factors)\n        self.warm_user_idx_ = c[\"u\"].values\n        self.warm_user_factors_ = uf[self.warm_user_idx_]\n        self.item_factors_ = _to_numpy(model.item_factors)\n        self.preproc_ = preproc\n\n        # YENİ: popularity skorları hesapla\n        if train_tx is not None:\n            cutoff = train_tx[\"t_dat\"].max() - pd.Timedelta(days=popularity_window_days)\n            recent = train_tx[train_tx[\"t_dat\"] >= cutoff]\n            pop_counts = recent[\"article_id\"].value_counts()\n\n            self.popularity_scores_ = np.zeros(preproc.n_items_, dtype=np.float32)\n            for aid, count in pop_counts.items():\n                idx = preproc.item_idx(aid)\n                if idx is not None:\n                    self.popularity_scores_[idx] = count\n\n            # min-max normalize: [0, 1]\n            if self.popularity_scores_.max() > 0:\n                self.popularity_scores_ /= self.popularity_scores_.max()\n\n    def recommend(self, new_user_demo: dict, N: int = 12, beta: float = 1.0):\n        \"\"\"\n        beta: kNN ağırlığı.\n          1.0 = pure kNN (default, geriye uyumlu)\n          0.0 = pure popularity\n          0.2-0.4 = tipik mix\n        \"\"\"\n        row = pd.DataFrame([new_user_demo])\n        X_new = self.pipe.transform(row[self.numeric_cols + self.categorical_cols])\n\n        dist, idx = self.nn.kneighbors(X_new, n_neighbors=self.k)\n        sims = np.clip(1.0 - dist[0], 1e-6, None)\n        weights = sims / sims.sum()\n\n        # direction-normalized averaging\n        neighbor_factors = self.warm_user_factors_[idx[0]]\n        norms = np.linalg.norm(neighbor_factors, axis=1, keepdims=True)\n        norms = np.maximum(norms, 1e-9)\n        unit_factors = neighbor_factors / norms\n        synth_dir = (weights[:, None] * unit_factors).sum(axis=0)\n        synth_user = synth_dir * float(norms.mean())\n\n        kNN_scores = self.item_factors_ @ synth_user\n\n        # YENİ: popularity ile karıştır\n        if beta < 1.0 and self.popularity_scores_ is not None:\n            # min-max normalize kNN scores [0, 1]\n            kmin, kmax = kNN_scores.min(), kNN_scores.max()\n            if kmax > kmin:\n                kNN_scores_norm = (kNN_scores - kmin) / (kmax - kmin)\n            else:\n                kNN_scores_norm = np.zeros_like(kNN_scores)\n            scores = beta * kNN_scores_norm + (1 - beta) * self.popularity_scores_\n        else:\n            scores = kNN_scores\n\n        top = np.argpartition(-scores, N)[:N]\n        top = top[np.argsort(-scores[top])]\n\n        return self.preproc_.item_ids(top), scores[top]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:16.166642Z","iopub.execute_input":"2026-04-29T20:13:16.166913Z","iopub.status.idle":"2026-04-29T20:13:16.183111Z","shell.execute_reply.started":"2026-04-29T20:13:16.166892Z","shell.execute_reply":"2026-04-29T20:13:16.182332Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(type(model.user_factors))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:16.184012Z","iopub.execute_input":"2026-04-29T20:13:16.184378Z","iopub.status.idle":"2026-04-29T20:13:16.199288Z","shell.execute_reply.started":"2026-04-29T20:13:16.184345Z","shell.execute_reply":"2026-04-29T20:13:16.198563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apk(actual, predicted, k=12):\n    \"\"\"Kaggle-style Average Precision @ k.\"\"\"\n    if not actual:\n        return 0.0\n    predicted = predicted[:k]\n    score, hits = 0.0, 0\n    seen = set()\n    for i, p in enumerate(predicted):\n        if p in actual and p not in seen:\n            hits += 1\n            score += hits / (i + 1)\n            seen.add(p)\n    return score / min(len(actual), k)\n\n\ndef map_at_k(actuals, predicteds, k=12):\n    return float(np.mean([apk(a, p, k) for a, p in zip(actuals, predicteds)]))\n\n\ndef recall_at_k(actuals, predicteds, k=12):\n    vals = []\n    for a, p in zip(actuals, predicteds):\n        if not a:\n            continue\n        hits = len(set(a) & set(p[:k]))\n        vals.append(hits / len(a))\n    return float(np.mean(vals)) if vals else 0.0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:16.201664Z","iopub.execute_input":"2026-04-29T20:13:16.201931Z","iopub.status.idle":"2026-04-29T20:13:16.211125Z","shell.execute_reply.started":"2026-04-29T20:13:16.201912Z","shell.execute_reply":"2026-04-29T20:13:16.210360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Ground truth: items each user bought during the test window\ngt_per_user = (\n    test_tx.groupby(\"customer_id\")[\"article_id\"]\n    .apply(set)\n    .to_dict()\n)\n\n# Identify which test users are warm (had train history) vs naturally cold\ntrain_users = set(prep.user_categories_.tolist())  # users known to ALS\ntest_users = set(gt_per_user.keys())\n\nwarm_test_users = sorted(test_users & train_users)\ncold_test_users = sorted(test_users - train_users)\n\nprint(f\"Test users with ground truth: {len(test_users):,}\")\nprint(f\"  Warm (in train): {len(warm_test_users):,}\")\nprint(f\"  Cold (not in train): {len(cold_test_users):,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:16.212110Z","iopub.execute_input":"2026-04-29T20:13:16.212745Z","iopub.status.idle":"2026-04-29T20:13:17.387548Z","shell.execute_reply.started":"2026-04-29T20:13:16.212713Z","shell.execute_reply":"2026-04-29T20:13:17.386636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Last-week popularity (a hard-to-beat baseline on H&M)\nlast_week_start = train_tx[\"t_dat\"].max() - pd.Timedelta(days=7)\nrecent = train_tx[train_tx[\"t_dat\"] >= last_week_start]\ntop_recent = recent[\"article_id\"].value_counts().head(12).index.tolist()\n\n# All-time popularity in training window\ntop_alltime = train_tx[\"article_id\"].value_counts().head(12).index.tolist()\n\n\ndef evaluate_static_baseline(predicted_list, test_users, gt_per_user, N=12):\n    \"\"\"Score a fixed prediction list against all test users.\"\"\"\n    actuals = [list(gt_per_user[cid]) for cid in test_users if cid in gt_per_user]\n    preds = [predicted_list for _ in actuals]\n    return {\n        \"n_users\": len(actuals),\n        \"MAP@12\": map_at_k(actuals, preds, N),\n        \"Recall@12\": recall_at_k(actuals, preds, N),\n    }\n\n\nprint(\"Last-week popularity (warm test users):\")\nprint(evaluate_static_baseline(top_recent, warm_test_users, gt_per_user))\n\nprint(\"\\nAll-time popularity (warm test users):\")\nprint(evaluate_static_baseline(top_alltime, warm_test_users, gt_per_user))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:17.388679Z","iopub.execute_input":"2026-04-29T20:13:17.389006Z","iopub.status.idle":"2026-04-29T20:13:18.336666Z","shell.execute_reply.started":"2026-04-29T20:13:17.388981Z","shell.execute_reply":"2026-04-29T20:13:18.335850Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"prep:\", \"OK\" if \"prep\" in dir() else \"MISSING\")\nprint(\"user_item:\", \"OK\" if \"user_item\" in dir() else \"MISSING\")\nprint(\"model:\", \"OK\" if \"model\" in dir() else \"MISSING\")\nprint(\"train_tx:\", \"OK\" if \"train_tx\" in dir() else \"MISSING\")\nprint(\"test_tx:\", \"OK\" if \"test_tx\" in dir() else \"MISSING\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:18.337671Z","iopub.execute_input":"2026-04-29T20:13:18.337970Z","iopub.status.idle":"2026-04-29T20:13:18.342650Z","shell.execute_reply.started":"2026-04-29T20:13:18.337940Z","shell.execute_reply":"2026-04-29T20:13:18.342024Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\nuf = model.user_factors\nif hasattr(uf, \"to_numpy\"):\n    uf = uf.to_numpy()\nelif hasattr(uf, \"get\"):\n    uf = uf.get()\n\nprint(f\"User factor norms — mean: {np.linalg.norm(uf, axis=1).mean():.3f}, \"\n      f\"std: {np.linalg.norm(uf, axis=1).std():.3f}\")\nprint(f\"Any NaN? {np.isnan(uf).any()}\")\nprint(f\"Any Inf? {np.isinf(uf).any()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:18.343632Z","iopub.execute_input":"2026-04-29T20:13:18.344165Z","iopub.status.idle":"2026-04-29T20:13:18.551733Z","shell.execute_reply.started":"2026-04-29T20:13:18.344143Z","shell.execute_reply":"2026-04-29T20:13:18.551132Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def evaluate_cold_start(cold_start_model, customers, cold_test_users,\n                        gt_per_user, N=12):\n    \"\"\"\n    Evaluates naturally-cold users: those in test window with NO training history.\n    Ground truth is what they bought during the test window only.\n    \"\"\"\n    demo_map = customers.set_index(\"customer_id\").to_dict(orient=\"index\")\n\n    actuals, preds = [], []\n    skipped = 0\n    for cid in cold_test_users:\n        if cid not in demo_map or cid not in gt_per_user:\n            skipped += 1\n            continue\n        d = demo_map[cid]\n        rec_ids, _ = cold_start_model.recommend({\n            \"age\": d.get(\"age\"),\n            \"club_member_status\": d.get(\"club_member_status\"),\n            \"fashion_news_frequency\": d.get(\"fashion_news_frequency\"),\n        }, N=N)\n        preds.append(rec_ids.tolist())\n        actuals.append(list(gt_per_user[cid]))\n\n    return {\n        \"n_users\": len(actuals),\n        \"n_skipped_no_demo\": skipped,\n        \"MAP@12\": map_at_k(actuals, preds, N),\n        \"Recall@12\": recall_at_k(actuals, preds, N),\n    }\n\n\n# Train cold-start model on the warm users\ncold_start = DemographicColdStart(k_neighbors=20)\ncold_start.fit(customers, prep, model)\n\ncold_results = evaluate_cold_start(cold_start, customers, cold_test_users, gt_per_user)\nprint(\"Cold-start eval (demographic kNN):\")\nfor k, v in cold_results.items():\n    print(f\"  {k}: {v:.5f}\" if isinstance(v, float) else f\"  {k}: {v}\")\n\nprint(\"\\nPopularity baseline (cold test users):\")\nprint(evaluate_static_baseline(top_recent, cold_test_users, gt_per_user))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T20:13:18.552688Z","iopub.execute_input":"2026-04-29T20:13:18.552973Z","iopub.status.idle":"2026-04-29T20:29:57.369714Z","shell.execute_reply.started":"2026-04-29T20:13:18.552940Z","shell.execute_reply":"2026-04-29T20:29:57.368946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cold-start modelini popularity bilgisiyle yeniden fit et\ncold_start = DemographicColdStart(k_neighbors=20)\ncold_start.fit(customers, prep, model, train_tx=train_tx, popularity_window_days=7)\n\n# evaluate_cold_start fonksiyonunu beta destekleyecek şekilde güncelle\ndef evaluate_cold_start_beta(cold_start_model, customers, cold_test_users,\n                              gt_per_user, N=12, beta=1.0):\n    demo_map = customers.set_index(\"customer_id\").to_dict(orient=\"index\")\n    actuals, preds = [], []\n    skipped = 0\n    for cid in cold_test_users:\n        if cid not in demo_map or cid not in gt_per_user:\n            skipped += 1\n            continue\n        d = demo_map[cid]\n        rec_ids, _ = cold_start_model.recommend({\n            \"age\": d.get(\"age\"),\n            \"club_member_status\": d.get(\"club_member_status\"),\n            \"fashion_news_frequency\": d.get(\"fashion_news_frequency\"),\n        }, N=N, beta=beta)\n        preds.append(rec_ids.tolist())\n        actuals.append(list(gt_per_user[cid]))\n    return {\n        \"n_users\": len(actuals),\n        \"MAP@12\": map_at_k(actuals, preds, N),\n        \"Recall@12\": recall_at_k(actuals, preds, N),\n    }\n\n\n# Beta taraması\nprint(\"Beta sweep (cold users):\\n\")\nprint(f\"{'beta':>6}  {'MAP@12':>10}  {'Recall@12':>10}\")\nprint(\"-\" * 32)\n\nfor beta in [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.7, 1.0]:\n    result = evaluate_cold_start_beta(\n        cold_start, customers, cold_test_users, gt_per_user, beta=beta\n    )\n    label = \"\"\n    if beta == 0.0:\n        label = \"  (pure popularity)\"\n    elif beta == 1.0:\n        label = \"  (pure kNN)\"\n    print(f\"{beta:>6.2f}  {result['MAP@12']:>10.5f}  {result['Recall@12']:>10.5f}{label}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T22:13:39.497101Z","iopub.execute_input":"2026-04-29T22:13:39.497548Z","iopub.status.idle":"2026-04-29T22:13:45.228446Z","shell.execute_reply.started":"2026-04-29T22:13:39.497526Z","shell.execute_reply":"2026-04-29T22:13:45.227189Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def evaluate_warm(model, prep, user_item, test_users, gt_per_user,\n                  N=12, sample_size=5000, min_train_interactions=2,\n                  seed=42):\n    eligible = []\n    for cid in test_users:\n        u = prep.user_idx(cid)\n        if u is not None and user_item[u].nnz >= min_train_interactions:\n            eligible.append(cid)\n\n    rng = np.random.default_rng(seed)\n    if sample_size and len(eligible) > sample_size:\n        eligible = rng.choice(eligible, size=sample_size, replace=False).tolist()\n\n    user_indices = np.array([prep.user_idx(cid) for cid in eligible])\n    ids_mat, _ = recommend_warm_batch(model, user_item, user_indices, N=N)\n    predicted = [prep.item_ids(row).tolist() for row in ids_mat]\n    actuals = [list(gt_per_user[cid]) for cid in eligible]\n\n    return {\n        \"n_users\": len(eligible),\n        \"MAP@12\": map_at_k(actuals, predicted, N),\n        \"Recall@12\": recall_at_k(actuals, predicted, N),\n    }\n\n\nwarm_results = evaluate_warm(\n    model, prep, user_item,\n    test_users=warm_test_users,\n    gt_per_user=gt_per_user,\n    N=12, sample_size=5000, min_train_interactions=2,\n)\nprint(\"Warm-user eval (ALS):\")\nfor k, v in warm_results.items():\n    print(f\"  {k}: {v:.5f}\" if isinstance(v, float) else f\"  {k}: {v}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T22:13:45.229197Z","iopub.status.idle":"2026-04-29T22:13:45.229503Z","shell.execute_reply.started":"2026-04-29T22:13:45.229378Z","shell.execute_reply":"2026-04-29T22:13:45.229394Z"}},"outputs":[],"execution_count":null}]}