视频作者:菜菜TsaiTsai
链接:【技术干货】菜菜的机器学习sklearn【全85集】Python进阶_哔哩哔哩_bilibili

分训练集和测试集

from sklearn.model_selection import train_test_split
X = pd.DataFrame(X)
y = pd.DataFrame(y)Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.3,random_state=420)model_data = pd.concat([Ytrain,Xtrain],axis=1)
model_data.index = range(model_data.shape[0])
model_data.columns = data.columns
vali_data = pd.concat([Ytest,Xtest],axis=1)
vali_data.index = range(vali_data.shape[0])
vali_data.columns = data.columnsmodel_data.to_csv(r'D:\ObsidianWorkSpace\SklearnData\model_data.csv',header=True,index=False)
vali_data.to_csv(r'D:\ObsidianWorkSpace\SklearnData\vali_data.csv',header=True,index=False)

分箱

前面提到过,我们要制作评分卡,是要给各个特征进行分档,以便业务人员能够根据新客户填写的信息为客户打分。因此在评分卡制作过程中,一个重要的步骤就是分箱。
分箱的本质,其实就是离散化连续变量,好让拥有不同属性的人被分成不同的类别(打上不同的分数),其实本质比较类似于聚类。
我们在分箱中要回答几个问题:

  • 要分多少个箱子才合适

最开始我们并不知道,但是既然是将连续型变量离散化,想也知道箱子个数必然不能太多,最好控制在十个以下。而用来制作评分卡,最好能在4~5个为最佳。我们知道,离散化连续变量必然伴随着信息的损失,并且箱子越少,信息损失越大。为了衡量特征上的信息量以及特征对预测函数的贡献,银行业定义了概念Information value(IV):
IV=∑i=1N(good%−bad%)×WOEi\text{IV}=\sum\limits_{i=1}^{N}(good\%-bad\%)\times \text{WOE}_{i} IV=i=1∑N​(good%−bad%)×WOEi​
IV是对整个特征来说的,IV代表的意义是我们特征上的信息量以及这个特征对模型的贡献
其中NNN是这个特征上箱子的个数,iii代表每个箱子,good%是这个箱内的优质客户(标签为0的客户)站整个特征中所有优质客户的比例,bad%是这个箱子里的坏客户(标签为1的那些客户)占整个特征中所有坏客户的比例
WOE写作
WOEi=ln⁡(good%bad%)\text{WOE}_{i}=\ln \left(\frac{good\%}{bad\%}\right) WOEi​=ln(bad%good%​)

我们一般认为WOE=ln⁡(bad%good%)\begin{aligned} WOE=\ln (\frac{bad\%}{good\%})\end{aligned}WOE=ln(good%bad%​)​,IV=(bad%−good%)×WOEIV=(bad\%-good\%)\times WOEIV=(bad%−good%)×WOE
也就是与视频里正好相反,如果想用一般的方法,可以把标签×−1+1标签\times -1 +1标签×−1+1,来互换标签0,1

这是银行业中用来衡量违约概率的指标,中文叫做证据权重(weight of Evidence),本质其实就是优质客户比上坏客户的比例的对数。
WOE是对一个箱子来说的,WOE越大,代表了这个箱子里的优质客户越多。绝对值越高,表明该组别好坏客户的区隔程度越高;各组之间的WOE值差距应尽可能拉开并呈现由低至高的合理趋势

IV 特征对预测函数的贡献
<0.03 特征几乎不带有效信息,对模型没有贡献,这种特征可以被删除
0.03 ~ 0.09 有效信息很少,对模型的贡献度低
0.1 ~ 0.29 有效信息一般,对模型的贡献度中等
0.3 ~ 0.49 有效信息较多,对模型的贡献度较高
>=0.5 有效信息非常多,对模型的贡献超高并且可疑

可见,IV并非越大越好,我们想要找到IV的大小和箱子个数的平衡点。箱子越多,IV必然越小,因为信息损失会非常多,所以,我们会对特征进行分箱,然后计算每个特征在每个箱子数目下的WOE值,利用IV值的曲线,找出合适的分箱个数。

  • 分箱要达成什么样的效果?

