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