☰
随机森林算法在电商推销影响因素分析中的实战指南
2026/9/26 23:41:22 网站建设 项目流程

简介:面向数据挖掘初学者与电商运营分析人员,这份案例以随机森林算法为核心,探究电商网站推销商品时各因素对销售结果的影响,完整演示从数据整理、特征探索到模型训练与结果解读的流程。压缩包共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'], prefix='channel') # 时间特征:拆出小时和星期几,星期几用 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_weight='balanced'。我的建议是优先用class_weight,因为它不会改变样本分布,只是给少数类更高的惩罚权重,特征重要性的稳定性更好。欠采样会直接改变先验分布,同一批特征在不同随机种子下算出来的重要性可能差很多,你得多次欠采样取平均才靠谱。

from sklearn.utils import resample # 先看一眼分布,确认失衡程度 print(df_raw['is_buy'].value_counts(normalize=True)) # 如果一定要用欠采样,负样本控制在正样本 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, replace=False, n_samples=len(df_pos) * 3, random_state=42) df_bal = pd.concat([df_pos, df_neg_down]).sample(frac=1, random_state=42)

这里的n_samples=len(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_estimators=500, # 树的数量,500 比默认 100 更稳 max_depth=8, # 限制单棵树深度,防过拟合 min_samples_leaf=20, # 叶子节点最少 20 个样本 max_features='sqrt', # 每次分裂只看 sqrt(特征数) 个特征 oob_score=True, # 开启袋外打分,随机森林的免费验证 n_jobs=-1, # 用满所有 CPU 核 random_state=42, # 固定种子,否则结果每次都不一样 ) clf.fit(X_train, y_train) print('OOB score:', clf.oob_score_) print('Test accuracy:', clf.score(X_test, y_test))

这里的参数可以拆开说。n_estimators=500不是越大越好,超过 800 之后收益递减明显,训练时间却线性增长,500 是效果和成本的平衡点。max_depth=8在电商这种特征数量 20 到 50 的场景下够用,太深容易把单个用户的噪声行为学进去。min_samples_leaf=20是树模型里最值得调的剪枝参数,它比max_depth更温和,可以让分支在样本量不足时自动停止。max_features='sqrt'是分类任务默认推荐值,它强迫每棵树用不同的特征子集分裂,增加树间多样性。

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', ascending=False) print(importance_df.head(10)) # 画水平条形图,取前 10 个特征 importance_df.head(10).plot.barh(x='feature', y='importance') 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_splits=5) scores = cross_val_score(clf, X, y, cv=tscv, scoring='roc_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_estimators=500, max_depth=None, # 回归场景通常比分类更深一点 min_samples_leaf=15, max_features=1.0, # 回归推荐用全部特征,而不是 sqrt n_jobs=-1, random_state=42, ) reg.fit(X_reg, y_reg)

回归模型的参数和分类有两处不一样。max_depth我没有限制,因为回归的 Y 是连续值,树不容易被单个极端样本带偏,反而需要更深的树去捕捉价格分段。max_features=1.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_state=42,用 OOB 分数观察min_samples_leaf从 1 到 30 的变化,通常曲线会在某个区间出现拐点,选拐点附近的值。再固定它去调max_depth,最后才调n_estimators。random_state=42必须放在第一位,不固定的话模型每次结果都不同,你连“这个参数改了到底是变好还是变差”都判断不了。

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_estimators=500, max_depth=8, min_samples_leaf=leaf, oob_score=True, random_state=42, 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_depth=None和max_depth=50之间反复横跳,最后发现差距只有 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_weight='balanced',不动样本分布;如果业务必须欠采样,就多次跑不同随机种子,把多次重要性取平均,而不是相信单次结果。

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'], kind='average') plt.show() # push_hour 对 is_buy 的影响,看几点转化率最高 PartialDependenceDisplay.from_estimator(clf, X_train, ['push_hour'], kind='average') plt.show()

从图里你一眼就能看出:折扣率在 0.8 到 0.9 区间,购买概率陡增,超过 0.9 之后趋于平缓甚至回落,这说明八五折到九折是转化率杠杆最强的区间,再往下让利就是浪费毛利;推送时段在 19 点到 21 点出现峰值,白天和深夜效果都差。有了这两条,运营的推销方案就变成了:每晚 20 点给 L3 以上会员推八八折商品券,而不是笼统地“多发券、多推送”。

最后还有一件必须做的事:拿真实流量做一次 A/B 验证。随机森林在这里的角色是假设生成器,它告诉你“折扣率和时段可能有交互作用”,但要不要上线、能不能放量,取决于接下来两周的小流量验证。因为模型是用历史数据训练的,历史里的用户行为和下一次大促的用户心态不一定完全一样。

我自己的血泪教训是,早年间做这类案例只交一份特征重要性表,被业务方连续追问“所以呢”之后,才彻底把部分依赖图变成标准交付物。随机森林擅长给结论,但只有把结论翻译成“几点发、打几折、发给谁”的可执行动作,这个大数据分析案例才算真正做完。数据也好,模型也罢,最终都是为了帮运营少做几次拍脑袋决定,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询