sklearn-逻辑回归-制作评分卡

devtools/2025/1/11 12:22:34/

目录

数据集处理

分箱

分多少个箱子合适

分箱要达成什么样的效果

对一个特征进行分箱的步骤

分箱的实现

封装计算 WOE 值和 IV值函数

画IV曲线,判断最佳分箱数量

结论

pd.qcut 执行报错

功能函数封装

判断分箱个数


在银行借贷场景中,评分卡是一种以分数形式来衡量一个客户的信用风险大小的手段,它衡量向别人借钱的人(受信人,需要融资的公司)不能如期履行合同中的还本付息责任,并让借钱给别人的人(授信人,银行)造成经济损失的可能性。一般来说,评分卡打出的分值越高,客户的信用越好,风险越小。

数据集处理

分箱


要制作评分卡,是要给各个特征进行分档,以便业务人员能够根据新客户填写的信息,为这个新客户来打分。因此在评分卡制作过程中,一个重要的步骤就是分箱,本质就是对特征进行分档。

分箱是评分卡最难,也是最核心的部分。分箱的本质,就是离散化连续变量,好让拥有不同属性的人被分成不同的类别(打上不同的分数)。

分多少个箱子合适

既然是将连续型变量离散化,箱子的个数必然不能太多,最好控制在十个以下,用来制作评分卡,最好能在4~5个为最佳。离散化连续变量必然伴随着信息的损失,而且箱子越少,信息损失越大。
为了衡量特征上的信息量以及特征对预测函数的贡献,银行业定义了概念Information value(IV):
 

  •  N 是这个特征上箱子的个数
  •  i 代表每个箱子
  •  good% 是这个箱内的优质客户(标签为0)占整个特征中所有优质客户的比例
  •  bad% 是这个箱子里的坏客户(那些会违约的,标签为1)占整个特征中所有坏客户的比例
  •  WOE 是银行业中用来衡量违约概率的指标,中文叫做证据权重(weight of Evidence),本质就是优质客户比上坏客户的比例的对数,WOEi写作 

WOE是对一个箱子来说的,WOE越大,代表这个箱子里的优质客户越多,IV是对整个特征来说的,IV代表的意义由 表1 来控制

表1:

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

分箱要达成什么样的效果

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

对一个特征进行分箱的步骤

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

这些步骤都完成后,我们可以对各个特征都进行分箱,然后观察每个特征的IV值,以此来挑选特征。

分箱的实现

封装计算 WOE 值和 IV值函数

# 计算 WOE 和 BAD RATE
# BAD RATE 是一个箱中,坏的样本所占的比例
# bad% 是一个箱中的坏样本占整个特征中的坏样本的比例def get_woe(num_bins):# 通过 num_bins 数据计算 woecolumns = ["min", "max", "count_0", "count_1"]dataf = pd.DataFrame(num_bins, columns=columns)# 一个箱子中所有的样本数dataf["total"] = dataf.count_0 + dataf.count_1# 一个箱子里的样本数,占所有样本数的比例dataf["percentage"] = dataf.total / dataf.total.sum()dataf["bad_rate"] = dataf.count_1 / dataf.totaldataf["good%"] = dataf.count_0 / dataf.count_0.sum()dataf["bad%"] = dataf.count_1/dataf.count_0.sum()dataf["woe"] = np.log(dataf["good%"] / dataf["bad%"])return dataf# 计算 IV 值
def get_iv(bins_df):rate = bins_df["good%"] - bins_df["bad%"]iv = np.sum(rate * bins_df.woe)return iv

画IV曲线,判断最佳分箱数量

