Python构建xgboost模型-特征分箱、WOE转换、IV值计算

构建机器学习模型时(尤其是逻辑回归),一般都会对连续型特征进行特征分箱,特征分箱优点有:

1、分箱后的特征对异常数据有更强的鲁棒性。如入模特征异常大时,分箱后只是作为一个类别入模,如果将值直接入模,则会对模型造成较大的干扰。

2、可以处理缺失值,将缺失值直接作为一个类别放入模型当中。

3、特征离散化后可以起到简化模型的作用,降低模型的过拟合作用。

4、特征离散化之后,每个变量有单独的权重,可以为逻辑回归模型引入了非线性,能够提升模型表达能力,加大拟合。

如何对连续型入模特征进行分箱

1、无监督中的等频、等距分箱

2、有监督的chi ^{2}分箱

代码事例

# 计算卡方值函数
def chi3(arr):
    '''
    计算卡方值
    arr:频数统计表,二维numpy数组。
    '''
    assert(arr.ndim==2)
    #计算每行总频数
    R_N = arr.sum(axis=1)
    #每列总频数
    C_N = arr.sum(axis=0)
    #总频数
    N = arr.sum()
    # 计算期望频数 C_i * R_j / N。
    E = np.ones(arr.shape)* C_N / N
    E = (E.T * R_N).T
    square = (arr-E)**2 / E
    #期望频数为0时,做除数没有意义,不计入卡方值
    square[E==0] = 0
    #卡方值
    v = square.sum()
    return v


# 确定卡方分箱点
def chiMerge(df,col,target,max_groups=None,threshold=None):

    '''
    卡方分箱
    df: pandas dataframe数据集
    col: 需要分箱的变量名(数值型)
    target: 类标签
    max_groups: 最大分组数。
    threshold: 卡方阈值,如果未指定max_groups,默认使用置信度95%设置threshold。
    return: 包括各组的起始值的列表.
    '''

    freq_tab = pd.crosstab(df[col],df[target])

    #转成numpy数组用于计算。
    freq = freq_tab.values

    #初始分组切分点,每个变量值都是切分点。每组中只包含一个变量值.

    #分组区间是左闭右开的,如cutoffs = [1,2,3],则表示区间 [1,2) , [2,3) ,[3,3+)。
    cutoffs = freq_tab.index.values

    #如果没有指定最大分组
    if max_groups is None:    
        #如果没有指定卡方阈值,就以95%的置信度(自由度为类数目-1)设定阈值。
        if threshold is None:
            #类数目
            cls_num = freq.shape[-1]
            threshold = chi2.isf(0.05,df= cls_num - 1)

    while True:
        minvalue = None
        minidx = None
        #从第1组开始,依次取两组计算卡方值,并判断是否小于当前最小的卡方
        for i in range(len(freq) - 1):
            v = chi3(freq[i:i+2])
            if minvalue is None or (minvalue > v): #小于当前最小卡方,更新最小值
                minvalue = v
                minidx = i

        #如果最小卡方值小于阈值,则合并最小卡方值的相邻两组,并继续循环
        if  (max_groups is not None and  max_groups< len(freq) ) or (threshold is not None and minvalue < threshold):
            #minidx后一行合并到minidx
            tmp  = freq[minidx] + freq[minidx+1]
            freq[minidx] = tmp
            #删除minidx后一行
            freq = np.delete(freq,minidx+1,0)
            #删除对应的切分点
            cutoffs = np.delete(cutoffs,minidx+1,0)

        else: #最小卡方值不小于阈值,停止合并。
            break
    return cutoffs


# 生成分箱后的新变量
def value2group(x,cutoffs):

    '''
    将变量的值转换成相应的组
    x: 需要转换到分组的值
    cutoffs: 各组的起始值
    return: x对应的组,如group1 从group1开始
    '''

    # 切分点从小到大排序
    cutoffs = sorted(cutoffs)
    num_groups = len(cutoffs)

    # 异常情况:小于第一组的起始值。这里直接放到第一组
    # 异常值建议在分组之前先处理妥善。
    if x < cutoffs[0]:
        return 'group1'

    for i in range(1,num_groups):
        if cutoffs[i-1] <= x < cutoffs[i]:
            return 'group{}'.format(i)

    # 最后一组,也可能会包括一些非常大的异常值。
    return 'group{}'.format(num_groups)


 对data数据框中的特定指标开展分箱,并形成新的分箱后的指标

# 需要分箱的指标
column_cart_list = ['column1','column2','column3','column4']
# 调取分箱函数形成新指标(设置分成8箱)
for column_cart in column_cart_list:
    cutoffs = chiMerge(data_test, column_cart, max_groups = 8)
    new_column_name = column_cart + '_cart'
    data_test[new_column_name] = data_test[column_cart].apply(value2group, args=(cutoffs,))

