☰
高潜用户购买意向预测全流程:特征工程与LightGBM调参实战
2026/10/22 5:29:48 网站建设 项目流程

简介:本资源源自京东JData算法大赛高潜用户购买意向预测赛题,是一套完整可直接复跑的机器学习项目。内容覆盖数据预处理、正负样本组合、特征生成与筛选、模型训练与验证等核心环节,代码含Python脚本及编译缓存文件,配套项目说明文档与运行教程,另有历史代码备份包。资源共24个文件,以py源码、pyc缓存、markdown说明及txt使用指南为主,压缩包仅约92KB,结构清晰,适合人工智能、数据科学等相关专业的毕设、课设、竞赛复现与入门学习。目前已有176人学习下载,项目中涉及的特征工程与梯度提升树建模思路,对理解电商用户购买意向预测的完整流程具有直接参考价值,下载后即可对照说明快速运行和二次开发。

1. 高潜用户购买意向预测:为什么这个赛题值得一行行拆着学

在电商营销和数据挖掘的场景里,“京东JData算法大赛-高潜用户购买意向预测”是一道特别典型的成交建模题:给你一批用户过去一段时间的浏览、加购、关注、下单行为,让你判断某个用户对指定商品接下来的购买可能性。它和点击率预估最大的区别是样本稀疏、正负样本悬殊,但业务价值又比点击率直接得多,做起来很考验特征功底。

这篇文章把完整路线拆开讲:从字段理解、评估指标,到特征工程、模型训练和调参,再到最容易翻车的地方。源码包里的项目说明和资料,我会按做这类赛题最常见的实现方式把逻辑补齐,你拿到手后可以直接对着改,不需要重新推一遍流程。

适合两类人:一类是想拿真实比赛练手、把“用户购买意向预测”做成完整项目经历的数据新人;另一类是在做电商转化预测、总被离线在线指标不一致折磨的从业者。不预设你懂很多,但从基线到调优,每一步都保证能照着复现。

2. 预测目标与数据边界:字段结构、评估指标和第一个规则基线

做这类赛题最忌讳一上来就训练模型。先把“预测谁、拿什么预测、怎么算分”三件事钉死,后面的特征和模型才谈得上意义。JData 这类比赛的数据通常围绕三个维度组织:用户信息、商品信息、用户行为日志。用户信息里常见的是年龄、性别、城市级别这类静态属性;商品信息里常见的是商品编号、类别、品牌、价格;行为日志则是重头戏,记录每个用户对每个商品在什么时间做了什么操作,行为类型基本覆盖浏览、加购、关注、下单几种。

时间跨度上,行为日志一般覆盖三十天左右,预测目标落在日志末尾之后,比如预测未来三到五天内用户是否会产生购买。读数据时有一个关键点不能马虎:训练集和测试集的时间边界。赛题经常在这里埋坑,比如训练集行为截止日期和测试集不一致,或者测试集里混入训练集时间之后才出现的用户。拿到资料后第一步不是跑模型,而是把每张表的日期范围打出来,标清楚边界再继续。

2.1 字段梳理与数据质检:先解决脏数据再谈特征

我不建议直接读完整数据。更稳妥的做法是写一个轻量概览脚本,确认每张表的主键、时间范围和缺失情况,同时把训练集和测试集的结构对齐。这套动作看着基础,但能省掉后面大量排查时间,属于典型的“磨刀不误砍柴工”。

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']) # 查看表结构和缺失情况 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) # 行为日志的时间范围,这个必须确认 print('行为日志时间范围:', actions['action_time'].min(), '->', actions['action_time'].max()) # 行为类型取值分布 print('行为类型分布:') print(actions['action_type'].value_counts())

这段代码没有任何建模逻辑,但它是整个项目的地基。字段类型和时间范围常常比想象中更坑:时间格式不统一、行为类型用数字编码、缺失值分布在训练集和测试集之间不一致,这些都是常见状况。确认清楚之后再做特征拼接,才不会在后续步骤里报错或产生莫名其妙的空值。

