{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":31254,"databundleVersionId":3103714,"sourceType":"competition"},{"sourceId":9334353,"sourceType":"datasetVersion","datasetId":5546448},{"sourceId":14103533,"sourceType":"datasetVersion","datasetId":8946787}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# **XỬ LÝ DỮ LIỆU H&M**","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom pathlib import Path\n\nTRANSACTIONS = \"/kaggle/input/h-and-m-personalized-fashion-recommendations/transactions_train.csv\"\n\nOUT_DIR = Path(\"/kaggle/working/hm_subset\")\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\ndtype_tx = {\n    \"customer_id\": \"string\",\n    \"article_id\": \"int32\",\n}\ntx = pd.read_csv(\n    TRANSACTIONS,\n    usecols=[\"customer_id\", \"article_id\", \"t_dat\"],\n    dtype=dtype_tx,\n    parse_dates=[\"t_dat\"]\n).rename(columns={\"customer_id\": \"user\", \"article_id\": \"item\", \"t_dat\": \"ts\"})\n\nprint(tx.shape, tx.ts.min(), tx.ts.max())","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-06T10:14:53.818715Z","iopub.execute_input":"2025-12-06T10:14:53.819021Z","iopub.status.idle":"2025-12-06T10:17:50.770028Z","shell.execute_reply.started":"2025-12-06T10:14:53.818999Z","shell.execute_reply":"2025-12-06T10:17:50.769061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MONTHS = 9 \n\nmax_date = tx[\"ts\"].max()\ncutoff = max_date - pd.DateOffset(months=MONTHS)\ntx_recent = tx[tx[\"ts\"] >= cutoff].copy()\n\nprint(f\"Max date: {max_date.date()} | Cutoff: {cutoff.date()} | Kept: {len(tx_recent):,} rows\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T10:18:25.531863Z","iopub.execute_input":"2025-12-06T10:18:25.532212Z","iopub.status.idle":"2025-12-06T10:18:27.054691Z","shell.execute_reply.started":"2025-12-06T10:18:25.532183Z","shell.execute_reply":"2025-12-06T10:18:27.053769Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def core_filter(df, u_col=\"user\", i_col=\"item\", min_u=5, min_i=5, max_loops=5):\n    prev_len = -1\n    cur = df\n    for _ in range(max_loops):\n        if len(cur) == prev_len: break\n        prev_len = len(cur)\n\n        u_cnt = cur[u_col].value_counts()\n        cur = cur[cur[u_col].isin(u_cnt[u_cnt >= min_u].index)]\n\n        i_cnt = cur[i_col].value_counts()\n        cur = cur[cur[i_col].isin(i_cnt[i_cnt >= min_i].index)]\n    return cur\n\ntx_filt = core_filter(tx_recent, min_u=5, min_i=5)\nprint(f\"After 5-core: {len(tx_filt):,} rows | users: {tx_filt.user.nunique():,} | items: {tx_filt.item.nunique():,}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T10:22:37.539509Z","iopub.execute_input":"2025-12-06T10:22:37.539874Z","iopub.status.idle":"2025-12-06T10:23:12.933997Z","shell.execute_reply.started":"2025-12-06T10:22:37.539849Z","shell.execute_reply":"2025-12-06T10:23:12.932818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1) Tạo danh sách ID duy nhất (sau khi đã 5-core)\nu_list = tx_filt[\"user\"].drop_duplicates().reset_index(drop=True)\ni_list = tx_filt[\"item\"].drop_duplicates().reset_index(drop=True)\n\n# 2) Ánh xạ về chỉ số liên tục 0-based\nuser2idx = {u: i for i, u in enumerate(u_list)}\nitem2idx = {it: i for i, it in enumerate(i_list)}\n\n# 3) Gắn cột chỉ số mới\ntx_filt[\"u\"] = tx_filt[\"user\"].map(user2idx).astype(\"int32\")\ntx_filt[\"v\"] = tx_filt[\"item\"].map(item2idx).astype(\"int32\")\n\n# 4) Sắp theo (u, ts) để chuẩn bị cho split theo thời gian ở bước sau\ntx_filt = tx_filt.sort_values([\"u\", \"ts\"]).reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T10:26:07.767194Z","iopub.execute_input":"2025-12-06T10:26:07.768253Z","iopub.status.idle":"2025-12-06T10:26:18.437665Z","shell.execute_reply.started":"2025-12-06T10:26:07.768219Z","shell.execute_reply":"2025-12-06T10:26:18.436484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"n_rows = len(tx_filt)\nn_users = tx_filt[\"u\"].nunique()\nn_items = tx_filt[\"v\"].nunique()\nrows_per_user = n_rows / n_users\n\nu_deg = tx_filt.groupby(\"u\").size()\ni_deg = tx_filt.groupby(\"v\").size()\n\nsummary = {\n    \"rows_after_5core\": n_rows,\n    \"users\": n_users,\n    \"items\": n_items,\n    \"avg_interactions_per_user\": round(rows_per_user, 2),\n    \"min_deg_user\": int(u_deg.min()),\n    \"p5_deg_user\": int(np.percentile(u_deg, 5)),\n    \"p50_deg_user\": int(np.percentile(u_deg, 50)),\n    \"p95_deg_user\": int(np.percentile(u_deg, 95)),\n    \"min_deg_item\": int(i_deg.min()),\n    \"p5_deg_item\": int(np.percentile(i_deg, 5)),\n    \"p50_deg_item\": int(np.percentile(i_deg, 50)),\n    \"p95_deg_item\": int(np.percentile(i_deg, 95)),\n    \"time_span\": (tx_filt[\"ts\"].min().date().isoformat(), tx_filt[\"ts\"].max().date().isoformat())\n}\nsummary\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T10:26:24.575764Z","iopub.execute_input":"2025-12-06T10:26:24.576795Z","iopub.status.idle":"2025-12-06T10:26:25.360638Z","shell.execute_reply.started":"2025-12-06T10:26:24.576759Z","shell.execute_reply":"2025-12-06T10:26:25.359308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"OUT_DIR = Path(\"/kaggle/working/hm_subset_final\")\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# Lưu file interactions \ntx_filt[[\"u\",\"v\",\"ts\"]].to_parquet(OUT_DIR / \"hm_9m_5core.parquet\", index=False)\n\n# Lưu mapping \npd.DataFrame({\"raw_user_id\": u_list, \"user_idx\": range(len(u_list))}).to_csv(OUT_DIR / \"user_id_map_9m.csv\", index=False)\npd.DataFrame({\"raw_item_id\": i_list, \"item_idx\": range(len(i_list))}).to_csv(OUT_DIR / \"item_id_map_9m.csv\", index=False)\n\nprint(\"Saved:\", OUT_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T11:02:15.078935Z","iopub.execute_input":"2025-12-06T11:02:15.084598Z","iopub.status.idle":"2025-12-06T11:02:19.852872Z","shell.execute_reply.started":"2025-12-06T11:02:15.084506Z","shell.execute_reply":"2025-12-06T11:02:19.851639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SUBSET_DIR = Path(\"/kaggle/working/hm_subset_final\") \nPARQUET = SUBSET_DIR / \"hm_9m_5core.parquet\"          \nOUT_DIR = Path(\"/kaggle/working/hm_split_9m\")\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\ndf = pd.read_parquet(PARQUET)\ndf = df.sort_values([\"u\", \"ts\"]).reset_index(drop=True)\n\n\n# SPLIT: val = last-1, test = last, còn lại train \ntrain_pairs, val_pairs, test_pairs = {}, {}, {}\nskipped_users = 0\n\nfor u, g in df.groupby(\"u\", sort=False):\n    items = g[\"v\"].tolist()  # giữ thứ tự thời gian và repeat\n    if len(items) < 3:\n        skipped_users += 1\n        continue\n\n    test_item = items[-1]\n    val_item  = items[-2]\n    train_items = items[:-2]\n\n    # Khi ghi train.txt, mỗi item xuất hiện MỘT lần (đúng format LightGCN/NGCF)\n    train_unique = list(dict.fromkeys(train_items))  # giữ thứ tự, remove duplicates\n\n    if len(train_unique) == 0:\n        skipped_users += 1\n        continue\n\n    train_pairs[u] = train_unique\n    val_pairs[u]   = [val_item]\n    test_pairs[u]  = [test_item]\n\ndef write_user_item_list(path, user_dict):\n    with open(path, \"w\") as f:\n        for u, items in user_dict.items():\n            f.write(str(u))\n            for it in items:\n                f.write(\" \" + str(it))\n            f.write(\"\\n\")\n\nwrite_user_item_list(OUT_DIR/\"train.txt\", train_pairs)\nwrite_user_item_list(OUT_DIR/\"val.txt\",   val_pairs)\nwrite_user_item_list(OUT_DIR/\"test.txt\",  test_pairs)\n\nstats = {\n    \"users_total_in_subset\": int(df[\"u\"].nunique()),\n    \"users_used_for_split\":  int(len(train_pairs)),\n    \"users_skipped_lt3\":     int(skipped_users),\n    \"train_lines\":           sum(1 for _ in open(OUT_DIR/\"train.txt\",\"r\")),\n    \"val_lines\":             sum(1 for _ in open(OUT_DIR/\"val.txt\",\"r\")),\n    \"test_lines\":            sum(1 for _ in open(OUT_DIR/\"test.txt\",\"r\")),\n    \"policy\": \"per-user temporal split (val = last-1, test = last), keep repeats; train.txt lists unique items\"\n}\n\nimport json, os\nwith open(OUT_DIR/\"split_manifest.json\", \"w\") as f:\n    json.dump(stats, f, indent=2)\n\nfor p in [\"train.txt\",\"val.txt\",\"test.txt\",\"split_manifest.json\"]:\n    sz = os.path.getsize(OUT_DIR/p)/1024**2\n    print(f\"{p}: {sz:.2f} MB\")\n\nrepeat_val = sum(1 for u in val_pairs if val_pairs[u][0] in set(train_pairs[u]))\nrepeat_test = sum(1 for u in test_pairs if test_pairs[u][0] in set(train_pairs[u]))\nprint({\"val_item_in_train_cnt\": repeat_val, \"test_item_in_train_cnt\": repeat_test})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T11:22:06.464100Z","iopub.execute_input":"2025-12-06T11:22:06.464499Z","iopub.status.idle":"2025-12-06T11:22:44.540740Z","shell.execute_reply.started":"2025-12-06T11:22:06.464474Z","shell.execute_reply":"2025-12-06T11:22:44.539229Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom pathlib import Path\nimport shutil\nimport math\n\n# ========= 0. PATH SETUP =========\nINPUT_DIR = Path(\"/kaggle/input/hm-subset-final\")     # Thư mục input (read-only)\nPARQUET_PATH = INPUT_DIR / \"hm_9m_5core.parquet\"      # File parquet (u, v, ts)\n\nWORK_SPLIT_DIR = Path(\"/kaggle/working/hm_splits\")    # Nơi sẽ chứa train/val/test + weights\nWORK_SPLIT_DIR.mkdir(parents=True, exist_ok=True)\n\nprint(\"INPUT_DIR:\", INPUT_DIR)\nprint(\"WORK_SPLIT_DIR:\", WORK_SPLIT_DIR)\n\n# ========= 1. COPY train/val/test.txt SANG /kaggle/working =========\nfor name in [\"train.txt\", \"val.txt\", \"test.txt\"]:\n    src = INPUT_DIR / name\n    dst = WORK_SPLIT_DIR / name\n    if src.exists():\n        shutil.copy(src, dst)\n        print(f\"✅ Copied {src} -> {dst}\")\n    else:\n        print(f\"⚠ File không tồn tại: {src}\")\n\n# ========= 2. LOAD parquet gốc (u, v, ts) =========\ndf = pd.read_parquet(PARQUET_PATH)\nprint(\"\\n=== PARQUET HEAD ===\")\nprint(df.head())\nprint(\"Min ts:\", df[\"ts\"].min(), \"| Max ts:\", df[\"ts\"].max())\n\n# ========= 3. LOAD train.txt hiện tại (ở WORK_SPLIT_DIR) =========\ntrain_path = WORK_SPLIT_DIR / \"train.txt\"\ntrain_pairs = []\n\nwith open(train_path) as f:\n    for line in f:\n        parts = line.strip().split()\n        if not parts:\n            continue\n        u = int(parts[0])\n        items = list(map(int, parts[1:]))\n        for v in items:\n            train_pairs.append((u, v))\n\nedges_train = pd.DataFrame(train_pairs, columns=[\"u\", \"v\"])\nedges_train = edges_train.drop_duplicates()\n\nprint(\"\\nSố cặp (u,v) trong train.txt:\", len(edges_train))\n\n# ========= 4. LỌC parquet chỉ giữ (u,v) có trong train.txt =========\ndf_merge = df.merge(edges_train, on=[\"u\", \"v\"], how=\"inner\")\nprint(\"Số dòng khớp (u,v) giữa parquet & train:\", len(df_merge))\n\n# ========= 5. TÍNH TIME-DECAY (half-life = 45 ngày) =========\nmax_ts = df_merge[\"ts\"].max()\nprint(\"\\nMax ts dùng làm mốc thời gian:\", max_ts)\n\nHALF_LIFE_DAYS = 45.0\nlambda_ = math.log(2.0) / HALF_LIFE_DAYS  # ln 2 / half-life\n\ndf_merge[\"age_days\"] = (max_ts - df_merge[\"ts\"]).dt.days.astype(\"float32\")\ndf_merge[\"weight_raw\"] = np.exp(-lambda_ * df_merge[\"age_days\"])\n\nprint(\"\\n=== THỐNG KÊ WEIGHT_RAW ===\")\nprint(\"Min age_days:\", df_merge[\"age_days\"].min(), \"Max:\", df_merge[\"age_days\"].max())\nprint(\"Mean weight_raw:\", df_merge[\"weight_raw\"].mean())\nprint(\"Min weight_raw:\", df_merge[\"weight_raw\"].min(), \"Max:\", df_merge[\"weight_raw\"].max())\n\n# ========= 6. GỘP THEO (u,v): cộng weight cho các lần mua lặp lại =========\nedges = (\n    df_merge\n    .groupby([\"u\", \"v\"], as_index=False)[\"weight_raw\"]\n    .sum()\n    .rename(columns={\"weight_raw\": \"weight\"})\n)\n\nprint(\"\\nEdges before norm (5 dòng đầu):\")\nprint(edges.head())\n\n# ========= 7. CHUẨN HOÁ THEO USER (tổng weight mỗi user = 1) =========\nedges[\"sum_u\"] = edges.groupby(\"u\")[\"weight\"].transform(\"sum\")\nedges[\"weight_norm\"] = edges[\"weight\"] / edges[\"sum_u\"]\n\nedges_final = edges[[\"u\", \"v\", \"weight_norm\"]].rename(columns={\"weight_norm\": \"weight\"})\n\n# ========= 8. LƯU RA CSV Ở /kaggle/working/hm_splits =========\nout_path = WORK_SPLIT_DIR / \"train_time_weights.csv\"\nedges_final.to_csv(out_path, index=False)\nprint(\"\\n✅ Saved time-decay weights to:\", out_path)\n\nsummary = {\n    \"num_train_pairs\": int(len(edges_train)),\n    \"num_weighted_edges\": int(len(edges_final)),\n    \"half_life_days\": HALF_LIFE_DAYS,\n}\nprint(\"\\n=== SUMMARY ===\")\nprint(summary)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T07:34:10.971593Z","iopub.execute_input":"2025-12-08T07:34:10.972893Z","iopub.status.idle":"2025-12-08T07:34:53.841877Z","shell.execute_reply.started":"2025-12-08T07:34:10.972821Z","shell.execute_reply":"2025-12-08T07:34:53.840478Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **XỬ LÝ DỮ LIỆU VIBRENT**","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport math\nfrom pathlib import Path\n\nVIB_ROOT = Path(\"/kaggle/input/vibrent-clothes-rental-dataset\")\nOUT_DIR  = Path(\"/kaggle/working/vibrent_cf_clean\")\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\nvib_raw = pd.read_csv(\n    VIB_ROOT / \"user_activity_triplets.csv\",\n    sep=\";\" \n)\n\nvib_raw = vib_raw[[\"customer.id\", \"outfit.id\", \"rentalPeriod.start\"]].copy()\nvib_raw = vib_raw.rename(columns={\n    \"customer.id\": \"user_raw\",\n    \"outfit.id\": \"item_raw\",\n    \"rentalPeriod.start\": \"ts\",\n})\nvib_raw[\"ts\"] = pd.to_datetime(vib_raw[\"ts\"])\n\nprint(\"==== [RAW VIBRENT] ====\")\nprint(f\"Rows raw:   {len(vib_raw):,}\")\nprint(f\"Users raw:  {vib_raw['user_raw'].nunique():,}\")\nprint(f\"Items raw:  {vib_raw['item_raw'].nunique():,}\")\nprint(f\"Earliest ts: {vib_raw['ts'].min()}\")\nprint(f\"Latest ts:   {vib_raw['ts'].max()}\")\nprint(f\"Span days:   {(vib_raw['ts'].max() - vib_raw['ts'].min()).days}\")\ndisplay(vib_raw.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T08:10:24.546725Z","iopub.execute_input":"2025-12-09T08:10:24.547067Z","iopub.status.idle":"2025-12-09T08:10:24.671294Z","shell.execute_reply.started":"2025-12-09T08:10:24.547043Z","shell.execute_reply":"2025-12-09T08:10:24.669853Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"min_interactions = 3\n\nuser_counts = vib_raw.groupby(\"user_raw\")[\"item_raw\"].count()\ngood_users = user_counts[user_counts >= min_interactions].index\n\nvib = vib_raw[vib_raw[\"user_raw\"].isin(good_users)].copy()\n\nprint(\"\\n==== [AFTER FILTER USER >= 3] ====\")\nprint(f\"Rows filtered: {len(vib):,}\")\nprint(f\"Users filtered: {vib['user_raw'].nunique():,}\")\nprint(f\"Items filtered: {vib['item_raw'].nunique():,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T08:10:48.236876Z","iopub.execute_input":"2025-12-09T08:10:48.237197Z","iopub.status.idle":"2025-12-09T08:10:48.266967Z","shell.execute_reply.started":"2025-12-09T08:10:48.237176Z","shell.execute_reply":"2025-12-09T08:10:48.265611Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"vib = vib.sort_values([\"user_raw\", \"ts\"]).reset_index(drop=True)\n\nu_list = vib[\"user_raw\"].drop_duplicates().reset_index(drop=True)\ni_list = vib[\"item_raw\"].drop_duplicates().reset_index(drop=True)\n\nuser2idx = {u: i for i, u in enumerate(u_list)}\nitem2idx = {it: i for i, it in enumerate(i_list)}\n\nvib[\"u\"] = vib[\"user_raw\"].map(user2idx).astype(\"int32\")\nvib[\"v\"] = vib[\"item_raw\"].map(item2idx).astype(\"int32\")\n\nprint(\"\\n==== [MAPPING] ====\")\nprint(f\"num_users (CF index): {vib['u'].nunique():,}\")\nprint(f\"num_items (CF index): {vib['v'].nunique():,}\")\n\n# Lưu mapping (chỉ cho user đã lọc)\npd.DataFrame({\"raw_user_id\": u_list, \"user_idx\": range(len(u_list))}).to_csv(\n    OUT_DIR / \"user_id_map_vibrent.csv\", index=False\n)\npd.DataFrame({\"raw_item_id\": i_list, \"item_idx\": range(len(i_list))}).to_csv(\n    OUT_DIR / \"item_id_map_vibrent.csv\", index=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T08:11:06.685285Z","iopub.execute_input":"2025-12-09T08:11:06.685602Z","iopub.status.idle":"2025-12-09T08:11:06.756613Z","shell.execute_reply.started":"2025-12-09T08:11:06.685580Z","shell.execute_reply":"2025-12-09T08:11:06.755618Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# SPLIT LOO: train / val / test\n# =========================\ndf = vib[[\"u\", \"v\", \"ts\"]].copy()\ndf = df.sort_values([\"u\", \"ts\"]).reset_index(drop=True)\n\ntrain_lines, val_lines, test_lines = [], [], []\ntotal_users_cf = df[\"u\"].nunique()\nused_users = 0\nskipped_users_loo = 0\n\nfor u, g in df.groupby(\"u\"):\n    items = g[\"v\"].tolist()\n    if len(items) < 3:\n        # theoretically không nên xảy ra vì đã lọc user >=3 interactions,\n        # nhưng mình vẫn check cho chắc\n        skipped_users_loo += 1\n        continue\n\n    train_items = sorted(set(items[:-2]))\n    val_item    = items[-2]\n    test_item   = items[-1]\n\n    if len(train_items) == 0:\n        skipped_users_loo += 1\n        continue\n\n    used_users += 1\n    train_lines.append(\" \".join([str(u)] + [str(it) for it in train_items]))\n    val_lines.append(f\"{u} {val_item}\")\n    test_lines.append(f\"{u} {test_item}\")\n\nsplit_dir = OUT_DIR\nwith open(split_dir / \"train.txt\", \"w\") as f:\n    f.write(\"\\n\".join(train_lines))\nwith open(split_dir / \"val.txt\", \"w\") as f:\n    f.write(\"\\n\".join(val_lines))\nwith open(split_dir / \"test.txt\", \"w\") as f:\n    f.write(\"\\n\".join(test_lines))\n\nprint(\"\\n==== [SPLIT LOO] ====\")\nprint(f\"Tổng user CF (sau filter): {total_users_cf:,}\")\nprint(f\"User thực sự có trong train/val/test: {used_users:,}\")\nprint(f\"User bị skip ở bước LOO (nếu có):    {skipped_users_loo:,}\")\nprint(\"train.txt lines:\", len(train_lines))\nprint(\"val.txt lines:  \", len(val_lines))\nprint(\"test.txt lines: \", len(test_lines))\n\n# =========================\n# LƯU INTERACTIONS (u, v, ts)\n# =========================\ndf_inter = df.sort_values([\"u\", \"ts\"]).reset_index(drop=True)\nparquet_path = OUT_DIR / \"vibrent_interactions.parquet\"\ndf_inter.to_parquet(parquet_path, index=False)\n\nprint(\"\\n==== [INTERACTIONS PARQUET] ====\")\nprint(\"Saved to:\", parquet_path)\nprint(\"Rows:\", len(df_inter))\nprint(\"Users in df_inter:\", df_inter['u'].nunique())\nprint(\"Items in df_inter:\", df_inter['v'].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T08:11:41.186427Z","iopub.execute_input":"2025-12-09T08:11:41.186782Z","iopub.status.idle":"2025-12-09T08:11:41.387708Z","shell.execute_reply.started":"2025-12-09T08:11:41.186757Z","shell.execute_reply":"2025-12-09T08:11:41.386563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# TIME-DECAY WEIGHTS (HALF-LIFE = 365 DAYS)\n# =========================\nprint(\"\\n==== [TIME-DECAY WEIGHTING] ====\")\n\ndf_all = df_inter.copy().sort_values([\"u\", \"ts\"]).reset_index(drop=True)\n\n# đọc train.txt để lấy các cặp (u,v) trong train\ntrain_pairs = []\nwith open(split_dir / \"train.txt\") as f:\n    for line in f:\n        parts = line.strip().split()\n        if not parts:\n            continue\n        u = int(parts[0])\n        items = list(map(int, parts[1:]))\n        for v_ in items:\n            train_pairs.append((u, v_))\n\nedges_train = pd.DataFrame(train_pairs, columns=[\"u\", \"v\"]).drop_duplicates()\n\nprint(\"Users in train.txt:\", len({u for u, _ in train_pairs}))\nprint(\"Unique (u,v) in train:\", len(edges_train))\n\n# merge để chỉ giữ interactions thuộc train\ndf_merge = df_all.merge(edges_train, on=[\"u\", \"v\"], how=\"inner\")\nprint(\"Interactions thuộc train:\", len(df_merge))\nprint(\"Users trong df_merge:\", df_merge['u'].nunique())\n\n# half-life\nHALF_LIFE_DAYS = 365.0\nlambda_ = math.log(2.0) / HALF_LIFE_DAYS\n\nmax_ts = df_merge[\"ts\"].max()\ndf_merge[\"age_days\"] = (max_ts - df_merge[\"ts\"]).dt.days.astype(\"float32\")\ndf_merge[\"weight_raw\"] = np.exp(-lambda_ * df_merge[\"age_days\"])\n\nprint(\"\\n[Stats] age_days:\")\nprint(df_merge[\"age_days\"].describe())\nprint(\"\\n[Stats] weight_raw:\")\nprint(df_merge[\"weight_raw\"].describe())\n\n# gộp (u,v), chuẩn hóa theo user\nedges = (\n    df_merge\n    .groupby([\"u\", \"v\"], as_index=False)[\"weight_raw\"]\n    .sum()\n    .rename(columns={\"weight_raw\": \"weight\"})\n)\n\nedges[\"sum_u\"] = edges.groupby(\"u\")[\"weight\"].transform(\"sum\")\nedges[\"weight_norm\"] = edges[\"weight\"] / edges[\"sum_u\"]\nedges_final = edges[[\"u\", \"v\", \"weight_norm\"]].rename(columns={\"weight_norm\": \"weight\"})\n\nprint(\"\\n[Stats] weight_norm (sau chuẩn hóa theo user):\")\nprint(edges_final[\"weight\"].describe())\nprint(\"Users trong weights:\", edges_final[\"u\"].nunique())\n\nweights_path = OUT_DIR / \"train_time_weights_vibrent.csv\"\nedges_final.to_csv(weights_path, index=False)\n\nprint(\"\\n==== DONE VIBRENT PIPELINE (CLEAN) ====\")\nprint(\"Output folder:\", OUT_DIR)\nprint(\"  - train.txt\")\nprint(\"  - val.txt\")\nprint(\"  - test.txt\")\nprint(\"  - user_id_map_vibrent.csv\")\nprint(\"  - item_id_map_vibrent.csv\")\nprint(\"  - vibrent_interactions.parquet\")\nprint(\"  - train_time_weights_vibrent.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T08:12:43.970213Z","iopub.execute_input":"2025-12-09T08:12:43.970576Z","iopub.status.idle":"2025-12-09T08:12:44.230221Z","shell.execute_reply.started":"2025-12-09T08:12:43.970549Z","shell.execute_reply":"2025-12-09T08:12:44.228885Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# XỬ LÝ ĐẶC TRƯNG ẢNH HMM","metadata":{}},{"cell_type":"code","source":"import os\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\nfrom tqdm import tqdm\n\n# Kiểm tra device\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T06:54:14.702482Z","iopub.execute_input":"2025-12-11T06:54:14.703238Z","iopub.status.idle":"2025-12-11T06:54:21.480982Z","shell.execute_reply.started":"2025-12-11T06:54:14.703211Z","shell.execute_reply":"2025-12-11T06:54:21.480286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Competition H&M (ảnh)\nIMAGES_ROOT = Path(\"/kaggle/input/h-and-m-personalized-fashion-recommendations/images\")\n\n# Dataset custom của bạn\nSUBSET_ROOT = Path(\"/kaggle/input/hm-subset-final\")\n\n# File mapping item\nMETA_PATH = SUBSET_ROOT / \"item_id_map_9m.csv\"\n\n# Nơi lưu output\nOUT_FEAT_NPY  = Path(\"/kaggle/working/image_features_resnet50.npy\")\nOUT_FEAT_META = Path(\"/kaggle/working/image_features_meta.csv\")\n\nprint(\"Images root:\", IMAGES_ROOT)\nprint(\"Meta path  :\", META_PATH)\nmeta = pd.read_csv(META_PATH)\nprint(meta.head())\nprint(meta.columns)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T06:55:24.214061Z","iopub.execute_input":"2025-12-11T06:55:24.214338Z","iopub.status.idle":"2025-12-11T06:55:24.253644Z","shell.execute_reply.started":"2025-12-11T06:55:24.214316Z","shell.execute_reply":"2025-12-11T06:55:24.253050Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Chuẩn hóa tên cột: raw_item_id -> article_id\nmeta = pd.read_csv(META_PATH)\n\n# Đổi tên cột\nmeta = meta.rename(columns={\"raw_item_id\": \"article_id\"})\n\n# Ép kiểu cho chắc\nmeta[\"article_id\"] = meta[\"article_id\"].astype(int)\nmeta[\"item_idx\"]   = meta[\"item_idx\"].astype(int)\n\nprint(meta.head())\nprint(meta.dtypes)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T06:56:54.185622Z","iopub.execute_input":"2025-12-11T06:56:54.186230Z","iopub.status.idle":"2025-12-11T06:56:54.210529Z","shell.execute_reply.started":"2025-12-11T06:56:54.186202Z","shell.execute_reply":"2025-12-11T06:56:54.209906Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def article_id_to_path(article_id: int) -> Path:\n    \"\"\"\n    Map article_id → đường dẫn ảnh trong folder images/\n    Ví dụ: 705854001 -> '0705854001' -> folder '070' -> '0705854001.jpg'\n    \"\"\"\n    aid_str = f\"{int(article_id):010d}\"   # zero-pad 10 digits (10 chữ số)\n    folder = aid_str[:3]                  # 3 chữ số đầu làm tên folder\n    filename = aid_str + \".jpg\"\n    return IMAGES_ROOT / folder / filename\n\n# Tạo cột path\nmeta[\"image_path\"] = meta[\"article_id\"].apply(article_id_to_path)\n\n# Kiểm tra file có tồn tại không\nmeta[\"has_image\"] = meta[\"image_path\"].apply(lambda p: p.exists())\n\nprint(\"Tổng item trong meta:\", len(meta))\nprint(\"Số item có ảnh:\", meta[\"has_image\"].sum())\n\n# Lọc ra chỉ giữ item thực sự có ảnh\nmeta = meta[meta[\"has_image\"]].reset_index(drop=True)\n\nprint(\"Sau khi lọc, còn:\", len(meta))\nmeta.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T06:57:20.872622Z","iopub.execute_input":"2025-12-11T06:57:20.873202Z","iopub.status.idle":"2025-12-11T06:59:48.009152Z","shell.execute_reply.started":"2025-12-11T06:57:20.873177Z","shell.execute_reply":"2025-12-11T06:59:48.008393Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\n\n# Transform theo chuẩn ImageNet cho ResNet50\nimg_transform = transforms.Compose([\n    transforms.Resize((224, 224)),           # ResNet50 input 224x224\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],         # Mean ImageNet\n        std=[0.229, 0.224, 0.225],          # Std ImageNet\n    ),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:13:02.615458Z","iopub.execute_input":"2025-12-11T07:13:02.616135Z","iopub.status.idle":"2025-12-11T07:13:02.621628Z","shell.execute_reply.started":"2025-12-11T07:13:02.616103Z","shell.execute_reply":"2025-12-11T07:13:02.620830Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class HMImageDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path   = row[\"image_path\"]\n        item_idx   = int(row[\"item_idx\"])\n        article_id = int(row[\"article_id\"])\n\n        # Đọc ảnh\n        with Image.open(img_path) as img:\n            img = img.convert(\"RGB\")  # đảm bảo 3 kênh\n\n        # Áp dụng transform\n        if self.transform is not None:\n            img = self.transform(img)\n\n        # Trả về: tensor ảnh + 2 id để lưu sau này\n        return img, item_idx, article_id\n\ndataset = HMImageDataset(meta, transform=img_transform)\nprint(\"Số ảnh trong dataset:\", len(dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:13:05.029099Z","iopub.execute_input":"2025-12-11T07:13:05.029368Z","iopub.status.idle":"2025-12-11T07:13:05.035946Z","shell.execute_reply.started":"2025-12-11T07:13:05.029348Z","shell.execute_reply":"2025-12-11T07:13:05.035061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 128       # nếu bị thiếu VRAM thì giảm 32\nNUM_WORKERS = 2       # 2–4 tùy Kaggle\n\ndataloader = DataLoader(\n    dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,      # không cần shuffle, chỉ extract\n    num_workers=NUM_WORKERS,\n    pin_memory=True,\n)\n\nprint(\"Số batch:\", len(dataloader))\n\n# Thử lấy 1 batch để kiểm tra shape\nimgs, item_idx_batch, article_id_batch = next(iter(dataloader))\nprint(\"Batch image shape:\", imgs.shape)          # [B, 3, 224, 224]\nprint(\"Batch item_idx shape:\", item_idx_batch.shape)\nprint(\"Batch article_id shape:\", article_id_batch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:13:19.157339Z","iopub.execute_input":"2025-12-11T07:13:19.157914Z","iopub.status.idle":"2025-12-11T07:13:28.802969Z","shell.execute_reply.started":"2025-12-11T07:13:19.157888Z","shell.execute_reply":"2025-12-11T07:13:28.802104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch import nn\nfrom torchvision import models\n\n# Đảm bảo có device\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# Thử API mới của torchvision trước\ntry:\n    from torchvision.models import resnet50, ResNet50_Weights\n    weights = ResNet50_Weights.IMAGENET1K_V1\n    backbone = resnet50(weights=weights)\n    print(\"Loaded ResNet50 with new weights API\")\nexcept Exception as e:\n    print(\"New torchvision API failed, fallback to old pretrained=True. Error:\", e)\n    backbone = models.resnet50(pretrained=True)\n    print(\"Loaded ResNet50 with pretrained=True\")\n\n# Bỏ lớp fully-connected cuối cùng, chỉ lấy feature 2048-d\nbackbone.fc = nn.Identity()\n\nbackbone.to(device)\nbackbone.eval()\n\n# Tắt gradient cho nhanh & đỡ tốn VRAM\nfor p in backbone.parameters():\n    p.requires_grad = False\n\nprint(backbone.fc)   # để thấy nó là Identity()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:14:14.305023Z","iopub.execute_input":"2025-12-11T07:14:14.305596Z","iopub.status.idle":"2025-12-11T07:14:15.393374Z","shell.execute_reply.started":"2025-12-11T07:14:14.305565Z","shell.execute_reply":"2025-12-11T07:14:15.392711Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Lấy 1 batch từ dataloader\nimgs, item_idx_batch, article_id_batch = next(iter(dataloader))\nimgs = imgs.to(device)\n\nwith torch.no_grad():\n    feats = backbone(imgs)   # dự kiến: [B, 2048]\n\nprint(\"Input batch shape :\", imgs.shape)\nprint(\"Feature batch shape:\", feats.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:14:25.243711Z","iopub.execute_input":"2025-12-11T07:14:25.244299Z","iopub.status.idle":"2025-12-11T07:14:31.935129Z","shell.execute_reply.started":"2025-12-11T07:14:25.244276Z","shell.execute_reply":"2025-12-11T07:14:31.934345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom pathlib import Path\n\n# Đảm bảo lại đường dẫn output (nếu trước đó bạn đã khai báo thì có thể bỏ phần này)\nOUT_FEAT_NPY  = Path(\"/kaggle/working/image_features_resnet50.npy\")\nOUT_FEAT_META = Path(\"/kaggle/working/image_features_meta.csv\")\n\nall_features   = []\nall_item_idx   = []\nall_article_id = []\n\nbackbone.eval()\nwith torch.no_grad():\n    for imgs, item_idx_batch, article_id_batch in tqdm(dataloader, desc=\"Extracting features\"):\n        # Đưa ảnh lên GPU\n        imgs = imgs.to(device)\n\n        # Forward qua ResNet50 -> [B, 2048]\n        feats = backbone(imgs)\n\n        # Đưa về CPU & numpy\n        feats = feats.cpu().numpy()\n        item_idx_batch   = item_idx_batch.numpy()\n        article_id_batch = article_id_batch.numpy()\n\n        # Lưu vào list\n        all_features.append(feats)\n        all_item_idx.append(item_idx_batch)\n        all_article_id.append(article_id_batch)\n\n# Ghép tất cả batch lại\nall_features   = np.concatenate(all_features, axis=0)        # [N, 2048]\nall_item_idx   = np.concatenate(all_item_idx, axis=0)        # [N]\nall_article_id = np.concatenate(all_article_id, axis=0)      # [N]\n\nprint(\"Final feature shape   :\", all_features.shape)\nprint(\"Số item_idx   thu được:\", len(all_item_idx))\nprint(\"Số article_id thu được:\", len(all_article_id))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:16:38.267477Z","iopub.execute_input":"2025-12-11T07:16:38.268141Z","iopub.status.idle":"2025-12-11T07:28:58.377038Z","shell.execute_reply.started":"2025-12-11T07:16:38.268100Z","shell.execute_reply":"2025-12-11T07:28:58.376119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 6.1. Lưu features dạng .npy\nnp.save(OUT_FEAT_NPY, all_features)\nprint(f\"Saved features to: {OUT_FEAT_NPY}\")\n\n# 6.2. Lưu meta tương ứng mỗi dòng trong .npy\nfeat_meta = pd.DataFrame({\n    \"row_idx\": np.arange(len(all_item_idx)),   # index dòng trong .npy\n    \"item_idx\": all_item_idx,\n    \"article_id\": all_article_id,\n})\n\nfeat_meta.to_csv(OUT_FEAT_META, index=False)\nprint(f\"Saved feature meta to: {OUT_FEAT_META}\")\n\nfeat_meta.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:30:18.883629Z","iopub.execute_input":"2025-12-11T07:30:18.884467Z","iopub.status.idle":"2025-12-11T07:30:19.139249Z","shell.execute_reply.started":"2025-12-11T07:30:18.884437Z","shell.execute_reply":"2025-12-11T07:30:19.138662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load lại thử\nfeats = np.load(OUT_FEAT_NPY)\nmeta_loaded = pd.read_csv(OUT_FEAT_META)\n\nprint(\"feats.shape      :\", feats.shape)\nprint(\"meta_loaded.shape:\", meta_loaded.shape)\n\nprint(meta_loaded.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:30:27.064913Z","iopub.execute_input":"2025-12-11T07:30:27.065602Z","iopub.status.idle":"2025-12-11T07:30:27.200306Z","shell.execute_reply.started":"2025-12-11T07:30:27.065577Z","shell.execute_reply":"2025-12-11T07:30:27.199679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nfrom pathlib import Path\n\n# Thư mục nguồn trên Kaggle (chứa các file picture.***)\nSRC_DIR = Path(\"/kaggle/input/vibrent-clothes-rental-dataset/embeddings/EfficientNet_V2_L_final\")\n\n# Tên file zip sẽ tạo trong /kaggle/working\nZIP_BASE = Path(\"/kaggle/working/vibrent_image_embeddings_efficientnet_v2_l\")\n\nprint(\"Source dir exists:\", SRC_DIR.exists())\nprint(\"Zipping from:\", SRC_DIR)\n\n# Tạo file .zip\nshutil.make_archive(str(ZIP_BASE), 'zip', SRC_DIR)\n\nprint(\"Saved zip to:\", ZIP_BASE.with_suffix(\".zip\"))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T07:49:16.515671Z","iopub.execute_input":"2025-12-11T07:49:16.516114Z","iopub.status.idle":"2025-12-11T07:53:09.199000Z","shell.execute_reply.started":"2025-12-11T07:49:16.516084Z","shell.execute_reply":"2025-12-11T07:53:09.198263Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visual\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\npath = \"/kaggle/input/hm-subset-final/hm_9m_5core.parquet\"\ndf = pd.read_parquet(path)\n\nprint(\"Shape:\", df.shape)\nprint(\"Columns:\", df.columns.tolist())\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T04:35:48.877149Z","iopub.execute_input":"2025-12-18T04:35:48.877759Z","iopub.status.idle":"2025-12-18T04:35:49.716592Z","shell.execute_reply.started":"2025-12-18T04:35:48.877731Z","shell.execute_reply":"2025-12-18T04:35:49.715994Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tự detect cột user/item/time theo những tên phổ biến trong pipeline của bạn\ncols = set(df.columns)\n\nuser_col = \"user\" if \"user\" in cols else (\"customer_id\" if \"customer_id\" in cols else (\"u\" if \"u\" in cols else None))\nitem_col = \"item\" if \"item\" in cols else (\"article_id\" if \"article_id\" in cols else (\"v\" if \"v\" in cols else None))\ntime_col = \"t_dat\" if \"t_dat\" in cols else (\"ts\" if \"ts\" in cols else (\"date\" if \"date\" in cols else None))\n\nassert user_col and item_col and time_col, f\"Cannot detect columns. Found: {df.columns.tolist()}\"\nprint(\"Detected:\", user_col, item_col, time_col)\n\ndf = df[[user_col, item_col, time_col]].copy()\ndf.columns = [\"user\", \"item\", \"t_dat\"]\ndf[\"t_dat\"] = pd.to_datetime(df[\"t_dat\"])\ndf = df.sort_values([\"user\", \"item\", \"t_dat\"]).reset_index(drop=True)\n\ndf.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T04:35:58.038793Z","iopub.execute_input":"2025-12-18T04:35:58.039696Z","iopub.status.idle":"2025-12-18T04:36:02.060226Z","shell.execute_reply.started":"2025-12-18T04:35:58.039669Z","shell.execute_reply":"2025-12-18T04:36:02.059625Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# diff ngày giữa 2 lần mua liên tiếp của cùng (user,item)\ndf[\"prev_t\"] = df.groupby([\"user\", \"item\"])[\"t_dat\"].shift(1)\ndf[\"gap_days\"] = (df[\"t_dat\"] - df[\"prev_t\"]).dt.days\n\n# Chỉ lấy những record thật sự là repeat (gap_days not null and >=0)\nrep = df[df[\"gap_days\"].notna() & (df[\"gap_days\"] >= 0)].copy()\n\nprint(\"Total interactions:\", len(df))\nprint(\"Repeat interactions (non-first per user-item):\", len(rep))\nprint(\"Repeat ratio:\", len(rep)/len(df))\n\n# Thống kê percentiles\ngaps = rep[\"gap_days\"].values\npct = [10, 25, 50, 75, 90, 95, 99]\nstats = {f\"p{p}\": np.percentile(gaps, p) for p in pct}\nstats[\"mean\"] = float(np.mean(gaps))\nstats[\"median(p50)\"] = float(np.median(gaps))\nstats\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T04:36:05.131083Z","iopub.execute_input":"2025-12-18T04:36:05.131770Z","iopub.status.idle":"2025-12-18T04:36:06.624546Z","shell.execute_reply.started":"2025-12-18T04:36:05.131743Z","shell.execute_reply":"2025-12-18T04:36:06.623827Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8,4))\nplt.hist(rep[\"gap_days\"], bins=100)\nplt.title(\"Repeat purchase gap days (raw histogram)\")\nplt.xlabel(\"gap_days\")\nplt.ylabel(\"count\")\nplt.show()\n\nplt.figure(figsize=(8,4))\nplt.hist(rep[\"gap_days\"], bins=200, log=True)\nplt.title(\"Repeat purchase gap days (log y-scale)\")\nplt.xlabel(\"gap_days\")\nplt.ylabel(\"count (log)\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T04:36:14.461912Z","iopub.execute_input":"2025-12-18T04:36:14.462180Z","iopub.status.idle":"2025-12-18T04:36:15.511775Z","shell.execute_reply.started":"2025-12-18T04:36:14.462158Z","shell.execute_reply":"2025-12-18T04:36:15.511153Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def weight(age_days, half_life):\n    lam = np.log(2) / half_life\n    return np.exp(-lam * age_days)\n\ncands = [15, 30, 45, 60, 90]\nages_to_check = [30, 45, 60, 90, 120, 180]\n\ntable = []\nfor hl in cands:\n    row = {\"half_life\": hl}\n    for a in ages_to_check:\n        row[f\"w@{a}d\"] = weight(a, hl)\n    table.append(row)\n\npd.DataFrame(table)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T04:36:24.100398Z","iopub.execute_input":"2025-12-18T04:36:24.101146Z","iopub.status.idle":"2025-12-18T04:36:24.113855Z","shell.execute_reply.started":"2025-12-18T04:36:24.101120Z","shell.execute_reply":"2025-12-18T04:36:24.112951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nVIB_ROOT = Path(\"/kaggle/input/vibrent-clothes-rental-dataset\")\n\n# 1) Load raw (giống bạn)\nvib_raw = pd.read_csv(VIB_ROOT / \"user_activity_triplets.csv\", sep=\";\")\nvib_raw = vib_raw[[\"customer.id\", \"outfit.id\", \"rentalPeriod.start\"]].copy()\nvib_raw = vib_raw.rename(columns={\n    \"customer.id\": \"user_raw\",\n    \"outfit.id\": \"item_raw\",\n    \"rentalPeriod.start\": \"ts\",\n})\nvib_raw[\"ts\"] = pd.to_datetime(vib_raw[\"ts\"], errors=\"coerce\")\nvib_raw = vib_raw.dropna(subset=[\"user_raw\",\"item_raw\",\"ts\"])\n\nprint(\"==== [RAW VIBRENT] ====\")\nprint(f\"Rows raw:   {len(vib_raw):,}\")\nprint(f\"Users raw:  {vib_raw['user_raw'].nunique():,}\")\nprint(f\"Items raw:  {vib_raw['item_raw'].nunique():,}\")\nprint(f\"Earliest ts: {vib_raw['ts'].min()}\")\nprint(f\"Latest ts:   {vib_raw['ts'].max()}\")\nprint(f\"Span days:   {(vib_raw['ts'].max() - vib_raw['ts'].min()).days}\")\n\n# 2) Filter user >= 3 interactions (giống bạn)\nmin_interactions = 3\nuser_counts = vib_raw.groupby(\"user_raw\")[\"item_raw\"].count()\ngood_users = user_counts[user_counts >= min_interactions].index\nvib = vib_raw[vib_raw[\"user_raw\"].isin(good_users)].copy()\n\nprint(\"\\n==== [AFTER FILTER USER >= 3] ====\")\nprint(f\"Rows filtered: {len(vib):,}\")\nprint(f\"Users filtered: {vib['user_raw'].nunique():,}\")\nprint(f\"Items filtered: {vib['item_raw'].nunique():,}\")\n\n# 3) Sort & compute return gaps (ngày giữa 2 lần thuê liên tiếp của cùng user)\nvib = vib.sort_values([\"user_raw\",\"ts\"]).reset_index(drop=True)\nvib[\"prev_ts\"] = vib.groupby(\"user_raw\")[\"ts\"].shift(1)\nvib[\"gap_days\"] = (vib[\"ts\"] - vib[\"prev_ts\"]).dt.days\ngaps = vib[\"gap_days\"].dropna().astype(int)\n\nprint(\"\\n==== [RETURN GAP STATS] ====\")\nprint(f\"#gaps (user consecutive rentals): {len(gaps):,}\")\n\ndef pct_summary(x):\n    return {\n        \"p10\": float(np.percentile(x, 10)),\n        \"p25\": float(np.percentile(x, 25)),\n        \"p50\": float(np.percentile(x, 50)),\n        \"p75\": float(np.percentile(x, 75)),\n        \"p90\": float(np.percentile(x, 90)),\n        \"p95\": float(np.percentile(x, 95)),\n        \"p99\": float(np.percentile(x, 99)),\n        \"mean\": float(np.mean(x)),\n        \"median\": float(np.median(x)),\n        \"max\": float(np.max(x)),\n    }\n\nstats = pct_summary(gaps.values)\ndisplay(pd.DataFrame([stats]))\n\n# 4) Plot: histogram + CDF (rất hữu ích để “justify” half-life)\nplt.figure()\nplt.hist(gaps.clip(0, 365), bins=60)\nplt.title(\"Vibrent: gap days between consecutive rentals (clipped to 0..365)\")\nplt.xlabel(\"gap_days\")\nplt.ylabel(\"count\")\nplt.show()\n\n# log-scale histogram (nếu tail dài)\nplt.figure()\nplt.hist(gaps[gaps>0], bins=60, log=True)\nplt.title(\"Vibrent: gap_days histogram (log count, gap>0)\")\nplt.xlabel(\"gap_days\")\nplt.ylabel(\"count (log)\")\nplt.show()\n\n# CDF\nx_sorted = np.sort(gaps.values)\ncdf = np.arange(1, len(x_sorted)+1)/len(x_sorted)\nplt.figure()\nplt.plot(x_sorted, cdf)\nplt.title(\"Vibrent: CDF of gap_days\")\nplt.xlabel(\"gap_days\")\nplt.ylabel(\"CDF\")\nplt.xlim(0, np.percentile(x_sorted, 99))  # zoom tới p99 cho dễ nhìn\nplt.show()\n\n# 5) Table weights for candidate half-life values\ndef weight_at(days, half_life):\n    return 2 ** (-days/half_life)  # same as exp(-ln2*days/hl)\n\ncandidate_hl = [30, 60, 90, 180, 365, 540, 730]\nprobe_days = [30, 90, 180, 365, 540, 730]\n\nrows = []\nfor hl in candidate_hl:\n    row = {\"half_life\": hl}\n    for d in probe_days:\n        row[f\"w@{d}d\"] = weight_at(d, hl)\n    rows.append(row)\n\ndf_w = pd.DataFrame(rows)\ndisplay(df_w)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T05:36:07.813050Z","iopub.execute_input":"2025-12-18T05:36:07.813940Z","iopub.status.idle":"2025-12-18T05:36:09.123614Z","shell.execute_reply.started":"2025-12-18T05:36:07.813903Z","shell.execute_reply":"2025-12-18T05:36:09.122611Z"}},"outputs":[],"execution_count":null}]}