拿到分箱指标后,进行WOE编码(数值编码),WOE值主要作为入模型特征,计算方式如下

优点:

1、可以提高模型的性能:根据公式以每一箱中的相对全体的log odds的超出作为编码依据,能够提高模型的预测精度,同时公式也符合LR的思想

2、分层抽样中的WOE不变性:如果建模需要对好坏样本进行分层抽样,则抽样后计算的WOE与没分层计算的WOE是一致的。

3、其次可以统一变量的一个尺度,一般是【-4,4】之间

缺点:

1、根据公式来看可以看出每一个bin中必须包含bad和good样本

2、对多类别标签无效:如果是多分类,分箱后的WOE无法计算

计算的python代码

# 进行WOE编码
def calWOE(df ,var ,target):
    
    '''
    计算WOE编码
    param df:数据集pandas.dataframe
    param var:已分组的列名,无缺失值(缺失)
    param target:响应变量(0,1)
    return:编码字典
    '''
    eps = 0.000001  #避免除以0
    gbi = pd.crosstab(df[var],df[target]) + eps
    gb = df[target].value_counts() + eps
    gbri = gbi/gb
    gbri['woe'] = np.log(gbri[1]/gbri[0])
    return gbri['woe'].to_dict()

基于woe编码后的结果计算特征的IV值

IV值主要表示特征的预测能力,判断特征重要性的,因为做完特征工程后,会衍生出很多特征,有的变量是没用的,或者说对模型贡献非常非常小,所以我们需要筛选最重要的特征。

IV值计算与WOE编码的关系

计算代码:

# 进行值的计算
def calIV(df,var,target):

    '''
    计算IV值
    param df:数据集pandas.dataframe
    param var:已分组的列名,无缺失值(缺失)
    param target:响应变量(0,1)
    return:IV值
    '''
    eps = 0.000001        # 避免除以0
    gbi = pd.crosstab(df[var],df[target]) + eps
    gb = df[target].value_counts() + eps
    gbri = gbi/gb
    gbri['woe'] = np.log(gbri[1]/gbri[0])
    gbri['iv'] = (gbri[1] - gbri[0])*gbri['woe']
    return gbri['iv'].sum()

同样基于循环指标计算对应指标的IV值

colums_iv_list = ['column1','column2']
# woe可能为负,但iv值不可能为负(is_invergt_case为y标签)
for colums_iv in colums_iv_list:
    woe_map = calWOE(data_test, colums_iv, 'is_invergt_case')
    iv = calIV(data_test, colums_iv, 'is_invergt_case')
    print('当前分箱指标的IV值:', colums_iv, iv)

一般来说IV>=0.2 是有较高的重要性,0.1~0.2之间是有较弱的重要性,小于0.1几乎没有重要性,但是当IV异常高的时候,比如超过1的时候,那么此时的分箱方式可能是不稳定的

变量分布的稳定性:合适的变量,各箱的占比不会很悬殊。如果某变量有一箱的占比远远低于其他箱,则该变量的稳定性也比较弱。

单变量分析是从重要性及分布的稳定性两个角度来考虑。通常先选择 IV 高于阈值的变量,再挑选出分箱比较均匀的变量。如果变量的 IV 普遍较低,那么需要放宽 IV 的选择条件,比如原来是大于0.2的入模,发现得到的 IV 都很低 ,低到0.0几的时候,可以用0.02进行粗筛选。

3、无监督的K-means聚类分箱案例

IV值计算函数

# 同样注意保证每个分箱下既有正例样本又有负例样本,否则除0会报错
def CalcIV(Xvar, Yvar):
    fea_seg_list,ivi_list,woei_list,iv_list0 =[],[],[],[]
    
    N_0 = np.sum(Yvar==0)        # 负样本量
    N_1 = np.sum(Yvar==1)        # 正样本量
    N_0_group = np.zeros(np.unique(Xvar).shape) 
    N_1_group = np.zeros(np.unique(Xvar).shape)
    a=0
    for i in range(len(np.unique(Xvar))):
        # 当前分箱负样本量
        N_0_group[i] = Yvar[(Xvar == np.unique(Xvar)[i]) & (Yvar == 0)].count() 
        # 当前分箱正样本量
        N_1_group[i] = Yvar[(Xvar == np.unique(Xvar)[i]) & (Yvar == 1)].count()
        N_2 = np.sum((Xvar == np.unique(Xvar)[i]))
        b=N_1_group[i]/N_1
        g=N_0_group[i]/N_0
        # 每一个分箱的iv值
        ivi=(b - g) * np.log(b/g)
        # 每一个分箱的woe值
        woei = np.log((N_1_group[i]/N_0_group[i])/(N_1/N_0))
        fea_seg_list.append(np.unique(Xvar)[i])
        ivi_list.append(ivi)
        woei_list.append(woei)
        
        
    # 特征的iv值
    iv = np.sum((N_1_group/N_1-N_0_group/N_0) * np.log((N_1_group/N_1)/(N_0_group/N_0)))
    lens = len(ivi_list)
    iv_list0 = lens*[iv]
    
    return fea_seg_list,ivi_list,woei_list,iv_list0
  
