news 2026/9/25 5:59:09

天猫复购预测高分代码复现:特征工程与LightGBM调参避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
天猫复购预测高分代码复现:特征工程与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(lower=1)

这里有个细节:usr_days用nunique统计不同日期数,比count精确,因为同一天大量点击会被count放大。转化率的分母我用了clip(lower=1),防止浏览为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(lower=1)

注意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_value=0) period_buy.columns = ['early_buy', 'mid_buy', 'recent_buy']

这段代码把行为按时间段拆开,再重塑成宽表。unstack会把缺失的购买次数填成0,这一步很必要。但有个坑:如果某个period里没有购买行为,pandas会生成NaN,fill_value=0能解决。更复杂的序列特征是"用户对某商品相邻两次行为的时间间隔",这需要按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是NaN,LightGBM原生支持缺失值,不需要填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_state=42),你本地分数再高,线上也可能变脸。原因很简单:随机划分会把同一时间段的行为同时分到训练集和验证集,特征统计里就包含了未来信息。

我一般按时间戳切分,用观测期前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调到5000,learning_rate调到0.1,看起来很高大上,实际会过拟合。我按以下顺序调五个参数:

  1. learning_rate先固定0.05,不要一开始就冲0.01,训练太慢;
  2. num_leaves控制在31以下,这个参数和树的复杂度强相关;
  3. feature_fraction每棵树随机采样特征比例,我常用0.8;
  4. lambda_l2正则项,从1.0开始,特征多的时候可以再调高;
  5. 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, label=y_train) d_val = lgb.Dataset(X_val, label=y_val) model = lgb.train( params, d_train, num_boost_round=3000, 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_id,lightgbm在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得分。

最后建议你复现时先读文档说明,再看特征构建函数,最后才看模型代码。把源代码当作黑匣子来跑是没有意义的,你要能解释每一个特征为什么存在。照着这个顺序做,聊到复购预测这个方向时,你会越做越顺,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 5:59:09

Android+XAMPP+MySQL家校互动平台:环境搭建与联调实战

简介&#xff1a;这是一份面向Android开发学习者与毕业设计/课程设计人员的家校互动平台项目资料&#xff0c;基于Android客户端、XAMPP服务端与MySQL数据库实现&#xff0c;采用CS架构完成家校通知、成绩查询、互动留言等核心功能&#xff0c;适用于相关项目设计及Android与服…

作者头像 李华
网站建设 2026/9/25 5:58:09

treg:规则驱动的终端目录树工具,告别tree命令的忽略尴尬

1. 项目概述&#xff1a;treg 到底是什么&#xff0c;解决什么问题如果你和我一样&#xff0c;日常在 Linux 终端下干活&#xff0c;大概率用过tree命令来查看目录结构。tree在展示层级目录上是把好手&#xff0c;但它有个很尴尬的地方&#xff1a;没有任何内置规则引擎&#x…

作者头像 李华
网站建设 2026/9/25 5:54:53

杭州正规的全屋定制服务商合作实力参考,口碑好的靠谱企业甄选

在杭州改善型住宅市场中&#xff0c;越来越多精装房业主和家居升级需求的家庭&#xff0c;都在寻找知名的全屋定制品牌&#xff0c;希望通过实力强的全屋定制机构解决空间规划、风格搭配和交付协调的问题。面对市场上众多全屋定制机构推荐信息&#xff0c;如何筛选出正规靠谱的…

作者头像 李华