目录
分箱
分箱的概念
什么是分箱?如果你初入机器学习的道路,你可能比较的懵逼,为什么要分箱?
数据分箱指的是将连续数据离散化;离散化对异常值具有鲁棒性,运算更快方便存储,而且特征可变性更强方便迭代,特征离散后的模型更加稳定。
那是因为分箱对有些模型带来的好处比起调参优化都要来的直接
举个例子
决策树可以构建更为复杂的数据模型,但这强烈依赖于数据表示。有一种方法可以让线性模型在连续数据上变得更加强大,就是使用特征分箱(binning,也叫离散化,即 discretization)将其划分为多个特征,如下所述。
比如数据集很大、维度很高,但有些特征与输出的关系是非线性的——那么分箱是提高建模能力的好方法。
卡方分箱及代码实现
1.先确定最终分几个箱,也就是最后分几个离散值。
2.如果变量样本大于100,那么先等距的划分为100箱。
3.计算每一对相邻箱间的卡方值
4.将卡方值最小的两个区间合并,一直重复3-4直到满足最终分箱个数。
def tagcount(series,tags):
"""
统计该series中不同标签的数量,可以针对多分类
series:只含有标签的series
tags:为标签的列表,以实际为准,比如[0,1],[1,2,3]
"""
result = []
countseries = series.value_counts()
for tag in tags:
try:
result.append(countseries[tag])
except:
result.append(0)
return result
def ChiMerge3(df, num_split,tags=[1,2,3],pvalue_edge=0.1,biggest=10,smallest=3,sample=None):
"""
df:只包含要分箱的参数列和标签两列
num_split:初始化时划分的区间个数,适合数据量特别大的时候。
tags:标签列表,二分类一般为[0,1]。以实际为准。
pvalue_edge:pvalue的置信度值
bin:最多箱的数目
smallest:最少箱的数目
sample:抽样的数目,适合数据量超级大的情况。可以使用抽样的数据进行分箱。百万以下不需要
"""
import pandas as pd
import numpy as np
import scipy
variable = df.columns[0]
flag = df.columns[1]
#进行是否抽样操作
if sample != None:
df = df.sample(n=sample)
else:
df
#将原始序列初始化为num_split个区间,计算每个区间中每类别的数量,放置在一个矩阵中。方便后面计算pvalue值。
percent = df[variable].quantile([1.0*i/num_split for i in range(num_split+1)],interpolation= "lower").drop_duplicates(keep="last").tolist()
percent = percent[1:]
np_regroup = []
for i in range(len(percent)):
if i == 0:
tempdata = tagcount(df[df[variable]<=percent[i]][flag],tags)
tempdata.insert(0,percent[i])
elif i == len(percent)-1:
tempdata = tagcount(df[df[variable]>percent[i-1]][flag],tags)
tempdata.insert(0,percent[i])
else:
tempdata = tagcount(df[(df[variable]>percent[i-1])&(df[variable]<=percent[i])][flag],tags)
tempdata.insert(0,percent[i])
np_regroup.append(tempdata)
np_regroup = pd.DataFrame(np_regroup)
np_regroup = np.array(np_regroup)
#如果两个区间某一类的值都为0,就会报错。先将这类的区间合并,当做预处理吧
i = 0
while (i <= np_regroup.shape[0] - 2):
check = 0
for j in range(len(tags)):
if np_regroup[i,j+1] ==0 and np_regroup[i+1,j+1]==0:
check += 1
"""
这个for循环是为了检查是否有某一个或多个标签在两个区间内都是0,如果是的话,就进行下面的合并。
"""
if check>0:
np_regroup[i,1:] = np_regroup[i,1:] + np_regroup[i+1,1:]
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):
temparray = np_regroup[i:i+2,1:]
pvalue = scipy.stats.chi2_contingency(temparray,correction=False)[1]
chi_table = np.append(chi_table, pvalue)
temp = max(chi_table)
#把pvalue最大的两个区间进行合并。注意的是,这里并没有合并一次就重新循环计算相邻区间的pvalue,而是只更新影响到的区间。
while (1):
#终止条件,可以根据自己的期望定制化
if (len(chi_table) <= (biggest - 1) and temp <= pvalue_edge):
break
if len(chi_table)<smallest:
break
num = np.argwhere(chi_table==temp)
for i in range(num.shape[0]-1,-1,-1):
chi_min_index = num[i][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, 0] = np_regroup[chi_min_index + 1, 0]
np_regroup = np.delete(np_regroup, chi_min_index + 1, 0)
#最大pvalue在最后两个区间的时候,只需要更新一个,删除最后一个。大家可以画图,很容易明白
if (chi_min_index == np_regroup.shape[0] - 1):
temparray = np_regroup[chi_min_index-1:chi_min_index+1,1:]
chi_table[chi_min_index - 1] = scipy.stats.chi2_contingency(temparray,correction=False)[1]
chi_table = np.delete(chi_table, chi_min_index, axis=0)
#最大pvalue是最先两个区间的时候,只需要更新一个,删除第一个。
elif (chi_min_index == 0):
temparray = np_regroup[chi_min_index:chi_min_index+2,1:]
chi_table[chi_min_index] = scipy.stats.chi2_contingency(temparray,correction=False)[1]
chi_table = np.delete(chi_table, chi_min_index+1, axis=0)
#最大pvalue在中间的时候,影响和前后区间的pvalue,需要更新两个值。
else:
# 计算合并后当前区间与前一个区间的pvalue替换
temparray = np_regroup[chi_min_index-1:chi_min_index+1,1:]
chi_table[chi_min_index - 1] = scipy.stats.chi2_contingency(temparray,correction=False)[1]
# 计算合并后当前与后一个区间的pvalue替换
temparray = np_regroup[chi_min_index:chi_min_index+2,1:]
chi_table[chi_min_index] = scipy.stats.chi2_contingency(temparray,correction=False)[1]
# 删除替换前的pvalue
chi_table = np.delete(chi_table, chi_min_index + 1, axis=0)
#更新当前最大的相邻区间的pvalue
temp = max(chi_table)
print("*"*40)
print("最终相邻区间的pvalue值为:")
print(chi_table)
print("*"*40)
#把结果保存成一个数据框。
"""
可以根据自己的需求定制化。我保留两个结果。
1. 显示分割区间,和该区间内不同标签的数量的表
2. 为了方便pandas对该参数处理,把apply的具体命令打印出来。方便直接对数据集处理。
serise.apply(lambda x:XXX)中XXX的位置
"""
#将结果整合到一个表中,即上述中的第一个
interval = []
interval_num = np_regroup.shape[0]
for i in range(interval_num):
if i == 0:
interval.append('x<=%f'%(np_regroup[i,0]))
elif i == interval_num-1:
interval.append('x>%f'%(np_regroup[i-1,0]))
else:
interval.append('x>%f and x<=%f'%(np_regroup[i-1,0],np_regroup[i,0]))
result = pd.DataFrame(np_regroup)
result[0] = interval
result.columns = ['interval']+tags
#整理series的命令,即上述中的第二个
premise = "str(0) if "
length_interval = len(interval)
for i in range(length_interval):
if i == length_interval-1:
premise = premise[:-4]
break
premise = premise + interval[i] + " else " + 'str(%d+1)'%i + " if "
return result,premise
pandas.cut:
pandas.cut(x, bins, right=True, labels=None, retbins=False, precision=3, include_lowest=False)
参数:
x,类array对象,且必须为一维,待切割的原形式
bins, 整数、序列尺度、或间隔索引。如果bins是一个整数,它定义了x宽度范围内的等宽面元数量,但是在这种情况下,x的范围在每个边上被延长1%,以保证包括x的最小值或最大值。如果bin是序列,它定义了允许非均匀in宽度的bin边缘。在这种情况下没有x的范围的扩展。
right,布尔值。是否是左开右闭区间
labels,用作结果箱的标签。必须与结果箱相同长度。如果FALSE,只返回整数指标面元。
retbins,布尔值。是否返回面元
precision,整数。返回面元的小数点几位
include_lowest,布尔值。第一个区间的左端点是否包含
t/cc_jjj/article/details/78878878
自定义分箱代码实现
import pandas as pd
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
# 有一组人员年龄数据,希望将这些数据划分为“18到25”,“26到35”,“36到60”,“60以上”几个面元
bins = [18, 25, 35, 60, 100]
# 返回的是一个特殊的Categorical对象 → 一组表示面元名称的字符串
cats = pd.cut(ages, bins)
print(cats)
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']###打上标签
cats1 = pd.cut(ages, bins, labels=group_names)
print(cats1)
aa = pd.value_counts(cats) # 按照区间计数
print(aa)
变量分箱对模型的好处
1、降低异常值的影响,增加模型的稳定性
通过分箱来降低噪声,使模型鲁棒性更好
2、缺失值作为特殊变量参与分箱,减少缺失值填补的不确定性(分箱还可以解决缺失值 )
分箱的方法往往要配合变量编码使用,这就大大提高了变量的可解释性
3、增加变量的非线性
提高了模型的拟合能力
4、增加模型的预测效果
通常假设训练集和测试集满足同分布,分箱使连续变量离散化,更容易满足同分布的假设
即减少模型在训练集的表现和测试集的偏差
KS分箱
Best-KS 分箱方法是一种自顶向下的分箱方法。与卡方分箱相比,Best-KS分箱方法只是目标函数采用了 KS 统计量,其余分箱步骤没有差别
注意KS只能处理连续变量
可以用于模型对好坏样本的区分能力
基本思想:
根据KS曲线,取TPR和FPR之间的最大差值,就是KS统计率,也就是KS分箱最优切分点的位置
KS曲线
横轴就是认为设定的阈值,就是区分好坏样本的界限
纵轴:一个是真正率TPR,一个是假正率FPR
之间的差值一定程度反映模型对好坏样本的区分能力
我们希望真正率高一点,假正率低一点(好样本多一点,坏样本少一点)
真正率:正样本预测数 / 正样本实际数
TP /(TP + FN)
假正率:被预测为正的负样本结果数 / 负样本实际数
FP /(FP + TN)
KS分箱过程也就是递归的找最优切分点的过程
KS值越大 模型的区分能力越强
代码实现
# -*- coding: utf-8 -*-
"""
创建KS分箱实验
"""
import pandas as pd
def best_ks_box(data, var_name, box_num):
data = data[[var_name, '是否违约']]
"""
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
# 测试: ks_bin(data,data.shape[0]/7)
"""
区间选取函数
"""
def ks_zone(data_, list_):
list_zone = list()
list_.sort()
n = 0
for i in list_:
m = sum(data_.iloc[:, 0] <= i) - n
n = sum(data_.iloc[:, 0] <= i)
list_zone.append(m)
list_zone.append(50000 - sum(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
# 测试: ks_zone(data_,[23]) #左开右闭
data_ = data.copy()
limit_ = data.shape[0] / 20 # 总体的5%
""""
循环体
"""
zone = list()
for i in range(box_num - 1):
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()
df_ = pd.DataFrame(columns=[0, 1])
for i in range(len(zone) - 1):
if i == 0:
data_ = data[(data.iloc[:, 0] >= zone[i]) & (data.iloc[:, 0] <= zone[i + 1])]
else:
data_ = data[(data.iloc[:, 0] > zone[i]) & (data.iloc[:, 0] <= zone[i + 1])]
data_cro = pd.crosstab(data_.iloc[:, 0], data_.iloc[:, 1])
df_.loc['{0}-{1}'.format(data_cro.index.min(), data_cro.index.max())] = data_cro.apply(sum)
return df_
data = pd.read_excel('测试1.xlsx')
var_name = '年龄'
print(best_ks_box(data, var_name, 5))
最优IV分箱
最优 IV 分箱方法也是自顶向下的分箱方式,其目标函数为 IV 值
IV 值其本质是对称化的 K-L 距离,即在切分点处分裂得到的两部分数据中,选择好坏样本的分布差异最大点作为最优切分点。分箱结束后,计算每个箱内的 IV 值加和得到变量的 IV 值,可以用来刻画变量对目标值的预测能力。即变量的 IV 值越大,则对目标变量的区分能力越强,因此,IV 值还可以用来做变量选择。
代码实现
def feature_woe_iv(x: pd.Series, y: pd.Series, nan: float = -999.) -> pd.DataFrame:
'''
计算变量各个分箱的WOE、IV值,返回一个DataFrame
'''
x = x.fillna(nan)
boundary = optimal_binning_boundary(x, y, nan) # 获得最优分箱边界值列表
df = pd.concat([x, y], axis=1) # 合并x、y为一个DataFrame,方便后续计算
df.columns = ['x', 'y'] # 特征变量、目标变量字段的重命名
df['bins'] = pd.cut(x=x, bins=boundary, right=False) # 获得每个x值所在的分箱区间
grouped = df.groupby('bins')['y'] # 统计各分箱区间的好、坏、总客户数量
result_df = grouped.agg([('good', lambda y: (y == 0).sum()),
('bad', lambda y: (y == 1).sum()),
('total', 'count')])
result_df['good_pct'] = result_df['good'] / result_df['good'].sum() # 好客户占比
result_df['bad_pct'] = result_df['bad'] / result_df['bad'].sum() # 坏客户占比
result_df['total_pct'] = result_df['total'] / result_df['total'].sum() # 总客户占比
result_df['bad_rate'] = result_df['bad'] / result_df['total'] # 坏比率
result_df['woe'] = np.log(result_df['good_pct'] / result_df['bad_pct']) # WOE
result_df['iv'] = (result_df['good_pct'] - result_df['bad_pct']) * result_df['woe'] # IV
print(f"该变量IV = {result_df['iv'].sum()}")
return result_df
测试函数
feature_woe_iv(x=data['RevolvingUtilizationOfUnsecuredLines'],
y=data['SeriousDlqin2yrs'])
分箱WOE趋势单调,bad_rate风险排序性较好,IV值>1.0则说明该变量预测能力很强。
基于树的最优分箱方法
基于树的分箱方法借鉴了决策树在树生成的过程中特征选择(最优分裂点)的目标函数来完成变量分箱过程,可以理解为单变量的决策树模型。
决策树采用自顶向下递归的方法进行树的生成,每个节点的选择目标是为了分类结果的纯度更高,也就是样本的分类效果更好。
因此,不同的损失函数有不同的决策树,ID3采用信息增益方法,C4.5 采用信息增益比,CART 采用基尼系数(Gini)指标
每文一语
身体是革命的本钱!