参数说明:parse_dates直接把时间列转成 datetime 类型,后续做时间窗口计算才方便;value_counts用于确认行为类型有几种、占比如何。如果发现训练集有五种行为类型而测试集只有四种,后续做特征时就要按并集处理,不是按交集。

2.2 评估指标:AUC 还是 F1,决定了你怎么调阈值

这类预测赛题常用两类指标:一类是 AUC 这类排序指标,只看模型给正样本打的分是否普遍高于负样本;另一类是 F1 这类阈值指标,要在某个分数之上才算正样本。两者的差别非常大:AUC 高不代表 F1 高,因为 F1 还受正负样本比例和阈值选择的影响。

在高潜购买意向预测中,正样本通常只占整体样本的百分之几,直接算 F1 会很难看,必须配合阈值搜索。我一般会先以 AUC 为主优化模型的排序能力,最后单独做一轮阈值搜索,把 F1 或业务上更关心的精确率拉起来。这里要提醒一句:如果你拿到的资料里官方指标是 F1,那么训练目标就不要设成分类正确率,而是先把模型分数输出,再在验证集上搜最优阈值,这两条路径最终分数差距可能超过十个点。

2.3 第一个基线:用“加购未购”做规则排序

很多新手上来就上 LightGBM,但第一个基线用规则更好,原因有两个:一是能快速验证数据是否存在泄露或时间错位,二是规则分数可以当作后续模型的强特征。我通常会先构造一个最简单的预测逻辑:如果用户在窗口里对某商品加购过但没有下单,就给这个组合一个很高的分数。

import numpy as np # actions: user_id, item_id, action_type, action_time # 先把行为表转成用户-商品对的行为集合 act = actions.groupby(['user_id', 'item_id'])['action_type'].apply(set).reset_index() # 规则打分:加购未购是高意向,只浏览是低意向 def rule_score(s): if 'buy' in s: # 已购买过,存量用户短期再购概率低 return 0.3 if 'cart' in s: # 加购未购,意向最强 return 0.9 if 'fav' in s: # 关注过,次强 return 0.6 if 'view' in s: # 只浏览过,弱意向 return 0.4 return 0.0 act['rule_score'] = act['action_type'].apply(rule_score) # 与测试集候选对做左连接,得到每个候选的基线分数 test = pd.read_csv('data/test_candidates.csv') test = test.merge(act[['user_id', 'item_id', 'rule_score']], on=['user_id', 'item_id'], how='left') test['rule_score'] = test['rule_score'].fillna(0.0) # 按分数排序输出 test.sort_values('rule_score', ascending=False).to_csv('baseline_rule.csv', index=False)

逻辑说明:这个基线把行为类型按购买意图强弱排序,加购未购分最高,关注次之,只看过的最弱。它不含任何机器学习,但能很快暴露两个问题:一是如果加购未购在测试集里大量存在,说明意向信号充足,模型应该重点利用;二是如果连这种规则基线的线下 AUC 都很低,说明候选集构成可能和训练集差异很大,需要先排查数据配比。

参数说明:rule_score的取值不是硬性的,把加购未购调到 0.95、关注调到 0.7,不影响排序指标,但会影响阈值搜索。规则分数的主要作用是给后面的模型做参照,验证我们构造的特征是否超过了这个“常识线”。

3. 特征工程:把三十天行为日志压缩成三类特征宽表

特征工程是这个赛题真正的重头戏。行为日志是典型的稀疏长表,特征是“压”出来的,不是在原始表上直接学习。我习惯从三个方向分别压:用户侧、商品侧、用户-商品交叉侧,最后拼成一张宽表。主线思路是:所有能在业务上解释购买动机的行为,都应该被数字特征显式表达,而不是让模型自己从原始日志里找规律。

