import scipy
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from imblearn.over_sampling import SMOTE
from sklearn.linear_model import LogisticRegression as LR
from sklearn.model_selection import train_test_split
plt.rcParams[u'font.sans-serif'] = ['Arial Unicode MS']
'''
1)我们首先把连续型变量分成一组数量较多的分类型变量,比如,将几万个样本分成100组,或50组
2)确保每一组中都要包含两种类别的样本,否则IV值会无法计算
3)我们对相邻的组进行卡方检验,卡方检验的P值很大的组进行合并,直到数据中的组数小于设定的N箱为止
4)我们让一个特征分别分成[2,3,4.....20]箱,观察每个分箱个数下的IV值如何变化,找出最适合的分箱个数
5)分箱完毕后,我们计算每个箱的WOE值, ,观察分箱效果
这些步骤都完成后,我们可以对各个特征都进行分箱,然后观察每个特征的IV值,以此来挑选特征。
'''
class ChiMerge(object):
def __init__(self, X, y, n=5, q=20, graph=True):
self.X = X
self.y = y
self.n = n
self.q = q
self.graph = graph
self.woe = None
def get_woe(self, num_bins):
columns = ["min","max","count_0","count_1"]
df = pd.DataFrame(num_bins,columns=columns)
df["total"] = df.count_0 + df.count_1
df["percentage"] = df.total / df.total.sum()
df["bad_rate"] = df.count_1 / df.total
df["good%"] = df.count_0/df.count_0.sum()
df["bad%"] = df.count_1/df.count_1.sum()
df["woe"] = np.log(df["good%"] / df["bad%"])
return df
def get_iv(self, df):
rate = df["good%"] - df["bad%"]
iv = np.sum(rate * df.woe)
return iv
def fit(self, train_data):
'''获取最好的分箱个数
train_data:包含特征和标签的数据集
X:数据集中特征的名称 []
y:数据集中标签的名称 []
q:初始分箱个数
n:目标分箱个数
'''
woe_ = {}
for x in self.X:
temp_data=train_data[[x,self.y]].copy()
temp_data["qcut"],updown = pd.qcut(temp_data[x], retbins=True, q=self.q, duplicates="drop")
temp_data["qcut"].value_counts()
count_y0 = temp_data[temp_data[self.y] == 0].groupby(by="qcut").count()[self.y]
count_y1 = temp_data[temp_data[self.y] == 1].groupby(by="qcut").count()[self.y]
num_bins = [*zip(updown,updown[1:],count_y0,count_y1)]#num_bins值分别为每个区间的上界,下界,0出现的次数,1出现的次数
#确保每个箱中都有0和1
for i in range(self.q):
if 0 in num_bins[0][2:]:#第一个if循环确保num_bins中的第一组一定同时存在正负样本,使用向后合并
num_bins[0:2] = [(num_bins[0][0],num_bins[1][1],num_bins[0][2]+num_bins[1][2], num_bins[0][3]+num_bins[1][3])]
continue
for i in range(len(num_bins)):#第二个if循环确保num_bins中剩下组一定同时存在正负样本,使用向前合并
if 0 in num_bins[i][2:]:
num_bins[i-1:i+1] = [(num_bins[i-1][0],num_bins[i][1],num_bins[i-1][2]+num_bins[i][2],num_bins[i-1][3]+num_bins[i][3])]
break
else:
break
IV = []
axisx = []
while len(num_bins) > self.n:
pvs = []
for i in range(len(num_bins)-1):
x1 = num_bins[i][2:]
x2 = num_bins[i+1][2:]
pv = scipy.stats.chi2_contingency([x1,x2])[1]#计算卡方值
pvs.append(pv)
i = pvs.index(max(pvs))#选择卡方值最大的分箱
num_bins[i:i+2] = [(num_bins[i][0],num_bins[i+1][1],num_bins[i][2]+num_bins[i+1][2],num_bins[i][3]+num_bins[i+1][3])]
bins_df = pd.DataFrame(self.get_woe(num_bins))
axisx.append(len(num_bins))
IV.append(self.get_iv(bins_df))
woe_[x] = bins_df
self.woe = woe_
if self.graph:
plt.figure()
plt.plot(axisx,IV,c='k')
plt.xticks(axisx)
plt.title(x)
plt.ylabel('IV值')
plt.xlabel('分箱')
plt.grid()
plt.savefig('分箱操作图像/{}'.format(x))
def get_bins_of_col(self, auto_col_bins, hand_bins, train_data):
'''得到分箱的临界值
auto_col_bins:自动分箱
hand_col_bins:手动分箱
train_data:包含特征和标签的数据集
X:数据集中特征的名称 []
y:数据集中标签的名称 ''
q:初始分箱个数
n:目标分箱个数
'''
hand_bins = {k:[-np.inf,*v[:-1],np.inf] for k,v in hand_bins.items()}#保证区间覆盖使用 np.inf替换最大值,用-np.inf替换最小值
bins_of_col = {}
# 生成自动分箱的分箱区间和分箱后的 IV 值
for auto in auto_col_bins:
bins_df = self.woe[auto]
bins_list = sorted(set(bins_df["min"]).union(bins_df["max"]))
bins_list[0],bins_list[-1] = -np.inf,np.inf#保证区间覆盖使用 np.inf 替换最大值 -np.inf 替换最小值
bins_of_col[auto] = bins_list
bins_of_col.update(hand_bins)#合并手动分箱数据
return bins_of_col
def get_woe_data(self, df, col, bins):
df = df[[col,self.y]].copy()
df["cut"] = pd.cut(df[col],bins)
bins_df = df.groupby("cut")[self.y].value_counts().unstack()
bins_df["woe"] =np.log((bins_df[0]/bins_df[0].sum())/(bins_df[1]/bins_df[1].sum()))
#self.woe = bins_df["woe"]
return bins_df["woe"]
def transform(self, bins_of_col, train_data, test_data):
"""得到训练和测试的woe值
bins_of_col:分箱的依据,各个特征的分箱临界值
train_data:包含X,y的训练矩阵
test_data:包含X,y的测试矩阵
y:数据集中标签的名称 ''
"""
#将所有特征的WOE存储到字典当中
woeall = {}
for col in bins_of_col:
woeall[col] = self.get_woe_data(train_data,col,bins_of_col[col])
self.woe = woeall
#不希望覆盖掉原本的数据,创建一个新的DataFrame,索引和原始数据model_data一模一样
train_woe = pd.DataFrame(index=train_data.index)
test_woe = pd.DataFrame(index=test_data.index)
#将原数据分箱后,按箱的结果把WOE结构用map函数映射到数据中
for col in bins_of_col:
train_woe[col] = pd.cut(train_data[col],bins_of_col[col]).map(woeall[col])
test_woe[col] = pd.cut(test_data[col],bins_of_col[col]).map(woeall[col])
#将标签补充到数据中
train_woe[self.y] = train_data[self.y]
test_woe[self.y] = test_data[self.y]
return train_woe,test_woe
简单找了一个金融贷款数据测试一下
LR准确率(未分箱): 0.6737207362840293
KS_train: 0.38 AUC_train: 0.75
KS_test: 0.37 AUC_test: 0.75
==================================================
LR准确率(分箱): 0.7740811342109966
KS_train: 0.56 AUC_train: 0.86
KS_test: 0.55 AUC_test: 0.86
==================================================
XGBoost准确率(分箱): 0.7911628045511407
KS_train: 0.62 AUC_train: 0.89
KS_test: 0.58 AUC_test: 0.87
==================================================
使用分箱后准确率大幅度上升,使用逻辑回归过拟合现象不明显;
使用XGBoost总的准确率 较 LR(分箱) 小幅度提高,但KS指标显示存在一定过拟合。