2022年招行数据赛道公司存款流失预测赛后复盘
公司存款流失预测(2022年招行数据赛道)
这个四月参加了招行的数据赛道比赛,感觉氛围不错学到了很多知识。最后在1800+人中拿到了43名的成绩。
赛题简介
本次比赛为参赛选手提供了两个数据集,即训练数据集(train)和测试数据集(test_A榜/ test_B榜)。通过有效的特征提取,使用分类算法构建公司客户存款流失预测模型,并将模型应用在测试数据集上,输出对测试数据集中公司客户存款流失概率的预测。
数据简介
训练数据集中共包含4万条脱敏数据,
- CUST_UID为公司客户唯一标识,
- LABEL为公司客户存款是否流失的标志
- 其中1表示该公司客户在三个月后存款发生流失
- 0表示该公司客户在三个月后存款未发生流失。
- 后续的数据列是每个公司客户当月的数据情况,可作为模型使用的变量
- 需要注意的是,不是所有变量都适合入模,参赛选手需要对变量进行过滤和筛选。
- 测试数据集中包含1.2万条脱敏数据 ,CUST_UID为公司客户唯一标识(训练集和测试集中若存在相同的CUST_UID,不代表同一用户),其余数据列为每个公司客户当月的数据情况。
A榜上分回顾
主要采用的方法
- 检验训练集和测试集的分布是否一致
- 处理类别不平衡
- 特征构造
- 数值特征统计量
- 类别特征交叉
- 通过树模型筛选高重要性特征
- 降维
- pca(实践证明效果不如集成树模型importance过滤)
- 模型
- XGBoost
- 多模型stacking
- 结果融合
主要的问题
- 竞赛后期没有什么好的上分方法,不过前50的排名基本也证明了在没有相关知识的情况下也只能做到这里
B榜上分回顾
B榜测试数据与训练数据有很大不同,导致线上线下差异极大,很长一段时间线下都是完全过拟合的状态。最后导致了较差的结果
主要采用的方法
- 用A榜的测试集做伪标签
- ranking融合
- 对抗验证
基本思路
- 重点在于筛选特征,难点在能提交的次数很少,运气不好很难试出有用的特征。
- 而且线下构造验证集很困难,经常出现线下低线上高的情况。过拟合难以控制。
对抗验证(详见附录A)
简述:将来自训练集的标签赋值为1,测试集的样本标签为0,训练一个学习器判断这样本来自哪一个集合
对于A榜数据
valid_0's auc: 0.492741
AUC约为0.5,表明分类器无法区分样本是来自训练集还是测试集
对于B榜数据
valid_0's auc: 0.969135 valid_0's l2: 0.0640045
valid_0's auc: 0.978514 valid_0's l2: 0.0516688
valid_0's auc: 0.982417 valid_0's l2: 0.0461824
valid_0's auc: 0.984529 valid_0's l2: 0.0429678
valid_0's auc: 0.98615 valid_0's l2: 0.040339
valid_0's auc: 0.986933 valid_0's l2: 0.0388808
Early stopping, best iteration is: valid_0's auc: 0.987338 valid_0's l2: 0.0381312
很明显A榜的训练集和测试集就是从一个数据集中划分得来,而B榜的测试集来自另一个数据集。
而后采用这一分类器从训练集中选择与测试集类似的样本,有助于减少过拟合。
赛后总结
赛后参考一些大佬的开源后发现B榜关键在于删除会导致过拟合的特征
- 根据分布系数筛选特征(裸筛)发现有一个特征“CUR_MON_COR_DPS_MON_DAY_AVG_BAL”加入模型会导致线下AUC为0.89,不加则是0.74,线下的0.89到线上却只有0.7,所以这个特征最后被剔除了
- 我第一次交的时候筛了一半的特征线上线下都在0.74左右,后来增加特征量发现了上面的这个异常特征,然后结果到了0.85
- 最后是根据分布曲线筛选特征,依据就是保持线下的训练集和验证集尽量维持一致,同时在增加特征时发现有些特征加了对验证集的结果一点影响都没有,因此这些特征最后都被删除了
附录A:Adversarial validation-对抗验证| 一种解决训练集与测试集分布不一致的方法
验证集上的KS/AUC相对较高,测试集上天然会存在decay的现象
训练集与测试集差异明显的典型例子很容易发生在Kaggle比赛,或者国内高水平的比赛上,比赛前期显示的都是在公榜上成绩,最后的评判的却是的额外的私榜上,就会产生很大喜闻乐见的地震。如Kaggle比赛LANL Earthquake Prediction
,在public Leaderboard中排名第一的,在private Leaderboard却掉到了2734,而后面一大票老哥却冲到了前排,上演数据世界的地震。
我们发现线下的K折验证无法反映线上效果时该怎么办
- 构建一个样本的分类器,该二分类器的任务用于区分样本来源于训练集,还是测试集。因此,需要为原始数据新增一个标签列,将训练集中的样本标记为0, 测试集中的样本标记为1,样本的特征是数据中已有的特征,或者衍生的新特征,生成新的训练数据集;
- 将新的训练数据集进行划分,保留部分样本作为该样本分类任务的测试集\(T\), 利用分类算法(XGBoost, LightGBM)等对数据集进行训练,AUC作为模型指标;
- 在测试集\(T\)中进行验证,如果模型效果AUC在0.5左右,说明该样本分类模型无法区分样本来源训练集,还是测试集,说明原始数据中训练集,测试集分布是一致的;如果AUC较大,如0.9, 说明样本分类器很容易区分样本,间接说明训练集与测试集存在很大差异;
- 根据第3步的结论,对于分布一致的,正常对目标任务训练即可;
- 对于分布不一致的,可以继续进行样本挑选的尝试。利用上述样本分类器模型,对原始的训练集进行打分预测,并将样本按照模型分从大到小排序,模型分越大,说明与测试集越接近,那么取训练集中的TOP N 的样本作为目标任务的验证集,这样即可将原始的样本进行拆分得到 训练集,验证集,测试集。那么线上模型验证时,在这样的数据上调参等得到模型,如果在验证集上效果稳定,那么应用在测试集上,大概率结果是一致的
用对抗验证进行特征筛选
对抗验证作为一种特征筛选方法时,可以找出时序波动明显的特征,帮我们快速找到在训练集和测试集上不稳定的特征。步骤如下:
- 对划分的训练集和验证集(测试集)进行二元编码, e.g. 训练集增加一个ad_target=1,验证集增加一个ad_target=0
- 训练一个简单的二分类模型,目标是对ad_target做二分类
- 每次训练只使用一个特征,记录模型收敛时验证集的AUC
- 迭代步骤3直到遍历所有特征,对特征按照AUC指标从高到底排序
- 重点分析AUC较高所对应的特征,经验阈值可以选0.7,0.8;注意缺失值问题
- auc越高,代表越能区分出训练集和测试集,可以选择丢弃这些特征。
附录B:提交记录与笔记
第1次提交|0.9518|
- 参数:默认
- 数据处理:
- 类别特征:类别为object,按这个标准有5个
- 对类别特征做编码
- 线上auc:0.9518
第2次提交|0.9523|0.0005
- 参数:默认
- 数据处理:
- 类别特征:unique数小于100,按这个标准有12个
- 对类别特征做自然数编码
- 数值特征做 max, min, mean, std, ptp
- 线下auc
auc | l2 | |
---|---|---|
0 | 0.951297 | 0.0694433 |
1 | 0.951351 | 0.0713171 |
2 | 0.951684 | 0.0707 |
3 | 0.945938 | 0.0745949 |
4 | 0.950595 | 0.0725664 |
- 线上auc:0.9523
第3次提交|0.9533|0.001
数据:采用第一次提交的数据
特征:使用autogulon,没调任何参数
线下auc:0.952672
线下实验
实验一:
label='LABEL'
save_path = '../usrdata/automl' # specifies folder to store trained models
predictor = TabularPredictor(label=label,
problem_type='binary',
eval_metric='roc_auc',
path=save_path).fit(x_train)
输入数据为第二次提交的数据,即做了数值特征统计的数据,验证集最优分数为0.952989
,hyperparameters='multimodal', num_bag_folds=5, num_stack_levels=1
第5次提交|0.954766|0.0015
数据(以下处理按顺序进行):
- 区分类别特征和数值特征
- 对类别特征进行自然数编码
- 数值特征分桶(分桶后的数值特征作为类别特征)
- 聚类方法打标签(kmeans n=100, DBSCAN)
- 数值特征交叉(加减乘除)
- 数值特征统计(由交叉得到的特征不做统计)
模型:使用autogulon,默认参数
predictor = TabularPredictor(label=label,
problem_type='binary',
eval_metric='roc_auc',
path=save_path).fit(x_train,)
线下auc:0.952419
model | score_val | |
---|---|---|
0 | WeightedEnsemble_L2 | 0.952419 |
1 | CatBoost | 0.951544 |
2 | LightGBMXT | 0.950950 |
3 | LightGBMLarge | 0.950814 |
4 | LightGBM | 0.0725664 |
还可以考虑的点 |
- 类别不平衡
- 根据catboost的特征重要性做特征选择
第9次提交|0.95483|6.4e-5
数据:根据catboost筛选特征,取重要性大于0的特征
模型:取boost系列一共五个模型,基模型内部做平均。基模型之间做stack
rawdata=pd.read_parquet('../usrdata/data_stat_cross_cat.par')
x_train,x_test=train_test_split(rawdata)
label='LABEL'
save_path = '../usrdata/automl' # specifies folder to store trained models
predictor = TabularPredictor(label=label,
problem_type='binary',
eval_metric='roc_auc',
path=save_path).fit(x_train,
num_bag_folds=5,
num_bag_sets=5,
excluded_model_types = ['KNN', 'NN_TORCH','FASTAI','XT','RF']
# hyperparameters='multimodal',
# presets='best_quality'
)
线下auc:0.953084
model | score_val | |
---|---|---|
0 | WeightedEnsemble_L2 | 0.953084 |
1 | CatBoost | 0.952732 |
2 | LightGBMXT | 0.952622 |
3 | LightGBMLarge | 0.952592 |
4 | LightGBM | 0.952578 |
5 | CatBoost_BAG_L1 | 0.951938 |
还可以考虑的点: |
- 大模型速度太慢了(上面的实验大约两小时,特征数1500左右),尝试安装lightgbm的GPU版 (编译成功,但精度有明显下降)
- 类别特征交叉
第10次提交|0.9545|反面教材——准确率下降
数据:加入类别特征组合(这里的数据有问题,类别特征组合不正确)
模型:
predictor = TabularPredictor(label=label,
problem_type='binary',
eval_metric='roc_auc',
path=save_path).fit(x_train,
excluded_model_types = ['KNN', 'NN_TORCH','FASTAI','XT','RF'])
线下AUC:0.953336
model | score_val | |
---|---|---|
0 | WeightedEnsemble_L2 | 0.953336 |
1 | LightGBMXT | 0.952803 |
2 | LightGBM | 0.952675 |
3 | CatBoost | 0.951666 |
4 | LightGBMLarge | 0.950629 |
5 | XGBoost | 0.950304 |
特征选择后的结果 | ||
![[aa0ba6d4-2676-4d0d-bc78-37e1517be678.png]]-= |
第12次提交|0.95501|0.00018
数据:加入类别特征组合后用catboost做特征选择
模型:
predictor = TabularPredictor(label=label,
problem_type='binary',
eval_metric='roc_auc',
path=save_path).fit(x_train,
num_bag_folds=5,
num_bag_sets=5,
excluded_model_types = ['KNN', 'NN_TORCH','FASTAI','XT','RF'])
基本没什么突破,该用的招差不多都用完了
第13次提交|0.95506|0.00001
就是把前几次提交中比较好的按0.2,0.2,0.6的比例重新融合
submit-cat_cross_5-2022-05-02-22-05.txt | submit-cross_5-2022-05-01-19-51.txt | submit-auto_ml_with_num_stat-2022-04-30-18-12.txt | 融合成绩 | |
---|---|---|---|---|
单模成绩 | 0.95501 | 0.95482 | 0.95476 | |
融合比例 | 0.6 | 0.2 | 0.2 | 0.95506 |
0.6 | 0.3 | 0.1 | 0.95503 | |
0.7 | 0.3 | 0 | 0.95499 | |
最后成绩0.95506 |
- 可以把前面三个表现较好的大模型融合起来
- 五折交叉验证之后得到的特征重要性居然都不太一样,一共8000+特征,五个模型中相同的只有180个
第17次提交|0.95508|0.00002
特征:在类别特征中取三个交叉(之前是两两交叉),用5折lightgbm做特征选择,取5次交叉的特征重要性之和>10的特征
最后得到2326个特征
模型:AutoGulon常规的13个模型
线下auc:0.9541
- 统计某个类别下的数值特征统计量
第22次提交|0.95527|0.0002
融合之前的提交
submit-2022-05-03-12-31-3_fusion.txt | submit-2022-05-05-09-51-stat_3_combine_cross_cat.txt | 融合成绩 | |
---|---|---|---|
单模成绩 | 0.95511 | 0.95513 | |
融合比例 | 0.4 | 0.6 | 0.955271 |
0.3 | 0.7 | 0.955243 | |
0.5 | 0.5 | 0.955278 | |
0.55 | 0.45 | 0.955275 |
还可以考虑的方向
- 类别特征和数值特征的组合
- 数值特征自身的变换和组合
附录C:竞赛高排名方案
方案一:招商银行2022FinTech精英训练营-数据赛道_叶 无忧98的博客-CSDN博客
核心思想是结合分类准确率和对抗验证AUC综合进行筛选, 通过middle_sort对所有特征进行排序,筛选有用的特征
middle_sort= np.where(importance_sort == i)+ np.where(AUC_sort == i)* 2 # auc_gain
方案二:librauee/ZSYH2022: 招商银行2022FinTech精英训练营 (github.com)
- 观察完数据与其他选手分享的得分之后就一个思路:删除异常特征
- 删除训练集与测试集分布不一致的特征
- 首先通过对抗验证的思路找到auc指标远大于0.5的特征,(这些特征名中有较大一部分带有CUR,猜测测试集中的数据取自和训练集不同的年份,导致差异显著)遂全部删除带有CUR的特征
- 然后
玄学删除特征 (总结发现的主要规律是线下越低,线上越高【奇怪的现象】) - 上一步骤主要通过特征重要性,迭代删除最重要的特征(和A榜做法相反,以获得更低的线下得分和更高的线上得分)
- 提交进行玄学测试(次数有限,做的很差)