第三次参加数据挖掘比赛复盘(二)

前面处理方式见另一篇博文:
1.第三次参加数据挖掘比赛复盘(一)

A.1.分析训练集数据
log表的数据处理方式:
log表由于是该用户购买贷款/理财产品的历史数据,与profile表,与train_flag都是多对一的关系。
那么在梳理过程中可以分为两类,一类的常规数据梳理方式,即同profile表类似的操作。另一类是,对于历史数据进行多对一的数据聚合处理方式。
1.常规数据处理方式:

那么首先是同profile表一样,进行数据初步分析:
1.1 首先看行列数

#数据分析 训练集log:l_t
#行数、列数
print(l_t.shape)
print(l_t['客户编号'].unique().shape)
print(label_t['客户编号'].unique().shape)
print(l_t.columns)

在这里插入图片描述
在这里插入图片描述
从输出结果可以看出,log历史信息表的客户编号数,是小于给出结果label表/用户信息profile表的编号数的。
这也就是说明,可能有四千条数据,是没有历史办理理财/贷款产品的信息,只能通过他们profile表的信息来预测的。那么将表关联起来后,这四千条信息的log表部分是留空的。

1.2 数据中值的分布
**加粗样式**
本表的数据分布大致分为三种情况:
1)首先是1的值数量相对较小的列,这种列即使进行历史数据聚合统计,客户历史办理该理财/贷款产品的可能性也相对较低:储蓄账户、担保业务、衍生账号、特级账户、特定账户种类1、家庭账户,共六列
2)剩下是相对办理可能性较高的列:活期账户、工资账户、特定账户种类2、特定账户种类3、电子账户、基金业务、税务业务、信用卡业务、证券业务,共9列
3)养老金业务列,除了空值,其他值全部都是否。该列可以考虑删除。

缺失值统计如下图,从图中可以得到,超过50%缺失的列有:担保业务,活期账户,工资账户。再综合上图分析,活期账户和工资账户虽然缺失值很高,但特征分布较为清晰。而担保业务这一列可以考虑删除。
在这里插入图片描述

综上养老金业务、担保业务两列考虑删除。

1.3 具体数据探索

面对log数据,首先思考,“关于是否开办xx业务,xx账户”,此列的1值,是代表已经开办xx业务的状态,还是代表来开办xx业务的行动?
那么通过观察具体数据,由于可以观测的1出现多在连续的年月时间,可以大胆推测,log表的1值,表示的是该客户该月是否开办业务的状态。
在这里插入图片描述
那么该表的空缺值很多,此时是否应该进行填充?
当时我考虑的填充策略是:对于某一列,在所有值为1的统计日期集合中,最早日期和最晚日期区间的空缺值,是可以考虑填充为1的。但我没有这么填充,因为我们不能保证在这个日期区间内,该用户不会转而取消产品“1”的购买后重新购买“1”(也就是这个区间内也是有可能插入0的)。
且经过这几次数据挖掘比赛实践,填充空缺值对lgb模型准确度的优化程度,并没有矫正错误数据与造特征来的大(因为LGB树结构对空缺值有自己的处理)。因此我是将它当做后期优化策略,先跑了baseline。

1.4 带时间的log表再处理
由于profile表与预测flag都带有时间,此时笔者考虑了一个方案
该方案是:利用rolling方法将所有数据,对于每一条log表的“客户编号+统计日期”合并作为主键的一行,都求出该“客户编号+统计日期”在该统计日期的上个月、上三个月聚合、上半年聚合、和历史所有聚合 这四种条件下是否开办xx,是否办理xx的状态。

但代码写完了,跑了一下午没出结果。笔者猜想是因为,rolling会加入另一行index“统计日期”,那对于一个有两个index“客户编号“、”统计日期”的表,在做筛选多条件数据处理(非排序操作)时候的效率很低。

那么笔者将上述想法进行删减。考虑到log表有17列特征,如果对其每一列历史信息对客户编号聚合后,求last、count、mean、sum等计算,每计算一次便是一次特征的广泛扩充,**且log表空缺值较多、数据质量并不好。**所以需要考虑如果求哪些计算的实际意义更大。

最终选择了last(“客户编号+统计日期”中统计日期的上个月数据)与历史所有数据(早于“客户编号+统计日期”中统计日期的所有历史数据)的count(0、1)计算。(count(1)=sum=代表办理“1”产品的次数,count(0),可以考虑再加入mean,是因为需要考虑历史数据统计条数的不同,即mean代表办理“1”产品的概率)

**历史一个月、三个月、六个月都能做,为什么偏偏只选择last上个月呢?**是因为有时间序列加入统计的条件下,**如果上一个月为购买“1”产品的状态,那么本月极有可能延续这个状态。**因此对于每一条log表的“客户编号+统计日期”合并字段所代表的的一行,笔者都用如下代码求出了该客户编号在上个月的是否开办xx,是否办理xx的状态
在这里插入图片描述

