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

news2024/11/30 8:43:16

视频作者:菜菜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.columns

model_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 = 1 N ( g o o d % − b a d % ) × WOE i \text{IV}=\sum\limits_{i=1}^{N}(good\%-bad\%)\times \text{WOE}_{i} IV=i=1N(good%bad%)×WOEi
IV是对整个特征来说的,IV代表的意义是我们特征上的信息量以及这个特征对模型的贡献
其中 N N N是这个特征上箱子的个数, i i i代表每个箱子,good%是这个箱内的优质客户(标签为0的客户)站整个特征中所有优质客户的比例,bad%是这个箱子里的坏客户(标签为1的那些客户)占整个特征中所有坏客户的比例
WOE写作
WOE i = ln ⁡ ( g o o d % b a d % ) \text{WOE}_{i}=\ln \left(\frac{good\%}{bad\%}\right) WOEi=ln(bad%good%)

我们一般认为 W O E = ln ⁡ ( b a d % g o o d % ) \begin{aligned} WOE=\ln (\frac{bad\%}{good\%})\end{aligned} WOE=ln(good%bad%) I V = ( b a d % − g o o d % ) × W O E IV=(bad\%-good\%)\times WOE IV=(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值, b a d % 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: int64

updown # 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_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(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 scipy

x1 = 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()

![[附件/Pasted image 20221110110808.png|300]]

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

对应步骤中的第四步:让一个特征分别分成 [ 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值, b a d % 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])]
            continue
        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
        else:
            break
        
    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_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(df):
        rate = df["good%"] - df["bad%"]
        iv = np.sum(rate * df.woe)
        return iv
    
    axisx = []
    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,其分箱图像为
![[附件/Pasted image 20221110114550.png|400]]

观察下面的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.inf
    bins_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                   24872
              0                   14797
(36.0, 54.0]  1                   51429
              0                   39070
(54.0, 61.0]  0                   15743
              1                   12213
(61.0, 74.0]  0                   20344
              1                    7410
(74.0, inf]   0                    7737
              1                    1393
Name: SeriousDlqin2yrs, dtype: int64

type(data.groupby("cut")["SeriousDlqin2yrs"].value_counts()) # 实际上还是一个Series,只不过索引有两层
---
pandas.core.series.Series

data.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	1393

bins_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

bins_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 woe

woeall = {}
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.030408
 dtype: 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
4

model_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: float64

pd.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 LR

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

看看ROC曲线上的结果

from sklearn.metrics import roc_curve,roc_auc_score

FPR,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()

![[附件/Pasted image 20221111195711.png|300]]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/41241.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DSP-FIR滤波器设计

目录 Gibbs现象:用三角函数逼近间断点: Gibbs现象特点: 常见窗函数&#xff1a; 窗函数的主要频谱参数: 矩形窗(Rectangular)&#xff1a; 汉宁窗(Hanning)&#xff1a; 汉明窗(Hamming)&#xff1a; 布莱克曼窗(Blackman)&#xff1a; 窗函数之间的性能对比&#xff…

Script file ‘F:.....\pip-script.py‘ is not present 原因及解决办法

一 报错类型 二 原因 可能我们使用pip install --upgrade pip或者conda安装一下包时因为网络原因导致只是卸载旧版本而未安装。 三 解决策略 3.1 Anaconda 切换到你的anaconda安装目录并进入Scripts文件夹内(D:\Apps\anaconda3\Scripts) 运行以下代码&#xff1a; conda i…

【Kafka】Kafka基础架构及相关概念

文章目录前言一、Kafka基础知识二、Kafka分区副本参考前言 在以前的定义中&#xff0c;Kafka被定义为一个分布式的基于发布/订阅模式的消息队列&#xff08;Message Queue&#xff09;&#xff0c;主要应用于大数据实时处理领域&#xff0c;类似的产品主要有ActiveMQ、RabbitM…

flutter useRootNavigator属性的作用

useRootNavigator 用于确定是否将对话框推到距给定上下文最远或最接近给定上下文的Navigator 问题&#xff1a; 在使用showDatePicker的时候&#xff0c;android手机如果侧滑返回的时候&#xff0c;页面会关闭&#xff0c;showDatePicker弹出的dailog缺没有关闭。 使用如下…

【学习笔记42】操作DOM

操作DOM一、操作DOM1、步骤2、创建元素节点3、创建文本节点4、增加dom(添加到指定父节点的最后)5、增加dom(添加到指定父节点的最后)6、增加dom(添加到父节点的最前边)7、删除DOM8、修改某一个节点二、克隆DOM1、说明2、复制(克隆)一个LI三、获取元素尺寸(占地面积)四、获取浏览…

WordPress怎么禁止用户使用HTML标签,自动过滤HTML代码?

WordPress怎么禁止用户使用HTML标签&#xff0c;自动过滤HTML代码&#xff1f;出于安全考虑WordPress默认禁止角色为作者的用户写文章时直接添加HTML代码&#xff0c;包括读者留言时也是不允许的。如果想开放此限制&#xff0c;允许作者撰写文章和读者留言时添加HTML代码&#…

Java项目——博客系统(前后端交互)

项目展示 项目说明 使用servlet&#xff0c;实现前后端交互&#xff0c;通过jdbc和mysql数据库进行数据传输&#xff0c;使得可以将用户信息和博客列表存储在数据库中&#xff0c;实现真正的服务器&#xff0c;客户端&#xff0c;数据库三者的交互 项目代码 数据库 在sql数…