窗口长度是第一个要决定的事。窗口太短,统计不稳定;窗口太长,容易混入过期信息。三十天数据我一般同时保留 7 天、15 天和全量 30 天三个窗口,这样模型既能捕捉近期意图的突变,又能保留长期偏好。

3.1 时间窗口统计特征:不同窗口长度为什么都要留

下面这个函数按窗口天数做聚合,对浏览、加购、关注、购买分别计数,同时把行为天数、最近一次行为间隔等时间结构也记录下来。生成三份不同窗口的特征再横向拼接,让模型自己判断哪个时间尺度更有效。

def window_features(actions, window_days, date_ref): """对截止日期 date_ref 往前 window_days 天内的行为做统计""" start = date_ref - pd.Timedelta(days=window_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(axis=1) feat['interact_diversity'] = (feat[['view_cnt', 'cart_cnt', 'fav_cnt', 'buy_cnt']] > 0).sum(axis=1) feat.columns = ['user_id', 'item_id'] + [f'w{window_days}_' + c for c in feat.columns[2:]] return feat # 以资料中给定的预测截止日为例,换成你的实际数据时间 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'], how='outer') wide = wide.merge(feat_30, on=['user_id', 'item_id'], how='outer') print(wide.shape)

逻辑说明:这个函数把行为日志按用户-商品对压缩成统计量,每个窗口都生成浏览数、加购数、关注数、购买数、行为天数、最后一次行为距截止日的间隔、行为总量和交互多样性。其中last_action_gap特别有用:用户三天前还在反复看某商品,说明注意力还在;最后一次行为是二十天前,意向大概率已经凉了。interact_diversity用来区分“被动刷到一次”和“多行为类型反复触发”,后者的购买意愿明显更强。

参数说明:窗口天数必须和赛题时间边界对齐。30 天数据里只做 7 天窗口,特征会偏稀疏,但短期意向信号更强烈。三个窗口都保留而不是只用一个,是为了让模型自己决定在什么时间尺度上做判断,后续用 feature importance 排查也无从下手。

3.2 用户-商品交叉特征:加购未购和浏览转加购率

交叉特征把用户、商品两侧的信息放在同一个对里计算,目的是捕捉关系型信号。最有代表性的两个特征是“加购未购”和“浏览转加购率”。加购未购的含义很直白:用户把商品放进购物车但始终没有下单,放到真实业务里就是强烈购买意图但还在犹豫,模型拿到这个信号后可以直接把分数拉高。浏览转加购率则区分深度浏览和浅层浏览,看五次才加购和看一次就加购,意图强度完全不同。

# 接上一节 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) # 距离上次加购的时间间隔,加购动作离预测截止日越近,意向越新鲜 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'], how='left') 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直接表达“加购未购”这个业务强信号,在成交预测里属于少数高区分度特征。view_to_cart_rate分母加 1 是为了防止除零,同时也让只有浏览没有加购的样本得到一个接近 0 的小值,而不是 NaN。last_cart_gap用 999 填充空值,是为了让模型能区分“从未加购”和“加购了很久都没买”两类样本,避免缺失值在树模型中被合并到错误的分支。

参数说明:999 这个填充值必须远大于正常间隔,正常加购间隔最多 30 天,999 能把缺失值和最大间隔彻底拉开。如果你用 LightGBM,缺失值本身可以保留为 NaN,但填一个大值在特征重要性和排查阶段更直观,不容易让人误以为这个字段没参与分裂。

3.3 商品维度特征:热度、价格带与品牌集中度

只从用户侧做特征,模型会缺少对商品本身的理解。同一个用户对不同商品的行为差异很大,但加入商品属性后,模型就能把这种差异解释清楚。商品侧常用的特征有三类:热度、价格带、品牌集中度。热度可以用商品在全部用户中的浏览次数排名来表示;价格带把商品价格分桶,不同价格带对应完全不同的决策周期;品牌集中度衡量销售是否高度集中于爆款,集中度高时非爆款单品被直接购买的概率预期要保守。

# 商品侧特征 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(pct=True) # 和商品信息表合并,构造价格带 item_feat = item_feat.merge(items[['item_id', 'price', 'brand_id', 'cate_id']], on='item_id', how='left') 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']], on='item_id', how='left') 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, on='brand_id', how='left') item_feat = item_feat.merge(brand_top1_share, on='brand_id', how='left') print(item_feat.head())

逻辑说明:热度用 rank 而不是原始浏览数,是因为热门商品的浏览量可能是冷门商品的上千倍,直接归一化会让大部分商品被压到接近 0。价格带把连续价格离散化,让树模型能清晰区分不同决策周期的品类。品牌集中度的计算里,先统计每个品牌下每个商品的购买次数,再用 top1 商品购买量除以品牌总购买量;集中度高时,非爆款商品的购买意向预测要更保守。

参数说明:价格分桶边界不是固定的,可以按数据分布调整。如果发现大部分商品价格集中在 0-100,就要把第一档再切细成 0-50、50-100。pd.cut的 labels 参数也不一定非用 0-3,树模型可以吃任意有顺序关系的数值,关键是分桶边界要贴合商品真实价格分布。

4. 建模与调参:把 LightGBM 稳定地用在高潜购买预测上

特征宽表构造完成之后,建模本身反而直接。我的习惯是先跑一个默认参数的 LightGBM,看验证集 AUC 和特征重要性,再按顺序调参。不要一开始就追求 SOTA,先把流程完整跑通,才有资格谈优化。

4.1 模型选型:表格数据上为什么优先用 GBDT

特征工程完成后,样本通常是几千到几十万行的宽表,特征是数值型和少量类别型的混合。这种结构下 GBDT 类模型几乎总是比深度模型更容易拿到更好的 AUC。原因是表格数据里的特征交互可以靠树的天然分裂机制表达,不需要像神经网络那样手动设计 embedding 结构,而且 GBDT 对特征尺度不敏感,数值型特征直接丢进去就好。

在几个 GBDT 实现里,我通常先用 LightGBM,而不是默认 XGBoost。LightGBM 训练速度快,在这个量级能快速迭代特征;它对类别特征有原生支持,省掉大量手动编码工作;直方图算法对内存也更友好。这不代表 XGBoost 不好,只是起步效率上 LightGBM 更适合这种要频繁试特征的赛题。

4.2 样本不均衡:别急着对负样本做极端下采样

购买意向预测里正负样本比例可能达到 1:20 甚至更悬殊。遇到这种情况,很多新手第一反应是把负样本砍到和正样本一样多,这是过度反应。GBDT 对不均衡的容忍度比想象中高,直接使用全部样本、只调整评估方式和阈值,往往就能取得不错的效果。

如果正样本实在太少,我才会考虑两种做法:一是负样本按比例下采样,比如保留负样本的 20%-50%,同时让验证集保持原始比例;二是在 LightGBM 里设置scale_pos_weight,本质上给正样本的梯度乘以权重。两者相比,我更愿意先调scale_pos_weight而不是采掉大量负样本,毕竟保留数据就是保留信息。

4.3 三个关键参数:n_estimators、learning_rate、max_depth

下面是最小可用的训练与验证流程。先固定 learning_rate 从 0.05 开始,用 early stopping 决定树的数量,之后再去动树的复杂度。

import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # X 是前面拼好的宽表特征,y 是标签(1 表示成交) X = wide.drop(columns=['user_id', 'item_id', 'label']) y = wide['label'] # 按用户切分,避免同一用户的数据同时出现在训练和验证里 X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.2, random_state=42) 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_round=1000, 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))

逻辑说明:按用户而不是按行切分验证集是一个关键决策。购买行为在同一用户内部高度相关,如果同一用户的一部分行为出现在训练集、另一部分出现在验证集,指标会被严重高估。scale_pos_weight按训练集实际正负比例计算,比写死一个数更稳。early_stopping在验证集 AUC 连续 100 轮不提升时停止,既防止过拟合,也不需要手动纠结树的数量。

参数说明:常用体积明细如下表所示,先固根基再动结构。

参数作用建议起点
learning_rate控制每棵树贡献的步长,越小越稳越慢0.05
max_depth限制树高,防止单棵过深6
num_leaves叶子数上限,复杂度控制核心31
min_child_samples叶子节点最小样本数,抑制噪声分裂20
scale_pos_weight正样本权重,缓解不平衡训练集负/正比例

调参顺序建议:先固定 learning_rate,把num_leaves从 31 调到 63、127 看验证集变化;再回来把max_depth调成 8 或 -1;最后观察特征重要性,砍掉重要性接近 0 的列重新训练。这套顺序能让调整可解释,而不是同时动五六个参数,最后连自己都不知道哪一项起了作用。

5. 高潜购买意向预测的 5 个常见坑与排查思路

这类赛题做几轮之后,真正决定水平的是避坑能力。以下五个坑按出现频率排,每条都按现象、原因、解决三段式写清楚。

5.1 坑:验证集 AUC 极高,提交后分数暴跌

现象:本地验证集 AUC 0.85 以上,看起来很漂亮,一换成测试集就掉到 0.7 附近。

原因:最常见的是数据泄露。行为日志里混进了预测日期之后的数据,或者某些静态字段本身就携带未来信息。另一种常见泄露是特征拼接时,不小心把用户-商品对在测试期的行为也算了进去。

解决:做一个固定动作,把训练集和测试集的日期边界重新打印一遍,再检查所有特征的取值分布。具体做法是在建模前把一个时间截止日钉死,所有特征只用截止日之前的数据;如果某个特征使用全量数据聚合,就要追问它有没有引用未来的行为。

5.2 坑:同一用户既在训练集又在验证集,指标虚高

现象:线下 AUC 稳定,线上结果不如预期,回头检查发现切分是按行随机切,而不是按用户切。

原因:同一个用户的多条行为天然相似,随机按行切分等于让模型在训练时见过验证集用户的习惯,评估结果自然虚高。

解决:使用train_test_split时先按 user_id 分组,或者直接用 GroupKFold 做按用户的交叉验证。电商场景里我还会额外加按时间切分:用前 25 天行为构造特征,预测后 5 天,而不是在整个时间区间里随机抽样。

5.3 坑:特征分布训练和测试不一致

现象:特征重要性里某个特征排第一,但它只在训练集里表现好,在测试集里几乎全为空或分布完全改变。

原因:常见于价格、库存、促销活动相关字段,训练期和测试期对应的商品池或时间点不同,导致该特征在两侧覆盖度不一致。

解决:特征构造完成后做一个分布对比脚本,逐列输出训练和测试的缺失率、均值、分位数。如果某个特征在测试集缺失率超过 30%,要么重新构造只用覆盖率高的字段,要么把缺失率本身做成特征,让模型学会在信息缺失时如何决策。

5.4 坑:AUC 尚可但 F1 上不去

现象:AUC 0.72 看着尚可,但按阈值搜出来 F1 只有 0.15,业务完全没法用。

原因:正样本比例太低,AUC 主要靠大量负样本的低分拉高,模型对正样本的召回严重不足。

解决:先不要调模型结构,而是做两件事。一是检查正样本里“加购未购”的覆盖率,如果正样本连加购都没有过,说明特征方向要转向商品侧或用户侧更宏观的行为;二是单独搜阈值而不是默认用 0.5。阈值搜索要在验证集上做,且验证集的正负比例要和测试集一致,否则搜出来的阈值不可迁移。

5.5 坑:同一个行为类型在不同时间窗里语义不同

现象:把 30 天内的全部行为汇总,得出“浏览次数越多越可能买”的结论,但实际情况是最后一周的浏览和第一周的浏览对购买的影响完全相反。

原因:用户第一次浏览可能来自首页推荐,属于被动曝光;临近购买前的再次浏览是主动搜索,意图强度差异很大。把不同时间窗的浏览压成一个总和,等于抹掉了行为节奏。

解决:不要只做总量特征。把行为拆成最近 3 天、最近 7 天、最近 15 天多个窗口分别统计,让模型判断哪个窗口更重要。也可以再加一个“第一次行为距预测截止日间隔”字段,用来刻画行为周期的起点。

6. 验证与进阶:滚动时间窗、分桶检查和提交前复盘

模型和特征稳定后,最后一步不是直接提交,而是做几件验证可靠性的工作:滚动时间窗验证、分桶排序检查、特征重要性复盘。这三件事做完,整个项目的完整度就有了,后面迭代和写说明资料都有明确依据。

6.1 滚动时间窗验证而不是随机 K 折

随机 K 折没有模拟真实的时间先后关系。真实场景里永远是用历史预测未来,不可能让模型看未来的数据。我会用滚动时间窗:比如 30 天数据按前 25 天做特征、后 5 天做标签,再往前滚一段重新构造训练集,每段独立训练评估,最后取均值。

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_splits=5) for train_idx, val_idx in tscv.split(X): X_tr, X_va = X.iloc[train_idx], X.iloc[val_idx] # 验证集时间必须晚于训练集,否则检查排序逻辑 assert X_va['last_action_day'].min() > X_tr['last_action_day'].max()

