码字不易,看完点赞!!!
WEO编码
为什么要进行分箱???
一般在建立分类模型时,需要对连续变量离散化,特征离散化后,模型会更稳定,降低了模型过拟合的风险。比如在建立申请评分卡模型时用logsitic作为基模型就需要对连续变量进行离散化,离散化通常采用分箱法。
前言:
WOE(证据全重)是对原始自变量的一种编码形式。要对一个变量进行WOE编码,需要首先把这个变量进行分箱。分箱后,对于第i组,WOE的计算公式如下:
yi是这个分组中响应客户(即取值为1)的数量,yT是全部样本中所有响应客户(即取值为1)的数量
ni是这个分组中未响应客户(即取值为0)的数量,nT是全部样本中所有未响应客户(即取值为0)的数量
from sklearn.model_selection import train_test_split
X = pd.DataFrame(X)
y = pd.DataFrame(y)
X_train, X_vali, Y_train, Y_vali = train_test_split(X,y,test_size=0.3,random_state=420)
model_data = pd.concat([Y_train, X_train], axis=1)#训练数据构建模型
model_data.index = range(model_data.shape[0])
model_data.columns = data.columns
vali_data = pd.concat([Y_vali, X_vali], axis=1)#验证集
vali_data.index = range(vali_data.shape[0])
vali_data.columns = data.columns
model_data.to_csv(r".\model_data.csv")#训练数据
vali_data.to_csv(r".\vali_data.csv")#验证数据
2、按照等频对需要分箱的列进行分箱(区间的边界值要经过选择,使得每个区间包含大致相等的实例数量。比如说 N=10 ,每个区间应该包含大约10%的实例)
model_data["qcut"], updown = pd.qcut(model_data["age"], retbins=True, q=20)#等频分箱
"""
pd.qcut,基于分位数的分箱函数,本质是将连续型变量离散化
只能够处理一维数据。返回箱子的上限和下限
参数q:要分箱的个数
参数retbins=True来要求同时返回结构为索引为样本索引,元素为分到的箱子的Series
现在返回两个值:每个样本属于哪个箱子,以及所有箱子的上限和下限
"""
#在这里时让model_data新添加一列叫做“分箱”,这一列其实就是每个样本所对应的箱子
model_data["qcut"]
model_data["qcut"].value_counts()
统计每个箱中0和1的数量,zip成(上限,下限,0出现的次数,1出现的次数)
# 这里使用了数据透视表的功能groupby
coount_y0 = model_data[model_data["SeriousDlqin2yrs"] == 0].groupby(by="qcut").count()["SeriousDlqin2yrs"]
coount_y1 = model_data[model_data["SeriousDlqin2yrs"] == 1].groupby(by="qcut").count()["SeriousDlqin2yrs"]
#num_bins值分别为每个区间的上界,下界,0出现的次数,1出现的次数
num_bins = [*zip(updown,updown[1:],coount_y0,coount_y1)]
#注意zip会按照最短列来进行结合
num_bins
for i in range(20):
#如果第一个组没有包含正样本或负样本,向后合并
if 0 in num_bins[0][2:]:
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
"""
合并了之后,第一行的组是否一定有两种样本了呢?不一定
如果原本的第一组和第二组都没有包含正样本,或者都没有包含负样本,那即便合并之后,第一行的组也还是没有
包含两种样本
所以我们在每次合并完毕之后,还需要再检查,第一组是否已经包含了两种样本
这里使用continue跳出了本次循环,开始下一次循环,所以回到了最开始的for i in range(20), 让i+1
这就跳过了下面的代码,又从头开始检查,第一组是否包含了两种样本
如果第一组中依然没有包含两种样本,则if通过,继续合并,每合并一次就会循环检查一次,最多合并20次
如果第一组中已经包含两种样本,则if不通过,就开始执行下面的代码
"""
#已经确认第一组中肯定包含两种样本了,如果其他组没有包含两种样本,就向前合并
#此时的num_bins已经被上面的代码处理过,可能被合并过,也可能没有被合并
#但无论如何,我们要在num_bins中遍历,所以写成in range(len(num_bins))
for i in range(len(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
#如果对第一组和对后面所有组的判断中,都没有进入if去合并,则提前结束所有的循环
else:
break
"""
这个break,只有在if被满足的条件下才会被触发
也就是说,只有发生了合并,才会打断for i in range(len(num_bins))这个循环
为什么要打断这个循环?因为我们是在range(len(num_bins))中遍历
但合并发生后,len(num_bins)发生了改变,但循环却不会重新开始
举个例子,本来num_bins是5组,for i in range(len(num_bins))在第一次运行的时候就等于for i in
range(5)
range中输入的变量会被转换为数字,不会跟着num_bins的变化而变化,所以i会永远在[0,1,2,3,4]中遍历
进行合并后,num_bins变成了4组,已经不存在=4的索引了,但i却依然会取到4,循环就会报错
因此在这里,一旦if被触发,即一旦合并发生,我们就让循环被破坏,使用break跳出当前循环
循环就会回到最开始的for i in range(20)中
此时判断第一组是否有两种标签的代码不会被触发,但for i in range(len(num_bins))却会被重新运行
这样就更新了i的取值,循环就不会报错了
"""
3、计算IV、WOE和BAD RATE
BAD RATE与bad%不是一个东西
BAD RATE是一个箱中,坏的样本所占的比例 (bad/total)
而bad%是一个箱中的坏样本占整个特征中的坏样本的比例
def get_woe(num_bins):
# 通过 num_bins 数据计算 woe
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
#计算IV值
def get_iv(df):
rate = df["good%"] - df["bad%"]
iv = np.sum(rate * df.woe)
return iv
根据iv值和箱子数量图像来确定分箱个数
num_bins_ = num_bins.copy()
import matplotlib.pyplot as plt
import scipy
IV = []
axisx = []
while len(num_bins_) > 2:#大于设置的最低分箱个数
pvs = []
#获取 num_bins_两两之间的卡方检验的置信度(或卡方值)
for i in range(len(num_bins_)-1):
x1 = num_bins_[i][2:]
x2 = num_bins_[i+1][2: ]
# 0 返回 chi2 值,1 返回 p 值。
pv = scipy.stats.chi2_contingency([x1,x2])[1]#p值
# chi2 = scipy.stats.chi2_contingency([x1,x2])[0]#计算卡方值
pvs.append(pv)
# 通过 p 值进行处理。合并 p 值最大的两组
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 = get_woe(num_bins_)
axisx.append(len(num_bins_))
IV.append(get_iv(bins_df))
plt.figure()
plt.plot(axisx,IV)
plt.xticks(axisx)
plt.xlabel("number of box")
plt.ylabel("IV")
plt.show()
#选择转折点处,也就是下坠最快的折线点,所以这里对于age来说选择箱数为6
def get_bin(num_bins_,n):
while len(num_bins_) > 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]
# chi2 = scipy.stats.chi2_contingency([x1,x2])[0]
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])]
return num_bins_
afterbins = get_bin(num_bins,6)
afterbins
bins_df = get_woe(num_bins)
bins_df
#希望每组的bad_rate相差越大越好;
# woe差异越大越好,应该具有单调性,随着箱的增加,要么由正到负,要么由负到正,只能有一个转折过程;
# 如果woe值大小变化是有两个转折,比如呈现w型,证明分箱过程有问题
# num_bins保留的信息越多越好