为期2周的比赛,最后b榜线上AUC0.7765,排名130,不知道第一名是多少,但看群聊,10名左右的人成绩是0.7842,差一个百分点,就与大奖差之千里啊,我还是太菜了=-=。
不过,还是很开心的,以前学了一堆机器学习算法,现在这个比赛正好实战了一下。以前知道特征重要,现在真正体验到特征是多么的重要。话不多说,直入主题了
目录
一、赛题描述
二、方案介绍
2.1 EDA
2.2 特征工程
2.3 构建模型
2.4 调参
2.5 进阶
三、总结
3.1 个人参赛总结
3.2 赛后学习优秀开源代码
主办方提供了两个数据集(训练数据集和评分数据集),包含用户标签数据、过去60天的交易行为数据、过去30天的APP行为数据。希望参赛选手基于训练数据集,通过有效的特征提取,构建信用违约预测模型,并将模型应用在评分数据集上,输出评分数据集中每个用户的违约概率。评分指标为AUC:
自己真是个铁憨憨,还自己写了个AUC的计算函数,想着反正数据量不大,就写了个双重循环来做,其实直接调用from sklearn.metrics import roc_auc_score就好了,算出来的结果一模一样。
def auc(df):
# df:a dateframe,column 'preds' is predict probs,'flag' is true label
f_plus = df[df['flag']==1]['preds'] # 违约
f_minus = df[df['flag']==0]['preds']
s = 0
for x in f_plus:
for y in f_minus:
if x>y:
s += 1
elif x==y:
s += 0.5
else:
pass
s /= (len(f_plus)*len(f_minus))
return s
不过这个AUC的计算也恰恰说明其物理含义是:任意取一个正样本和负样本,正样本得分大于负样本的概率。
方案总览:
其中建模和特征工程存在循环,即根据模型表现在决定特征。
1 label的分布,在训练集中是30970:8953,不平衡
2 数据字段类型 df.info()
3 数据字段分布 df.describe()
3 异常值、缺失值。比如有个工作年限99年的,这绝对是异常了,这种类型的异常值不多,修正与否对模型结果影响并不大,大概是这种类型的异常比较少吧,不到10行。缺失值是有的字段存在"~","nan","\\N"这种,全被我替换为np.nan了,在python3里面,np.nan是float类型的,所以有缺失值的字段的数据类型就全变为float了。
4 训练集与测试集的分布差异。我对比了训练集和测试集在各个字段的分布,发现有些字段相差甚大,所以对字段取log来尽可能的让分布差异不要那么大,但并没有带来A榜效果的提升,线下五折效果倒是提升了。
5 训练集和测试集label的分布。训练集约为4:1,但模型预测出来的label分布都差不多11:1了(阈值为0.5的话),我尝试着用过采样或者改一下scale_pos_weight,来使得预测结果的label分布约4:1,效果下降了=-=我断定,评分数据集的label分布与训练数据集不一样
三个表,每个表都构建了特征,最后合在一起得到最终的特征表,每一行对应于一个用户的特征,根据这些特征预测这个用户违约的概率。查阅了一些资料,毕竟风控类的比赛,贷款违约预测的比赛有些多啊=-=
前期主要就是些原始特征,后期加了些时序特征,其中有用的特征大致如下,
tag表:用户的基本信息表,字段deg_cd(学位)的缺失率达96.8%,缺失太多了,删掉。其余的特征保留。
trd表:交易行为表,主要特征如下,其实主要就是些统计信息,主要是挖掘交易金额,交易金额与交易时间、交易类型的交互。
其中①②属于对交易金额的挖掘,③④属于金额与时间的交互,⑤属于金额与交易类型的交互。
①总金额的Log值
②交易金额的小数部分,这个特征挺有用的,也许是否是整数交易有特别的含义吧,像一个人如果买一些大额商品,比如家电等都是整数交易的,如果常去超时,那一般都是非整数交易。
data['trx_amt_decimal'] = ((data['cny_trx_amt'] - data['cny_trx_amt'].astype(int)) * 1000).astype(int)
③分别计算最近3天、7天、30天、60天的最大金额、最小金额、金额的平均值、金额的标准差、交易次数、总金额、总收入、收入次数、总支出、支出次数。其中金额的平均值和标准差的重要度挺大的。
④对交易时间做一些挖掘。分别按周几、是否周末、小时进行分组,计算组内次数。
⑤对交易类型做一些挖掘。分别按交易方向、支付方式、收支一级分类代码进行分组统计,统计组内交易总额和交易次数。
beh表:app行为表,这部分数据的处理方式与trd表类似,但加上后效果没有提升或者提高很少,所以最终没有使用这部分数据。之所以不好,大概是由于这部分数据只涉及一小部分用户,所以加上导致了很多数据缺失吧。
(ps. trd表特征的加入提高了约8个百分点。)
也许有人会说,这些特征很多都是强相关的啊,比如总额/交易次数=平均金额,没错,确实是这样,所以如果采用像逻辑回归一类的模型的话,会因为多重共线性而导致效果不好,需要进行精细的特征选择。而本文采取的是树模型,特征是否相关对模型的效果没有任何影响,(虽然会影响特征的重要度)。极端的来说,比如现在有个特征age,我把age复制一下成为一个新的特征age_copy,那这两列特征就是完全相关的,但对树模型而言,模型的性能丝毫不会降低或者升高,只不过在分裂的时候,有时候使用age,有时候使用age_copy而已。
根据我对机器学习模型“精度+速度”的了解,以及类别特征占总特征的比例,选用了lightgbm模型。emm用起来也很方便,官方文档比那些二手博客好用且准确多了,直接上代码,如下。
需要注意的是,
①LightGBM可以直接处理分类特征,只要把参数categorical_feature设置下就好了,但要注意,这部分数据的数据类型必须是'category',即df[cat] = df[cat].astype('category')。
②验证集的选取至关重要,要使得线下验证集的评测结果和线上评测结果尽可能接近且趋势一致,即两者分布类似。经过试验,cv5比cv10的效果好,cv10会过拟合。
from sklearn.model_selection import StratifiedKFold
from sklearn.utils import shuffle
random_seed = 19950413
params = {
'num_leaves': 62,
'min_data_in_leaf': 174,
'min_child_weight': 0.06754699429052921,
'bagging_fraction': 0.6175808529241882,
'feature_fraction': 0.4149706165984789,
'learning_rate': 0.008252485997984926,
'max_depth': -1,
'reg_alpha': 0.12292704650786135,
'reg_lambda': 0.01601284792461355,
'objective': 'binary',
'seed': random_seed,
'feature_fraction_seed': random_seed,
'bagging_seed': random_seed,
'drop_seed': random_seed,
'data_random_seed': random_seed,
'boosting_type': 'gbdt',
'verbose': 1,
# 'scale_pos_weight':0.333,
# 'is_unbalance': True,
'boost_from_average': True,
'metric': 'auc'}
x = train_tag[x_col]
y = train_tag['flag']
train_model_pred = np.zeros(len(train_tag))
test_model_pred = np.zeros(len(test_tag))
n_cv = 5
kf = StratifiedKFold(n_splits=n_cv, shuffle=True, random_state=666)
for train_index, test_index in kf.split(x, y):
print('*********************')
train, valid = train_tag.iloc[train_index], train_tag.iloc[test_index]
lgb_train = lgb.Dataset(train[x_col], train['flag'])
lgb_eval = lgb.Dataset(valid[x_col], valid['flag'], reference=lgb_train)
# cat:the list of columns name
clf = lgb.train(params, lgb_train, 2000, valid_sets = [lgb_train,lgb_eval], verbose_eval=100, early_stopping_rounds=100,
categorical_feature=cat)
clf.predict(valid[x_col],num_iteration=clf.best_iteration)
train_model_pred[test_index] = clf.predict(valid[x_col],num_iteration=clf.best_iteration)
test_model_pred += clf.predict(test_tag[x_col],num_iteration=clf.best_iteration)
train_tag['preds'] = train_model_pred
test_tag['preds'] = test_model_pred/n_cv
这里StratifiedKFold的random_state=666的原因是,采用这个随机种子,线下5折的模型表现和线上表现的变化趋势是一致的。有些随机种子,比如2022可以让线下效果提升不少,线上反而下降。
有了模型以后,我们可以看下特征的重要性,代码如下
feature_importances = pd.DataFrame()
feature_importances['feature'] = x_col
feature_importances['import'] = clf.feature_importance()
plt.figure(figsize=(32, 32))
sns.barplot(data=feature_importances.sort_values(by='import', ascending=False), x='import', y='feature');
部分图如下
可以结合这个图,考虑删去一些不重要的特征,再看看模型效果。
人工调参,随机搜索,网格搜索,贝叶斯调参 都挺好用的,鉴于要调的参数有6个,每个参数的范围又比较大,所以选用贝叶斯调参。通过调参,提高了约0.5个百分点。不过这有可能是因为我的初始参数就很好(我拷贝了网上相似任务的lightgbm模型的参数,毕竟刚开始我不会贝叶斯调参),要是初始参数不好,那估计会提高更多百分点吧。
下面来说说贝叶斯调参的原理。
自变量是要调整的超参数X=(x1,x2,x3,……),因变量是y=F(X)是衡量这组参数好不好的评价指标。放在本文的问题里,y就是五折交叉验证的auc得分。
# 超参数
bounds_LGB = {
'num_leaves': (20, 500),
'min_data_in_leaf': (20, 200),
'bagging_fraction' : (0.1, 0.9),
'feature_fraction' : (0.1, 0.9),
'learning_rate': (0.001, 0.01),
'min_child_weight': (0.0001, 0.1),
'reg_alpha': (0, 2),
'reg_lambda': (0, 2),
}
# F(x)
def LGB_bayesian(
learning_rate,
num_leaves,
bagging_fraction,
feature_fraction,
min_child_weight,
min_data_in_leaf,
# max_depth,
reg_alpha,
reg_lambda,
# scale_pos_weight
):
# LightGBM expects next three parameters need to be integer.
num_leaves = int(num_leaves)
min_data_in_leaf = int(min_data_in_leaf)
# max_depth = int(max_depth)
assert type(num_leaves) == int
assert type(min_data_in_leaf) == int
# assert type(max_depth) == int
random_seed = 19950413
param = {
'num_leaves': num_leaves,
'min_data_in_leaf': min_data_in_leaf,
'min_child_weight': min_child_weight,
'bagging_fraction': bagging_fraction,
'feature_fraction': feature_fraction,
'learning_rate': learning_rate,
'max_depth': -1,
'reg_alpha': reg_alpha,
'reg_lambda': reg_lambda,
'objective': 'binary',
'seed': random_seed,
'feature_fraction_seed': random_seed,
'bagging_seed': random_seed,
'drop_seed': random_seed,
'data_random_seed': random_seed,
'boosting_type': 'gbdt',
'verbose': 1,
# 'is_unbalance': True,
# 'scale_pos_weight':scale_pos_weight,
'boost_from_average': True,
'metric': 'auc'}
x = train_tag[x_col]
y = train_tag['flag']
train_model_pred = np.zeros(len(train_tag))
n_cv = 5
kf = StratifiedKFold(n_splits=n_cv, shuffle=True, random_state=666)
for train_index, test_index in kf.split(x, y):
train, valid = train_tag.iloc[train_index], train_tag.iloc[test_index]
# tmp = train[train['flag']==1]
# train = train.append(tmp,ignore_index=True).append(tmp,ignore_index=True)
# train = shuffle(train,random_state=666)
lgb_train = lgb.Dataset(train[x_col], train['flag'])
lgb_eval = lgb.Dataset(valid[x_col], valid['flag'], reference=lgb_train)
clf = lgb.train(param, lgb_train, 2000, valid_sets=[lgb_train, lgb_eval], verbose_eval=0,
early_stopping_rounds=100, categorical_feature=cat)
train_model_pred[test_index] = clf.predict(valid[x_col], num_iteration=clf.best_iteration)
score = roc_auc_score(train_tag['flag'], train_model_pred)
return score
贝叶斯调参基于高斯过程,它直接表示函数F的分布,是非参数模型。(而像线性回归之类的模型,通过引入权重参数来避免直接对函数的分布进行表示,属于参数类模型),另外需要知晓的是高斯分布的共轭分布还是高斯分布。下面是调参过程:
图中所说的statistical model指的是P(y|x,Dn),这被假设为高斯分布。图来自论文《Taking the Human Out of the Loop:A Review of Bayesian Optimization》
如果调包的话就很简单了,如下
from bayes_opt import BayesianOptimization
LGB_BO = BayesianOptimization(LGB_bayesian, bounds_LGB, random_state=666)
#n_iter: How many steps of bayesian optimization you want to perform. The more #steps the more likely to find a good maximum you are.
#init_points: How many steps of random exploration you want to perform. Random #exploration can help by diversifying the exploration space.
init_points = 10
n_iter = 50
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
LGB_BO.maximize(init_points=init_points, n_iter=n_iter, acq='ucb', xi=0.0, alpha=1e-6)
以下是部分训练过程,紫色说明是目前最高的F(x)值,也就是本文的AUC。
①单模之后,模型集成自然是少不了的,不过因为我训练的都是lightgbm,虽然超参或者特征有些许差异,可能多样性不够吧,所以并没有带来线上的提升,线下倒是有提升emm
②尝试了伪标签,带来了B榜少许的提升。就是说找了评测集中认为预测的比较准的数据拿出来,放到每一折参与训练,相当于在模型中引入评测集的分布,虽然并不是真正的标签。代码如下
n_cv = 5
kf = StratifiedKFold(n_splits=n_cv, shuffle=True, random_state=666)
for train_index, test_index in kf.split(x, y):
print('*********************')
train, valid = train_tag.iloc[train_index], train_tag.iloc[test_index]
# psudo中存储的是评测集的数据,其标签是预测的,并不是真正的标签
train = train.append(psudo[train.columns], ignore_index=True)
train[cat] = train[cat].astype('category')
lgb_train = lgb.Dataset(train[x_col], train['flag'])
lgb_eval = lgb.Dataset(valid[x_col], valid['flag'], reference=lgb_train)
clf = lgb.train(params, lgb_train, 2000, valid_sets = [lgb_train,lgb_eval], verbose_eval=100, early_stopping_rounds=100,
categorical_feature=cat)
clf.predict(valid[x_col],num_iteration=clf.best_iteration)
train_model_pred[test_index] = clf.predict(valid[x_col],num_iteration=clf.best_iteration)
test_model_pred += clf.predict(test_tag[x_col],num_iteration=clf.best_iteration)
train_tag['preds'] = train_model_pred
test_tag['preds'] = test_model_pred/n_cv
①就比赛态度而言,我是满意的,自从我开始做的那天起,先搭建好baseline,然后每天我都会分出一部分时间进行优化,比如学习贝叶斯调参,或者深挖特征,或者尝试模型集成,或者尝试伪标签。还有一个方法我尚未尝试,就是用lstm来做时序特征的embedding,然后和非时序特征concat送入MLP进行分类;或者时序特征经过lstm,其他特征经过MLP,两者得到的结果concat然后送入MLP进行分类。之所以没有尝试,一方面是时间问题,另一方面就是我觉得大概率不行,因为数据量太少了。
②庆幸自己对每一个模型的结果做了详细的记录。其实切换B榜后,在A榜表现最好的模型并不是B榜表现最好的模型,而是之前在线下表现比较好的模型。
③就比赛结果而言,还是有点遗憾的。有些知识,比如RFM,我是清楚的,只是我业务敏感度不高,长久不接触这些定义,我都忘记使用了,只是在单纯的做一些统计特征。没有从业务角度出发是我的一大遗憾之处。
另外一个遗憾,就是我没想到对A榜没用的特征可能会对B榜有用,这大概就是为什么公司有些特征明明暂时没发挥作用,却依旧因为其业务含义来保留这个特征的原因吧。
招商银行2020FinTech精英训练营数据赛道(信用风险评分)方案分享(B榜0.78422)里面用到了RFM的思想,嘿呀,我是知道RFM的,只是我没有往这方面想,客户价值、客户的忠诚度这些本身就是与是否违约有联系的。所以特征里面可以加上最后一次购买相关的信息。另外还可以统计用户交易行为发生了几天,而不是单纯的统计交易次数,因为也许这个用户虽然交易次数很多,但都集中在某几天啊,哦说起这个,还可以看看用户的收支是不是存在周期性呀,如果存在周期性,且收支平衡的话是不会违约的。
2020招商银行fintech数据赛,线上0.78026,最终53名~~菜鸡分享,提出了强特增益,就lightgbm的特征重要度来看,信用卡天数、借记卡天数、信用卡等级等一系列特征是强特。对这些特征进行相乘,目的使用户之间的差距增大,强特增益。代码如下
def tag(data):
#信用卡:持卡天数*等级
data['credit_level1']=data['cur_credit_min_opn_dt_cnt']*pd.to_numeric(data['l1y_crd_card_csm_amt_dlm_cd'])
data['credit_level2']=data['cur_credit_min_opn_dt_cnt']*pd.to_numeric(data['perm_crd_lmt_cd'])
data['credit_level3']=data['cur_credit_min_opn_dt_cnt']*pd.to_numeric(data['hld_crd_card_grd_cd'])
data['level_level']=pd.to_numeric(data['l1y_crd_card_csm_amt_dlm_cd'])*pd.to_numeric(data['perm_crd_lmt_cd']) #等级*等级
data['credit_amount']=data['cur_credit_min_opn_dt_cnt']*data['cur_credit_cnt'] #持卡天数*持卡数量
#信用卡:持卡数量*等级
data['amount_level1']=data['cur_credit_cnt']*pd.to_numeric(data['perm_crd_lmt_cd'])
data['amount_level2']=data['cur_credit_cnt']*pd.to_numeric(data['l1y_crd_card_csm_amt_dlm_cd'])
data['amount_level3']=data['cur_credit_cnt']*pd.to_numeric(data['hld_crd_card_grd_cd'])
return data
赛后学习总结:
学习到一波银行类风控比赛的强特以及特征工程的一些思路:
①强特,除去我自己构造的特征外,下面这些特征也很重要:"最后一次交易"有关的特征;按天统计的特征(交易了多少天,平均每天的交易金额等等);对于类别特征,除了统计每类的次数,还可以统计占比。
②特征工程的思路:一方面考虑业务模型,从业务入手考虑特征;另一方面对强特可以考虑强强联合,对强特做一些进一步挖掘,比如说强特之间做点特征交叉。