我们希望不同属性的人有不同的分数,因此我们希望在同一个箱子内的人的属性是尽量相似的,而不同箱子的人的属性是尽量不同的,即业界常说的”组间差异大,组内差异小“。对于评分卡来说,就是说我们希望一个箱子内的人违约概率是类似的,而不同箱子的人的违约概率差距很大,即WOE差距要大,并且每个箱子中坏客户所占的比重(bad%)也要不同。
我们可以使用卡方检验来对比两个箱子之间的相似性,如果两个箱子之间卡方检验的P值很大,则说明他们非常相似,那我们就可以将这两个箱子合并为一个箱子。

基于这样的思想,我们总结出我们对一个特征进行分箱的步骤:

  1. 把连续型变量分成一组数量较多的分类型变量,比如,将几万个样本分成100组,或50组
  2. 确保每一组中都要包含两种类别的样本,否则IV值会无法计算
  3. 对相邻的组进行卡方检验,卡方检验的P值很大的组进行合并,直到数据中的组数小于设定的N箱为止
  4. 让一个特征分别分成[2,3,4.....20][2,3,4.....20][2,3,4.....20]箱,观察每个分箱个数下的IV值如何变化,找出最适合的分箱个数
  5. 分箱完毕后,我们计算每个箱的WOE值,bad%bad\%bad%,观察分箱效果

这些步骤都完成后,我们可以对各个特征都进行分箱,然后观察每个特征的IV值,以此来挑选特征。
接下来,我们就以"age"为例子,来看看分箱如何完成

等频分箱

对应步骤中的第一步:把连续型变量分成一组数量较多的分类型变量,比如,将几万个样本分成100组,或50组。这里我们演示分20组

# 按照等频对需要分箱的列进行分箱
model_data["qcut"], updown = pd.qcut(model_data["age"],retbins=True,q=20)
# 在这里时让model_data新添加一列叫做“分箱”,这一列其实就是每个样本所对应的箱子model_data["qcut"].value_counts()
---
(36.0, 39.0]      12646
(20.999, 28.0]    11773
(58.0, 61.0]      11393
……
(56.0, 58.0]       7870
(34.0, 36.0]       7494
(45.0, 46.0]       5699
Name: qcut, dtype: int64updown # 21个元素,代表20个箱子的上下限
---
array([ 21., 28., 31., 34., 36., 39., 41., 43., 45., 46., 48., 50., 52., 54., 56., 58., 61., 64., 68., 74., 107.])

说一下qcut()

pd.qcut(['x', 'q', 'labels=None', 'retbins: bool = False', 'precision: int = 3', "duplicates: str = 'raise'"],
)
# x:一维数组或者Serise
# q:表示分位数的整数或者数组。如果是正数,例如q=10,表示分成10个箱;如果是数组,注意这个数组的元素必须是分数,例如[0,0.25,0.5,0.75,1]也就是分四个箱,里面的元素表示数据的分位数
# rebins:布尔值。是否显示分箱的分界值。
# duplicates:看例子

链接:pd.qcut()数据分箱_一尺荷叶的博客-CSDN博客_pd.qcut()

duplicates=“drop”,常用于某一元素重复出现次数过多,导致分箱上下限为同一个数字,甚至不同分箱上下限为同一个数字,当等于“drop”时合并上下限相同的分箱,这会导致返回的分享个数小于设定的q值

pd.qcut([1,1,2],q=5,duplicates="drop",retbins=True)
---
([(0.999, 1.2], (0.999, 1.2], (1.6, 2.0]]Categories (3, interval[float64, right]): [(0.999, 1.2] < (1.2, 1.6] < (1.6, 2.0]],array([1. , 1.2, 1.6, 2. ]))

要求q=5即5个分箱,但是实际上我们最后只有3个分箱