# 导入数据
df = mlp.get_data_source("data8770d35cf63711ee80850242ac850002", return_type="dataframe")
# 删除不用于逻辑回归的列,只有数字类型的列才参与逻辑回归
# axis=1为删除列,第一个参数则输入表头的列表
# axis=0为删除行,第一个参数则输入索引的列表
df.drop(["apply_no", "pt", "query_no", "request_hash"], inplace=True, axis=1)
# 通过查看标签值的分布,发现1的个数只有80个,占比只有1%左右,属于严重不均衡的。如 图1 所示
print("标签值y分布情况:\n{}".format(y.value_counts()))
print("标签值y取1的占比情况:\n{}".format(y.value_counts()[1]/X.shape[0]))# 查看数据分布情况,图2 所示
df.describe([0.01,0.1,0.25,.5,.75,.9,.99]).TX = 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])# 按照等频对需要分箱的列进行分箱,保证每个箱子中好的样本数量和坏的样本数据都 > 0,如果有0,需要减少分箱数量
# "sq_model_score_high" 为例
model_data["qcut"], updown = pd.qcut(model_data["sq_model_score_high"], retbins=True, q=10, duplicates='drop')"""
pd.qcut,基于分位数的分箱函数,本质是将连续型变量离散化,只能够处理一维数据
返回箱子的上限和下限
参数q: 要分箱的个数
参数 retbins=True来要求除了返回箱子的上 限和下限,同时返回一个为索引为样本的索引、元素为分到的箱子的Series的结构
现在返回两个值:每个样本属于哪个箱子(赋值给model_data["qcut"]列),以及所有箱子的上限和下限(赋值给updown变量)
"""
# 查看分箱情况,如 图3 所示
print("分箱结束后,每个箱子的元素个数:\n{}".format(model_data["qcut"].value_counts()))# 统计每个分段 0, 1 的数,结果如 图4 所示
# 这里使用了数据透视表的功能 groupby
count_y0 = model_data[model_data["y"] == 0].groupby(by="qcut").count()["y"]
count_y1 = model_data[model_data["y"] == 1].groupby(by="qcut").count()["y"]print("对列 sq_model_score_high 进行分箱后,每个箱子对应的0的数量为:\n{}".format(count_y0))
print("对列 sq_model_score_high 进行分箱后,每个箱子对应的1的数量为: \n{}".format(count_y1))# num_bins 值分别为每个区间的上界、下届,0 出现的次数,1 出现的次数
num_bins = [*zip(updown, updown[1:], count_y0, count_y1)]# 卡方检验num_bins_ = num_bins.copy()
import matplotlib.pyplot as plt
import scipy
IV = []
axisx = []
# 定义箱的数量,这里为2
N = 2while len(num_bins_) > N:pvs = []# 获取 num_bins_两两之间的卡方检验的置信度(卡方值)for i in range(len(num_bins_)-1):x1 = num_bins_[i][2:]x2 = num_bins_[i+1][2:]# scipy.stats.chi2_contingency()0索引返回 chi2 值,1索引返回 p 值pv = scipy.stats.chi2_contingency([x1, x2])[1]pvs.append(pv)# 通过 p 值进行处理,合并 p 值最大的两组# 找出 p 值最大的那一组所在的下标 i,num_bins_的[i]和[i+1]就是可以合并的组i = pvs.index(max(pvs))# 具体合并第i个箱子和i+1个箱子操作,把 num_bins_的[i:i+2]左闭右开换成新列表[()],新列表元素内容按照新方式计算# 1、第0个元素取第i个箱子的第0个元素,前一个箱子的下限# 2、第1个元素取第i+1个箱子的第1个元素,后一个箱子的上限# 3、第2个元素取第i个箱子的第2个元素和第i+1个箱子的第2个元素加和# 4、第3个元素取第i个箱子的第3个元素和第i+1个箱子的第3个元素加和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值IV.append(get_iv(bins_df))plt.figure()
plt.plot(axisx, IV)
plt.xticks(axisx)
# y 坐标为 IV值
plt.ylabel("IV")
# x 坐标为箱子数量
plt.xlabel("N")
# 画出 IV 值随着箱子数量的变化而变化图,如 图5 所示
plt.show()

图1:

图2:

图3:

图4:

图5:

结论

如 图5 所示,分箱数量越多,IV值越高,在这条线中,寻找随着箱子数量减少,IV值下降最快的那个点,这个转折点对应的箱子数量就是我们要找的相对合适的箱子数量。
这样,就这出了特征 sq_model_score_high 的最佳分箱个数

pd.qcut 执行报错

图6:

如 图6 所示,运行 pd.qcut()函数的时候,如果出现"ValueError: Bin edges must be unique"的错误,这通常意味着在尝试对数据进行分箱时,边界值出现了重复。这可能会导致qcut函数无法确定如何对数据进行分箱,因此需要进行调整以确保边界值唯一。
为了解决这个问题,可以在调用qcut()函数时,传入duplicates='drop'参数来指定处理重复边界值的方式,选择将重复的边界值删除,这可能会导致最终分箱后的箱的数量减少,以及数据条数减少;或者使用cut()函数,pd.cut()函数是根据值本身来确定分箱的边界,因此可以处理重复的边界值,并将它们归入相邻的箱中

model_data["cut"], updown = pd.cut(model_data["sq_model_score_high"], retbins=True, bins=20)

