题目
根据提供的用户在1月1日至6月30日之间真实线上线下消费行为,预测用户在7月领取优惠券后15天以内的使用情况。
评价方式
本赛题目标是预测投放的优惠券是否核销。使用优惠券核销预测的平均AUC(ROC曲线下面积)作为评价标准。 即对每个优惠券coupon_id单独计算核销预测的AUC值,再对所有优惠券的AUC值求平均作为最终的评价标准。
字段表
Table 1: 用户线下消费和优惠券领取行为
| Field | Description |
|---|---|
| User_id | 用户ID |
| Merchant_id | 商户ID |
| Coupon_id | 优惠券ID:null表示无优惠券消费,此时Discount_rate和Date_received字段无意义 |
| Discount_rate | 优惠率:x \in [0,1]代表折扣率;x:y表示满x减y。单位是元 |
| Distance | user经常活动的地点离该merchant的最近门店距离是x*500米(如果是连锁店,则取最近的一家门店),x\in[0,10];null表示无此信息,0表示低于500米,10表示大于5公里; |
| Date_received | 领取优惠券日期 |
| Date | 消费日期:如果Date=null & Coupon_id != null,该记录表示领取优惠券但没有使用,即负样本;如果Date!=null & Coupon_id = null,则表示普通消费日期;如果Date!=null & Coupon_id != null,则表示用优惠券消费日期,即正样本; |
Table 2: 用户线上点击/消费和优惠券领取行为
| Field | Description |
|---|---|
| User_id | 用户ID |
| Merchant_id | 商户ID |
| Action | 0 点击, 1购买,2领取优惠券 |
| Coupon_id | 优惠券ID:null表示无优惠券消费,此时Discount_rate和Date_received字段无意义。“fixed”表示该交易是限时低价活动。 |
| Discount_rate | 优惠率:x \in [0,1]代表折扣率;x:y表示满x减y;“fixed”表示低价限时优惠; |
| Date_received | 领取优惠券日期 |
| Date | 消费日期:如果Date=null & Coupon_id != null,该记录表示领取优惠券但没有使用;如果Date!=null & Coupon_id = null,则表示普通消费日期;如果Date!=null & Coupon_id != null,则表示用优惠券消费日期; |
Table 3:用户O2O线下优惠券使用预测样本
| Field | Description |
|---|---|
| User_id | 用户ID |
| Merchant_id | 商户ID |
| Coupon_id | 优惠券ID |
| Discount_rate | 优惠率:x \in [0,1]代表折扣率;x:y表示满x减y. |
| Distance | user经常活动的地点离该merchant的最近门店距离是x*500米(如果是连锁店,则取最近的一家门店),x\in[0,10];null表示无此信息,0表示低于500米,10表示大于5公里; |
| Date_received | 领取优惠券日期 |
Table 4:选手提交文件字段,其中user_id,coupon_id和date_received均来自Table 3,而Probability为预测值
| Field | Description |
|---|---|
| User_id | 用户ID |
| Coupon_id | 优惠券ID |
| Date_received | 领取优惠券日期 |
| Probability | 15天内用券概率 |
代码
# 1. 数据读取与EDA
offline_train = pd.read_csv('data/ccf_offline_stage1_train.csv')
online_train = pd.read_csv('data/ccf_online_stage1_train.csv')
test = pd.read_csv('data/ccf_offline_stage1_test_revised.csv')
sample_submission = pd.read_csv('data/sample_submission.csv')
# 核销标签构造
def get_label(row):
if pd.isna(row['Date_received']):
return -1
if pd.isna(row['Date']):
return 0
diff = pd.to_datetime(row['Date']) - pd.to_datetime(row['Date_received'])
return 1 if diff.days <= 15 else 0
offline_train['label'] = offline_train.apply(get_label, axis=1)
offline_train = offline_train[offline_train['label'] != -1]
# 2. 特征工程
def parse_discount(s):
if pd.isna(s):
return 1.0, 0
if ':' in str(s):
a, b = s.split(':')
rate = 1 - float(b) / float(a)
is_manjian = 1
else:
rate = float(s) if float(s) < 1 else 1.0
is_manjian = 0
return rate, is_manjian
def feature_engineer(df):
df['discount_rate'], df['is_manjian'] = zip(*df['Discount_rate'].map(parse_discount))
df['Distance'] = df['Distance'].replace('null', -1).astype(float)
df['weekday'] = pd.to_datetime(df['Date_received'], format='%Y%m%d').dt.weekday
df['is_weekend'] = df['weekday'].apply(lambda x: 1 if x >= 5 else 0)
# 用户、商家、券的历史统计特征
for col in ['User_id', 'Merchant_id', 'Coupon_id']:
df[f'{col}_count'] = df.groupby(col)[col].transform('count')
return df
offline_train = feature_engineer(offline_train)
test = feature_engineer(test)
# 3. 数据准备
features = [
'discount_rate', 'is_manjian', 'Distance', 'weekday', 'is_weekend',
'User_id_count', 'Merchant_id_count', 'Coupon_id_count'
]
X = offline_train[features]
y = offline_train['label']
X_test = test[features]
# 填充缺失值
X = X.fillna(-1)
X_test = X_test.fillna(-1)
# 划分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"训练集样本数: {len(X_train)}, 验证集样本数: {len(X_val)}")
print(f"正负样本比例 - 训练集: {y_train.mean():.4f}, 验证集: {y_val.mean():.4f}")
# 4. 交叉验证评估函数
def cross_validate_model(model, X, y, n_splits=5):
skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
auc_scores = []
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
model.fit(X_train, y_train)
val_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, val_pred)
auc_scores.append(auc)
print(f"Fold {fold+1} | AUC: {auc:.4f}")
print(classification_report(y_val, model.predict(X_val)))
print(f"\n平均AUC: {np.mean(auc_scores):.4f} (±{np.std(auc_scores):.4f})")
return np.mean(auc_scores)
# 5. 模型训练与调优
# 5.1 XGBoost
print("\n" + "="*50)
print("XGBoost 交叉验证")
print("="*50)
xgb = XGBClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
random_state=42,
eval_metric='auc'
)
xgb_auc = cross_validate_model(xgb, X_train, y_train)
# 5.2 随机森林
print("\n" + "="*50)
print("随机森林 交叉验证")
print("="*50)
rf = RandomForestClassifier(
n_estimators=200,
max_depth=8,
min_samples_split=5,
random_state=42,
class_weight='balanced'
)
rf_auc = cross_validate_model(rf, X_train, y_train)
# 5.3 GBDT
print("\n" + "="*50)
print("GBDT 交叉验证")
print("="*50)
gbdt = GradientBoostingClassifier(
n_estimators=200,
max_depth=5,
learning_rate=0.05,
subsample=0.8,
random_state=42
)
gbdt_auc = cross_validate_model(gbdt, X_train, y_train)
# 6. 超参数调优 (以XGBoost为例)
print("\n" + "="*50)
print("XGBoost 超参数调优")
print("="*50)
param_grid = {
'max_depth': [4, 6, 8],
'learning_rate': [0.01, 0.05, 0.1],
'subsample': [0.7, 0.8, 0.9]
}
xgb_tuned = GridSearchCV(
estimator=XGBClassifier(
n_estimators=200,
random_state=42,
eval_metric='auc'
),
param_grid=param_grid,
cv=3,
scoring='roc_auc',
n_jobs=-1,
verbose=1
)
xgb_tuned.fit(X_train, y_train)
print(f"最佳参数: {xgb_tuned.best_params_}")
print(f"最佳AUC: {xgb_tuned.best_score_:.4f}")
# 7. 验证集评估
print("\n" + "="*50)
print("验证集评估")
print("="*50)
best_model = xgb_tuned.best_estimator_
val_pred = best_model.predict_proba(X_val)[:, 1]
val_auc = roc_auc_score(y_val, val_pred)
print(f"验证集AUC: {val_auc:.4f}")
# 绘制ROC曲线
from sklearn.metrics import roc_curve
fpr, tpr, _ = roc_curve(y_val, val_pred)
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'AUC = {val_auc:.4f}')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.show()
# 8. 模型融合
print("\n" + "="*50)
print("模型融合")
print("="*50)
# 使用调优后的参数重新训练模型
xgb_final = XGBClassifier(**xgb_tuned.best_params_, n_estimators=200, random_state=42)
rf_final = RandomForestClassifier(n_estimators=200, max_depth=8, random_state=42)
gbdt_final = GradientBoostingClassifier(n_estimators=200, max_depth=5, learning_rate=0.05, random_state=42)
models = {
'XGBoost': xgb_final,
'RandomForest': rf_final,
'GBDT': gbdt_final
}
# 训练所有模型并记录验证集表现
for name, model in models.items():
model.fit(X_train, y_train)
pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, pred)
print(f"{name} 验证集AUC: {auc:.4f}")
# 加权平均融合 (权重根据验证集AUC分配)
weights = {
'XGBoost': 0.5,
'RandomForest': 0.3,
'GBDT': 0.2
}
final_pred = (
weights['XGBoost'] * xgb_final.predict_proba(X_val)[:, 1] +
weights['RandomForest'] * rf_final.predict_proba(X_val)[:, 1] +
weights['GBDT'] * gbdt_final.predict_proba(X_val)[:, 1]
)
final_auc = roc_auc_score(y_val, final_pred)
print(f"\n融合后验证集AUC: {final_auc:.4f}")
# 9. 测试集预测与提交
print("\n" + "="*50)
print("生成提交文件")
print("="*50)
# 使用全部训练数据重新训练最佳模型
best_model.fit(X, y)
# 模型融合预测
test_pred = (
weights['XGBoost'] * xgb_final.predict_proba(X_test)[:, 1] +
weights['RandomForest'] * rf_final.predict_proba(X_test)[:, 1] +
weights['GBDT'] * gbdt_final.predict_proba(X_test)[:, 1]
)
submission = test[['User_id', 'Coupon_id', 'Date_received']].copy()
submission['Probability'] = test_pred
submission.to_csv('submission.csv', index=False)
print('预测完成,结果已保存为 submission.csv')
代码分析
1. 数据准备与预处理
-
数据加载:读取了线下训练数据(
ccf_offline_stage1_train.csv)、线上训练数据和测试数据。 -
标签构造:定义了
get_label函数,根据优惠券接收日期(Date_received)和消费日期(Date)的关系构造标签:- 如果15天内消费则为正样本(1)
- 如果超过15天或未消费则为负样本(0)
- 如果没有收到优惠券则标记为-1并过滤掉
2. 特征工程
-
折扣率解析:
parse_discount函数将折扣信息解析为折扣率和是否为满减标志- 满减格式(如"100:10")转换为折扣率
- 直接折扣(如"0.8")直接使用
-
其他特征:
- 处理距离字段,将"null"替换为-1
- 从日期提取星期几和是否周末
- 添加用户、商家和优惠券的计数特征
3. 模型训练与评估
代码使用了三种集成学习模型并进行比较:
3.1 XGBoost
- 初始参数设置合理(n_estimators=200, max_depth=6等)
- 使用交叉验证评估模型性能
- 进行了超参数调优(GridSearchCV)
3.2 随机森林
- 设置了class_weight='balanced'处理类别不平衡
- 使用较大的树数量(n_estimators=200)和适当深度(max_depth=8)
3.3 GBDT(Gradient Boosting)
- 参数设置类似于XGBoost但实现方式不同
4. 模型融合
采用了加权平均融合策略:
- 根据各模型在验证集上的表现分配权重
- XGBoost权重最高(0.5),随机森林(0.3),GBDT(0.2)
- 融合后AUC有所提升