測全流程:特征工程與LightGBM調(diào)參實戰(zhàn))
簡介本資源源自京東JData算法大賽高潛用戶購買意向預(yù)測賽題是一套完整可直接復(fù)跑的機(jī)器學(xué)習(xí)項目。內(nèi)容覆蓋數(shù)據(jù)預(yù)處理、正負(fù)樣本組合、特征生成與篩選、模型訓(xùn)練與驗證等核心環(huán)節(jié)代碼含Python腳本及編譯緩存文件配套項目說明文檔與運(yùn)行教程另有歷史代碼備份包。資源共24個文件以py源碼、pyc緩存、markdown說明及txt使用指南為主壓縮包僅約92KB結(jié)構(gòu)清晰適合人工智能、數(shù)據(jù)科學(xué)等相關(guān)專業(yè)的畢設(shè)、課設(shè)、競賽復(fù)現(xiàn)與入門學(xué)習(xí)。目前已有176人學(xué)習(xí)下載項目中涉及的特征工程與梯度提升樹建模思路對理解電商用戶購買意向預(yù)測的完整流程具有直接參考價值下載后即可對照說明快速運(yùn)行和二次開發(fā)。1. 高潛用戶購買意向預(yù)測為什么這個賽題值得一行行拆著學(xué)在電商營銷和數(shù)據(jù)挖掘的場景里“京東JData算法大賽-高潛用戶購買意向預(yù)測”是一道特別典型的成交建模題給你一批用戶過去一段時間的瀏覽、加購、關(guān)注、下單行為讓你判斷某個用戶對指定商品接下來的購買可能性。它和點擊率預(yù)估最大的區(qū)別是樣本稀疏、正負(fù)樣本懸殊但業(yè)務(wù)價值又比點擊率直接得多做起來很考驗特征功底。這篇文章把完整路線拆開講從字段理解、評估指標(biāo)到特征工程、模型訓(xùn)練和調(diào)參再到最容易翻車的地方。源碼包里的項目說明和資料我會按做這類賽題最常見的實現(xiàn)方式把邏輯補(bǔ)齊你拿到手后可以直接對著改不需要重新推一遍流程。適合兩類人一類是想拿真實比賽練手、把“用戶購買意向預(yù)測”做成完整項目經(jīng)歷的數(shù)據(jù)新人另一類是在做電商轉(zhuǎn)化預(yù)測、總被離線在線指標(biāo)不一致折磨的從業(yè)者。不預(yù)設(shè)你懂很多但從基線到調(diào)優(yōu)每一步都保證能照著復(fù)現(xiàn)。2. 預(yù)測目標(biāo)與數(shù)據(jù)邊界字段結(jié)構(gòu)、評估指標(biāo)和第一個規(guī)則基線做這類賽題最忌諱一上來就訓(xùn)練模型。先把“預(yù)測誰、拿什么預(yù)測、怎么算分”三件事釘死后面的特征和模型才談得上意義。JData 這類比賽的數(shù)據(jù)通常圍繞三個維度組織用戶信息、商品信息、用戶行為日志。用戶信息里常見的是年齡、性別、城市級別這類靜態(tài)屬性商品信息里常見的是商品編號、類別、品牌、價格行為日志則是重頭戲記錄每個用戶對每個商品在什么時間做了什么操作行為類型基本覆蓋瀏覽、加購、關(guān)注、下單幾種。時間跨度上行為日志一般覆蓋三十天左右預(yù)測目標(biāo)落在日志末尾之后比如預(yù)測未來三到五天內(nèi)用戶是否會產(chǎn)生購買。讀數(shù)據(jù)時有一個關(guān)鍵點不能馬虎訓(xùn)練集和測試集的時間邊界。賽題經(jīng)常在這里埋坑比如訓(xùn)練集行為截止日期和測試集不一致或者測試集里混入訓(xùn)練集時間之后才出現(xiàn)的用戶。拿到資料后第一步不是跑模型而是把每張表的日期范圍打出來標(biāo)清楚邊界再繼續(xù)。2.1 字段梳理與數(shù)據(jù)質(zhì)檢先解決臟數(shù)據(jù)再談特征我不建議直接讀完整數(shù)據(jù)。更穩(wěn)妥的做法是寫一個輕量概覽腳本確認(rèn)每張表的主鍵、時間范圍和缺失情況同時把訓(xùn)練集和測試集的結(jié)構(gòu)對齊。這套動作看著基礎(chǔ)但能省掉后面大量排查時間屬于典型的“磨刀不誤砍柴工”。import pandas as pd # 讀入三張原始表路徑按你解壓后的實際目錄修改 users pd.read_csv(data/user_info.csv) items pd.read_csv(data/item_info.csv) actions pd.read_csv(data/user_action.csv, parse_dates[action_time]) # 查看表結(jié)構(gòu)和缺失情況 for df, name in [(users, users), (items, items), (actions, actions)]: print(f {name} ) print(shape:, df.shape) print(缺失情況:) print(df.isnull().sum()) print(字段類型:) print(df.dtypes) # 行為日志的時間范圍這個必須確認(rèn) print(行為日志時間范圍, actions[action_time].min(), -, actions[action_time].max()) # 行為類型取值分布 print(行為類型分布) print(actions[action_type].value_counts())這段代碼沒有任何建模邏輯但它是整個項目的地基。字段類型和時間范圍常常比想象中更坑時間格式不統(tǒng)一、行為類型用數(shù)字編碼、缺失值分布在訓(xùn)練集和測試集之間不一致這些都是常見狀況。確認(rèn)清楚之后再做特征拼接才不會在后續(xù)步驟里報錯或產(chǎn)生莫名其妙的空值。參數(shù)說明parse_dates直接把時間列轉(zhuǎn)成 datetime 類型后續(xù)做時間窗口計算才方便value_counts用于確認(rèn)行為類型有幾種、占比如何。如果發(fā)現(xiàn)訓(xùn)練集有五種行為類型而測試集只有四種后續(xù)做特征時就要按并集處理不是按交集。2.2 評估指標(biāo)AUC 還是 F1決定了你怎么調(diào)閾值這類預(yù)測賽題常用兩類指標(biāo)一類是 AUC 這類排序指標(biāo)只看模型給正樣本打的分是否普遍高于負(fù)樣本另一類是 F1 這類閾值指標(biāo)要在某個分?jǐn)?shù)之上才算正樣本。兩者的差別非常大AUC 高不代表 F1 高因為 F1 還受正負(fù)樣本比例和閾值選擇的影響。在高潛購買意向預(yù)測中正樣本通常只占整體樣本的百分之幾直接算 F1 會很難看必須配合閾值搜索。我一般會先以 AUC 為主優(yōu)化模型的排序能力最后單獨(dú)做一輪閾值搜索把 F1 或業(yè)務(wù)上更關(guān)心的精確率拉起來。這里要提醒一句如果你拿到的資料里官方指標(biāo)是 F1那么訓(xùn)練目標(biāo)就不要設(shè)成分類正確率而是先把模型分?jǐn)?shù)輸出再在驗證集上搜最優(yōu)閾值這兩條路徑最終分?jǐn)?shù)差距可能超過十個點。2.3 第一個基線用“加購未購”做規(guī)則排序很多新手上來就上 LightGBM但第一個基線用規(guī)則更好原因有兩個一是能快速驗證數(shù)據(jù)是否存在泄露或時間錯位二是規(guī)則分?jǐn)?shù)可以當(dāng)作后續(xù)模型的強(qiáng)特征。我通常會先構(gòu)造一個最簡單的預(yù)測邏輯如果用戶在窗口里對某商品加購過但沒有下單就給這個組合一個很高的分?jǐn)?shù)。import numpy as np # actions: user_id, item_id, action_type, action_time # 先把行為表轉(zhuǎn)成用戶-商品對的行為集合 act actions.groupby([user_id, item_id])[action_type].apply(set).reset_index() # 規(guī)則打分加購未購是高意向只瀏覽是低意向 def rule_score(s): if buy in s: # 已購買過存量用戶短期再購概率低 return 0.3 if cart in s: # 加購未購意向最強(qiáng) return 0.9 if fav in s: # 關(guān)注過次強(qiáng) return 0.6 if view in s: # 只瀏覽過弱意向 return 0.4 return 0.0 act[rule_score] act[action_type].apply(rule_score) # 與測試集候選對做左連接得到每個候選的基線分?jǐn)?shù) test pd.read_csv(data/test_candidates.csv) test test.merge(act[[user_id, item_id, rule_score]], on[user_id, item_id], howleft) test[rule_score] test[rule_score].fillna(0.0) # 按分?jǐn)?shù)排序輸出 test.sort_values(rule_score, ascendingFalse).to_csv(baseline_rule.csv, indexFalse)邏輯說明這個基線把行為類型按購買意圖強(qiáng)弱排序加購未購分最高關(guān)注次之只看過的最弱。它不含任何機(jī)器學(xué)習(xí)但能很快暴露兩個問題一是如果加購未購在測試集里大量存在說明意向信號充足模型應(yīng)該重點利用二是如果連這種規(guī)則基線的線下 AUC 都很低說明候選集構(gòu)成可能和訓(xùn)練集差異很大需要先排查數(shù)據(jù)配比。參數(shù)說明rule_score的取值不是硬性的把加購未購調(diào)到 0.95、關(guān)注調(diào)到 0.7不影響排序指標(biāo)但會影響閾值搜索。規(guī)則分?jǐn)?shù)的主要作用是給后面的模型做參照驗證我們構(gòu)造的特征是否超過了這個“常識線”。3. 特征工程把三十天行為日志壓縮成三類特征寬表特征工程是這個賽題真正的重頭戲。行為日志是典型的稀疏長表特征是“壓”出來的不是在原始表上直接學(xué)習(xí)。我習(xí)慣從三個方向分別壓用戶側(cè)、商品側(cè)、用戶-商品交叉?zhèn)茸詈笃闯梢粡垖挶?。主線思路是所有能在業(yè)務(wù)上解釋購買動機(jī)的行為都應(yīng)該被數(shù)字特征顯式表達(dá)而不是讓模型自己從原始日志里找規(guī)律。窗口長度是第一個要決定的事。窗口太短統(tǒng)計不穩(wěn)定窗口太長容易混入過期信息。三十天數(shù)據(jù)我一般同時保留 7 天、15 天和全量 30 天三個窗口這樣模型既能捕捉近期意圖的突變又能保留長期偏好。3.1 時間窗口統(tǒng)計特征不同窗口長度為什么都要留下面這個函數(shù)按窗口天數(shù)做聚合對瀏覽、加購、關(guān)注、購買分別計數(shù)同時把行為天數(shù)、最近一次行為間隔等時間結(jié)構(gòu)也記錄下來。生成三份不同窗口的特征再橫向拼接讓模型自己判斷哪個時間尺度更有效。def window_features(actions, window_days, date_ref): 對截止日期 date_ref 往前 window_days 天內(nèi)的行為做統(tǒng)計 start date_ref - pd.Timedelta(dayswindow_days) mask (actions[action_time] start) (actions[action_time] date_ref) sub actions[mask] feat sub.groupby([user_id, item_id]).agg( view_cnt(action_type, lambda x: (x view).sum()), cart_cnt(action_type, lambda x: (x cart).sum()), fav_cnt(action_type, lambda x: (x fav).sum()), buy_cnt(action_type, lambda x: (x buy).sum()), action_days(action_time, lambda x: x.dt.date.nunique()), last_action_gap(action_time, lambda x: (date_ref - x.max()).days) ).reset_index() feat[action_total] feat[[view_cnt, cart_cnt, fav_cnt, buy_cnt]].sum(axis1) feat[interact_diversity] (feat[[view_cnt, cart_cnt, fav_cnt, buy_cnt]] 0).sum(axis1) feat.columns [user_id, item_id] [fw{window_days}_ c for c in feat.columns[2:]] return feat # 以資料中給定的預(yù)測截止日為例換成你的實際數(shù)據(jù)時間 date_ref pd.Timestamp(2023-05-15) feat_7 window_features(actions, 7, date_ref) feat_15 window_features(actions, 15, date_ref) feat_30 window_features(actions, 30, date_ref) # 合并成寬表 wide feat_7.merge(feat_15, on[user_id, item_id], howouter) wide wide.merge(feat_30, on[user_id, item_id], howouter) print(wide.shape)邏輯說明這個函數(shù)把行為日志按用戶-商品對壓縮成統(tǒng)計量每個窗口都生成瀏覽數(shù)、加購數(shù)、關(guān)注數(shù)、購買數(shù)、行為天數(shù)、最后一次行為距截止日的間隔、行為總量和交互多樣性。其中l(wèi)ast_action_gap特別有用用戶三天前還在反復(fù)看某商品說明注意力還在最后一次行為是二十天前意向大概率已經(jīng)涼了。interact_diversity用來區(qū)分“被動刷到一次”和“多行為類型反復(fù)觸發(fā)”后者的購買意愿明顯更強(qiáng)。參數(shù)說明窗口天數(shù)必須和賽題時間邊界對齊。30 天數(shù)據(jù)里只做 7 天窗口特征會偏稀疏但短期意向信號更強(qiáng)烈。三個窗口都保留而不是只用一個是為了讓模型自己決定在什么時間尺度上做判斷后續(xù)用 feature importance 排查也無從下手。3.2 用戶-商品交叉特征加購未購和瀏覽轉(zhuǎn)加購率交叉特征把用戶、商品兩側(cè)的信息放在同一個對里計算目的是捕捉關(guān)系型信號。最有代表性的兩個特征是“加購未購”和“瀏覽轉(zhuǎn)加購率”。加購未購的含義很直白用戶把商品放進(jìn)購物車但始終沒有下單放到真實業(yè)務(wù)里就是強(qiáng)烈購買意圖但還在猶豫模型拿到這個信號后可以直接把分?jǐn)?shù)拉高。瀏覽轉(zhuǎn)加購率則區(qū)分深度瀏覽和淺層瀏覽看五次才加購和看一次就加購意圖強(qiáng)度完全不同。# 接上一節(jié) wide 表 wide[cart_not_buy] (wide[w30_cart_cnt] 0) (wide[w30_buy_cnt] 0) wide[view_to_cart_rate] wide[w30_cart_cnt] / (wide[w30_view_cnt] 1) # 距離上次加購的時間間隔加購動作離預(yù)測截止日越近意向越新鮮 last_cart_time actions[actions[action_type] cart].groupby([user_id, item_id])[action_time].max() last_cart_gap (date_ref - last_cart_time).dt.days.rename(last_cart_gap) wide wide.merge(last_cart_gap, on[user_id, item_id], howleft) wide[last_cart_gap] wide[last_cart_gap].fillna(999) # 粗略檢查新特征的分布 print(wide[[cart_not_buy, view_to_cart_rate, last_cart_gap]].describe())邏輯說明cart_not_buy直接表達(dá)“加購未購”這個業(yè)務(wù)強(qiáng)信號在成交預(yù)測里屬于少數(shù)高區(qū)分度特征。view_to_cart_rate分母加 1 是為了防止除零同時也讓只有瀏覽沒有加購的樣本得到一個接近 0 的小值而不是 NaN。last_cart_gap用 999 填充空值是為了讓模型能區(qū)分“從未加購”和“加購了很久都沒買”兩類樣本避免缺失值在樹模型中被合并到錯誤的分支。參數(shù)說明999 這個填充值必須遠(yuǎn)大于正常間隔正常加購間隔最多 30 天999 能把缺失值和最大間隔徹底拉開。如果你用 LightGBM缺失值本身可以保留為 NaN但填一個大值在特征重要性和排查階段更直觀不容易讓人誤以為這個字段沒參與分裂。3.3 商品維度特征熱度、價格帶與品牌集中度只從用戶側(cè)做特征模型會缺少對商品本身的理解。同一個用戶對不同商品的行為差異很大但加入商品屬性后模型就能把這種差異解釋清楚。商品側(cè)常用的特征有三類熱度、價格帶、品牌集中度。熱度可以用商品在全部用戶中的瀏覽次數(shù)排名來表示價格帶把商品價格分桶不同價格帶對應(yīng)完全不同的決策周期品牌集中度衡量銷售是否高度集中于爆款集中度高時非爆款單品被直接購買的概率預(yù)期要保守。# 商品側(cè)特征 item_feat actions.groupby(item_id).agg( item_view_cnt(action_type, lambda x: (x view).sum()), item_cart_cnt(action_type, lambda x: (x cart).sum()), item_buy_cnt(action_type, lambda x: (x buy).sum()), item_user_cnt(user_id, nunique) ).reset_index() # 熱度分桶用 rank 而不是直接歸一化減少長尾影響 item_feat[item_hot_rank] item_feat[item_view_cnt].rank(pctTrue) # 和商品信息表合并構(gòu)造價格帶 item_feat item_feat.merge(items[[item_id, price, brand_id, cate_id]], onitem_id, howleft) item_feat[price_level] pd.cut(item_feat[price], bins[0, 100, 500, 1000, np.inf], labels[0, 1, 2, 3]) # 品牌購買集中度品牌下 top1 商品購買量占品牌總購買量比例 brand_buy actions[actions[action_type] buy].merge(items[[item_id, brand_id]], onitem_id, howleft) brand_total brand_buy.groupby(brand_id)[item_id].count().rename(brand_buy_total) brand_top1_share brand_buy.groupby([brand_id, item_id])[action_type].count().rename(item_buy_in_brand) brand_top1_share brand_top1_share.groupby(brand_id).max() / brand_total # 簡化版 item_feat item_feat.merge(brand_total, onbrand_id, howleft) item_feat item_feat.merge(brand_top1_share, onbrand_id, howleft) print(item_feat.head())邏輯說明熱度用 rank 而不是原始瀏覽數(shù)是因為熱門商品的瀏覽量可能是冷門商品的上千倍直接歸一化會讓大部分商品被壓到接近 0。價格帶把連續(xù)價格離散化讓樹模型能清晰區(qū)分不同決策周期的品類。品牌集中度的計算里先統(tǒng)計每個品牌下每個商品的購買次數(shù)再用 top1 商品購買量除以品牌總購買量集中度高時非爆款商品的購買意向預(yù)測要更保守。參數(shù)說明價格分桶邊界不是固定的可以按數(shù)據(jù)分布調(diào)整。如果發(fā)現(xiàn)大部分商品價格集中在 0-100就要把第一檔再切細(xì)成 0-50、50-100。pd.cut的 labels 參數(shù)也不一定非用 0-3樹模型可以吃任意有順序關(guān)系的數(shù)值關(guān)鍵是分桶邊界要貼合商品真實價格分布。4. 建模與調(diào)參把 LightGBM 穩(wěn)定地用在高潛購買預(yù)測上特征寬表構(gòu)造完成之后建模本身反而直接。我的習(xí)慣是先跑一個默認(rèn)參數(shù)的 LightGBM看驗證集 AUC 和特征重要性再按順序調(diào)參。不要一開始就追求 SOTA先把流程完整跑通才有資格談優(yōu)化。4.1 模型選型表格數(shù)據(jù)上為什么優(yōu)先用 GBDT特征工程完成后樣本通常是幾千到幾十萬行的寬表特征是數(shù)值型和少量類別型的混合。這種結(jié)構(gòu)下 GBDT 類模型幾乎總是比深度模型更容易拿到更好的 AUC。原因是表格數(shù)據(jù)里的特征交互可以靠樹的天然分裂機(jī)制表達(dá)不需要像神經(jīng)網(wǎng)絡(luò)那樣手動設(shè)計 embedding 結(jié)構(gòu)而且 GBDT 對特征尺度不敏感數(shù)值型特征直接丟進(jìn)去就好。在幾個 GBDT 實現(xiàn)里我通常先用 LightGBM而不是默認(rèn) XGBoost。LightGBM 訓(xùn)練速度快在這個量級能快速迭代特征它對類別特征有原生支持省掉大量手動編碼工作直方圖算法對內(nèi)存也更友好。這不代表 XGBoost 不好只是起步效率上 LightGBM 更適合這種要頻繁試特征的賽題。4.2 樣本不均衡別急著對負(fù)樣本做極端下采樣購買意向預(yù)測里正負(fù)樣本比例可能達(dá)到 1:20 甚至更懸殊。遇到這種情況很多新手第一反應(yīng)是把負(fù)樣本砍到和正樣本一樣多這是過度反應(yīng)。GBDT 對不均衡的容忍度比想象中高直接使用全部樣本、只調(diào)整評估方式和閾值往往就能取得不錯的效果。如果正樣本實在太少我才會考慮兩種做法一是負(fù)樣本按比例下采樣比如保留負(fù)樣本的 20%-50%同時讓驗證集保持原始比例二是在 LightGBM 里設(shè)置scale_pos_weight本質(zhì)上給正樣本的梯度乘以權(quán)重。兩者相比我更愿意先調(diào)scale_pos_weight而不是采掉大量負(fù)樣本畢竟保留數(shù)據(jù)就是保留信息。4.3 三個關(guān)鍵參數(shù)n_estimators、learning_rate、max_depth下面是最小可用的訓(xùn)練與驗證流程。先固定 learning_rate 從 0.05 開始用 early stopping 決定樹的數(shù)量之后再去動樹的復(fù)雜度。import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # X 是前面拼好的寬表特征y 是標(biāo)簽1 表示成交 X wide.drop(columns[user_id, item_id, label]) y wide[label] # 按用戶切分避免同一用戶的數(shù)據(jù)同時出現(xiàn)在訓(xùn)練和驗證里 X_tr, X_va, y_tr, y_va train_test_split(X, y, test_size0.2, random_state42) params { objective: binary, metric: auc, learning_rate: 0.05, max_depth: 6, num_leaves: 31, min_child_samples: 20, scale_pos_weight: (len(y_tr) - y_tr.sum()) / y_tr.sum(), verbosity: -1, seed: 42 } model lgb.train( params, lgb.Dataset(X_tr, y_tr), num_boost_round1000, valid_sets[lgb.Dataset(X_va, y_va)], callbacks[lgb.early_stopping(100), lgb.log_evaluation(50)] ) pred model.predict(X_va) print(驗證集 AUC:, roc_auc_score(y_va, pred))邏輯說明按用戶而不是按行切分驗證集是一個關(guān)鍵決策。購買行為在同一用戶內(nèi)部高度相關(guān)如果同一用戶的一部分行為出現(xiàn)在訓(xùn)練集、另一部分出現(xiàn)在驗證集指標(biāo)會被嚴(yán)重高估。scale_pos_weight按訓(xùn)練集實際正負(fù)比例計算比寫死一個數(shù)更穩(wěn)。early_stopping在驗證集 AUC 連續(xù) 100 輪不提升時停止既防止過擬合也不需要手動糾結(jié)樹的數(shù)量。參數(shù)說明常用體積明細(xì)如下表所示先固根基再動結(jié)構(gòu)。參數(shù)作用建議起點learning_rate控制每棵樹貢獻(xiàn)的步長越小越穩(wěn)越慢0.05max_depth限制樹高防止單棵過深6num_leaves葉子數(shù)上限復(fù)雜度控制核心31min_child_samples葉子節(jié)點最小樣本數(shù)抑制噪聲分裂20scale_pos_weight正樣本權(quán)重緩解不平衡訓(xùn)練集負(fù)/正比例調(diào)參順序建議先固定 learning_rate把num_leaves從 31 調(diào)到 63、127 看驗證集變化再回來把max_depth調(diào)成 8 或 -1最后觀察特征重要性砍掉重要性接近 0 的列重新訓(xùn)練。這套順序能讓調(diào)整可解釋而不是同時動五六個參數(shù)最后連自己都不知道哪一項起了作用。5. 高潛購買意向預(yù)測的 5 個常見坑與排查思路這類賽題做幾輪之后真正決定水平的是避坑能力。以下五個坑按出現(xiàn)頻率排每條都按現(xiàn)象、原因、解決三段式寫清楚。5.1 坑驗證集 AUC 極高提交后分?jǐn)?shù)暴跌現(xiàn)象本地驗證集 AUC 0.85 以上看起來很漂亮一換成測試集就掉到 0.7 附近。原因最常見的是數(shù)據(jù)泄露。行為日志里混進(jìn)了預(yù)測日期之后的數(shù)據(jù)或者某些靜態(tài)字段本身就攜帶未來信息。另一種常見泄露是特征拼接時不小心把用戶-商品對在測試期的行為也算了進(jìn)去。解決做一個固定動作把訓(xùn)練集和測試集的日期邊界重新打印一遍再檢查所有特征的取值分布。具體做法是在建模前把一個時間截止日釘死所有特征只用截止日之前的數(shù)據(jù)如果某個特征使用全量數(shù)據(jù)聚合就要追問它有沒有引用未來的行為。5.2 坑同一用戶既在訓(xùn)練集又在驗證集指標(biāo)虛高現(xiàn)象線下 AUC 穩(wěn)定線上結(jié)果不如預(yù)期回頭檢查發(fā)現(xiàn)切分是按行隨機(jī)切而不是按用戶切。原因同一個用戶的多條行為天然相似隨機(jī)按行切分等于讓模型在訓(xùn)練時見過驗證集用戶的習(xí)慣評估結(jié)果自然虛高。解決使用train_test_split時先按 user_id 分組或者直接用 GroupKFold 做按用戶的交叉驗證。電商場景里我還會額外加按時間切分用前 25 天行為構(gòu)造特征預(yù)測后 5 天而不是在整個時間區(qū)間里隨機(jī)抽樣。5.3 坑特征分布訓(xùn)練和測試不一致現(xiàn)象特征重要性里某個特征排第一但它只在訓(xùn)練集里表現(xiàn)好在測試集里幾乎全為空或分布完全改變。原因常見于價格、庫存、促銷活動相關(guān)字段訓(xùn)練期和測試期對應(yīng)的商品池或時間點不同導(dǎo)致該特征在兩側(cè)覆蓋度不一致。解決特征構(gòu)造完成后做一個分布對比腳本逐列輸出訓(xùn)練和測試的缺失率、均值、分位數(shù)。如果某個特征在測試集缺失率超過 30%要么重新構(gòu)造只用覆蓋率高的字段要么把缺失率本身做成特征讓模型學(xué)會在信息缺失時如何決策。5.4 坑AUC 尚可但 F1 上不去現(xiàn)象AUC 0.72 看著尚可但按閾值搜出來 F1 只有 0.15業(yè)務(wù)完全沒法用。原因正樣本比例太低AUC 主要靠大量負(fù)樣本的低分拉高模型對正樣本的召回嚴(yán)重不足。解決先不要調(diào)模型結(jié)構(gòu)而是做兩件事。一是檢查正樣本里“加購未購”的覆蓋率如果正樣本連加購都沒有過說明特征方向要轉(zhuǎn)向商品側(cè)或用戶側(cè)更宏觀的行為二是單獨(dú)搜閾值而不是默認(rèn)用 0.5。閾值搜索要在驗證集上做且驗證集的正負(fù)比例要和測試集一致否則搜出來的閾值不可遷移。5.5 坑同一個行為類型在不同時間窗里語義不同現(xiàn)象把 30 天內(nèi)的全部行為匯總得出“瀏覽次數(shù)越多越可能買”的結(jié)論但實際情況是最后一周的瀏覽和第一周的瀏覽對購買的影響完全相反。原因用戶第一次瀏覽可能來自首頁推薦屬于被動曝光臨近購買前的再次瀏覽是主動搜索意圖強(qiáng)度差異很大。把不同時間窗的瀏覽壓成一個總和等于抹掉了行為節(jié)奏。解決不要只做總量特征。把行為拆成最近 3 天、最近 7 天、最近 15 天多個窗口分別統(tǒng)計讓模型判斷哪個窗口更重要。也可以再加一個“第一次行為距預(yù)測截止日間隔”字段用來刻畫行為周期的起點。6. 驗證與進(jìn)階滾動時間窗、分桶檢查和提交前復(fù)盤模型和特征穩(wěn)定后最后一步不是直接提交而是做幾件驗證可靠性的工作滾動時間窗驗證、分桶排序檢查、特征重要性復(fù)盤。這三件事做完整個項目的完整度就有了后面迭代和寫說明資料都有明確依據(jù)。6.1 滾動時間窗驗證而不是隨機(jī) K 折隨機(jī) K 折沒有模擬真實的時間先后關(guān)系。真實場景里永遠(yuǎn)是用歷史預(yù)測未來不可能讓模型看未來的數(shù)據(jù)。我會用滾動時間窗比如 30 天數(shù)據(jù)按前 25 天做特征、后 5 天做標(biāo)簽再往前滾一段重新構(gòu)造訓(xùn)練集每段獨(dú)立訓(xùn)練評估最后取均值。from sklearn.model_selection import TimeSeriesSplit # 用行為日志中每個用戶最后一次行為日期作為排序鍵 X[last_action_day] X[user_id].map( actions.groupby(user_id)[action_time].max().dt.date ) tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): X_tr, X_va X.iloc[train_idx], X.iloc[val_idx] # 驗證集時間必須晚于訓(xùn)練集否則檢查排序邏輯 assert X_va[last_action_day].min() X_tr[last_action_day].max()邏輯說明TimeSeriesSplit按數(shù)據(jù)順序遞增地劃分不會把后面的數(shù)據(jù)放進(jìn)訓(xùn)練集。用最后一次行為日期作為排序鍵可以保證驗證集在時間上嚴(yán)格晚于訓(xùn)練集。這個結(jié)構(gòu)比隨機(jī) K 折更有說服力因為測試集本來就在訓(xùn)練集之后。參數(shù)說明n_splits5表示做 5 輪切分第一輪訓(xùn)練集只有 20% 的數(shù)據(jù)越往后訓(xùn)練數(shù)據(jù)越多。樣本量不大時可以跑滿 5 折幾十萬行數(shù)據(jù)時建議調(diào)成 3降低重復(fù)訓(xùn)練的時間開銷。6.2 分桶看預(yù)測分?jǐn)?shù)和實際轉(zhuǎn)化率AUC 只告訴你整體水平不告訴你模型在哪個業(yè)務(wù)切片上好用。我習(xí)慣做一張分組表現(xiàn)表按用戶所在城市級別、商品價格帶、行為活躍度分桶分別統(tǒng)計每個桶的樣本數(shù)、實際購買率和預(yù)測分?jǐn)?shù)分位數(shù)。X[pred_score] model.predict(X[X.columns.difference([pred_score])]) X[pred_bucket] pd.qcut(X[pred_score], 5, labels[1, 2, 3, 4, 5]) result X.groupby(pred_bucket).agg( sample_cnt(pred_score, count), real_buy_rate(label, mean), pred_avg(pred_score, mean) ).reset_index() print(result)邏輯說明這一步看的是分?jǐn)?shù)單調(diào)性——預(yù)測分最高的桶實際購買率應(yīng)該明顯更高。如果第 4 桶的購買率比第 5 桶還高說明這個區(qū)間排序混亂需要回頭補(bǔ)特征。如果某個高價商品桶樣本很少但購買率極高說明部署時要額外加規(guī)則保護(hù)。參數(shù)說明pd.qcut按樣本量等分為 5 桶每個桶樣本數(shù)相同購買率對比更穩(wěn)定。改成 10 桶也可以但樣本少時高桶的統(tǒng)計波動會很大。5 桶起步是我反復(fù)試過更穩(wěn)的方案。6.3 提交前的一輪特征復(fù)盤建模到最后我會把重要性前 20 的特征逐條過一遍這個特征在業(yè)務(wù)上是否解釋得通它和購買意向的邏輯關(guān)系是什么。如果某個重要性極高的特征在業(yè)務(wù)上完全講不通就要警惕是不是數(shù)據(jù)泄露。這一輪復(fù)盤也是給項目說明準(zhǔn)備素材把特征按用戶意圖、商品屬性、交叉行為三類整理每類挑兩個最重要的解釋業(yè)務(wù)含義再附上驗證集指標(biāo)讀者就能理解你的決策過程而不是看到一個黑匣子分?jǐn)?shù)。我習(xí)慣至少準(zhǔn)備兩份結(jié)果一份按模型分?jǐn)?shù)排序一份把加購未購等業(yè)務(wù)強(qiáng)信號的樣本權(quán)重上調(diào)后排序?qū)Ρ冗x擇更穩(wěn)的那份提交。這也是我做過幾輪購買意向預(yù)測之后沉淀下來的習(xí)慣不要只信一個模型也不要只信一個指標(biāo)。希望這次的高潛用戶購買意向預(yù)測拆解能幫到你——從數(shù)據(jù)邊界到特征、從模型調(diào)參到坑位排查拿這套流程去對照你自己的數(shù)據(jù)集能少走很多彎路。本文還有配套的精品資源點擊獲取