def caliv_batch(df, Kvar, Yvar):
    iv_df = pd.DataFrame()
    df_Xvar = df.drop([Kvar, Yvar], axis=1)
    ivlist = []
    for col in df_Xvar.columns:
        fea_seg_list,ivi_list,woei_list,iv_list0 = CalcIV(df[col], df[Yvar])
        feaname_list = len(ivi_list)*[col]
        iv_ret_tmp = pd.DataFrame({'col':feaname_list,'var':fea_seg_list,'iv':ivi_list,'woe':woei_list,'iv_all':iv_list0})            
        iv_df = pd.concat([iv_df,iv_ret_tmp],ignore_index=True)
  
    return iv_df

利用K-mean聚类进行指标分箱

### 使用KMeans进行聚类
from sklearn.cluster import KMeans
typelabel2={}
for col in num_feats_6:             # num_feats_6是进行聚类的指标列表
    typelabel2[col]=col+'_1'
k =6                                # k是聚类的类别数
keys = list(typelabel2.keys())
result1 = pd.DataFrame()
# 需要进行分箱的数据值
data = df[num_feats_6].copy() 

# 对每一个连续值进行聚类分箱
for i in range(len(keys)):
    # 调用k-means算法,进行聚类离散化
    print(u'正在进行“%s”的聚类...' % keys[i])
    kmodel = KMeans(n_clusters = k, n_jobs = 4) # n_jobs是并行数,一般等于CPU数较好
    kmodel.fit(data[[keys[i]]]) 
    print(kmodel.cluster_centers_)
    r1 = pd.DataFrame(kmodel.cluster_centers_, columns = [typelabel2[keys[i]]]) # 聚类中心
    r2 = pd.Series(kmodel.labels_).value_counts() # 分类统计
    r2 = pd.DataFrame(r2, columns = [typelabel2[keys[i]]+'n']) # 转为DataFrame,记录各个类别的数目

    r = pd.concat([r1, r2], axis = 1).sort_values(typelabel2[keys[i]]) #匹配聚类中心和类别数目
    r.index = [1, 2, 3, 4,5,6] #--------
    print('r-1',r)
    r[typelabel2[keys[i]]] = r[typelabel2[keys[i]]].rolling(2).mean() 
    # rolling_mean()用来计算相邻2列的均值,主要计算分箱的边界点。
    print('r-2', r)
    r.iloc[0,0] = 0.0   # 这两句代码将原来的聚类中心改为边界点。
    print('r-3', r)
    result1 = result1.append(r.T)
    
result1 = result1.sort_index()   # 以Index排序,即以A,B,C,D,E,F顺序排,得出分段的切分阈值
result1.to_excel('processedfile6.xlsx',index=False)



data22 =df[['docuno_id']+num_feats_6].copy()
#连续特征进行分箱
for index,row in result1.iterrows():
    for key,value in typelabel2.items():
        if index == value:
            data22.loc[data22[key]<row[2],key+'new']="-inf~{}".format(str(row[2]))
            data22.loc[(data22[key]>=row[2])&(data22[key]<row[3]),key+'new']="{}~{}".format(str(row[2]),str(row[3]))
            data22.loc[(data22[key]>=row[3])&(data22[key]<row[4]),key+'new']="{}~{}".format(str(row[3]),str(row[4]))
            data22.loc[(data22[key]>=row[4])&(data22[key]<row[5]),key+'new']="{}~{}".format(str(row[4]),str(row[5]))
            
            data22.loc[(data22[key]>=row[5])&(data22[key]<row[6]),key+'new']="{}~{}".format(str(row[5]),str(row[6]))
            data22.loc[(data22[key]>=row[6]),key+'new']="{}~inf".format(str(row[6]))
            
            
# 最中保留分箱后的特征
series_newcols =[]
for col in num_feats_6:
    series_newcols.append(col+'new')
data33=data22[['docuno_id']+series_newcols].copy()
data33.rename(columns={col :col[:-3] for col in data33.columns if col !='docuno_id'},inplace=True)

最后计算所有特征的IV值(注意关联y特征值)

# 计算所有特征的iv值
#计算iv
data_iv = data3.copy()
Kvar = 'docuno'
Yvar = 'is_invergt_case'
ret = caliv_batch(data_iv,Kvar,Yvar)
ret
# ret.to_excel('特征iv值.xlsx',index=False)