WEO编码

码字不易,看完点赞!!!
WEO编码
为什么要进行分箱???
一般在建立分类模型时,需要对连续变量离散化,特征离散化后,模型会更稳定,降低了模型过拟合的风险。比如在建立申请评分卡模型时用logsitic作为基模型就需要对连续变量进行离散化,离散化通常采用分箱法。

前言:
WOE(证据全重)是对原始自变量的一种编码形式。要对一个变量进行WOE编码,需要首先把这个变量进行分箱。分箱后,对于第i组,WOE的计算公式如下:
在这里插入图片描述
yi是这个分组中响应客户(即取值为1)的数量,yT是全部样本中所有响应客户(即取值为1)的数量

ni是这个分组中未响应客户(即取值为0)的数量,nT是全部样本中所有未响应客户(即取值为0)的数量

在这里插入图片描述
1、分割数据集,分成训练集和测试集

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.columnsvali_data = pd.concat([Y_vali, X_vali], axis=1)#验证集
vali_data.index = range(vali_data.shape[0])
vali_data.columns = data.columnsmodel_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 数据计算 woecolumns = ["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 scipyIV = []
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保留的信息越多越好

在这里插入图片描述

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注