历史所有count(1)、count(0)
在这里插入图片描述

到这里,本次比赛数据处理的相关基本思路就说完了。
具体的数据修正与清洗过程,由于不同项目数据质量与待修正点的不同,这里就不多提了,发一些代码吧,处理方式大同小异。**但值得再强调的是:A、B榜训练集的数据理论上分布与质量都不同,需要分别观察,做出不同的处理。**这时肝A榜的兄弟能休息还是休息下吧,如果有团队的话可以换一个兄弟来做数据探索。因为后面还有特征筛选、欠拟合过拟合调整等地方需要处理
在这里插入图片描述
在这里插入图片描述
可以看出上面代码里,我对A、B榜问题数据处理的方式也不一样
在这里插入图片描述

接下来想讲讲本次比赛笔者遭遇到的新的坑:
从A榜前三切到B榜,AUC直接从92.8掉到70,排名当夜直接掉到10+。
为什么模型的泛化性如此重要?
特征筛选又是什么?具体怎么做特征筛选才能提高模型的泛化性?
什么是PSI、WOE、IV?
PSI的适用场景又是什么?

B1
终于到了B榜。

笔者还是经验少,A榜的时候狂肝,以为B榜并不会变化很多。A榜切B榜的当天是个周末,当时还美滋滋还睡了一整个白天。结果晚上起来,原代码跑一轮,AUC直接从93掉到70。

傻眼了。

当时一开始查错的方向就错了,当时开始怀疑B榜数据join错位了,或者是代码里的时间相关对应对错了,先开始怀疑自己代码,等于把代码又在juypter重写了一遍

其实此时可以首先看一下A、B榜的数据分布差异。
比赛数据与真实数据不同。比赛的训练数据,往往都会带有大量人为的故意做出来干扰的误差

当自己代码检查完了还是一头雾水。笔者也不敢问同参赛有竞争的同事,只能咨询了一些其他的同事,经过同事的提醒,再意识到B榜数据集可能分布与A榜不同,待将数据误差都处理完后,AUC回到了79。

与A榜的93还是差很多啊,这是为什么呢?
此时就要引入模型的泛化性这个概念了。

什么是模型的泛化性?

引用一下当时我们比赛群里的讨论。因为以下原因,我们需要提升模型的泛化性。
在这里插入图片描述

什么是WOE?

WOE、IV|强烈推荐这篇解释文章,讲得特别清楚

什么是PSI?
群体稳定性指标(population stability index),

所谓特征稳定性,就是关注该特征的取值随着时间的推移会不会发生大的波动。

对特征稳定性的关注,一定一定要在建模之前完成,从一开始就避免将那些本身不太稳定的特征选入模型。遗憾的是,很多做模型的同学并没有留意这一点,而是喜欢在特征ready后立刻开始建模,直到模型临近上线,才意识到应该去看看有没有不太稳定的特征,一旦发现有特征稳定性不满足要求,则需要对其进行剔除后重新建模,导致了不必要的重复性劳动。

通常采用PSI(PopulationStability Index,群体稳定性指数)指标评估特征稳定性。计算公式如下:
在这里插入图片描述

PSI是对两个日期的特征数据进行计算,可以任选其一作为base集,另一则是test集(也有其他叫法为expected集和actual集)。

下面介绍特征的PSI是如何计算出来的,有了这个,就可以读懂上面的公式了:

• 特征取值等频分段:对这个特征在base集的取值进行等频划分(通常等频分10份即可),用字母i表示第i个分段区间。

在这里插入图片描述

看到经管之家论坛上对PSI解释比较清楚:链接:模型的稳定性用PSI指标来检验 reference1

比如训练一个logistic回归模型,预测时候会有个概率输出p。

测试集上的输出设定为p1吧,将它从小到大排序后10等分,如0-0.1,0.1-0.2,…现在用这个模型去对新的样本进行预测,预测结果叫p2,按p1的区间也划分为10等分。

实际占比就是p2上在各区间的用户占比,预期占比就是p1上各区间的用户占比。

那么如果模型很稳定,那么p1和p2上各区间的用户应该是相近的,占比不会变动很大,即就是预测出来的概率不会差距很大。

一般认为PSI小于0.1时候模型稳定性很高,0.1-0.25一般,大于0.25模型稳定性差,建议重做。

PS:除了按概率值大小等距十等分外,还可以对概率排序后按数量十等分,两种方法计算得到的psi可能有所区别但数值相差不大。

等元旦或者春节再把特征筛选方法仔细学习一下做个总结吧。

以下是赛后代码:

import warnings
warnings.filterwarnings(action='ignore',category=UserWarning,module='gensim')
import pandas as pd
import numpy as np
#from tqdm import tqdm
from multiprocessing import Pool,Process
#import gensim
#from gensim.models import word2vec
import time
import re
import copy
import math
from datetime import datetime
from datetime import timedelta
import gc
#import sys
#import editdistance




#向前逐步回归函数
def forward_selected(data, response):
    import statsmodels.formula.api as smf
    remaining = set(data.columns)
    remaining.remove(response)
    selected = []
    current_score, best_new_score = 0.0, 0.0
    while remaining and current_score == best_new_score:
        scores_with_candidates = []
        for candidate in remaining:
            formula = "{} ~ {} + 1".format(response,
                                           ' + '.join(selected + [candidate]))
            score = smf.ols(formula, data).fit().rsquared_adj
            scores_with_candidates.append((score, candidate))
        scores_with_candidates.sort()
        best_new_score, best_candidate = scores_with_candidates.pop()
        if current_score < best_new_score:
            remaining.remove(best_candidate)
            selected.append(best_candidate)
            current_score = best_new_score
    formula = "{} ~ {} + 1".format(response,
                                   ' + '.join(selected))
    model = smf.ols(formula, data).fit()
    result={
   'model':model,'params':selected}
    return result

	
def fun_woe(data, colname,classx,data_total,factorname='is_fpd30',max_box=10,min_box=3,max_loss_value=0.01,
            min_sample=0.01,min_box_sample=0.05,trim='T'):
    #0.对即将处理的列统一命名
    rename_eval="data.rename(columns={'"+factorname+"':'remark','"+colname+"':'coly'}, inplace = True)"
    eval(rename_eval)
    #去除原数据中的缺失值(缺失值会在后面单独处理)
    data0=data.loc[data['coly']==data['coly']]

    #以实际列值分组
    group1=data0.groupby(['coly'])[['remark']].sum()
    group2=data0.groupby(['coly'])[['remark']].count()
    if len(group1)<3:
        classx='class'
    colyx=list(group1.index)
    if classx=='linear':
        x0=pd.DataFrame(colyx,columns=['coly'])
    else:
        x0=pd.DataFrame(colyx,columns=['coly_name'])
        x0['coly']=range(0,len(x0))
    #离散数据数据值对应colyid
    colyname_info=copy.deepcopy(x0)
    x0['nrow1']=list(group1['remark'])
    x0['nrow01']=list(group2['remark'])
    #总好用户数
    sum0=len(data)-sum(data['remark'])
    #总坏用户数
    sum1=sum(data['remark'])
    
    #1.快速合并初始分箱
    x1=copy.deepcopy(x0)
    # cumsum1=np.cumsum(list(x1['nrow1']))
    # cumsum01=np.cumsum(list(x1['nrow01']))
    if classx!='linear':
        x1=x1.sort_index(by='nrow01',axis=0,ascending=True)
    #     x2=pd.DataFrame(columns=['nrow1','nrow01','coly','coly_name'])
    # else:
    #     x2=pd.DataFrame(columns=['nrow1','nrow01','coly'])
    x1['cumsum01']=np.cumsum(list(x1['nrow01']))
    #本次分箱最小样本数
    min_box_x=(sum0+sum1)*min_box_sample
    backout=0
    x2=pd.DataFrame(columns=['nrow1','nrow01','coly'])
    #离散数据用来记录合并过程
    coly_set=pd.DataFrame(columns=['coly_from','coly_to'])
    while backout==0:
        if (min(x1['nrow01'])>=min_box_x):
            x2=x2.append(x1[['nrow1','nrow01','coly']])
            backout=1
        else:
            cumsum_1=x1['cumsum01'].loc[x1['cumsum01']>=min_box_x][0:1]
            if len(cumsum_1)==0:
                x1i=copy.deepcopy(x1)
            else:
                x1i=x1.loc[x1['cumsum01']<=int(cumsum_1)]
            if classx!='linear':
#                 coly_seti=pd.DataFrame(list(x1i['coly'])[1:len(x1i)],columns=['coly_from'])
#                 coly_seti['coly_to']=list(x1i['coly'])[0]
#                 coly_set=coly_set.append(coly_seti)
#                 s = pd.Series({'coly_from':list(x1i['coly'])[1:len(x1i)], 'coly_to':list(x1i['coly'])[0]})
#                 coly_set = coly_set.append(s, ignore_index=True)
                for i in range(1,len(x1i)):
                    s = pd.Series({
   'coly_from':list(x1i['coly'])[i], 'coly_to':list(x1i['coly'])[0]})
                    coly_set = coly_set.append(s, ignore_index=True)
            s = pd.Series({
   'nrow1':sum(x1i['nrow1']), 'nrow01':sum(x1i['nrow01']), 'coly':list(x1i['coly'])[0]})
            x2 = x2.append(s, ignore_index=True)
            x1=
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值