目录
[封装计算 WOE 值和 IV值函数](#封装计算 WOE 值和 IV值函数)
[pd.qcut 执行报错](#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值很大,说明他们非常相似,就可以将这两个箱子合并为一个箱子。
对一个特征进行分箱的步骤
- 首先把连续型变量分成一组数量较多的分类型变量,比如,将几万个样本分成100组或者50组
- 确保每一组中都要包含两种类别的样本,否则IV值会无法计算
- 对相邻的组进行卡方检验,卡方检验的P值很大的组进行合并,直到数据中的组数小于设定的N箱为止
- 我们让一个特征分别分成[2,3,4...20]箱,观察每个分箱个数下的IV值如何变化,找出最适合的分箱个数
- 分箱完毕后,我们计算每个箱的WOE值,bad%,观察分箱效果
这些步骤都完成后,我们可以对各个特征都进行分箱,然后观察每个特征的IV值,以此来挑选特征。
分箱的实现
封装计算 WOE 值和 IV值函数
python
# 计算 WOE 和 BAD RATE
# BAD RATE 是一个箱中,坏的样本所占的比例
# bad% 是一个箱中的坏样本占整个特征中的坏样本的比例
def get_woe(num_bins):
# 通过 num_bins 数据计算 woe
columns = ["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.total
dataf["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曲线,判断最佳分箱数量
python
# 导入数据
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]).T
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])
# 按照等频对需要分箱的列进行分箱,保证每个箱子中好的样本数量和坏的样本数据都 > 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 = 2
while 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()函数是根据值本身来确定分箱的边界,因此可以处理重复的边界值,并将它们归入相邻的箱中
python
model_data["cut"], updown = pd.cut(model_data["sq_model_score_high"], retbins=True, bins=20)
功能函数封装
判断分箱个数
python
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 plt
import scipy
model_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])]
break
else:
break
def 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_1
dataf["percentage"] = dataf.total / dataf.total.sum()
dataf["bad_rate"] = dataf.count_1 / dataf.total
dataf["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 iv
IV = []
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