1. 项目简介
随着移动互联网的快速发展,O2O(Online to Offline)模式已成为电商领域的一大亮点。优惠券作为一种有效的营销工具,被广泛应用于吸引新客户和激活老用户。然而,传统的随机投放方式往往效率低下,不仅对用户造成干扰,还可能损害品牌形象。因此,个性化优惠券投放成为提高营销效果的关键。本文将详细介绍如何利用机器学习技术进行电商优惠券使用预测,以实现优惠券的精准投放。
2. 数据准备
2.1 数据来源与收集
本研究使用的数据集包括线下和线上两个部分。线下数据集包含用户ID、商户ID、优惠券ID、折扣率、距离、领券日期和消费日期等信息。线上数据集则包含用户ID、商户ID、行为类型、优惠券ID、折扣率、领券日期和消费日期等信息。
2.2 数据预处理
数据预处理是机器学习中的关键步骤。首先,我们需要处理缺失值,例如将字符串类型的缺失值替换为np.nan
。其次,对于异常值,如距离字段中的null
值,我们将其替换为-1,并转换为整数类型。最后,我们需要对数据类型进行转换,确保所有数值字段都是正确的数据类型。
# 处理缺失值和异常值
t2.replace('null', -1, inplace=True)
t2.distance = t2.distance.astype('int')
t2.replace(-1, np.nan, inplace=True)
3. 特征工程
特征工程是机器学习中提高模型性能的重要环节。我们从以下几个方面构建特征:
3.1 优惠券相关特征
-
优惠券类型(直接优惠为0,满减为1)
-
优惠券折率
-
满减优惠券的最低消费
-
历史出现次数
-
历史核销次数
-
历史核销率
-
历史核销时间率
-
领取优惠券是一周的第几天
-
领取优惠券是一月的第几天
-
历史上用户领取该优惠券次数
-
历史上用户消费该优惠券次数
-
历史上用户对该优惠券的核销率
def get_coupon_related_feature(dataset3, filename='coupon3_feature'):
# 计算折扣率函数
def calc_discount_rate(s):
s = str(s)
s = s.split(':')
if len(s) == 1:
return float(s[0])
else:
return 1.0 - float(s[1]) / float(s[0])# 提取满减优惠券中,满对应的金额 def get_discount_man(s): s = str(s) s = s.split(':') if len(s) == 1: return 'null' else: return int(s[0]) # 提取满减优惠券中,减对应的金额 def get_discount_jian(s): s = str(s) s = s.split(':') if len(s) == 1: return 'null' else: return int(s[1]) # 是不是满减卷 def is_man_jian(s): s = str(s) s = s.split(':') if len(s) == 1: return 0 else: return 1.0 # 周几领取的优惠券 dataset3['day_of_week'] = dataset3.date_received.astype('str').apply( lambda x: date(int(x[0:4]), int(x[4:6]), int(x[6:8])).weekday() + 1) # 每月的第几天领取的优惠券 dataset3['day_of_month'] = dataset3.date_received.astype('str').apply( lambda x: int(x[6:8])) # 领取优惠券的时间与当月初距离多少天 dataset3['days_distance'] = dataset3.date_received.astype('str').apply( lambda x: (date(int(x[0:4]), int(x[4:6]), int(x[6:8])) - date(2016, 6, 30)).days) # 满减优惠券中,满对应的金额 dataset3['discount_man'] = dataset3.discount_rate.apply(get_discount_man) # 满减优惠券中,减对应的金额 dataset3['discount_jian'] = dataset3.discount_rate.apply(get_discount_jian) # 优惠券是不是满减卷 dataset3['is_man_jian'] = dataset3.discount_rate.apply(is_man_jian) # 优惠券的折扣率(满减卷进行折扣率转换) dataset3['discount_rate'] = dataset3.discount_rate.apply(calc_discount_rate) # 特定优惠券的总数量 d = dataset3[['coupon_id']] d['coupon_count'] = 1 d = d.groupby('coupon_id').agg('sum').reset_index() dataset3 = pd.merge(dataset3, d, on='coupon_id', how='left') dataset3.to_csv(os.path.join('features', filename + '.csv'), index=None) return dataset3
3.2 商户相关特征
-
商家优惠券被领取次数
-
商家优惠券被领取后不核销次数
-
商家优惠券被领取后核销次数
-
商家优惠券被领取后核销率
-
商家优惠券核销的平均/最小/最大消费折率
-
核销商家优惠券的不同用户数量,及其占领取不同的用户比重
-
商家优惠券平均每个用户核销多少张
-
商家被核销过的不同优惠券数量
-
商家被核销过的不同优惠券数量占所有领取过的不同优惠券数量的比重
-
商家平均每种优惠券核销多少张
-
商家被核销优惠券的平均时间率
-
商家被核销优惠券中的平均/最小/最大用户-商家距离
def get_merchant_related_feature(feature3, filename='merchant3_feature'):
merchant3 = feature3[['merchant_id', 'coupon_id', 'distance', 'date_received', 'date']]# 提取不重复的商户集合 t = merchant3[['merchant_id']] t.drop_duplicates(inplace=True) # 商户的总销售次数 t1 = merchant3[merchant3.date != 'null'][['merchant_id']] t1['total_sales'] = 1 t1 = t1.groupby('merchant_id').agg('sum').reset_index() # 商户被核销优惠券的销售次数 t2 = merchant3[(merchant3.date != 'null') & (merchant3.coupon_id != 'null')][['merchant_id']] t2['sales_use_coupon'] = 1 t2 = t2.groupby('merchant_id').agg('sum').reset_index() # 商户发行优惠券的总数 t3 = merchant3[merchant3.coupon_id != 'null'][['merchant_id']] t3['total_coupon'] = 1 t3 = t3.groupby('merchant_id').agg('sum').reset_index() # 商户被核销优惠券的用户-商户距离,转化为int数值类型 t4 = merchant3[(merchant3.date != 'null') & (merchant3.coupon_id != 'null')][['merchant_id', 'distance']] t4.replace('null', -1, inplace=True) t4.distance = t4.distance.astype('int') t4.replace(-1, np.nan, inplace=True) # 商户被核销优惠券的最小用户-商户距离 t5 = t4.groupby('merchant_id').agg('min').reset_index() t5.rename(columns={'distance': 'merchant_min_distance'}, inplace=True) # 商户被核销优惠券的最大用户-商户距离 t6 = t4.groupby('merchant_id').agg('max').reset_index() t6.rename(columns={'distance': 'merchant_max_distance'}, inplace=True) # 商户被核销优惠券的平均用户-商户距离 t7 = t4.groupby('merchant_id').agg('mean').reset_index() t7.rename(columns={'distance': 'merchant_mean_distance'}, inplace=True) # 商户被核销优惠券的用户-商户距离的中位数 t8 = t4.groupby('merchant_id').agg('median').reset_index() t8.rename(columns={'distance': 'merchant_median_distance'}, inplace=True) # 合并上述特征 merchant3_feature = pd.merge(t, t1, on='merchant_id', how='left') merchant3_feature = pd.merge(merchant3_feature, t2, on='merchant_id', how='left') merchant3_feature = pd.merge(merchant3_feature, t3, on='merchant_id', how='left') merchant3_feature = pd.merge(merchant3_feature, t5, on='merchant_id', how='left') merchant3_feature = pd.merge(merchant3_feature, t6, on='merchant_id', how='left') merchant3_feature = pd.merge(merchant3_feature, t7, on='merchant_id', how='left') merchant3_feature = pd.merge(merchant3_feature
4. 数据集可视化分析
4.1 预测标签的类别分布