# 统计分箱里0和1的数量
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"]model_data[model_data["SeriousDlqin2yrs"] == 0].groupby(by='qcut').count().head()
# 注意结果是DataFrame,列标签是所有列# num_bins值分别为每个区间的上界,下界,0出现的次数,1出现的次数
num_bins = [*zip(updown,updown[1:],coount_y0,coount_y1)]
# zip会按照最短的列表进行合并
# 这就是为什么updown,updown[1:]能被zip到一起,因为按照updown[1:]的长度来合并df = pd.DataFrame(num_bins,columns=["min","max","count_0","count_1"])
df.head()
---min  max count_0 count_1
0   21.0    28.0    4243    7530
1   28.0    31.0    3571    5974
2   31.0    34.0    4075    6782
3   34.0    36.0    2908    4586
4   36.0    39.0    5182    7464

确保每个箱中都有0和1

对应步骤中的第二步:确保每一组中都要包含两种类别的样本,否则IV值会无法计算

for i in range(20):# 这里range(20)只是为了保证循环足量,除非全部都是空箱,否则该循环结束的条件为break,用不到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# 合并了之后第一行的组不一定有两种样本,如[(0,1,0,0),(1,2,1,0)……]# 我们每次合并完后,还需要重新检查第一组是否已经包含了两种样本# 这里使用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:]:# 这里虽然有i-1,但是上面我们保证了第一个箱一定不含0,所以不会报错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# 这里的break,只有在if被满足的条件下才会被触发# 也就是说,只有发生了合并,才会打断for i in range(len(num_bins))这个循环# 因为我们在range(len(num_bins))中遍历,但合并发生后,len(num_bins)发生了改变,但循环却不会重新开始。也就是说,如果仍然用原来的i会出现越界错误# 本来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的取值,循环就不会报错了else:break

定义WOE和IV函数

是第三步之前的准备

# 注意BAD RATE与bad%不是一个含义
# BAD RATE是一个箱中,坏的样本所占的比例 (bad/total)
# bad%是一个箱中的坏样本占整个特征中的坏样本的比例df["total"] = df.count_0 + df.count_1 # 一个箱子中所有的样本数
df["precentage"] = 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%"])rate = df["good%"] - df["bad%"]
iv_age = np.sum(rate * df.woe)
iv_age
---
0.3536513548464584

包装成函数

def get_woe(num_bins):columns = ["min","max","count_0","count_1"]df = pd.DataFrame(num_bins,columns=columns)df["total"] = df.count_0 + df.count_1df["percentage"] = df.total / df.total.sum()df["bad_rate"] = df.count_1 / df.totaldf["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 dfdef get_iv(df):rate = df["good%"] - df["bad%"]iv = np.sum(rate * df.woe)return iv

卡方检验,合并箱体,画出IV曲线

对应步骤中的第三步:对相邻的组进行卡方检验,卡方检验的P值很大的组进行合并,直到数据中的组数小于设定的N箱为止

# 卡方检验检验的是相邻的两个箱卡方的置信度,如果P值很大(卡方很小),说明两个箱相似,就可以合并两个箱
num_bins_ = num_bins.copy()import matplotlib.pyplot as plt
import scipyx1 = num_bins_[0][2:]
x2 = num_bins_[1][2:]
scipy.stats.chi2_contingency([x1,x2])
# 第一个元素是卡方值,第二个元素是P值,第三个元素是若标签与特诊完全独立的预测值
# (对于第三个元素,由于存在Yate校正,此时手算结果会与程序运算结果不一致,属于正常现象)
---
(4.2156240212309815,0.04005333124426569,1,array([[4315.33080026, 7457.66919974],[3498.66919974, 6046.33080026]]))# 卡方值越小,认为两个箱越相似
# 极端的例子,如果卡方为0,即两个箱中的样本是成比例的
# 此时这两个箱显然最好是合并起来,并且这个操作信息损失也不会太大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:]pv = scipy.stats.chi2_contingency([x1,x2])[1] # 取出P值# 按照上面说的我们应该是要P值大的pvs.append(pv)i = pvs.index(max(pvs)) # 找出P值最大的两个箱# 合并这两个箱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])]# 获取这次合并之后IV值的变化,用于画图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.show()

用最佳分箱个数分箱,并验证分箱结果

对应步骤中的第四步:让一个特征分别分成[2,3,4.....20][2,3,4.....20][2,3,4.....20]箱,观察每个分箱个数下的IV值如何变化,找出最适合的分箱个数
根据第三步的曲线找出第四步要选的箱个数n。
实际上,我们第三步循环到2个箱,此时我们仍可以后当时的代码,只要把2换成n,返回合并后的分箱即可

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]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_num_bins_ = num_bins.copy()
afterbins = get_bin(num_bins_,6)
afterbins
---
[(21.0, 36.0, 14797, 24872),(36.0, 54.0, 39070, 51429),(54.0, 61.0, 15743, 12213),(61.0, 64.0, 6968, 3192),(64.0, 74.0, 13376, 4218),(74.0, 107.0, 7737, 1393)]

