数据预处理——数据分箱

数据预处理——数据分箱

建模数据的预处理的过程中,变量分箱(即变量离散化)往往是较为核心一环。变量分箱的优劣往往会影响模型评分效果。

上一步:数据预处理——缺失值、异常值、重复值处理

下一步:变量显著性检验——计算 WOE、IV


那么本篇文章要解决2个问题:

1. 变量分箱的必要性

2. 三类变量分箱方法的及注意要点


一、变量分箱的必要性

知其然知其所以然:我刚开始接触建模时,只是看着其他建模流程中存在这个操作,但却不知道为什么要进行变量离散化,实践过后发现分箱主要有以下4个好处:

1.1 对异常数据有很强的鲁棒性:比如一个特征是会话时长=702341sec,换算成天是8.1天,这属于明显的异常值。如果特征没有离散化,一个异常数据“会话时长=8.1天”会给模型造成很大的干扰;

在很多网页分析系统中,0点之后会话将被强行切分,所以会话时长不可能超过1天。

1.2 在逻辑回归模型中,单变量离散化为N个哑变量后,每个哑变量有单独的权重,相当于为模型引入了非线性,能够提升模型表达能力,加大拟合;

1.3 缺失值也可以作为一类特殊的变量进入模型

1.4 分箱后降低模型运算复杂度,提升模型运算速度,对后后期生产上线较为友好

在真正生产环境中,模型的反馈时效是非常重要的指标。一般地,如果反馈时间超过200ms,系统会返回一个超时的默认结果,以避免用户产品体验变差。

二、三类变量分箱方法的及注意要点

3.1 无序变量分箱

举个例子,在实际模型建立当中,有个 job 职业的特征,取值为(“国家机关人员”,“专业技术人员”,“商业服务人员”),对于这一类变量,如果我们将其依次赋值为(国家机关人员=1;专业技术人员=2;商业服务人员=3),就很容易产生一个问题,不同种类的职业在数据层面上就有了大小顺序之分,国家机关人员和商业服务人员的差距是2,专业技术人员和商业服务人员的之间的差距是1,而我们原来的中文分类中是不存在这种先后顺序关系的。所以这么简单的赋值是会使变量失去原来的衡量效果。

怎么处理这个问题呢,“一位有效编码” (one-hot Encoding)可以解决这个问题,通常叫做虚变量或者哑变量(dummpy variable):比如职业特征有3个不同变量,那么将其生成个2哑变量,分别是“是否国家党政职业人员”,“是否专业技术人员” ,每个虚变量取值(1,0)。

为什么2个哑变量而非3个?
在模型中引入多个虚拟变量时,虚拟变量的个数应按下列原则确定:
1.回归模型有截距:一般的,若该特征下n个属性均互斥(如,男/女;儿童/青年/中年/老年),在生成虚拟变量时,应该生成 n-1个虚变量,这样可以避免产生多重共线性。
2. 回归模型无截距项:有n个特征,设置n个虚拟变量

【1】国家党政职业人员= 1 &专业技术人员 =0,则为国家党政职业人员;

【2】国家党政职业人员= 0 &专业技术人员 =1,则为专业技术人员;

【3】国家党政职业人员= 0 &专业技术人员 =0,则为商业服务人员;

使用pandas可以很方便的对离散型特征进行one-hot编码:

# python 实现
import pandas as pd

df= pd.DataFrame([['专业技术人员','A',1],['国家机关人员','C',2],['国家机关人员','A',1],['商业人员','C',4],['国家机关人员','B',5]],columns=['job','class','value'])
df = pd.get_dummies(df,columns=['job','class'],drop_first=0)# columns表示你要引入分箱的变量,drop_first=0 代表使用 n-1个虚拟变量
print(df)
为避免特征的多重共线性,设置 drop_first =0

3.2有序变量分箱

有序多分类变量是很常见的变量形式,通常在变量中有多个可能会出现的取值,各取值之间还存在等级关系。比如高血压分级(0=正常,1=正常高值,2=1级高血压,3=2级高血压,4=3级高血压)这类变量处理起来简直不要太省心,使用 pandas 中的 map()替换相应变量就行。

import pandas as pd
df= pd.DataFrame(['正常','3级高血压','正常','2级高血压','正常','正常高值','1级高血压'],columns=['blood_pressure'])
dic_blood = {'正常':0,'正常高值':1,'1级高血压':2,'2级高血压':3,'3级高血压':4}
df['blood_pressure_enc'] = df['blood_pressure'].map(dic_blood)
print(df)

