简介一份基于阿里天池大赛学习赛的天猫复购预测完整案例面向准备机器学习竞赛、学习数据挖掘或完成课程设计、毕设项目的学生与开发者。案例围绕用户复购行为建模展开包含训练与测试数据集、逻辑回归与随机森林两种模型对应的脚本代码以及用于数据分析和可视化的交互式笔记可帮助读者从数据预处理、特征构建到模型训练与评估走通全流程。压缩包内共7个文件大小约4.01MB以表格数据、脚本、说明文档和交互式笔记为主结构清晰便于对照学习。项目代码均已运行验证配有简短说明文档遇到运行问题还可私下咨询远程教学作为入门进阶或项目演示都很实用。目前已有488人学习下载适合想积累实战经验、快速上手经典营销预测任务的学习者。1. 电商复购预测案例一份能直接跑通的源码包这个复购预测案例出自某公开学习赛完整源码包里带三个数据表、两个模型脚本、一个可视化分析笔记和一份说明文档。它要解决的问题很具体给定一批用户与商家的历史交互记录判断哪些用户会在未来一段时间内再次产生购买行为。看起来只是二分类难点在于用户行为稀疏、特征交互复杂直接喂原始表进去效果很差。适合正在做课程设计、入门数据竞赛或者想快速搭一个可解释基线模型的开发者。我拆完这个包之后的感受是源码能跑通只是起点真正值得花时间的是数据预处理和特征构造那部分模型脚本反而是最省事的。2. 数据理解与预处理从原始表到可训练特征2.1 数据长什么样复购标签怎么定义先说结论这份资源里的三个 CSV 不是并列的有主从关系。user_info_format1.csv 存的是用户静态画像train_format1.csv 是用户与商家的交互记录同时带着标签列test_format1.csv 的结构和 train 相同但没有标签用来做预测。典型复购预测任务会把训练期和评估期分开用训练期的行为去预测评估期是否发生复购。所以第一步不是建模而是把三个表的粒度统一。实际跑这个案例时我一般会先按 user_id 和 merchant_id 去重看每个用户-商家对有多少条记录。train_format1.csv 里的 label 列就是复购标签1 表示在评估期内再次购买0 表示没有。这个标签是监督信号不能拿测试表的任何信息去构造特征否则会信息泄漏。这也是新手最容易踩的坑先合并再分训练测试结果用测试集统计特征一上分就翻车。import pandas as pd # 读入三个数据文件 user_info pd.read_csv(user_info_format1.csv) train pd.read_csv(train_format1.csv) test pd.read_csv(test_format1.csv) # 看列名和缺失情况 print(train.columns.tolist()) print(train.isnull().sum()) print(test.columns.tolist())逻辑说明先把列名打出来确认标签列和特征列。train 和 test 的列结构应该一致区别只在 label。缺失值统计决定后面是填充还是丢弃比如年龄这类字段缺失率高的直接填充中位数比删除行更稳妥。参数说明read_csv 默认以逗号分隔如果文件是其他分隔符就要显式传 sep。文件路径建议用相对路径避免把编码带进代码里。2.2 特征工程把交互记录压缩成统计量复购预测的常规做法是把训练表按 user_id merchant_id 分组统计出行为特征再拼上用户画像。常见做法是统计购买次数、用户被购买次数、用户购买商品数等。这些特征把时序行为压缩成二维表模型才能直接吃。# 按 user 和 merchant 分组统计交互强度 group train.groupby([user_id, merchant_id]).agg( total_actions(action_type, count), buy_count(action_type, lambda x: (x 2).sum()) ).reset_index() # 合并用户静态画像 data group.merge(user_info, onuser_id, howleft)逻辑说明action_type 是用户行为类型不同数值代表浏览、收藏、加购、购买。这里把购买行为单独计数和总交互数一起作为特征。lambda 写法慢数据量大时改成先转 bool 再 sum。参数说明groupby 后面的 agg 可以直接传字典比如 {action_type: [count, nunique]}一次生成多个统计列。howleft 保证所有交互记录都有画像画像缺失的用户也保留。再补充一些时间特征和交叉特征。如果原始数据有时间戳建议拆成小时、星期因为复购行为有明显时段性。交叉特征我常用的是用户总购买数乘以商家总购买数这比单独看两个特征更容易捕捉“双方都活跃”的关系。代码# 时间特征示例数据里有 time 字段 train[time] pd.to_datetime(train[time]) train[hour] train[time].dt.hour train[weekday] train[time].dt.weekday # 用户与商家的交叉特征 user_buy_total train[train[action_type] 2].groupby(user_id).size() merchant_buy_total train[train[action_type] 2].groupby(merchant_id).size() data data.merge( user_buy_total.rename(user_buy_total), onuser_id, howleft ).merge( merchant_buy_total.rename(merchant_buy_total), onmerchant_id, howleft ) data[user_x_merchant] data[user_buy_total] * data[merchant_buy_total]逻辑说明时间字段处理要谨慎先确认是不是标准格式。交叉特征本质是构造新的多项式特征在树模型里也能提升一点但逻辑回归受益更明显。参数说明.dt.hour 依赖 pd.to_datetime 成功转换失败会出现 NaT要用 fillna 处理。两个 groupby 的 size() 返回的是 Series用 rename 改成列名才能正常 merge。合并后出现 NaN交给模型前需要填充 0。2.3 训练集与验证集的切分方法完成特征合并后不能直接拿全部数据训练要留验证。常见做法是按用户 id 切分确保同一用户记录不会同时出现在训练和验证里否则信息泄漏。代码from sklearn.model_selection import train_test_split X data.drop(columns[user_id, merchant_id, label]) y data[label] X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy )逻辑说明stratifyy 让训练和验证标签比例一致避免正样本过少。random_state 固定种子保证可复现。参数说明test_size0.2 是常用值。如果数据量小可以调到 0.3但要注意验证集波动。这里没有按时间切分如果原始数据有序最好用时间前 80% 做训练后 20% 做验证能更贴近真实评测。2.4 类别特征编码与内存优化用户画像里很多是性别、年龄层这种分类属性。逻辑回归不能直接吃字符串要编码。常见做法是 LabelEncoder 或 one-hot。对于树模型LabelEncoder 就够用。代码from sklearn.preprocessing import LabelEncoder le LabelEncoder() for col in [gender, age_range]: if col in data.columns: data[col] le.fit_transform(data[col].astype(str))逻辑说明直接把缺失值编码进一个特殊类别比单独填充 0 更稳。astype(str) 把 NaN 变成字符串避免编码器报错。参数说明LabelEncoder 在训练集上 fit 后验证集要复用同一个编码器不能用验证集重新 fit否则类别值不一致。one-hot 在特征少的时候可以用但复购预测里类别维度高one-hot 会让矩阵稀疏反而拖慢逻辑回归。内存优化也是必做的一步。pandas 默认 int64 和 float64 会吃掉大量内存做特征工程时经常把内存占用推到 10G 以上。所以在 merge 后立刻做类型降级for col in data.select_dtypes(int64).columns: data[col] data[col].astype(int32)逻辑说明int32 范围足够覆盖计数类特征最多能到 21 亿复购场景用不上 int64。跑验证集时内存减下去训练速度也会快一些。参数说明如果某列计数可能超过 20 亿就不要转 int32。实际中几乎不会发生。category 类型也可以用在性别这类基数小的列上能进一步省内存。3. 逻辑回归基线LogisticRegression.py 跑通与参数调整3.1 为什么先选逻辑回归逻辑回归是二分类最稳的基线。复购预测本质是用户-商家对的二分类逻辑回归给出的是概率而不是硬分类方便后续调阈值。另外它线性可分对特征重要性解释性也强。很多人一上来直接上 XGBoost结果特征没构造好效果还不如逻辑回归。先跑基线再上复杂模型这条路线是最省时间的。这个案例的源码里有一个 LogisticRegression.py脚本不大但把读数据、特征工程、训练、评估串起来了。我在拆这个脚本时发现它能在十几行代码里把 AUC 跑到 0.7 左右说明特征工程已经做得比较完整。如果你在自己机器上跑要注意复现时需要保持相同的随机种子和特征顺序否则输出结果会有一点点浮动。3.2 LogisticRegression.py 代码拆解项目里的 LogisticRegression.py 的结构大致是读数据、做特征工程、切分、训练、评估。以下是常见做法和源码逻辑一致import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, accuracy_score from sklearn.model_selection import train_test_split # 假设 data 已由上面的特征工程得到 X data.drop(columns[user_id, merchant_id, label]) y data[label] X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 训练逻辑回归 model LogisticRegression( C1.0, solverliblinear, max_iter100, random_state42 ) model.fit(X_train, y_train) # 验证 y_pred model.predict_proba(X_val)[:, 1] auc roc_auc_score(y_val, y_pred) acc accuracy_score(y_val, (y_pred 0.5).astype(int)) print(fAUC: {auc:.4f}, ACC: {acc:.4f})逻辑说明predict_proba 拿第二列作为正样本概率。直接用 0.5 当阈值只是基线复购场景正样本比例低0.5 不一定最优后面会讲。参数说明C 是正则化强度的倒数C 越小正则越强。数据特征多、共线性高时把 C 调小到 0.1 能减少过拟合。solverliblinear 适合小数据数据量大时换 lbfgs 更省内存。max_iter 不够会出现收敛警告调到 200 或 500 即可。逻辑回归对数值尺度敏感像购买次数和年龄量纲差很远不标准化会让正则化方向跑偏。所以训练前用 StandardScaler 处理数值特征from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val) model.fit(X_train_scaled, y_train) y_pred model.predict_proba(X_val_scaled)[:, 1]逻辑说明fit_transform 在训练集上计算均值和方差并缩放transform 在验证集上只用训练集的参数避免验证集信息进入模型。参数说明如果特征里有 0/1 编码的列要不要标准化看情况。树模型不需要逻辑回归建议标准化。这里有一个小坑StandardScaler 会把稀疏矩阵变成稠密矩阵内存占用上升所以先降级类型再做标准化。3.3 评估指标为什么 AUC 比准确率更重要复购预测里大部分样本是负样本准确率很容易虚高。比如 90% 不购买全预测 0 也有 90% 准确率。所以要看 AUCAUC 衡量的是正样本得分高于负样本的概率不依赖阈值。逻辑回归输出概率后用 roc_auc_score 比较靠谱。如果线上要求的是召回率或 F1那还要在验证集上扫描阈值。我一般会扫描多个阈值找 F1 最大点from sklearn.metrics import f1_score best_th 0.5 best_f1 0 for th in [0.3, 0.4, 0.5, 0.6, 0.7]: pred (y_pred th).astype(int) cur_f1 f1_score(y_val, pred) if cur_f1 best_f1: best_f1 cur_f1 best_th th print(fbest threshold: {best_th}, F1: {best_f1:.4f})逻辑说明阈值扫描这个操作很朴素但能看出模型对正样本的区分能力。复购场景通常更看重少数高价值用户所以要选 F1 最大的阈值。参数说明F1 在正负样本不均时比准确率更能反映真实效果。扫描的阈值步长可以按业务需求调整比如 0.02 一档避免漏掉最优。提示阈值选择不能只看验证集要结合业务成本。如果一次复购带来的利润高阈值可以放宽多召回一些潜在用户。3.4 看系数判读特征方向逻辑回归的系数就是每个特征的权重正系数表示该特征越大越容易复购。随机森林给特征重要性逻辑回归给系数方向两者结合能发现矛盾点。比如某个特征在树模型里重要但在逻辑回归里系数接近 0说明它和标签的关系可能不是线性。coef pd.Series(model.coef_[0], indexX_train.columns) print(coef.sort_values()[:5]) print(coef.sort_values()[::-1][:5])逻辑说明coef_ 是 sklearn 逻辑回归的系数数组只有一行是因为二分类。索引套特征列名输出前几名和后几名就能看到正向和负向特征。参数说明如果用了 StandardScaler这里解析系数要小心它是在标准化尺度上的权重不能直接说“购买次数每增加 1 次概率增加多少”。如果要还原成原始尺度需要除以标准差。4. 随机森林进阶RandomForestClassifier.py 与可视化分析4.1 随机森林在复购预测里的优势逻辑回归的问题是只能捕捉线性关系复购行为很多是分组效应某个商家下高频用户更容易复购这种条件关系需要决策树结构。随机森林通过多棵树投票对非线性特征和特征交互更友好也不用做标准化。缺点是模型变大、解释性弱、容易过拟合。在样本量不太大的场景里随机森林的默认参数往往就能超过调过参的逻辑回归。我拆这个案例里的 RandomForestClassifier.py 时最直接的感觉是它把随机森林的几个关键参数都做了显式指定而不是依赖默认值。这里有一个容易忽略的点随机森林对类别不平衡也有过拟合倾向所以代码里一般会加 class_weightbalanced或者在评估时用 AUC 而不是准确率。4.2 RandomForestClassifier.py 参数调整源码里的随机森林脚本大致是这样from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score rf RandomForestClassifier( n_estimators200, max_depth8, min_samples_leaf10, random_state42, n_jobs-1, class_weightbalanced ) rf.fit(X_train, y_train) y_pred_rf rf.predict_proba(X_val)[:, 1] print(fRF AUC: {roc_auc_score(y_val, y_pred_rf):.4f})逻辑说明n_estimators 是树的数量越多越稳但到 200 后收益递减。max_depth 限制树的深度防止单棵树把训练集背下来。min_samples_leaf 是叶节点最少样本数设大一点能平滑噪声。class_weightbalanced 让少数类得到更高权重缓解复购样本少的问题。参数说明n_jobs-1 表示用满所有 CPU 核心。数据量几十万时200 棵树可能有点慢可以先 50 棵跑通再慢慢加。max_depth 建议从 6 开始用网格搜索找最优。调参可以写网格搜索from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200], max_depth: [6, 8, 10], min_samples_leaf: [5, 10] } gs GridSearchCV( RandomForestClassifier(random_state42, n_jobs-1, class_weightbalanced), param_grid, scoringroc_auc, cv3, verbose1 ) gs.fit(X_train, y_train) print(gs.best_params_)逻辑说明GridSearchCV 直接在验证集上扫描参数比较费时间但数据量小可以接受。scoringroc_auc 保证按 AUC 选模型而不是默认准确率。cv3 是三层交叉验证减少随机切分波动。参数说明random_state42 很重要不固定的话每次调参结果都不同。后面的树特征重要性要用同一个模型。如果数据量大把 n_estimators 列表从 50 开始减少搜索时间。这里要注意网格搜索和前面的 train_test_split 之间的关系。我在拆这个项目时习惯先用默认参数跑一遍记下 AUC再用网格搜索避免过度调参导致验证集过拟合。4.3 可视化分析笔记里的关键图项目里那个分析笔记不是摆设里面有几个图能帮你理解数据。一是购买行为的频率直方图看负样本占比二是复购用户和未复购用户的行为特征对比比如用户总交互数分布三是随机森林特征重要性条形图。特征重要性能直接告诉你哪些特征贡献最大多数情况下“用户购买次数”这类行为特征比年龄性别更靠前。画特征重要性的代码import matplotlib.pyplot as plt importances rf.feature_importances_ features X_train.columns order importances.argsort()[::-1] plt.figure(figsize(8, 6)) plt.barh(range(10), importances[order[:10]][::-1]) plt.yticks(range(10), features[order[:10]][::-1]) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)逻辑说明这个图能回答“为什么这个模型这么重”的问题也能帮你砍掉没用的列减少训练时间。值得注意的是特征重要性在树模型上会偏向取值多的特征所以用来砍特征要谨慎。参数说明argsort()[::-1] 是为了从大到小排序取前 10 画水平条形图。savefig 的 dpi 设 150 输出清晰度足够。如果中文字体显示乱码用plt.rcParams[font.sans-serif] [SimHei]配合plt.rcParams[axes.unicode_minus] False处理。还可以画两个模型的 ROC 对比图直观看出差异from sklearn.metrics import roc_curve fpr_lr, tpr_lr, _ roc_curve(y_val, y_pred) fpr_rf, tpr_rf, _ roc_curve(y_val, y_pred_rf) plt.plot(fpr_lr, tpr_lr, labelLogistic Regression) plt.plot(fpr_rf, tpr_rf, labelRandom Forest) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.legend() plt.savefig(roc_compare.png)逻辑说明ROC 曲线对比比只看 AUC 更有信息量。如果两条曲线交叉说明在不同阈值下各有优势比如随机森林在低误报率区域更好逻辑回归在高召回区域更好。参数说明roc_curve 返回三个值第三个是阈值。曲线下面积就是 AUC所以画图前不用单独算。确保两个模型用的是同一个验证集。5. 避坑与常见问题跑这个案例最容易翻车的五个点5.1 文件编码和中文路径现象pandas 读 CSV 报 UnicodeDecodeError或者模型脚本在别的机器上跑不了。原因原始数据可能是 GBK 或 UTF-8 编码而 Windows 默认编码不一致路径里的中文目录也会让某些库读不到文件。解决统一用 utf-8 读取加 encodingutf-8如果报错再试 encodinggbk。项目文件夹尽量用英文路径不要放在带空格的目录里。我一般会在 read_csv 时显式写 encodingutf-8并在项目根目录放一个依赖清单避免换机器后环境不一致。5.2 测试集特征泄漏现象验证集 AUC 很高但提交到评测集分数很低或者线下和线上差距大。原因在构造特征时误用了全量数据统计比如用同一个 user_id 在训练和验证里都算均值或者用测试集信息填充缺失值。解决严格遵守“先切分再构造特征”的顺序。在时间序列场景中用过去窗口统计特征不能用整个数据集的统计量。代码结构上把所有特征工程包成一个函数在训练集上 fit再 transform 验证集和测试集。如果你发现训练集和验证集 AUC 差得离谱先怀疑特征构造过程里是不是用了未来信息。有一个笨办法把特征重要性排序后看有没有哪个特征明显和业务逻辑对不上比如“评估期购买次数”这种特征出现在训练集里就是泄漏。5.3 类别不平衡把模型带偏现象预测结果全是 0准确率很高但 AUC 很难看。原因复购标签正样本占比往往低于 20%逻辑回归的默认阈值 0.5 会把所有样本判为负。解决用 class_weightbalanced 给少数类加权或者训练后调整阈值。更稳的做法是评估时只看 AUC对模型输出概率做阈值扫描再定最终预测。这个案例里随机森林也可以设置 class_weightbalanced_subsample。我一般会在训练前后各打印一次正样本比例确认 class_weight 是否生效。还可以画 PR 曲线比 ROC 曲线更直观。5.4 内存不足导致训练中断现象处理完特征后内存占用到百分之八九十脚本直接被系统杀掉。原因pandas 默认 int64 和 float64 占用过大groupby 聚合结果太大。解决读入后立刻把不用的列 drop整数列转 int32类别列转 category。如果还不行用分块读取或只取必要的列。代码data[total_actions] data[total_actions].astype(int32) data[gender] data[gender].astype(category)逻辑说明int32 内存占用是 int64 的一半category 类型对低基数字段也很省内存。这两个转换在数据量大时能省出几个 G。参数说明category 列在做 pandas 操作时可能变慢所以在合并且确认不再修改后再转。模型训练前需要把 category 列编码成数值。5.5 模型不收敛或概率输出异常现象逻辑回归报 ConvergenceWarning或者随机森林树深度大得离谱。原因max_iter 太小或特征尺度差异太大随机森林 max_depth 没有限制。解决逻辑回归加 StandardScalermax_iter 调到 500随机森林设置 max_depth 和 min_samples_leaf。如果概率输出全是 0 或 1检查是不是标签列泄漏进了特征最常见的是把评估期行为统计进去了。另外如果逻辑回归输出概率只有 0.5 附近说明模型没有学到有效特征优先检查特征列里是不是混入了 user_id。6. 进阶把复购预测改造成你的完整预测流程6.1 特征交叉与滑动窗口基础模型跑通后我从这份源码里学到最重要的是特征怎么组合。复购预测里用户-商家对的“最近一次购买距离训练截止日期的天数”非常有效代表用户对商家的新鲜感。常见做法是在训练表里取每个 user-merchant 对的最大时间然后用全局最大时间减去它得到 recency。这个特征比单纯购买次数更能抓住短期复购动机。# 构造距离训练截止日的最近购买间隔 train[time] pd.to_datetime(train[time]) last_buy train[train[action_type] 2].groupby( [user_id, merchant_id] )[time].max().reset_index() last_buy[recency] (train[time].max() - last_buy[time]).dt.days data data.merge( last_buy[[user_id, merchant_id, recency]], on[user_id, merchant_id], howleft ) data.fillna({recency: 99999}, inplaceTrue)逻辑说明这里只取购买行为防止只看过没买过的记录干扰。recency 越小说明越近买过复购概率更高。fillna(99999) 代表从未购买过让模型认为这条记录非常久远。参数说明pd.to_datetime 转换失败会变成 NaT先做 dropna 或稍微宽松的处理。如果训练表里没有 action_type 字段可以退化成对全部行为取 max 时间但要注意行为定义的一致性。6.2 验证与提交的最终习惯最后一步是固定流程先切分、再构造特征、然后训练、看图、调阈值。这个项目里源码和笔记已经给了完整链路我建议把你自己的特征加进去后一定重新跑一遍随机森林的特征重要性看新特征有没有排到前面。提交预测的代码也要养成一套自己的模板test_data test.merge(user_info, onuser_id, howleft) test_proba best_model.predict_proba(test_data)[:, 1] sub pd.DataFrame({ user_id: test[user_id], merchant_id: test[merchant_id], probability: test_proba }) sub.to_csv(submission.csv, indexFalse)逻辑说明test 表本身是 user-merchant 对预测概率直接写进提交文件。判断门槛不要写在提交流程里保留概率是一个好习惯因为线上阈值可能和线下不一样。参数说明merge 是 left join保证测试对不丢失。如果测试表里有大量缺失画像概率可能会偏向训练集分布这时候可以用全量用户统计量填充。以后我拿到新的预测需求都会强制先检查三件事标签是否对齐、特征是否泄漏、评估指标是否匹配业务。这套流程都是从跑这个案例里练出来的。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?