天猫复购预测实战:特征工程与LightGBM调参全解析
简介用户行为预测是电商数据分析中的核心任务其本质是通过历史行为日志判断用户未来的购买意愿。在结构化数据建模中特征工程的质量往往决定着模型的上限而梯度提升树如LightGBM则因其高效性和对非线性关系的强拟合能力成为解决此类问题的常用选择。面对购物平台的海量行为序列如何设计用户维度、商品维度与时间窗口特征同时规避数据泄漏风险是提升模型泛化能力的关键。以阿里天池“天猫复购预测”赛题为实践案例工程师需要综合运用统计特征、序列特征与基于AUC的模型评估策略在有限资源下实现预测效果的持续优化。这类任务不仅应用于电商促销与会员运营也为推荐系统、点击率预估等场景提供了可借鉴的方法论。1. 项目背景与核心挑战1.1 天池“天猫复购预测”到底在预测什么阿里天池大赛里的“天猫复购预测”是一项经典的电商用户行为预测任务。简单来说平台会提供一段时间内用户在天猫上的浏览、收藏、加购、购买等行为日志我们要做的是根据这些行为预测某个用户在接下来的某个时间窗口内会不会再次购买某个商品。这不是一个“推荐用户买什么”的问题而是一个“用户会不会再买”的概率判断问题。我当时参与这个赛题时最直观的感受是数据量真的不小但字段又“朴素”得让人无从下手。原始数据里没有用户画像、没有商品详情文本基本就是行为类型、时间戳、商品ID、用户ID和类别ID这几列。绝大多数信息都要靠我们自己从行为序列里挖。换句话说这个赛题比的不是模型有多花哨而是特征工程和对业务逻辑的理解深度。复购预测本身在电商场景里非常常见比如会员运营里的流失预警、优惠券触达人群选择、个性化推送时机判断本质上都在解决同一个问题谁在什么条件下最有可能再次购买。天池这个赛题把这个问题放在了一个相对干净的竞赛环境里所以非常适合用来练手也适合作为入门结构化数据竞赛的高质量案例。1.2 这个赛题的难点在哪里难点有几个而且都是那种不真正跑一遍不会意识到的。第一行为数据是稀疏的。绝大多数用户的购买行为很少大量用户在观测窗口内可能只买过一次甚至一次都没买。样本分布天然倾斜直接建模容易学出一堆“什么都不买”的平庸预测。第二时间依赖性很强。用户的行为会随时间变化促销节点、星期几、一天中的哪个时段都会影响购买意愿这些都需要通过时间窗口特征或者行为序列特征去刻画。第三评估指标不是准确率而是AUC。这个细节很关键它决定了我们在训练目标、样本采样和阈值选择上的所有策略。我自己在动手前先做了一件事把官方提供的样例数据完整梳理一遍确认训练集和测试集的时间范围、用户和商品的数量级、行为类型的分布。这一步看起来笨但能帮你避免后面大量的返工。很多人一上来就写特征工程脚本结果写到一半发现对时间字段的理解错了所有特征全都得推倒重来。2. 整体方案设计从 baseline 到高分的路径2.1 评估指标决定了技术选型这个赛题的官方评估指标是AUC也就是ROC曲线下的面积。AUC衡量的是模型把正样本排到负样本前面的概率它不关心具体阈值只关心排序能力。所以这个赛题从根上就是一个排序问题而不是分类问题。理解这一点之后技术选型就清晰了很多我们不需要死磕“分类概率校准”而是要把精力放在“特征是否能把两类样本拉开距离”上。我当时的主流方案是用LightGBM这类梯度提升树模型因为它对特征缩放不敏感、能自动处理缺失值、对非线性关系拟合能力强在中小型表格数据上往往比深度学习模型更稳。后来我也试过用XGBoost和CatBoost做对比但最终版本用的是LightGBM理由后面会详细说。AUC还有一个隐含要求样本量要够大否则线上和线下的AUC波动会很大。所以我在划分验证集时不会只做简单的随机切分而是严格按时间切分保证训练集的时间范围完全在验证集之前。这样线下AUC才更接近线上表现。2.2 特征工程的整体框架针对复购预测我的特征体系分成了五个层次每个层次解决一类信息用户维度特征用户的历史行为次数、购买次数、行为种类数、活跃天数、最近一次购买距今的天数。商品维度特征商品的热度、销量、被购买不同用户的分布情况。用户-商品交叉特征同一个用户对同一个商品的历史行为次数、是否收藏过、是否加购过、历史购买间隔。时间窗口特征过去1天、3天、7天、14天、30天内的行为统计。这一层最容易提分也最容易泄漏。行为序列特征用户最近几次行为之间的间隔、行为类型转移规律等基于行为序列做滚动统计。特征框架的构建一定要在写代码之前想清楚。我不是那种“先跑个baseline再加特征”的选手因为那样容易陷入无休止的特征堆砌。我的习惯是先把特征框架都列出来按信息源分类再逐个验证有效性。整套特征下来差不多一百多个但最终真正留下来用于训练的只有六七十个。2.3 模型方案的演进路线刚开始我直接用逻辑回归跑了一个baselineAUC大概在0.65出头。这个阶段的意义不是拿高分而是确认数据和特征管线是否正常。之后我切到LightGBM同样的特征AUC直接跳到0.72左右。这验证了一件事该场景下的特征与目标之间存在明显的非线性关系线性模型不太够用。再往后就是不断地加特征、删特征、调参数和做交叉验证的循环。我发现有一个很有效的操作把用户最近的购买行为单独拆出来比如“该用户是否在最近7天内购买过该商品”这一类“瞬间”特征对AUC的提升非常明显因为它们直接刻画了短期购买意愿。但这类特征也最容易过拟合必须在验证集上反复确认。我还试过用深度模型比如对用户行为序列用GRU编码再拼上统计特征丢进MLP。效果不差但训练成本高、调参复杂线下AUC也就比LightGBM高0.3个百分点。考虑到竞赛时间有限我放弃了深度模型方案把精力全部集中在LightGBM和特征优化上。2.4 训练集与验证集的划分策略时间序列数据的划分绝不能随意打乱。这个赛题给的数据是有时间顺序的用户的购买行为在时间上是有依赖的。如果随机划分就会出现“用未来的数据训练用过去的数据验证”的泄漏情况线下分数会虚高线上直接崩盘。我的划分方法是把用户最后一次行为时间作为排序依据按时间顺序将用户分成训练集和验证集保证验证集用户在时间上整体晚于训练集用户。具体比例我用了80%训练、20%验证。为了更稳我还额外做了一次按时间滑动的交叉验证虽然训练成本高一些但对最终模型的选择非常有参考价值。3. 核心细节特征工程与数据处理的实测经验3.1 用户维度的统计特征用户维度是整个方案里的地基。一个用户在过去30天总共买了几次、浏览了多少个商品、收藏了多少次这些信息直接反映用户的活跃程度和购买力。我通常会生成这样一组统计特征用户行为总次数按行为类型分别统计。用户独立商品数、独立类别数衡量用户兴趣的广度。用户平均每天行为次数行为日活跃度。用户购买转化率也就是购买行为次数除以总行为次数。用户最近一次行为距今天数反映用户是否已经流失。这些特征虽然简单但我实测下来对AUC的贡献极大。原因也好理解复购预测本质上是在判断“用户当前的状态”而状态最直接的表达就是历史行为统计。如果这些基础特征都没做好后面再花哨的交叉特征都很难救回来。有个细节要注意统计口径必须保持一致。训练集和测试集的时间范围不同所以特征必须在各自的时间窗口内计算不能拿全量数据一次性计算再切分否则会造成时间泄漏。我在代码里用了一个统一的时间边界参数所有统计特征都通过这个边界传入这样训练和预测时逻辑完全一致。3.2 商品维度的踩坑点商品维度的特征主要刻画商品的热度和供需关系。比如每个商品被购买的总次数、被多少个不同用户购买过、商品在最近7天内的销量、商品所在类目的平均销量等。这里最值得说的是“商品冷启动”问题。在测试集里会出现一些训练集里没有见过的商品。对这些商品我们没有任何历史统计信息特征值全是零或者缺失。我在初期没有做任何处理结果模型对这些商品的预测基本等于随机。后来我加了一招所有商品维度的特征都做了一层“全局均值填充”如果某个商品没有历史数据就用所有商品的平均值替代。这样模型至少能给出一个合理的基础预测。另外一个坑是商品ID的稀疏编码问题。如果直接把商品ID作为类别特征喂给LightGBM会造成特征空间过大、过拟合严重。正确做法是只保留销量靠前的少量商品ID作为类别特征其余全部归为一个“其他”类。这个操作能显著降低过拟合风险。3.3 时间窗口特征必须注意泄漏时间窗口特征是这个赛题里最能提分、也最容易出事的部分。我的做法是对每个用户分别计算过去1天、3天、7天、14天、30天内的行为计数然后在这些窗口统计基础上再算变化量比如“过去7天购买次数相比过去14天购买次数的比例”。这些都是线下能明显提分的特征。但这里有一个经典泄漏场景如果某条训练样本的标签是“用户在6月1日购买了商品A”而我们用这个行为本身去计算“过去7天是否买过A”那这个特征就包含了答案模型线下AUC能跑到0.95以上线上则直接翻车。解决办法是特征计算的时间截止点必须严格在标签时间之前。对于复购预测标签定义通常是“用户在某个时间点之后的某个窗口内是否购买”所以特征计算必须只用那个时间点之前的数据。我写了一个工具函数把所有统计特征都限定在截止时间之前计算并且在验证集上做了一次泄漏检测如果某个特征的AUC单独超过0.9就要怀疑是不是算进去了未来信息。3.4 如何避免内存爆炸这份数据包含千万级的行为记录如果直接以“用户-商品-日期”为粒度展开特征内存很快就爆了。我的经验是先做数据压缩把用户ID和商品ID都映射成连续的整数编号同时把pandas的dtype都调成int32或float32。这一步能把内存节省一半以上。另外特征计算尽量避免用groupby之后直接merge到全量数据的方式那种写法会生成巨大的中间DataFrame。我通常会先按用户维度聚合出用户特征再按商品维度聚合出商品特征最后在样本表上用小表merge大表的方式拼接。这样内存占用稳定代码跑起来也更快。我在实际比赛时还加了一个技巧把原始行为数据按时间切成多个小文件每个小文件单独计算窗口特征最后在汇总阶段做合并且去重。这样即使单机内存只有16G也能跑完整个流程。3.5 特征筛选不是特征越多越好特征堆得太多不一定是好事。首先是训练时间变长其次是噪声特征会干扰模型的分裂选择。我用的筛选方法很简单先跑一遍LightGBM输出特征重要性把重要性为0的特征删掉再跑一遍对比AUC是否下降。如果下降说明有特征之间存在较强的替代关系需要人工判断哪些该留。还有一招是看特征之间的相关性。如果两个特征的相关性超过0.98只保留其中一个。我在实际项目里发现很多基于同类统计的不同窗口特征高度相关比如“过去7天加购次数”和“过去14天加购次数”相关性可能超过0.95。这时候保留其中一个就足够了硬塞两个进去只会增加计算开销对AUC没有帮助。4. 模型训练与调参的实操记录4.1 LightGBM 的快速起步配置LightGBM是我最终选择的主模型原因是它训练速度快、内存占用低、对类别特征和缺失值都有比较好的支持。我的基础配置如下import lightgbm as lgb params { objective: binary, metric: auc, boosting_type: gbdt, learning_rate: 0.05, num_leaves: 63, max_depth: 7, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l1: 0.1, lambda_l2: 1.0, verbose: -1, seed: 42 } d_train lgb.Dataset(X_train, y_train) d_valid lgb.Dataset(X_valid, y_valid, referenced_train) model lgb.train( params, d_train, num_boost_round5000, valid_sets[d_valid], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)] )这套配置是我在不同赛题里反复用过的起点线上基本不会差。learning_rate设成0.05配合num_boost_round上限5000可以在训练时间和精度之间取得平衡。feature_fraction和bagging_fraction都设0.8能随机降采样特征和样本降低过拟合。有一个容易被忽略的参数是num_leaves。在LightGBM里叶子节点数对模型复杂度的影响比深度更明显一般我控制在32到127之间。太小欠拟合太大必过拟合。我最终选了63配合max_depth7算是比较稳妥的组合。4.2 超参数调节的有效顺序调参如果一股脑全调会非常浪费时间。我的顺序是固定的先调叶子节点数和深度再调学习率最后调正则化参数和采样比例。第一步把learning_rate固定为0.1先用默认参数跑一遍观察验证集AUC随迭代次数的变化曲线。这一步能快速判断模型是欠拟合还是过拟合。如果验证AUC一直上升没有明显平台期说明模型容量不够需要增加num_leaves如果验证AUC很快达到顶峰然后开始下降说明过拟合需要增加min_data_in_leaf或正则化强度。第二步在确定num_leaves之后把learning_rate降到0.01重新跑一遍。学习率降低通常能带来小幅AUC提升但需要增加迭代轮数训练时间也会变长。我当时用0.01跑了一次AUC提升了大概0.002考虑到线上稳定性我最终保留了0.05。第三步才是调feature_fraction和bagging_fraction。这两个参数的主要作用是增加模型的随机性减少过拟合。如果验证集AUC和训练集AUC差距较大优先降低这两个值。我不建议盲目用网格搜索因为参数空间太大单次训练时间又长。如果你的机器足够好可以用optuna做贝叶斯优化但一定要限制搜索次数一般100轮以内足够了。4.3 类别不平衡的处理方式复购预测里的正负样本比例大约是1比9甚至更低。如果不做任何处理模型会倾向于把所有样本预测为负类导致AUC看起来还行但实际正样本基本没得分。我用过两种有效的方法。第一种是设置is_unbalance: true或scale_pos_weight。这个参数可以有效放大正样本的梯度让模型更关注正样本。第二种是负样本下采样把负样本随机抽一部分使正负比例接近1比3。下采样之后记得在验证集上保持原始分布否则线下AUC会被低估。这两种方式我最终选择了下采样。原因是在这个赛题里正样本的绝对数量已经足够大下采样不会损失太多信息但能明显加速训练。is_unbalance虽然有效但训练时会让模型对正样本过拟合如果特征工程不到位线上分数波动会比较明显。还有一个小技巧AUC对样本比例不敏感所以即使采样比例变了AUC仍然可以横向比较。如果你发现线下AUC和线上差距特别大除了排查泄漏也要去看看是不是采样导致训练集分布和测试集分布不一致。4.4 对抗过拟合的做法表格数据竞赛里最常见的过拟合信号是训练AUC一路涨到0.95以上验证AUC却停在0.75左右。这时候要做的不是急着调模型而是先查特征。我排查过拟合的顺序一般是查时间泄漏检查特征计算是否使用了未来信息。查重复样本有没有同一用户同一商品在同一时间段出现了多次导致训练集和验证集交叉。查高基数特征商品ID或类别ID这样的高基数类别特征是否保留太多可以考虑删除或用目标编码替代。加正则调大lambda_l1和lambda_l2增大min_data_in_leaf。减少模型复杂度降低num_leaves减少迭代轮数。我遇到过一个大坑是一个用户可能同时出现在训练集和验证集里因为我们的样本粒度是“用户-商品-时间窗口”同一个用户会在不同时间窗口出现多次。如果用户层面的统计特征用了全量数据计算那就等于把验证集的信息泄漏给了训练集。解决方式是用户维度特征必须按各自时间窗口分别计算绝不能用全量聚合结果。5. 常见问题与排查技巧实录5.1 线上分数跟线下对不齐这是竞赛中最让人头疼的问题之一。我遇到过线下AUC 0.78线上只有0.72的情况。排查了一圈最终定位在特征计算的时间边界不一致上。具体来说训练集和测试集的时间范围不同我在计算特征时如果用了同一个全局时间截止点那么在测试集上就会有一部分用户的行为没有被统计进去。修正方式是训练集和测试集分别使用各自的时间截止点来生成特征。也就是说特征工程代码必须支持传入“当前样本对应的截止时间”而不是全局写死。另一个常见原因是验证集划分和线上测试分布不一致。线上的测试样本可能是从多个时间点截取的而我如果只用某一个时间点划分验证集就可能在验证集上高估模型泛化能力。建议做多个时间点的滑动验证取平均AUC作为模型选择的依据。5.2 跑了一个小时还没出来先排查这些特征工程脚本跑太久通常是某一步用了全量笛卡尔积或者对超大DataFrame做了逐行操作。我的建议是优先对ID做整数映射减少字符串的内存和计算消耗。所有groupby聚合都尽量用agg一次完成不要循环遍历。如果窗口数量很多先合并成小的聚合结果再整体merge。使用polars或pandas的category类型处理类别特征能大幅提速。有一次我的脚本跑了两个小时没跑完最后发现是groupby之后没有reset_index导致merge时索引匹配混乱生成了全连接。重启之后加上reset_index整个流程压缩到二十分钟。这种低级错误很浪费生命建议每次merge前都打印一下结果的shape确认是否合理。5.3 为什么加入某类特征反而掉分我加了一组基于“用户对某商品类别整体的购买次数”特征结果AUC反而下降了。最初很困惑后来分析了一下发现这组特征跟已有的用户维度特征高度相关同时它又引入了类别之间的干扰信息。比如用户A喜欢买食品但他也偶尔买电器这个特征会把电器的权重拉低反而干扰了对电器复购的预测。这类问题的通用解法是新加特征后不要只看AUC涨跌还要看特征重要性和相关系数。如果新特征跟某几个旧特征相关性极高优先考虑替换而不是叠加。特征的“信息增量”才是真正有用的纯粹的冗余特征只会增加训练噪声。5.4 数据重复导致的时间泄漏案例有一次我差点犯了大错。在构造训练样本时我用的是“用户-商品”作为粒度但没有去重。结果同一个用户对同一个商品在多个时间窗口都有样本而这些样本的标签其实是同一个购买行为。这导致部分样本的“近期是否购买”特征实际上是同一个买卖行为从而产生严重泄漏。排查方法很简单按用户ID、商品ID、时间窗口三个列联合去重统计样本数量是否明显减少。如果减少比例超过10%说明原始样本构建逻辑有问题。我当时去重后样本量减少了8.5%说实话这个量级如果不仔细看根本发现不了。修正思路是样本的构建必须满足“一条样本对应一个预测时点”不能有重叠。每次预测时点不同特征计算窗口也不同这样样本之间才独立。5.5 一份可以直接抄的落地调参顺序我在多个赛题里验证过一套比较通用的调参顺序这里简单记录一下跑一个LightGBM基础参数下的结果确认特征管线无误。固定learning_rate0.05调num_leaves和max_depth看验证AUC变化。固定最优num_leaves将learning_rate降到0.01确认AUC提升幅度。如果提升小于0.001回到0.05。调min_data_in_leaf从50开始往大到200试几档。调feature_fraction和bagging_fraction一般从0.8开始如果过拟合就收到0.6。最后加l1/l2正则观察验证AUC是否稳定。这套顺序不是数学最优但很实用因为每一步改动都只动一个参数能清楚看到每个参数的影响方便复盘。6. 源代码组织与文档说明的要点6.1 代码目录怎么设计才方便复盘赛题代码如果堆在一两个文件里前期写起来爽后期改起来想哭。我最终采用的是模块化目录结构也推荐给后来者参考project/ ├── config.py # 全局参数文件路径、时间边界、抽样比例 ├── utils/ │ ├── data_preprocess.py # 数据加载、ID映射、类型转换 │ ├── feature_engineering.py # 特征计算的统一接口 │ └── validation.py # 时间划分、自定义交叉验证 ├── train.py # 训练与验证 ├── predict.py # 生成最终提交结果 └── output/ ├── model/ # 保存训练好的模型 └── submission/ # 保存提交文件这样做的好处是每次实验只需要改config.py里的参数不需要翻几千行代码。特征工程里的函数都是独立的可以单独跑一个特征并验证效果也可以整体跑一遍。每改进一个特征我还会顺手在feature_engineering.py里写一个简短的注释说明特征含义和计算逻辑方便两周后自己还能看懂。我在竞赛结束后还会把每轮实验的关键结果记录在experiment_log.md里包括使用了哪些特征、参数是什么、线下AUC多少、线上分数多少以及有哪些改动导致分数变化。这个习惯帮我避开了很多重复劳动。6.2 说明文档应该写哪些内容如果你准备把源代码分享或者开源一份好的说明文档能让读者少走弯路。我觉得除了标准的运行环境说明和数据下载方式之外最重要的是写出“特征含义对照表”和“复现实验步骤”。特征含义对照表要列出每个特征的名称、计算方法、所属维度以及当时添加这个特征的原因。这个表格对理解整个项目非常有帮助。复现实验步骤则要写清楚先运行哪个脚本生成特征再运行哪个脚本训练模型最后怎么生成提交文件。最好附带一个在公开数据集或小样本上进行快速测试的命令方便别人验证代码是否正常。我写说明文档时还会附上模型的关键参数和调参理由。即使别人不打算复现只是在阅读代码也能从文档里快速理解你为什么这么设计特征、为什么选这个模型、为什么用这套验证方式。这才是“高分源代码”真正的价值所在。6.3 复现的重要性固定随机种子和环境复现性在竞赛和项目交付里都非常重要。我踩过一次坑某次改动代码后别的什么都没变但复现结果跟之前差了0.01的AUC。查了半天发现是随机种子没有固定LightGBM的bagging和feature_fraction在随机性上不受控制。解决办法很简单在脚本开头统一设置随机种子import random import numpy as np import lightgbm as lgb random.seed(42) np.random.seed(42)同时LightGBM的params里也要设置seed和bagging_seed。如果用了PyTorch或者TensorFlow还需要设置对应的全局随机种子和cudnn的确定性配置。这个步骤虽然看起来不起眼但在需要严格复现的场景下非常关键。另外建议把运行时的Python版本、pandas版本、LightGBM版本也记录在文档里。版本升级有时候会改变底层算法实现导致相同代码跑出不同结果。记录下来能省去后续无数的排查时间。7. 我的个人心得与给后来者的小建议7.1 分数之外最重要的事竞赛的最终分数当然是衡量成绩的标准但我觉得比分数更重要的是你在整个过程中建立起来的那套方法体系。比如如何快速验证一个特征是否有效如何判断线下分数是否可信如何组织代码让团队协作更顺畅这些能力在真实的业务项目里同样适用。天池的复购预测赛题虽然数据规模和线上环境跟工业界相比有一定简化但它的核心挑战比如行为序列特征、时间窗口设计、防泄漏、模型调参每一项都是实际工作中会反复遇到的问题。认真做完一个项目比匆忙刷完十个比赛收获大得多。7.2 接下来可以扩展的方向如果你对这个赛题感兴趣在已经拿到一份高分解法之后还可以尝试几个扩展方向。一个是把特征工程换成更自动化的方案比如用开源工具做自动化特征衍生看看能否发现人工没有考虑到的有效特征。另一个是尝试把多个时间窗口的模型预测结果做融合比如分别训练基于7天窗口和30天窗口的两个模型再对结果取加权平均通常能进一步提升稳定性。还有一个方向是序列模型。虽然LightGBM能拿高分但如果你想提升自己的建模能力可以对用户行为序列做embedding再用Transformer或者GRU建模学习行为之间的时序依赖。这样得到的向量可以作为新特征拼接到LightGBM中也可以单独训练模型做融合。我虽然没有在这个赛题里用上深度模型但在后续的很多项目里这种融合思路帮我拿到了不少额外收益。7.3 最后提醒最后说一个很多人容易忽略的点正式提交前一定要检查提交文件的格式包括列名、行数、样本顺序是否和官方要求完全一致。我见过不少成绩很好的选手因为提交文件多了一列或者行数不对最后被判定无效。比赛固然拼算法但这些基础细节往往决定了你辛辛苦苦做的模型能不能被正确认可。拿到一份高分的源代码和文档千万不要只当“答案”去看而是当成一条已经有人走过的路径。你走一遍带着自己的理解去改特征、调参数踩过几个坑之后再回头来看才能真正掌握复购预测这一类问题背后的逻辑。本文还有配套的精品资源点击获取

相关新闻

最新新闻

日新闻

周新闻

月新闻