{"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":"none","dataSources":[{"sourceId":31254,"databundleVersionId":3103714,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"#  FABSOC Fashion Recommender – H&M","metadata":{}},{"cell_type":"markdown","source":"## Import thư viện & cấu hình chung","metadata":{}},{"cell_type":"code","source":"import os\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.neighbors import NearestNeighbors\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\n# Cố định seed cho reproducibility\nRANDOM_STATE = 42\nnp.random.seed(RANDOM_STATE)\ntorch.manual_seed(RANDOM_STATE)\n\n# Đường dẫn dataset H&M trên Kaggle\nDATA_DIR = Path(\"/kaggle/input/h-and-m-personalized-fashion-recommendations\")\nprint(\"DATA_DIR:\", DATA_DIR, \"| Exists:\", DATA_DIR.exists())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:37:55.021259Z","iopub.execute_input":"2026-01-08T10:37:55.021792Z","iopub.status.idle":"2026-01-08T10:37:55.032448Z","shell.execute_reply.started":"2026-01-08T10:37:55.021748Z","shell.execute_reply":"2026-01-08T10:37:55.031436Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Hàm phụ trợ","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\n# Thư mục ảnh H&M \nIMG_DIR = DATA_DIR / \"images\"\n\ndef get_image_path(article_id):\n    \"\"\"\n    Trả về đường dẫn file ảnh tương ứng article_id (theo cấu trúc H&M).\n    \"\"\"\n    aid_str = str(article_id).zfill(10)\n    subdir = aid_str[:3]\n    return IMG_DIR / subdir / f\"{aid_str}.jpg\"\n\n\ndef _plot_article_row(article_ids, axes_row, fontsize=8):\n    \"\"\"\n    Vẽ một hàng ảnh trên dãy axes_row (mảng Axes).\n    Nếu số cột > số article thì các ô dư sẽ tắt trục.\n    \"\"\"\n    n_cols = len(axes_row)\n\n    for col_idx, ax in enumerate(axes_row):\n        if col_idx < len(article_ids):\n            aid = article_ids[col_idx]\n            try:\n                img = Image.open(get_image_path(aid))\n                ax.imshow(img)\n            except Exception:\n                # Nếu không load được ảnh thì in id ra giữa ô\n                ax.text(0.5, 0.5, str(aid), ha=\"center\", va=\"center\")\n            ax.set_title(str(aid), fontsize=fontsize)\n        else:\n            # Ô trống\n            ax.set_title(\"\")\n        ax.axis(\"off\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:38:21.335951Z","iopub.execute_input":"2026-01-08T10:38:21.336281Z","iopub.status.idle":"2026-01-08T10:38:21.344184Z","shell.execute_reply.started":"2026-01-08T10:38:21.336253Z","shell.execute_reply":"2026-01-08T10:38:21.343327Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_cf_svd_visual(customer_id, topk=8):\n    \"\"\"\n    Trực quan hoá kết quả cho 1 user:\n    - Hàng trên: các sản phẩm thực tế user mua trong tuần test\n    - Hàng dưới: các sản phẩm được gợi ý bởi CF–SVD\n    \"\"\"\n    true_items = true_items_dict.get(customer_id, [])[:topk]\n    if len(true_items) == 0:\n        print(f\"[CF–SVD] Người dùng {customer_id} không có giao dịch trong tuần test.\")\n        return\n\n    pred_items = recommend_cf_svd(customer_id, topk=topk)\n\n    print(f\"Người dùng: {customer_id}\")\n    print(\"  Sản phẩm thực tế (tuần test):\", true_items)\n    print(\"  Sản phẩm gợi ý bởi CF–SVD :\", pred_items)\n\n    n_cols = max(len(true_items), len(pred_items))\n    if n_cols == 0:\n        return\n\n    fig, axes = plt.subplots(2, n_cols, figsize=(2.2 * n_cols, 6))\n\n    # Khi n_cols == 1 thì axes là 1D, ép về (2, n_cols)\n    axes = np.atleast_2d(axes)\n\n    # Hàng 1: thực tế\n    _plot_article_row(true_items, axes[0, :], fontsize=9)\n    # Hàng 2: gợi ý\n    _plot_article_row(pred_items, axes[1, :], fontsize=9)\n\n    # Nhãn hàng (tiếng Việt)\n    fig.text(0.5, 0.94, \"LỌC CỘNG TÁC (CF) DÙNG SVD VÀ MA TRẬN TƯƠNG TÁC USER–ITEM\",\n             ha=\"center\", va=\"center\", fontsize=14, fontweight=\"bold\")\n    fig.text(0.02, 0.70, \"Thực tế\", rotation=90,\n             ha=\"center\", va=\"center\", fontsize=11, fontweight=\"bold\")\n    fig.text(0.02, 0.25, \"Gợi ý\", rotation=90,\n             ha=\"center\", va=\"center\", fontsize=11, fontweight=\"bold\")\n\n    plt.tight_layout(rect=[0.05, 0.02, 1.0, 0.92])\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:38:24.512696Z","iopub.execute_input":"2026-01-08T10:38:24.513195Z","iopub.status.idle":"2026-01-08T10:38:24.522529Z","shell.execute_reply.started":"2026-01-08T10:38:24.513163Z","shell.execute_reply":"2026-01-08T10:38:24.521589Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nimport csv\nfrom tqdm import tqdm\n\n# =========================\n# Visualize Content-based\n# =========================\ndef show_cb_visual(customer_id, topk=8):\n    \"\"\"\n    Trực quan hoá kết quả Content-based cho 1 user:\n    - Hàng trên: sản phẩm thực tế trong tuần test\n    - Hàng dưới: sản phẩm gợi ý từ ma trận đặc trưng user–item\n    \"\"\"\n    true_items = true_items_dict.get(customer_id, [])[:topk]\n    if len(true_items) == 0:\n        print(f\"[Content-based] Người dùng {customer_id} không có giao dịch trong tuần test.\")\n        return\n\n    pred_items = recommend_content_based(customer_id, topk=topk)\n\n    print(f\"Người dùng: {customer_id}\")\n    print(\"  Sản phẩm thực tế (tuần test):\", true_items)\n    print(\"  Sản phẩm gợi ý Content-based:\", pred_items)\n\n    n_cols = max(len(true_items), len(pred_items))\n    if n_cols == 0:\n        return\n\n    fig, axes = plt.subplots(2, n_cols, figsize=(2.2 * n_cols, 6))\n\n    # axes có thể là (2,) nếu n_cols=1 => reshape về (2, 1)\n    axes = np.array(axes)\n    if axes.ndim == 1:\n        axes = axes.reshape(2, n_cols)\n\n    # Hàng 1: thực tế\n    _plot_article_row(true_items, axes[0, :], fontsize=9)\n    # Hàng 2: gợi ý\n    _plot_article_row(pred_items, axes[1, :], fontsize=9)\n\n    fig.text(\n        0.5, 0.94,\n        \"GỢI Ý DỰA TRÊN NỘI DUNG (CONTENT-BASED) – MA TRẬN ĐẶC TRƯNG USER–ITEM\",\n        ha=\"center\", va=\"center\", fontsize=14, fontweight=\"bold\"\n    )\n    fig.text(\n        0.02, 0.70, \"Thực tế\",\n        rotation=90, ha=\"center\", va=\"center\", fontsize=11, fontweight=\"bold\"\n    )\n    fig.text(\n        0.02, 0.25, \"Gợi ý\",\n        rotation=90, ha=\"center\", va=\"center\", fontsize=11, fontweight=\"bold\"\n    )\n\n    plt.tight_layout(rect=[0.05, 0.02, 1.0, 0.92])\n    plt.show()\n\n\n# =========================\n# Submission helpers\n# =========================\ndef pad_article_id(aid) -> str:\n    \"\"\"Kaggle submission thường dùng 10-digit article_id.\"\"\"\n    try:\n        return str(int(aid)).zfill(10)\n    except Exception:\n        s = str(aid)\n        return s.zfill(10) if s.isdigit() else s\n\n\ndef uniq_keep_order(xs):\n    seen = set()\n    out = []\n    for x in xs:\n        if x not in seen:\n            out.append(x)\n            seen.add(x)\n    return out\n\n\ndef ensure_12(recs, fallback_12):\n    recs = [pad_article_id(x) for x in recs]\n    recs = uniq_keep_order(recs)\n    if len(recs) < 12:\n        recs = recs + [pad_article_id(x) for x in fallback_12]\n        recs = uniq_keep_order(recs)\n    return recs[:12]\n\n\ndef write_submission_stream(customer_ids, rec_fn, fallback_12, out_csv, max_customers=None):\n    \"\"\"\n    Ghi submission dạng stream (không giữ DataFrame lớn trong RAM).\n    rec_fn(cid) -> list article_id (có thể < 12)\n    fallback_12: list 12 article_id dự phòng (popular)\n    \"\"\"\n    out_csv = str(out_csv)\n    n = len(customer_ids) if max_customers is None else min(len(customer_ids), max_customers)\n\n    with open(out_csv, \"w\", newline=\"\") as f:\n        w = csv.writer(f)\n        w.writerow([\"customer_id\", \"prediction\"])\n        for cid in tqdm(customer_ids[:n], total=n, desc=f\"Write {Path(out_csv).name}\"):\n            recs = rec_fn(cid)\n            recs12 = ensure_12(recs, fallback_12)\n            w.writerow([cid, \" \".join(recs12)])\n\n    print(\"Saved:\", out_csv, \"| rows:\", n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:38:27.198358Z","iopub.execute_input":"2026-01-08T10:38:27.198719Z","iopub.status.idle":"2026-01-08T10:38:27.213987Z","shell.execute_reply.started":"2026-01-08T10:38:27.198691Z","shell.execute_reply":"2026-01-08T10:38:27.212869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def pad_article_id(aid) -> str:\n    # Kaggle submission thường dùng 10-digit article_id\n    try:\n        return str(int(aid)).zfill(10)\n    except Exception:\n        s = str(aid)\n        return s.zfill(10) if s.isdigit() else s\n\ndef uniq_keep_order(xs):\n    seen = set()\n    out = []\n    for x in xs:\n        if x not in seen:\n            out.append(x)\n            seen.add(x)\n    return out\n\ndef ensure_12(recs, fallback_12):\n    recs = [pad_article_id(x) for x in recs]\n    recs = uniq_keep_order(recs)\n    if len(recs) < 12:\n        recs = recs + [pad_article_id(x) for x in fallback_12]\n        recs = uniq_keep_order(recs)\n    return recs[:12]\n\ndef write_submission_stream(customer_ids, rec_fn, fallback_12, out_csv, max_customers=None):\n    out_csv = str(out_csv)\n    n = len(customer_ids) if max_customers is None else min(len(customer_ids), max_customers)\n\n    with open(out_csv, \"w\", newline=\"\") as f:\n        w = csv.writer(f)\n        w.writerow([\"customer_id\", \"prediction\"])\n        for i, cid in enumerate(tqdm(customer_ids[:n], total=n, desc=f\"Write {Path(out_csv).name}\")):\n            recs = rec_fn(cid)\n            recs12 = ensure_12(recs, fallback_12)\n            w.writerow([cid, \" \".join(recs12)])\n\n    print(\"Saved:\", out_csv, \"| rows:\", n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:38:30.421012Z","iopub.execute_input":"2026-01-08T10:38:30.421603Z","iopub.status.idle":"2026-01-08T10:38:30.431236Z","shell.execute_reply.started":"2026-01-08T10:38:30.421573Z","shell.execute_reply":"2026-01-08T10:38:30.430078Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Đọc dữ liệu & chia train / validation theo thời gian","metadata":{}},{"cell_type":"code","source":"# Ta dùng 3 file chính:\n# - `articles.csv`: metadata sản phẩm\n# - `customers.csv`: thông tin khách hàng\n# - `transactions_train.csv`: lịch sử giao dịch\n#\n# Chiến lược thời gian:\n# - Lấy **6 tuần gần cuối** để train\n# - Tuần cuối cùng làm **validation / test nội bộ**","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:27:04.551820Z","iopub.execute_input":"2025-12-03T12:27:04.552709Z","iopub.status.idle":"2025-12-03T12:27:04.588122Z","shell.execute_reply.started":"2025-12-03T12:27:04.552674Z","shell.execute_reply":"2025-12-03T12:27:04.586830Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Đọc dữ liệu\narticles = pd.read_csv(DATA_DIR / \"articles.csv\")\ncustomers = pd.read_csv(DATA_DIR / \"customers.csv\")\ntransactions = pd.read_csv(\n    DATA_DIR / \"transactions_train.csv\",\n    parse_dates=[\"t_dat\"]\n)\n\nprint(\"articles:\", articles.shape)\nprint(\"customers:\", customers.shape)\nprint(\"transactions:\", transactions.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:38:33.544755Z","iopub.execute_input":"2026-01-08T10:38:33.545567Z","iopub.status.idle":"2026-01-08T10:39:49.157838Z","shell.execute_reply.started":"2026-01-08T10:38:33.545534Z","shell.execute_reply":"2026-01-08T10:39:49.156902Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Xem nhanh vài dòng\ndisplay(articles.head())\ndisplay(customers.head())\ndisplay(transactions.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:49.183111Z","iopub.execute_input":"2026-01-08T10:39:49.183444Z","iopub.status.idle":"2026-01-08T10:39:49.373932Z","shell.execute_reply.started":"2026-01-08T10:39:49.183414Z","shell.execute_reply":"2026-01-08T10:39:49.373134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Xác định mốc chia thời gian\nmax_date = transactions[\"t_dat\"].max()\ntest_start = max_date - pd.Timedelta(weeks=1)\ntrain_start = test_start - pd.Timedelta(weeks=6)\n\nprint(\"Max date      :\", max_date)\nprint(\"Train from    :\", train_start)\nprint(\"Validation from:\", test_start)\n\ntrain_df = transactions[(transactions[\"t_dat\"] >= train_start) & (transactions[\"t_dat\"] < test_start)]\ntest_df  = transactions[transactions[\"t_dat\"] >= test_start]\n\nprint(\"Train shape:\", train_df.shape)\nprint(\"Test shape :\", test_df.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:49.374805Z","iopub.execute_input":"2026-01-08T10:39:49.375107Z","iopub.status.idle":"2026-01-08T10:39:49.981103Z","shell.execute_reply.started":"2026-01-08T10:39:49.375080Z","shell.execute_reply":"2026-01-08T10:39:49.980201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Map: customer_id -> set(article_id) đã mua trong tuần test\ntrue_items_dict = (\n    test_df.groupby(\"customer_id\")[\"article_id\"]\n    .apply(list)  # dùng list để giữ thứ tự\n    .to_dict()\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:49.983448Z","iopub.execute_input":"2026-01-08T10:39:49.983693Z","iopub.status.idle":"2026-01-08T10:39:51.512338Z","shell.execute_reply.started":"2026-01-08T10:39:49.983674Z","shell.execute_reply":"2026-01-08T10:39:51.511486Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# map customer -> purchased list \ntest_true = test_df.groupby(\"customer_id\")[\"article_id\"].apply(list).to_dict()\nprint(\"test users:\", len(test_true))\n\nall_customer_ids = customers[\"customer_id\"].astype(str).values","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:51.513362Z","iopub.execute_input":"2026-01-08T10:39:51.513734Z","iopub.status.idle":"2026-01-08T10:39:53.043901Z","shell.execute_reply.started":"2026-01-08T10:39:51.513706Z","shell.execute_reply":"2026-01-08T10:39:53.043021Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## EDA ","metadata":{}},{"cell_type":"code","source":"# - Phân bố nhóm tuổi khách hàng\n# - Top product type\n# - Số giao dịch theo ngày trong 6 tuần train","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:28:25.694526Z","iopub.execute_input":"2025-12-03T12:28:25.694908Z","iopub.status.idle":"2025-12-03T12:28:25.701228Z","shell.execute_reply.started":"2025-12-03T12:28:25.694873Z","shell.execute_reply":"2025-12-03T12:28:25.699886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Nhóm tuổi khách hàng\ncust_basic = customers[[\"customer_id\", \"age\"]].copy()\ncust_basic[\"age_bucket\"] = pd.cut(\n    cust_basic[\"age\"],\n    bins=[0, 18, 25, 35, 50, 100],\n    labels=[\"<18\", \"18-25\", \"25-35\", \"35-50\", \"50+\"]\n)\n\nplt.figure(figsize=(6,4))\ncust_basic[\"age_bucket\"].value_counts().sort_index().plot(kind=\"bar\")\nplt.title(\"Phân bố nhóm tuổi khách hàng\")\nplt.xlabel(\"Nhóm tuổi\")\nplt.ylabel(\"Số khách hàng\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:53.044723Z","iopub.execute_input":"2026-01-08T10:39:53.045104Z","iopub.status.idle":"2026-01-08T10:39:53.501605Z","shell.execute_reply.started":"2026-01-08T10:39:53.045077Z","shell.execute_reply":"2026-01-08T10:39:53.500612Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Top product_type\nplt.figure(figsize=(10,4))\narticles[\"product_type_name\"].value_counts().head(15).plot(kind=\"bar\")\nplt.title(\"Top 15 Product Type phổ biến\")\nplt.xticks(rotation=60, ha=\"right\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:53.502816Z","iopub.execute_input":"2026-01-08T10:39:53.503144Z","iopub.status.idle":"2026-01-08T10:39:53.766641Z","shell.execute_reply.started":"2026-01-08T10:39:53.503118Z","shell.execute_reply":"2026-01-08T10:39:53.765422Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Transactions theo ngày (6 tuần train)\ntmp = train_df.copy()\ntmp[\"date\"] = tmp[\"t_dat\"].dt.date\ntx_per_day = tmp.groupby(\"date\")[\"t_dat\"].count()\n\nplt.figure(figsize=(10,4))\ntx_per_day.plot()\n\nplt.title(\"Số giao dịch mỗi ngày (6 tuần train)\")\nplt.xticks(rotation=45)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:53.767554Z","iopub.execute_input":"2026-01-08T10:39:53.767852Z","iopub.status.idle":"2026-01-08T10:39:54.628632Z","shell.execute_reply.started":"2026-01-08T10:39:53.767822Z","shell.execute_reply":"2026-01-08T10:39:54.627598Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Popularity Recommender","metadata":{}},{"cell_type":"code","source":"# Ý tưởng:\n# - Đếm số lần xuất hiện của mỗi `article_id` trong `train_df`\n# - Sắp xếp giảm dần → danh sách `popular_items`\n# - Với mỗi customer: recommend top-12 sản phẩm phổ biến nhất.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:28:27.685485Z","iopub.execute_input":"2025-12-03T12:28:27.685846Z","iopub.status.idle":"2025-12-03T12:28:27.690958Z","shell.execute_reply.started":"2025-12-03T12:28:27.685823Z","shell.execute_reply":"2025-12-03T12:28:27.689559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_global_popular_items(train_df, topk=1000):\n    item_pop = (\n        train_df.groupby(\"article_id\")[\"t_dat\"]\n        .count()\n        .sort_values(ascending=False)\n    )\n    popular_items = item_pop.head(topk).index.tolist()\n    return popular_items, item_pop\n\npopular_items, item_pop_series = get_global_popular_items(train_df, topk=1000)\npopular_12 = popular_items[:12]\n\nprint(\"Số item phổ biến lưu lại:\", len(popular_items))\nprint(\"Top 12 article_id phổ biến:\", popular_12)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:54.629768Z","iopub.execute_input":"2026-01-08T10:39:54.630111Z","iopub.status.idle":"2026-01-08T10:39:54.679714Z","shell.execute_reply.started":"2026-01-08T10:39:54.630083Z","shell.execute_reply":"2026-01-08T10:39:54.678584Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def recommend_popular_for_customer(customer_id, k=12, popular_items=popular_items):\n    \"\"\"\n    Trả về top-k sản phẩm phổ biến nhất (không phụ thuộc customer).\n    \"\"\"\n    return popular_items[:k]\n\n# Thử demo cho 5 khách hàng\nsample_customers = customers[\"customer_id\"].sample(5, random_state=RANDOM_STATE).tolist()\nfor cid in sample_customers:\n    print(\"Customer:\", cid)\n    print(\"  Recs:\", recommend_popular_for_customer(cid))\n    print()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:39:54.681925Z","iopub.execute_input":"2026-01-08T10:39:54.682195Z","iopub.status.idle":"2026-01-08T10:39:54.751986Z","shell.execute_reply.started":"2026-01-08T10:39:54.682173Z","shell.execute_reply":"2026-01-08T10:39:54.751208Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def rec_popularity(cid):\n    return popular_12\n\n# Xuất submission (full hoặc giới hạn để test nhanh)\nMAX_CUSTOMERS_SUB = None   \nwrite_submission_stream(\n    all_customer_ids,\n    rec_popularity,\n    fallback_12=popular_12,\n    out_csv=Path(\"/kaggle/working/submission_popularity.csv\"),\n    max_customers=MAX_CUSTOMERS_SUB\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:44:18.389548Z","iopub.execute_input":"2026-01-08T10:44:18.389916Z","iopub.status.idle":"2026-01-08T10:44:36.587645Z","shell.execute_reply.started":"2026-01-08T10:44:18.389887Z","shell.execute_reply":"2026-01-08T10:44:36.586750Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Phân tầng người dùng theo SỐ GIAO DỊCH","metadata":{}},{"cell_type":"code","source":"# Mục tiêu:\n# - Chuẩn hoá hành vi mua sắm theo mức độ hoạt động.\n# - Đánh giá / huấn luyện mô hình riêng cho từng nhóm user segment.\n#\n# Quy tắc phân tầng:\n# - New users:     0 – 20 giao dịch\n# - Medium users:  21 – 100 giao dịch\n# - Frequent users:101 – 200 giao dịch\n# - Heavy users:   201 – 500 giao dịch\n# - Super users:   > 500 giao dịch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:55:29.790207Z","iopub.execute_input":"2026-01-08T10:55:29.790515Z","iopub.status.idle":"2026-01-08T10:55:29.795373Z","shell.execute_reply.started":"2026-01-08T10:55:29.790492Z","shell.execute_reply":"2026-01-08T10:55:29.794164Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"user_tx_counts = (\n    transactions\n    .groupby(\"customer_id\")[\"article_id\"]\n    .count()\n    .reset_index(name=\"n_transactions\")\n)\n\nuser_tx_counts.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:55:32.166296Z","iopub.execute_input":"2026-01-08T10:55:32.167106Z","iopub.status.idle":"2026-01-08T10:55:43.438950Z","shell.execute_reply.started":"2026-01-08T10:55:32.167067Z","shell.execute_reply":"2026-01-08T10:55:43.437825Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Hàm phân tầng theo rule đã nêu\ndef assign_segment(n_tx: int) -> str:\n    if n_tx <= 20:\n        return \"new_users\"\n    if n_tx <= 100:\n        return \"medium_users\"\n    if n_tx <= 200:\n        return \"frequent_users\"\n    if n_tx <= 500:\n        return \"heavy_users\"\n    return \"super_users\"\n\n\nuser_tx_counts[\"user_segment\"] = user_tx_counts[\"n_transactions\"].apply(\n    assign_segment\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:55:43.440329Z","iopub.execute_input":"2026-01-08T10:55:43.440582Z","iopub.status.idle":"2026-01-08T10:55:43.693036Z","shell.execute_reply.started":"2026-01-08T10:55:43.440561Z","shell.execute_reply":"2026-01-08T10:55:43.691981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Thứ tự dùng cho vẽ biểu đồ\nsegment_order = [\n    \"new_users\",\n    \"medium_users\",\n    \"frequent_users\",\n    \"heavy_users\",\n    \"super_users\",\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:55:43.694005Z","iopub.execute_input":"2026-01-08T10:55:43.694336Z","iopub.status.idle":"2026-01-08T10:55:43.698968Z","shell.execute_reply.started":"2026-01-08T10:55:43.694315Z","shell.execute_reply":"2026-01-08T10:55:43.697846Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transactions_seg = transactions.merge(\n    user_tx_counts[[\"customer_id\", \"user_segment\"]],\n    on=\"customer_id\",\n    how=\"left\",\n)\n\ntransactions_seg.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:55:43.700746Z","iopub.execute_input":"2026-01-08T10:55:43.701056Z","iopub.status.idle":"2026-01-08T10:55:56.858516Z","shell.execute_reply.started":"2026-01-08T10:55:43.701034Z","shell.execute_reply":"2026-01-08T10:55:56.857626Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Biểu đồ phân bố segment trên TOÀN BỘ dữ liệu\nseg_counts = (\n    user_tx_counts[\"user_segment\"]\n    .value_counts()\n    .reindex(segment_order)\n)\n\nplt.figure(figsize=(8,5))\nseg_counts.plot(kind=\"bar\", color=\"skyblue\")\nplt.title(\"Phân bố phân tầng người dùng (toàn bộ dữ liệu)\")\nplt.xlabel(\"Phân tầng người dùng\")\nplt.ylabel(\"Số lượng khách hàng\")\nplt.xticks(rotation=45, ha=\"right\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:55:56.859466Z","iopub.execute_input":"2026-01-08T10:55:56.859767Z","iopub.status.idle":"2026-01-08T10:55:57.140087Z","shell.execute_reply.started":"2026-01-08T10:55:56.859734Z","shell.execute_reply":"2026-01-08T10:55:57.138997Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= Thống kê trên TOÀN BỘ dữ liệu =================\ntotal_tx_all = len(transactions)\nunique_customers_all = transactions[\"customer_id\"].nunique()\nunique_articles_all = transactions[\"article_id\"].nunique()\n\nprint(\"Thống kê trên TOÀN BỘ dữ liệu:\\n\")\nprint(f\"Tổng số giao dịch: {total_tx_all:,}\")\nprint(f\"Số khách hàng duy nhất: {unique_customers_all:,}\")\nprint(f\"Số sản phẩm duy nhất: {unique_articles_all:,}\\n\")\n\n# Tỷ lệ % khách hàng theo từng phân tầng trên toàn bộ dữ liệu\nseg_ratio_all = seg_counts / seg_counts.sum()\nprint(\"Tỷ lệ % khách hàng theo phân tầng (toàn bộ dữ liệu):\")\nprint((seg_ratio_all * 100).round(2).astype(str) + \" %\")\nprint(\"\\n\" + \"=\" * 60 + \"\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:55:57.141372Z","iopub.execute_input":"2026-01-08T10:55:57.141733Z","iopub.status.idle":"2026-01-08T10:56:05.372659Z","shell.execute_reply.started":"2026-01-08T10:55:57.141706Z","shell.execute_reply":"2026-01-08T10:56:05.371710Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Lấy mẫu người dùng theo phân tầng ","metadata":{}},{"cell_type":"code","source":"N_PER_SEGMENT = 1000  # số user tối đa chọn trong mỗi segment\n\nsampled_user_ids = []\n\nfor seg in segment_order:\n    seg_users = user_tx_counts.loc[\n        user_tx_counts[\"user_segment\"] == seg, \"customer_id\"\n    ]\n    if len(seg_users) == 0:\n        continue\n\n    n_pick = min(N_PER_SEGMENT, len(seg_users))\n    sampled = seg_users.sample(n_pick, random_state=RANDOM_STATE)\n    sampled_user_ids.append(sampled)\n\nsampled_user_ids = pd.concat(sampled_user_ids)\n\nlen(sampled_user_ids), sampled_user_ids.head()\n\ntransactions_seg = train_df.merge(\n    user_tx_counts[[\"customer_id\", \"user_segment\"]],\n    on=\"customer_id\",\n    how=\"left\",\n)\n\ntransactions_seg.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:19.992595Z","iopub.execute_input":"2026-01-08T10:56:19.992932Z","iopub.status.idle":"2026-01-08T10:56:21.885569Z","shell.execute_reply.started":"2026-01-08T10:56:19.992906Z","shell.execute_reply":"2026-01-08T10:56:21.884729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Lấy toàn bộ giao dịch của các user đã sample\nsampled_tx = transactions_seg[\n    transactions_seg[\"customer_id\"].isin(sampled_user_ids)\n]\n\nsampled_tx.shape, sampled_tx[\"customer_id\"].nunique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:23.116975Z","iopub.execute_input":"2026-01-08T10:56:23.117293Z","iopub.status.idle":"2026-01-08T10:56:23.269179Z","shell.execute_reply.started":"2026-01-08T10:56:23.117270Z","shell.execute_reply":"2026-01-08T10:56:23.268249Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Biểu đồ phân bố segment trong SAMPLE\nsample_seg_counts = (\n    user_tx_counts[user_tx_counts[\"customer_id\"].isin(sampled_user_ids)]\n    [\"user_segment\"]\n    .value_counts()\n    .reindex(segment_order)\n)\n\nplt.figure(figsize=(8, 5))\nsample_seg_counts.plot(kind=\"bar\", color=\"skyblue\")\nplt.title(\"Phân bố phân tầng người dùng (tập dữ liệu mẫu)\")\nplt.xlabel(\"Phân tầng người dùng\")\nplt.ylabel(\"Số lượng khách hàng\")\nplt.xticks(rotation=45, ha=\"right\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:25.989411Z","iopub.execute_input":"2026-01-08T10:56:25.989719Z","iopub.status.idle":"2026-01-08T10:56:26.448841Z","shell.execute_reply.started":"2026-01-08T10:56:25.989697Z","shell.execute_reply":"2026-01-08T10:56:26.447744Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= THỐNG KÊ TRÊN TẬP MẪU  =================\ntotal_tx_sample = len(sampled_tx)\nunique_customers_sample = sampled_tx[\"customer_id\"].nunique()\nunique_articles_sample = sampled_tx[\"article_id\"].nunique()\n\nprint(\"Thống kê trên TẬP DỮ LIỆU MẪU:\\n\")\nprint(f\"Tổng số giao dịch: {total_tx_sample:,}\")\nprint(f\"Số khách hàng duy nhất: {unique_customers_sample:,}\")\nprint(f\"Số sản phẩm duy nhất: {unique_articles_sample:,}\\n\")\n\nseg_ratio_sample = sample_seg_counts / sample_seg_counts.sum()\nprint(\"Tỷ lệ % khách hàng theo phân tầng (tập dữ liệu mẫu):\")\nprint((seg_ratio_sample * 100).round(2).astype(str) + \" %\")\nprint(\"\\n\" + \"=\" * 60 + \"\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:29.837736Z","iopub.execute_input":"2026-01-08T10:56:29.838085Z","iopub.status.idle":"2026-01-08T10:56:29.855826Z","shell.execute_reply.started":"2026-01-08T10:56:29.838058Z","shell.execute_reply":"2026-01-08T10:56:29.854771Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Thông tin sản phẩm trong mẫu ","metadata":{}},{"cell_type":"code","source":"sampled_tx_articles = sampled_tx.merge(\n    articles[[\"article_id\", \"prod_name\", \"product_group_name\"]],\n    on=\"article_id\",\n    how=\"left\",\n)\n\nsampled_tx_articles.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:36.734652Z","iopub.execute_input":"2026-01-08T10:56:36.735483Z","iopub.status.idle":"2026-01-08T10:56:36.780545Z","shell.execute_reply.started":"2026-01-08T10:56:36.735450Z","shell.execute_reply":"2026-01-08T10:56:36.779521Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1) Top 5 sản phẩm bán chạy\ntop_products = (\n    sampled_tx_articles\n    .groupby([\"article_id\", \"prod_name\"])[\"t_dat\"]\n    .count()\n    .sort_values(ascending=False)\n    .head(5)\n    .reset_index(name=\"n_transactions\")\n)\n\nplt.figure(figsize=(8, 5))\nplt.bar(top_products[\"prod_name\"], top_products[\"n_transactions\"])\nplt.xticks(rotation=45, ha=\"right\")\nplt.xlabel(\"Tên sản phẩm\")\nplt.ylabel(\"Số lượng giao dịch\")\nplt.title(\"Top 5 sản phẩm bán chạy (tập dữ liệu mẫu)\")\nplt.tight_layout()\nplt.show()\n\ntop_products","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:39.489200Z","iopub.execute_input":"2026-01-08T10:56:39.489488Z","iopub.status.idle":"2026-01-08T10:56:39.710540Z","shell.execute_reply.started":"2026-01-08T10:56:39.489467Z","shell.execute_reply":"2026-01-08T10:56:39.709443Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2) Top 10 nhóm sản phẩm\ntop_groups = (\n    sampled_tx_articles\n    .groupby(\"product_group_name\")[\"t_dat\"]\n    .count()\n    .sort_values(ascending=False)\n    .head(10)\n    .reset_index(name=\"n_transactions\")\n)\n\nplt.figure(figsize=(8, 5))\nplt.bar(top_groups[\"product_group_name\"], top_groups[\"n_transactions\"])\nplt.xticks(rotation=45, ha=\"right\")\nplt.xlabel(\"Nhóm sản phẩm\")\nplt.ylabel(\"Số lượng giao dịch\")\nplt.title(\"Top 10 nhóm sản phẩm (tập dữ liệu mẫu)\")\nplt.tight_layout()\nplt.show()\n\ntop_groups","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:44.411583Z","iopub.execute_input":"2026-01-08T10:56:44.411934Z","iopub.status.idle":"2026-01-08T10:56:44.672011Z","shell.execute_reply.started":"2026-01-08T10:56:44.411907Z","shell.execute_reply":"2026-01-08T10:56:44.671226Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3) Phân bố kênh bán (1 = Online, 2 = In-Store)\nchannel_map = {1: \"Online\", 2: \"In-Store\"}\nsampled_tx = sampled_tx.copy()\nsampled_tx[\"sales_channel\"] = sampled_tx[\"sales_channel_id\"].map(\n    channel_map\n)\n\nplt.figure(figsize=(6, 4))\nsampled_tx[\"sales_channel\"].value_counts().plot(kind=\"bar\")\nplt.title(\"Phân bố kênh bán hàng (tập dữ liệu mẫu)\")\nplt.xlabel(\"Kênh bán hàng\")\nplt.ylabel(\"Số lượng giao dịch\")\nplt.xticks(rotation=0)\nplt.tight_layout()\nplt.show()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:49.032248Z","iopub.execute_input":"2026-01-08T10:56:49.032551Z","iopub.status.idle":"2026-01-08T10:56:49.225257Z","shell.execute_reply.started":"2026-01-08T10:56:49.032529Z","shell.execute_reply":"2026-01-08T10:56:49.224328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 4) Phân bố tuổi khách hàng trong SAMPLE\nsampled_tx_cust = sampled_tx.merge(\n    customers[[\"customer_id\", \"age\"]],\n    on=\"customer_id\",\n    how=\"left\",\n)\n\nplt.figure(figsize=(8, 4))\nsns.histplot(sampled_tx_cust[\"age\"].dropna(), bins=50, kde=True)\nplt.title(\"Phân bố tuổi khách hàng (tập dữ liệu mẫu)\")\nplt.xlabel(\"Tuổi\")\nplt.ylabel(\"Tần suất\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T10:56:52.737809Z","iopub.execute_input":"2026-01-08T10:56:52.738142Z","iopub.status.idle":"2026-01-08T10:56:53.975234Z","shell.execute_reply.started":"2026-01-08T10:56:52.738116Z","shell.execute_reply":"2026-01-08T10:56:53.974256Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Collaborative Filtering dùng SVD","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom sklearn.decomposition import TruncatedSVD","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:20.261475Z","iopub.execute_input":"2025-12-03T12:29:20.262043Z","iopub.status.idle":"2025-12-03T12:29:20.270845Z","shell.execute_reply.started":"2025-12-03T12:29:20.262005Z","shell.execute_reply":"2025-12-03T12:29:20.268954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== Tạo ma trận tương tác USER–ITEM (0/1 đã mua) =====\n\n# Chỉ giữ cột cần thiết\nui_df = sampled_tx[[\"customer_id\", \"article_id\"]].drop_duplicates()\n\n# Gán index số cho user và item\nuser_ids = ui_df[\"customer_id\"].unique()\nitem_ids = ui_df[\"article_id\"].unique()\n\nuser_id_to_idx = {u: i for i, u in enumerate(user_ids)}\nitem_id_to_idx = {a: j for j, a in enumerate(item_ids)}\n\nn_users = len(user_ids)\nn_items = len(item_ids)\nprint(\"n_users:\", n_users, \"| n_items:\", n_items)\n\n# Ma trận tương tác nhị phân R (n_users x n_items)\nR = np.zeros((n_users, n_items), dtype=np.float32)\nfor _, row in ui_df.iterrows():\n    u_idx = user_id_to_idx[row[\"customer_id\"]]\n    i_idx = item_id_to_idx[row[\"article_id\"]]\n    R[u_idx, i_idx] = 1.0\n\nprint(\"Interaction matrix shape:\", R.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:20.272773Z","iopub.execute_input":"2025-12-03T12:29:20.273119Z","iopub.status.idle":"2025-12-03T12:29:22.000791Z","shell.execute_reply.started":"2025-12-03T12:29:20.273094Z","shell.execute_reply":"2025-12-03T12:29:21.999726Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== SVD: R ≈ U Σ V^T (TruncatedSVD) =====\n\nk = 50  # số nhân tố ẩn (latent factors), có thể tune theo slide SVD\nsvd = TruncatedSVD(n_components=k, random_state=42)\nU = svd.fit_transform(R)           # (n_users, k)\nSigma = svd.singular_values_       # (k,)\nVt = svd.components_               # (k, n_items)\n\n# R_approx = U Σ V^T\nSigma_matrix = np.diag(Sigma)\nR_hat = np.dot(np.dot(U, Sigma_matrix), Vt)  # (n_users, n_items)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:22.002045Z","iopub.execute_input":"2025-12-03T12:29:22.002563Z","iopub.status.idle":"2025-12-03T12:29:23.567174Z","shell.execute_reply.started":"2025-12-03T12:29:22.002515Z","shell.execute_reply":"2025-12-03T12:29:23.566353Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== Hàm gợi ý bằng CF–SVD =====\n\nidx_to_user_id = {i: u for u, i in user_id_to_idx.items()}\nidx_to_item_id = {j: a for a, j in item_id_to_idx.items()}\n\ndef recommend_cf_svd(customer_id, topk=10):\n    \"\"\"\n    Gợi ý top-k sản phẩm cho 1 khách hàng,\n    dựa trên điểm dự đoán trong R_hat.\n    \"\"\"\n    if customer_id not in user_id_to_idx:\n        # user mới hoàn toàn trong sample -> fallback: chưa xử lý\n        return []\n\n    u_idx = user_id_to_idx[customer_id]\n    scores = R_hat[u_idx]  # vector điểm cho tất cả item\n\n    # Không gợi ý lại item đã mua\n    bought_items = set(\n        ui_df.loc[ui_df[\"customer_id\"] == customer_id, \"article_id\"].tolist()\n    )\n    candidate_indices = [\n        j for j, aid in idx_to_item_id.items() if aid not in bought_items\n    ]\n\n    candidate_scores = [(j, scores[j]) for j in candidate_indices]\n    candidate_scores.sort(key=lambda x: x[1], reverse=True)\n\n    top_items = [idx_to_item_id[j] for j, _ in candidate_scores[:topk]]\n    return top_items\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.568447Z","iopub.execute_input":"2025-12-03T12:29:23.569204Z","iopub.status.idle":"2025-12-03T12:29:23.578555Z","shell.execute_reply.started":"2025-12-03T12:29:23.569171Z","shell.execute_reply":"2025-12-03T12:29:23.577668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Demo\nsample_users = ui_df[\"customer_id\"].drop_duplicates().sample(3, random_state=42)\nfor cid in sample_users:\n    print(\"User:\", cid)\n    print(\"  CF–SVD recs:\", recommend_cf_svd(cid, topk=5))\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.579716Z","iopub.execute_input":"2025-12-03T12:29:23.580137Z","iopub.status.idle":"2025-12-03T12:29:23.646474Z","shell.execute_reply.started":"2025-12-03T12:29:23.580109Z","shell.execute_reply":"2025-12-03T12:29:23.645202Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Lấy danh sách user vừa xuất hiện trong sample (ui_df) vừa có giao dịch test\ncandidate_users = [\n    cid for cid in ui_df[\"customer_id\"].unique()\n    if cid in true_items_dict\n]\n\ndemo_users = pd.Series(candidate_users).sample(3, random_state=42).tolist()\n\nfor cid in demo_users:\n    print(\"=\" * 80)\n    show_cf_svd_visual(cid, topk=8)   # hình CF","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:36:37.944842Z","iopub.execute_input":"2025-12-03T12:36:37.945208Z","iopub.status.idle":"2025-12-03T12:36:54.058727Z","shell.execute_reply.started":"2025-12-03T12:36:37.945183Z","shell.execute_reply":"2025-12-03T12:36:54.057420Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Content-based Recommendation (User & Item Feature Matrix)","metadata":{}},{"cell_type":"markdown","source":"- Item Profile Matrix: vector đặc trưng cho mỗi sản phẩm\n\n- User Profile Matrix: vector đặc trưng cho mỗi user = trung bình (hoặc tổng) các vector item mà user đã mua\n\n- User–Item Feature Matrix / Similarity: dùng dot-product / cosine để chọn Top-N.","metadata":{}},{"cell_type":"code","source":"from sklearn.preprocessing import OneHotEncoder, StandardScaler\nfrom sklearn.metrics.pairwise import cosine_similarity","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:36:54.060382Z","iopub.execute_input":"2025-12-03T12:36:54.060703Z","iopub.status.idle":"2025-12-03T12:36:54.069634Z","shell.execute_reply.started":"2025-12-03T12:36:54.060677Z","shell.execute_reply":"2025-12-03T12:36:54.068043Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from gensim.models import Word2Vec\nfrom scipy.sparse import hstack, csr_matrix\n\n# ===== Vector hoá mô tả sản phẩm bằng Word2Vec =====\n\n# Lấy cột mô tả chi tiết\narticles_text = articles[[\"article_id\", \"detail_desc\"]].copy()\narticles_text[\"detail_desc\"] = (\n    articles_text[\"detail_desc\"]\n    .fillna(\"\")\n    .astype(str)\n    .str.lower()\n)\n\n# Tạo corpus câu (danh sách token) cho Word2Vec\nsentences = [\n    desc.split()\n    for desc in articles_text[\"detail_desc\"].values\n    if desc.strip() != \"\"\n]\n\nprint(\"Số câu dùng train Word2Vec:\", len(sentences))\n\n# Train Word2Vec\nw2v_dim = 100  # số chiều vector từ, có thể chỉnh 50/100/200\nw2v_model = Word2Vec(\n    sentences,\n    vector_size=w2v_dim,\n    window=5,\n    min_count=2,\n    workers=4,\n    sg=1,                # skip-gram\n    seed=RANDOM_STATE,\n)\n\ndef get_w2v_vector(text: str, model: Word2Vec, dim: int) -> np.ndarray:\n    \"\"\"\n    Trả về vector Word2Vec (trung bình các từ) cho 1 mô tả sản phẩm.\n    Nếu không có từ nào trong vocab -> vector 0.\n    \"\"\"\n    tokens = text.lower().split()\n    vecs = [model.wv[w] for w in tokens if w in model.wv]\n    if not vecs:\n        return np.zeros(dim, dtype=np.float32)\n    return np.mean(vecs, axis=0).astype(np.float32)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:36:59.798729Z","iopub.execute_input":"2025-12-03T12:36:59.799173Z","iopub.status.idle":"2025-12-03T12:38:23.347090Z","shell.execute_reply.started":"2025-12-03T12:36:59.799144Z","shell.execute_reply":"2025-12-03T12:38:23.345292Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== Tạo ITEM PROFILE MATRIX (One-hot + price + Word2Vec) =====\n\n# 1) Tính giá trung bình mỗi article từ transactions\narticle_price = (\n    transactions.groupby(\"article_id\")[\"price\"]\n    .mean()\n    .reset_index(name=\"price\")\n)\n\n# 2) Lấy meta từ articles, thêm detail_desc và merge giá vào\nitem_meta = articles[[\n    \"article_id\",\n    \"product_type_name\",\n    \"product_group_name\",\n    \"graphical_appearance_name\",\n    \"colour_group_name\",\n    \"detail_desc\",\n]].copy()\n\nitem_meta = item_meta.merge(article_price, on=\"article_id\", how=\"left\")\n\n# Chỉ giữ các item xuất hiện trong SAMPLE\nitem_meta = item_meta[item_meta[\"article_id\"].isin(item_ids)].reset_index(drop=True)\n\n# 3) Xử lý categorical + numerical\ncat_cols = [\n    \"product_type_name\",\n    \"product_group_name\",\n    \"graphical_appearance_name\",\n    \"colour_group_name\",\n]\nnum_cols = [\"price\"]\n\nfor c in cat_cols:\n    item_meta[c] = item_meta[c].fillna(\"UNK\").astype(str)\n\nitem_meta[\"price\"] = item_meta[\"price\"].fillna(item_meta[\"price\"].median())\n\n# 4) One-hot cho categorical\nohe = OneHotEncoder(handle_unknown=\"ignore\", sparse=True)\nitem_cat_ohe = ohe.fit_transform(item_meta[cat_cols])\n\n# 5) Chuẩn hoá numeric\nscaler = StandardScaler()\nitem_num_scaled = scaler.fit_transform(item_meta[num_cols])\n\n# 6) Vector Word2Vec cho detail_desc\nitem_meta[\"detail_desc\"] = (\n    item_meta[\"detail_desc\"]\n    .fillna(\"\")\n    .astype(str)\n    .str.lower()\n)\n\nw2v_vectors = np.vstack([\n    get_w2v_vector(desc, w2v_model, w2v_dim)\n    for desc in item_meta[\"detail_desc\"].values\n])\n\n# Chuyển sang sparse để ghép với one-hot\nw2v_sparse = csr_matrix(w2v_vectors)\n\n# 7) Ghép thành ITEM PROFILE MATRIX\nitem_feature_matrix = hstack([item_cat_ohe, item_num_scaled, w2v_sparse]).tocsr()\nprint(\"Item feature matrix shape:\", item_feature_matrix.shape)\n\n# Map article_id <-> index trong item_feature_matrix\nitemid_to_feat_idx = {aid: i for i, aid in enumerate(item_meta[\"article_id\"].values)}\nfeat_idx_to_itemid = {i: aid for aid, i in itemid_to_feat_idx.items()}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:39:34.276068Z","iopub.execute_input":"2025-12-03T12:39:34.276557Z","iopub.status.idle":"2025-12-03T12:39:37.325258Z","shell.execute_reply.started":"2025-12-03T12:39:34.276516Z","shell.execute_reply":"2025-12-03T12:39:37.324307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.sparse import coo_matrix, diags\n\n# ----- 1. Tạo ma trận user–item nhị phân sparse -----\n\n# ui_df chỉ gồm (customer_id, article_id) trong SAMPLE\nrows = np.array([user_id_to_idx[c] for c in ui_df[\"customer_id\"].values], dtype=np.int32)\ncols = np.array([item_id_to_idx[a] for a in ui_df[\"article_id\"].values], dtype=np.int32)\ndata = np.ones(len(ui_df), dtype=np.float32)\n\nn_users = len(user_ids)\nn_items = len(item_ids)\n\nUI = coo_matrix((data, (rows, cols)), shape=(n_users, n_items)).tocsr()\nprint(\"UI shape:\", UI.shape)\n\n# ----- 2. Đếm số item mỗi user đã mua -----\n\nuser_item_counts = np.asarray(UI.sum(axis=1)).ravel()   # (n_users,)\n# Tránh chia cho 0 (về lý thì user trong UI đều có giao dịch, nhưng cứ phòng)\nuser_item_counts[user_item_counts == 0] = 1.0\n\n# Ma trận đường chéo D^{-1}\nD_inv = diags(1.0 / user_item_counts)\n\n# ----- 3. Tính USER PROFILE MATRIX = D^{-1} * UI * ItemFeatures -----\n\n# item_feature_matrix phải là CSR\n# item_feature_matrix = hstack([...]).tocsr()  # bạn đã làm ở trên\nuser_profile_matrix = D_inv.dot(UI).dot(item_feature_matrix)\n\nprint(\"User profile matrix shape:\", user_profile_matrix.shape)\n\n# Map id <-> index cho user profile\nuser_profile_ids = user_ids\ncid_to_profile_idx = {cid: i for i, cid in enumerate(user_profile_ids)}\nprofile_idx_to_cid = {i: cid for i, cid in enumerate(user_profile_ids)}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:39:41.226719Z","iopub.execute_input":"2025-12-03T12:39:41.227768Z","iopub.status.idle":"2025-12-03T12:39:41.296455Z","shell.execute_reply.started":"2025-12-03T12:39:41.227728Z","shell.execute_reply":"2025-12-03T12:39:41.295191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== Hàm gợi ý Content-based =====\n\ndef recommend_content_based(customer_id, topk=10):\n    \"\"\"\n    Gợi ý top-k sản phẩm dựa trên:\n    - User profile: trung bình feature của item đã mua\n    - Tính cosine similarity với toàn bộ item_feature_matrix\n    \"\"\"\n    if customer_id not in cid_to_profile_idx:\n        return []  # user không có profile trong sample\n\n    u_idx = cid_to_profile_idx[customer_id]\n    u_vec = user_profile_matrix[u_idx]          # (1, d)\n\n    # Tính cosine similarity giữa user và tất cả item\n    sims = cosine_similarity(u_vec, item_feature_matrix).ravel()  # (n_items,)\n\n    # Không gợi ý lại item đã mua\n    bought_items = set(user_to_items.get(customer_id, []))\n    candidate_indices = [\n        i for i, aid in feat_idx_to_itemid.items()\n        if aid not in bought_items\n    ]\n\n    candidate_scores = [(i, sims[i]) for i in candidate_indices]\n    candidate_scores.sort(key=lambda x: x[1], reverse=True)\n\n    top_items = [feat_idx_to_itemid[i] for i, _ in candidate_scores[:topk]]\n    return top_items","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:39:44.499117Z","iopub.execute_input":"2025-12-03T12:39:44.499526Z","iopub.status.idle":"2025-12-03T12:39:44.509173Z","shell.execute_reply.started":"2025-12-03T12:39:44.499500Z","shell.execute_reply":"2025-12-03T12:39:44.507600Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Map: mỗi user -> danh sách (hoặc set) các article_id đã mua trong SAMPLE\nuser_to_items = (\n    ui_df.groupby(\"customer_id\")[\"article_id\"]\n    .apply(list)            # hoặc .apply(set) nếu muốn set luôn\n    .to_dict()\n)\nprint(\"Số user trong user_to_items:\", len(user_to_items))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:42:03.183513Z","iopub.execute_input":"2025-12-03T12:42:03.183933Z","iopub.status.idle":"2025-12-03T12:42:03.253681Z","shell.execute_reply.started":"2025-12-03T12:42:03.183907Z","shell.execute_reply":"2025-12-03T12:42:03.252105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Demo\nfor cid in sample_users:\n    print(\"User:\", cid)\n    print(\"  CB recs:\", recommend_content_based(cid, topk=5))\n    print()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:42:09.460677Z","iopub.execute_input":"2025-12-03T12:42:09.461107Z","iopub.status.idle":"2025-12-03T12:42:09.610609Z","shell.execute_reply.started":"2025-12-03T12:42:09.461068Z","shell.execute_reply":"2025-12-03T12:42:09.609234Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Lấy danh sách user vừa xuất hiện trong sample (ui_df) vừa có giao dịch test\ncandidate_users = [\n    cid for cid in ui_df[\"customer_id\"].unique()\n    if cid in true_items_dict\n]\n\ndemo_users = pd.Series(candidate_users).sample(3, random_state=42).tolist()\n\nfor cid in demo_users:\n    print(\"=\" * 80)\n    show_cb_visual(cid, topk=8)       # hình Content-based","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:42:12.038895Z","iopub.execute_input":"2025-12-03T12:42:12.039367Z","iopub.status.idle":"2025-12-03T12:42:27.989001Z","shell.execute_reply.started":"2025-12-03T12:42:12.039338Z","shell.execute_reply":"2025-12-03T12:42:27.987690Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Hybrid Neural Network (User + Item Features)","metadata":{}},{"cell_type":"code","source":"# Chuẩn bị user features\nuser_cols = [\"customer_id\", \"age\", \"club_member_status\", \"fashion_news_frequency\"]\nitem_cols = [\"article_id\", \"product_type_name\", \"product_group_name\",\n             \"graphical_appearance_name\", \"colour_group_name\", \"price\"]\n\nuser_df = customers[user_cols].copy()\nuser_df[\"age\"] = user_df[\"age\"].fillna(30)\nuser_df[\"age_bucket\"] = pd.cut(\n    user_df[\"age\"],\n    bins=[0, 18, 25, 35, 50, 100],\n    labels=[\"<18\", \"18-25\", \"25-35\", \"35-50\", \"50+\"]\n)\n\nitem_df = articles[item_cols].copy()\nitem_df[\"price\"] = item_df[\"price\"].fillna(item_df[\"price\"].median())\n\ndisplay(user_df.head())\ndisplay(item_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.710184Z","iopub.status.idle":"2025-12-03T12:29:23.710507Z","shell.execute_reply.started":"2025-12-03T12:29:23.710371Z","shell.execute_reply":"2025-12-03T12:29:23.710386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Label encode các cột categorical\ndef label_encode(df, col):\n    values = df[col].fillna(\"UNK\").astype(str).values\n    uniques = pd.unique(values)\n    mapping = {v: i for i, v in enumerate(uniques)}\n    df[col + \"_le\"] = [mapping[v] for v in values]\n    return mapping\n\nuser_mappings = {}\nitem_mappings = {}\n\nfor col in [\"age_bucket\", \"club_member_status\", \"fashion_news_frequency\"]:\n    user_mappings[col] = label_encode(user_df, col)\n\nfor col in [\"product_type_name\", \"product_group_name\",\n            \"graphical_appearance_name\", \"colour_group_name\"]:\n    item_mappings[col] = label_encode(item_df, col)\n\ndisplay(user_df.head())\ndisplay(item_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.712654Z","iopub.status.idle":"2025-12-03T12:29:23.712951Z","shell.execute_reply.started":"2025-12-03T12:29:23.712817Z","shell.execute_reply":"2025-12-03T12:29:23.712828Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Đặt index để join nhanh\nuser_df_indexed = user_df.set_index(\"customer_id\")\nitem_df_indexed = item_df.set_index(\"article_id\")\n\nprint(\"Num users in user_df:\", len(user_df_indexed))\nprint(\"Num items in item_df:\", len(item_df_indexed))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.713476Z","iopub.status.idle":"2025-12-03T12:29:23.713800Z","shell.execute_reply.started":"2025-12-03T12:29:23.713632Z","shell.execute_reply":"2025-12-03T12:29:23.713644Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Tạo dataset train cho Hybrid","metadata":{}},{"cell_type":"code","source":"# - Positive: tất cả (customer, article) có thật trong train_df\n# - Negative: với mỗi (customer, article_pos), sample một vài article_neg khác\n# - Kết quả: `train_full` chứa (customer_id, article_id, label)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.717041Z","iopub.status.idle":"2025-12-03T12:29:23.717614Z","shell.execute_reply.started":"2025-12-03T12:29:23.717396Z","shell.execute_reply":"2025-12-03T12:29:23.717416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Positive\ntrain_pairs = train_df[[\"customer_id\", \"article_id\"]].drop_duplicates()\ntrain_pairs[\"label\"] = 1\n\n# Negative sampling đơn giản\nall_article_ids = item_df_indexed.index.values\nn_negative_per_pos = 3\n\nneg_rows = []\nrng = np.random.default_rng(RANDOM_STATE)\n\nfor cust_id, art_id in train_pairs[[\"customer_id\", \"article_id\"]].values:\n    candidate_neg = rng.choice(all_article_ids, size=n_negative_per_pos, replace=False)\n    for neg_item in candidate_neg:\n        if neg_item == art_id:\n            continue\n        neg_rows.append((cust_id, neg_item, 0))\n\nneg_df = pd.DataFrame(neg_rows, columns=[\"customer_id\", \"article_id\", \"label\"])\n\ntrain_full = pd.concat([train_pairs, neg_df], ignore_index=True)\nprint(\"train_pairs:\", train_pairs.shape, \"| neg_df:\", neg_df.shape)\nprint(\"train_full:\", train_full.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.719006Z","iopub.status.idle":"2025-12-03T12:29:23.719654Z","shell.execute_reply.started":"2025-12-03T12:29:23.719446Z","shell.execute_reply":"2025-12-03T12:29:23.719466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Loại bỏ cặp không có thông tin user / item\nmask = train_full[\"customer_id\"].isin(user_df_indexed.index) & \\\n       train_full[\"article_id\"].isin(item_df_indexed.index)\ntrain_full = train_full[mask].reset_index(drop=True)\n\nprint(\"train_full (after filtering):\", train_full.shape)\ntrain_full.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.721616Z","iopub.status.idle":"2025-12-03T12:29:23.722027Z","shell.execute_reply.started":"2025-12-03T12:29:23.721819Z","shell.execute_reply":"2025-12-03T12:29:23.721838Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### PyTorch Dataset","metadata":{}},{"cell_type":"code","source":"# - `HybridRecDataset`: trả về (user_cat, item_cat, item_num, label)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.723497Z","iopub.status.idle":"2025-12-03T12:29:23.724053Z","shell.execute_reply.started":"2025-12-03T12:29:23.723735Z","shell.execute_reply":"2025-12-03T12:29:23.723754Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class HybridRecDataset(Dataset):\n    def __init__(self, df, user_df_indexed, item_df_indexed):\n        self.df = df\n        self.user_df_idx = user_df_indexed\n        self.item_df_idx = item_df_indexed\n        \n        # Cột categorical & numeric\n        self.user_cat_cols = [\"age_bucket_le\", \"club_member_status_le\", \"fashion_news_frequency_le\"]\n        self.item_cat_cols = [\"product_type_name_le\", \"product_group_name_le\",\n                              \"graphical_appearance_name_le\", \"colour_group_name_le\"]\n        self.item_num_cols = [\"price\"]\n        \n        self.user_cat = []\n        self.item_cat = []\n        self.item_num = []\n        self.labels = []\n        \n        for _, row in df.iterrows():\n            uid = row[\"customer_id\"]\n            iid = row[\"article_id\"]\n            label = row[\"label\"]\n            \n            u = self.user_df_idx.loc[uid]\n            it = self.item_df_idx.loc[iid]\n            \n            self.user_cat.append(u[self.user_cat_cols].values.astype(np.int64))\n            self.item_cat.append(it[self.item_cat_cols].values.astype(np.int64))\n            self.item_num.append(it[self.item_num_cols].values.astype(np.float32))\n            self.labels.append(label)\n        \n        self.user_cat = torch.tensor(np.stack(self.user_cat), dtype=torch.long)\n        self.item_cat = torch.tensor(np.stack(self.item_cat), dtype=torch.long)\n        self.item_num = torch.tensor(np.stack(self.item_num), dtype=torch.float32)\n        self.labels = torch.tensor(self.labels, dtype=torch.float32)\n\n    def __len__(self):\n        return len(self.labels)\n\n    def __getitem__(self, idx):\n        return (\n            self.user_cat[idx],\n            self.item_cat[idx],\n            self.item_num[idx],\n            self.labels[idx],\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.725115Z","iopub.status.idle":"2025-12-03T12:29:23.725444Z","shell.execute_reply.started":"2025-12-03T12:29:23.725301Z","shell.execute_reply":"2025-12-03T12:29:23.725316Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### HybridRecModel","metadata":{}},{"cell_type":"code","source":"#- `HybridRecModel`:\n#   - Embedding cho mỗi cột categorical\n#   - MLP cho nhánh user, MLP cho nhánh item\n#   - Ghép 2 vector lại và qua MLP cuối để ra score \\[0,1]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.726585Z","iopub.status.idle":"2025-12-03T12:29:23.727216Z","shell.execute_reply.started":"2025-12-03T12:29:23.727053Z","shell.execute_reply":"2025-12-03T12:29:23.727071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class HybridRecModel(nn.Module):\n    def __init__(self, user_cardinalities, item_cardinalities, n_item_num=1, emb_dim=16):\n        super().__init__()\n        # Embedding user\n        self.user_emb_layers = nn.ModuleDict()\n        for col, card in user_cardinalities.items():\n            self.user_emb_layers[col] = nn.Embedding(card, emb_dim)\n        \n        # Embedding item\n        self.item_emb_layers = nn.ModuleDict()\n        for col, card in item_cardinalities.items():\n            self.item_emb_layers[col] = nn.Embedding(card, emb_dim)\n        \n        user_input_dim = len(user_cardinalities) * emb_dim\n        item_input_dim = len(item_cardinalities) * emb_dim + n_item_num\n        \n        # MLP cho user branch\n        self.user_mlp = nn.Sequential(\n            nn.Linear(user_input_dim, 64),\n            nn.ReLU(),\n            nn.Linear(64, 32),\n            nn.ReLU()\n        )\n        # MLP cho item branch\n        self.item_mlp = nn.Sequential(\n            nn.Linear(item_input_dim, 64),\n            nn.ReLU(),\n            nn.Linear(64, 32),\n            nn.ReLU()\n        )\n        \n        # MLP cuối\n        self.final_mlp = nn.Sequential(\n            nn.Linear(32 + 32, 32),\n            nn.ReLU(),\n            nn.Linear(32, 1)\n        )\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, user_cat, item_cat, item_num):\n        # user_cat: [B, n_user_cat]\n        # item_cat: [B, n_item_cat]\n        # item_num: [B, n_item_num]\n        user_embs = []\n        for i, col in enumerate(user_cardinalities.keys()):\n            user_embs.append(self.user_emb_layers[col](user_cat[:, i]))\n        user_emb = torch.cat(user_embs, dim=-1)\n        \n        item_embs = []\n        for i, col in enumerate(item_cardinalities.keys()):\n            item_embs.append(self.item_emb_layers[col](item_cat[:, i]))\n        item_emb_cat = torch.cat(item_embs, dim=-1)\n        item_input = torch.cat([item_emb_cat, item_num], dim=-1)\n        \n        u_vec = self.user_mlp(user_emb)\n        i_vec = self.item_mlp(item_input)\n        \n        x = torch.cat([u_vec, i_vec], dim=-1)\n        logit = self.final_mlp(x)\n        score = self.sigmoid(logit).squeeze(-1)\n        return score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.728681Z","iopub.status.idle":"2025-12-03T12:29:23.729173Z","shell.execute_reply.started":"2025-12-03T12:29:23.728973Z","shell.execute_reply":"2025-12-03T12:29:23.728992Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train Hybrid Model","metadata":{}},{"cell_type":"code","source":"max_train_samples = 200_000\nif len(train_full) > max_train_samples:\n    train_sample = train_full.sample(max_train_samples, random_state=RANDOM_STATE)\nelse:\n    train_sample = train_full\n\ntrain_sample = train_sample.reset_index(drop=True)\ntrain_sample.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.730883Z","iopub.status.idle":"2025-12-03T12:29:23.731180Z","shell.execute_reply.started":"2025-12-03T12:29:23.731034Z","shell.execute_reply":"2025-12-03T12:29:23.731054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = HybridRecDataset(train_sample, user_df_indexed, item_df_indexed)\nlen(dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.731857Z","iopub.status.idle":"2025-12-03T12:29:23.732184Z","shell.execute_reply.started":"2025-12-03T12:29:23.731999Z","shell.execute_reply":"2025-12-03T12:29:23.732010Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nindices = np.arange(len(dataset))\ntrain_idx, val_idx = train_test_split(indices, test_size=0.1, random_state=RANDOM_STATE)\n\ntrain_subset = torch.utils.data.Subset(dataset, train_idx)\nval_subset   = torch.utils.data.Subset(dataset, val_idx)\n\ntrain_loader = DataLoader(train_subset, batch_size=1024, shuffle=True, num_workers=0)\nval_loader   = DataLoader(val_subset, batch_size=2048, shuffle=False, num_workers=0)\n\nlen(train_loader), len(val_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.734007Z","iopub.status.idle":"2025-12-03T12:29:23.734378Z","shell.execute_reply.started":"2025-12-03T12:29:23.734189Z","shell.execute_reply":"2025-12-03T12:29:23.734204Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\ndevice","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.734780Z","iopub.status.idle":"2025-12-03T12:29:23.735018Z","shell.execute_reply.started":"2025-12-03T12:29:23.734906Z","shell.execute_reply":"2025-12-03T12:29:23.734916Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = HybridRecModel(user_cardinalities, item_cardinalities, n_item_num=1, emb_dim=16)\nmodel = model.to(device)\n\ncriterion = nn.BCELoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.735420Z","iopub.status.idle":"2025-12-03T12:29:23.735649Z","shell.execute_reply.started":"2025-12-03T12:29:23.735539Z","shell.execute_reply":"2025-12-03T12:29:23.735549Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_one_epoch(loader, model, criterion, optimizer=None):\n    is_train = optimizer is not None\n    model.train(is_train)\n    \n    total_loss = 0.0\n    n_samples = 0\n    \n    for user_cat, item_cat, item_num, labels in loader:\n        user_cat = user_cat.to(device)\n        item_cat = item_cat.to(device)\n        item_num = item_num.to(device)\n        labels = labels.to(device)\n        \n        scores = model(user_cat, item_cat, item_num)\n        loss = criterion(scores, labels)\n        \n        if is_train:\n            optimizer.zero_grad()\n            loss.backward()\n            optimizer.step()\n        \n        bs = labels.size(0)\n        total_loss += loss.item() * bs\n        n_samples += bs\n    \n    return total_loss / max(1, n_samples)\n\nn_epochs = 3  # demo\nfor epoch in range(1, n_epochs+1):\n    train_loss = run_one_epoch(train_loader, model, criterion, optimizer)\n    val_loss = run_one_epoch(val_loader, model, criterion, optimizer=None)\n    print(f\"Epoch {epoch}/{n_epochs} - train_loss={train_loss:.4f} - val_loss={val_loss:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.736792Z","iopub.status.idle":"2025-12-03T12:29:23.737124Z","shell.execute_reply.started":"2025-12-03T12:29:23.736941Z","shell.execute_reply":"2025-12-03T12:29:23.736953Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Sử dụng Hybrid Model để gợi ý","metadata":{}},{"cell_type":"code","source":"# Chiến lược candidate đơn giản:\n# - Với mỗi customer:\n#   - Candidate = top-N popular items\n#   - + các item tương tự (content-based) từ những sản phẩm họ đã mua trong train\n# - Score từng (customer, item) bằng Hybrid model\n# - Lấy top-12 theo score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.738551Z","iopub.status.idle":"2025-12-03T12:29:23.739472Z","shell.execute_reply.started":"2025-12-03T12:29:23.739235Z","shell.execute_reply":"2025-12-03T12:29:23.739256Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Map: customer -> list article_id đã mua trong train\ncustomer_to_items_train = (\n    train_df.groupby(\"customer_id\")[\"article_id\"]\n    .apply(list)\n    .to_dict()\n)\n\ndef build_candidates_for_customer(customer_id, n_pop=200, n_cb_per_item=5):\n    \"\"\"\n    Candidate items:\n    - top-n_pop popular\n    - + các item CB tương tự những item user đã mua\n    \"\"\"\n    cands = set(popular_items[:n_pop])\n    past_items = customer_to_items_train.get(customer_id, [])\n    # Lấy tối đa 5 item gần nhất\n    for aid in past_items[-5:]:\n        sim_items = get_similar_items(aid, topk=n_cb_per_item)\n        cands.update(sim_items)\n    # Chỉ giữ items có metadata\n    cands = [aid for aid in cands if aid in item_df_indexed.index]\n    return cands","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.743895Z","iopub.status.idle":"2025-12-03T12:29:23.744263Z","shell.execute_reply.started":"2025-12-03T12:29:23.744077Z","shell.execute_reply":"2025-12-03T12:29:23.744090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def score_items_for_customer(customer_id, candidate_items, model, topk=12):\n    \"\"\"\n    Tính score (customer, item) bằng Hybrid model.\n    Nếu customer không có info → fallback Popularity.\n    \"\"\"\n    if customer_id not in user_df_indexed.index:\n        return popular_items[:topk]\n    \n    u = user_df_indexed.loc[customer_id]\n    user_cat_vals = u[[\"age_bucket_le\", \"club_member_status_le\", \"fashion_news_frequency_le\"]].values.astype(np.int64)\n    user_cat_t = torch.tensor(user_cat_vals, dtype=torch.long).unsqueeze(0).to(device)\n    \n    scored_items = []\n    for aid in candidate_items:\n        it = item_df_indexed.loc[aid]\n        item_cat_vals = it[[\"product_type_name_le\", \"product_group_name_le\",\n                            \"graphical_appearance_name_le\", \"colour_group_name_le\"]].values.astype(np.int64)\n        item_num_vals = it[[\"price\"]].values.astype(np.float32)\n        \n        item_cat_t = torch.tensor(item_cat_vals, dtype=torch.long).unsqueeze(0).to(device)\n        item_num_t = torch.tensor(item_num_vals, dtype=torch.float32).unsqueeze(0).to(device)\n        \n        with torch.no_grad():\n            score = model(user_cat_t, item_cat_t, item_num_t).item()\n        scored_items.append((aid, score))\n    \n    scored_items.sort(key=lambda x: x[1], reverse=True)\n    top_items = [aid for aid, _ in scored_items[:topk]]\n    return top_items","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.745864Z","iopub.status.idle":"2025-12-03T12:29:23.746329Z","shell.execute_reply.started":"2025-12-03T12:29:23.746104Z","shell.execute_reply":"2025-12-03T12:29:23.746121Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Demo gợi ý cho 5 khách hàng trong tập test\nsample_test_customers = list(true_items_dict.keys())[:5]\n\nfor cid in sample_test_customers:\n    cands = build_candidates_for_customer(cid)\n    recs = score_items_for_customer(cid, cands, model, topk=12)\n    print(\"Customer:\", cid)\n    print(\"  Recs:\", recs[:5], \"...\")\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.748004Z","iopub.status.idle":"2025-12-03T12:29:23.748451Z","shell.execute_reply.started":"2025-12-03T12:29:23.748224Z","shell.execute_reply":"2025-12-03T12:29:23.748249Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Đánh giá Recall@12 cho Hybrid Mode","metadata":{}},{"cell_type":"code","source":"def recall_at_k_hybrid(true_items_dict, model, sample_size=2000, k=12):\n    customers_list = list(true_items_dict.keys())\n    if len(customers_list) > sample_size:\n        customers_list = customers_list[:sample_size]\n    \n    hits = 0\n    n_cust = 0\n    \n    for cid in customers_list:\n        true_items = true_items_dict[cid]\n        if not true_items:\n            continue\n        \n        cands = build_candidates_for_customer(cid)\n        recs = score_items_for_customer(cid, cands, model, topk=k)\n        recs_set = set(recs)\n        \n        if len(recs_set & true_items) > 0:\n            hits += 1\n        n_cust += 1\n    \n    return hits / max(1, n_cust)\n\nhybrid_recall = recall_at_k_hybrid(true_items_dict, model, sample_size=2000, k=12)\nprint(\"Hybrid model Recall@12 (subset):\", hybrid_recall)\nprint(\"Baseline Popularity Recall@12 :\", baseline_recall)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.749730Z","iopub.status.idle":"2025-12-03T12:29:23.750312Z","shell.execute_reply.started":"2025-12-03T12:29:23.750147Z","shell.execute_reply":"2025-12-03T12:29:23.750161Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Tạo submission demo cho Kaggle","metadata":{}},{"cell_type":"code","source":"customers_all = customers[\"customer_id\"].unique()\nmax_customers_submission = 50_000\n\nsub_rows = []\nfor i, cid in enumerate(customers_all[:max_customers_submission]):\n    cands = build_candidates_for_customer(cid)\n    recs = score_items_for_customer(cid, cands, model, topk=12)\n    pred_str = \" \".join([str(aid) for aid in recs])\n    sub_rows.append((cid, pred_str))\n    \n    if (i+1) % 5000 == 0:\n        print(f\"Đã xử lý {i+1} khách hàng\")\n\nsubmission_demo = pd.DataFrame(sub_rows, columns=[\"customer_id\", \"prediction\"])\nsubmission_demo.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.751642Z","iopub.status.idle":"2025-12-03T12:29:23.751971Z","shell.execute_reply.started":"2025-12-03T12:29:23.751840Z","shell.execute_reply":"2025-12-03T12:29:23.751855Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_demo.to_csv(\"submission_demo.csv\", index=False)\nprint(\"Đã lưu file: submission_demo.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:29:23.753960Z","iopub.status.idle":"2025-12-03T12:29:23.754403Z","shell.execute_reply.started":"2025-12-03T12:29:23.754173Z","shell.execute_reply":"2025-12-03T12:29:23.754190Z"}},"outputs":[],"execution_count":null}]}