可以看出,标签为 1 的占比非常少,是一个类别极度不均衡的二分类问题。
4.2 特征相关性分析

4.3 商户的总销售次数分布情况

4.4 领取优惠券的时间与当月初距离天数分布

由于特征太多篇幅有限,此处只列出部分特征的分布可视化。
5. 训练集和验证集切分
由于比赛已结束,所以此处将手动切分出训练集、验证集、测试集,测试集用于不同模型的性能对比。
df_columns = dataset12_x.columns.values
print('===> feature count: {}'.format(len(df_columns)))
X_train, X_valid, y_train, y_valid = train_test_split(dataset12_x, dataset12_y, test_size=0.1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, test_size=0.1, random_state=42)
print('train: {}, valid: {}, test: {}'.format(X_train.shape[0], X_valid.shape[0], X_test.shape[0]))
===> feature count: 53
train: 327856, valid: 40477, test: 36429
6. Xgboost 建模预测
Xgboost是一种高效的梯度提升框架,它可以用来解决分类、回归等多种机器学习任务。Xgboost通过集成多个弱学习器(通常是决策树),并优化损失函数来提高模型的准确性。
xgb_params = {
'eta': 0.01,
'min_child_weight': 20,
'colsample_bytree': 0.5,
'max_depth': 15,
'subsample': 0.9,
'lambda': 2.0,
'eval_metric': 'auc',
'objective': 'binary:logistic',
'nthread': -1,
'silent': 1,
'booster': 'gbtree'
}
pre_xgb_model = xgb.train(dict(xgb_params),
dtrain,
evals=watchlist,
verbose_eval=50)
交叉验证获取最佳迭代次数:
print('---> cv train to choose best_num_boost_round')
cv_result = xgb.cv(dict(xgb_params),
dtrain,
num_boost_round=5000,
early_stopping_rounds=100,
verbose_eval=100,
show_stdv=False,
)
best_num_boost_rounds = len(cv_result)
mean_train_logloss = cv_result.loc[best_num_boost_rounds-11 : best_num_boost_rounds-1, 'train-auc-mean'].mean()
mean_test_logloss = cv_result.loc[best_num_boost_rounds-11 : best_num_boost_rounds-1, 'test-auc-mean'].mean()
print('best_num_boost_rounds = {}'.format(best_num_boost_rounds))
print('mean_train_auc = {:.7f} , mean_test_auc = {:.7f}\n'.format(mean_train_logloss, mean_test_logloss))
[0] train-auc:0.87954 test-auc:0.87309
[100] train-auc:0.90277 test-auc:0.89217
[200] train-auc:0.90981 test-auc:0.89533
[300] train-auc:0.91590 test-auc:0.89786
[400] train-auc:0.92089 test-auc:0.89978
[500] train-auc:0.92522 test-auc:0.90138
[600] train-auc:0.92873 test-auc:0.90252
[700] train-auc:0.93169 test-auc:0.90334
[800] train-auc:0.93411 test-auc:0.90396
[900] train-auc:0.93610 test-auc:0.90444
[1000] train-auc:0.93786 test-auc:0.90482
[1100] train-auc:0.93937 test-auc:0.90512
[1200] train-auc:0.94078 test-auc:0.90540
[1300] train-auc:0.94218 test-auc:0.90564
[1400] train-auc:0.94347 test-auc:0.90583
[1500] train-auc:0.94468 test-auc:0.90595
[1600] train-auc:0.94578 test-auc:0.90607
[1700] train-auc:0.94686 test-auc:0.90616
[1800] train-auc:0.94787 test-auc:0.90626
[1900] train-auc:0.94886 test-auc:0.90632
[2000] train-auc:0.94986 test-auc:0.90636
6.1 特征重要程度分析

