{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":31254,"databundleVersionId":3103714},{"sourceType":"datasetVersion","sourceId":15880486,"datasetId":9869115,"databundleVersionId":16833966},{"sourceType":"datasetVersion","sourceId":15824421,"datasetId":10143666,"databundleVersionId":16773498},{"sourceType":"datasetVersion","sourceId":15899854,"datasetId":10195611,"databundleVersionId":16854840},{"sourceType":"kernelVersion","sourceId":313364519}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-20T14:38:45.509225Z","iopub.execute_input":"2026-04-20T14:38:45.509489Z","iopub.status.idle":"2026-04-20T14:38:46.717141Z","shell.execute_reply.started":"2026-04-20T14:38:45.509464Z","shell.execute_reply":"2026-04-20T14:38:46.716399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import polars as pl\n\n# Danh sách đường dẫn các file\nfiles = {\n    \"FE_Train\": \"/kaggle/input/datasets/huyenpham22/h-and-m-fe-data-merged/fe_train.parquet\",\n    \"FE_Val\": \"/kaggle/input/datasets/huyenpham22/h-and-m-fe-data-merged/fe_val.parquet\",\n    \"Train_Candidates\": \"/kaggle/input/datasets/huyenpham22/h-and-m-0-4-stratified-sample/train_candidates.parquet\",\n    \"Train_Labels\": \"/kaggle/input/datasets/huyenpham22/h-and-m-0-4-stratified-sample/train_labels.csv\",\n    \"Val_Candidates\": \"/kaggle/input/datasets/huyenpham22/h-and-m-0-4-stratified-sample/val_candidates.parquet\",\n    \"Val_Labels\": \"/kaggle/input/datasets/huyenpham22/h-and-m-0-4-stratified-sample/val_labels.csv\"\n}\n\ndef quick_inspect(name, path):\n    print(f\"\\n===== ĐANG KIỂM TRA FILE: {name} =====\")\n    # Nếu là file csv thì dùng read_csv, nếu là parquet thì dùng read_parquet\n    if path.endswith(\".csv\"):\n        df = pl.read_csv(path)\n    else:\n        df = pl.read_parquet(path)\n    \n    print(f\"- Số lượng dòng: {len(df):,}\")\n    print(f\"- Các cột dữ liệu: {df.columns}\")\n    print(\"- 5 dòng dữ liệu đầu tiên:\")\n    print(df.head(5))\n    return df\n\n# Chạy lệnh kiểm tra cho tất cả các file\nfor name, path in files.items():\n    try:\n        quick_inspect(name, path)\n    except Exception as e:\n        print(f\"Lỗi khi đọc file {name}: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-20T14:53:14.086041Z","iopub.execute_input":"2026-04-20T14:53:14.086960Z","iopub.status.idle":"2026-04-20T14:53:24.011233Z","shell.execute_reply.started":"2026-04-20T14:53:14.086929Z","shell.execute_reply":"2026-04-20T14:53:24.010292Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Quy trình 2 bước: Học và Thi\n\nHọc (Train): Dùng **fe_train** + **train_labels** để dạy máy tính quy luật.\n\nThi (Validation): Dùng **fe_val** để máy đoán, sau đó lấy **val_labels** ra để \"chấm điểm\" (tính MAP@12).","metadata":{}},{"cell_type":"markdown","source":"**Bước 1**: Tạo bảng gán nhãn","metadata":{}},{"cell_type":"code","source":"import polars as pl\n\n# 1. Đọc dữ liệu (Dùng scan để tối ưu bộ nhớ)\nprint(\"--- Đang nạp dữ liệu ---\")\nfe_train_lazy = pl.scan_parquet(\"/kaggle/input/datasets/huyenpham22/h-and-m-fe-data-merged/fe_train.parquet\")\ntrain_labels_lazy = pl.scan_csv(\"/kaggle/input/datasets/huyenpham22/h-and-m-0-4-stratified-sample/train_labels.csv\")\n\n# 2. XỬ LÝ KIỂU DỮ LIỆU: Dùng pad_start thay cho rjust\n# Ép article_id sang String và thêm số 0 vào đầu cho đủ 10 ký tự\ntrain_labels_lazy = train_labels_lazy.with_columns([\n    pl.col(\"article_id\").cast(pl.String).str.pad_start(10, fill_char=\"0\")\n])\n\n# 3. Thực hiện lệnh Gán nhãn (Left Join)\ntrain_with_label = fe_train_lazy.join(\n    train_labels_lazy.with_columns(pl.lit(1).alias(\"target\")),\n    on=[\"customer_id\", \"article_id\"],\n    how=\"left\"\n).fill_null(0)\n\n# 4. Thu thập kết quả (Collect) và đếm số lượng\nprint(\"--- Đang tính toán tỷ lệ nhãn (Vui lòng đợi giây lát) ---\")\n# Đếm số lượng target 0 và 1\nlabel_counts = train_with_label.select(pl.col(\"target\")).collect().to_series().value_counts()\n\nprint(\"\\nKẾT QUẢ KIỂM TRA NHÃN:\")\nprint(label_counts)\n\n# In thêm 5 dòng đầu để xem mặt mũi bảng dữ liệu\nprint(\"\\n5 DÒNG ĐẦU TIÊN CỦA BẢNG ĐÃ GÁN NHÃN:\")\nprint(train_with_label.head(5).collect())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T13:24:37.036891Z","iopub.execute_input":"2026-04-21T13:24:37.037189Z","iopub.status.idle":"2026-04-21T13:24:44.874039Z","shell.execute_reply.started":"2026-04-21T13:24:37.037144Z","shell.execute_reply":"2026-04-21T13:24:44.873269Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Nhãn 1 (53,836 dòng): Đây là những \"viên kim cương\". Trong số hơn 21 triệu gợi ý, chỉ có gần 54 nghìn món đồ là khách thực sự mua. Con số này rất ổn để mô hình có thể học được \"gu\" của khách.Nhãn 0 (21,551,864 dòng): Đây là \"biển cát\". Tỷ lệ hiện tại là khoảng $1:400$ (1 món mua trên 400 món không mua). Nếu để nguyên thế này train, mô hình sẽ bị \"ngợp\" và xu hướng đoán bừa là \"Không mua\" để cho an toàn.","metadata":{}},{"cell_type":"markdown","source":"**Bước 2**: Negative Sampling (Lấy mẫu âm) theo kiểu Top 1Bây giờ, chúng ta sẽ thực hiện chiêu thức \"Gạn đục khơi trong\". Chúng ta giữ lại toàn bộ 53,836 nhãn 1 và chỉ bốc ra khoảng 1,500,000 nhãn 0 ngẫu nhiên.Tại sao con số 1.5 triệu?Tỷ lệ vàng: Nó đưa tỷ lệ về khoảng $1:28$. Đây là tỷ lệ \"đẹp\" để các mô hình Ranker như LightGBM hoạt động hiệu quả nhất.Nhẹ máy: Tổng cộng bảng dữ liệu mới chỉ còn khoảng 1.55 triệu dòng. Với 42 cột tính năng, máy bạn sẽ chạy cực nhanh và không bao giờ bị tràn RAM.","metadata":{}},{"cell_type":"code","source":"import polars as pl\n\n# 1. Nấu riêng nhóm nhãn 1 (vì nó rất ít, chỉ 53k dòng, cực nhẹ)\nprint(\"--- Đang gom nhóm mua thật (Positives) ---\")\npos_df = train_with_label.filter(pl.col(\"target\") == 1).collect()\n\n# 2. Nấu nhóm nhãn 0 rồi mới bốc mẫu (21M dòng sẽ tốn khoảng 5-8GB RAM)\nprint(\"--- Đang gom nhóm không mua và lấy mẫu (Negatives) ---\")\n# Chúng ta collect() trước để đưa vào RAM, sau đó mới .sample()\nneg_df = train_with_label.filter(pl.col(\"target\") == 0).collect().sample(n=1500000, seed=42)\n\n# 3. Gộp lại và sắp xếp\nprint(\"--- Đang hợp nhất dữ liệu ---\")\ndf_train_final = pl.concat([pos_df, neg_df]).sort(\"customer_id\")\n\n# 4. Kiểm tra thành quả\nprint(\"\\n--- HOÀN THÀNH LẤY MẪU ÂM ---\")\nprint(f\"Số lượng nhãn 1: {df_train_final.filter(pl.col('target') == 1).shape[0]}\")\nprint(f\"Số lượng nhãn 0: {df_train_final.filter(pl.col('target') == 0).shape[0]}\")\nprint(f\"Tổng số dòng cuối cùng: {df_train_final.shape[0]:,}\")\nprint(f\"Dung lượng bảng trong RAM: {df_train_final.estimated_size('mb'):.2f} MB\")\n\n# In 5 dòng đầu để kiểm tra\nprint(df_train_final.head(5))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T13:24:47.536199Z","iopub.execute_input":"2026-04-21T13:24:47.537095Z","iopub.status.idle":"2026-04-21T13:24:56.701619Z","shell.execute_reply.started":"2026-04-21T13:24:47.537056Z","shell.execute_reply":"2026-04-21T13:24:56.700902Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Bây giờ chúng ta sẽ sang bước quan trọng nhất của thuật toán Lambdarank: đó là Grouping (Tạo nhóm).\n\nTại sao phải Grouping? Nếu bạn dùng mô hình Phân loại (Classifier) thông thường, máy tính chỉ nhìn từng dòng một cách độc lập. Nhưng với Ranker, máy tính cần nhìn theo kiểu: \"Trong danh sách 100 món này của ông Việt Anh, món nào đứng nhất?\".Để làm được điều đó, bạn phải đưa cho LightGBM một cái \"danh sách số lượng\" (mảng group). \n\nVí dụ:Khách A có 50 món ứng viên. Khách B có 30 món ứng viên.Khách C có 100 món ứng viên. Mảng group sẽ là [50, 30, 100].Vì ở bước trước chúng ta đã dùng lệnh .sort(\"customer_id\"), nên các món đồ của cùng một khách hàng đã nằm sát cạnh nhau. Việc đếm cực kỳ dễ dàng.\n\n\n**Bước 3**: Tạo mảng Group và Chia dữ liệu:","metadata":{}},{"cell_type":"code","source":"# 1. Tính toán số lượng ứng viên cho mỗi khách hàng (Mảng group)\n# Lưu ý: Phải giữ đúng thứ tự đã sort ở bước trước\ngroup_counts = df_train_final.group_by(\"customer_id\", maintain_order=True).count()\nq_train = group_counts[\"count\"].to_numpy()\n\n# 2. Chọn ra các cột sẽ dùng để \"Học\" (Features)\n# Chúng ta sẽ loại bỏ các cột định danh (ID) và cột đáp án (target)\nexcluded_cols = [\"customer_id\", \"article_id\", \"target\", \"postal_code\"]\nfeatures = [col for col in df_train_final.columns if col not in excluded_cols]\n\n# 3. Tách X (Đặc điểm) và y (Đáp án)\nX_train = df_train_final.select(features).to_pandas()\ny_train = df_train_final.select(\"target\").to_pandas()\n\nprint(\"--- CHUẨN BỊ GROUPING XONG ---\")\nprint(f\"Số lượng nhóm (khách hàng): {len(q_train):,}\")\nprint(f\"Số lượng tính năng (features) sẽ dùng: {len(features)}\")\nprint(f\"Ví dụ 5 tính năng đầu tiên: {features[:5]}\")\nprint(f\"Mảng group (5 phần tử đầu): {q_train[:5]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T13:25:09.339514Z","iopub.execute_input":"2026-04-21T13:25:09.340222Z","iopub.status.idle":"2026-04-21T13:25:09.862547Z","shell.execute_reply.started":"2026-04-21T13:25:09.340192Z","shell.execute_reply":"2026-04-21T13:25:09.861638Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ngon lành rồi! Các con số hiện ra rất khớp: 72,019 khách hàng với khoảng 1.5 triệu dòng dữ liệu là một \"tỉ lệ vàng\". RAM chỉ tốn gần 400MB nên chúng ta có thể thoải mái huấn luyện mà không sợ bị văng (Crash).\n\nBây giờ là lúc chúng ta thực hiện bước \"Khai hỏa\" — Huấn luyện 3 con LightGBM (3 Seeds) theo đúng chiến thuật của đội Top 1.\n\n\n**Bước 4**: Huấn luyện Hội đồng chuyên gia (3 Seeds)\nChúng ta sẽ sử dụng LGBMRanker. Khác với các mô hình dự đoán \"Mua/Không mua\" thông thường, con Ranker này sẽ học cách sắp xếp thứ tự. Nó sẽ cố gắng đẩy món đồ có nhãn 1 lên cao nhất có thể trong danh sách của từng khách hàng.\n\nMột lưu ý nhỏ: Trong 38 tính năng của bạn, có một vài cột là dạng chữ hoặc phân loại (như club_member_status, fashion_news_frequency). Mình sẽ bảo LightGBM tự động nhận diện và xử lý chúng.","metadata":{}},{"cell_type":"code","source":"from lightgbm import LGBMRanker\nimport joblib # Dùng để lưu mô hình lại sau khi train\n\n# 1. Danh sách các \"hạt giống\" (Seeds) để tạo ra 3 chuyên gia khác nhau\nseeds = [42, 123, 2024]\nmodels = []\n\n# 2. Xác định các cột là \"Dữ liệu phân loại\" (Categorical) để LightGBM xử lý riêng\n# Dựa trên danh sách features của bạn, đây là các cột thường là categorical\ncat_features = ['FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'age_binning', 'customer_cluster_id']\n# Lọc lại để chắc chắn các cột này có trong danh sách features hiện tại của bạn\ncat_features = [c for c in cat_features if c in features]\n\nprint(f\"--- BẮT ĐẦU HUẤN LUYỆN 3 MÔ HÌNH ---\")\n\nfor seed in seeds:\n    print(f\"\\n>> Đang luyện chuyên gia với Seed: {seed}...\")\n    \n    # Khởi tạo mô hình Ranker\n    ranker = LGBMRanker(\n        objective=\"lambdarank\",\n        metric=\"map\",          # Tối ưu trực tiếp cho điểm MAP\n        importance_type=\"gain\",\n        random_state=seed,\n        n_estimators=300,      # Số lượng cây (bạn có thể tăng lên 500 nếu muốn \"mạnh\" hơn)\n        learning_rate=0.05,    # Tốc độ học (nhỏ thì học kỹ nhưng lâu)\n        leaf_estimation_method=\"gradient\",\n        boosting_type=\"gbdt\"\n    )\n    \n    # Bắt đầu học\n    ranker.fit(\n        X_train, \n        y_train, \n        group=q_train,\n        categorical_feature=cat_features\n    )\n    \n    models.append(ranker)\n    print(f\"Hoàn thành mô hình Seed {seed}!\")\n\nprint(\"\\n--- CHÚC MỪNG! BẠN ĐÃ CÓ 3 CHUYÊN GIA AI ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T13:25:14.803518Z","iopub.execute_input":"2026-04-21T13:25:14.803822Z","iopub.status.idle":"2026-04-21T13:27:41.396022Z","shell.execute_reply.started":"2026-04-21T13:25:14.803795Z","shell.execute_reply":"2026-04-21T13:27:41.395064Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Tại sao bước này quan trọng?\nObjective lambdarank: Đây là \"linh hồn\" của Reranking. Nó giúp máy tính hiểu rằng: \"Tôi không quan tâm xác suất món này được mua là bao nhiêu, tôi chỉ quan tâm nó phải đứng TRÊN những món còn lại của khách đó\".\n\nCategorical Features: Bằng cách khai báo cat_features, bạn giúp LightGBM hiểu rằng \"Thành viên câu lạc bộ\" là các nhóm khác nhau, chứ không phải là những con số có thứ tự lớn bé.\n\nHệ thống 3 Seeds: Sau khi chạy xong, bạn sẽ thấy tiến trình chạy khá nhanh (mỗi con chắc chỉ mất 1-2 phút). Kết quả cuối cùng sẽ cực kỳ ổn định.\n\n","metadata":{}},{"cell_type":"markdown","source":"Lên luôn \"Gia vị\" đặc biệt! Việc thêm CatBoost vào giống như bạn đang mời một \"chuyên gia về chất liệu\" về làm việc cùng \"chuyên gia về tốc độ\" LightGBM vậy. CatBoost nổi tiếng với thuật toán YetiRank (một dạng nâng cấp của Lambdarank) và khả năng xử lý các cột phân loại (Màu sắc, Nhóm hàng, Phân khúc) cực kỳ thông minh mà không cần bạn phải tác động gì nhiều.\n\nTự xử lý Categorical: Bạn không cần lo lắng về việc ép kiểu hay xử lý giá trị âm. CatBoost tự xây dựng các bản đồ thống kê bên trong để hiểu mối quan hệ giữa các nhóm khách hàng.\n\nChống Overfitting cực tốt: Thuật toán của nó giúp mô hình bớt \"học vẹt\", điều này cực kỳ quan trọng khi tập fe_train và fe_val có sự lệch pha về thời gian.\n\n","metadata":{}},{"cell_type":"code","source":"from catboost import CatBoostRanker, Pool\n\n# 1. Danh sách hạt giống cho CatBoost\ncat_seeds = [7, 88]\ncat_models = []\n\n# 2. Chuẩn bị dữ liệu (CatBoost thích Pool để chạy nhanh hơn)\nprint(\"--- Đang chuẩn bị Pool cho CatBoost ---\")\n# CatBoost tự nhận diện categorical nên ta cần chỉ định chỉ số cột hoặc tên cột\ntrain_pool = Pool(\n    data=X_train,\n    label=y_train,\n    group_id=df_train_final[\"customer_id\"].to_numpy(), # Dùng ID khách làm Group ID\n    cat_features=cat_features\n)\n\nprint(f\"--- BẮT ĐẦU HUẤN LUYỆN 2 MÔ HÌNH CATBOOST ---\")\n\nfor seed in cat_seeds:\n    print(f\"\\n>> Đang luyện chuyên gia CatBoost với Seed: {seed}...\")\n    \n    cat_ranker = CatBoostRanker(\n        iterations=500,          # Tương đương n_estimators\n        learning_rate=0.05,\n        depth=6,                 # Độ sâu của cây\n        loss_function='YetiRank', # Thuật toán xếp hạng đặc trưng của CatBoost\n        random_seed=seed,\n        verbose=100              # In kết quả mỗi 100 vòng\n    )\n    \n    cat_ranker.fit(train_pool)\n    cat_models.append(cat_ranker)\n\nprint(\"\\n--- XONG! BẠN ĐÃ CÓ THÊM 2 CHUYÊN GIA CATBOOST ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T13:29:07.182036Z","iopub.execute_input":"2026-04-21T13:29:07.183304Z","iopub.status.idle":"2026-04-21T13:58:25.835790Z","shell.execute_reply.started":"2026-04-21T13:29:07.183270Z","shell.execute_reply":"2026-04-21T13:58:25.834931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import joblib\nimport os\n\n# Tạo thư mục lưu nếu cần (mặc định là /kaggle/working)\noutput_dir = \"/kaggle/working/\"\n\nprint(\"--- Đang 'đóng gói' chuyên gia để lưu vào Output ---\")\n\n# 1. Lưu 3 ông LightGBM\nfor i, lgbm_model in enumerate(models):\n    path = os.path.join(output_dir, f'lgbm_model_seed_{i}.pkl')\n    joblib.dump(lgbm_model, path)\n    print(f\"Đã lưu: {path}\")\n\n# 2. Lưu 2 ông CatBoost \nfor i, cat_model in enumerate(cat_models):\n    path = os.path.join(output_dir, f'catboost_model_seed_{i}.cbm')\n    cat_model.save_model(path)\n    print(f\"Đã lưu: {path}\")\n\nprint(\"✅ Xong! Bây giờ khi bạn Save & Run All, các file này sẽ xuất hiện trong tab Output.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T14:05:49.221122Z","iopub.execute_input":"2026-04-21T14:05:49.221754Z","iopub.status.idle":"2026-04-21T14:05:49.309497Z","shell.execute_reply.started":"2026-04-21T14:05:49.221727Z","shell.execute_reply":"2026-04-21T14:05:49.308814Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\"Trộn\" kết quả (Ensemble Blending)\nBây giờ mình sẽ dùng cả 3 ông LGBM và 2 ông CatBoost để cùng chấm điểm cho tập fe_val. Chúng ta sẽ dùng công thức Weighted Average (Trung bình có trọng số). Mình sẽ đặt tỷ lệ 60% cho LGBM và 40% cho CatBoost vì LGBM thường ổn định hơn trên dữ liệu này.","metadata":{}},{"cell_type":"code","source":"import polars as pl\n\n# 1. Nạp file Test (Dùng scan để xem metadata trước cho nhẹ)\ntest_path = \"/kaggle/input/datasets/huyenpham22/h-and-m-test-merge-fe/merged_final_test.parquet\"\ntest_lazy = pl.scan_parquet(test_path)\n\n# 2. Tính toán các thông số cơ bản\nprint(\"--- ĐANG KIỂM TRA FILE TEST ---\")\nshape = test_lazy.select(pl.len()).collect().item()\ncolumns = test_lazy.collect_schema().names()\nnum_features = len(columns)\n\n# 3. Đếm số lượng khách hàng duy nhất có trong file này\nunique_customers = test_lazy.select(pl.col(\"customer_id\").n_unique()).collect().item()\n\n# 4. Kiểm tra xem có đủ các cột quan trọng không\nimportant_cols = [\"customer_id\", \"article_id\"]\nmissing_cols = [c for c in important_cols if c not in columns]\n\n# 5. Xem thử 5 dòng đầu\nsample_df = test_lazy.head(5).collect()\n\nprint(f\"✅ Tổng số dòng (Candidates): {shape:,}\")\nprint(f\"✅ Số lượng khách hàng có Candidates: {unique_customers:,} / 1,371,980\")\nprint(f\"✅ Tổng số cột: {num_features}\")\nprint(f\"❌ Cột bị thiếu: {missing_cols if missing_cols else 'Không có'}\")\n\nprint(\"\\n--- DANH SÁCH 10 CỘT ĐẦU TIÊN ---\")\nprint(columns[:10])\n\nprint(\"\\n--- XEM THỬ DỮ LIỆU ---\")\nprint(sample_df)\n\n# 6. Kiểm tra Null ở các cột Categorical (Rất quan trọng cho CatBoost)\ncat_features = ['FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'age_binning', 'customer_cluster_id']\n# Lọc lại những cột thực sự có trong file test\npresent_cat_features = [c for c in cat_features if c in columns]\nnull_counts = test_lazy.select([pl.col(c).null_count() for c in present_cat_features]).collect()\n\nprint(\"\\n--- KIỂM TRA NULL TRONG CÁC CỘT PHÂN LOẠI ---\")\nprint(null_counts)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-24T06:21:09.813130Z","iopub.execute_input":"2026-04-24T06:21:09.813435Z","iopub.status.idle":"2026-04-24T06:21:23.512063Z","shell.execute_reply.started":"2026-04-24T06:21:09.813412Z","shell.execute_reply":"2026-04-24T06:21:23.511250Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Vì 411 triệu dòng là quá lớn, chúng ta sẽ không dự đoán một lượt. Thay vào đó, mình sẽ viết code để chia 1.37 triệu khách hàng ra thành nhiều nhóm nhỏ (Chunks).\n\nMỗi nhóm khoảng 100.000 khách hàng.\n\nDự đoán xong nhóm nào, lọc ngay Top 12, sau đó giải phóng RAM rồi mới làm nhóm tiếp theo.","metadata":{}},{"cell_type":"code","source":"import polars as pl\nimport numpy as np\nimport joblib\nimport gc\nimport os\nfrom catboost import Pool, CatBoostRanker\n\n# ==========================================\n# 1. KHỞI TẠO VÀ TRIỆU HỒI CHUYÊN GIA\n# ==========================================\nprint(\"--- BƯỚC 1: TRIỆU HỒI 5 CHUYÊN GIA & ĐỐI CHIẾU FEATURES ---\")\nMODEL_DIR = \"/kaggle/input/notebooks/phmthcvitanh/rerank/\"\nTEST_PATH = \"/kaggle/input/datasets/huyenpham22/h-and-m-test-merge-fe/merged_final_test.parquet\"\n\n# ĐƯỜNG DẪN ĐÃ SỬA THEO YÊU CẦU CỦA VIỆT ANH\nSAMPLE_SUB_PATH = \"/kaggle/input/competitions/h-and-m-personalized-fashion-recommendations/sample_submission.csv\"\n\n# Nạp 3 ông LightGBM\nlgbm_models = [joblib.load(os.path.join(MODEL_DIR, f'lgbm_model_seed_{i}.pkl')) for i in range(3)]\n\n# Nạp 2 ông CatBoost\ncat_models = []\nfor i in range(2):\n    m = CatBoostRanker()\n    m.load_model(os.path.join(MODEL_DIR, f'catboost_model_seed_{i}.cbm'))\n    cat_models.append(m)\n\n# Lấy danh sách 38 cột yêu cầu từ mô hình\ntry:\n    trained_features = lgbm_models[0].feature_name()\nexcept:\n    trained_features = lgbm_models[0].feature_name_\n\nprint(f\"✅ Mô hình yêu cầu chính xác: {len(trained_features)} tính năng.\")\n\ncat_features = ['FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'age_binning', 'customer_cluster_id']\ncat_features = [c for c in cat_features if c in trained_features]\n\n# ==========================================\n# 2. CHIA ĐỂ TRỊ (CHUNK_SIZE = 30K ĐỂ CỨU RAM)\n# ==========================================\nprint(\"--- BƯỚC 2: BẮT ĐẦU DỰ ĐOÁN THEO ĐỢT (BATCHING) ---\")\nall_customers = pl.read_parquet(TEST_PATH, columns=[\"customer_id\"])[\"customer_id\"].unique().to_list()\nCHUNK_SIZE = 30000 \nsubmission_chunks = []\n\nfor i in range(0, len(all_customers), CHUNK_SIZE):\n    batch_cust = all_customers[i : i + CHUNK_SIZE]\n    print(f\">> Xử lý đợt {i//CHUNK_SIZE + 1}/{(len(all_customers)//CHUNK_SIZE)+1}...\")\n    \n    # 2.1 Nạp dữ liệu Batch & Downcasting (Tiết kiệm 50% RAM)\n    df_batch = pl.scan_parquet(TEST_PATH).filter(pl.col(\"customer_id\").is_in(batch_cust)).collect()\n    df_batch = df_batch.with_columns([pl.col(pl.Float64).cast(pl.Float32)])\n    \n    # Bù đắp cột thiếu\n    missing_cols = [f for f in trained_features if f not in df_batch.columns]\n    if missing_cols:\n        df_batch = df_batch.with_columns([pl.lit(0).alias(f) for f in missing_cols])\n    \n    # Xử lý nhiễu age_binning\n    if \"age_binning\" in df_batch.columns:\n        df_batch = df_batch.with_columns([\n            pl.when(pl.col(\"age_binning\") < 0).then(999).otherwise(pl.col(\"age_binning\")).alias(\"age_binning\")\n        ])\n    \n    # Chuyển sang Pandas\n    X_batch = df_batch.select(trained_features).to_pandas()\n    \n    # Ép kiểu cho CatBoost\n    for col in cat_features:\n        if X_batch[col].dtype in ['float64', 'float32']:\n            X_batch[col] = X_batch[col].fillna(0).astype(np.int32)\n        else:\n            X_batch[col] = X_batch[col].astype(str)\n            \n    # 2.3 Dự đoán Hybrid (Cộng dồn trực tiếp)\n    lgbm_scores = np.zeros(len(X_batch), dtype=np.float32)\n    for m in lgbm_models:\n        lgbm_scores += m.predict(X_batch)\n    lgbm_scores /= 3\n    \n    cat_scores = np.zeros(len(X_batch), dtype=np.float32)\n    batch_pool = Pool(data=X_batch, cat_features=cat_features)\n    for m in cat_models:\n        cat_scores += m.predict(batch_pool)\n    cat_scores /= 2\n    \n    final_scores = (0.6 * lgbm_scores) + (0.4 * cat_scores)\n    \n    # 2.4 Lọc Top 12 & Nối chuỗi dự đoán\n    df_batch = df_batch.with_columns(pl.Series(name=\"score\", values=final_scores))\n    \n    top_12 = (\n        df_batch.select([\"customer_id\", \"article_id\", \"score\"])\n        .sort([\"customer_id\", \"score\"], descending=[False, True])\n        .group_by(\"customer_id\")\n        .head(12)\n        .with_columns(pl.col(\"article_id\").cast(pl.String).str.pad_start(10, fill_char=\"0\"))\n        .group_by(\"customer_id\")\n        .agg(pl.col(\"article_id\").str.join(\" \").alias(\"prediction\"))\n    )\n    \n    submission_chunks.append(top_12)\n    \n    # DỌN DẸP RAM\n    del df_batch, X_batch, batch_pool, lgbm_scores, cat_scores, final_scores\n    gc.collect()\n\n# ==========================================\n# 3. HỢP NHẤT VÀ XUẤT FILE\n# ==========================================\nprint(\"--- BƯỚC 3: HỢP NHẤT VÀ LƯU FILE ---\")\nsubmission_df = pl.concat(submission_chunks)\n\n# Kiểm tra file mẫu trước khi nạp\nif os.path.exists(SAMPLE_SUB_PATH):\n    sample_sub = pl.read_csv(SAMPLE_SUB_PATH)\n    final_sub = sample_sub.select([\"customer_id\"]).join(submission_df, on=\"customer_id\", how=\"left\")\n\n    global_popular = \"0706016001 0706016002 0372860001 0610776002 0759871002 0464297007 0372860002 0610776001 0399223001 0706016003 0720125001 0156231001\"\n    final_sub = final_sub.with_columns(pl.col(\"prediction\").fill_null(global_popular))\n\n    final_sub.write_csv(\"submission.csv\")\n    print(\"✅ HOÀN TẤT! File 'submission.csv' đã sẵn sàng để nộp.\")\nelse:\n    print(f\"❌ VẪN LỖI: Không tìm thấy file tại {SAMPLE_SUB_PATH}. Hãy kiểm tra lại mục Data bên phải!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}