逻辑说明:TimeSeriesSplit按数据顺序递增地划分,不会把后面的数据放进训练集。用最后一次行为日期作为排序键,可以保证验证集在时间上严格晚于训练集。这个结构比随机 K 折更有说服力,因为测试集本来就在训练集之后。

参数说明:n_splits=5表示做 5 轮切分,第一轮训练集只有 20% 的数据,越往后训练数据越多。样本量不大时可以跑满 5 折;几十万行数据时建议调成 3,降低重复训练的时间开销。

6.2 分桶看预测分数和实际转化率

AUC 只告诉你整体水平,不告诉你模型在哪个业务切片上好用。我习惯做一张分组表现表:按用户所在城市级别、商品价格带、行为活跃度分桶,分别统计每个桶的样本数、实际购买率和预测分数分位数。

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)

逻辑说明:这一步看的是分数单调性——预测分最高的桶,实际购买率应该明显更高。如果第 4 桶的购买率比第 5 桶还高,说明这个区间排序混乱,需要回头补特征。如果某个高价商品桶样本很少但购买率极高,说明部署时要额外加规则保护。

参数说明:pd.qcut按样本量等分为 5 桶,每个桶样本数相同,购买率对比更稳定。改成 10 桶也可以,但样本少时高桶的统计波动会很大。5 桶起步是我反复试过更稳的方案。

6.3 提交前的一轮特征复盘

建模到最后,我会把重要性前 20 的特征逐条过一遍:这个特征在业务上是否解释得通,它和购买意向的逻辑关系是什么。如果某个重要性极高的特征在业务上完全讲不通,就要警惕是不是数据泄露。这一轮复盘也是给项目说明准备素材:把特征按用户意图、商品属性、交叉行为三类整理,每类挑两个最重要的解释业务含义,再附上验证集指标,读者就能理解你的决策过程,而不是看到一个黑匣子分数。

我习惯至少准备两份结果,一份按模型分数排序,一份把加购未购等业务强信号的样本权重上调后排序,对比选择更稳的那份提交。这也是我做过几轮购买意向预测之后沉淀下来的习惯:不要只信一个模型,也不要只信一个指标。希望这次的高潜用户购买意向预测拆解能帮到你——从数据边界到特征、从模型调参到坑位排查,拿这套流程去对照你自己的数据集,能少走很多弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询