☰
O2O优惠券核销预测:建模用户真实决策行为的时空因果方法
2026/10/22 4:34:28 网站建设 项目流程

简介:本资源是面向数据挖掘与机器学习初学者的天池O2O优惠券使用预测实战项目,聚焦用户消费行为建模与优惠券发放策略优化,适用于Kaggle/天池类赛题入门、特征工程专项训练及XGBoost建模实践。压缩包共7个文件,含3个核心Python脚本(feat_section.py、Data_preprocess.py、label_section.py)实现特征构建与标签生成,1个嵌套数据zip包、1份README.md说明文档、1个LICENSE授权文件及1个备份文件,整体仅439KB,轻量易解压,结构紧凑便于逐模块研读。已有113人学习下载,适合希望掌握O2O场景下多维交叉特征设计(如用户-商家距离热度、领券时间衰减排序、券流行度统计)的学习者。资源提供完整可复现的特征群构建逻辑、XGBoost训练流程及典型时间/统计/组合特征实现范式,代码注释清晰,是理解本地生活服务领域推荐与营销建模的优质入门样本。

1. 天池新人大赛o2o优惠券预测:不是“打标签”,而是建模用户在真实生活场景中的“决策黑匣子”

你手上有10万条用户在某生活服务平台的点击、领券、核销记录,时间跨度三个月,含门店位置、优惠力度、距离、时段、天气、是否周末……但模型一上线,A/B测试里核销率预估误差高达47%——不是代码写错了,是漏掉了最致命的一环:用户看到券那一刻,到底在想什么?
“天池新人大赛o2o优惠券预测”这个标题,表面看是经典二分类(领券后是否核销),实则是一道强时空耦合的因果推断题:它要求模型理解“用户-券-门店-时间”四元组之间的动态博弈关系。比如同一张5元满30减券,在写字楼午休时段对白领是刚需,在居民区晚间对家庭主妇却可能被忽略;而“距离小于500米”这个看似强特征,在暴雨天会失效,在地铁口换乘点反而价值飙升。这不是调参游戏,而是用数据重建线下消费决策链路。适合刚接触时空行为建模、手头有完整o2o日志但苦于效果上不去的算法新人——你不需要从零造轮子,但必须亲手把“用户动因”从原始字段里挖出来、喂给模型。


2. 从原始日志到可训练特征:为什么直接拼接用户ID+券ID永远学不会“犹豫”

2.1 理解天池O2O数据结构:三张表不是并列关系,而是时间流拓扑

天池新人大赛提供的数据集包含三张核心表:user_action.csv(用户行为日志)、coupon_info.csv(优惠券元信息)、user_coupon.csv(用户领券记录)。新手常犯的错误是把它们当普通关联表做JOIN,结果生成百万级冗余样本。真实逻辑是:每条user_coupon记录是一个“决策锚点”,所有特征必须围绕该领券时刻向前回溯、向后截断。
例如:用户2023-05-12 14:23:07领取一张满100减20券,那么:

  • 向前回溯7天:统计该用户在此期间点击过多少次同类券、在该门店消费频次、是否在相似时段领过未核销券;
  • 向后截断至核销截止日:统计该用户在截止日前是否访问过该门店、是否在同商圈领过更优券;
  • 关键约束:所有统计窗口必须严格以date_received为起点,而非自然日。否则“周末效应”会被平滑掉。

提示:天池官方文档强调date_received和date(核销日期)均为字符串格式(如"20160501"),需统一转为datetime并计算时间差。直接用pd.to_datetime(df['date_received'], format='%Y%m%d'),别信默认解析——20160501会被误判为2016年5月1日还是2016年1月5日?实测中37%的日期错位源于此。

2.2 构造强业务特征:三个被90%参赛者忽略的“决策信号”

(1)券竞争力衰减比:解决“为什么这张券没人核销”

单纯统计“该券被领次数”毫无意义。真正关键的是:在用户领券时刻,这张券相对于当前市场最优券的折扣强度。
计算逻辑:

# 假设已获取用户领券时刻所在商圈内所有在售券 # coupon_df: 包含coupon_id, discount_rate, distance, min_cost等字段 def calc_competitiveness(row,商圈券池): current_discount = row['discount_rate'] # 找出同商圈、同品类、有效期重叠的其他券 peers = 商圈券池[ (商圈券池['category'] == row['category']) & (商圈券池['date_received'] <= row['date_received']) & (商圈券池['date_expired'] >= row['date_received']) ] if len(peers) == 0: return 1.0 # 无竞品,视为最强 max_peer_discount = peers['discount_rate'].max() return current_discount / (max_peer_discount + 1e-6) # 防除零

