泗县住房和城乡建设局网站,扬州外贸网站建设公司,厦门网站建设创建有哪些,成都电商网站制作背景知识#xff1a;
在银行借贷过程中#xff0c;评分卡是一种以分数形式来衡量一个客户的信用风险大小的手段。今天我们来复现一个评分A卡的模型。完整的模型开发所需流程包括#xff1a;获取数据#xff0c;数据清洗和特征工程#xff0c;模型开发#xff0c…背景知识
在银行借贷过程中评分卡是一种以分数形式来衡量一个客户的信用风险大小的手段。今天我们来复现一个评分A卡的模型。完整的模型开发所需流程包括获取数据数据清洗和特征工程模型开发模型检验和评估模型上线模型检测和报告。
我们先来导入相关的模块
获取数据——数据清洗——特征工程——模型训练和开发——模型检验和评估——模型上线和监控
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler,MinMaxScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
先获取数据并查看数据的形状
# 1.获取数据
data pd.read_csv(rE:\AI课程笔记\机器学习_2\05逻辑回归与评分卡\rankingcard.csv)
data.drop(Unnamed: 0, axis1, inplaceTrue)
data.shape #(150000, 11) 接着做数据清洗包括重复值缺失值和异常值。先去除重复值并重置索引
# 2.1 去除重复值
data.drop_duplicates(inplaceTrue)
data.shape
data.index range(data.shape[0]) # 重置索引
查看有多少缺失值
data.isnull().sum() # 查看缺失值 发现monthly Income和numberofdependents有缺失值。在这里NumberOfDependents用所在列的平均值来填充
(注意在具体业务中算法工程师需要和业务人员具体了解每项业务指标的含义来筛选最合适的填充方式)
data[NumberOfDependents].fillna(int(data[NumberOfDependents].mean()), inplaceTrue) # 用平均值填补缺失值
MonthlyIncome我们用随机森林回归随机森林回归的原理是基于用特征ABC去预测Z的思想所以也可以用ABZ去预测C来填充
def fill_missing_rf(x, y, to_fill):使用随机森林填补一个特征的缺失值的函数参数x:要填补的特征矩阵y:完整的没有缺失值的标签to_fill:字符串,要填补的那一列的名称# 构建我们的新特征矩阵和新标签df x.copy() # 复制特征矩阵fill df.loc[:, to_fill] # 提取我们的标签df pd.concat([df.loc[:, df.columns ! to_fill], pd.DataFrame(y)], axis1) # 构建新的特征矩阵# 找出我们的训练集和测试集Ytrain fill[fill.notnull()]Ytest fill[fill.isnull()]Xtrain df.iloc[Ytrain.index, :]Xtest df.iloc[Ytest.index, :]# 用随机森林回归来填补缺失值from sklearn.ensemble import RandomForestRegressor as rfrrfr rfr(n_estimators100).fit(Xtrain, Ytrain)Ypredict rfr.predict(Xtest)return Ypredict
X data.iloc[:, 1:]
Y data[SeriousDlqin2yrs]
y_pred fill_missing_rf(X, Y, MonthlyIncome)
data.loc[data.loc[:, MonthlyIncome].isnull(), MonthlyIncome] y_pred
data.isnull().sum() # 查看缺失值
将缺失值填充完毕后查看数据信息 发现数据已经没有缺失值了。最后我们来处理异常值。显示数据永远都会有异常值我们需要去根据业务性质去捕捉。在这里我们发现有一条年龄为0的数据这显然是异常值因此我们将它删除并返回删除后的原数据还有更多的异常值需要银行业务方面的知识和算法无关这里就不赘述了
data data[data[age] ! 0]
到这里重复值缺失值和异常值我们都处理完毕了。再考虑是否需要做标准化答案是不需要。因为对业务人员来说他们无法理解标准化后的数据是什么意思。
接下来我们查看一下好客户和坏客户分别有多少
Y.value_counts() 我们发现标签值0有13w的数据1只有不到1w的数据这说明数据有严重的样本不均衡问题。在这里我们可以使用上采样法去平衡样本
# 样本不均衡用上采样算法生成新的样本
import imblearn # imblearn是专门用来处理样本不均衡问题的库
from imblearn.over_sampling import SMOTE # SMOTE是上采样算法
sm SMOTE(random_state42) # 实例化
X data.iloc[:, 1:]
Y data[SeriousDlqin2yrs]
X, Y sm.fit_resample(X, Y) # 返回上采样过后的特征矩阵和标签
X pd.DataFrame(X) # 将X转换为DataFrame格式
Y pd.DataFrame(Y) # 将Y转换为DataFrame格式
data2 pd.concat([Y, X], axis1) # 将X和Y合并
data2.columns data.columns # 将data2的列名改为data的列名
data2.head(5)
data2.shape
Y.value_counts() 这个时候我们发现样本就均衡了。
到这里我们就完成了数据预处理的全部工作。接下来我们将数据切片成特征矩阵和标签矩阵在其基础上划分为训练集和测试集后将特征训练集和标签训练集合并特征测试集和标签测试集合并并将他们保存至本地
# 数据集划分
X data2.iloc[:, 1:]
Y data2.iloc[:, 0]
Xtrain, Xtest, Ytrain, Ytest train_test_split(X, Y, test_size0.3, random_state420)# 训练集和测试集分别存储至本地
train pd.concat([Ytrain, Xtrain], axis1)
train.index range(train.shape[0])
train.columns data.columnstest pd.concat([Ytest, Xtest], axis1)
test.index range(test.shape[0])
test.columns data.columnstrain.to_csv(rE:\AI课程笔记\机器学习_2\05逻辑回归与评分卡\train.csv)
test.to_csv(rE:\AI课程笔记\机器学习_2\05逻辑回归与评分卡\test.csv)
接下来我们对各个特征进行分档我们使用分箱来离散化连续变量好让拥有不同属性的人根据不同的特征被分成不同的类别打上不同的分数类似于聚类。分箱最好在4-5个为佳。
分箱有几个重要的原因
简化模型将连续数据分成箱子后可以将其视为离散数据更容易建立和理解模型。处理异常值分箱可以帮助识别和处理异常值将其归入适当的箱子中减少异常值对模型的影响。解决非线性关系某些情况下变量与目标之间的关系可能是非线性的分箱可以捕捉到这种非线性关系。
在这里还要介绍两个概念IV和WOE。 每个箱子的WOE越大代表这个箱子的优质客户越多IV值衡量的是某一个变量的信息量可用来表示一个变量的预测能力用来做特征选择。箱子越多IV会越小因为信息损失会很多IV越小说明特征几乎不带有有效信息对模型没有贡献可以被删除但IV越大有效信息非常多对模型的贡献率超高并且可疑。所以我们需要找到V的大小和箱子个数的平衡点。 在分箱的过程中箱子的数量是一个重要的参数。箱子的数量越多每个箱子的区间就越小模型对数据的拟合程度就越高但是也会导致信息损失更多。因为当箱子的数量增加时每个箱子中的样本数量就会减少从而导致每个箱子中的样本分布更加不均匀可能会出现某些箱子中只有少数样本或者某些箱子中只有一种样本。这些情况都会导致模型的泛化能力下降从而影响模型的预测效果。所以我们需要画出IV值的学习曲线。
分箱的步骤是①先把连续性变量分成分类型变量②确保每一组都包含两种类型的样本③对相邻的组进行卡方检验如果P值很大则进行合并直到少于N箱。④让一个特征分成(2,3,4,20)箱观察每个特征的IV值如何变化找出最适合的分箱个数。⑤计算每个分箱的WOE值观察分箱效果。
接下来以[age]特征为例来对数据进行分箱在这里我们用pandas库的qcut函数来分箱假设先分成20箱q 20并生成一个“qcut新列”
# qcut等频分箱
train1 train.copy()
train1[qcut], updown pd.qcut(train1[age], retbinsTrue, q20) # 等频分箱
train1[qcut].value_counts() # 查看每个分箱中的样本量
updown # 查看每个分箱的上限和下限
新生成的列如下图所示 可以清晰的看到每个样本所在的分箱情况我们再来看看每个箱子里面包含的样本数 接下来我们再来看看每个箱子中0和1的个数
# 查看每个分箱中0和1的数量
coount_y0 train1[train1[SeriousDlqin2yrs] 0].groupby(byqcut).count()[SeriousDlqin2yrs] # 每个箱子中0的个数
coount_y1 train1[train1[SeriousDlqin2yrs] 1].groupby(byqcut).count()[SeriousDlqin2yrs] # 每个箱子中1的个数 上图所示的是每个箱子中0的个数。为了将数据信息统一展示我们运行如下代码将数据合并-
num_bins [*zip(updown, updown[1:], coount_y0, coount_y1)] # 将每个分箱的上限、下限、0的个数、1的个数放在一起 为了让数据可读性更强我们重新生成表头 columns [min, max, count_0, count_1]df pd.DataFrame(num_bins, columnscolumns) 每个箱子的上限和下限以及0的数量1的数量都清晰可见了。接下来我们构造两个函数分别计算WOE和IV值
# 计算WOE和iv值
def get_woe(num_bins):# 通过num_bins数据计算woecolumns [min, max, count_0, count_1]df pd.DataFrame(num_bins, columnscolumns) # 将num_bins转换为DataFramedf[total] df.count_0 df.count_1 # 每个箱子的总数df[percentage] df.total / df.total.sum() # 每个箱子的占比df[bad_rate] df.count_1 / df.total # 每个箱子中1的占比df[good%] df.count_0 / df.count_0.sum() # 每个箱子中0的占比df[bad%] df.count_1 / df.count_1.sum() # 每个箱子中1的占比df[woe] np.log(df[good%] / df[bad%]) # 计算每个箱子的woe值return df
# 计算IV值
def get_iv(df): # 通过df计算IV值rate df[good%] - df[bad%] # 计算每个箱子中好人和坏人的占比差iv np.sum(rate * df.woe) # 计算IV值return iv
接下来我们通过卡方检验判断箱子之间的相似性
# 卡方检验 用来检验两个变量之间是否独立
num_bins_ num_bins.copy()
import scipy.stats
IV []
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返回卡方值1返回p值pv scipy.stats.chi2_contingency([x1, x2])[1] # p值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() 由图可知我们要找到转折点也就是当箱体等于6时可以得到最优的IV。因为当箱体从6开始IV值的增长速率由快转慢。
接下来我们把分箱过程包装成1个函数
# 将合并箱体的过程包装成函数实现分箱
def get_bin(num_bins,n):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:]# 0返回卡方值1返回p值pv scipy.stats.chi2_contingency([x1, x2])[1] # p值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])] # 将卡方值最大的两组合并return num_binsafterbins get_bin(num_bins, 6)
afterbins可以看到原先20箱的数据现在变成了6箱。查看一下每组的WOE值
bins_df get_woe(afterbins)
bins_df 可以看到WOE的组间差距很大并且WOE单调递增如果WOE有超过两个转折点说明分箱过程有问题。接下来我们将上述的全部分箱过程打包成一个函数
# 接下来我们将选取最佳分箱个数的过程包装成函数对所有特征进行分箱
def graphforbestbin(DF, X, Y, n5, q20, graphTrue):自动最优分箱函数基于卡方检验的分箱参数DF: 需要输入的数据X: 需要分箱的列名Y: 分箱数据对应的标签 Y 列名n: 保留分箱个数q: 初始分箱的个数graph: 是否要画出IV图像区间为前开后闭 (]DF DF[[X, Y]].copy()DF[qcut], bins pd.qcut(DF[X], retbinsTrue, qq, duplicatesdrop)coount_y0 DF.loc[DF[Y] 0].groupby(byqcut).count()[Y] # 每个箱子中0的个数coount_y1 DF.loc[DF[Y] 1].groupby(byqcut).count()[Y] # 每个箱子中1的个数num_bins [*zip(bins, bins[1:], coount_y0, coount_y1)] # 将每个分箱的上限、下限、0的个数、1的个数放在一起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):# 通过num_bins数据计算woecolumns [min, max, count_0, count_1]df pd.DataFrame(num_bins, columnscolumns) # 将num_bins转换为DataFramedf[total] df.count_0 df.count_1 # 每个箱子的总数df[percentage] df.total / df.total.sum() # 每个箱子的占比df[bad_rate] df.count_1 / df.total # 每个箱子中1的占比df[good%] df.count_0 / df.count_0.sum() # 每个箱子中0的占比df[bad%] df.count_1 / df.count_1.sum() # 每个箱子中1的占比df[woe] np.log(df[good%] / df[bad%]) # 计算每个箱子的woe值return dfdef get_iv(df): # 通过df计算IV值rate df[good%] - df[bad%] # 计算每个箱子中好人和坏人的占比差iv np.sum(rate * df.woe) # 计算IV值return ivIV []axisx []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:]# 0返回卡方值1返回p值pv scipy.stats.chi2_contingency([x1, x2])[1]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 pd.DataFrame(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()return bins_df
for i in train.columns[1:-1]:print(i)graphforbestbin(train, i, SeriousDlqin2yrs, n2, q20, graphTrue)
运行一下看看结果 可以发现有的可以自动分箱有的无法自动分箱。无法自动分箱的原因是该特征本身就是分类特征不是连续特征因此系统无法绘制出分箱图像。对于无法自动分箱的特征我们用负无穷和正无穷替换原有的最小值和最大值这是为了可以覆盖所有情况
# 可以自动分箱的变量
auto_col_bins {RevolvingUtilizationOfUnsecuredLines: 6,age: 5,DebtRatio: 4,MonthlyIncome: 3,NumberOfOpenCreditLinesAndLoans: 5}# 不能自动分箱的变量
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], np.inf] for k, v in hand_bins.items()}