简介这是一份围绕京东JData算法大赛高潜用户购买意向预测的完整项目资料定位清晰主要面向计算机相关专业的高校学生、教师及科研人员适用于课程设计、毕业设计、竞赛训练或项目立项演示。资源共包含二十四个文件以Python脚本及编译生成的字节码文件为主覆盖数据合并、正负样本构建、特征生成与模型测试等完整流程同时附有项目说明文档、运行教程和原始数据压缩包便于对照理解与复现。整套资料体积仅为九十二KB轻量而模块完整目前已有一百七十六人学习下载。内含可直接运行的赛题源码支持随机森林与梯度提升树等模型评测可帮助读者快速梳理高潜用户预测的特征工程思路、模型调参与效果验证方法代码框架清晰也便于在此基础上扩展改造用于其他分类预测实战场景。1. 高潜用户购买意向预测从“用户行为日志”到“业务决策分数”的关键一跃假设你手里有一份电商平台近 30 天的用户行为日志——谁浏览了什么商品、谁把哪件商品加进了购物车、谁最终完成了支付。现在要你回答一个问题未来 5 天内哪些人最有可能买走某件高价值商品这个问题直接决定了大促期间的库存预留、优惠券发放和广告预算分配。京东 JData 算法大赛的“高潜用户购买意向预测”就是围绕这个场景展开的标准赛题它不考模型结构有多新奇考的是你能否把噪声极大的行为日志转成真正可信的购买概率评分。这个方向尤其适合两类人一类是从纯分类比赛转向业务建模的从业者另一类是希望用真实脱敏数据系统训练特征工程基本功的学习者。赛题本身的难点不在模型而在于时间窗口怎么切、样本怎么标、特征怎么避免穿越。2. 读懂 JData 赛题结构与评估口径先弄清预测的是什么2.1 表格结构与字段含义四张表怎么串起来JData 赛题的数据组织形式是典型的关系型多表结构不是单一文件。拿到资料包解开后你通常能看到用户信息表、商品信息表、用户行为表和用户关注关系表这四类核心文件。用户信息表里是用户的年龄、性别、职业、城市级别等静态画像商品信息表里是商品的品类、品牌、价格等属性行为表则是整个赛题的主角每条记录代表一次用户对商品的具体操作行为字段至少包含用户 ID、商品 ID、行为类型和时间戳。行为表里的行为类型一般有六个取值浏览、收藏、加购、删除、购买、关注它们共同刻画了用户从“看到”到“想买”再到“付出真金白银”的完整链路。标注的构造逻辑也很直接如果某个用户在某段预测窗口内购买过某件商品这个“用户—商品”对就是正样本否则就是负样本。拿到这类多表数据我一般会先用 pandas 按用户 ID 和商品 ID 把行为表与静态表关联起来然后立刻检查每个表的记录数和 ID 去重情况。这个步骤能提前暴露很多对齐问题比如同一个用户 ID 在不同表里的类型不一致、行为表里有商品信息表不存在的商品 ID 等。import pandas as pd # 读入四张核心表只保留建模需要的字段 user pd.read_csv(JData_User.csv, encodinggbk) product pd.read_csv(JData_Product.csv, encodinggbk) action pd.read_csv(JData_Action.csv, encodinggbk) # 关注关系表字段较多先只看结构和缺失情况 relation pd.read_csv(JData_Relation.csv, encodinggbk) print(user shape:, user.shape, product shape:, product.shape) print(action shape:, action.shape, relation shape:, relation.shape) print(action type values:, action[type].unique()) print(null counts:\n, action.isnull().sum())这段代码的核心目的是确认四张表的体量、行为类型取值和缺失情况。要注意type字段的取值含义因赛程而异常见映射是 1 代表浏览、2 代表收藏、3 代表加购、4 代表删除、5 代表购买、6 代表关注。后续所有特征都建立在这个行为类型映射之上如果映射错了后面加购转化率、购买转化率全部都会算错。2.2 评估指标与提交格式AUC 是唯一裁判这个赛题的评估指标是 AUC——更准确地说是针对预测窗口内所有“用户—商品”对计算出的 AUC 值。AUC 衡量的是模型把真实购买用户排在非购买用户前面的能力它天然不关心绝对概率值只关心排序质量。这意味着你在提交时不需要输出 0-1 的“购买概率”只需要输出一个能体现相对高低的分数即可这个特性后面会直接影响我们对缺失值的处理和特征编码方式。提交文件通常要求三列用户 ID、商品 ID、预测分数。很多人在这里翻车——把训练时用的下标当作用户 ID 直接提交或者遗漏了某些“用户—商品”对。还有一个容易忽视的细节AUC 的样本权重是“用户—商品”对级别的这意味着行为越多的用户产生的测试对越多对整体分数的影响也越大冷启动用户的预测错误对最终分数的影响反而有限。2.3 时间窗口设计训练集与预测集的错位逻辑赛题的时间设计是整个建模的地基。常见安排是给出某段时间比如 4 月某几天的用户行为作为训练数据要求预测其后 5 天内用户的购买行为。这里的关键是训练数据和预测目标之间存在一条明确的时间分界线任何在分界线之后发生的真实购买都不能参与训练特征构造。训练集和验证集的切分也必须严格按时间错开不能随机打乱。我一般用最后一个完整时间段作为离线验证窗口比如把行为数据按天切分为 D1 到 D30用 D1-D25 构造特征、D26-D30 作为预测验证期模型在 D26-D30 的 AUC 近似估算线上成绩。# 按时间戳切分训练与验证验证窗口取最后 5 天 action[time] pd.to_datetime(action[time]) max_day action[time].max() valid_start max_day - pd.Timedelta(days5) train_action action[action[time] valid_start] valid_action action[action[time] valid_start] print(train actions:, len(train_action), valid actions:, len(valid_action)) # 验证集正样本验证窗口内购买行为涉及的用户-商品对 valid_y valid_action[valid_action[type] 5][[user_id, sku_id]].drop_duplicates() valid_y[label] 1 print(valid positive pairs:, len(valid_y))这里的两条规则是第一训练行为数据的截止点必须严格早于验证窗口开始点否则特征会包含验证期的未来信息第二验证集的正样本只能用验证窗口内的购买行为生成窗口之前的历史购买行为不能算作验证期的正样本。整个 AUC 评估体系建立在“预测未发生行为”之上任何时间穿越最终都会在离线分数上给你一个虚假的高分上线后立刻崩塌。3. 特征工程把 30 天行为日志压成模型能读的数字3.1 用户维度基础特征购买力、活跃度与转化率这个赛题的特征工程核心是“三维特征”用户维、商品维、用户—商品交叉维。用户维特征回答的是“这个人整体上有多强的购买意愿和购买力”。我习惯先按用户分组统计行为表中每个用户的行为总量、购买次数、加购次数、收藏次数和活跃天数然后组合出转化率类特征比如加购转化率购买次数除以加购次数、浏览转化率购买次数除以浏览次数。这些特征虽然看起来简单但对树模型来说信息量非常大。购买行为稀薄的用户往往加购转化率特征为缺失值或 0树模型能够利用分裂条件把这类“无意愿用户”快速分到低分段。需要注意的是加购转化率分母为 0 的情况我一般会先做一层防护把分母为 0 的记录直接填 0同时保留“是否产生过加购”这个指示特征让模型自己判断哪些 0 是有意义的、哪些是数据缺失造成的。user_feat action.groupby(user_id).agg( total_behaviors(type, count), total_browse(type, lambda x: (x 1).sum()), total_add_cart(type, lambda x: (x 3).sum()), total_purchase(type, lambda x: (x 5).sum()), total_fav(type, lambda x: (x 2).sum()), active_days(date, nunique) ).reset_index() # 转化率特征分母先行保护再保留原始计数做指示特征 user_feat[add_cart_conv_rate] user_feat[total_purchase] / user_feat[total_add_cart].replace(0, float(nan)) user_feat[add_cart_conv_rate] user_feat[add_cart_conv_rate].fillna(0) user_feat[has_add_cart] (user_feat[total_add_cart] 0).astype(int) print(user_feat.head()) print(users with zero add_cart:, (user_feat[total_add_cart] 0).sum())这段代码里最重要的是replace(0, float(nan))这一步它先把分母为 0 的情况置空再统一填充为 0避免出现 0/0 得到的 NaN 对后续 LightGBM 训练产生干扰。has_add_cart指示特征的存在意义在于当add_cart_conv_rate为 0 时模型需要区分“因为没加购所以转化率为 0”和“加购了但没买所以转化率为 0”这两种用户的后续行为模式完全不同。3.2 商品维度特征热度、价格带与品类竞争力商品维特征回答的是“这个商品本身被市场接受的程度”。一个商品被多少人浏览过、被多少人加购过、又被多少人买走直接反映它的热度和转化效率。我用同样的分组逻辑按商品 ID 聚合出浏览总量、购买总量、加购总量再计算商品热度转化率购买量除以浏览总量和加购转化率加购量除以浏览总量。价格带是这里面容易被忽略但很关键的特征。同样是一款加购量很高的商品4000 元的轻薄本和 40 元的手机壳带来的用户决策链路完全不同前者的浏览到购买之间间隔天数更长、流失率更高。我会把商品价格做分位数离散化生成价格带标签而不是直接把原始价格丢给模型因为树模型对价格的线性关系不敏感对区间关系更敏感。品类特征也可以做同样的频次编码。# 按 sku_id 或 product_id 聚合商品热度特征 product_feat action.groupby(sku_id).agg( sku_browse(type, lambda x: (x 1).sum()), sku_add_cart(type, lambda x: (x 3).sum()), sku_purchase(type, lambda x: (x 5).sum()), ).reset_index() product_feat[sku_hot_rate] product_feat[sku_purchase] / product_feat[sku_browse].replace(0, float(nan)) product_feat[sku_add_rate] product_feat[sku_add_cart] / product_feat[sku_browse].replace(0, float(nan)) # 价格带分箱pd.qcut 按分位数切成 5 档 product[price_band] pd.qcut(product[price], 5, labelsFalse, duplicatesdrop) product_feat product_feat.merge(product[[sku_id, price_band]], onsku_id, howleft) print(product_feat.head())这里的duplicatesdrop是处理分箱边界上大量重复价格的防护参数有些商品价格高度集中在少数几个值上直接按分位数分箱会报错。商品维度特征不能贪多像“被浏览十万次”这种极端热度特征在训练集和真实预测窗口之间分布往往不一致这一条后面避坑章还会展开。3.3 行为序列与时间衰减给“最近动作”更高的权重用户维和商品维特征都是“整体统计”但购买意向有明显的时效性——一个用户今天加购了一件商品和 20 天前加购过同一件商品他们的真实购买概率差一个量级。因此在用户—商品交叉特征里必须引入时间衰减因子。我常用的做法是把行为时间距预测窗口结束的天数做指数衰减衰减半衰期一般取 1 到 3 天。对每个“用户—商品”对统计最近一次浏览、加购、收藏距预测窗口结束的天数同时对行为次数做带衰减的加权求和。# 对每个用户-商品对构造交叉特征基于最近 7 天行为 cross action[action[time] valid_start - pd.Timedelta(days7)].copy() cross[days_to_end] (valid_start - cross[time]).dt.days cross[decay_weight] np.exp(-cross[days_to_end] / 3.0) g cross.groupby([user_id, sku_id]) cross_feat g.agg( last_browse_days(days_to_end, lambda x: x.min() if (cross.loc[x.index, type] 1).any() else np.nan), recent_add_cart(decay_weight, lambda x: x[cross.loc[x.index, type] 3].sum()), recent_view(decay_weight, lambda x: x[cross.loc[x.index, type] 1].sum()), days_since_last_behavior(days_to_end, min), ).reset_index() # 最近一次行为距今的天数越小代表越新鲜 print(cross_feat.head())时间衰减参数3.0表示半衰期约为 2.08 天即两天前的行为权重只有当天的一半左右。这个参数可以调半衰期太短会把早期有效行为全部抹掉太长又会让一个月前的浏览和今天的浏览同等重要。我在实践中通常把半衰期设为 3 到 5 天同时增加一个“最近行为距现在多少天”的原始特征让模型自己决定如何利用这个时间信息。4. 模型训练与离线验证用树模型打出基线再谈提分4.1 选择 LightGBM 的理由表格数据上的默认最优解高潜用户购买意向预测的最终数据形态是标准表格特征以计数、转化率、时间间隔为主这类数据用树模型是最稳妥的选择。LightGBM 是梯度提升树框架里兼顾速度和精度的主流方案对类别特征有原生支持对缺失值的处理也有内置策略。深度模型在这个赛题里反而不划算——行为序列的建模自然可以用 LSTM 或 Transformer但样本量、特征稀疏度和评估指标的性质决定了这类复杂模型的上限提升有限调参成本却高出一个数量级。我一般不会一开始就全量特征上模型而是先拿用户维、商品维、交叉维三大类特征中最基础的几十个特征训练一个 baseline确认离线 AUC 稳定在某个合理区间后再逐步添加复杂的交互特征。这样能避免特征一上来就做了一百多个出了问题根本不知道是特征错还是模型错。4.2 离线验证的切分方式用时间截断逼近线上环境离线验证的目的是模拟“用过去的预测未来”所以验证集必须严格在时间上晚于训练特征构造截止点。我用第 2 章里切好的valid_start作为分界线训练特征全部用valid_start之前的行为构造验证集正负样本生成只看valid_start之后的行为。特征生成代码里所有统计操作都要带上时间过滤条件这是整个赛题最容易出现穿越的死角。正负样本的构造也要注意类别不平衡。预测窗口内购买的用户—商品对通常只占全部对数的极小百分比直接把所有非购买对当作负样本会让训练集极其庞大且正样本占比过低。我一般会对负样本做下采样同时保持 AUC 评估时使用全量样本。注意下采样的比例会改变预测分数的绝对数值但对排序意义上的 AUC 没有影响。from sklearn.model_selection import GroupKFold from sklearn.metrics import roc_auc_score import lightgbm as lgb # 假设 features 是前面构造好的特征矩阵labels 是对应标签 # 时间截断验证直接用第 2 章切好的验证窗口数据 train_idx features[feature_day] valid_start valid_idx features[feature_day] valid_start train_data lgb.Dataset(features[train_idx], labellabels[train_idx]) valid_data lgb.Dataset(features[valid_idx], labellabels[valid_idx]) params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1 } model lgb.train(params, train_data, num_boost_round500, valid_sets[valid_data], callbacks[lgb.early_stopping(50)]) valid_pred model.predict(features[valid_idx]) print(valid AUC:, roc_auc_score(labels[valid_idx], valid_pred))这段训练代码里有三个参数值得重点说明。min_data_in_leaf设为 50 能避免树模型在稀疏特征上学习到只有极少样本的极端分裂直接关系到泛化能力feature_fraction0.8和bagging_fraction0.8分别做列采样和行采样是防止过拟合的双保险early_stopping(50)让模型在验证集 AUC 连续 50 轮不提升时自动停止避免训练过头。4.3 核心参数的作用与调整方向在上述默认参数基础上可调整空间最大的是num_leaves、learning_rate和min_data_in_leaf三者的组合。num_leaves控制模型的复杂度增大它可以拟合更复杂的交互模式但很容易过拟合行为数据中的噪声我会用早停轮数和验证集 AUC 的差值来判断是否过拟合。一条血泪经验learning_rate和num_boost_round是一对联动参数。把学习率从 0.05 降到 0.01 时最优迭代次数通常要乘以 4 到 5 倍如果不调整迭代轮数模型会欠拟合离线 AUC 反而下降。另外LightGBM 对类别特征的处理可以用categorical_feature参数显式指定让树分裂时做类别最优切分而不是把类别编号当作连续值处理。用户职业、城市级别、品类这类高基数类别特征都建议走这个参数。5. 高潜预测避坑指南最容易翻车的 5 个细节5.1 时间穿越验证集分数虚高的头号元凶现象离线 AUC 高达 0.85 以上自己都觉得离谱但提交上去线上分数比离线低一截。原因构造特征时统计了整个行为周期内的用户行为总量这个统计区间包含验证窗口内的行为相当于让模型在训练时看到了未来。解决所有特征必须在“行为截止时间”上严格限定每生成一个特征都要确认它的数据来源区间不超过特征构造日。# 错误写法统计全量行为未来信息混入特征 user_err action.groupby(user_id)[type].apply(lambda x: (x 5).sum()) # 正确写法只统计 valid_start 之前的行为 past_action action[action[time] valid_start] user_ok past_action.groupby(user_id)[type].apply(lambda x: (x 5).sum())这个对比代码看起来简单但实际项目中因为多表 join 导致穿越的情况非常多。比如先把行为表 join 了商品表再统一做特征聚合很容易忘记 join 后的时间条件已经不明显了。我的习惯是特征工程脚本里先定义feature_end_time这个全局变量所有聚合操作都基于action[action[time] feature_end_time]从根上堵死穿越。5.2 热度过拟合把“大家都在买”当成“你会买”现象商品热度类特征比如商品总购买量在训练集上区分度极高模型大量依赖这类特征但真实预测窗口内的热门商品和训练期的热门商品重叠度有限导致泛化失败。原因训练窗口内购买量高的商品天然容易和正样本关联模型学到了“热商品必买”的错误规律。解决商品热度特征要相对建模比如热度分位数排名而不是绝对购买量同时限制此类特征在特征重要性里的主导地位。5.3 冷启动用户特征全零模型直接漏判现象新注册用户没有任何行为历史用户维特征全部为 0 或缺失模型给出的预测分数几乎全部集中在低位。原因树模型对全零特征向量天然无法区分只能靠用户画像特征年龄、性别等兜底但画像特征的预测力度远低于行为特征。解决为缺失和零值特征添加显式的“无行为指示”字段让模型学习到“这个用户是全新用户”这一独立信号如果画像特征可用优先保证画像特征的完整性。5.4 提交格式与 ID 对齐错误现象本地验证 AUC 正常代码流程没报错但提交后提示格式错误或分数极低。原因用训练集的内部索引当作测试集用户 ID或者预测分数表排序的时候把user_id和sku_id列一起排错导致预测分数和实际 ID 错位。解决提交前做一次反向验证——从最终文件里随机抽取几行手工去原始行为表里核对 ID 对应的行为记录是否存在。5.5 AUC 与阈值选择脱节现象模型 AUC 很高但业务上想圈定“高潜用户清单”做精准投放时怎么选阈值都不舒服。原因AUC 关心排序不关心概率绝对水平模型所有预测分数整体偏移时排序不受影响但选择固定阈值就会出问题。解决用验证集上的精确率—召回率曲线辅助选择阈值而不是直接拿预测分数的中位数或 0.5 一刀切。同时可以按业务预算倒推所需召回率再用验证集确定对应阈值。6. 进阶收尾规则桩定下限、双模型分工与行为截断特征6.1 规则桩先给自己一个不可能输的下限正式建模前写一条最朴素的业务规则桩比如“只要加购过目标商品且距今天数小于 3 天就预测高潜”把它提交或参与离线评估得到一个基础 AUC。这个数字是后续一切特征工程和模型工作的参照系。如果复杂模型跑出的离线 AUC 连规则桩都超不过说明特征或验证切分大概率有问题回头查比继续调参更有价值。6.2 双模型分工把“会不会买”和“能不能买”分开建模高潜用户的购买意向其实由两个子问题组成用户对这个品类有没有需求以及用户有没有能力支付这个价格。单模型把这两个信号混在一起容易被“购买力强但没需求”的高消费用户干扰。常见做法是分别训练一个“品类兴趣模型”和一个“价格承受模型”前者用行为活跃度、浏览品类分布特征后者用历史购买价格带、客单价特征最终把两个模型的分数做乘法或加权融合。6.3 行为截断特征只取最近一个周期的行为时间衰减特征解决了“行为时效性”的问题但还有一个更朴素的操作被很多人忽略把行为按最近 1 天、3 天、7 天、30 天分别聚合一批特征而不是只聚合全量。这样模型可以自己判断“最近 3 天频繁加购但没有付款”和“30 天前买过同品类”这两种信号哪个更有价值。def build_window_feature(action_src, window_days, feature_name): end valid_start start end - pd.Timedelta(dayswindow_days) seg action_src[(action_src[time] start) (action_src[time] end)] feat seg.groupby(user_id).agg( **{f{feature_name}_behaviors: (type, count), f{feature_name}_add_cart: (type, lambda x: (x 3).sum()), f{feature_name}_buy: (type, lambda x: (x 5).sum())} ).reset_index() return feat feat_3d build_window_feature(past_action, 3, w3) feat_7d build_window_feature(past_action, 7, w7) feat_30d build_window_feature(past_action, 30, w30) # 三个窗口特征再与用户主特征表做 left join窗口特征的数量控制要克制三个窗口乘以三类行为就已经是 9 个特征再加窗口间的比值特征就更多了。我的习惯是先上 3 天和 7 天两个窗口用特征重要性验证 30 天窗增益如果增益不大就不放避免特征膨胀带来的训练噪声。最后的个人习惯每次训练完成后保存一份包含特征名、特征重要性和验证 AUC 的日志下次迭代时先看这份日志再动手。高潜用户预测这类赛题分数的提升不是靠某一个天才想法而是靠一轮一轮把穿越堵死、把特征做细。希望这篇笔记能帮你少走几趟弯路。本文还有配套的精品资源点击获取