前言:在做数据挖掘项目的时候,特征工程通常是其中非常重要的一个环节,但是难度也比较高,并且不同项目特征工程会有所差异,因此在做相关项目时可以多总结一些对结果提升比较明显的操作,近期做了一下天池上面的入门金融风控的项目,发现在对部分连续变量和离散变量进行分箱之后和woe编码后效果有一定提升。
废话不多说进入正题:在特征工程我们通常可以对连续值属性进行分箱操作(也就是常说的离散化),并且对于取值较多的离散变量也可进行分箱操作,分箱之后主要有以下好处:
- 分箱后的特征对异常数据有更好的鲁棒性。例如年龄中含有200,300这类的数据,分箱之后就可能划到>80这一箱中,但是如果直接传入模型训练的话会对模型造成很大干扰。
- 可以将变量转换到相似的尺度上。例如收入有1000,10000,百万,千万等,可以离散化为0(低收入),1(中等收入),2(高收入)等。
- 离散后的变量方便我们做特征交叉。例如将离散后的特征与连续变量进行groupby,构造均值、方差等。
- 缺失值的处理,可以将缺失值合并为一个箱。
并且分箱我们通常会遵循以下原则:
- 组内差异小
- 组间差异大
- 每组占比不小于5%
- 必须有好坏两种分类(对于二分类而言)
对于某个属性分箱过后的结果是好还是坏,我们可以使用WOE和IV进行评估。
1.WOE和IV
(1)WOE(Weight Of Evidence),即证据权重,其表达式如下:
W
O
E
i
=
l
n
(
g
o
o
d
占
比
/
b
a
d
占
比
)
=
l
n
(
g
o
o
d
i
/
g
o
o
d
T
b
a
d
i
/
b
a
d
T
)
WOE_i=ln(good占比/bad占比)\\=ln(\frac{good_i/good_T}{bad_i/bad_T})
WOEi=ln(good占比/bad占比)=ln(badi/badTgoodi/goodT)
其中,
W
O
E
I
WOE_I
WOEI表示第i组的证据权重,
g
o
o
d
i
good_i
goodi表示第i组中标签为good的数,
g
o
o
d
T
good_T
goodT表示数据集中标签为good的总的数量,
b
a
d
bad
bad同理。
(2)IV(Information Value),是用来衡量某一变量的信息量。其计算方式如下:
I
V
=
∑
i
=
1
N
(
g
o
o
d
占
比
−
b
a
d
占
比
)
∗
W
O
E
i
IV=\sum_{i=1}^{N}(good占比-bad占比)*WOE_i
IV=i=1∑N(good占比−bad占比)∗WOEi
其中N是分箱的箱数,IV可以用来衡量变量的预测能力。也就是当我们的分箱结果IV值小于0.03就可以不考虑的分箱。
woe和iv的python实现:
def WOE(data, feat, label):
bin_values = data[feat].unique()
good_total_num = len(data[data[label]==1])
bad_total_num = len(data[data[label]==0])
woe_dic = {}
df = pd.DataFrame()
for i,val in enumerate(bin_values):
good_num = len(data[(data[feat]==val) & (data[label]==1)])
bad_num = len(data[(data[feat]==val) & (data[label]==0)])
df.loc[i,feat] = val
df.loc[i, feat+'_woe'] = np.log( (good_num/good_total_num) / ((bad_num/bad_total_num+0.0001)) )
woe_dic[val] = np.log( (good_num/good_total_num) / ((bad_num/bad_total_num+0.0001)) )
return woe_dic,df
def IV(data, woe_dic, feat, label):
good_total_num = len(data[data[label] == 1])
bad_total_num = len(data[data[label] == 0])
bin_values = data[feat].unique()
feat_IV = 0
for val in bin_values:
woe = woe_dic[val]
good_num = len(data[(data[feat] == val) & (data[label] == 1)])
bad_num = len(data[(data[feat] == val) & (data[label] == 0)])
feat_IV += ((good_num/good_total_num)-(bad_num/bad_total_num))*woe
return feat_IV
知道了特征分箱后的评估方法之后就可以考虑如何进行分箱。而分箱的方法主要分为两大类:无监督分箱(等频分箱、等距分箱),有监督分箱(best-ks分箱、决策树分箱、卡方分箱)。
2.无监督分箱
2.1等频分箱
等频分箱的意思就是分箱之后,每个箱内的数据量相等。可以用pandas提供的qcut方法进行处理。
2.2等距分箱
等距分箱就是每个箱的区间的大小是相等的,每个箱内的数据量不一定相等。
例子:
import pandas as pd
import numpy as np
data = np.random.randn(100)
#等频分箱
data_bins = pd.qcut(data, q=5)
print(data_bins.value_counts())
#等距分箱
data_bins = pd.cut(data,bins=5)
print(data_bins.value_counts())
输出结果:通过结果我们也能很明显看出两种分箱的差异
(-2.554, -0.844] 20
(-0.844, -0.153] 20
(-0.153, 0.25] 20
(0.25, 1.049] 20
(1.049, 2.385] 20
dtype: int64
(-2.558, -1.566] 6
(-1.566, -0.578] 21
(-0.578, 0.41] 43
(0.41, 1.397] 21
(1.397, 2.385] 9
dtype: int64
3.有监督分箱
3.1决策树分箱
决策树分箱的原理就是用想要离散化的变量单变量用树模型拟合目标变量,例如直接使用sklearn提供的决策树(是用cart决策树实现的),然后将内部节点的阈值作为分箱的切点。
补充,cart决策树和ID3、C4.5决策树不同,cart决策树对于
离散变量的处理其实和连续变量一样,都是将特征的所有取值从小到大排序,然后取两两之间的均值,然后遍历所有这些均值,然后取gini系数最小的点作为阈值进行划分数据集。并且该特征后续还可参与划分。
这里需要说明一下:cart的决策树是一颗二叉树,所以对于离散变量处理时会遍历所有值,然后取一个作为一类,剩下的作为另一类,这样建树的结果就是一颗二叉树;但在sklearn包并没有实现对离散属性的单独处理,所以我们传入的离散属性值也会被当成连续值去处理。
“scikit-learn uses an optimised version of the CART algorithm; however, scikit-learn implementation does not support categorical variables for now.”
python实现:
from sklearn.tree import DecisionTreeClassifier
def optimal_binning_boundary(x, y):
'''
利用决策树获得最优分箱的边界值列表,利用决策树生成的内部划分节点的阈值,作为分箱的边界
'''
boundary = [] # 待return的分箱边界值列表
x = x.fillna(-1).values # 填充缺失值
y = y.values
clf = DecisionTreeClassifier(criterion='entropy', # “信息熵”最小化准则划分
max_leaf_nodes=6, # 最大叶子节点数
min_samples_leaf=0.05) # 叶子节点样本数量最小占比
clf.fit(x, y) # 训练决策树
#tree.plot_tree(clf) #打印决策树的结构图
#plt.show()
n_nodes = clf.tree_.node_count #决策树的节点数
children_left = clf.tree_.children_left #node_count大小的数组,children_left[i]表示第i个节点的左子节点
children_right = clf.tree_.children_right #node_count大小的数组,children_right[i]表示第i个节点的右子节点
threshold = clf.tree_.threshold #node_count大小的数组,threshold[i]表示第i个节点划分数据集的阈值
for i in range(n_nodes):
if children_left[i] != children_right[i]: # 非叶节点
boundary.append(threshold[i])
boundary.sort()
min_x = x.min()
max_x = x.max() + 0.1 # +0.1是为了考虑后续groupby操作时,能包含特征最大值的样本
boundary = [min_x] + boundary + [max_x]
return boundary
3.2best-ks分箱
KS(Kolmogorov Smirnov)用于模型风险区分能力进行评估,指标衡量的是好坏样本累计部分之间的差距 。KS值越大,表示该变量越能将正,负客户的区分程度越大。通常来说,KS>0.2即表示特征有较好的准确率。
KS的计算方式:计算分箱后每组的好账户占比和坏帐户占比的差值的绝对值: ∣ g o o d 占 比 − b a d 占 比 ∣ |good占比-bad占比| ∣good占比−bad占比∣,然后取所有组中最大的KS值作为变量的KS值。
best-ks分箱的原理:
- 将变量的所有取值从小到大排序
- 计算每一点的KS值
- 选取最大的KS值对应的特征值 x m x_m xm,将特征x分为 x i ≤ x m {x_i\leq x_m} xi≤xm和 x i > x m {x_i>x_m} xi>xm两部分。然后对于每部分重复2-3步。
python代码实现:
def best_ks_box(data, var_name, box_num):
data = data[[var_name, 'isDefault']]
"""
KS值函数
"""
def ks_bin(data_, limit):
g = data_.iloc[:, 1].value_counts()[0]
b = data_.iloc[:, 1].value_counts()[1]
data_cro = pd.crosstab(data_.iloc[:, 0], data_.iloc[:, 1])
data_cro[0] = data_cro[0] / g
data_cro[1] = data_cro[1] / b
data_cro_cum = data_cro.cumsum()
ks_list = abs(data_cro_cum[1] - data_cro_cum[0])
ks_list_index = ks_list.nlargest(len(ks_list)).index.tolist()
for i in ks_list_index:
data_1 = data_[data_.iloc[:, 0] <= i]
data_2 = data_[data_.iloc[:, 0] > i]
if len(data_1) >= limit and len(data_2) >= limit:
break
return i
"""
区间选取函数
"""
def ks_zone(data_, list_):
list_zone = list()
list_.sort()
n = 0
for val in list_:
m = sum(data_.iloc[:, 0] <= val) - n
n = sum(data_.iloc[:, 0] <= val)
print(val,' , m:',m,' n:',n)
list_zone.append(m)
#list_zone[i]存放的是list_[i]-list[i-1]之间的数据量的大小
list_zone.append(50000 - sum(list_zone))
print('sum ',sum(list_zone[:-1]))
print('list zone ',list_zone)
#选取最大数据量的区间
max_index = list_zone.index(max(list_zone))
if max_index == 0:
rst = [data_.iloc[:, 0].unique().min(), list_[0]]
elif max_index == len(list_):
rst = [list_[-1], data_.iloc[:, 0].unique().max()]
else:
rst = [list_[max_index - 1], list_[max_index]]
return rst
data_ = data.copy()
limit_ = data.shape[0] / 20 # 总体的5%
""""
循环体
"""
zone = list()
for i in range(box_num - 1):
#找出ks值最大的点作为切点,进行分箱
ks_ = ks_bin(data_, limit_)
zone.append(ks_)
new_zone = ks_zone(data, zone)
data_ = data[(data.iloc[:, 0] > new_zone[0]) & (data.iloc[:, 0] <= new_zone[1])]
zone.append(data.iloc[:, 0].unique().max())
zone.append(data.iloc[:, 0].unique().min())
zone.sort()
return zone
3.3卡方分箱
卡方检验的原理:卡方分箱是依赖于卡方检验的分箱方法,在统计指标上选择卡方统计量(chi-Square)进行判别,分箱的基本思想是判断相邻的两个区间是否有分布差异,基于卡方统计量的结果进行自下而上的合并,直到满足分箱的限制条件为止。
基本步骤:
- 预先设定一个卡方的阈值
- 对需要进行离散的属性进行排序,每个取值属于一个区间
- 合并区间:(1)计算每一对相邻区间的卡方值:
X
2
=
∑
i
=
1
2
∑
j
=
1
2
(
A
i
j
−
E
i
j
)
2
E
i
j
X^2=\sum_{i=1}^2\sum_{j=1}^{2}\frac{(A_{ij}-E_{ij})^2}{E_{ij}}
X2=∑i=12∑j=12Eij(Aij−Eij)2;(2)然后将卡方值最小的一对区间合并。
其中 A i j A_{ij} Aij是第i区间第j类的样本数量, E i j E_{ij} Eij是 A i j A_{ij} Aij的期望数量 = N i ∗ C j N =N_i*\frac{C_j}{N} =Ni∗NCj,N是总样本数,N_i是第i组的样本数,C_j是第j类样本数量。
上述步骤的终止条件:
- 分箱个数:每次将样本中具有最小卡方值的区间与相邻的最小卡方区间进行合并,直到分箱个数达到限制条件为止。
- 卡方阈值:根据自由度和显著性水平得到对应的卡方阈值,如果分箱的各区间最小卡方值小于卡方阈值,则继续合并,直到最小卡方值超过设定阈值为止。
python代码实现:
# 计算2*2列联表的卡方值
def get_chi2_value(arr):
rowsum = arr.sum(axis=1) # 对行求和,大小为M的数组,是当前分箱情况下,每个箱的样本数量
colsum = arr.sum(axis=0) # 对列求和,大小为N_class的数组,每个元素就是每个类别的样本数量
n = arr.sum()
emat = np.array([i * j / n for i in rowsum for j in colsum]) #计算每个区间下每个类别样本数量的期望值,大小为M*N_class
arr_flat = arr.reshape(-1)
arr_flat = arr_flat[emat != 0] # 剔除了期望为0的值,不参与求和计算,不然没法做除法!
emat = emat[emat != 0] # 剔除了期望为0的值,不参与求和计算,不然没法做除法!
E = (arr_flat - emat) ** 2 / emat
return E.sum()
# 自由度以及分位点对应的卡方临界值
def get_chi2_threshold(percents, nfree):
return chi2.isf(percents, df=nfree)
# 计算卡方切分的切分点
def get_chimerge_cutoff(ser, tag, max_groups=None, threshold=None):
freq_tab = pd.crosstab(ser, tag)
cutoffs = freq_tab.index.values # 保存每个分箱的下标
freq = freq_tab.values # [M,N_class]大小的矩阵,M是初始箱体的个数,N_class是目标变量类别的个数
while True:
min_value = None #存放所有对相邻区间中卡方值最小的区间的卡方值
min_idx = None #存放最小卡方值的一对区间中第一个区间的下标
for i in range(len(freq) - 1):
chi_value = get_chi2_value(freq[i:(i + 2)]) #计算第i个区间和第i+1个区间的卡方值
if min_value == None or min_value > chi_value:
min_value = chi_value
min_idx = i
if (max_groups is not None and max_groups < len(freq)) or (
threshold is not None and min_value < get_chi2_threshold(threshold, len(cutoffs)-1)):
tmp = freq[min_idx] + freq[min_idx + 1] #合并卡方值最小的那一对区间
freq[min_idx] = tmp
freq = np.delete(freq, min_idx + 1, 0) #删除被合并的区间
cutoffs = np.delete(cutoffs, min_idx + 1, 0)
else:
break
return cutoffs
参考博客:
python实现卡方分箱Chi-Merge
特征工程之分箱–Best-KS分箱
风控场景下的常用特征分箱介绍:BestKs分箱、卡方分箱、聚类分箱等
【评分卡】评分卡入门与创建原则——分箱、WOE、IV、分值分配