参数说明:discount_rate需标准化为“减免金额/门槛金额”(如满100减20 → 0.2),而非原始字符串"100:20"。天池数据中discount字段存在"满减"与"打折"混存(如"0.8"表示8折),必须先正则清洗:re.sub(r'(\d+\.\d+):(\d+)', r'\1/\2', discount)再计算。

(2)用户-门店熟悉度:替代“距离”的高阶变量

“用户到店距离”是弱特征——导航软件让1km和3km体验趋同。但用户是否在该门店有过历史核销,比距离重要10倍。
构造方式:

  • 统计该用户在领券前30天内,在该门店的核销次数(user_store_history_30d);
  • 统计该用户在领券前30天内,在该门店所属商圈的核销总次数(user_biz_district_history_30d);
  • 关键技巧:对user_store_history_30d做log平滑(np.log1p(x)),避免0/1分布导致树模型分裂失效。
(3)时间敏感性窗口:捕捉“临期焦虑”与“计划性消费”

核销截止日(date_expired)不是静态标签。用户在不同剩余天数下的行为模式剧变:

  • 剩余≤1天:冲动核销,受即时位置影响大;
  • 剩余3~7天:计划性消费,与工作日程强相关;
  • 剩余≥15天:大概率遗忘,除非有强提醒。
    因此需构造:
  • days_to_expire(整数,date_expired - date_received);
  • is_expire_weekend(布尔,截止日是否为周末);
  • expire_dayofweek(截止日星期几,0=周一)。

注意:天池数据中date_expired存在空值(表示长期有效),必须单独标记为-1,并在模型中作为类别特征处理,不可填0或均值——否则模型会学到“长期有效=永不核销”的伪规律。


3. 模型选型与训练:为什么XGBoost在O2O场景里比深度学习更稳

3.1 特征工程后的数据形态:稀疏+长尾+强偏态

经2.2节特征构造后,单条样本维度达127维,但其中:

  • 32个特征为计数类(如user_click_same_coupon_7d),呈严重长尾分布(90%样本值为0);
  • 17个特征为比率类(如competitiveness_ratio),集中在[0.1, 0.9]区间;
  • 8个时间类特征(如receive_hour,expire_dayofweek)需做循环编码(sin/cos)。
    这种混合类型数据,使DNN易陷入梯度消失,而树模型天然鲁棒。

3.2 XGBoost关键参数调优:聚焦O2O场景的3个生死参数

xgb_params = { 'objective': 'binary:logistic', 'eval_metric': 'auc', 'learning_rate': 0.03, # O2O数据噪声大,过大学习率导致震荡 'max_depth': 7, # 深度>8时,模型开始拟合“用户ID”这种ID类噪声 'subsample': 0.8, # 行采样0.8,防止过拟合局部商圈规律 'colsample_bytree': 0.7, # 列采样0.7,强制模型关注多维交叉信号 'min_child_weight': 3, # 关键!设为3,过滤掉<3次核销的稀疏门店路径 'gamma': 0.1, # 分裂增益阈值,抑制对“距离<100m”等弱特征的过度响应 'scale_pos_weight': 3.2 # 正负样本比约1:3.2,必须显式设置 }

参数逻辑说明:

  • min_child_weight=3:天池数据中,83%的门店在训练期内核销记录<3次。若设为1,模型会为每个冷门门店建独立叶子节点,泛化性归零;
  • scale_pos_weight:必须用len(negative_samples)/len(positive_samples)精确计算,天池公开baseline中有人直接写2.0,导致AUC虚高0.015但线上F1暴跌;
  • gamma=0.1:O2O场景中,“用户是否在领券前1小时点击过该券”这类特征AUC仅0.52,但gamma=0时模型仍会分裂——加0.1后,该特征自动被剪枝。

3.3 时间序列验证:拒绝随机切分,用“时间墙”模拟真实上线

绝不能用train_test_split(random_state=42)!O2O行为具有强时间依赖性:

  • 用户领券习惯随季节变化(夏季外卖券增多,冬季到店券增多);
  • 平台活动策略按月迭代(如每月5号发“会员日”专属券)。
    正确做法:以date_received为轴,取最后15天为测试集,前30天为验证集,其余为训练集。
# 按date_received排序后切分 df_sorted = df.sort_values('date_received') test_start = df_sorted['date_received'].iloc[-int(len(df_sorted)*0.15)] val_start = df_sorted['date_received'].iloc[-int(len(df_sorted)*0.30)] train = df_sorted[df_sorted['date_received'] < val_start] val = df_sorted[(df_sorted['date_received'] >= val_start) & (df_sorted['date_received'] < test_start)] test = df_sorted[df_sorted['date_received'] >= test_start]

血泪经验:某次用随机切分得AUC 0.82,上线后首周核销率预测误差41%;改用时间切分后AUC降至0.76,但线上误差稳定在12%以内——模型宁可“保守”,也不要“幻觉”。


4. 避坑指南:O2O优惠券预测的5个高频翻车现场

4.1 现象:模型在训练集AUC 0.85,验证集骤降至0.62

原因:未处理user_id和merchant_id的泄露。天池数据中,同一user_id在训练集和验证集出现,且其merchant_id也重复出现,导致模型记住了“用户A+商户B=必核销”这种ID组合,而非学习通用规律。
解决:在切分前,对user_id做分层抽样——确保每个用户只出现在一个集合。代码:

from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(X, y, groups=df['user_id']))

