o2o优惠券使用预测

77 阅读7分钟

题目

根据提供的用户在1月1日至6月30日之间真实线上线下消费行为,预测用户在7月领取优惠券后15天以内的使用情况。

评价方式

    本赛题目标是预测投放的优惠券是否核销。使用优惠券核销预测的平均AUC(ROC曲线下面积)作为评价标准。 即对每个优惠券coupon_id单独计算核销预测的AUC值,再对所有优惠券的AUC值求平均作为最终的评价标准。

字段表

Table 1: 用户线下消费和优惠券领取行为

FieldDescription
User_id用户ID
Merchant_id商户ID
Coupon_id优惠券ID:null表示无优惠券消费,此时Discount_rate和Date_received字段无意义
Discount_rate优惠率:x \in [0,1]代表折扣率;x:y表示满x减y。单位是元
Distanceuser经常活动的地点离该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: 用户线上点击/消费和优惠券领取行为

FieldDescription
User_id用户ID
Merchant_id商户ID
Action0 点击, 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线下优惠券使用预测样本

FieldDescription
User_id用户ID
Merchant_id商户ID
Coupon_id优惠券ID
Discount_rate优惠率:x \in [0,1]代表折扣率;x:y表示满x减y.
Distanceuser经常活动的地点离该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为预测值

FieldDescription
User_id用户ID
Coupon_id优惠券ID
Date_received领取优惠券日期
Probability15天内用券概率

代码

# 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有所提升

预测结果

image.png