功能函数封装

判断分箱个数
 

def graph_for_best_bin(DF, X, Y, N=5, q=20, graph=True):"""自动优化分箱函数,基于卡方验证的分箱DF:需要输入的数据X:需要分箱的列名Y:分箱数据对应的标签 Y 列名N:保留分箱个数q: 初始分箱的个数graph:是否要画出IV图像区间为前开后闭(]"""import matplotlib.pyplot as pltimport scipymodel_data = DF[[X,Y]].copy()model_data["qcut"], updown = pd.qcut(model_data[X], retbins=True, q=q, duplicates='drop')count_y0 = model_data.loc[model_data[Y] == 0].groupby(by="qcut").count()[Y]count_y1 = model_data.loc[model_data[Y] == 1].groupby(by="qcut").count()[Y]num_bins_ = [*zip(updown, updown[1:], count_y0, count_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])]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])]breakelse:breakdef get_woe(num_bins):columns = ["min", "max", "count_0", "count_1"]dataf = pd.DataFrame(num_bins, columns=columns)dataf["total"] = dataf.count_0 + dataf.count_1dataf["percentage"] = dataf.total / dataf.total.sum()dataf["bad_rate"] = dataf.count_1 / dataf.totaldataf["good%"] = dataf.count_0 / dataf.count_0.sum()dataf["bad%"] = dataf.count_1/dataf.count_1.sum()dataf["woe"] = np.log(dataf["good%"] / dataf["bad%"])return dataf# 计算 IV 值def get_iv(bins_df):rate = bins_df["good%"] - bins_df["bad%"]iv = np.sum(rate * bins_df.woe)return ivIV = []axisx = []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])]bins_df = get_woe(num_bins_)# 记录箱子数axisx.append(len(num_bins_))# 记录对应的IV值IV.append(get_iv(bins_df))if graph:plt.figure()plt.plot(axisx, IV)plt.xticks(axisx)plt.ylabel("IV")plt.xlabel("N")plt.show()return bins_df


http://www.ppmy.cn/devtools/149613.html

相关文章

宝塔安装教程,bt怎么安装 linux

Centos安装脚本 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh 37a09b35 Ubuntu/Deepin安装脚本 wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo b…

《HeadFirst设计模式》笔记(下)

11 代理模式 代理要做的就是控制和管理访问。 你的客户对象所做的就像是在做远程方法调用,但其实只是调用本地堆中的“代理”对象上的方法,再由代理处理所有网络通信的低层细节。 Java的RMI提供了客户辅助对象和服务辅助对象,为客户辅助对…

LeetCode599 两个列表的最小索引总和

解决餐厅选择难题:寻找共同喜爱且索引和最小的餐厅 在生活中,我们常常会面临各种选择难题,就像 Andy 和 Doris 在决定晚餐去哪家餐厅时遇到的困扰。他们各自心中都有一份喜爱餐厅的清单,而现在的任务是找出他们共同喜爱的餐厅中&…

命令模式详解与应用

命令模式(Command Pattern),是一种行为型设计模式。它将请求封装成对象,从而可以参数化其他对象,使得不同的请求、队列或者日志请求等操作都可以被实现,并且支持可撤销的操作。通过引入命令对象来解耦请求的…

日常工作之 Elasticsearch 常用查询语句汇总

日常工作之 Elasticsearch 常用查询语句汇总 查询现有索引创建索引查询索引结构插入数据查询索引数据查看索引磁盘占用信息删除索引查看分词器分词结果指定查询数量指定条件查询数据迁移统计索引数据量更新数据 在使用 es 的过程中,总是会用到 es 的查询语句&#x…

机器学习顶会NeurIPS: AGILE: A Novel Reinforcement Learning Framework of LLM Agents

🌟 研究背景 🌟 随着大型语言模型(LLMs)在指令遵循、推理和零样本学习等方面展现出卓越的能力,基于LLMs的自主代理(LLM Agents)的研究逐渐兴起。然而,如何将规划、反思、工具使用等…

Android中的Service

一、Service的简介 Service是Android系统四大组件之一&#xff0c;定义是服务&#xff0c;一种长时间在后台长时间运行的操作或处理异步任务的组件。Service可以不依赖于用户界面的情况下运行&#xff0c;并且可以在应用被关闭后继续运行。 二、<service> 标签详解 在…

smplx blender插件笔记

目录 liunx安装&#xff1a; liunx安装&#xff1a; pip install smplx 这个创建模型报错 SMPL_blender_addon