{"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":15306974,"datasetId":9790863,"databundleVersionId":16211685}],"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cudf\nimport pandas as pd\nimport numpy as np\nimport lightgbm as lgb\nimport pyarrow.parquet as pq\nimport gc\n\n# ============================================================\n# BƯỚC 0: [CPU] ĐỌC + LỌC + TẠO VAL_PURCHASES + ENCODE\n# ============================================================\nprint(\"0. [CPU] Đọc transactions, tạo val_purchases, encode...\")\n\ntrans_pd = pd.read_csv(\n    '/kaggle/input/competitions/h-and-m-personalized-fashion-recommendations/transactions_train.csv',\n    usecols=['customer_id', 'article_id', 'price', 't_dat'],\n    dtype={'customer_id': 'str', 'article_id': 'str', 'price': 'float32'}\n)\ntrans_pd = trans_pd[trans_pd['t_dat'] >= '2020-07-01'].reset_index(drop=True)\nprint(f\"  -> Sau lọc ngày: {len(trans_pd):,} dòng\")\n\nVAL_START_STR = '2020-09-15'\n\ntrans_pd['customer_id'] = trans_pd['customer_id'].str.strip().str.lower()\ntrans_pd['article_id']  = trans_pd['article_id'].str.strip().str.lower()\n\nval_mask      = trans_pd['t_dat'] >= VAL_START_STR\nval_purch_set = set(\n    zip(trans_pd.loc[val_mask, 'customer_id'],\n        trans_pd.loc[val_mask, 'article_id'])\n)\nprint(f\"  -> val_purchases set: {len(val_purch_set):,} cặp\")\n\ncust_map = {v: i for i, v in enumerate(trans_pd['customer_id'].unique())}\nart_map  = {v: i for i, v in enumerate(trans_pd['article_id'].unique())}\n\ntrans_pd['customer_id'] = trans_pd['customer_id'].map(cust_map).astype('int32')\ntrans_pd['article_id']  = trans_pd['article_id'].map(art_map).astype('int32')\n\npd.Series({v: k for k, v in cust_map.items()}).to_pickle('cust_decode.pkl')\npd.Series({v: k for k, v in art_map.items()}).to_pickle('art_decode.pkl')\nprint(f\"  -> Encode: {len(cust_map):,} customers | {len(art_map):,} articles\")\n\n# ============================================================\n# BƯỚC 1: [GPU] TRANSACTIONS LÊN GPU\n# ============================================================\nprint(\"\\n1. [GPU] Đẩy transactions lên GPU...\")\n\ntrans = cudf.DataFrame(trans_pd)\ndel trans_pd; gc.collect()\n\ntrans['t_dat'] = cudf.to_datetime(trans['t_dat'])\nVAL_START      = cudf.to_datetime(VAL_START_STR)\n\ntrans_hist = trans[trans['t_dat'] < VAL_START].copy()\ndel trans; gc.collect()\nprint(f\"  -> trans_hist: {len(trans_hist):,}\")\n\n# ============================================================\n# BƯỚC 2: [CPU] CANDIDATES THEO CHUNK\n# ============================================================\nprint(\"\\n2. [CPU] Đọc Candidates theo chunk, gán label, downsample, encode...\")\n\nPARQUET_PATH = '/kaggle/input/datasets/phngphngtrn/hihiiii/candidates_v4_final_top1000.parquet'\nCHUNK_SIZE   = 3_000_000\nNEG_FRAC     = 0.05\n\npf = pq.ParquetFile(PARQUET_PATH)\nchunks_pos, chunks_neg = [], []\ntotal_pos = total_neg = 0\n\nfor i, batch in enumerate(pf.iter_batches(batch_size=CHUNK_SIZE)):\n    chunk = batch.to_pandas()\n    chunk['customer_id'] = chunk['customer_id'].astype(str).str.strip().str.lower()\n    chunk['article_id']  = chunk['article_id'].astype(str).str.strip().str.lower()\n\n    chunk['label'] = chunk.apply(\n        lambda r: 1 if (r['customer_id'], r['article_id']) in val_purch_set else 0,\n        axis=1\n    ).astype('int8')\n\n    pos = chunk[chunk['label'] == 1].copy()\n    neg = chunk[chunk['label'] == 0].sample(frac=NEG_FRAC, random_state=42).copy()\n    total_pos += len(pos)\n    total_neg += len(neg)\n\n    for part in [pos, neg]:\n        part['customer_id'] = part['customer_id'].map(cust_map)\n        part['article_id']  = part['article_id'].map(art_map)\n        part.dropna(subset=['customer_id', 'article_id'], inplace=True)\n        part['customer_id'] = part['customer_id'].astype('int32')\n        part['article_id']  = part['article_id'].astype('int32')\n        if len(part) > 0:\n            if part['label'].iloc[0] == 1:\n                chunks_pos.append(part)\n            else:\n                chunks_neg.append(part)\n\n    print(f\"  chunk {i+1}: pos={len(pos):,} | neg_sampled={len(neg):,} \"\n          f\"| running total pos={total_pos:,}\")\n    del chunk, pos, neg; gc.collect()\n\ndel val_purch_set; gc.collect()\n\nif total_pos == 0:\n    sample = pq.read_table(PARQUET_PATH).slice(0, 3).to_pandas()\n    print(\"⚠️  pos=0! Candidates sample:\", sample[['customer_id', 'article_id']].values.tolist())\n    raise ValueError(\"ID format không khớp\")\n\ndf_pd = pd.concat(chunks_pos + chunks_neg, ignore_index=True)\ndel chunks_pos, chunks_neg; gc.collect()\n\nprint(f\"\\n  -> Tổng sau downsample: {len(df_pd):,} dòng | \"\n      f\"pos={int((df_pd['label']==1).sum()):,} | \"\n      f\"neg={int((df_pd['label']==0).sum()):,}\")\n\ndf = cudf.DataFrame(df_pd)\ndel df_pd; gc.collect()\nprint(f\"  -> Lên GPU: {len(df):,} dòng\")\n\n# ============================================================\n# BƯỚC 3: [GPU] ĐỌC BẢNG PHỤ + ENCODE\n# ============================================================\nprint(\"\\n3. [GPU] Đọc bảng phụ và encode...\")\n\n# Đọc articles — kiểm tra dtype index_group_no\narts_raw = pd.read_csv(\n    '/kaggle/input/datasets/phngphngtrn/hihiiii/articles_cleaned_full.csv',\n    nrows=2\n)\nprint(f\"  -> Articles columns: {arts_raw.columns.tolist()}\")\nprint(f\"  -> index_group_no sample:\\n{arts_raw[['article_id','index_group_no']]}\")\narts_pd = pd.read_csv(\n    '/kaggle/input/datasets/phngphngtrn/hihiiii/articles_cleaned_full.csv',\n    usecols=['article_id', 'index_group_no'],\n    dtype={'article_id': 'str'}\n)\n\n# ★ SỬA Ở ĐÂY: Thêm .zfill(10) để khôi phục số 0 ở đầu bị mất\narts_pd['article_id'] = arts_pd['article_id'].str.strip().str.zfill(10)\n\n# Sau đó mới map với art_map (được tạo ra từ transactions có đủ 10 ký tự)\narts_pd['article_id'] = arts_pd['article_id'].map(art_map)\narts_pd = arts_pd.dropna(subset=['article_id'])\narts_pd['article_id'] = arts_pd['article_id'].astype('int32')\n\n# Ép kiểu int8\narts_pd['index_group_no'] = pd.to_numeric(arts_pd['index_group_no'], errors='coerce')\narts_pd['index_group_no'] = arts_pd['index_group_no'].fillna(-1).astype('int8')\narts = cudf.DataFrame(arts_pd)\n\nprint(f\"  -> arts_pd shape sau map: {arts_pd.shape}\")\nprint(f\"  -> arts index_group_no unique: {arts_pd['index_group_no'].nunique()} | \"\n      f\"sample: {arts_pd['index_group_no'].value_counts().head().to_dict()}\")\ndel arts_pd; gc.collect()\n\ncusts_pd = pd.read_csv(\n    '/kaggle/input/competitions/h-and-m-personalized-fashion-recommendations/customers.csv',\n    usecols=['customer_id', 'age'],\n    dtype={'customer_id': 'str'}\n)\ncusts_pd['age']         = custs_pd['age'].fillna(custs_pd['age'].median()).astype('int8')\ncusts_pd['customer_id'] = custs_pd['customer_id'].str.strip().str.lower().map(cust_map)\ncusts_pd = custs_pd.dropna(subset=['customer_id'])\ncusts_pd['customer_id'] = custs_pd['customer_id'].astype('int32')\ncusts = cudf.DataFrame(custs_pd)\ndel custs_pd; gc.collect()\n\nprint(\"  -> articles và customers sẵn sàng\")\n\n# ============================================================\n# BƯỚC 4: [GPU] FEATURE ENGINEERING — BASE FEATURES\n# ============================================================\nprint(\"\\n4. [GPU] Feature Engineering — Base...\")\n\n# USER FEATURES\nuser_price = (\n    trans_hist.groupby('customer_id')['price']\n    .agg(['mean', 'max', 'std'])\n    .reset_index()\n)\nuser_price.columns = ['customer_id', 'user_mean_price', 'user_max_price', 'user_std_price']\ndf = df.merge(user_price, on='customer_id', how='left')\ndel user_price; gc.collect()\n\nuser_last = trans_hist.groupby('customer_id')['t_dat'].max().reset_index()\nuser_last['user_recency'] = (VAL_START - user_last['t_dat']).dt.days.astype('int16')\ndf = df.merge(user_last[['customer_id', 'user_recency']], on='customer_id', how='left')\ndel user_last; gc.collect()\n\nuser_total = trans_hist.groupby('customer_id').size().reset_index()\nuser_total.columns = ['customer_id', 'user_total_purchases']\ndf = df.merge(user_total, on='customer_id', how='left')\ndel user_total; gc.collect()\n\nuser_diversity = trans_hist.groupby('customer_id')['article_id'].nunique().reset_index()\nuser_diversity.columns = ['customer_id', 'user_unique_items']\ndf = df.merge(user_diversity, on='customer_id', how='left')\ndel user_diversity; gc.collect()\n\ndf = df.merge(custs, on='customer_id', how='left')\n\n# ITEM FEATURES\nitem_pop = trans_hist.groupby('article_id').size().reset_index()\nitem_pop.columns = ['article_id', 'item_total_sales']\ndf = df.merge(item_pop, on='article_id', how='left')\ndel item_pop; gc.collect()\n\nitem_sales_7d = (\n    trans_hist[trans_hist['t_dat'] >= (VAL_START - pd.Timedelta(days=7))]\n    .groupby('article_id').size().reset_index()\n)\nitem_sales_7d.columns = ['article_id', 'item_sales_7d']\ndf = df.merge(item_sales_7d, on='article_id', how='left')\ndel item_sales_7d; gc.collect()\n\nitem_sales_14d = (\n    trans_hist[trans_hist['t_dat'] >= (VAL_START - pd.Timedelta(days=14))]\n    .groupby('article_id').size().reset_index()\n)\nitem_sales_14d.columns = ['article_id', 'item_sales_14d']\ndf = df.merge(item_sales_14d, on='article_id', how='left')\ndel item_sales_14d; gc.collect()\n\nitem_sales_28d = (\n    trans_hist[trans_hist['t_dat'] >= (VAL_START - pd.Timedelta(days=28))]\n    .groupby('article_id').size().reset_index()\n)\nitem_sales_28d.columns = ['article_id', 'item_sales_28d']\ndf = df.merge(item_sales_28d, on='article_id', how='left')\ndel item_sales_28d; gc.collect()\n\nitem_mean_price = trans_hist.groupby('article_id')['price'].mean().reset_index()\nitem_mean_price.columns = ['article_id', 'item_current_price']\ndf = df.merge(item_mean_price, on='article_id', how='left')\ndel item_mean_price; gc.collect()\n\nitem_age     = trans_hist[['customer_id', 'article_id']].merge(custs, on='customer_id', how='inner')\nitem_avg_age = item_age.groupby('article_id')['age'].mean().reset_index()\nitem_avg_age.columns = ['article_id', 'item_avg_age']\ndf = df.merge(item_avg_age, on='article_id', how='left')\ndel item_age, item_avg_age; gc.collect()\n\nitem_first_seen = trans_hist.groupby('article_id')['t_dat'].min().reset_index()\nitem_first_seen['item_age_days'] = (VAL_START - item_first_seen['t_dat']).dt.days.astype('int16')\ndf = df.merge(item_first_seen[['article_id', 'item_age_days']], on='article_id', how='left')\ndel item_first_seen; gc.collect()\n\n# USER-ITEM FEATURES\nuser_item_rep = trans_hist.groupby(['customer_id', 'article_id']).size().reset_index()\nuser_item_rep.columns = ['customer_id', 'article_id', 'user_item_purchase_count']\ndf = df.merge(user_item_rep, on=['customer_id', 'article_id'], how='left')\ndel user_item_rep; gc.collect()\n\n# ARTICLE META — merge arts + verify\ndf = df.merge(arts, on='article_id', how='left')\nnull_cat = int(df['index_group_no'].isna().sum())\nprint(f\"  -> index_group_no null sau merge: {null_cat:,} / {len(df):,}\")\n# ★ FIX: fillna -1 ngay sau merge, TRƯỚC khi dùng cho category features\ndf['index_group_no'] = df['index_group_no'].fillna(-1).astype('int8')\nprint(f\"  -> index_group_no unique: {df['index_group_no'].nunique()} | \"\n      f\"sample: {df['index_group_no'].value_counts().head().to_pandas().to_dict()}\")\n\n# ============================================================\n# BƯỚC 4B: TEMPORAL DECAY FEATURES\n# ============================================================\nprint(\"\\n4B. Temporal Decay features...\")\n\ntmp = trans_hist.copy()\ntmp['days_ago'] = (VAL_START - tmp['t_dat']).dt.days.astype('float32')\ntmp['weight']   = (1.0 / (tmp['days_ago'] + 1)).astype('float32')\n\nitem_weighted_pop = tmp.groupby('article_id')['weight'].sum().reset_index()\nitem_weighted_pop.columns = ['article_id', 'item_weighted_pop']\ndf = df.merge(item_weighted_pop, on='article_id', how='left')\ndel item_weighted_pop; gc.collect()\n\nuser_weighted_act = tmp.groupby('customer_id')['weight'].sum().reset_index()\nuser_weighted_act.columns = ['customer_id', 'user_weighted_activity']\ndf = df.merge(user_weighted_act, on='customer_id', how='left')\ndel user_weighted_act; gc.collect()\n\nuser_item_weighted = tmp.groupby(['customer_id', 'article_id'])['weight'].sum().reset_index()\nuser_item_weighted.columns = ['customer_id', 'article_id', 'user_item_weighted_score']\ndf = df.merge(user_item_weighted, on=['customer_id', 'article_id'], how='left')\ndel user_item_weighted; gc.collect()\n\nuser_item_last = tmp.groupby(['customer_id', 'article_id'])['t_dat'].max().reset_index()\nuser_item_last['user_item_recency'] = (VAL_START - user_item_last['t_dat']).dt.days.astype('int16')\ndf = df.merge(user_item_last[['customer_id', 'article_id', 'user_item_recency']],\n              on=['customer_id', 'article_id'], how='left')\ndel user_item_last, tmp; gc.collect()\n\nprint(\"  -> Xong: item_weighted_pop, user_weighted_activity, \"\n      \"user_item_weighted_score, user_item_recency\")\n\n# ============================================================\n# BƯỚC 4C: CATEGORY AFFINITY FEATURES\n# ============================================================\nprint(\"\\n4C. Category Affinity features...\")\n\n# ★ FIX: merge arts vào trans_hist với dtype đã chuẩn\ntrans_with_cat = trans_hist.merge(arts, on='article_id', how='left')\ntrans_with_cat['index_group_no'] = trans_with_cat['index_group_no'].fillna(-1).astype('int8')\n\n# Verify join\nn_known_cat = int((trans_with_cat['index_group_no'] != -1).sum())\nprint(f\"  -> trans_with_cat: {len(trans_with_cat):,} dòng | \"\n      f\"known category: {n_known_cat:,} ({100*n_known_cat/len(trans_with_cat):.1f}%)\")\n\n# Chỉ tính trên known category (loại -1)\ntwc = trans_with_cat[trans_with_cat['index_group_no'] != -1]\n\n# B1. User-Category purchase count\nuser_cat_count = (\n    twc.groupby(['customer_id', 'index_group_no'])\n    .size().reset_index()\n)\nuser_cat_count.columns = ['customer_id', 'index_group_no', 'user_cat_purchase_count']\ndf = df.merge(user_cat_count, on=['customer_id', 'index_group_no'], how='left')\nprint(f\"  -> user_cat_purchase_count non-zero: \"\n      f\"{int((df['user_cat_purchase_count'].fillna(0) > 0).sum()):,}\")\ndel user_cat_count; gc.collect()\n\n# B2. User-Category ratio\nuser_total_tmp = trans_hist.groupby('customer_id').size().reset_index()\nuser_total_tmp.columns = ['customer_id', 'user_total_tmp']\nuser_cat_ratio = twc.groupby(['customer_id', 'index_group_no']).size().reset_index()\nuser_cat_ratio.columns = ['customer_id', 'index_group_no', 'cat_count_tmp']\nuser_cat_ratio = user_cat_ratio.merge(user_total_tmp, on='customer_id', how='left')\nuser_cat_ratio['user_cat_ratio'] = (\n    user_cat_ratio['cat_count_tmp'] / (user_cat_ratio['user_total_tmp'] + 1)\n).astype('float32')\ndf = df.merge(\n    user_cat_ratio[['customer_id', 'index_group_no', 'user_cat_ratio']],\n    on=['customer_id', 'index_group_no'], how='left'\n)\ndel user_cat_ratio, user_total_tmp; gc.collect()\n\n# B3. User-Category recency\nuser_cat_last = (\n    twc.groupby(['customer_id', 'index_group_no'])['t_dat']\n    .max().reset_index()\n)\nuser_cat_last['user_cat_recency'] = (\n    (VAL_START - user_cat_last['t_dat']).dt.days.astype('int16')\n)\ndf = df.merge(\n    user_cat_last[['customer_id', 'index_group_no', 'user_cat_recency']],\n    on=['customer_id', 'index_group_no'], how='left'\n)\ndel user_cat_last; gc.collect()\n\n# B4. User-Category weighted score\ntmp_cat = twc.copy()\ntmp_cat['days_ago'] = (VAL_START - tmp_cat['t_dat']).dt.days.astype('float32')\ntmp_cat['weight']   = (1.0 / (tmp_cat['days_ago'] + 1)).astype('float32')\nuser_cat_weighted = (\n    tmp_cat.groupby(['customer_id', 'index_group_no'])['weight']\n    .sum().reset_index()\n)\nuser_cat_weighted.columns = ['customer_id', 'index_group_no', 'user_cat_weighted_score']\ndf = df.merge(user_cat_weighted, on=['customer_id', 'index_group_no'], how='left')\ndel user_cat_weighted, tmp_cat; gc.collect()\n\n# B5. Category sales 7d & 14d\ncat_pop_7d = (\n    trans_hist[trans_hist['t_dat'] >= (VAL_START - pd.Timedelta(days=7))]\n    .merge(arts, on='article_id', how='left')\n    .groupby('index_group_no').size().reset_index()\n)\ncat_pop_7d.columns = ['index_group_no', 'cat_sales_7d']\ndf = df.merge(cat_pop_7d, on='index_group_no', how='left')\ndel cat_pop_7d; gc.collect()\n\ncat_pop_14d = (\n    trans_hist[trans_hist['t_dat'] >= (VAL_START - pd.Timedelta(days=14))]\n    .merge(arts, on='article_id', how='left')\n    .groupby('index_group_no').size().reset_index()\n)\ncat_pop_14d.columns = ['index_group_no', 'cat_sales_14d']\ndf = df.merge(cat_pop_14d, on='index_group_no', how='left')\ndel cat_pop_14d, twc, trans_with_cat; gc.collect()\n\ndel arts, custs, trans_hist; gc.collect()\n\nprint(\"  -> Xong: user_cat_purchase_count, user_cat_ratio, user_cat_recency, \"\n      \"user_cat_weighted_score, cat_sales_7d/14d\")\nprint(f\"\\n  -> Tổng sau feature engineering: {len(df):,} dòng x {len(df.columns)} cột\")\n\n# ============================================================\n# BƯỚC 5: [GPU] FILL NA + CROSS FEATURES\n# ============================================================\nprint(\"\\n5. [GPU] Fill NA và Cross Features...\")\n\ndf['user_recency']          = df['user_recency'].fillna(999).astype('int16')\ndf['user_item_recency']     = df['user_item_recency'].fillna(999).astype('int16')\ndf['user_cat_recency']      = df['user_cat_recency'].fillna(999).astype('int16')\ndf['item_age_days']         = df['item_age_days'].fillna(999).astype('int16')\ndf = df.fillna(0)\n\ndf['price_diff']         = (df['item_current_price'] - df['user_mean_price']).astype('float32')\ndf['age_diff']           = (df['age'] - df['item_avg_age']).abs().astype('float32')\ndf['trend_acceleration'] = (df['item_sales_7d']  / (df['item_sales_14d'] + 1)).astype('float32')\ndf['trend_28d']          = (df['item_sales_14d'] / (df['item_sales_28d'] + 1)).astype('float32')\ndf['purchase_rate']      = (df['user_item_purchase_count'] / (df['user_total_purchases'] + 1)).astype('float32')\ndf['diversity_ratio']    = (df['user_unique_items'] / (df['user_total_purchases'] + 1)).astype('float32')\ndf['affinity_x_trend']   = (df['user_cat_weighted_score'] * df['item_weighted_pop']).astype('float32')\ndf['cat_momentum']       = (df['cat_sales_7d'] / (df['cat_sales_14d'] + 1)).astype('float32')\ndf['user_item_affinity'] = (df['user_item_weighted_score'] * df['user_cat_ratio']).astype('float32')\ndf['is_new_to_user']     = (df['user_item_purchase_count'] == 0).astype('int8')\n\nprint(f\"  -> Tổng {len(df.columns)} cột sau cross features\")\n\n# ============================================================\n# BƯỚC 6: [GPU → CPU] TRAIN LIGHTGBM LAMBDARANK\n# ============================================================\nprint(\"\\n6. [GPU→CPU] Train LightGBM LambdaRank...\")\n\ndf_train = df.to_pandas()\ndel df; gc.collect()\n\ndf_train = df_train.sort_values('customer_id').reset_index(drop=True)\ndf_train['index_group_no'] = df_train['index_group_no'].astype('category')\n\nCOLS_DROP = ['customer_id', 'article_id', 'label']\nfeatures  = [c for c in df_train.columns if c not in COLS_DROP]\n\nprint(f\"  -> {len(df_train):,} dòng | {len(features)} features:\")\nfor f in features:\n    print(f\"     - {f}\")\nprint(f\"  -> pos={int((df_train['label']==1).sum()):,} | \"\n      f\"neg={int((df_train['label']==0).sum()):,}\")\n\nquery_groups  = df_train.groupby('customer_id', sort=False).size().values\ntrain_dataset = lgb.Dataset(\n    data=df_train[features],\n    label=df_train['label'],\n    group=query_groups\n)\n\nparams = {\n    'objective':         'lambdarank',\n    'metric':            'ndcg',\n    'ndcg_eval_at':      [12],\n    'boosting_type':     'gbdt',\n    'learning_rate':     0.03,\n    'num_leaves':        127,\n    'min_child_samples': 20,\n    'feature_fraction':  0.8,\n    'bagging_fraction':  0.8,\n    'bagging_freq':      1,\n    'lambda_l1':         0.1,\n    'lambda_l2':         0.1,\n    'device':            'gpu',\n    'random_state':      42,\n    'verbose':           -1,\n}\n\nmodel = lgb.train(params, train_dataset, num_boost_round=300)\nmodel.save_model('lgbm_hm_v5.txt')\nprint(\"  -> Đã lưu: lgbm_hm_v5.txt\")\n\nimportance = (\n    pd.DataFrame({\n        'feature':    features,\n        'importance': model.feature_importance('gain')\n    })\n    .sort_values('importance', ascending=False)\n)\nprint(\"\\n--- TOP FEATURES ---\")\nprint(importance.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T15:04:27.299766Z","iopub.execute_input":"2026-03-22T15:04:27.300740Z","iopub.status.idle":"2026-03-22T15:16:40.072464Z","shell.execute_reply.started":"2026-03-22T15:04:27.300697Z","shell.execute_reply":"2026-03-22T15:16:40.071510Z"}},"outputs":[],"execution_count":null}]}