
简介本资源源自京东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], howleft) test[rule_score] test[rule_score].fillna(0.0) # 按分数排序输出 test.sort_values(rule_score, ascendingFalse).to_csv(baseline_rule.csv, indexFalse)逻辑说明这个基线把行为类型按购买意图强弱排序加购未购分最高关注次之只看过的最弱。它不含任何机器学习但能很快暴露两个问题一是如果加购未购在测试集里大量存在说明意向信号充足模型应该重点利用二是如果连这种规则基线的线下 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(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 # 以资料中给定的预测截止日为例换成你的实际数据时间 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)逻辑说明这个函数把行为日志按用户-商品对压缩成统计量每个窗口都生成浏览数、加购数、关注数、购买数、行为天数、最后一次行为距截止日的间隔、行为总量和交互多样性。其中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], 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直接表达“加购未购”这个业务强信号在成交预测里属于少数高区分度特征。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(pctTrue) # 和商品信息表合并构造价格带 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 而不是原始浏览数是因为热门商品的浏览量可能是冷门商品的上千倍直接归一化会让大部分商品被压到接近 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_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))逻辑说明按用户而不是按行切分验证集是一个关键决策。购买行为在同一用户内部高度相关如果同一用户的一部分行为出现在训练集、另一部分出现在验证集指标会被严重高估。scale_pos_weight按训练集实际正负比例计算比写死一个数更稳。early_stopping在验证集 AUC 连续 100 轮不提升时停止既防止过拟合也不需要手动纠结树的数量。参数说明常用体积明细如下表所示先固根基再动结构。参数作用建议起点learning_rate控制每棵树贡献的步长越小越稳越慢0.05max_depth限制树高防止单棵过深6num_leaves叶子数上限复杂度控制核心31min_child_samples叶子节点最小样本数抑制噪声分裂20scale_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_splits5) 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_splits5表示做 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 的特征逐条过一遍这个特征在业务上是否解释得通它和购买意向的逻辑关系是什么。如果某个重要性极高的特征在业务上完全讲不通就要警惕是不是数据泄露。这一轮复盘也是给项目说明准备素材把特征按用户意图、商品属性、交叉行为三类整理每类挑两个最重要的解释业务含义再附上验证集指标读者就能理解你的决策过程而不是看到一个黑匣子分数。我习惯至少准备两份结果一份按模型分数排序一份把加购未购等业务强信号的样本权重上调后排序对比选择更稳的那份提交。这也是我做过几轮购买意向预测之后沉淀下来的习惯不要只信一个模型也不要只信一个指标。希望这次的高潜用户购买意向预测拆解能帮到你——从数据边界到特征、从模型调参到坑位排查拿这套流程去对照你自己的数据集能少走很多弯路。本文还有配套的精品资源点击获取