简介:这是一份基于阿里天池大赛学习赛的天猫复购预测完整案例,面向计算机相关专业的在校学生、毕业设计人员以及希望入门数据挖掘与机器学习的初学者。资源提供可运行的Python源码,包括逻辑回归与随机森林两种模型实现,覆盖数据读取、特征构造、模型训练与结果预测等主要环节;配套的Jupyter Notebook则对天猫复购数据做了分析与可视化,便于直观理解用户行为特征。压缩包共7个文件,包含3个CSV数据集、2个Python脚本、1个Markdown说明文档和1个Notebook文件,整体大小仅4.01MB,结构精简、下载便捷。目前已有488人学习下载,代码均经过测试运行成功,可靠性和完成度较高,适合作为课程设计、毕业设计或项目初期演示的基础。文档说明中清晰标注了各文件用途,读者可在此基础上快速定位代码与数据,进行二次修改或功能扩展,尤其适合需要从零跑通复购预测流程的入门者。
1. 复购预测入门赛:这个案例到底在解决什么问题
很多人第一次接触数据竞赛,选的是手写数字识别或者房价预测,跑通之后觉得自己会了,但一到真正的业务数据就发懵。这个基于某数据竞赛平台学习赛的复购预测案例,给出的是一个更接近真实电商场景的任务:给你大促前六个月的用户行为日志,包含浏览、收藏、加购、购买四种行为,让你预测大促当天用户会不会对他曾经买过的商家再次下单。我第一次跑完基线模型时,准确率跑到百分之九十以上,当时还以为自己很强,结果一看 F1 直接傻眼。这个赛题的重点根本不在于堆模型,而在于怎么把“复购”定义成模型能学的目标,怎么从原始日志里造出有效特征,以及怎么用正确的验证方式衡量效果。本文会顺着这个案例,把从数据清洗、特征工程到训练评估的完整链路拆开讲,每一步都配可以直接改着用的代码,尤其适合想从“跑通 Demo”走向“能处理业务数据”的初学者。
2. 把赛题拆成模型任务:用户-商家对为什么是预测的基本单位
2.1 先别急着建模:把“复购”定义成一个可计算的目标
拿到这个案例的原始数据,第一眼看到的是几张大表:用户行为日志、商品信息、商家信息、类目信息。行为日志里每天都有成千上万条记录,一条记录代表某个用户在某个时间点对某件商品做了一次操作,操作类型分为浏览、收藏、加购、购买四种。
这时候最忌讳的就是直接开个 notebook 开始跑模型。先想清楚一个问题:预测的“样本”到底是什么。常见误区是把用户当成样本单位,预测“这个用户大促当天会不会买东西”,这个目标太粗了,几乎等于预测当天整个平台的活跃购买用户,完全丢失了商家维度的信息。反过来,如果拿“用户-商品”对当样本,又会遇到另一个问题:用户复购的大多是店铺里的其他商品,同一个商品被同一用户重复购买的样本量极少,数据稀疏到模型根本学不到规律。
这个案例的赛题定义非常巧妙,它把预测单位定成“用户-商家对”。也就是说,只要某个用户在过去六个月内对某家商家产生过购买行为,就形成一个候选样本;预测目标是这个用户在双11当天会不会再在这家商家下单。这里的“再下单”不要求买同一件商品,可能是这家店铺里任意商品,只要商家 ID 对得上就算复购。这个定义既贴合电商场景里的“复购率”业务指标,又让正负样本量变得可接受,模型学起来才有意义。
我在一开始也犯过迷糊,总想着从用户维度做各种画像特征,把商家信息完全丢在一边。后来才意识到,复购预测天然是一种“关系预测”,用户和商家之间的历史互动强度,远比用户本身的属性重要。所以整个特征工程的主线,都应该围着“用户-商家对”这个粒度来展开。
2.2 四张原始表怎么组织在一起:先画清楚关系再动代码
行为日志表是这个案例的核心,里面至少包含用户 ID、商品 ID、商家 ID、类目 ID、行为类型、时间戳这几个字段。商品表、商家表、类目表则是外围的维度表,用来补充商品和商家的静态属性。
我一般会在写任何聚合代码之前,先画一张简单的字段关系图,理清楚哪张表为主表、哪张表为辅表。行为日志表显然就是主表,因为赛题要求“根据行为预测行为”,所有关键信息都在日志里。商品、商家、类目这三张维度表,价值在于补充商家维度特征,比如某个商家总共有多少商品、覆盖了几个类目、商品价格带的分布,这些静态特征虽然简单,但对复购预测有很直接的帮助。
还有一个容易忽略的点:时间字段的处理。行为日志里的时间一般是字符串,需要准确解析成 datetime 类型,并且要注意时区是否统一。这个案例的时间跨度是六个月,数据量不小,所以从读入数据开始就要考虑内存占用。我的习惯是读表时只保留需要的字段,把能转成 category 类型的 ID 字段提前转好,时间字段解析成 datetime64,这样后面做分组聚合时能省不少时间和内存。
表之间的关联关系也不复杂:行为日志通过商品 ID 关联商品表,通过商家 ID 关联商家表,通过类目 ID 关联类目表。但要注意,行为日志里可能有一些商品 ID 在商品表中不存在,或者商家 ID 在商家表中缺失,这是脏数据的常见来源。处理方式也很简单,左右关联时设置 indicator 参数,把匹配不上的记录单独拿出来看看比例,如果比例很小直接丢弃,如果比例偏大,就要回头检查是不是 ID 字段的统一性问题了。
2.3 打标签的完整逻辑:用 merge 代替逐行判断
定义好“用户-商家对”之后,接下来就是构造候选样本集和标签列。候选样本集可以从历史购买记录里生成:先筛选出行为类型等于购买的所有记录,再对用户 ID 和商家 ID 去重,得到“六个月内买过这家商家”的全部用户-商家对。注意这一步不能用浏览或加购记录,不然负样本会混入大量只逛不买的用户,模型学出来的东西就完全变味了。
标签的构造则需要用到双11当天的购买记录。如果某个候选对在双11当天有购买行为,标签为 1,否则为 0。这里有个关键细节:标签为 1 的样本一定是在历史购买记录里出现过的,因为候选集本身就是从“历史买过”这一条件生成的。所以双11当天第一次购买的用户-商家对,不会被当成正样本,也不会被评估,这是复购预测和普通购买预测最大的差别。
我给出一个用 merge 实现打标签的代码骨架,比逐行 apply 快得多,也更容易读:
import pandas as pd import datetime # 行为日志读入,time 列解析为时间类型 behavior = pd.read_csv('user_log.csv', parse_dates=['time']) # 只保留购买行为,behavior_type == 4 表示购买 buy = behavior[behavior['behavior_type'] == 4].copy() # 候选样本:六个月内出现过购买行为的用户-商家对 cand = ( buy.groupby(['user_id', 'seller_id']) .size() .reset_index(name='history_buy_times') ) # 双11当天的购买行为,用于构造标签 nov_start = pd.Timestamp('2019-11-11') nov_end = pd.Timestamp('2019-11-12') nov11_buy = buy[(buy['time'] >= nov_start) & (buy['time'] < nov_end)] # 对候选样本打标签:当天有购买记为 1,否则记为 0 cand = cand.merge( nov11_buy[['user_id', 'seller_id']] .drop_duplicates() .assign(label=1), on=['user_id', 'seller_id'], how='left' ) cand['label'] = cand['label'].fillna(0).astype(int)这段代码的逻辑主线是:先用历史购买记录生成候选对,再用当天的购买记录做左连接,连接上了就是正样本。history_buy_times字段先保留下来,后面可以当作一个特征直接用,因为它本身就代表了用户对这个商家的历史忠诚度。
关于时间边界的处理,这里用了左闭右开区间,也就是把 11 月 11 日 00:00:00 到 11 月 12 日 00:00:00 之间当作双11当天。实际比赛中时间窗口的边界要严格对齐原始数据的说明,差一分钟都可能让线上分数出现波动。另外,drop_duplicates()很重要,因为同一个用户-商家对在当天可能有多条购买记录,不先去重会导致一个候选样本对应多条标签记录,最后生成的数据出现重复行。最后用fillna(0)把没有匹配到的候选对补成负样本,再转成 int 类型,这样标签列就干净了。
3. 从行为日志到特征矩阵:可直接复用的特征工程代码
3.1 第一版特征:用户-商家维度的计数特征
标签构造好之后,下一步就是特征工程。复购预测的特征可以分成两大类:一类是计数特征,描述用户和商家之间互动的“量”;另一类是时间特征,描述互动的“新与旧”。两类特征各有各的作用,量不够说明关系太浅,间隔太长说明热度已经衰退。
先说计数特征。直接对行为日志按照用户 ID 和商家 ID 分组,统计浏览、收藏、加购、购买四种行为的次数,这是最基础也是最有效的特征。还可以算一些衍生比例,比如加购转购买率、浏览转购买率,这些比例特征比单纯的绝对值更能反映购买意愿的强度。
这里有一个需要特别注意的点:统计购买次数时,要去掉双11当天的记录。很多初学者在构造特征时,直接把整张行为日志表拿来聚合,完全没有排除当天的行为,结果就是特征里包含了标签的信息,线下验证分数高得离谱,一提交就露馅。正确的做法是在读入数据时就设置一个截止时间,所有特征统计都只用截止时间之前的记录。
代码也很直白:
# 截止时间:双11当天之前的所有行为,严格不包含标签当天时间 cutoff = pd.Timestamp('2019-11-11') feat_data = behavior[behavior['time'] < cutoff] # 按用户-商家对聚合计数特征 count_feat = ( feat_data.groupby(['user_id', 'seller_id']) .agg( pv=('behavior_type', lambda x: (x == 1).sum()), fav=('behavior_type', lambda x: (x == 2).sum()), cart=('behavior_type', lambda x: (x == 3).sum()), buy=('behavior_type', lambda x: (x == 4).sum()), active_days=('time', 'nunique') ) .reset_index() ) # 加购转购买率,分子分母都加 1 做平滑,避免除零 count_feat['cart_buy_rate'] = count_feat['cart'] / (count_feat['cart'] + count_feat['buy'] + 1) count_feat['view_buy_rate'] = count_feat['pv'] / (count_feat['pv'] + count_feat['buy'] + 1)这段代码里的四个 lambda 分别统计四种行为次数,active_days统计的是这个用户在这家商家活跃过的天数,比单纯的行为次数更能体现持续互动。需要注意 lambda 在 groupby.agg 里耗时较长,数据量上百万时可以考虑把行为类型先转成 one-hot 列再去求和,速度会快很多。两个比例特征做了平滑处理,主要是为了防止分母为 0 或者样本太少导致的极端值。在实际项目中,我还倾向于把history_buy_times也作为一个特征直接放进特征矩阵里,因为它本身就是从候选集生成时带出来的,能提供有效信息。
3.2 第二批特征:时间间隔与近期行为窗口
计数特征解决的是“买得多不多”的问题,但复购预测还有一个关键维度是“最近还买没买”。一个用户半年内深度互动过很多次的商家,如果最近两个月没有任何动作,复购概率可能还不如一个最近一周刚买过的新商家。这就要用到时间窗口特征。
最常用的两个时间特征:一是用户最后一次在该商家购买距截止日的天数,反映关系的“热度”;二是在截至日前 7 天、14 天、30 天内的行为强度,反映“近因效应”。这两个特征通常能带来比基础计数更大的提升。
我的实现方式是把时间特征单独做成一张特征表,再和计数特征合并:
import datetime # 最近一次购买距今的天数 last_buy = ( buy[buy['time'] < cutoff] .groupby(['user_id', 'seller_id'])['time'] .max() .reset_index() ) last_buy['last_buy_days'] = (cutoff - last_buy['time']).dt.days # 近 7 天、近 30 天行为统计 feat_data['is_last7'] = (feat_data['time'] >= cutoff - datetime.timedelta(days=7)).astype(int) feat_data['is_last30'] = (feat_data['time'] >= cutoff - datetime.timedelta(days=30)).astype(int) last7_feat = ( feat_data[feat_data['is_last7'] == 1] .groupby(['user_id', 'seller_id']) .agg( last7_buy=('behavior_type', lambda x: (x == 4).sum()), last7_cart=('behavior_type', lambda x: (x == 3).sum()), last7_pv=('behavior_type', lambda x: (x == 1).sum()) ) .reset_index() ) last30_feat = ( feat_data[feat_data['is_last30'] == 1] .groupby(['user_id', 'seller_id']) .agg( last30_buy=('behavior_type', lambda x: (x == 4).sum()), last30_cart=('behavior_type', lambda x: (x == 3).sum()) ) .reset_index() )加上last_buy_days之后,这个案例的特征总量大概在二十个左右。没必要一上来堆上百个特征,先把这些基础特征跑一遍,把基线分数抬起来,再做迭代。注意last_buy是按用户-商家对取购买时间的最大值,得到的是 Timestamp 类型,用截止时间减去它就得到间隔天数。如果某个候选对在六个月内只在早期买过一次,这个值就会很大,模型会把它当作热度衰退的信号。
3.3 合并所有特征:留出补零的思路
计数特征表、时间间隔表、近 7 天表、近 30 天表都构造好之后,把它们全部 merge 到候选样本表上。这里有一个常见问题:某些用户-商家对在近 7 天没有任何行为,所以last7_feat里根本没有这行记录,merge 之后会出现缺失值。
缺失值处理上,我建议对计数类特征直接填 0,因为“没有行为”本身就代表一种状态,填 0 是最符合逻辑的。对last_buy_days这种间隔特征,如果出现缺失,说明该用户-商家对没有任何购买行为,理论上这种样本不该出现在候选集里,但如果出现了,说明前面候选集构造有问题,需要回头检查。
合并的代码遵循同一个模式:
train = cand.merge(count_feat, on=['user_id', 'seller_id'], how='left') train = train.merge(last_buy[['user_id', 'seller_id', 'last_buy_days']], on=['user_id', 'seller_id'], how='left') train = train.merge(last7_feat, on=['user_id', 'seller_id'], how='left') train = train.merge(last30_feat, on=['user_id', 'seller_id'], how='left') # 计数类缺失值填 0,间隔类缺失值填一个较大值表示“很久没买” fill_zero_cols = ['pv', 'fav', 'cart', 'buy', 'active_days', 'last7_buy', 'last7_cart', 'last7_pv', 'last30_buy', 'last30_cart'] train[fill_zero_cols] = train[fill_zero_cols].fillna(0) train['last_buy_days'] = train['last_buy_days'].fillna(180)这里要注意两次 merge 的写法,last_buy传给 merge 时我先只保留了需要的三列,避免把时间列也带进来造成列名冲突。last_buy_days填 180 是人为设定的一个较大间隔,表示“整个统计周期内都没有购买记录”。如果候选集是从历史购买记录生成的,这个情况本不该出现,但保留这个兜底逻辑会安全很多。
4. 训练与验证:用 LightGBM 跑通最小可复现流程
4.1 为什么选 LightGBM 而不是逻辑回归
到了建模阶段,有人会问,为什么这类赛题案例普遍选择 LightGBM,而不是更基础的逻辑回归或者更复杂的深度学习模型。原因很实际:电商行为日志产生的特征大多是稀疏、离散且互相之间有复杂交互的,逻辑回归需要手写大量的交叉特征才能拟合这种交互关系,而树模型天然能自动做特征分裂和组合。
LightGBM 比 XGBoost 在这种数据上更有优势的地方在于速度。特征矩阵达到几十万行、几十列时,LightGBM 的直方图算法能显著减少内存占用和训练时间。我做过一个对比,同样数据量下,XGBoost 训练一轮要五分钟,LightGBM 只要一分钟左右,而且精度几乎没有差别。对于学习和调参阶段,这种速度优势意味着可以多跑好几个版本的实验。
还有一个考虑是训练集规模。这个案例是学习赛,数据量不会特别大,但行为日志展开成用户-商家对之后,样本量也会到几十万级别。这样的规模刚好在 LightGBM 的高效区间。如果以后换到上亿样本的业务场景,再考虑分布式方案也不迟。
4.2 验证方式:用时间切分,不用随机切分
模型验证是复购预测里最容易被忽略又最致命的环节。很多人习惯直接用train_test_split随机切分,这在一般分类问题里没有问题,但在这种带时间序列属性的赛题里会带来严重的数据泄漏。
原因很简单:同一天产生的行为记录,一部分被分进训练集,一部分被分进验证集,验证集里其实包含了训练集附近的信息。模型在评估时能看到“未来的邻居”,线下指标自然虚高。而真实预测场景是拿六个月的行为预测未来某一天,时间永远是往前的,所以验证集必须严格晚于训练集。
我的做法是用双11前最近一段时间作为验证集。假设完整行为数据截止到 11 月 10 日,那么可以把 10 月 1 日之前的行为作为训练特征,10 月 1 日之后作为验证期。注意这个切分一定要在打标签之前设计好,不要等特征都算完了再切,不然特征里已经包含了验证期的统计信息。
如果觉得单次时间切分不够稳定,可以考虑按多个时间点滚动切分,比如分别以 10 月 15 日、10 月 25 日作为截止日,构造三组训练验证数据,分别在每组上训练并评估,最后取平均。这样能更可靠地估计模型在新数据上的表现。
4.3 训练代码与关键参数:从默认参数开始调
直接给出一个最小可复现的训练流程。特征矩阵和标签列准备好之后,先做训练验证切分,再训练 LightGBM 二分类模型:
import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import f1_score feature_cols = [c for c in train.columns if c not in ('user_id', 'seller_id', 'label')] X = train[feature_cols] y = train['label'] # 按时间切分:这里用 idx 列模拟时间切分 # 实际案例中应在特征构造前完成时间切分 train_idx = train['is_train'] == 1 val_idx = train['is_train'] == 0 params = { 'objective': 'binary', 'metric': 'binary_logloss', 'learning_rate': 0.05, 'num_leaves': 31, 'min_child_samples': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'scale_pos_weight': 5, 'verbosity': -1 } dtrain = lgb.Dataset(X[train_idx], label=y[train_idx], feature_name=feature_cols) dvalid = lgb.Dataset(X[val_idx], label=y[val_idx], reference=dtrain) model = lgb.train( params, dtrain, num_boost_round=2000, valid_sets=[dvalid], callbacks=[lgb.early_stopping(100), lgb.log_evaluation(100)] ) # 预测验证集概率,扫描阈值找最优 F1 val_pred = model.predict(X[val_idx]) for thresh in [0.2, 0.3, 0.4, 0.5]: f1 = f1_score(y[val_idx], (val_pred >= thresh).astype(int)) print(f'threshold={thresh}, f1={f1:.4f}')这里几个参数的调整逻辑需要说明一下。scale_pos_weight是正负样本不平衡的调节参数,案例中复购正样本占比通常只有百分之几,设置为 5 意味着模型会把正样本的权重放大五倍,这对提高召回有明显作用。feature_fraction和bagging_fraction都是防止过拟合的设置,学习赛数据量不大,树模型很容易过拟合,一般我会把这两个值设置在 0.7 到 0.9 之间。min_child_samples控制叶子节点的最少样本数,默认 20 在几十万样本下没有太大问题,但如果验证集的 F1 波动很大,可以调大到 50 试试。
最后扫描阈值那段代码很有用,分类模型输出的概率并不能直接和最优 F1 对应起来,特别是正负样本不平衡时,默认的 0.5 阈值往往不是最优选择。习惯上会在验证集上把阈值从 0.1 到 0.5 扫一遍,选 F1 最高的那个作为最终提交的判定标准。
5. 避坑与常见问题:复购场景里最容易翻车的五个点
5.1 数据泄漏:特征里混进了双11当天的行为
这是复购预测案例里最容易犯、也最难察觉的错误。现象是线下验证的 F1 出奇地高,比如随手一个基线就能到 0.5 以上,但一提交或者换到测试集,分数立刻掉到 0.1 左右。很多人这时候会怀疑模型问题,反复调参,其实根源出在特征构造上。
原因是统计特征时没有加时间过滤,把行为日志里双11当天的浏览、加购、购买记录全部聚合进了特征矩阵。当天的购买行为本身就是要预测的标签,特征里带上了它,模型相当于开卷考试,线下分数自然好看。解决方法是严格在特征构造前加一道过滤,用behavior['time'] < cutoff把所有特征统计限制在双11之前,并且把截止时间定义为双11零点,而不是双11 23:59:59。这个边界差一分钟都会把当天记录包含进来,我建议在代码里写死cutoff之后,顺手打印一下特征数据的最大时间戳,确认没有越界。
5.2 用准确率评估模型,被正负样本误导
复购预测的正样本比例通常只有个位数,负样本占绝大多数。如果拿准确率当核心指标,模型全部预测为 0,准确率也能达到九成以上。现象是模型文件很漂亮,准确率 0.93,但业务上根本没法用,因为一个正样本都没有捞出来。
原因是准确率在不平衡数据上没有区分度,它假设了预测全部类别错误的代价相同,但复购业务更关心的是把真正会复购的用户找出来,哪怕误判一些不买的用户。解决方法是改用 F1 分数,同时关注召回率和精确率的平衡。在验证时,我还会额外看一眼 PR 曲线下的面积,这个指标对正负样本比例不敏感,能更稳定地反映模型排序能力。
5.3 随机切分验证集,时间信息串场
现象是做了十次随机切分的交叉验证,F1 都很稳定,模型上线后效果却大打折扣。原因在于复购预测的数据本质是时间序列,同一个用户在相邻时间的行为高度相关。随机切分会把 10 月上旬和 10 月下旬的用户行为分别放到训练集和验证集,验证集里全是“熟悉的面孔”,测试集却是完全未来的数据,评估结果严重失真。
解决方法是采用时间切分,训练集使用截止日之前的数据,验证集使用截止日之后的数据,保证验证集在时间上永远晚于训练集。如果数据量充足,可以做滚动验证,取得多个时间点的平均分,这比单次切分更可靠。我在实际项目中,会把时间切分放在特征构造的第一步,而不是等特征算完再切,这样就不会在特征里看到验证期的任何信号。
5.4 候选负样本构造不当,模型学到错误规律
有段时间我为了增加训练数据,把全量用户-商家对都当成负样本,哪怕用户和商家之间从未有过任何互动。现象是训练速度变慢,特征矩阵稀疏度大增,模型效果反而下降。原因是这些“完全无互动”的样本太容易识别了,模型只要学到“只要历史行为次数大于 0 就是正样本”就能把训练集分开,完全没有学到复购的真实规律。
正确做法是只把有过历史购买行为的用户-商家对作为候选集,正样本是其中双11当天又买了的,负样本是其中双11当天没买的。这样模型学习的问题是“已经买过的人,会不会再买”,而不是“毫无关系的人,会不会突然买”。这两个问题有本质区别。如果实在想加入“无互动负样本”,也要控制比例,通过下采样让数据分布尽量接近真实场景。
5.5 内存爆炸:行为日志分组聚合写得太笨
这个坑在大数据量学习赛上特别常见。现象是代码跑到 groupby 那一步,内存直接占满,电脑风扇狂转,最后进程被杀掉。原因是行为日志动辄几千万行,直接对全表做groupby加 lambda 聚合,中间过程会产生大量临时对象,内存根本扛不住。
解决方法是分三步走:第一步,把不用的字段全部丢掉,只保留 user_id、seller_id、behavior_type、time 四个核心列;第二步,将 user_id 和 seller_id 转成 pandas 的 category 类型,这能把字符串对象的内存占用减少一大半;第三步,避免在 groupby.agg 里写 Python 原生 lambda,改成先用布尔条件筛出子集,再分别 groupby 后 merge。比如统计购买次数,直接feat_data[feat_data['behavior_type'] == 4].groupby(...).size(),比在 agg 里传 lambda 快得多。如果数据量实在太大,还可以把数据按用户 ID 分块处理,最后再合并结果。
6. 进阶:用阈值扫描和概率校准把模型变成业务动作
训练完模型、拿到验证集上的 F1 之后,很多人就以为任务结束了。但在实际落地时,光有一个模型文件还不够,关键在于怎么把预测分数转成清晰可执行的动作。复购预测的业务目标是找到一批最有可能复购的用户做营销触达,而营销资源是有限的,所以需要圈定一个高价值人群,而不是对全量用户发同样的优惠。
这个环节最常见的做法是阈值扫描加概率校准。先用验证集预测出每个样本的概率,然后按 0.1 到 0.6 间隔扫描阈值,画出 F1、召回率、精确率随阈值变化的趋势,找到业务可接受的平衡点。如果营销成本高、期望精准,就把阈值调高,牺牲召回换精确;如果希望尽量覆盖潜在复购用户、容忍一定误判,就把阈值调低。
概率校准是另一个值得尝试的进阶技巧。LightGBM 输出的概率值只是样本的排序分数,并不严格等于真实概率,直接用它做 ROI 计算会有偏差。最常用的方法是使用 sklearn 的 CalibratedClassifierCV,基于验证集做 Isotonic 回归或 Platt Scaling 校准,让输出的分数更接近真实概率。校准之后再算人群的期望收益,营销预算的分配会更靠谱。
我之前在一个模拟项目里就吃过亏,当时只盯着验证集 F1 调参,把阈值定为 0.5,结果上线后发现触达成本远高于预期,因为模型预测出的概率普遍偏高,0.5 以上的人群规模太大。后来用校准模型重新跑了一遍,发现实际概率和预测概率系统性偏差了十几个百分点。从那以后,但凡模型输出要做业务决策,我都先做概率校准再定阈值,也算是血泪教训了。
这个案例给我最大的收获,不是某个特征的技巧或某个参数的最佳取值,而是“先定义清楚问题,再做模型”的习惯。复购预测看起来是一个分类任务,但样本粒度、时间边界、验证方式这些前置设计,直接决定了后面所有工作的价值。如果你也要在不熟悉的业务场景里做建模,建议先把这些问题想清楚再写代码。希望帮到你。
本文还有配套的精品资源,点击获取