进阶的风控策略篇:如果筛选最佳策略帮我们锁定优质客群

在番茄风控往期的内容中&#xff0c;我们一直在跟大家介绍风控策略干货内容&#xff0c;相关内容包括&#xff1a; ①风控的拒绝捞回策略 ②多规则的策略筛选 ③策略的调优 ④策略的开发与应用 … 策略相关的内容可谓干货满满&#xff0c;比如关于策略开发与应用的内容上&#…

SpringBoot SpringBoot 原理篇 1 自动配置 1.15 自动配置原理【1】

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 原理篇 文章目录SpringBootSpringBoot 原理篇1 自动配置1.15 自动配置原理【1】1.15.1 看源码了1.15.2 Import({AutoConfig…

初识图学习

初识图学习 一、简单图的基础 什么是图 七桥问题的定义是&#xff1a;一个步行者怎样才能不重复&#xff0c;不遗漏的一次走完七座桥。最后回到出发点。 当年&#xff0c;大数学家在解答七桥问题的同时&#xff0c;也开创了数学的一个新分支图论。 可以毫不夸张的说&#xff…

半小时制作简单版澳大利亚导游地图,太简单了,你也可以

目录 1 前言 2 QQ扫码注册一个账号 3 编辑景区 3.1 新建一个景区 3.2 增加景点 4 预览 5 申请管理员审核 6 看一下发布后的效果 1 前言 不少景区为了提升游客旅游体验&#xff0c;需要制作自己的导游地图&#xff0c;游客扫一下二维码就可以看到景区全貌和景点介绍。制作这…

销售词汇Sell In、Sell Through、Sell Out辨析

原文出处&#xff1a;https://zhuanlan.zhihu.com/p/89563704 销售的过程其实是一个货物和资金双向流动的过程&#xff0c;货物从厂家流向消费者&#xff0c;资金从消费者流向厂家。 但是大多数情况下&#xff0c;厂家是没办法直接把货物卖给消费者的&#xff0c;或者说厂家是没…

虚拟机安装openEuler/MobaXterm工具登录系统

✨✨个人主页:沫洺的主页 &#x1f4da;&#x1f4da;系列专栏: &#x1f4d6; JavaWeb专栏&#x1f4d6; JavaSE专栏 &#x1f4d6; Java基础专栏&#x1f4d6;vue3专栏 &#x1f4d6;MyBatis专栏&#x1f4d6;Spring专栏&#x1f4d6;SpringMVC专栏&#x1f4d6;SpringBoot专…

一个用C#开发的操作系统的开源项目

自从C#的AOT编译机制发布以来&#xff0c;有趣的项目越来越多&#xff0c;今天给大家推荐一个开源项目&#xff0c;用C#开发的64位操作系统。 文章目录项目简介项目源码开发环境虚拟机与裸机硬件支持功能列表完善的编译、安装文档操作系统界面项目地址项目简介 这是一个使用.N…

SpringBoot SpringBoot 原理篇 2 自定义starter 2.2 IP计数业务功能开发【自定义starter】

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 原理篇 文章目录SpringBootSpringBoot 原理篇2 自定义starter2.2 IP计数业务功能开发【自定义starter】2.2.1 大概看看别人…

阿里云服务器安装mysql8

1. 安装前准备 查看是否安装&#xff1a; rpm -qa | grep mysql移除不想要的版本&#xff1a; yum remove 名称查找关于mysql的所有文件&#xff08;配置文件&#xff09; find / -name mysql ##或者 whereis mysql删除配置文件 rm -rf 文件最后需要注意的是&#xff1a;卸载后…

配置iSCSI实现Linux的远程块存储

文章目录一 名词解释二 需求三 环境准备四 搭建流程1 服务端准备硬盘并分区2 服务端安装软件包&#xff0c;并启动服务3 服务端 5个create 操作3-1 定义块设备&#xff0c;创建后备存储。3-2 为目标创建IQN3-3 创建一个ACL来控制特定的客户端访问3-4 为每个后备存储创建一个LUN…

基于C#的校园闲置物品共享系统的开发和实现(Asp.net+Web)

目 录 摘 要 I Abstract II 第1章 绪论 1 1.1选题背景 1 1.1.1校园闲置物品共享系统的开发背景 1 1.1.2学生闲置物品交易活动的现状 1 1.2 校园闲置物品共享系统的研究方向和内容 1 1.2.1研究方向 1 1.2.2研究内容 2 1.3 校园闲置物品共享系统的设计目标 2 1.4 校园闲置物品共…

Visual Studio Code(vs code) 安装c# .net环境 solution

Visual Studio Code(vs code) 安装c# .net环境 solution 一、安装.net必要环境 1.安装环境 https://dotnet.microsoft.com/zh-cn/download 我这里选择的是 .net 6.0 ;版本 .net SDK x86 2.在vs code拓展 搜索.net 全名&#xff1a;.NET Core Extension Pack 作者&#xf…

操作系统的发展与分类

文章目录世界上第一台通用电子计算机操作系统的发展阶段1. 手工操作阶段&#xff08;无操作系统的计算机系统&#xff09;2. 批处理阶段&#xff1a;同时处理多道程序3. 分时操作系统&#xff08;Time Sharing System&#xff09;4. 实时操作系统&#xff08;Real Time System&…