6.2 性能评估
# predict train
predict_train = xgb_model.predict(dtrain)
after_xgb_train_auc = evaluate_score(predict_train, y_train)
# predict validate
predict_valid = xgb_model.predict(dvalid)
after_xgb_valid_auc = evaluate_score(predict_valid, y_valid)
dtest = xgb.DMatrix(X_test, feature_names=df_columns)
predict_test = xgb_model.predict(dtest)
after_xgb_test_auc = evaluate_score(predict_test, y_test)
print('训练集 auc = {:.7f} , 验证集 auc = {:.7f} , 测试集 auc = {:.7f}\n'.format(
after_xgb_train_auc, after_xgb_valid_auc, after_xgb_test_auc))
训练集 auc = 0.9042264 , 验证集 auc = 0.8958611 , 测试集 auc = 0.8960916
6.3 调参前后模型性能对比

可以看出,调参后,训练集、验证集和测试集的 AUC 都得到了不同程度的提升、
6.4 预测性能 ROC 曲线

7. 随机森林(RandomForest)建模预测
随机森林是一种集成学习方法,它通过构建多个决策树并将它们的预测结果进行汇总来提高整体模型的性能。随机森林在处理高维数据时表现出色,并且对于过拟合具有一定的抵抗力。
用RandomSearch+CV选取超参数:
# 建立一个分类器或者回归器
rf_clf = RandomForestClassifier()
# 给定参数搜索范围:list or distribution
param_dist = {
"n_estimators": [100, 500, 1000, 1500, 2000],
"max_depth": [3, 5, 8, 12, 15],
"max_features": [2, 5, 10,],
"min_samples_split": [2, 4, 6, 8, 10, 12],
"bootstrap": [True, False],
"criterion": ["gini", "entropy"],
}
n_iter_search = 20
random_search_cv = RandomizedSearchCV(rf_clf, param_distributions=param_dist, n_iter=n_iter_search, cv=5, n_jobs=-1, verbose=1)
最佳参数训练 RF 模型:
rf_model = RandomForestClassifier(
n_estimators=3000, criterion='gini', max_depth=12,
min_samples_split=1000, min_samples_leaf=6, min_weight_fraction_leaf=0.0,
max_features='sqrt', max_leaf_nodes=None, min_impurity_decrease=0.0,
bootstrap=True, n_jobs=-1, random_state=42,
verbose=1, warm_start=False,
max_samples=None
)

