[toc]
数据集来源
2021中国大学生保险数字挑战赛-数字赛道(2021年04月26开赛)
2021年中国大学生保险数字挑战赛”,是由中国银行保险监督管理委员会主管唯一工作日报《中国银行保险报》、中国保险学会、中国平安财产保险组成的大赛组委会主办,深圳市大数据研究院协办,知乎作为内容合作平台的校园综合赛事项目。
赛题描述
基于赛事官方提供的数据及建模分析平台,参赛者需要充分运用数据中所含的信息并结合非车保险的业务特点,进行合理的数据预处理并完成数据处理及分析,预测每一个标的(carid)对于非车产品的购买意向。(决赛赛题为保险业务相关问题并将加大难度,赛事具体细节将在复赛晋级完成后公布)
比赛结果
经过一周的的努力,最好的成绩是auc=0.90001787,名次:( 20/314)。
数据预处理
初探数据
- 读入训练和测试数据
df_train = pd.read_csv('/home/mw/input/pre8881/train.csv',index_col=0)
df_test = pd.read_csv('/home/mw/input/pretest_a3048/test_a.csv',index_col=0)
df_train.shape,df_test.shape
((684283, 65), (186925, 64))
其中训练集684283条,测试集186925。包含ylabel共65维度数据。赛题并没有给出具体的字段含义,这也加大了对赛题的理解难度。
Index(['dpt', 'xz', 'xb', 'carid', 'nprem_ly', 'ncd_ly', 'newvalue',
'bi_renewal_year', 'clmnum', 'regdate', 'trademark_cn', 'brand_cn',
'make_cn', 'series', 'capab', 'seats', 'use_type', 'change_owner',
'nprem_od', 'si_od', 'nprem_tp', 'si_tp', 'nprem_bt', 'si_bt',
'nprem_vld', 'si_vld', 'nprem_vlp', 'si_vlp', 'p1_prior_days_to_insure',
'suiche_nonauto_nprem_20', 'suiche_nonauto_nprem_19',
'suiche_nonauto_nprem_18', 'suiche_nonauto_nprem_17',
'suiche_nonauto_nprem_16', 'suiche_nonauto_amount_20',
'suiche_nonauto_amount_19', 'suiche_nonauto_amount_18',
'suiche_nonauto_amount_17', 'suiche_nonauto_amount_16',
'num_notcar_claim', 'p1_gender', 'p1_age', 'p1_census_register',
'p2_marital_status', 'f1_child_flag', 'f2_posses_house_flag',
'f2_cust_housing_price_total', 'p2_client_grade', 'w1_pc_wx_use_flag',
'p1_is_bank_eff', 'p2_is_enterprise_owner', 'p2_is_smeowner',
'active_7', 'active_30', 'active_90', 'active_365',
'p2_is_child_under_15_family', 'p2_is_adult_over_55_family',
'birth_month', 'p1_service_offer_cnt', 'p3_service_use_cnt',
'dur_personal_insurance_90', 'service_score_available',
'y1_is_purchase'],
dtype='object')
dtypes: float64(32), int64(9), object(23)
这个65个字段中,32个是float64,9个int64,object类型23个
缺失值填充
- 查看缺失值最多的20个字段
df_train.isnull().sum().sort_values(ascending=False)[:20]
字段为数字类型的用众数填充缺失值,标称属性暂不填充,使用lightgbm会将标称属性空值分为一类
columns = df_train.columns
for i in tqdm (range(len(columns))):
# print(df_train[columns[i]].dtype)
if df_train[columns[i]].dtype!='object' and columns[i]!='y1_is_purchase' :
# print(columns[i])
df_train[columns[i]].fillna(df_train[columns[i]].mode()[0], inplace=True)
df_test[columns[i]].fillna(df_test[columns[i]].mode()[0], inplace=True)
标称属性编码
LabelEncoder是用来对分类型特征值进行编码,即对不连续的数值或文本进行编码
# 其他类别变量,直接类别编码
no_features = ['client_no', 'carid','y1_is_purchase']
data = pd.concat([df_train, df_test], axis=0)
for col in df_train.select_dtypes(include=['object']).columns:
if col not in no_features:
lb = LabelEncoder()
lb.fit(data[col].astype(str))
df_train[col] = lb.transform(df_train[col].astype(str))
df_test[col] = lb.transform(df_test[col].astype(str))
features = [col for col in df_train.columns if col not in no_features]
features
处理日期数据
先将regdate转换成转换成datetime类型,再获取年份数据
df_train['regdate'] = pd.to_datetime(df_train['regdate'])
df_test['regdate'] = pd.to_datetime(df_test['regdate'])
df_train['regdate'] = df_train['regdate'].dt.year
df_test['regdate'] = df_test['regdate'].dt.year
添加属性
统计一下['dpt'], ['client_no'], ['trademark_cn'], ['brand_cn'], ['make_cn']字段属性值一共有多少个
# 计数
for f in [['dpt'], ['client_no'], ['trademark_cn'], ['brand_cn'], ['make_cn'], ['series']]:
df_temp = df_feature.groupby(f).size().reset_index()
df_temp.columns = f + ['{}_count'.format('_'.join(f))]
df_feature = df_feature.merge(df_temp, how='left')
统计一下['p1_census_register', 'dpt']字段不同属性在训练集中是购买的的平均值
# 简单统计
def stat(df, df_merge, group_by, agg):
group = df.groupby(group_by).agg(agg)
columns = []
for on, methods in agg.items():
for method in methods:
columns.append('{}_{}_{}'.format('_'.join(group_by), on, method))
group.columns = columns
group.reset_index(inplace=True)
df_merge = df_merge.merge(group, on=group_by, how='left')
del (group)
gc.collect()
return df_merge
def statis_feat(df_know, df_unknow):
for f in tqdm(['p1_census_register', 'dpt']):
df_unknow = stat(df_know, df_unknow, [f], {
'y1_is_purchase': ['mean']})
return df_unknow
数据分析
样本均衡性分析
fig = plt.figure()
plt.pie(df_train['y1_is_purchase'].value_counts(),
labels=df_train['y1_is_purchase'].value_counts().index,
autopct='%1.2f%%',counterclock = False)
plt.title('购买率')
plt.show()
# flag_0 = df_train['y1_is_purchase']==0
# n = sum(flag_0)
# flag_1 = df_train['y1_is_purchase']==1
# n,sum(flag_1)
# df_train[flag_0].shape
# df_train = pd.concat([df_train[flag_0][:n], df_train[flag_1][:n*1.5]])
# # df_train = df_train.reset_index()
# df_train['y1_is_purchase'].value_counts()
很明显数据是不是均衡的,立马想到了下采样和过采样,但效果反而不及原来的数据数据的好。
数据分布分析
通过箱线图查看数值字段分布情况,将明显偏离的数据删除
columns = df_train.columns.tolist()
fig = plt.figure(figsize=(80,60),dpi=75)
j = 0
for i in range(len(columns)):
if not df_train[columns[i]].dtype=='object':
j+=1
plt.subplot(7,10,j+1)
sns.boxplot(df_train[columns[i]],orient='v',width=0.5)
plt.ylabel(columns[i],fontsize=36)
plt.show()
train_rows = len(df_train.columns)
plt.figure(figsize=(10*4,10*4))
columns = df_train.columns.tolist()
fig = plt.figure(figsize=(80,60),dpi=75)
j = 0
for i in range(len(columns)):
# print(df_train[columns[i]].dtype)
if df_train[columns[i]].dtype=='int64' and columns[i]!='y1_is_purchase':
# print(df_train[columns[i]].dtype)
df_train[columns[i]].fillna(df_train[columns[i]].mode(), inplace=True)
df_test[columns[i]].fillna(df_test[columns[i]].mode(), inplace=True)
j+=1
ax = plt.subplot(4,4,j)
sns.distplot(df_train[columns[i]],fit=stats.norm)
j+=1
ax = plt.subplot(4,4,j)
stats.probplot(df_train[columns[i]],plot=plt)
plt.tight_layout()
plt.show()
模型应用
决策树
决策树是一种基本的分类与回归方法。决策树模型具有分类速度快,模型容易可视化的解释,但是同时是也有容易发生过拟合,虽然有剪枝,但也是差强人意。
提升方法(boosting)在分类问题中,它通过改变训练样本的权重(增加分错样本的权重,减小分队样本的的权重),学习多个分类器,并将这些分类器线性组合,提高分类器性能。boosting 数学表示为:
其中 w 是权重, 是弱分类器的集合,可以看出最终就是基函数的线性组合。
于是决策树与 boosting 结合产生许多算法,主要有提升树、GBDT 等
Gradient Boosting是一种Boosting的方法,它主要的思想是,每一次建立模型是在之前建立模型损失函数的梯度下降方向。损失函数是评价模型性能(一般为拟合程度+正则项),认为损失函数越小,性能越好。而让损失函数持续下降,就能使得模型不断改性提升性能,其最好的方法就是使损失函数沿着梯度方向下降(讲道理梯度方向上下降最快)。
lightgbm
Gradient Boost是一个框架,里面可以套入很多不同的算法。
- 基于Histogram的决策树算法。
直方图算法的基本思想:先把连续的浮点特征值离散化成k个整数,同时构造一个宽度为k的直方图。遍历数据时,根据离散化后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分割点。
- 单边梯度采样 Gradient-based One-Side Sampling(GOSS):使用GOSS可以减少大量只具有小梯度的数据实例,这样在计算信息增益的时候只利用剩下的具有高梯度的数据就可以了,相比XGBoost遍历所有特征值节省了不少时间和空间上的开销。
- 互斥特征捆绑 Exclusive Feature Bundling(EFB):使用EFB可以将许多互斥的特征绑定为一个特征,这样达到了降维的目的。 *带深度限制的Leaf-wise的叶子生长策略:大多数GBDT工具使用低效的按层生长 (level-wise) 的决策树生长策略,因为它不加区分的对待同一层的叶子,带来了很多没必要的开销。实际上很多叶子的分裂增益较低,没必要进行搜索和分裂。LightGBM使用了带有深度限制的按叶子生长 (leaf-wise) 算法。
- 直接支持类别特征(Categorical Feature)
- 支持高效并行
- Cache命中率优化
```python
ycol = 'y1_is_purchase'
feature_names = features
model = lgb.LGBMClassifier(objective='binary',
boosting_type='gbdt',
num_leaves=64,
max_depth=10,
learning_rate=0.01,
n_estimators=10000,
subsample=0.8,
feature_fraction=0.6,
reg_alpha=10,
reg_lambda=12,
random_state=seed,
is_unbalance=True,
metric='auc')
df_oof = df_train[['carid', ycol]].copy()
df_oof['prob'] = 0
prediction = df_test[['carid']]
prediction['prob'] = 0
df_importance_list = []
kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=seed)
for fold_id, (trn_idx, val_idx) in enumerate(
kfold.split(df_train[feature_names], df_train[ycol])):
X_train = df_train.iloc[trn_idx][feature_names]
Y_train = df_train.iloc[trn_idx][ycol]
X_val = df_train.iloc[val_idx][feature_names]
Y_val = df_train.iloc[val_idx][ycol]
print('\nFold_{} Training ================================\n'.format(
fold_id + 1))
# print(df_oof.loc[val_idx]['prob'])
lgb_model = model.fit(X_train,
Y_train,
eval_names=['train', 'valid'],
eval_set=[(X_train, Y_train), (X_val, Y_val)],
verbose=100,
early_stopping_rounds=50)
pred_val = lgb_model.predict_proba(
X_val, num_iteration=lgb_model.best_iteration_)[:, 1]
# print(df_oof.iloc[val_idx])
df_oof.loc[val_idx, 'prob'] = pred_val
pred_test = lgb_model.predict_proba(
df_test[feature_names], num_iteration=lgb_model.best_iteration_)[:, 1]
prediction['prob'] += pred_test / kfold.n_splits
df_importance = pd.DataFrame({
'column': feature_names,
'importance': lgb_model.feature_importances_,
})
df_importance_list.append(df_importance)
del lgb_model, pred_val, pred_test, X_train, Y_train, X_val, Y_val
gc.collect()
模型评估
五折交叉验证
贝叶斯调参
贝叶斯优化用于机器学习调参由J. Snoek(2012)提出,主要思想是,给定优化的目标函数(广义的函数,只需指定输入和输出即可,无需知道内部结构以及数学性质),通过不断地添加样本点来更新目标函数的后验分布(高斯过程,直到后验分布基本贴合于真实分布。简单的说,就是考虑了上一次参数的信息**,从而更好的调整当前的参数。
他与常规的网格搜索或者随机搜索的区别是:
- 贝叶斯调参采用高斯过程,考虑之前的参数信息,不断地更新先验;网格搜索未考虑之前的参数信息
- 贝叶斯调参迭代次数少,速度快;网格搜索速度慢,参数多时易导致维度爆炸
- 贝叶斯调参针对非凸问题依然稳健;网格搜索针对非凸问题易得到局部优最
| 学习控制参数 | 含义 | 用法 |
|---|---|---|
max_depth | 树的最大深度 | 当模型过拟合时,可以考虑首先降低 max_depth |
min_data_in_leaf | 叶子可能具有的最小记录数 | 默认20,过拟合时用 |
feature_fraction | 例如 为0.8时,意味着在每次迭代中随机选择80%的参数来建树 | boosting 为 random forest 时用 |
bagging_fraction | 每次迭代时用的数据比例 | 用于加快训练速度和减小过拟合 |
early_stopping_round | 如果一次验证数据的一个度量在最近的early_stopping_round 回合中没有提高,模型将停止训练 | 加速分析,减少过多迭代 |
| lambda | 指定正则化 | 0~1 |
min_gain_to_split | 描述分裂的最小 gain | 控制树的有用的分裂 |
max_cat_group | 在 group 边界上找到分割点 | 当类别数量很多时,找分割点很容易过拟合时 |
```python
# 设置几个参数
def lgb_cv(colsample_bytree, min_child_samples,reg_alpha,reg_lambda,min_split_gain,subsample_freq,
num_leaves, subsample, max_depth,learning_rate,min_child_weight):
model = lgb.LGBMClassifier(boosting_type='gbdt',objective='binary',
colsample_bytree=float(colsample_bytree), learning_rate=float(learning_rate),
min_child_samples=int(min_child_samples), min_child_weight=float(min_child_weight),
min_split_gain = float(min_split_gain),subsample_freq = int(subsample_freq),
n_estimators=8000, n_jobs=-1, num_leaves=int(num_leaves),
random_state=None, reg_alpha=float(reg_alpha), reg_lambda=float(reg_lambda),max_depth=int(max_depth),
subsample=float(subsample))
cv_score = cross_val_score(model, x, y, scoring="f1", cv=5).mean()
return cv_score
# 使用贝叶斯优化
lgb_bo = BayesianOptimization(
lgb_cv,
{
'learning_rate': (0.008, 0.01),
'max_depth': (3, 10),
'num_leaves': (31, 127),
'min_split_gain':(0.0,0.4),
'min_child_weight':(0.001,0.002),
'min_child_samples':(18,22),
'subsample':(0.6,1.0),
'subsample_freq':(3,5),
'colsample_bytree':(0.6,1.0),
'reg_alpha':(0,0.5),
'reg_lambda':(0,0.5)
},
)
lgb_bo.maximize(n_iter=1000)
lgb_bo.max
# 将优化好的参数带入进行使用
决策过程可视化
from IPython.display import Image
model = lgb.LGBMClassifier(num_leaves=64,
max_depth=10,
learning_rate=0.01,
boosting_type = 'goss',
n_estimators=10000,
subsample=0.8,
feature_fraction=0.8,
reg_alpha=0.5,
reg_lambda=0.5,
random_state=seed,
metric="f1")
X_train = df_train[feature_names]
Y_train = df_train[ycol]
lgb_model = model.fit(X_train,
Y_train,
eval_names=['valid'],
eval_set=[(X_val, Y_val)],
verbose=10,
eval_metric='auc',
early_stopping_rounds=50)
import matplotlib.pyplot as plt
fig2 = plt.figure(figsize=(200, 200))
ax = fig2.subplots()
lgb.plot_tree(lgb_model, tree_index=1, ax=ax)
plt.show()
ROC曲线
各个属性重要程度分布
通过在决策的过程中查看各属性的重要程度,再对排名前几的字段进一步做特征工程
模型融合
通过简单加权的方式,将三个模型的结果融合,从而避免模型的过拟合问题
w_lgb = 0.333
w_xgb = 0.333
w_cbt = 0.333
oof['prob'] = oof['prob_lgb'] ** w_lgb * oof['prob_xgb'] ** w_xgb * oof['prob_cbt'] ** w_cbt
auc = roc_auc_score(oof['y1_is_purchase'], oof['prob'])
sub['label'] = sub['prob_lgb'] ** w_lgb * sub['prob_xgb'] ** w_xgb * sub['prob_cbt'] ** w_cbt
sub.head()
查看融合后的结果
总结
由于是第一次正式参加比赛,面对60w条数据,也有点不知所措的感觉。慢慢的边查资料边回顾课堂上讲的内容。从特征工程,到模型选择,到参数优化。一开始用决策树构建了自己baseline。再到梯度提升树,最终将XGBoost,LightGBM和CatBoost三个模型的融合。也在本次试验过程中学会了使用贝叶斯参数优化。本次实验的设计可能并不完美,也确实存在很多问题,在之后我们会进行改正和提高。实验虽然告一段落,但学习之路还很长,会一步步走下去,不断提高自己。
参考资料
LightGBM: A Highly Efficient Gradient Boosting Decision Tree
Biau G, Devroye L, Lugosi G. Consistency of Random Forests and Other Averaging Classifiers.[J]. Journal of Machine Learning Research, 2008, 9(1):2015-2033.
决策树模型,XGBoost,LightGBM和CatBoost模型可视化 LightGBM算法总结
Frazier P I. A tutorial on Bayesian optimization[J]. arXiv preprint arXiv:1807.02811, 2018.