3.3连续变量的分箱方式

连续变量的分箱可以划分2种:无监督分组,有监督分组

3.3.1无监督分组:

等宽划分:按照相同宽度将数据分成几等份。缺点是受到异常值的影响比较大。 pandas.cut方法可以进行等宽划分。

等频划分:将数据分成几等份,每等份数据里面的个数是一样的。pandas.qcut方法可以进行等频划分。

import pandas as pd

df = pd.DataFrame([[22,1],[13,1],[33,1],[52,0],[16,0],[42,1],[53,1],[39,1],[26,0],[66,0]],columns=['age','Y'])
#print(df)
df['age_bin_1'] = pd.qcut(df['age'],3) #新增一列存储等频划分的分箱特征
df['age_bin_2'] = pd.cut(df['age'],3)  #新增一列存储等距划分的分箱特征
print(df)

3.3.2有监督学习方法:

卡方分箱,卡方分箱我用的较少,下面的 python 是转载自github 博客贴上供大家参考:

转载:github.com/tatsumiw/Chi

# -*- coding: utf-8 -*-
"""
Created on Sun Oct 28 21:39:24 2018

@author: WZD
"""
def ChiMerge(df,variable,flag,confidenceVal=3.841,bin=10,sample=None):  
    '''
    param df:DataFrame| 必须包含标签列
    param variable:str| 需要卡方分箱的变量名称(字符串)
    param flag:str    | 正负样本标识的名称(字符串)
    param confidenceVal:float| 置信度水平(默认是不进行抽样95%)
    param bin:int            | 最多箱的数目
    param sample: int          | 为抽样的数目(默认是不进行抽样),因为如果观测值过多运行会较慢
    note: 停止条件为大于置信水平且小于bin的数目
    return :DataFrame|采样结果
    '''    
    import pandas as pd
    import numpy as np
    
    
    #进行是否抽样操作
    if sample != None:
        df = df.sample(n=sample)
    else:
        df   
        
    #进行数据格式化录入
    total_num = df.groupby([variable])[flag].count()  #统计需分箱变量每个值数目
    total_num = pd.DataFrame({'total_num': total_num})  #创建一个数据框保存之前的结果
    positive_class = df.groupby([variable])[flag].sum()  #统计需分箱变量每个值正样本数
    positive_class = pd.DataFrame({'positive_class': positive_class})  #创建一个数据框保存之前的结果
    regroup = pd.merge(total_num, positive_class, left_index=True, right_index=True,
                       how='inner')  # 组合total_num与positive_class
    regroup.reset_index(inplace=True)
    regroup['negative_class'] = regroup['total_num'] - regroup['positive_class']  #统计需分箱变量每个值负样本数
    regroup = regroup.drop('total_num', axis=1)
    np_regroup = np.array(regroup)  #把数据框转化为numpy(提高运行效率)
    #print('已完成数据读入,正在计算数据初处理')

    #处理连续没有正样本或负样本的区间,并进行区间的合并(以免卡方值计算报错)
    i = 0
    while (i <= np_regroup.shape[0] - 2):
        if ((np_regroup[i, 1] == 0 and np_regroup[i + 1, 1] == 0) or ( np_regroup[i, 2] == 0 and np_regroup[i + 1, 2] == 0)):
            np_regroup[i, 1] = np_regroup[i, 1] + np_regroup[i + 1, 1]  # 正样本
            np_regroup[i, 2] = np_regroup[i, 2] + np_regroup[i + 1, 2]  # 负样本
            np_regroup[i, 0] = np_regroup[i + 1, 0]
            np_regroup = np.delete(np_regroup, i + 1, 0)
            i = i - 1
        i = i + 1
 
    #对相邻两个区间进行卡方值计算
    chi_table = np.array([])  # 创建一个数组保存相邻两个区间的卡方值
    for i in np.arange(np_regroup.shape[0] - 1):
        chi = (np_regroup[i, 1] * np_regroup[i + 1, 2] - np_regroup[i, 2] * np_regroup[i + 1, 1]) ** 2 \
          * (np_regroup[i, 1] + np_regroup[i, 2] + np_regroup[i + 1, 1] + np_regroup[i + 1, 2]) / \
          ((np_regroup[i, 1] + np_regroup[i, 2]) * (np_regroup[i + 1, 1] + np_regroup[i + 1, 2]) * (
          np_regroup[i, 1] + np_regroup[i + 1, 1]) * (np_regroup[i, 2] + np_regroup[i + 1, 2]))
        chi_table = np.append(chi_table, chi)
    #print('已完成数据初处理,正在进行卡方分箱核心操作')

    #把卡方值最小的两个区间进行合并(卡方分箱核心)
    while (1):
        if (len(chi_table) <= (bin - 1) and min(chi_table) >= confidenceVal):
            break
        chi_min_index = np.argwhere(chi_table == min(chi_table))[0]  # 找出卡方值最小的位置索引
        np_regroup[chi_min_index, 1] = np_regroup[chi_min_index, 1] + np_regroup[chi_min_index + 1, 1]
        np_regroup[chi_min_index, 2] = np_regroup[chi_min_index, 2] + np_regroup[chi_min_index + 1, 2]
        np_regroup[chi_min_index, 0] = np_regroup[chi_min_index + 1, 0]
        np_regroup = np.delete(np_regroup, chi_min_index + 1, 0)

        if (chi_min_index == np_regroup.shape[0] - 1):  # 最小值试最后两个区间的时候
            # 计算合并后当前区间与前一个区间的卡方值并替换
            chi_table[chi_min_index - 1] = (np_regroup[chi_min_index - 1, 1] * np_regroup[chi_min_index, 2] - np_regroup[chi_min_index - 1, 2] * np_regroup[chi_min_index, 1]) ** 2 \
                                           * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) / \
                                       ((np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2]) * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index, 1]) * (np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 2]))
            # 删除替换前的卡方值
            chi_table = np.delete(chi_table, chi_min_index, axis=0)

        else:
            # 计算合并后当前区间与前一个区间的卡方值并替换
            chi_table[chi_min_index - 1] = (np_regroup[chi_min_index - 1, 1] * np_regroup[chi_min_index, 2] - np_regroup[chi_min_index - 1, 2] * np_regroup[chi_min_index, 1]) ** 2 \
                                       * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) / \
                                       ((np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2]) * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index, 1]) * (np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 2]))
            # 计算合并后当前区间与后一个区间的卡方值并替换
            chi_table[chi_min_index] = (np_regroup[chi_min_index, 1] * np_regroup[chi_min_index + 1, 2] - np_regroup[chi_min_index, 2] * np_regroup[chi_min_index + 1, 1]) ** 2 \
                                       * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2] + np_regroup[chi_min_index + 1, 1] + np_regroup[chi_min_index + 1, 2]) / \
                                   ((np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) * (np_regroup[chi_min_index + 1, 1] + np_regroup[chi_min_index + 1, 2]) * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index + 1, 1]) * (np_regroup[chi_min_index, 2] + np_regroup[chi_min_index + 1, 2]))
            # 删除替换前的卡方值
            chi_table = np.delete(chi_table, chi_min_index + 1, axis=0)
    #print('已完成卡方分箱核心操作,正在保存结果')

    #把结果保存成一个数据框
    result_data = pd.DataFrame()  # 创建一个保存结果的数据框
    result_data['variable'] = [variable] * np_regroup.shape[0]  # 结果表第一列:变量名
    list_temp = []
    for i in np.arange(np_regroup.shape[0]):
        if i == 0:
            x = '0' + ',' + str(np_regroup[i, 0])
        elif i == np_regroup.shape[0] - 1:
            x = str(np_regroup[i - 1, 0]) + '+'
        else:
            x = str(np_regroup[i - 1, 0]) + ',' + str(np_regroup[i, 0])
        list_temp.append(x)
    result_data['interval'] = list_temp  #结果表第二列:区间
    result_data['flag_0'] = np_regroup[:, 2]  # 结果表第三列:负样本数目
    result_data['flag_1'] = np_regroup[:, 1]  # 结果表第四列:正样本数目

    return result_data



##############################测试#############################################
from sklearn.model_selection import train_test_split
import seaborn as sn
import pandas as pd

df = sn.load_dataset(name="titanic")
train,test = train_test_split(df,test_size=0.2)

result_data = ChiMerge(df=df,variable="age",flag="survived",confidenceVal=3.841,bin=10,sample=None)

bins = [] #卡方的区间值
bins.append(-float('inf'))
for i in range(result_data["interval"].shape[0]-1):
    
    St = result_data["interval"][i].split(",")
    bins.append(float(St[1]))

bins.append(float('inf'))


train["age"] = pd.cut(x=train["age"],bins=bins,labels=[1,3,5,7,9,11,13,15,17])
test["age"] = pd.cut(x=test["age"],bins=bins,labels=[1,3,5,7,9,11,13,15,17])

发布于 2018-12-13

文章被以下专栏收录