天猫复购预测高分代码复现:特征工程与LightGBM调参避坑指南
简介这是一份基于阿里天池大赛学习赛的天猫复购预测完整案例面向需要完成期末大作业、课程设计或入门数据挖掘的 Python 学习者。项目涵盖数据下载与预处理、特征工程、模型训练与测试全流程代码注释详尽新手也能读懂并快速复现。资源共 8 个文件压缩包仅 4.64MB核心为 3 个 Python 脚本分别负责数据集准备、模型训练与预测输出并附有特征重要性可视化图、训练好的模型文件、预测结果 CSV、说明文档及数据下载说明从数据处理到结果产出均可按步骤复现部署简单。已有 1250 人学习/下载被较多用作课程设计与期末大作业参考。除源代码外还包含文档说明与中间结果可直接对照理解复购预测的建模思路功能完善、操作简单适合快速落地演示或在此基础上做扩展改进。1. 为什么这类学习赛值得复现一份源代码能榨出多少东西从标题看你手上是阿里天池大赛学习赛的天猫复购预测案例源代码加文档说明而且标注了高分。我这两年带着不少人复现过类似赛题最大的体会是这份项目最值钱的不是最后一行的模型调用而是藏在特征工程和数据切分里的坑。天猫复购预测不是一个能靠调高模型复杂度刷分的任务它考验的是你能否把用户行为日志整理成干净、无泄漏、高区分度的宽表。这篇文章给两类人看一类是刚学机器学习想找真实业务数据集练手另一类是已经跑通代码但分数不对想搞明白问题出在哪的人。下面我从数据口径开始一路讲到调参和翻车现场。2. 先读懂赛题与数据拿到代码之前先对齐三件事2.1 复购口径到底是什么预测的是用户还是用户-商品对很多新手拿到这份源代码后第一件事就是跑模型结果连特征名代表什么都不知道最后分数出来了也不知道对错。我吃过这个亏。首先要搞清楚预测目标天池的天猫复购预测不是预测某个用户会不会再买任何东西而是预测某个用户是否会在未来窗口内再次购买某个特定商品。样本单位是用户-商品对而不是单纯用户ID。确认口径的方法是看训练集的形状。如果训练集每一行是(user_id, item_id)去重后的组合那标签就是该用户对该商品是否发生复购。如果有多个不同的item_id对应同一个user_id那就要按用户-商品对构造样本而不是按用户聚合。文档说明里如果写了label字段的来源务必先读这一段。我一般会把训练数据按(user_id, item_id)去重后和整体行数对比如果两者相等基本可以确定官方就是按这个粒度打的标签。这个口径直接决定特征怎么构造。如果你错误地把标签定义成用户是否复购同一个用户会被压缩成一个样本多个商品的特征被平均掉模型很难学到商品维度的复购规律。正确做法是保留用户-商品对再往上拼用户全局特征和商品全局特征。判断技巧还有一条如果一份源代码里的特征名既有user_前缀又有item_前缀而且所有特征都能在同一个(user_id, item_id)粒度上关联那这个代码的设计思路就是对的。2.2 行为日志的字段与统计口径行为类型、时间戳、去重规则行为日志是开发票的坑。字段一般包括user_id、item_id、category_id、brand_id、behavior_type、timestamp这几个。behavior_type常见有浏览、收藏、加购、购买四种。统计前必须先明确三个问题同一用户连续点击同一商品算一次还是多次购买行为是否允许同一用户同一商品多次出现时间戳是秒还是毫秒我拿到日志后第一件事不是建特征而是跑一段探查代码。import pandas as pd log pd.read_csv(behavior_log.csv) # 先看行为类型分布确认数值还是字符串 print(log[behavior_type].value_counts()) # 检查同一用户对同一商品是否存在重复行为 dup log.groupby([user_id, item_id, behavior_type]).size() print(dup[dup 1].head(20))这段代码是拿来救命的。第一行value_counts能看出购买行为占比复购预测里正样本通常只有百分之几如果购买行为占了30%以上多半是行为类型编码理解错了。第二行groupby用来发现重复交互如果同一用户对同一商品有十几次浏览你就要决定统计次数还是只保留最近一次行为。注意这里的behavior_type在官方原始数据里可能是整数1/2/3/4也可能是字符串后续代码里所有lambda表达式都要保持一致否则统计出来全是0。时间戳的处理也很容易掉链子。有些日志给的是datetime字符串有些给的是数值时间戳混合类型直接报错。我习惯先统一转成int64数值再做排序和差值计算。如果源码里用了sort_values([user_id, item_id, timestamp])一定要先确认timestamp是纯数值字典序排序会把2017-11-09排在2017-11-10后面这还算运气好遇到两位数和一位数混排就是灾难。2.3 离线评估指标为什么用AUC而不是准确率天猫复购预测的正负样本极端不平衡购买行为本身占比很小。如果你用准确率评估全预测负样本就能拿到98%左右的分数毫无参考价值。天池这类赛题通常使用AUC也就是ROC曲线下面积。AUC的意义是随机给一个正样本和一个负样本模型把正样本排在前面的概率。它只关心排序不关心阈值非常适合复购这种给用户排序、只取头部转化的场景。from sklearn.metrics import roc_auc_score # y_val是验证集真实标签pred是预测概率 auc roc_auc_score(y_val, pred) print(validation auc:, auc)AUC计算的代码只有这几行但它隐含一个要求你提交线上结果时应提交预测概率而不是经过阈值二值化后的0/1。AUC排序只认概率大小如果你提交前做了截断等于人为破坏了顺序。我原来犯过这个错提交概率后精度提高了一截。另外AUC对样本量很敏感小数据上不同随机种子差距很大所以本地评估要固定验证集不能每次重采样否则你看不到真实调参效果。3. 特征工程定生死高分方案里最值得抄的十类特征3.1 用户维度统计特征次数、天数和转化率在复购预测里特征工程的权重至少占70%模型只是把特征拼乘积起来。我见过太多人把时间花在调整LightGBM参数上特征却只有原始表格的十几列最后分数卡在0.70上不去。先做用户维度统计这个用户在观测期内浏览了多少次、购买了多少次、收藏加购了多少次以及行为覆盖了多少天。高频用户和一次性用户的复购习惯完全不同。usr log.groupby(user_id).agg( usr_browse(behavior_type, lambda s: (s 1).sum()), usr_buy(behavior_type, lambda s: (s 4).sum()), usr_days(date, nunique) ).reset_index() usr[usr_buy_rate] usr[usr_buy] / usr[usr_browse].clip(lower1)这里有个细节usr_days用nunique统计不同日期数比count精确因为同一天大量点击会被count放大。转化率的分母我用了clip(lower1)防止浏览为0时除零报错。如果你的行为类型是字符串lambda里的判断条件要同步改成对应的字符串值比如buy。这种低级错误在复现源代码时特别常见文档说明里如果提到了行为类型字典一定先查一遍。用户维度还包括一个用户购买商品种类数用groupby(user_id)[item_id].nunique()得到。它和购买总次数是两种语义一个用户可能反复购买同一款商品也可能是到处尝鲜。前者更可能对特定商品复购后者更依赖商品本身的热度。这个特征在复购预测里区分度很高我强烈建议加。3.2 商品与类目维度特征热门度、价格带和复购率只看用户侧是片面的。有些商品天然复购周期短比如零食、日用品有些商品虽然行为多但用户只买一次。如果模型不知道商品属性它无法区分这两种情况。商品维度的特征核心是热门度和转化率该商品被多少人浏览过、被多少人购买过、浏览到购买的比例是多少。类目维度同理可以统计类目下的复购率均值。item_stat log.groupby(item_id).agg( item_browse(behavior_type, lambda s: (s 1).sum()), item_buy(behavior_type, lambda s: (s 4).sum()), item_buyers(user_id, nunique) ) item_stat[item_convert] item_stat[item_buy] / item_stat[item_browse].clip(lower1)注意item_buyers用到的是nunique这里计算的是去重用户数不是行为次数。如果把用户数也写成count同一个用户在一周内购买五次会被算成五个用户商品热门度瞬间虚高。我在复现别人代码时看到过这个错误当时那个方案的线下AUC虚高了0.005看似很高的分其实是脏数据喂出来的。品牌和类目ID我一般不直接做数值特征而是作为类别特征交给模型。但类目维度的转化率是数值特征可以先聚合出来类目总购买数除以类目总浏览数。这个特征可以作为全局信息补到每个样本上即使某个样本所在的user-item对数量少也能借助类目先验拿到信息。3.3 时间窗口与行为序列特征把日志切成三段再统计时间窗口特征才是高分方案的分水岭。把所有历史行为一股脑统计模型只能看到总量看不到近期变化。复购行为通常和上一次购买间隔强烈相关快消品一周复购耐用品半年复购。如果日志跨度较长你用整个观测期的平均值去拟合会被中间期冲淡。我一般按时间先后把观测期切成三段通常是早期、中期、近期比例可以按40%、30%、30%切。分别统计每段里的浏览数、加购数、购买数。这样模型能看到用户行为是在升温还是降温。切分基准是全体日志的timestamp分位数不是每个用户单独切否则不同用户的时间起点就对不齐了。cut_early log[timestamp].quantile(0.4) cut_mid log[timestamp].quantile(0.7) log[period] 0 log.loc[log[timestamp] cut_early, period] 0 log.loc[(log[timestamp] cut_early) (log[timestamp] cut_mid), period] 1 log.loc[log[timestamp] cut_mid, period] 2 period_buy log[log[behavior_type] 4].groupby([user_id, item_id, period]).size().unstack(fill_value0) period_buy.columns [early_buy, mid_buy, recent_buy]这段代码把行为按时间段拆开再重塑成宽表。unstack会把缺失的购买次数填成0这一步很必要。但有个坑如果某个period里没有购买行为pandas会生成NaNfill_value0能解决。更复杂的序列特征是用户对某商品相邻两次行为的时间间隔这需要按user-item对排序后做diff。log log.sort_values([user_id, item_id, timestamp]) log[gap] log.groupby([user_id, item_id])[timestamp].diff() gap_stat log.groupby([user_id, item_id])[gap].agg([mean, max])这个时间间隔特征能捕捉用户的复购节奏。注意第一次行为的gap是NaNLightGBM原生支持缺失值不需要填0。如果你用fillna(0)会把首次购买和间隔很短混为一谈反而破坏语义。同样sort_values里的user_id和item_id要一起排再加timestamp作为最后排序键确保同一个商品的行为按时间先后排列。4. 用LightGBM跑通基线选型理由和五个必调参数4.1 为什么表格赛题首选LightGBM速度、缺失值、类别特征天池这类表格型赛题主流方案是GBDT具体到工具上我首选LightGBM。理由有三条训练速度快百万级样本几十秒一轮适合反复试特征原生支持缺失值不用花时间填NaN支持类别特征像brand_id这类的可以直接喂不用one-hot。XGBoost也很好但在同样的数据量下调参成本稍微高一些迭代速度也慢一点。高分源代码里经常同时出现XGB和LGB的融合但你先用LGB把单模型调到靠谱再谈融合才有意义。4.2 训练集与验证集划分按时间切不看随机切复购预测是典型的时间序列决策数据泄漏主要发生在验证集划分上。如果源代码直接用train_test_split(random_state42)你本地分数再高线上也可能变脸。原因很简单随机划分会把同一时间段的行为同时分到训练集和验证集特征统计里就包含了未来信息。我一般按时间戳切分用观测期前80%作为训练特征后20%作为验证集。这个切分要和赛题的目标窗口对齐。比如如果文档说明里提到标签是在目标时间窗口生成的那么训练集的特征构造截止时间必须早于这个窗口开始时间。cut_time log[timestamp].quantile(0.8) train_mask log[timestamp] cut_time val_mask log[timestamp] cut_time切分完成后还要再做一次特征统计。很多人的错误是先在整个训练集上构造特征然后再切分这样验证集特征也用了验证集自身的行为等同于泄漏。正确顺序是先切分日志再用训练日志构造所有统计特征最后把这些特征映射到验证集样本上。这个顺序反了后面的调参全部失真。4.3 五个必调参数学习率、叶子数、随机采样、L2、早停拿到一份高分源代码最容易让人迷失的地方是参数。不少人喜欢把n_estimators调到5000learning_rate调到0.1看起来很高大上实际会过拟合。我按以下顺序调五个参数learning_rate先固定0.05不要一开始就冲0.01训练太慢num_leaves控制在31以下这个参数和树的复杂度强相关feature_fraction每棵树随机采样特征比例我常用0.8lambda_l2正则项从1.0开始特征多的时候可以再调高num_boost_round交给早停不要手动定死。import lightgbm as lgb params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, feature_fraction: 0.8, lambda_l2: 1.0, } d_train lgb.Dataset(X_train, labely_train) d_val lgb.Dataset(X_val, labely_val) model lgb.train( params, d_train, num_boost_round3000, valid_sets[d_val], valid_names[val], callbacks[lgb.early_stopping(100), lgb.log_evaluation(100)] )这里num_boost_round设3000只是上限早停触发后模型会停在最佳轮次。metric设成auc因为赛题评估就是AUC你会看到每轮的训练和验证AUC。每次调参后把早停轮次记录下来如果发现训练AUC一直涨但验证AUC纹丝不动说明num_leaves太大要往小调。如果feature_fraction太低树的多样性增加但每棵树的单棵表现变差需要更多轮数整体训练时间变长。你要在本地反复试验找到一个在验证集上AUC稳定不抖的参数组合。5. 避坑复现高分代码的五个翻车现场5.1 现象本地验证AUC漂亮提交成绩却对不上这是最常见的问题。本地AUC 0.78线上只有0.72差距大到让你怀疑自己是不是下错数据。原因多半是特征构造时用了全量日志线上推理时未来行为的统计值已经混入特征。比如你统计用户总购买次数时把目标窗口内的购买也算进去了这等于把标签的一部分塞进了特征。解决方法是严格按时间切分构造特征验证集当作未知数据所有统计量只允许使用训练部分。这一步做对了线上线下差距通常会缩到0.01以内。5.2 现象验证集里混进了购买记录特征泄漏有个隐蔽泄漏我排查了一天才发现在构造用户维度特征时统计了用户是否购买过该商品但做验证集时也用了目标窗口内的购买记录。这会让模型学到这个用户在这个商品上已经买过这个事实本身而不是复购倾向。解决方法是定义特征时明确设定一个截止时间只使用截止时间之前的历史行为。如果标签是在目标窗口内生成特征构造必须在这个窗口开始之前结束。更狠的做法是直接检查特征重要性如果目标时间段的购买次数出现在前五名你就要警觉了。5.3 现象时间戳处理成字符串排序和算间隔全部失效日志里的时间戳如果是2017-11-11 00:00:00这类字符串直接sort_values会按字母序排月份和日期的词法顺序和真实时间顺序不一致。更隐蔽的是有些行的时间戳是int类型有些是字符串拼在一起后统一变成object排序结果就乱了。我在构造diff特征时踩过这个坑算出来的间隔出现负数排查了很久才发现是字符串排序。解决方法是先统一转成数值型时间戳用pd.to_numeric或astype(int)再排序。转换后看一眼min和max确认没有溢出或负值。5.4 现象类别特征直接以object类型喂给LightGBM训练报错或结果异常LightGBM支持类别特征但前提是数据类型必须是category不能是object。很多源代码里直接读进来的是字符串类型groupby聚合后也没改dtype喂进去就报categorical feature not supported或者静默出错。解决方法是显式转换X[category_id] X[category_id].astype(category)另外要注意训练集和验证集的类别取值集合可能不一样。如果验证集出现了训练集里没见过的brand_idlightgbm在predict时会出现未定义状态。解决方法是先用pd.factorize把类别统一映射到整数再转category或者只保留出现次数足够多的类别。不要为了用类别特征而强行one-hot高基数特征会让训练变慢效果也未必好。5.5 现象五折交叉验证结果蹊跷其中一折特别高复购预测的样本之间存在强关联同一个用户的多条样本被随机分到不同折时实际上变成了预测已知用户的其他行为。用StratifiedKFold得到的验证AUC会虚高而且每一折的表现差别大因为有些折刚好包含高频用户的多条样本。解决方法是按user_id分组做GroupKFold或者干脆按时间切分留出最后一段。我复现高分代码时发现很多线上高分方案并没有用交叉验证而是直接按时段切分原因就在这里。6. 从照搬到超越一套能长期复用的提分检查清单我复现天池这份复购预测源代码时最容易犯的错误是跑通就以为完事了。后来给自己定了一套提分顺序先不加任何技巧按文档说明把baseline跑通记录验证AUC然后加用户特征再看涨不涨再加商品特征最后加时间窗口特征。每加一类就用同一份验证集评估只保留上涨超过0.003的特征。这个阈值因人而异但逻辑是避免一股脑堆几百个特征导致后面排查起来全是黑匣子。另一个长期有用的习惯是每次实验都记录参数、随机种子、特征列表、验证AUC和线上AUC。我建了一个csv每次训练后追加一行调参不再靠记忆。特征重要性用model.feature_importance()输出后我会人工检查排名前20的特征如果发现目标时间段购买数这种明显泄漏的字段出现在高位立刻回炉重造特征。这个习惯救过我很多次。说到教训最亏的一次是加了用户最近一次购买距今天数这个特征本地AUC涨了0.01线上却跌了0.02原因是线上推理时根本拿不到真实的今天。所以判断一个特征能不能用要看在线推理时它是否能构造出来而不仅仅看本地validation得分。最后建议你复现时先读文档说明再看特征构建函数最后才看模型代码。把源代码当作黑匣子来跑是没有意义的你要能解释每一个特征为什么存在。照着这个顺序做聊到复购预测这个方向时你会越做越顺希望帮到你。本文还有配套的精品资源点击获取