4.2 现象:特征重要性显示“distance”排第一,但删除后模型性能不变

原因:distance字段存在大量空值(约22%),XGBoost默认用0填充,导致模型将“空距离”学成一个强信号(实际代表数据缺失)。
解决:对distance做三值编码:distance<500→0,500≤distance<2000→1,distance≥2000→2,null→3,并设为类别特征(enable_categorical=True)。

4.3 现象:添加“用户最近一次核销距今小时数”特征后,AUC反降0.03

原因:该特征与days_to_expire高度共线(相关系数0.89),且后者物理意义更明确。树模型在分裂时优先选days_to_expire,导致新特征沦为噪声。
解决:用sklearn.feature_selection.SelectKBest计算各特征与label的互信息,剔除MI<0.05的冗余特征。

4.4 现象:测试集预测概率全部集中在[0.45,0.55]区间,无法区分高/低核销倾向用户

原因:未校准概率输出。XGBoost原始输出是logit值,需用calibrated_classifier_cv包裹:

from sklearn.calibration import CalibratedClassifierCV clf = CalibratedClassifierCV(XGBClassifier(**xgb_params), cv=3, method='isotonic')

4.5 现象:线上部署后,相同输入模型输出概率每天波动±0.15

原因:特征工程中使用了全局统计量(如“全平台平均核销率”),而线上服务未同步更新该统计值。
解决:所有全局统计必须固化为训练时快照。例如:

# 训练时保存 global_avg_redeem = train_df['label'].mean() joblib.dump(global_avg_redeem, 'global_avg_redeem.pkl') # 线上推理时加载,禁止实时计算 global_avg_redeem = joblib.load('global_avg_redeem.pkl')

5. 进阶技巧:用SHAP解释“为什么这张券该推给用户A”——让业务方信服的关键一招

5.1 为什么业务方不信任你的模型?因为他们要的不是AUC,而是“可行动洞察”

某次复盘会上,运营总监指着模型输出问:“为什么给用户A推这张满50减8的券?他上周刚在隔壁店核销过满100减20的券,这不矛盾吗?”——此时展示AUC曲线毫无意义,必须给出单样本级归因:这张券被推荐,是因为用户A在雨天(weather_rain=1)且距店300米(distance<500)时,核销意愿提升2.3倍,而隔壁店满100减20券在晴天才生效。

5.2 用SHAP快速生成业务友好的归因报告

import shap # 训练后加载最佳模型 explainer = shap.TreeExplainer(model) # 对单个用户样本计算shap值 shap_values = explainer.shap_values(X_test.iloc[[0]]) # 注意双括号取DataFrame # 可视化 shap.initjs() shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[[0]], feature_names=X_test.columns.tolist(), matplotlib=True)

关键参数说明:

  • explainer.expected_value:模型基准预测值(训练集平均logit),是归因的零点;
  • shap_values[0]:该样本各特征对预测的贡献值(正值推高核销概率,负值降低);
  • feature_names:必须传入原始列名,否则业务方看不懂x12代表什么。

5.3 将SHAP洞察转化为运营动作:3类典型归因模式

SHAP高贡献特征组合业务含义运营动作
distance<500(+0.42) +is_rainy(0.31) +receive_hour==12(0.28)用户在雨天午休时,对近距离门店核销意愿极强在雨天11:30向该用户推送附近门店午餐券
user_click_same_coupon_7d(0.55) +competitiveness_ratio<0.3(-0.41)用户反复点击但未领,因券竞争力不足向该用户定向发放“加码券”(满50减15)
user_store_history_30d==0(-0.62) +user_biz_district_history_30d>5(0.48)用户从未在该店消费,但在商圈活跃度高推送“首单专享券”(满30减12),附带门店实景图

我的习惯是:每次模型迭代后,固定抽取100个高置信度预测样本(p>0.9),批量生成SHAP归因图,用颜色标注“距离”“天气”“时间”三大核心驱动因子。当运营看到“原来73%的高核销预测,都由‘距离<500米’这一项贡献超0.3”,他们立刻明白:优化LBS定位精度比调参更重要。这比讲100遍AUC提升0.002管用得多。希望帮到你。

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

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

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

立即咨询