简介面向数据挖掘初学者与电商运营分析人员这份案例以随机森林算法为核心探究电商网站推销商品时各因素对销售结果的影响完整演示从数据整理、特征探索到模型训练与结果解读的流程。压缩包共2个文件包含一份电商数据xlsx表格和一份Python脚本xlsx用于存放实验原始数据py则集成数据预处理、随机森林建模与评估逻辑整体仅45KB轻量易用便于直接运行学习。目前已有104人浏览学习。通过该资源可掌握随机森林处理分类/回归问题的基本套路了解电商推销场景下的变量筛选与重要性分析思路脚本与数据文件分离便于对照运行与二次修改可将其迁移到其他促销活动或用户行为数据集是课程设计、毕业设计或入门实战的良好参考。1. 大数据分析案例里随机森林算法解决的是电商推销的什么死结大数据分析案例里随机森林算法最常见的一个应用场景不是预测销量而是做电商网站的推销商品影响因素探查。运营复盘时经常出现这样的拉扯大促前发了一波满减券系统推送触达几十万人成交却只涨几个点。老板问为什么运营说折扣不够狠渠道说用户不精准数据部门丢出一张PV/UV表谁也说服不了谁。这种拉扯的背后是没有把用户、商品、渠道、时间放到同一个模型里量化比较。随机森林在这里做的事就是把这一堆变量丢进去输出各因素对“是否购买”的贡献度排名再配合偏依赖图告诉运营往哪个方向使劲。这类题目也常出现在大数据分析与挖掘的案例习题里适合电商运营、数据分析师以及正在做大数据分析课程设计的人照着做就能把订单明细变成可解释的结论。2. 电商网站推销商品的X和Y先把业务问题翻译成随机森林能吃的表格很多人在拿到这种“影响因素分析”类题目时第一反应是直接调库训练。这样跑出来的模型往往汇报了一堆特征重要性业务方看完却不知道怎么用。问题不在算法而在于从业务问题到监督学习表格这一步没做扎实。随机森林本身是个黑匣子但它吃进去的每一列都是白盒表格怎么设计决定了最后能回答什么问题。2.1 标签Y的口径7日支付、退款剔除、未来变量隔离随机森林属于监督学习第一步永远不是调参而是定义 Y。在电商推销场景里最常用的 Y 是“是否购买”的二分类变量但这个二分类的口径必须收敛否则后面所有重要性分析都会跟着漂。我一般会先定三个口径。第一是时间窗口推送之后几天内下单算成交7天是比较稳的窗口太短会漏掉犹豫型用户太长会把自然流量成交误算成推销效果。第二是退款单推送后用户下单但退了款算不算影响因素分析里的正样本我建议先把它从正样本里剔除或者单独打一个标签不要混在一起训练。第三是中间行为点击、加购能不能当 Y不建议。点击和加购是“好奇心”不是“购买力”业务方真正关心的是成交是钱有没有进账。还有一个更隐蔽的坑不能把 Y 的未来结果放进 X。比如你已经知道这笔订单最终支付成功然后把“支付金额”放进特征里这叫数据穿越。随机森林对泄露数据的拟合能力很强训练集分数会异常好看但真实场景里这些特征根本不存在。判断标准很简单所有特征必须是在推送发生那一刻之前就能拿到的信息。2.2 特征X用户、商品、渠道、时间四类变量和编码方式用户类特征会员等级、历史消费金额、近30天购买次数、品类偏好。这类特征回答“这个用户值不值得推”。商品类特征折扣率、原价区间、商品类目、近30天销量、评分。这类特征回答“这个商品有没有吸引力”。渠道类特征短信、APP推送、站内信、公众号模板消息。回答“从哪个入口触达更有效”。时间类特征推送时段、星期几、距上次购买天数。回答“什么时候推更容易成交”。特征体系确定之后真正容易翻车的是编码方式。树模型虽然对数值大小不敏感但对特征内部的顺序关系是敏感的。会员等级这类有序分类直接映射成 1、2、3、4 没问题树能利用“高等级用户”这种切分。但推送渠道这类无序分类千万不要用 1、2、3 编码否则模型会强行认为“短信”和“APP推送”之间存在数值大小关系这是没有业务依据的。import pandas as pd # 假设你已经把订单、用户、推送记录合并成一张明细表 df_raw df_raw pd.read_csv(promotion_data.csv, parse_dates[push_time, order_time]) # Y推送后 7 天内是否支付且剔除退款订单 df_raw[is_buy] ((df_raw[order_time] - df_raw[push_time]).dt.days 7) (df_raw[refund_flag] 0) df_raw[is_buy] df_raw[is_buy].astype(int) # 会员等级有序分类直接用字典映射成数字 level_map {L1: 1, L2: 2, L3: 3, L4: 4} df_raw[member_level] df_raw[member_level].map(level_map) # 推送渠道无序分类用 one-hot禁止直接写成 1/2/3 df_raw pd.get_dummies(df_raw, columns[push_channel], prefixchannel) # 时间特征拆出小时和星期几星期几用 0-6 df_raw[push_hour] df_raw[push_time].dt.hour df_raw[push_weekday] df_raw[push_time].dt.weekday # 折扣率注意原价可能为 0 或空直接除会得到 inf 或 NaN df_raw[discount_rate] (df_raw[origin_price] - df_raw[pay_price]) / df_raw[origin_price] df_raw[discount_rate] df_raw[discount_rate].replace([float(inf), -float(inf)], 0).fillna(0)这段代码有几个参数值得说明。is_buy用了refund_flag 0的条件目的是把退款单排除在正样本外。parse_dates保证了时间列被解析成 datetime 类型后面dt.days、dt.hour才能正常工作。discount_rate的替换逻辑很重要原价为 0 的脏数据在除法中会产生 inf如果不处理树模型会把 inf 当成一个极大值单独切分直接干扰特征重要性。2.3 正负样本失衡欠采样还是class_weight以及时间切分的必要性电商推销的购买转化率通常只有 2% 到 5%也就是说一张十万级的样本表里负样本占了绝大多数。如果不做任何处理树在分裂的时候会拼命去拟合“绝大多数”特征重要性会偏向“识别不购买的人”而不是“区分购买的人”。常见做法有两个。一是对负样本做欠采样让它变成正样本的 3 倍左右二是用class_weightbalanced。我的建议是优先用class_weight因为它不会改变样本分布只是给少数类更高的惩罚权重特征重要性的稳定性更好。欠采样会直接改变先验分布同一批特征在不同随机种子下算出来的重要性可能差很多你得多次欠采样取平均才靠谱。from sklearn.utils import resample # 先看一眼分布确认失衡程度 print(df_raw[is_buy].value_counts(normalizeTrue)) # 如果一定要用欠采样负样本控制在正样本 3 倍以内 df_pos df_raw[df_raw[is_buy] 1] df_neg df_raw[df_raw[is_buy] 0] # sample 里的 n 是目标数量这里取正样本的 3 倍 df_neg_down resample(df_neg, replaceFalse, n_sampleslen(df_pos) * 3, random_state42) df_bal pd.concat([df_pos, df_neg_down]).sample(frac1, random_state42)这里的n_sampleslen(df_pos) * 3是经验值3 倍是我反复试下来比较稳的比例。低于 2 倍会让模型对正样本极度敏感出现大量误判高于 5 倍又回到失衡状态重要性重新偏向负样本。还有一个更关键的问题切分方式。这种案例的数据往往横跨多个促销周期如果你用train_test_split里的默认随机切分同一个促销周期的样本会同时出现在训练集和测试集里模型记住的是“这一轮促销的周期特征”而不是真正的规律。正确做法是按时间顺序切分前 80% 的时间窗口做训练后 20% 做验证。提示先做特征后切分。切分之后再做编码会导致验证集信息泄露到训练集里这个顺序错了后面所有评估都是虚的。3. 用sklearn在本地跑通随机森林最小模型与重要性读取表格造好之后随机森林的训练代码其实非常短。这一步的目标不是追求最高精度而是先把一条完整链路跑通训练、评估、读重要性、看偏依赖。链路通了后面换参数、换特征、换模型才有参照物。3.1 一分钟跑通的最小训练代码from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split # 特征列数值特征 one-hot 之后的渠道列 feature_cols [member_level, discount_rate, push_hour, push_weekday] \ [c for c in df_bal.columns if c.startswith(channel_)] X df_bal[feature_cols] y df_bal[is_buy] # 按时间切分避免同一促销周期的数据两边都出现 split_idx int(len(df_bal) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] clf RandomForestClassifier( n_estimators500, # 树的数量500 比默认 100 更稳 max_depth8, # 限制单棵树深度防过拟合 min_samples_leaf20, # 叶子节点最少 20 个样本 max_featuressqrt, # 每次分裂只看 sqrt(特征数) 个特征 oob_scoreTrue, # 开启袋外打分随机森林的免费验证 n_jobs-1, # 用满所有 CPU 核 random_state42, # 固定种子否则结果每次都不一样 ) clf.fit(X_train, y_train) print(OOB score:, clf.oob_score_) print(Test accuracy:, clf.score(X_test, y_test))这里的参数可以拆开说。n_estimators500不是越大越好超过 800 之后收益递减明显训练时间却线性增长500 是效果和成本的平衡点。max_depth8在电商这种特征数量 20 到 50 的场景下够用太深容易把单个用户的噪声行为学进去。min_samples_leaf20是树模型里最值得调的剪枝参数它比max_depth更温和可以让分支在样本量不足时自动停止。max_featuressqrt是分类任务默认推荐值它强迫每棵树用不同的特征子集分裂增加树间多样性。3.2 特征重要性怎么看排序只是第一步import matplotlib.pyplot as plt import pandas as pd # 把特征重要性转成 DataFrame按数值降序排列 importance_df pd.DataFrame({ feature: feature_cols, importance: clf.feature_importances_, }).sort_values(importance, ascendingFalse) print(importance_df.head(10)) # 画水平条形图取前 10 个特征 importance_df.head(10).plot.barh(xfeature, yimportance) plt.gca().invert_yaxis() plt.show()这段代码输出的importance是 sklearn 基于分裂增益累计得到的归一化值所有特征加起来等于 1。它的含义是“这个特征在树分裂时贡献了多少纯度提升”数值越高说明模型越依赖它。但要注意这个值是在训练集上算出来的高基数特征会虚高所以它只能作为初筛不能作为最终结论。更可靠的复核方式是后面要讲的置换重要性。3.3 OOB分数和交叉验证为什么两个都看随机森林有个其他模型没有的免费福利每棵树大约只用 63% 的样本训练剩下 37% 的样本没被这棵树见过。把所有树对“自己没用过”的样本预测结果汇总就能得到一个不依赖单独测试集的分数这就是oob_score。但 OOB 有个弱点它对数据泄露不敏感。如果你不小心把未来信息放进了特征OOB 分数照样会很高因为它本质上是“模型在训练数据内部的泛化估计”。所以我在这个案例里坚持两个都看OOB 做初步判断交叉验证做最终确认。from sklearn.model_selection import cross_val_score # 按时间窗口做 5 折的 cross_val_score # cv 传 5 表示均匀切 5 份但这里必须用 TimeSeriesSplit 才能保证时间顺序 from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) scores cross_val_score(clf, X, y, cvtscv, scoringroc_auc) print(TimeSeriesSplit AUC:, scores) print(Mean AUC:, scores.mean())TimeSeriesSplit会把数据按时间顺序切成连续训练块每次验证都在更晚的时间窗口上做这才符合“用历史预测未来”的真实业务场景。注意这里用的是roc_auc而不是默认的 accuracy因为推销场景里正负样本天然不平衡accuracy 在转化率 3% 的数据上动辄 97%没有参考价值。4. 随机森林回归算法什么时候换上来回归/分类边界与3个必调参数很多人在做“影响因素”类题目时默认就上分类器Y 定为“买/不买”。但电商推销的目标往往还有一个维度客单价。如果老板问的是“怎么让推销带来的 GMV 更大”那就需要随机森林回归算法出场了。这个边界不搞清楚模型结论可能只回答了一半问题。4.1 从“是否购买”到“客单价”回归和分类的边界选择随机森林回归和分类的核心区别只在于 Y 的类型和损失函数。分类的 Y 是离散的 0/1分裂时用基尼系数或信息增益回归的 Y 是连续金额分裂时用均方误差。业务含义也不同分类告诉你“哪些人群更容易被推销打动”回归告诉你“被打动的人愿意花多少钱”。两个一乘才是 GMV 的完整影响因素。实际项目里我会先跑分类把“买不买”的影响因素排序跑出来再跑回归把“买多少”的影响因素排序跑出来。如果回归结果的 top 特征和分类完全不一样这本身就是个有挖掘价值的发现比如促销节点会刺激新客首单但老客的客单价主要由品类偏好驱动。from sklearn.ensemble import RandomForestRegressor # 回归版的特征表可以复用但 Y 换成实际支付金额退款单剔除 y_reg df_bal.loc[df_bal[is_buy] 1, pay_price] X_reg df_bal.loc[df_bal[is_buy] 1, feature_cols] reg RandomForestRegressor( n_estimators500, max_depthNone, # 回归场景通常比分类更深一点 min_samples_leaf15, max_features1.0, # 回归推荐用全部特征而不是 sqrt n_jobs-1, random_state42, ) reg.fit(X_reg, y_reg)回归模型的参数和分类有两处不一样。max_depth我没有限制因为回归的 Y 是连续值树不容易被单个极端样本带偏反而需要更深的树去捕捉价格分段。max_features1.0意思是每次分裂看全部特征回归里sqrt效果反而不稳因为连续 Y 对特征组合的依赖更强。4.2 3个必调参数n_estimators、max_depth、min_samples_leaf网上到处是“随机森林玄学调参大全”晒出一堆参数组合放到自己业务数据上就翻车。我在电商推销这个场景下反复验证后真正值得手工调的参数只有三个。参数默认值我的常规区间调参作用n_estimators100300 到 800越大方差越小但超过 1000 边际收益极低max_depthNone6 到 12分类限制树深防止记住单个用户的噪声行为min_samples_leaf110 到 30叶子最少样本数最温和有效的剪枝手段调参顺序很关键别一上来就网格搜索。我一般先固定random_state42用 OOB 分数观察min_samples_leaf从 1 到 30 的变化通常曲线会在某个区间出现拐点选拐点附近的值。再固定它去调max_depth最后才调n_estimators。random_state42必须放在第一位不固定的话模型每次结果都不同你连“这个参数改了到底是变好还是变差”都判断不了。4.3 用学习曲线确认参数真的有效# 对比不同 min_samples_leaf 下的 OOB 和测试集表现 import numpy as np leaf_values [1, 5, 10, 20, 30] results [] for leaf in leaf_values: clf_tmp RandomForestClassifier( n_estimators500, max_depth8, min_samples_leafleaf, oob_scoreTrue, random_state42, n_jobs-1, ) clf_tmp.fit(X_train, y_train) results.append({ min_samples_leaf: leaf, oob: clf_tmp.oob_score_, test_auc: roc_auc_score(y_test, clf_tmp.predict_proba(X_test)[:, 1]), }) print(pd.DataFrame(results))判断标准很简单OOB 和测试集分数同时上升或持平说明这个参数确实有效如果 OOB 上升但测试集下降那是过拟合参数方向和业务目标背离如果两者都不动说明这个特征在你的数据上根本不敏感别在这个参数上浪费更多时间。我见过不少人在max_depthNone和max_depth50之间反复横跳最后发现差距只有 0.001反而把精力浪费掉了。5. 避坑随机森林在电商推销数据上翻车的6条踩坑记录随机森林以“不容易过拟合、上手快”著称但恰恰因为它太好用了很多人忽略了业务数据里的脏坑。以下这几条都是我在类似“推销商品影响因素”案例里亲眼见过或者自己踩过的全部按“现象、原因、解决”的路径写好可以直接对照排查。5.1 踩坑用户ID进了特征重要性直接霸榜现象特征重要性第一名是 user_id数值高出第二名一大截。原因高基数特征有上百个取值树可以反复按 ID 切分几乎为每个样本单独开一个叶子分裂增益虚高。解决建模前剔除所有 ID 类字段。如果想保留用户维度信息把 ID 替换成聚合特征比如“近30天购买次数”“平均客单价”“距上次购买天数”。5.2 踩坑把“是否已发货”当成特征训练分数虚高现象训练 AUC 0.98测试 AUC 0.92业务应用时发现根本没这回事。原因发货发生在支付之后属于 Y 产生后才存在的信息模型等于看到了答案。解决所有特征必须截止到推送那一刻。构建特征时用推送时间作为截止线推送之后的任何操作记录都不得进入特征表。5.3 踩坑随机切分训练集和测试集模型记住促销周期现象测试集精度很高但换到下一个促销周期再验证效果明显下滑。原因同一个促销周期的样本被随机分到了两边模型学到的是这一轮周期的特殊模式而不是通用规律。解决改用TimeSeriesSplit或者手动按时间比例切分前 80% 训练、后 20% 验证模拟真实的对未来预测。5.4 踩坑手动欠采样之后特征重要性排名反复变化现象同一批数据换一个随机种子欠采样top 特征顺序就变了。原因欠采样改变了模型看到的先验分布树分裂时的纯度计算跟着漂移重要性自然不稳定。解决优先改用class_weightbalanced不动样本分布如果业务必须欠采样就多次跑不同随机种子把多次重要性取平均而不是相信单次结果。5.5 踩坑只报一份特征重要性表业务方问“那几点发短信”答不上来现象你输出了一张排名表“推送时段”排第三但运营追问到底几点发最有效你哑住了。原因特征重要性只告诉你影响大小不告诉你影响方向和幅度这个值是正还是负、有没有拐点一张排序表完全看不出来。解决用部分依赖图配合解释看“推送时段”在几点达到转化峰值这个图比重要性表更适合向业务汇报。5.6 踩坑用accuracy评估推销模型正样本召回率几乎为0现象模型 accuracy 97%但推到真实场景里出单很少。原因转化率只有 3%模型只要全预测“不买”就能拿到 97% 的正确率它对正样本完全没识别能力。解决推销场景统一用 AUC 和召回率评估特别是在预算有限时应该在保证召回率达到某个阈值的前提下再看精确率。6. 把影响因素落进推销方案重要性排序之外的方向与幅度特征重要性是这个案例的核心产出但它只能回答“哪个因素影响大”回答不了“往哪个方向使劲”。折扣率重要性排第一那到底是打八折有效还是打五折有效推送时段排第二那是上午十点发还是晚上八点发这一步不解决前面所有分析到业务手里都还是一张废纸。我的做法是给每个进入 top5 的特征补一张部分依赖图。部分依赖图展示的是“当某个特征变化时模型预测的购买概率如何变化”它能把特征重要性和业务动作直接挂钩。from sklearn.inspection import PartialDependenceDisplay import matplotlib.pyplot as plt # discount_rate 对 is_buy 的影响方向和拐点 PartialDependenceDisplay.from_estimator(clf, X_train, [discount_rate], kindaverage) plt.show() # push_hour 对 is_buy 的影响看几点转化率最高 PartialDependenceDisplay.from_estimator(clf, X_train, [push_hour], kindaverage) plt.show()从图里你一眼就能看出折扣率在 0.8 到 0.9 区间购买概率陡增超过 0.9 之后趋于平缓甚至回落这说明八五折到九折是转化率杠杆最强的区间再往下让利就是浪费毛利推送时段在 19 点到 21 点出现峰值白天和深夜效果都差。有了这两条运营的推销方案就变成了每晚 20 点给 L3 以上会员推八八折商品券而不是笼统地“多发券、多推送”。最后还有一件必须做的事拿真实流量做一次 A/B 验证。随机森林在这里的角色是假设生成器它告诉你“折扣率和时段可能有交互作用”但要不要上线、能不能放量取决于接下来两周的小流量验证。因为模型是用历史数据训练的历史里的用户行为和下一次大促的用户心态不一定完全一样。我自己的血泪教训是早年间做这类案例只交一份特征重要性表被业务方连续追问“所以呢”之后才彻底把部分依赖图变成标准交付物。随机森林擅长给结论但只有把结论翻译成“几点发、打几折、发给谁”的可执行动作这个大数据分析案例才算真正做完。数据也好模型也罢最终都是为了帮运营少做几次拍脑袋决定希望帮到你。本文还有配套的精品资源点击获取