分箱完毕,观察WOE

对应步骤中的第五步:分箱完毕后,我们计算每个箱的WOE值,bad%bad\%bad%,观察分箱效果

bins_df = get_woe(afterbins)
bins_df['woe'] # 箱子尽可能要是单调的,如果有两个转折点一般来说分箱不太好
---
0   -0.523154
1   -0.278683
2    0.250059
3    0.776845
4    1.150265
5    1.710719
Name: woe, dtype: float64
# 这里woe是单调增加的,由负到正,说明效果不错

将选取最佳分箱个数的过程包装为函数

注意缩进,哭了,照着写都能写错

def graphforbestbin(DF,X,Y,n=5,q=20,graph=True):"""自动最优分箱函数,基于卡方检验的分箱参数:DF:需要输入的数据X:需要分箱的列名,注意一次只能输入一列Y:分箱数据对应的标签Y列名n:保留分箱的个数q:初始分箱的个数graph:是否画出IV图像"""DF = DF[[X,Y]].copy()DF["qcut"],bins = pd.qcut(DF[X],retbins=True,q=q,duplicates="drop")coount_y0 = DF[DF[Y] == 0].groupby(by='qcut').count()[Y]coount_y1 = DF[DF[Y] == 1].groupby(by='qcut').count()[Y]num_bins = [*zip(bins,bins[1:],coount_y0,coount_y1)]for i in range(q):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])]continuefor 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])]breakelse:breakdef get_woe(num_bins):columns = ["min","max","count_0","count_1"]df = pd.DataFrame(num_bins,columns=columns)df["total"] = df.count_0 + df.count_1df["percentage"] = df.total / df.total.sum()df["bad_rate"] = df.count_1 / df.totaldf["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 dfdef get_iv(df):rate = df["good%"] - df["bad%"]iv = np.sum(rate * df.woe)return ivaxisx = []IV = []while len(num_bins) > 2: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 = get_woe(num_bins)axisx.append(len(num_bins))IV.append(get_iv(bins_df))if graph:plt.figure()plt.plot(axisx,IV)plt.xticks(axisx)plt.xlabel("number of box")plt.ylabel("IV")plt.show()

对所有特征进行分箱选择

for i in model_data.columns[1:-1]:print(i)graphforbestbin(model_data,i,"SeriousDlqin2yrs",n=2,q=20)

我们发现,不是所有的特征都可以使用这个分箱函数,比如说有的特征,像家人数量,就无法分出20组。于是我们将可以分箱的特征放出来单独分组,不能自动分箱的变量自己观察然后手写
这里显然能自动分箱的很好看出来,我们先写出来

auto_col_bins = {"RevolvingUtilizationOfUnsecuredLines":6,"age":5,"DebtRatio":4,"MonthlyIncome":3,"NumberOfOpenCreditLinesAndLoans":5}
# 这些都是从图上用眼看的,如果数值选择在本例中选择左右都可以

对于不能自动分箱的变量自己观察然后手写,例如NumberOfTime30-59DaysPastDueNotWorse,其分箱图像为

观察下面的value_counts可以发现,有特别多的0,分20个箱导致了大多数箱的上下限为0,又因为我们在qcut中指定了duplicates=“drop”,这些上下限相同的分箱被合并删除,导致分箱数量极少,甚至只有1个或2个。我们又设定while len(num_bins) > 2:,也就是说只有箱数大于2,我们才会计算IV值,否则,IV值为空列表,因此画图为空白,即上图的情况

data["NumberOfTime30-59DaysPastDueNotWorse"].value_counts().sort_index()
---
0     125453
1      16032
2       4598
3       1754
4        747
5        342
6        140
7         54
8         25
9         12
10         4
11         1
12         2
13         1
Name: NumberOfTime30-59DaysPastDueNotWorse, dtype: int64

根据数值分布,我们考虑分箱界限为[0,1,2,13][0,1,2,13][0,1,2,13]。其他特征重复这个过程,得到最后的结果

hand_bins = {"NumberOfTime30-59DaysPastDueNotWorse":[0,1,2,13],"NumberOfTimes90DaysLate":[0,1,2,17],"NumberRealEstateLoansOrLines":[0,1,2,4,54],"NumberOfTime60-89DaysPastDueNotWorse":[0,1,2,8],"NumberOfDependents":[0,1,2,3]}

保证区间覆盖使用 np.inf替换最大值,用-np.inf替换最小值

hand_bins = {k:[-np.inf,*v[1:-1],np.inf] for k,v in hand_bins.items()}hand_bins.items()
---
dict_items([('NumberOfTime30-59DaysPastDueNotWorse', [0, 1, 2, 13]), ('NumberOfTimes90DaysLate', [0, 1, 2, 17]), ('NumberRealEstateLoansOrLines', [0, 1, 2, 4, 54]), ('NumberOfTime60-89DaysPastDueNotWorse', [0, 1, 2, 8]), ('NumberOfDependents', [0, 1, 2, 3])])

接下来对所有特征按照选择的箱体个数和手写的分箱范围进行分箱:

bins_of_col = {}for col in auto_col_bins:bins_df = graphforbestbin(model_data,col,"SeriousDlqin2yrs",n=auto_col_bins[col],q=20,graph=False)bins_list = sorted(set(bins_df["min"]).union(bins_df["max"]))# 保证区间覆盖使用np.inf替换最大值,-np.inf替换最小值bins_list[0],bins_list[-1] = -np.inf,np.infbins_of_col[col] = bins_list

合并手动分箱数据

bins_of_col.update(hand_bins)
bins_of_col
---
{'RevolvingUtilizationOfUnsecuredLines': [-inf,0.09904372745676276,0.29766696424041994,0.46496043070279325,0.9825517345741103,0.9999999,inf],'age': [-inf, 36.0, 54.0, 61.0, 74.0, inf],
……}

计算各箱的WOE并映射到数据中

实际上我们已经有了分箱的边界,储存在bins_of_col,根据分箱的边界可以对数据分箱,然后统计不同分箱中标签得到WOE,再用该WOE值映射回原数据并进行建模即可

之前的分箱是为了找到分箱边界,这里是已经找到了边界,对数据进行分箱

data = model_data.copy() # 分训练集测试集时候的训练集data = data[["age","SeriousDlqin2yrs"]]
data["cut"] = pd.cut(data.loc[:,"age"],bins_of_col["age"])
data.head()
---age  SeriousDlqin2yrs    cut
0   53  0   (36.0, 54.0]
1   63  0   (61.0, 74.0]
2   39  1   (36.0, 54.0]
3   73  0   (61.0, 74.0]
4   53  1   (36.0, 54.0]

qcut(),分割样本集,使得每个箱子中含有样本的数量是相同的
cut(),根据指定的间隔来划分箱子(如果不传入列表而是传入数字,那么cut()的作用是,分割样本集,使得每个箱子的间距是相同的)

data.groupby("cut")["SeriousDlqin2yrs"].value_counts()
# 先将data按照cut分组,索引为cut,分组后只取出SeriousDlqin2yrs列,再对这一列进行统计值出现次数,增加一层SeriousDlqin2yrs所有取值的索引。因此会出现两层索引
---
cut           SeriousDlqin2yrs
(-inf, 36.0]  1                   248720                   14797
(36.0, 54.0]  1                   514290                   39070
(54.0, 61.0]  0                   157431                   12213
(61.0, 74.0]  0                   203441                    7410
(74.0, inf]   0                    77371                    1393
Name: SeriousDlqin2yrs, dtype: int64type(data.groupby("cut")["SeriousDlqin2yrs"].value_counts()) # 实际上还是一个Series,只不过索引有两层
---
pandas.core.series.Seriesdata.groupby("cut")["SeriousDlqin2yrs"].value_counts().unstack()
# unstack()把多层索引拆开,相应的Series可能变为DataFrame
---
SeriousDlqin2yrs    0   1
cut
(-inf, 36.0]    14797   24872
(36.0, 54.0]    39070   51429
(54.0, 61.0]    15743   12213
(61.0, 74.0]    20344   7410
(74.0, inf]     7737    1393bins_df = data.groupby("cut")["SeriousDlqin2yrs"].value_counts().unstack()
bins_df["woe"] = np.log((bins_df[0]/bins_df[0].sum())/(bins_df[1]/bins_df[1].sum()))
bins_df
---
SeriousDlqin2yrs    0   1   woe
cut
(-inf, 36.0]    14797   24872   -0.523154
(36.0, 54.0]    39070   51429   -0.278683
(54.0, 61.0]    15743   12213   0.250059
(61.0, 74.0]    20344   7410    1.006120
(74.0, inf] 7737    1393    1.710719bins_df = data.groupby("cut")["SeriousDlqin2yrs"].value_counts().unstack()
bins_df["woe"] = np.log((bins_df[0]/bins_df[0].sum())/(bins_df[1]/bins_df[1].sum()))
bins_df
---
SeriousDlqin2yrs    0   1   woe
cut
(-inf, 36.0]    14797   24872   -0.523154
(36.0, 54.0]    39070   51429   -0.278683
(54.0, 61.0]    15743   12213   0.250059
(61.0, 74.0]    20344   7410    1.006120
(74.0, inf] 7737    1393    1.710719

把以上过程包装成函数:

data = model_data.copy()
def get_woe(df,col,y,bins):df = df[[col,y]].copy()df["cut"] = pd.cut(df[col],bins)bins_df = df.groupby("cut")[y].value_counts().unstack()woe = bins_df["woe"] = np.log((bins_df[0]/bins_df[0].sum())/(bins_df[1]/bins_df[1].sum()))return woewoeall = {}
for col in bins_of_col:woeall[col] = get_woe(model_data,col,"SeriousDlqin2yrs",bins_of_col[col])woeall
---
{'RevolvingUtilizationOfUnsecuredLines': cut(-inf, 0.099]     2.201208(0.099, 0.298]    0.666221(0.298, 0.465]   -0.125724(0.465, 0.983]   -1.072586(0.983, 1.0]     -0.483491(1.0, inf]       -2.030408dtype: float64, 'age': cut(-inf, 36.0]   -0.523154(36.0, 54.0]   -0.278683(54.0, 61.0]    0.250059(61.0, 74.0]    1.006120(74.0, inf]     1.710719
……}

相比于之前bins_of_col中只有分箱的边界,woeall中储存的是不同分箱的WOE值

接下来,把所有WOE映射到原始数据中

model_woe = pd.DataFrame(index=model_data.index) # 只有index的DataFrame
model_woe.head()
---
0
1
2
3
4model_woe["age"] = pd.cut(model_data["age"],bins_of_col["age"]).map(woeall["age"])pd.cut(model_data["age"],bins_of_col["age"]).head() # 就是之前将数据划分到分箱中
---
0    (36.0, 54.0]
1    (61.0, 74.0]
2    (36.0, 54.0]
3    (61.0, 74.0]
4    (36.0, 54.0]
Name: age, dtype: category
Categories (5, interval[float64, right]): [(-inf, 36.0] < (36.0, 54.0] < (54.0, 61.0] < (61.0, 74.0] < (74.0, inf]]woeall["age"] # 一个Series,使用map将前面Series的值与map里的键匹配,然后匹配到的键对应的值替换前面Series对应位置的值
---
cut
(-inf, 36.0]   -0.523154
(36.0, 54.0]   -0.278683
(54.0, 61.0]    0.250059
(61.0, 74.0]    1.006120
(74.0, inf]     1.710719
dtype: float64pd.cut(model_data["age"],bins_of_col["age"]).map(woeall["age"]).head()
---
0   -0.278683
1    1.006120
2   -0.278683
3    1.006120
4   -0.278683
Name: age, dtype: category
Categories (5, float64): [-0.523154 < -0.278683 < 0.250059 < 1.006120 < 1.710719]

对所有特征操作

for col in bins_of_col:model_woe[col] = pd.cut(model_data[col],bins_of_col[col]).map(woeall[col])

经过这样的操作,本身的数据就变成了对应分箱的WOE值
将标签补充到数据中

model_woe["SeriousDlqin2yrs"] = model_data["SeriousDlqin2yrs"]

测试集进行类似操作

vali_woe = pd.DataFrame(index=vali_data.index)
for col in bins_of_col:vali_woe[col] = pd.cut(vali_data[col],bins_of_col[col]).map(woeall[col])
vali_woe["SeriousDlqin2yrs"] = vali_data["SeriousDlqin2yrs"]

建模与模型验证

X = model_woe.iloc[:,:-1]
y = model_woe.iloc[:,-1]
vali_X = vali_woe.iloc[:,:-1]
vali_y = vali_woe.iloc[:,-1]from sklearn.linear_model import LogisticRegression as LRlr = LR().fit(X,y)
lr.score(vali_X,vali_y)
# 这里相比于视频下降了10%,实际上用视频中的代码执行了一次,结果是77%
# 猜测可能部分方法重写导致的
---
0.7697425098114291

看看ROC曲线上的结果

from sklearn.metrics import roc_curve,roc_auc_scoreFPR,recall,thresholds = roc_curve(vali_y,lr.predict_proba(vali_X)[:,1],pos_label=1)
area = roc_auc_score(vali_y,lr.predict_proba(vali_X)[:,1])plt.figure()
plt.plot(FPR,recall,color='red',label='ROC curve (area = %.2f)'%area)
plt.plot([0,1],[0,1],color='k',linestyle='--')
plt.xlim([-0.05,1.05])
plt.ylim([-0.05,1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('Recall')
plt.legend(loc='lower right')
plt.show()

【菜菜的sklearn课堂笔记】逻辑回归与评分卡-用逻辑回归制作评分卡-分箱相关推荐

  1. 【菜菜的sklearn课堂笔记】决策树-分类树

    视频作者:菜菜TsaiTsai 链接:[技术干货]菜菜的机器学习sklearn[全85集]Python进阶_哔哩哔哩_bilibili 关键概念:节点 根节点:没有进边,有出边.包含最初的,针对特征的 ...

  2. 菜菜的sklearn课堂——随机森林

    1.概述 1.1 集成算法概述 集成算法会考虑多个评估器的建模结果,汇总之后得到一个综合的结果,以此来获取比单个模型更好的回归或分类表现. 1.2 sklearn中的集成算法 sklearn中的集成算 ...

  3. sklearn保存svm分类模型_【菜菜的sklearn】07 支持向量机(上)

    小伙伴们大家好~o( ̄▽ ̄)ブ,我是菜菜,这里是我的sklearn课堂第7期,今天分享的内容是支持向量机(上),下周还有下篇哦~ 我的开发环境是Jupyter lab,所用的库和版本大家参考:Pyth ...

  4. 用逻辑回归制作评分卡

    目录 一.评分卡 二.导库,获取数据 三.探索数据与数据预处理 1.去除重复值 2.填补缺失值 3.描述性统计处理异常值 4.为什么不统一量纲,也不标准化数据分布 5.样本不均衡问题 6.分训练集和测 ...

  5. 《菜菜的机器学习sklearn课堂》逻辑回归

    逻辑回归 概述:名为"回归"的分类器 为什么需要逻辑回归? sklearn中的逻辑回归 linear_model.LogisticRegression 二元逻辑回归的损失函数 损失 ...

  6. 《菜菜的机器学习sklearn课堂》学习笔记 + 课件

    <菜菜的机器学习sklearn课堂> sklearn入门与决策树 随机森林应用泛化误差调参实例 数据预处理和特征工程 降维算法PCA和SVD 逻辑回归 聚类算法Kmeans SVM支持向量 ...

  7. sklearn_逻辑回归制作评分卡_菜菜视频学习笔记

    逻辑回归制作评分卡 3.0 前言 逻辑回归与线性回归的关系 消除特征间的多重共线性 为什么使用逻辑回归处理金融领域数据 正则化的选择 特征选择的方法 分箱的作用 3.1导库 3.2数据预处理 3.2. ...

  8. 《菜菜的机器学习sklearn课堂》聚类算法Kmeans

    聚类算法 聚类算法 无监督学习与聚类算法 sklearn中的聚类算法 KMeans KMeans是如何工作的 簇内误差平方和的定义和解惑 sklearn.cluster.KMeans 重要参数 n_c ...

  9. 《菜菜的机器学习sklearn课堂》sklearn入门与决策树

    决策树 sklearn简介 决策树 - 概述 决策树是如何工作的 sklearn中的决策树 分类树 DecisionTreeClassifier 重要参数 criterion 建立一颗树(只有一个参数 ...

最新文章

  1. PAT甲级1124 Raffle for Weibo Followers :[C++题解]哈希表、微博转发抽奖
  2. 支付宝支付php代码示例,Laravel使用支付宝进行支付的示例代码
  3. qtcreator下拉列表怎么制作_设置EXCEL动态下拉菜单,只需要一个组合键,新手也能快速掌握...
  4. python3.6.5下载安装教程_Ubuntu16.04安装python3.6.5步骤详解
  5. 使用fastadmin的页面跳转模板
  6. 从月薪5千到财富自由:有远见的人都在做这件事情
  7. HTTP ERROR 404 Problem accessing /hwi/authorize.jsp. Reason: HWI HIVE
  8. Nginx 上传图片500错误
  9. 电脑端微信如何清空聊天记录
  10. 讲给后台程序员看的前端系列教程(06)——HTML5标签(5)
  11. OpenFire源码学习之二十九:openfire集群配置
  12. 知到网课英语口语直通车期末考试答案分享!
  13. 金融量化分析基础知识
  14. 计算机桌面右下角的图标不见了,电脑右下角图标不见了怎么办 系统托盘图标消失了解决方法...
  15. 魔兽世界客户端数据研究(二)
  16. FITC/CY3/CY5-PEG-DPG/DPPS/DMG /C16, palmitic acid
  17. android 音频设备类型,Android 音频
  18. 人教版 初步使用计算机 教案,“第1课 信息和信息技术”教学设计
  19. 详解Van emde boas tree
  20. ARFoundation系列讲解 - 84 AR传送门

热门文章

  1. 【JavaLearn】 # (2)类和对象、变量、构造方法、普通方法、this关键字
  2. Redis面试完整版
  3. 华为p40pro,小米10pro,适合自己的才是最好的
  4. centos安装mysql并设置代理
  5. 基于Android的驾校预约管理系统
  6. 二极管、三极管、MOSFET管知识点总结
  7. 电脑里的文档不小心删除了恢复的方法
  8. 滚动轴承特征频率计算推导过程
  9. 建模语言UML在软件开发中的应用
  10. excel计算二元线性回归_谁说菜鸟不会数据分析(高级篇)及竞争力 excel②