{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ==============================================================================\n# SECTION 1: 資料匯入與全域優化\n# ==============================================================================\nimport os\nimport numpy as np\nimport pandas as pd\nimport lightgbm as lgb\n\n# 1. 基礎路徑定義\nBASE_PATH = '/kaggle/input/competitions/h-and-m-personalized-fashion-recommendations/'\n\nprint(\"[1/3] 正在載入三大核心資料表 (已啟動欄位型態優化)...\")\n\n# 【核心優化】直接在讀取時指定 article_id 為字串型態，避免前導零消失\narticles_df = pd.read_csv(BASE_PATH + 'articles.csv', dtype={'article_id': str})\ncustomers_df = pd.read_csv(BASE_PATH + 'customers.csv')\ntransactions_df = pd.read_csv(BASE_PATH + 'transactions_train.csv', dtype={'article_id': str})\n\nprint(\"[2/3] 正在優化時間特徵型態...\")\n# 將交易日期直接轉為 datetime 物件，方便後續時間切分\ntransactions_df['t_dat'] = pd.to_datetime(transactions_df['t_dat'])\n\nprint(\"[3/3] 正在建立全域特徵查找字典...\")\n# 預先填補年齡缺失值\ncustomers_df['age'] = customers_df['age'].fillna(customers_df['age'].mean())\n\n# 建立商品大類對照字典 (index_group_name)\nARTICLE_TO_GROUP = articles_df.set_index('article_id')['index_group_name'].to_dict()\n\nprint(\"\\n第 1 區塊完成，資料全域狀態：\")\nprint(f\"Articles 欄位形狀: {articles_df.shape} | ID型態: {articles_df['article_id'].dtype}\")\nprint(f\"Customers 欄位形狀: {customers_df.shape}\")\nprint(f\"Transactions 欄位形狀: {transactions_df.shape} | ID型態: {transactions_df['article_id'].dtype}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-06-17T12:15:36.312712Z","iopub.execute_input":"2026-06-17T12:15:36.313367Z","iopub.status.idle":"2026-06-17T12:16:19.899315Z","shell.execute_reply.started":"2026-06-17T12:15:36.313332Z","shell.execute_reply":"2026-06-17T12:16:19.898403Z"},"_kg_hide-input":false},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==============================================================================\n# SECTION 2: 本地模擬考場與 MAP@12 算分系統\n# ==============================================================================\n\nprint(\"[1/3] 正在進行時間序列切分 (最後 7 天為考卷，先前為歷史)...\")\nmax_date = transactions_df['t_dat'].max()\nval_start_date = max_date - pd.Timedelta(days=7)\n\n# 切分訓練集與驗證集\nval_df = transactions_df[transactions_df['t_dat'] >= val_start_date].copy()\ntrain_df = transactions_df[transactions_df['t_dat'] < val_start_date].copy()\n\nprint(\"[2/3] 正在整理考卷標準答案...\")\n# 整理出最後 7 天內，每位顧客實際買了哪些商品（此時 ID 已經全域皆為優化字串）\nval_truth = val_df.groupby('customer_id')['article_id'].apply(set).to_dict()\nactive_val_customers = list(val_truth.keys())\n\nprint(\"[3/3] 正在載入 MAP@12 核心算分評估指標函數...\")\ndef apk(actual, predicted, k=12):\n    \"\"\"計算單一用戶的 Average Precision at K\"\"\"\n    if not actual: \n        return 0.0\n    if len(predicted) > k: \n        predicted = predicted[:k]\n    score = 0.0\n    num_hits = 0.0\n    for i, p in enumerate(predicted):\n        if p in actual and p not in predicted[:i]:\n            num_hits += 1.0\n            score += num_hits / (i + 1.0)\n    return score / min(len(actual), k)\n\nprint(\"\\n第 2 區塊完成，模擬考場準備就緒：\")\nprint(f\"歷史交易樣本數 (Train): {train_df.shape[0]} 筆\")\nprint(f\"模擬考卷答案數 (Validation): {val_df.shape[0]} 筆\")\nprint(f\"模擬考場有效應試顧客數: {len(active_val_customers)} 人\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-17T12:18:02.922300Z","iopub.execute_input":"2026-06-17T12:18:02.923096Z","iopub.status.idle":"2026-06-17T12:18:07.162189Z","shell.execute_reply.started":"2026-06-17T12:18:02.923068Z","shell.execute_reply":"2026-06-17T12:18:07.161483Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==============================================================================\n# SECTION 3: 統計特徵、衰減熱度、W2V 語義與年齡基準建構\n# ==============================================================================\nimport pandas as pd\nimport numpy as np\nimport warnings\nwarnings.filterwarnings(\"ignore\")\nfrom gensim.models import Word2Vec\n\nprint(\"[1/5] 正在計算【時間衰減熱度】與【商業價格/生命週期字典】...\")\ntrain_max_date = train_df['t_dat'].max()\n\n# 1. 基礎時間衰減熱度 (最近 3 個月)\nrecent_3m_train = train_df[train_df['t_dat'] >= (train_max_date - pd.Timedelta(days=90))].copy()\nrecent_3m_train['days_ago'] = (train_max_date - recent_3m_train['t_dat']).dt.days\nrecent_3m_train['decay_weight'] = 1.0 / np.sqrt(recent_3m_train['days_ago'] + 1)\nITEM_DECAY_POP = recent_3m_train.groupby('article_id')['decay_weight'].sum().to_dict()\n\n# 商業特徵字典\nITEM_MAX_PRICE = train_df.groupby('article_id')['price'].max().to_dict() \nrecent_4w_train = train_df[train_df['t_dat'] >= (train_max_date - pd.Timedelta(weeks=4))].copy()\nrecent_4w_train['days_ago'] = (train_max_date - recent_4w_train['t_dat']).dt.days\nITEM_CURR_PRICE = recent_4w_train.groupby('article_id')['price'].mean().to_dict() \n\n# 商品生命週期\ntrain_df['days_ago_total'] = (train_max_date - train_df['t_dat']).dt.days\nITEM_RELEASE_AGE = train_df.groupby('article_id')['days_ago_total'].max().to_dict() \nITEM_LAST_SALE_AGE = recent_4w_train.groupby('article_id')['days_ago'].min().to_dict() \n\nprint(\"[2/5] 正在計算【商品平均買家年齡字典】...\")\n# 將顧客年齡 merge 到 3 個月歷史中，精準算出每件商品的平均買家年齡\ndf_temp_age = recent_3m_train.merge(customers_df[['customer_id', 'age']], on='customer_id', how='left')\nARTICLE_MEAN_AGE = df_temp_age.groupby('article_id')['age'].mean().to_dict()\n\nprint(\"[3/5] 正在計算「同年齡層」與「全站」的最新衰減爆款...\")\nbins = [0, 20, 30, 40, 50, 60, 100]\nlabels = ['<20', '21-30', '31-40', '41-50', '51-60', '>60']\ncustomers_df['age_group'] = pd.cut(customers_df['age'], bins=bins, labels=labels, right=True)\nCUST_AGE_DICT = customers_df.set_index('customer_id')['age_group'].to_dict()\n\nrecent_3m_train['age_group'] = recent_3m_train['customer_id'].map(CUST_AGE_DICT)\nAGE_HOT_DECAY = {}\nfor age_grp, a_df in recent_3m_train.groupby('age_group'):\n    AGE_HOT_DECAY[age_grp] = a_df.groupby('article_id')['decay_weight'].sum().sort_values(ascending=False).head(12).index.tolist()\nTOP_12_DECAY = recent_3m_train.groupby('article_id')['decay_weight'].sum().sort_values(ascending=False).head(12).index.tolist()\n\nprint(\"[4/5] 正在建立近期歷史、最愛大類與 W2V 錨點...\")\nrecent_4w_train['index_group_name'] = recent_4w_train['article_id'].map(ARTICLE_TO_GROUP)\nUSER_HISTORY_LOCAL = recent_4w_train.groupby('customer_id')['article_id'].apply(lambda x: list(dict.fromkeys(x))[:12]).to_dict()\nUSER_FAV_GROUP_LOCAL = recent_4w_train.groupby('customer_id')['index_group_name'].apply(lambda x: x.mode()[0] if not x.empty else None).to_dict()\nUSER_LAST_ITEM = recent_4w_train.groupby('customer_id')['article_id'].first().to_dict()\n\nprint(\"[5/5] 正在建立傳統協同過濾與訓練 Word2Vec 模型...\")\ncf_train = train_df[train_df['t_dat'] >= (train_max_date - pd.Timedelta(days=14))].copy()\ncf_train = cf_train.sort_values(by=['customer_id', 't_dat'], ascending=[True, True])\ncf_train['next_article'] = cf_train.groupby('customer_id')['article_id'].shift(-1)\ncf_train = cf_train.dropna(subset=['next_article'])\npair_counts = cf_train.groupby(['article_id', 'next_article']).size().reset_index(name='count').sort_values(by=['article_id', 'count'], ascending=[True, False])\nCF_DICT_LOCAL = {}\nfor aid, group in pair_counts.groupby('article_id'):\n    CF_DICT_LOCAL[aid] = group['next_article'].head(3).tolist()\n\nrecent_3m_train = recent_3m_train.sort_values(by=['customer_id', 't_dat'])\nsentences = recent_3m_train.groupby('customer_id')['article_id'].apply(list).tolist()\nw2v_model = Word2Vec(sentences, vector_size=32, window=5, min_count=3, workers=4, epochs=5)\nW2V_DICT_LOCAL = {}\nfor item in set(w2v_model.wv.index_to_key):\n    W2V_DICT_LOCAL[item] = w2v_model.wv.most_similar(item, topn=3)\n\nprint(\"\\n第 3 區塊字典基礎擴充完成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-17T12:20:34.873068Z","iopub.execute_input":"2026-06-17T12:20:34.873808Z","iopub.status.idle":"2026-06-17T12:22:02.713916Z","shell.execute_reply.started":"2026-06-17T12:20:34.873778Z","shell.execute_reply":"2026-06-17T12:22:02.713191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==============================================================================\n# SECTION 4: 機器學習 (LGBM + CatBoost 異質融合 & 類別特徵)\n# ==============================================================================\nimport lightgbm as lgb\nfrom catboost import CatBoostClassifier # 引入 Kaggle 內建的 CatBoost\nimport pandas as pd\nimport numpy as np\n\nprint(\"[1/5] 正在建立 60 人候選名單，加入召回排名特徵...\")\ncandidates_list = []\nfor c_id in active_val_customers:\n    u_age_grp = CUST_AGE_DICT.get(c_id)\n    last_item = USER_LAST_ITEM.get(c_id)\n    \n    user_items = list(USER_HISTORY_LOCAL.get(c_id, []))\n    cf_items = []\n    w2v_items = []\n    w2v_scores = {}\n    \n    for item in user_items:\n        cf_items.extend(CF_DICT_LOCAL.get(item, []))\n    if last_item and last_item in W2V_DICT_LOCAL:\n        for sim_item, score in W2V_DICT_LOCAL[last_item]:\n            w2v_items.append(sim_item)\n            w2v_scores[sim_item] = score\n            \n    age_items = AGE_HOT_DECAY.get(u_age_grp, [])\n    full_list = list(dict.fromkeys(user_items + cf_items + w2v_items + age_items + TOP_12_DECAY))[:60]\n    \n    for a_id in full_list:\n        candidates_list.append({\n            'customer_id': c_id,\n            'article_id': a_id,\n            'is_in_history': 1 if a_id in user_items else 0,\n            'rank_in_history': user_items.index(a_id) if a_id in user_items else 999,\n            'rank_in_cf': cf_items.index(a_id) if a_id in cf_items else 999,\n            'rank_in_w2v': w2v_items.index(a_id) if a_id in w2v_items else 999,\n            'rank_in_age_hot': age_items.index(a_id) if a_id in age_items else 999,\n            'w2v_similarity': w2v_scores.get(a_id, 0.0)\n        })\n\nml_df = pd.DataFrame(candidates_list)\n\nprint(\"[2/5] 正在串聯所有特徵 (長短期切片、W2V、折扣、生命週期)...\")\nml_df['max_price'] = ml_df['article_id'].map(ITEM_MAX_PRICE).fillna(0)\nml_df['curr_price'] = ml_df['article_id'].map(ITEM_CURR_PRICE).fillna(ml_df['max_price'])\nml_df['item_discount'] = (ml_df['max_price'] - ml_df['curr_price']) / (ml_df['max_price'] + 1e-6)\nml_df['item_release_age'] = ml_df['article_id'].map(ITEM_RELEASE_AGE).fillna(0)\nml_df['item_last_sale_age'] = ml_df['article_id'].map(ITEM_LAST_SALE_AGE).fillna(999)\n\ntrain_max_date = train_df['t_dat'].max()\nrecent_7d_train = train_df[train_df['t_dat'] >= (train_max_date - pd.Timedelta(days=7))].copy()\nuser_item_stats_7d = recent_7d_train.groupby(['customer_id', 'article_id']).size().reset_index(name='user_item_count_7d')\n\nrecent_28d_train = train_df[train_df['t_dat'] >= (train_max_date - pd.Timedelta(days=28))].copy()\nrecent_28d_train['days_ago'] = (train_max_date - recent_28d_train['t_dat']).dt.days\nuser_item_stats_28d = recent_28d_train.groupby(['customer_id', 'article_id']).agg(\n    user_item_count_28d=('article_id', 'count'), min_days_ago=('days_ago', 'min')\n).reset_index()\n\nml_df = ml_df.merge(user_item_stats_7d, on=['customer_id', 'article_id'], how='left')\nml_df['user_item_count_7d'] = ml_df['user_item_count_7d'].fillna(0)\nml_df = ml_df.merge(user_item_stats_28d, on=['customer_id', 'article_id'], how='left')\nml_df['user_item_count_28d'] = ml_df['user_item_count_28d'].fillna(0)\nml_df['min_days_ago'] = ml_df['min_days_ago'].fillna(999)\n\nml_df = ml_df.merge(customers_df[['customer_id', 'age']], on='customer_id', how='left')\nml_df = ml_df.merge(articles_df[['article_id', 'product_type_no']], on='article_id', how='left')\nml_df['item_decay_pop'] = ml_df['article_id'].map(ITEM_DECAY_POP).fillna(0)\n\nml_df['item_group_name'] = ml_df['article_id'].map(ARTICLE_TO_GROUP)\nml_df['user_fav_group'] = ml_df['customer_id'].map(USER_FAV_GROUP_LOCAL)\nml_df['is_fav_group'] = (ml_df['item_group_name'] == ml_df['user_fav_group']).astype(int)\n\nml_df['article_mean_age'] = ml_df['article_id'].map(ARTICLE_MEAN_AGE).fillna(ml_df['age'].mean())\nml_df['age_diff'] = ml_df['age'] - ml_df['article_mean_age']\nml_df['age_ratio'] = ml_df['age'] / (ml_df['article_mean_age'] + 1e-6)\n\n# 標籤對齊\nval_pairs = set()\nfor c_id, a_sets in val_truth.items():\n    for a_id in a_sets:\n        val_pairs.add((c_id, a_id))\nml_df['label'] = ml_df.apply(lambda row: 1 if (row['customer_id'], row['article_id']) in val_pairs else 0, axis=1)\n\nprint(\"[3/5] 執行負樣本下採樣 1:20 與【類別特徵安全轉換】...\")\npos_df = ml_df[ml_df['label'] == 1]\nneg_df = ml_df[ml_df['label'] == 0]\nsample_size = int(len(pos_df) * 20)\nif sample_size < len(neg_df):\n    neg_df_sampled = neg_df.sample(n=sample_size, random_state=42)\nelse:\n    neg_df_sampled = neg_df\ntrain_df_sampled = pd.concat([pos_df, neg_df_sampled]).sample(frac=1.0, random_state=42)\n\n# 明確轉換類別特徵型態，防止模型誤判為連續數值\ncat_features_list = ['product_type_no', 'is_fav_group']\nfor col in cat_features_list:\n    train_df_sampled[col] = train_df_sampled[col].fillna(-1).astype(int)\n    ml_df[col] = ml_df[col].fillna(-1).astype(int)\n\nprint(\"[4/5] 啟動異質融合：LightGBM + CatBoost 雙渦輪引擎...\")\nfeatures = [\n    'age', 'product_type_no', 'item_decay_pop', \n    'is_in_history', 'user_item_count_7d', 'user_item_count_28d', 'min_days_ago', 'is_fav_group', \n    'rank_in_history', 'rank_in_cf', 'rank_in_age_hot', 'rank_in_w2v', 'w2v_similarity',\n    'item_discount', 'item_release_age', 'item_last_sale_age',\n    'age_diff', 'age_ratio'\n]\nX_train = train_df_sampled[features]\ny_train = train_df_sampled['label']\n\n# 1. 訓練 LightGBM (明確宣告 categorical_feature)\nprint(\"   ► 正在訓練 Model 1: LightGBM...\")\ntrain_dataset = lgb.Dataset(X_train, label=y_train, categorical_feature=cat_features_list)\nlgb_params = {\n    'objective': 'binary',\n    'metric': 'binary_logloss',\n    'boosting_type': 'gbdt',\n    'learning_rate': 0.05,\n    'num_leaves': 63,        \n    'max_depth': 6,\n    'verbose': -1,\n    'is_unbalance': True     \n}\nmodel_lgb = lgb.train(lgb_params, train_dataset, num_boost_round=130)\n\n# 2. 訓練類別演算法 CatBoost\nprint(\"   ► 正在訓練 Model 2: CatBoost...\")\nmodel_cb = CatBoostClassifier(\n    iterations=150, \n    learning_rate=0.05, \n    depth=6, \n    cat_features=cat_features_list, # CatBoost 原生支援類別特徵\n    verbose=0, \n    random_seed=42,\n    auto_class_weights='Balanced' # 自動處理正負樣本不平衡\n)\nmodel_cb.fit(X_train, y_train)\n\nprint(\"[5/5] 雙引擎預測完成！正在計算 Local 最終得分...\")\n# 推論並取平均 (0.6 權重給 LGBM，0.4 權重給 CB，因為 LGBM 歷史表現更穩)\nprob_lgb = model_lgb.predict(ml_df[features])\nprob_cb = model_cb.predict_proba(ml_df[features])[:, 1]\nml_df['pred_prob'] = (prob_lgb * 0.6) + (prob_cb * 0.4)\n\nml_df = ml_df.sort_values(by=['customer_id', 'pred_prob'], ascending=[True, False])\npredictions_dict = ml_df.groupby('customer_id', sort=False)['article_id'].apply(list).to_dict()\n\nfinal_scores = []\nfor c_id in active_val_customers:\n    actual = val_truth[c_id]\n    predicted = predictions_dict.get(c_id, [])[:12]\n    final_scores.append(apk(actual, predicted, k=12))\n\nprint(f\"\\n第 4 區塊【LGBM + CatBoost 異質融合版】完成！\")\nprint(f\"Local MAP@12 最終評分: {np.mean(final_scores):.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-17T12:40:40.471985Z","iopub.execute_input":"2026-06-17T12:40:40.472432Z","iopub.status.idle":"2026-06-17T12:41:24.365598Z","shell.execute_reply.started":"2026-06-17T12:40:40.472401Z","shell.execute_reply":"2026-06-17T12:41:24.364669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==============================================================================\n# SECTION 5: 全量預測與 Kaggle Submission (LGBM + CB)\n# ==============================================================================\nimport pandas as pd\nimport numpy as np\nimport warnings\nwarnings.filterwarnings(\"ignore\")\nfrom gensim.models import Word2Vec\n\nprint(\"[1/4] 正在基於【全量數據】重新建構所有特徵與商業邏輯字典...\")\nmax_date_full = transactions_df['t_dat'].max()\n\n# 1. 商業邏輯與價格字典 (全量)\nITEM_MAX_PRICE_FULL = transactions_df.groupby('article_id')['price'].max().to_dict()\nrecent_4w_full = transactions_df[transactions_df['t_dat'] >= (max_date_full - pd.Timedelta(weeks=4))].copy()\nrecent_4w_full['days_ago'] = (max_date_full - recent_4w_full['t_dat']).dt.days\nITEM_CURR_PRICE_FULL = recent_4w_full.groupby('article_id')['price'].mean().to_dict()\n\ntransactions_df['days_ago_total'] = (max_date_full - transactions_df['t_dat']).dt.days\nITEM_RELEASE_AGE_FULL = transactions_df.groupby('article_id')['days_ago_total'].max().to_dict()\nITEM_LAST_SALE_AGE_FULL = recent_4w_full.groupby('article_id')['days_ago'].min().to_dict()\n\n# 2. 時間衰減與年齡爆款 (全量)\nrecent_3m_full = transactions_df[transactions_df['t_dat'] >= (max_date_full - pd.Timedelta(days=90))].copy()\nrecent_3m_full['days_ago'] = (max_date_full - recent_3m_full['t_dat']).dt.days\nrecent_3m_full['decay_weight'] = 1.0 / np.sqrt(recent_3m_full['days_ago'] + 1)\nITEM_DECAY_POP_FULL = recent_3m_full.groupby('article_id')['decay_weight'].sum().to_dict()\n\nrecent_3m_full['age_group'] = recent_3m_full['customer_id'].map(CUST_AGE_DICT)\nAGE_HOT_DECAY_FULL = {}\nfor age_grp, a_df in recent_3m_full.groupby('age_group'):\n    AGE_HOT_DECAY_FULL[age_grp] = a_df.groupby('article_id')['decay_weight'].sum().sort_values(ascending=False).head(12).index.tolist()\nTOP_12_DECAY_FULL = recent_3m_full.groupby('article_id')['decay_weight'].sum().sort_values(ascending=False).head(12).index.tolist()\n\ndf_temp_age_full = recent_3m_full.merge(customers_df[['customer_id', 'age']], on='customer_id', how='left')\nARTICLE_MEAN_AGE_FULL = df_temp_age_full.groupby('article_id')['age'].mean().to_dict()\n\n# 3. 歷史行為、最愛大類與 W2V 錨點 (全量)\nrecent_4w_full = recent_4w_full.sort_values(by='t_dat', ascending=False)\nrecent_4w_full['index_group_name'] = recent_4w_full['article_id'].map(ARTICLE_TO_GROUP)\n\nUSER_HISTORY_FULL = recent_4w_full.groupby('customer_id')['article_id'].apply(lambda x: list(dict.fromkeys(x))[:12]).to_dict()\nUSER_FAV_GROUP_FULL = recent_4w_full.groupby('customer_id')['index_group_name'].apply(lambda x: x.mode()[0] if not x.empty else None).to_dict()\nUSER_LAST_ITEM_FULL = recent_4w_full.groupby('customer_id')['article_id'].first().to_dict()\n\n# 4. 協同過濾 (全量)\ncf_full = transactions_df[transactions_df['t_dat'] >= (max_date_full - pd.Timedelta(days=14))].copy()\ncf_full = cf_full.sort_values(by=['customer_id', 't_dat'], ascending=[True, True])\ncf_full['next_article'] = cf_full.groupby('customer_id')['article_id'].shift(-1)\ncf_full = cf_full.dropna(subset=['next_article'])\npair_counts_full = cf_full.groupby(['article_id', 'next_article']).size().reset_index(name='count').sort_values(by=['article_id', 'count'], ascending=[True, False])\nCF_DICT_FULL = {}\nfor aid, group in pair_counts_full.groupby('article_id'):\n    CF_DICT_FULL[aid] = group['next_article'].head(3).tolist()\n\n# 5. 用戶商品互動特徵 (全量)\nrecent_7d_full = transactions_df[transactions_df['t_dat'] >= (max_date_full - pd.Timedelta(days=7))].copy()\nuser_item_stats_7d_full = recent_7d_full.groupby(['customer_id', 'article_id']).size().reset_index(name='user_item_count_7d')\n\nuser_item_stats_28d_full = recent_4w_full.groupby(['customer_id', 'article_id']).agg(\n    user_item_count_28d=('article_id', 'count'),\n    min_days_ago=('days_ago', 'min')\n).reset_index()\n\nprint(\"[2/4] 正在訓練【全量 Word2Vec 模型】挖掘最新商品語義...\")\nrecent_3m_full = recent_3m_full.sort_values(by=['customer_id', 't_dat'])\nsentences_full = recent_3m_full.groupby('customer_id')['article_id'].apply(list).tolist()\nw2v_model_full = Word2Vec(sentences_full, vector_size=32, window=5, min_count=3, workers=4, epochs=5)\n\nW2V_DICT_FULL = {}\nvalid_vocab_full = set(w2v_model_full.wv.index_to_key)\nfor item in valid_vocab_full:\n    W2V_DICT_FULL[item] = w2v_model_full.wv.most_similar(item, topn=3)\n\n\nprint(\"[3/4] 載入 Submission 模板，啟動 LGBM+CB 雙引擎推理...\")\nsubmission = pd.read_csv(BASE_PATH + 'sample_submission.csv')\nall_customers = submission['customer_id'].values\n\nBATCH_SIZE = 100000 \ntotal_customers = len(all_customers)\nfinal_preds_dict = {}\n\ncat_features_list = ['product_type_no', 'is_fav_group']\n\nfor i in range(0, total_customers, BATCH_SIZE):\n    batch_cust = all_customers[i:i+BATCH_SIZE]\n    print(f\"   ► 正在精準預測批次: {i} ~ {min(i+BATCH_SIZE, total_customers)} / {total_customers}\")\n    \n    batch_candidates = []\n    for c_id in batch_cust:\n        u_age_grp = CUST_AGE_DICT.get(c_id)\n        last_item = USER_LAST_ITEM_FULL.get(c_id)\n        \n        user_items = list(USER_HISTORY_FULL.get(c_id, []))\n        cf_items = []\n        w2v_items = []\n        w2v_scores = {}\n        \n        for item in user_items:\n            cf_items.extend(CF_DICT_FULL.get(item, []))\n        if last_item and last_item in W2V_DICT_FULL:\n            for sim_item, score in W2V_DICT_FULL[last_item]:\n                w2v_items.append(sim_item)\n                w2v_scores[sim_item] = score\n                \n        age_items = AGE_HOT_DECAY_FULL.get(u_age_grp, [])\n        full_list = list(dict.fromkeys(user_items + cf_items + w2v_items + age_items + TOP_12_DECAY_FULL))[:60]\n        \n        for a_id in full_list:\n            batch_candidates.append({\n                'customer_id': c_id,\n                'article_id': a_id,\n                'is_in_history': 1 if a_id in user_items else 0,\n                'rank_in_history': user_items.index(a_id) if a_id in user_items else 999,\n                'rank_in_cf': cf_items.index(a_id) if a_id in cf_items else 999,\n                'rank_in_age_hot': age_items.index(a_id) if a_id in age_items else 999,\n                'rank_in_w2v': w2v_items.index(a_id) if a_id in w2v_items else 999,\n                'w2v_similarity': w2v_scores.get(a_id, 0.0)\n            })\n            \n    batch_df = pd.DataFrame(batch_candidates)\n    if batch_df.empty:\n        continue\n        \n    # 商業特徵對齊\n    batch_df['max_price'] = batch_df['article_id'].map(ITEM_MAX_PRICE_FULL).fillna(0)\n    batch_df['curr_price'] = batch_df['article_id'].map(ITEM_CURR_PRICE_FULL).fillna(batch_df['max_price'])\n    batch_df['item_discount'] = (batch_df['max_price'] - batch_df['curr_price']) / (batch_df['max_price'] + 1e-6)\n    batch_df['item_release_age'] = batch_df['article_id'].map(ITEM_RELEASE_AGE_FULL).fillna(0)\n    batch_df['item_last_sale_age'] = batch_df['article_id'].map(ITEM_LAST_SALE_AGE_FULL).fillna(999)\n    \n    # 時間切片特徵對齊\n    batch_df = batch_df.merge(user_item_stats_7d_full, on=['customer_id', 'article_id'], how='left')\n    batch_df['user_item_count_7d'] = batch_df['user_item_count_7d'].fillna(0)\n    batch_df = batch_df.merge(user_item_stats_28d_full, on=['customer_id', 'article_id'], how='left')\n    batch_df['user_item_count_28d'] = batch_df['user_item_count_28d'].fillna(0)\n    batch_df['min_days_ago'] = batch_df['min_days_ago'].fillna(999)\n    \n    # 基礎特徵對齊\n    batch_df = batch_df.merge(customers_df[['customer_id', 'age']], on='customer_id', how='left')\n    batch_df = batch_df.merge(articles_df[['article_id', 'product_type_no']], on='article_id', how='left')\n    batch_df['item_decay_pop'] = batch_df['article_id'].map(ITEM_DECAY_POP_FULL).fillna(0)\n    \n    batch_df['item_group_name'] = batch_df['article_id'].map(ARTICLE_TO_GROUP)\n    batch_df['user_fav_group'] = batch_df['customer_id'].map(USER_FAV_GROUP_FULL)\n    batch_df['is_fav_group'] = (batch_df['item_group_name'] == batch_df['user_fav_group']).astype(int)\n    batch_df['article_mean_age'] = batch_df['article_id'].map(ARTICLE_MEAN_AGE_FULL).fillna(batch_df['age'].mean())\n    batch_df['age_diff'] = batch_df['age'] - batch_df['article_mean_age']\n    batch_df['age_ratio'] = batch_df['age'] / (batch_df['article_mean_age'] + 1e-6)\n    \n    #  【對齊類別特徵安全轉換】(給 CatBoost 用的)\n    for col in cat_features_list:\n        batch_df[col] = batch_df[col].fillna(-1).astype(int)\n    \n    features = [\n        'age', 'product_type_no', 'item_decay_pop', \n        'is_in_history', 'user_item_count_7d', 'user_item_count_28d', 'min_days_ago', 'is_fav_group', \n        'rank_in_history', 'rank_in_cf', 'rank_in_age_hot', 'rank_in_w2v', 'w2v_similarity',\n        'item_discount', 'item_release_age', 'item_last_sale_age',\n        'age_diff', 'age_ratio'\n    ]\n    \n    #  【雙引擎融合推理】(60% LGBM + 40% CatBoost)\n    prob_lgb = model_lgb.predict(batch_df[features])\n    prob_cb = model_cb.predict_proba(batch_df[features])[:, 1]\n    batch_df['pred_prob'] = (prob_lgb * 0.6) + (prob_cb * 0.4)\n    \n    batch_df = batch_df.sort_values(by=['customer_id', 'pred_prob'], ascending=[True, False])\n    preds = batch_df.groupby('customer_id', sort=False)['article_id'].apply(lambda x: \" \".join(list(x)[:12])).to_dict()\n    final_preds_dict.update(preds)\n\n\nprint(\"[4/4] 正在組裝最終 Submission 預測矩陣...\")\nsubmission['prediction'] = submission['customer_id'].map(final_preds_dict)\nsubmission['prediction'] = submission['prediction'].fillna(\" \".join(TOP_12_DECAY_FULL))\nsubmission.to_csv('/kaggle/working/submission.csv', index=False)\n\nprint(\"\\n第 5 區塊執行完成，異質雙引擎 (LGBM+CB) 預測檔案已生成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-17T12:41:31.373407Z","iopub.execute_input":"2026-06-17T12:41:31.373971Z","iopub.status.idle":"2026-06-17T12:47:53.735148Z","shell.execute_reply.started":"2026-06-17T12:41:31.373946Z","shell.execute_reply":"2026-06-17T12:47:53.734151Z"}},"outputs":[],"execution_count":null}]}