ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

天猫复购预测实战:从特征工程到模型调优的完整指南

天猫复购预测实战:从特征工程到模型调优的完整指南 简介本资源为阿里天池大赛学习赛「天猫复购预测」的完整案例包面向计算机、人工智能、通信工程等专业的在校学生、教师及企业员工尤其适合作为毕业设计、课程设计、作业或项目初期立项演示的参考素材。包内共7个文件以3个csv数据集、2个Python脚本、1个Jupyter Notebook和1份README说明文档为主压缩包约4.01MB结构清晰便于按模块检索学习。其中数据集涵盖用户信息、训练集与测试集脚本分别实现逻辑回归与随机森林两种建模思路Notebook则侧重数据分析与可视化可帮助读者完整走通从数据探索、特征处理到模型训练与评估的赛题流程。目前已有488人学习下载代码均经测试运行成功答辩评审平均分达96分适合基础尚可者在此基础上修改扩展实现其他功能或用于个人项目实践。1. 天猫复购预测这件事为什么值得你花一个周末跑通电商里最贵的流量不是新客是已经买过的人。天猫复购预测这个赛题本质是给你一批用户在过去一段时间里的行为日志让你判断他在未来某个窗口内会不会再次下单。它不预测金额、不预测品类只回答一个二分类问题复购还是不复购。听起来简单但真正做过的人都知道这里面藏着电商场景最典型的特征工程难题——行为稀疏、时间窗口敏感、正负样本极度不平衡。阿里天池把这道题放进了学习赛配套了源代码和文档说明这对想入门推荐与用户行为建模的工程师来说是个难得的练手场。你不需要自己爬数据、不需要自己定义标签拿到手的是一份结构清晰的离线数据集剩下的就是特征怎么造、模型怎么选、阈值怎么定。我见过太多人卡在「跑通了但分数上不去」问题往往不在模型而在对复购这件事的业务理解没到位。这篇文章面向两类人一类是刚接触天池学习赛、想拿一个完整项目练手的新手另一类是做过后端或数据分析、想往用户增长方向转的熟手。我会把从数据读取到提交结果的整条链路拆开讲包括我踩过的坑和调参时真正有用的那几个旋钮。你跟着走至少能复现一个能打的基线再往上就看你自己对特征的理解了。2. 复购预测的数据长什么样先看懂再动手2.1 天池学习赛的数据表结构与字段含义天猫复购预测的数据集通常包含两张核心表用户行为日志表和用户画像表。行为日志表记录的是用户在某个时间点对某个商品或店铺的交互行为常见字段包括 user_id、item_id、behavior_type、time_stamp。behavior_type 一般有四种取值点击、收藏、加购、购买。用户画像表则包含 user_id、age_range、gender、seller_id 等静态属性。这里有个容易翻车的地方很多人拿到数据直接 concat 然后丢进模型结果发现时间字段是乱序的或者 behavior_type 是字符串没做编码。我一般会先做三件事把时间戳转成 datetime、按 user_id 和时间排序、统计每个用户的行为序列长度。排序这一步不能省因为后面造滑动窗口特征时顺序错了整个特征就废了。import pandas as pd # 读取行为日志注意时间戳单位通常是秒 df pd.read_csv(user_behavior.csv) df[time] pd.to_datetime(df[time_stamp], units) # 按用户和时间排序这是后续所有窗口特征的前提 df df.sort_values([user_id, time]).reset_index(dropTrue) # 统计每个用户的行为条数看看稀疏程度 user_behavior_count df.groupby(user_id).size() print(user_behavior_count.describe())这段代码的逻辑很直白先把时间戳转成可读时间再按用户和时间双键排序最后看每个用户有多少条行为。参数上唯一需要注意的是 units如果数据源用的是毫秒这里要改成 ms否则时间会跑到 1970 年去。user_behavior_count.describe() 的输出能告诉你数据有多稀疏——如果中位数只有个位数那后面造特征时就得考虑用统计量而不是序列模型。2.2 标签定义与时间窗口切分别把未来信息漏进训练集复购预测的标签不是现成的需要你自己根据时间窗口切。常见做法是选一个时间点 T用 T 之前的行为做特征看用户在 T 到 TN 天内有没有购买行为有就是 1没有就是 0。这个 N 通常取 3 到 7 天具体看赛题说明。这里有个血泪经验切窗口时一定要保证特征窗口和标签窗口不重叠。我见过有人用全量数据造特征然后标签也是全量数据里的购买行为结果模型在训练集上 AUC 0.99一提交就露馅。正确的做法是严格按时间切特征只用 T 之前的数据标签只看 T 之后的数据。# 假设 T 为 2019-05-01标签窗口为之后 3 天 T pd.Timestamp(2019-05-01) label_end T pd.Timedelta(days3) # 特征数据T 之前的行为 feat_df df[df[time] T].copy() # 标签数据T 到 label_end 之间有购买行为的用户 buy_df df[(df[time] T) (df[time] label_end) (df[behavior_type] buy)] label_users buy_df[user_id].unique() # 构造标签列 all_users feat_df[user_id].unique() label_df pd.DataFrame({user_id: all_users}) label_df[label] label_df[user_id].isin(label_users).astype(int)这段代码的关键参数是 T 和 label_end。T 的选择要保证特征窗口内有足够的行为数据label_end 则决定了正样本的比例。如果正样本太少后面建模时得考虑过采样或调整 class_weight。label_df 的构造方式是以特征窗口内出现过的用户为全集这样不会引入未来才出现的新用户。2.3 正负样本不平衡有多严重先量化再决定怎么处理复购预测的正样本比例通常很低我跑过的几个版本里正样本占比在 2% 到 8% 之间。这个比例下如果你直接上逻辑回归或 XGBoost 而不做任何处理模型会倾向于全预测为负准确率看着高但 AUC 很难看。量化不平衡程度很简单label_df[label].value_counts() 就能看到。我一般会算一下正负比如果超过 1:20就会在训练时加 scale_pos_weight 或者用 SMOTE 做少量过采样。但注意过采样只能在训练集上做验证集和测试集必须保持原始分布否则评估指标会失真。# 查看正负样本比例 ratio label_df[label].value_counts() print(ratio) pos_ratio ratio[1] / ratio.sum() print(f正样本占比: {pos_ratio:.4f}) # 如果正样本太少计算 scale_pos_weight if pos_ratio 0.1: scale_pos_weight ratio[0] / ratio[1] print(f建议 scale_pos_weight: {scale_pos_weight:.2f})scale_pos_weight 是 XGBoost 和 LightGBM 里的参数它会让模型在训练时给正样本更高的权重。这个值不需要精确一般设成负正样本比就行。如果你用的是逻辑回归对应的参数是 class_weightbalanced。记住这些处理只在训练集上做验证集不要动。3. 特征工程复购预测的分数上限在这里3.1 用户行为统计特征从点击到购买的漏斗转化复购预测里最有效的特征往往是最简单的统计量。我一般会按 user_id 做聚合算每个用户在特征窗口内的点击次数、收藏次数、加购次数、购买次数以及这些行为之间的转化率。比如加购到购买的转化率、点击到加购的转化率这些比值比绝对次数更能反映用户的购买意愿。除了行为次数还会算一些时间相关的统计量最后一次行为距 T 的天数、行为的时间跨度、活跃天数。最后一次行为距 T 越近复购的可能性通常越高这个特征在很多版本里都是重要性排前几的。# 按用户聚合行为统计 user_feat feat_df.groupby(user_id).agg( click_cnt(behavior_type, lambda x: (x click).sum()), fav_cnt(behavior_type, lambda x: (x fav).sum()), cart_cnt(behavior_type, lambda x: (x cart).sum()), buy_cnt(behavior_type, lambda x: (x buy).sum()), last_time(time, max), first_time(time, min) ).reset_index() # 转化率特征 user_feat[cart_buy_rate] user_feat[buy_cnt] / (user_feat[cart_cnt] 1) user_feat[click_cart_rate] user_feat[cart_cnt] / (user_feat[click_cnt] 1) # 时间差特征 user_feat[days_since_last] (T - user_feat[last_time]).dt.days user_feat[active_span] (user_feat[last_time] - user_feat[first_time]).dt.days这段代码里加 1 是为了防止除零这是个小技巧但很实用。days_since_last 和 active_span 这两个特征在树模型里通常能排进前五。注意 last_time 和 first_time 是 datetime 类型减出来是 timedelta取 .dt.days 转成整数。3.2 商品与店铺维度的交叉特征用户到底在逛什么只看用户自身的行为还不够复购往往和商品或店铺有关。一个用户反复点击某个店铺的商品即使没买也可能在未来转化。我一般会算用户对每个店铺的交互次数然后取最大值、均值、方差作为特征。同样对商品也可以做类似处理。交叉特征的做法是先按 user_id 和 seller_id 分组统计再按 user_id 聚合这些统计量。这样得到的是「用户最常互动的店铺有多活跃」这类信息。计算量会大一些但收益通常值得。# 用户-店铺交叉统计 user_seller feat_df.groupby([user_id, seller_id]).size().reset_index(nameinteract_cnt) # 再按用户聚合取最大、均值、方差 seller_feat user_seller.groupby(user_id)[interact_cnt].agg( seller_maxmax, seller_meanmean, seller_stdstd ).reset_index() # 合并到主特征表 user_feat user_feat.merge(seller_feat, onuser_id, howleft)seller_std 这个特征有意思的地方在于它衡量的是用户互动的店铺是否集中。如果标准差大说明用户逛得很散复购意愿可能低如果标准差小说明用户有偏好复购概率高。这个特征在一些版本里比单纯的次数更有效。3.3 时间滑窗特征最近 3 天和最近 7 天的行为差异时间滑窗是复购预测里区分度很高的特征。同样是 10 次点击分布在 30 天里和集中在最近 3 天含义完全不同。我一般会算最近 3 天、最近 7 天的行为次数以及这些次数占总数比例。实现上先算出每个行为距 T 的天数然后按阈值过滤再聚合。注意这里要用特征窗口的截止时间 T而不是当前时间。# 计算每条行为距 T 的天数 feat_df[days_before_T] (T - feat_df[time]).dt.days # 最近 3 天的行为统计 recent_3 feat_df[feat_df[days_before_T] 3].groupby(user_id).size().reset_index(namerecent_3_cnt) recent_7 feat_df[feat_df[days_before_T] 7].groupby(user_id).size().reset_index(namerecent_7_cnt) # 合并并算比例 user_feat user_feat.merge(recent_3, onuser_id, howleft).fillna(0) user_feat user_feat.merge(recent_7, onuser_id, howleft).fillna(0) user_feat[recent_3_ratio] user_feat[recent_3_cnt] / (user_feat[click_cnt] user_feat[fav_cnt] user_feat[cart_cnt] user_feat[buy_cnt] 1)fillna(0) 不能省因为有些用户在最近 3 天没有行为merge 后会是 NaN。recent_3_ratio 这个特征在树模型里经常被用来做分裂点效果比绝对次数稳定。4. 模型训练与调参从基线到能提交4.1 用 LightGBM 跑通第一个基线参数怎么设LightGBM 是我做这类赛题的首选速度快、对缺失值友好、自带类别特征处理。基线参数不需要太复杂我一般先用一组保守值跑通流程再逐步调。核心参数就几个learning_rate 设 0.05n_estimators 设 500num_leaves 设 31max_depth 设 -1 让它自动生长。如果正样本少加上 scale_pos_weight。早停用验证集 AUC 来监控。import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # 合并特征和标签 data user_feat.merge(label_df, onuser_id, howinner) feature_cols [c for c in data.columns if c not in [user_id, label, last_time, first_time]] X data[feature_cols] y data[label] # 划分训练验证集 X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, stratifyy, random_state42) # LightGBM 基线 params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, max_depth: -1, scale_pos_weight: scale_pos_weight, verbose: -1 } train_data lgb.Dataset(X_train, y_train) val_data lgb.Dataset(X_val, y_val, referencetrain_data) model lgb.train( params, train_data, num_boost_round500, valid_sets[val_data], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] )stratifyy 保证训练验证集的正负比例一致这个在样本不平衡时很重要。early_stopping(50) 表示验证集 AUC 50 轮不提升就停防止过拟合。跑完后看 best_iteration 和 best_score如果 AUC 在 0.6 到 0.7 之间说明基线正常如果低于 0.55大概率是特征或标签切分出了问题。4.2 特征重要性怎么看哪些特征在真正起作用LightGBM 训练完可以直接输出特征重要性。我一般看两个split 和 gain。split 是特征被用来分裂的次数gain 是分裂带来的信息增益总和。gain 更能反映特征的实际贡献。# 特征重要性 importance pd.DataFrame({ feature: feature_cols, gain: model.feature_importance(gain), split: model.feature_importance(split) }).sort_values(gain, ascendingFalse) print(importance.head(15))如果 days_since_last、recent_3_cnt、cart_buy_rate 这些排在前列说明特征工程方向对了。如果某个特征 gain 很低但你觉得它重要可能是分箱或编码方式有问题可以试着做离散化再喂进去。4.3 阈值调整与提交文件生成别用默认 0.5模型输出的是概率提交需要的是 0/1 标签。默认阈值 0.5 在不平衡数据上往往不是最优的。我一般会在验证集上扫一遍阈值找 F1 或准确率最高的点。# 验证集预测概率 val_pred model.predict(X_val, num_iterationmodel.best_iteration) # 扫阈值 best_thresh 0.5 best_f1 0 from sklearn.metrics import f1_score for thresh in [i/100 for i in range(10, 90, 5)]: pred_label (val_pred thresh).astype(int) f1 f1_score(y_val, pred_label) if f1 best_f1: best_f1 f1 best_thresh thresh print(f最佳阈值: {best_thresh}, F1: {best_f1:.4f}) # 测试集预测并生成提交文件 test_pred model.predict(X_test, num_iterationmodel.best_iteration) submission pd.DataFrame({ user_id: test_user_ids, prediction: (test_pred best_thresh).astype(int) }) submission.to_csv(submission.csv, indexFalse)阈值扫描的范围我一般从 0.1 到 0.9步长 0.05。如果最佳阈值偏离 0.5 很远说明模型对正样本的区分度不够可以考虑加更多特征或调整 scale_pos_weight。提交文件的列名要和赛题要求一致别自己改。5. 避坑与排查那些让我熬夜的翻车现场5.1 时间穿越特征里混进了标签窗口的信息现象训练集 AUC 0.95提交后 AUC 0.55。原因造特征时用了全量数据或者聚合时没按时间过滤导致特征里包含了标签窗口内的行为。解决严格按 T 切分特征计算前先 df df[df[time] T]所有聚合都在这个子集上做。检查方法是看特征里有没有「未来」的统计量比如用全量数据算的购买次数。5.2 用户 ID 对不齐merge 后样本量暴涨或暴跌现象merge 特征和标签后样本数比预期多出几倍。原因特征表或标签表里有重复的 user_id导致笛卡尔积。解决merge 前先 drop_duplicates(user_id)merge 后用 data.shape 检查样本量。如果暴跌说明 howinner 丢掉了没有行为的用户改成 howleft 再 fillna。5.3 类别特征编码翻车LightGBM 的 categorical_feature 没设对现象模型训练报错或效果很差。原因把 seller_id 这类高基数类别特征当数值喂进去了或者 categorical_feature 参数没指定。解决在 lgb.Dataset 里用 categorical_feature 指定类别列或者提前做 target encoding。高基数类别别用 one-hot维度爆炸。5.4 验证集分布和测试集不一致随机划分的陷阱现象本地验证 AUC 很高提交后掉点。原因用 train_test_split 随机划分但测试集的时间窗口和训练集不同导致分布偏移。解决按时间划分验证集比如用 T 之前最后一段时间做验证而不是随机抽。如果赛题允许做时间序列交叉验证。5.5 提交文件格式错误列名、顺序、编码现象提交后提示格式错误或分数为 0。原因列名和赛题要求不一致、user_id 顺序乱了、或者编码用了 utf-8-sig 导致第一列多出 BOM。解决严格按赛题 sample_submission 的列名和顺序生成to_csv 时加 encodingutf-8不要加 index。6. 把分数再往上推一截几个我常用的进阶技巧6.1 用 target encoding 处理高基数类别特征seller_id、item_id 这类特征基数高one-hot 会爆炸直接当数值又没意义。target encoding 的思路是用类别对应的标签均值来编码但要注意防止泄漏。我一般用 K 折的方式做把训练集分 K 折每折用其他折的标签均值来编码当前折。from sklearn.model_selection import KFold def target_encode(train_df, val_df, col, target, k5): train_df[col _te] 0 val_df[col _te] 0 kf KFold(n_splitsk, shuffleTrue, random_state42) for tr_idx, va_idx in kf.split(train_df): tr_data train_df.iloc[tr_idx] mean_val tr_data.groupby(col)[target].mean() train_df.loc[train_df.index[va_idx], col _te] train_df.iloc[va_idx][col].map(mean_val) global_mean train_df[target].mean() train_df[col _te].fillna(global_mean, inplaceTrue) val_df[col _te] val_df[col].map(train_df.groupby(col)[target].mean()).fillna(global_mean) return train_df, val_df这个函数的核心是 K 折内用其他折的均值编码避免用当前样本的标签。global_mean 兜底防止某些类别在验证集里没出现过。target encoding 后的特征在树模型里通常比原始类别特征更有效。6.2 模型融合LightGBM XGBoost 逻辑回归的加权平均单模型到瓶颈后融合是最稳的提升方式。我一般跑三个模型LightGBM、XGBoost、逻辑回归然后在验证集上找最优权重。权重不用太精细粗扫就行。from scipy.optimize import minimize import numpy as np # 假设有三个模型的验证集预测概率 pred_lgb model_lgb.predict(X_val) pred_xgb model_xgb.predict(X_val) pred_lr model_lr.predict_proba(X_val)[:, 1] def loss_func(weights): weights np.array(weights) final_pred weights[0]*pred_lgb weights[1]*pred_xgb weights[2]*pred_lr return -roc_auc_score(y_val, final_pred) res minimize(loss_func, [0.4, 0.4, 0.2], methodNelder-Mead) print(最优权重:, res.x)融合后的 AUC 通常比单模型高 0.01 到 0.03。注意逻辑回归要先做标准化否则预测概率尺度不一致。权重和为 1 不是必须的但方便解释。6.3 一个我常用的验证习惯每次改完特征或参数我一定会做两件事一是看验证集 AUC 的变化二是看特征重要性的排序有没有大变动。如果 AUC 涨了但重要性排序面目全非大概率是过拟合了。另外我会保留一份「最小可用特征集」的提交结果万一新特征翻车还能回滚。这个习惯让我在几次比赛中避免了提交事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表