简介:面向参与算法竞赛、完成课程设计或毕业设计的计算机相关专业学生,这份资源实现了京东JData算法大赛“高潜用户购买意向预测”的完整流程。从数据加载、正负样本合并、特征自动化生成,到分类模型构建与效果测试,均有对应源码支撑,可直接运行并修改复用。压缩包共24个文件,核心是11个Python脚本和9个编译后的pyc字节码,另附项目说明Markdown文档、运行教程txt以及一个原始代码备份zip,总大小约92KB,结构清晰、轻量易用。目前已有176人学习使用,适合具备Python基础、希望复现经典机器学习比赛思路的读者。代码中同时提供随机森林与GBDT两种模型实现,并包含特征工程、数据集划分等模块,配合教程可完整走通从数据预处理到模型评估的全过程;原始备份包还能辅助对比不同版本代码,便于二次开发和拓展迁移。
1. 高潜用户购买意向预测:一套能直接跑通的比赛源码与三个落地价值
如果你正在找一份关于高潜用户购买意向预测的完整项目源码,用来做课程设计、毕业设计或者算法比赛练手,这套来自某电商平台算法大赛的代码包很值得拆一遍。它不是那种只放几个 PPT 截图的教学 Demo,而是从数据加载、正负样本构造、特征生成、模型训练到验证脚本全部齐全的真实项目。最直接的价值有三个:第一,代码能跑通,省去你从零搭建环境的痛苦;第二,里面的特征工程思路可以迁移到其他用户行为预测任务;第三,源码里的时间窗口和安全过滤设计,恰恰是很多新手容易忽略的“隐藏考点”。无论你是想交一份课设作业,还是想系统学习二分类建模流程,这份项目源码都能给你一个扎实的起点。
2. 赛题理解与数据拆解:先搞清楚“预测购买”到底在预测什么
2.1 赛题数据长什么样:行为表、用户表、商品表的字段边界
打开代码包,最先看到的是 load_data.py 和 generate_dataSet.py。这两个脚本负责把原始数据从文本文件读进来,转换成后续特征工程能直接用的 DataFrame。这类电商行为赛题的数据结构通常比较固定:一张用户信息表、一张商品信息表、一张用户行为日志表。用户信息表里常见字段是用户年龄、性别、城市等级、注册时长;商品表里则是商品品类、价格、上线时间;行为日志表是数据量最大的部分,记录用户对每个商品的浏览、加购、下单、关注等操作及对应时间戳。
我复现这份项目源码时,第一步不是急着跑模型,而是先跑 load_data.py 打印数据形状和字段列表。借用代码包里自带的“运行教程-仅供参考.txt”,你可以看到它建议的流程是先确认数据列名,再合并用户表和商品表。这里有个很容易踩的细节:行为日志里的“购买”和“加购”字段,定义并不一样。源码里的 generate_dataSet.py 在构造标签时只把“下单”行为标记为正样本,没有把加购算进去。如果你自己改造数据,一定要保持这个口径,否则模型会把“加购但没买”的用户当成已购买,验证集上分数虚高,提交后立刻被打回原形。
数据量方面,这类赛题的行为日志动辄几百万行,直接全量读进内存会比较吃紧。我一般会在 load_data.py 里给列指定 dtype,比如把 user_id 和 sku_id 设成 int32,把 action_type 设成 category,能在读取阶段省下三分之一内存。源码里虽然没有完全优化这一步,但结构上已经预留了字段过滤的位置,你可以自己加几行代码。
2.2 正负样本构造:直接拿原始订单做标签的翻车现场
如果你直接拿“是否发生过订单”当二分类标签,通常会遇到两个问题。第一是极度不平衡,电商场景里一段时间内下单用户占全部活跃用户的比例可能只有 3%~5%,模型只要全部预测负样本,准确率就能到 95% 以上,但这毫无意义。第二是样本选择的偏差,所有未购买用户都当负样本也不对,因为有些用户可能只是暂时没买,但他的行为特征与购买用户非常接近,模型很难区分。
代码包里专门安排了 combine_neg_pos 这个环节。它的作用是把正样本和负样本按比例组合,生成一个用于训练的比较均衡的数据集。典型做法是从未购买用户中随机抽样,让正负比例接近 1:2 到 1:4。源码里 combine_feature_dataSet.py 的核心逻辑大致如下:
# combine_feature_dataSet.py 关键片段(简化) import pandas as pd def combine_neg_pos(pos_df, neg_df, ratio=3, random_state=42): """ 正负样本拼接 :param pos_df: 正样本 DataFrame :param neg_df: 负样本 DataFrame :param ratio: 负样本数量 = 正样本数量 * ratio :param random_state: 固定随机种子,保证可复现 """ pos_count = len(pos_df) neg_sample = neg_df.sample(n=pos_count * ratio, random_state=random_state) df = pd.concat([pos_df, neg_sample], axis=0).reset_index(drop=True) # 打乱顺序,防止同类样本堆在一起 df = df.sample(frac=1, random_state=random_state).reset_index(drop=True) return df这里的 random_state 必须固定,否则每次运行抽出的负样本都不一样,模型结果就没法复现和调参。ratio 参数的调整逻辑是:如果业务上希望更多召回,可以把 ratio 降小一些;如果希望精确率更高,就把 ratio 加大。我一般先设成 3,跑通全流程后,再在验证集上扫一遍 ratio 的备选值,比如 1、2、3、5,选出 AUC 最高的那个。要注意的是,在验证集上评估时使用的负样本也要用同样的 ratio 从验证数据里抽取,不能直接用测试集全量数据,否则评估口径不一致。
2.3 数据预处理:缺失值、类型转换和时间字段的统一
跑通 load_data.py 之后,另一个必修动作是处理缺失值和时间格式。用户表里经常出现年龄或性别为空的情况,商品表里会有价格为零的异常记录。源码里对这种字段的处理比较简单,直接填充默认值。但我在实际使用时会稍微调整:对于类别型缺失字段(比如性别),用众数填充;对于连续型缺失字段(比如用户等级分),用中位数填充,避免均值被极端值带偏。
时间字段需要单独注意。行为日志里的时间戳一般是字符串,读进来之后要统一转成 datetime 类型。代码包里的 generate_feature_1.py 依赖“最近 N 天”这类时间窗口特征,如果时间字段不是 datetime 类型,做时间差计算就会报错或者算出负数。我建议在 load_data.py 末尾统一加上这样一段:
# 时间字段统一处理 df['behavior_time'] = pd.to_datetime(df['behavior_time'], format='%Y-%m-%d %H:%M:%S') df['behavior_date'] = df['behavior_time'].dt.date注意这里的 format 要和你手里的数据实际格式一致,否则会解析失败。如果数据里混有非法时间字符串,可以先加 errors='coerce',把解析不了的行变成 NaT 再单独处理,而不是让整个脚本中断。
数据预处理还有一个容易忽略的点:用户表、商品表、行为表合并时可能会产生笛卡尔积,导致行数爆炸。正确做法是先按 user_id + sku_id 去重行为日志,再决定是否与商品表左连接。源码里的 generate_dataSet.py 已经做了这个动作,但如果你自己修改了读取逻辑,一定要保留这一步,否则特征生成阶段内存会直接溢出。
3. 特征工程代码拆解:从行为日志到建模特征的完整链路
3.1 从原始数据到宽表:combine_feature_dataSet.py 在装配什么
特征工程是整个项目里最值钱的部分。代码包里 main_generate_feature.py 相当于流水线总控,它会把前面生成的样本表和后面生成的特征表拼接起来。combine_feature_dataSet.py 的名字有点绕,但它实际做的是把正负样本组合后,再与用户静态特征、商品静态特征进行拼接,最终形成一张“一行一个样本、每列一个特征”的宽表。
这张宽表的每一行都对应一个 user-sku 组合,也就是“某个用户对某个商品是否购买”。列则包括用户基础特征、商品基础特征,以及后续生成的行为统计特征。我复现的时候,单独跑这一步最容易遇到的问题就是内存。如果特征列太多,比如超过两百列,DataFrame 的每个列都要占内存,最后可能直接 OOM。解决思路是:先把用户静态特征和商品静态特征分别存成字典或稀疏格式,等行为特征生成后再统一合并,不要一开始就把所有表 join 到底。
下面这段代码展示了这种合并思路的骨架:
# 合并静态特征的简化逻辑 static_user = pd.read_csv('data/user_info.csv') static_item = pd.read_csv('data/item_info.csv') sample = pd.read_csv('data/train_sample.csv') # 左连接用户特征,保留所有样本行 df = sample.merge(static_user, on='user_id', how='left') # 再连接商品特征 df = df.merge(static_item, on='sku_id', how='left')这里的 merge 顺序会影响最终行数,必须用 how='left' 以样本表为主表。如果用了 inner,用户表或商品表里缺失的记录会被直接删掉,训练集就少了一批样本。我在 mock 数据上试过,inner join 会让最终样本数减少 15% 左右,对模型影响很大。
3.2 时间窗口统计特征:generate_feature_1.py 里的核心参数与计算公式
generate_feature_1.py 负责生成用户维度的行为统计特征。它的核心是“在某个时间窗口内,用户的行为次数和频率”。比如:最近 7 天浏览次数、最近 7 天加购次数、最近 7 天购买次数,以及最近一次行为距离标签时间的天数。窗口大小的选择直接决定了特征对新行为的敏感程度:窗口越短,特征越激进,可能只捕捉到临时性行为;窗口越长,特征越稳定,但容易把很久以前的无效行为也统计进来。
代码里通常用一个循环来生成多窗口特征:
# generate_feature_1.py 简化片段 def gen_time_window_features(behavior_df, anchor_time, windows=[3, 7, 14]): """ 生成时间窗口内的行为计数 :param behavior_df: 行为日志 :param anchor_time: 截止时间,之后的数据不允许出现在特征里 :param windows: 窗口天数列表 """ results = [] for w in windows: start_time = anchor_time - pd.Timedelta(days=w) mask = (behavior_df['behavior_time'] >= start_time) & \ (behavior_df['behavior_time'] < anchor_time) window_df = behavior_df[mask].groupby('user_id')['behavior_time'] \ .count().rename(f'act_count_{w}d') results.append(window_df) feature_df = pd.concat(results, axis=1) return feature_df这里最关键的是 anchor_time,它是样本的标签截止时间。特征生成时必须保证只使用这个时间之前的行为,否则就会造成时间穿越。我在复现这份源码时,特意在训练集和测试集上分别跑了一次,确认 anchor_time 不同,测试集不能使用来自训练集未来的信息。windows 参数我建议从 [3, 7, 14] 开始,如果数据时间跨度更长,再增加 30 天窗口。窗口算出来的特征会存在大量 0 值,因为不是每个用户每天都有行为,这没关系,树模型能自己处理这个稀疏性。
3.3 交叉特征与物品维度:generate_feature_2.py 强化了什么
generate_feature_2.py 更多是生成用户与商品之间的交互特征。比如用户在某商品品类下的购买次数、该商品被多少个不同用户浏览过、用户平均浏览该商品多少次。这些特征对预测“用户会不会买当前这个商品”特别有帮助。因为单纯统计用户整体行为,无法反映他对这个具体商品的态度。
典型实现是:
# generate_feature_2.py 简化片段 # 商品维度:被不同用户浏览的分散度 item_view_diversity = behavior_df.groupby('sku_id')['user_id'].nunique() df['item_view_diversity'] = df['sku_id'].map(item_view_diversity) # 用户-品类偏好:用户在该品类下的行为占比 user_cat_count = behavior_df.groupby(['user_id', 'cate_id'])['behavior_time'].count() user_cat_norm = user_cat_count / user_cat_count.groupby('user_id').transform('sum') df['user_cat_pref'] = df.apply(lambda r: user_cat_norm.get((r['user_id'], r['cate_id']), 0), axis=1)这段代码里的 user_cat_pref 计算出来是用户对某个品类的偏好占比,取值范围在 0 到 1 之间。它能直接反映用户是不是只在这个品类下买东西。但注意,用 apply 逐行取值在数据量大时非常慢,我一般会先把 user_cat_norm 转成字典,再用 map 方式批量赋值,速度能提升一个量级。这也是源码里可以自己优化的地方之一。
3.4 主控脚本与运行顺序:先跑哪个后跑哪个才不会互相覆盖
main_generate_feature.py 是总入口,但它的执行顺序隐藏着一个坑。如果直接运行它,它会自动调用 generate_dataSet.py;而 generate_dataSet.py 生成样本需要先读取已经预处理好的行为日志,这个日志又由 load_data.py 生成。也就是说,正确顺序是:
- 运行 load_data.py 生成 base_data
- 运行 generate_dataSet.py 生成训练样本
- 运行 combine_feature_dataSet.py 拼上静态特征
- 运行 generate_feature_1.py 和 generate_feature_2.py 生成行为特征
- 最后才用 main_generate_feature.py 汇总
实际操作中,main_generate_feature.py 有时会因为中间文件不存在而中断,因为它的默认路径是写死的。我建议按上面的顺序手动执行,每一步完成后打印一下输出文件的行数和列数,确认没丢失数据再进入下一步。如果某一步报错,优先检查是不是路径分隔符问题,Windows 和 Linux 的路径写法不一样。代码包里可能有部分 .pyc 文件,那是之前运行的缓存,不影响你从源码重新生成。
4. 模型训练与验证:随机森林、GBDT 与验证脚本的分工
4.1 两个基线模型:model_rf.py 和 model_gdbt.py 怎么选、怎么用
模型目录下的 model_rf.py 和 model_gdbt.py 分别对应随机森林和梯度提升树。这两个模型是树模型的经典代表,也是这个项目源码里最值得学习的部分。随机森林适合作为第一个跑通的模型,因为它的超参数相对不敏感,对特征缺失和异常值有较强的容忍度。GBDT 则在特征交互上更有优势,但需要更谨慎地调学习率和树深度。
model_rf.py 的核心训练代码通常是这样:
# model_rf.py 简化片段 from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=300, max_depth=10, min_samples_split=20, min_samples_leaf=15, max_features='sqrt', n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train)这里的 n_estimators 我一般先设 300,因为继续增加棵树,边际收益会递减,但训练时间线性增长。max_depth 控制在 10 以内,避免单棵树把训练集背下来。min_samples_split 和 min_samples_leaf 稍微调高一点,能有效缓解不平衡数据带来的过拟合。max_features='sqrt' 是随机森林的典型配置,让每棵树随机选取部分特征,增加树的多样性。如果你发现验证集上分数一直不涨,可以先调 max_depth 和 min_samples_leaf,其他保持默认。
4.2 验证脚本 ceshiyanzheng.py:AUC 和准确率的坑
ceshiyanzheng.py 是验证脚本,但它不只是一个“跑一遍出分数”的黑匣子。源码里它承担了两件事:一是划分训练集和验证集,二是计算指标。我在实际运行时会先看它输出的列的分布:
# ceshiyanzheng.py 中常用指标输出 from sklearn.metrics import roc_auc_score, f1_score, accuracy_score print(f"AUC: {roc_auc_score(y_valid, valid_prob):.4f}") print(f"F1: {f1_score(y_valid, valid_prob > 0.5):.4f}") print(f"ACC: {accuracy_score(y_valid, valid_prob > 0.5):.4f}")注意,在正负样本比例不均衡的电商购买预测里,accuracy_score 几乎没有信息量。即使全预测 0,准确率也能到 90% 以上。你这个项目里真正的指标是 AUC,因为它评估的是模型对正负样本排序的能力,而不是绝对的预测值。如果验证脚本只给你准确率,你一定要自己加上 AUC,否则调参就是瞎调。
划分验证集时,还要特别注意时间顺序。代码包里如果直接随机切分,会把来自同一时间段的用户混在一起,导致验证集比实际线上环境乐观。我习惯按时间排序后取最后 20% 的数据当验证集,训练集只使用更早的行为数据。这样得到的分数更接近提交结果。
4.3 调参与集成:先单模型调到合理区间再谈融合
在模型融合之前,要把两个单模型的 AUC 都调到能接受的范围。一般来说,在同类赛题里,单特征工程做好后,RF 和 GBDT 的验证 AUC 应该在 0.70~0.78 之间。如果低于 0.65,说明特征生成或者标签构造有问题,先回去查数据,而不是继续调参。
GBDT 调参时,核心参数是 learning_rate 和 n_estimators。参考代码:
# model_gdbt.py 简化片段 from sklearn.ensemble import GradientBoostingClassifier gbdt = GradientBoostingClassifier( learning_rate=0.05, n_estimators=500, max_depth=4, subsample=0.8, random_state=42 ) gbdt.fit(X_train, y_train)learning_rate 设成 0.05 表示模型每次迭代只迈一小步,需要更多树来拟合。subsample=0.8 表示每棵树随机使用 80% 的样本,增加随机性。如果学习率太大,模型容易过拟合;太小则训练时间太长。我一般先固定 learning_rate,再用早停机制确定 n_estimators。可以自己写一个循环,把验证集 AUC 变化打印出来,看到连续三轮不涨就停下来。
融合部分,最稳妥的是对两个模型输出的概率做加权平均:
# 模型融合 fused_prob = 0.4 * rf.predict_proba(X_valid)[:, 1] + 0.6 * gbdt.predict_proba(X_valid)[:, 1]这里的权重不是固定的。我一般先跑一遍两个模型的单独 AUC,再按 AUC 比例分配权重。比如 RF 是 0.72,GBDT 是 0.74,那权重可以大致是 0.47 对 0.53。最后在验证集上微调小数点后两位即可。要注意,融合的两个模型应该在特征集上相似,否则一个强一个弱,融合后反而被弱模型拖累。
5. 避坑指南:复现这套源码时最容易连续翻车的五个问题
5.1 坑之一:.py 与 .pyc 同名,到底哪份代码在生效?
现象:修改 model_rf.py 里的参数后重新运行,输出结果完全没变,像是代码被“锁死”了。
原因:代码包里有不少 .pyc 文件,这是 Python 编译后的字节码。当脚本里出现 import 语句时,如果目录下存在同名 .pyc,且 Python 的缓存机制把它当作最新版本,就可能优先加载 .pyc 而不是你刚保存的 .py。在 Spyder 这种 IDE 里尤其容易遇到,因为它在后台维护着一套自己的缓存。
解决:先删除项目目录下所有.pyc文件,再在命令行终端用python -B运行主脚本,禁用字节码缓存。比如:
find . -name "*.pyc" -delete python -B main_generate_feature.py提示:这些 .pyc 文件不是原项目必须的,删除后完全不影响源码运行。
5.2 坑之二:路径写死,换一台电脑立刻崩溃
现象:在自己电脑上跑得好好的,发给 A 同学后他一运行就报FileNotFoundError,日志显示找不到某个数据文件。
原因:load_data.py 里使用了类似C:\Users\xxx\...的绝对路径,环境一换路径就失效。这在从网上下载的项目源码里非常常见,源码作者自己调试时不一定注意到。
解决:把所有读取路径改成基于脚本所在目录的相对路径。我习惯在 load_data.py 开头加一段:
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent data_path = BASE_DIR / 'data' / 'behavior_log.csv'这样无论整个项目文件夹被放在哪个位置,脚本都能正确定位到 data 子目录下的数据文件。改完之后记得把所有用到路径的脚本统一改掉,包括后面的 generate_dataSet.py 和 main_generate_feature.py。
5.3 坑之三:负样本比例不对,AUC 高但提交分数崩
现象:验证集 AUC 跑到 0.80,感觉稳了,但提交后得分还不如随机猜测。或者测试集预测结果里几乎全是 0,没有一个正样本。
原因:combine_neg_pos 里负样本比例设置不合适。如果 ratio 过大,模型学到的决策边界过度保守,把所有用户都判断为不购买;如果 ratio 过小,模型又分不清哪些是真正的购买意向用户。还有一个隐藏问题是,验证集和训练集必须使用相同的比例构造负样本,否则评估结果不可信。
解决:把 ratio 当成一个超参数来调。在模拟项目X里,我用 [1, 2, 3, 5] 四个档位分别训练,最后发现 ratio=2 时验证 AUC 和测试得分最接近。另外,不要在生成样本时一次性把负样本全量塞进内存,而是在训练循环里动态抽样,这样更容易做多轮集成。
5.4 坑之四:时间穿越,特征里混进未来信息
现象:本地验证 AUC 高达 0.85,提交分数却只有 0.62,差距大到不像是正常波动。
原因:特征生成时没有严格按时间截止点过滤。比如生成“用户过去一个月浏览次数”时,用了整份数据的平均值,而不是用截至标签时间之前的窗口。这样等于让模型提前看到了答案,验证分数自然虚高。
解决:检查 generate_feature_1.py 里的 anchor_time 是否定义正确。我一般会在特征生成后做一次事后验证:随机挑一个训练样本,打印它的行为特征时间范围,确认所有行为时间都小于该样本的标签时间。如果发现特征里存在晚于标签时间的行为记录,说明特征生成逻辑有问题,马上去查 merge 或 groupby 的时间过滤条件。
5.5 坑之五:Spyder 工作区缓存,改了脚本不生效
现象:用 Spyder 打开项目时,工作区里看到的变量还是旧版本,运行结果跟直接在终端执行不一样,同一个函数在两个环境里结果不同。
原因:.spyderworkspace 文件把你的旧工作区变量缓存了下来。当你重新运行脚本时,如果脚本里的某个变量名与工作区里已有变量重名,调试时很容易误用旧值。
解决:在 Spyder 里运行前先清空工作区,或者干脆离开 IDE,在终端里跑完整脚本。我个人的习惯是:项目里的 main 脚本一律用终端运行,IDE 只用来做单步调试。这样能避免很多变量缓存的玄学问题,也让每次运行结果都是可复现的。
6. 从复现到提交:把模型输出整理成可提交的预测文件
6.1 生成 submission.csv 的标准流程
模型训练完成之后,最后一个任务是把测试集的预测概率整理成比赛要求的提交格式。源码里 model_test 目录下的 main_test.py 已经生成了预测结果,但很多时候还需要你手动调整列名和顺序。我一般会单独写一段生成提交文件的脚本:
# 生成提交文件 submission.py import pandas as pd test = pd.read_csv('data/test_user_sku.csv') pred_rf = rf.predict_proba(X_test)[:, 1] pred_gbdt = gbdt.predict_proba(X_test)[:, 1] final_pred = 0.5 * pred_rf + 0.5 * pred_gbdt sub = pd.DataFrame({ 'user_id': test['user_id'], 'sku_id': test['sku_id'], 'pred_prob': final_pred }) sub.to_csv('submission.csv', index=False, float_format='%.6f')这里的 float_format='%.6f' 是为了控制输出精度,很多评估系统对小数位数有限制,保留六位小数基本不会出错。提交前一定要打印前五行确认格式:
head -5 submission.csv我吃过不少次亏,提交文件列名不对或者顺序反了,直接被判成无效提交,白白浪费次数。
6.2 用 OOF 预测和阈值搜索再抠一点分
如果你还想要一个更稳的提交结果,我建议在基础模型上多走一步:OOF(Out-of-Fold)预测。做法是把训练数据分成 5 折,每次用 4 折训练、1 折预测,最后把每个样本的预测结果拼起来。这样做出来的 OOF 预测,比直接对整个训练集训练后得到的验证分数更接近真实测试表现。OOF 概率还可以作为新的特征,训练一个简单的 Logistic 回归或者直接做阈值搜索。
阈值搜索不是找 0.5,而是根据验证集上的 P/R 曲线找让 F1 最高的切分点。如果比赛只看概率排序的 AUC,阈值就不重要;但如果最终的提交结果需要二分类标签,这一步就很有必要。在模拟项目X 里,我把默认阈值从 0.5 调整到 0.32,最终 F1 提升了约 0.03。
从那以后我每次跑这类比赛代码,都会强制走一遍固定流程:删除所有 .pyc 缓存、统一相对路径、检查时间窗口、打印提交文件前五行、在验证集上搜索最优阈值。这套流程看起来琐碎,但每一步都能挡住一个看不见的坑。希望帮到你。
本文还有配套的精品资源,点击获取