我们都知道:卡方、best-ks、最优分箱等都是比较常用的有监督分箱,那么他们都是如何实现的呢?今天我们就先来学习一下卡方分箱吧,之后会再出其他分箱代码,期待一下吧~图片
1.分箱逻辑:
将变量排序后,计算每一对相邻区间的卡方值,然后将卡方值最小的两个相邻区间进行合并,直至达到箱子上限数、或者卡方阈值
2.核心思想:
如果两个区间合并,那么需要两个区间的样本分布相似,卡方值小,说明两个区间分布相似。(卡方值:用于衡量实际值与理论值的差异程度)
3.实现代码:
############################ 1. 提取样本数据
import numpy as np
import pandas as pd
from pandas import DataFrame as df
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
iris = load_iris()
X = df(iris.data, columns=iris.feature_names).rename(columns=lambda x:x.replace(' (cm)','').replace(' ','_'))
y = pd.Series(iris.target).replace(2,1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1)
train = pd.concat([X_train,y_train],axis=1).rename(columns={0:'y'})
- 调用toad包
################################### 调用toad包
import toad
###### 单个字段分箱 ######
import toad
toad.ChiMerge(X_train['sepal_length'],y_train,5) #array([4.9, 5.1, 5.5, 5.8])
toad.ChiMerge(X_train['sepal_width'],y_train,5) #array([2.4, 2.9, 3. , 3.4])
toad.ChiMerge(X_train['petal_length'],y_train,5) #array([3.3])
toad.ChiMerge(X_train['petal_width'],y_train,5) #array([1.])
###### 所有字段分箱 ######
# 初始化一个combiner类
combiner = toad.transform.Combiner()
# 训练数据并指定分箱方法,其它参数可选
combiner.fit(train,y='y',method='chi',min_samples = 0.05)
# 以字典形式保存分箱结果
bins = combiner.export()
- 手写分箱
############################ 计算卡方值
def cal_chi(regroup_per):
'''
args:
regroup_per:计算卡方值的两行数的坏好值,array
return:
chi_value:卡方值
'''
R_N = regroup_per.sum(axis=1) # 按行求和
C_N = regroup_per.sum(axis=0) # 按列求和
N = regroup_per.sum() # 总数
E = np.ones(regroup_per.shape)*C_N/N
E = (E.T*R_N).T # 期望频数
square = (regroup_per-E)**2/E
square[E==0] = 0 # 当期望频数为0时,作为分母无意义,不计入卡方值
chi_value = square.sum() # 卡方值
return chi_value
① 箱子个数
############################ 卡方分箱(箱子个数)
def ChiMerge_MaxInterval(data,cols_num,label,max_interval=5):
'''
args:
data:数据源,df
cols_num:数值分箱字段,列表
label:标签,str
max_interval:最大分箱数,int
return:
cols_num_points:各变量的切点,字典
'''
cols_num_points = {}
for col in cols_num:
print(col)
if len(data[col].unique()) <= max_interval: # 如果数据的有限值个数 < 最大分箱数,则不分箱,反之分箱
cols_num_points[col] = data[col].unique()
else:
regroup = data.groupby([col])[label].agg(['count','sum']).reset_index().rename(columns={'count':'CntRec','sum':'CntBad'})
regroup['CntGood'] = regroup['CntRec'] - regroup['CntBad']
while len(regroup) > max_interval:
###### 1.计算相邻两个箱子之间的卡方值,并找到卡方值最小的索引位置
minchi_value = np.nan
for i in range(len(regroup)-1):
print(i)
regroup_per = regroup.iloc[i:i+2,2:].values # 提取相邻两个箱子的统计数据
chi_value = cal_chi(regroup_per) # 计算卡方值
if (minchi_value is np.nan) or (minchi_value > chi_value): # 如果卡方值小于最小卡方值,则更新
minchi_value = chi_value
minchi_index = i
##### 2.将卡方值最小的两个箱子进行合并
regroup.iloc[minchi_index,1:] = regroup.iloc[minchi_index,1:] + regroup.iloc[minchi_index+1,1:] # 与后面的箱子进行合并
regroup = regroup.drop(index = minchi_index+1) # 删除后面箱子的索引
regroup.index = range(len(regroup))
cols_num_points[col] = regroup[col].unique().tolist()
return cols_num_points
② 阈值
############################ 卡方分箱(阈值)
## 根据置信度、*度计算卡方值
# from import chi2
# threshold = (0.05,4) # 置信度:95%,*度:4
def ChiMerge_MaxThreshold(data,cols_num,label,max_threshold=9.49,max_interval=5):
'''
args:
data:数据源,df
cols_num:数值分箱字段,列表
label:标签,str
max_threshold:最大阈值,float
max_interval:最大分箱数,int
return:
cols_num_points:各数值变量的切点,字典
'''
cols_num_points = {}
for col in cols_num:
print(col)
regroup = data.groupby([col])[label].agg(['count','sum']).reset_index().rename(columns={'count':'CntRec','sum':'CntBad'})
regroup['CntGood'] = regroup['CntRec'] - regroup['CntBad']
minchi_value = np.nan
while True:
###### 1.计算相邻两个箱子之间的卡方值,并找到卡方值最小的索引位置
for i in range(len(regroup)-1):
print(i)
regroup_per = regroup.iloc[i:i+2,2:].values # 提取相邻两个箱子的统计数据
chi_value = cal_chi(regroup_per) # 计算卡方值
if (minchi_value is np.nan) or (minchi_value > chi_value): # 如果卡方值小于最小卡方值,则更新
minchi_value = chi_value
minchi_index = i
##### 2.将卡方值最小的两个箱子进行合并
if (minchi_value < max_threshold) & (len(regroup)>max_interval):
regroup.iloc[minchi_index,1:] = regroup.iloc[minchi_index,1:] + regroup.iloc[minchi_index+1,1:] # 与后面的箱子进行合并
regroup = regroup.drop(index = minchi_index+1) # 删除后面箱子的索引
regroup.index = range(len(regroup))
else:
break
cols_num_points[col] = regroup[col].unique().tolist()
return cols_num_points