训练集 auc = 0.6242629 , 验证集 auc = 0.6232508 , 测试集 auc = 0.6194697
8. Stochastic Gradient Descent(SGD算法)
SGD是一种优化算法,它通过随机选择样本来更新模型参数,从而减少计算量并加快收敛速度。SGD适用于大规模和在线机器学习任务。
同样的方法,测试 SGD 算法建模预测性能,此处省略。
9. 模型对比
import matplotlib.pyplot as plt
import numpy as np
species = ['训练集', '验证集', '测试集']
penguin_means = {
'Xgboost': (xgb_train_auc, xgb_valid_auc, xgb_test_auc),
'RandomForest': (rf_train_auc, rf_valid_auc, rf_test_auc),
'SGD': (sgd_train_auc, sgd_valid_auc, sgd_test_auc),
}
xgb_train_auc
x = np.arange(len(species))
width = 0.25
multiplier = 0
plt.figure(figsize=(40, 20))
fig, ax = plt.subplots(layout='constrained', figsize=(30, 15))
for attribute, measurement in penguin_means.items():
offset = width * multiplier
rects = ax.bar(x + offset, measurement, width, label=attribute)
ax.bar_label(rects, padding=3, fontsize=26)
multiplier += 1
ax.set_ylabel('数据集', fontsize=26)
ax.set_title('不同模型的评测性能对比', fontsize=40)
ax.set_xticks(x + width, species, fontsize=26)
ax.legend(loc='upper left', fontsize=26)
ax.set_ylim(0, 1.5)
plt.show()

我们比较了Xgboost、随机森林和SGD三种模型的性能。结果显示,Xgboost模型在训练集、验证集和测试集上的AUC值均高于其他两种模型。
10. 结论
通过对用户行为和优惠券使用情况的分析,我们构建了一个基于机器学习的优惠券使用预测模型。该模型能够有效地预测用户是否会核销他们收到的优惠券,从而帮助企业更精准地进行营销活动。未来的工作可以进一步优化特征选择、调整模型参数,或者尝试其他类型的机器学习算法以提升预测准确性。