【风控】评分卡模型的代码实践 超详细可运行

写了好久,整理了好久,可运行,超详细的评分卡模型实践 。
跟着做一遍一定大有收获!
文章目录3、特征选择——分箱 4、建立模型 5、评分卡转换6、测试总结:
数据集:上的Give Me Some 的数据
1、了解变量
2、数据集分析与预处理 查看缺失值
df = pd.read_csv("./GiveMeSomeCredit/cs-training.csv").drop("Unnamed: 0", axis=1)df.info()
可知,月收入 与 家属数量 存在 缺失值 。
# 详细查看每个变量的情况与缺失率df.describe().T.assign(missing_rate = df.apply(lambda x: (len(x)-x.count())/float(len(x))))
缺失值处理
原则:缺失值较少可以直接删除(也可以进行一些其他的填补操作 。此例中删除),若较多则根据变量之间存在的关系来填补缺失值(此处采用随机森林的方法) 。
此处源码中有一个小报错,我这边有做修改:
from sklearn.ensemble import RandomForestRegressor# 用随机森林对'月收入'缺失值预测填充函数def set_missing(df):# 把已有的数值型特征取出来process_df = df.iloc[:, [5,0,1,2,3,4,6,7,8,9]]# 分成已知该特征和未知该特征两部分known = process_df[process_df.MonthlyIncome.notnull()]unknown = process_df[process_df.MonthlyIncome.isnull()]# X为特征属性值X = known.iloc[:, 1:]# y为结果标签值y = known.iloc[:, 0]# fit到RandomForestRegressor之中rfr = RandomForestRegressor(random_state=0, n_estimators=200, max_depth=3, n_jobs=-1)rfr.fit(X, y)# 用得到的模型进行未知特征值预测predicted = rfr.predict(unknown.iloc[:, 1:]).round(0)print("预测值: ", predicted)# 用得到的预测结果填补原缺失数据df.loc[(df.MonthlyIncome.isnull()), 'MonthlyIncome'] = predictedreturn dfdf = set_missing(df)# 删除所剩空值df = df.dropna()# 删除重复值df = df.drop_duplicates()
查看并处理异常值
异常值:偏离大多数抽样数据的数值,通常指测定值中与平均值的偏差超过两倍标准差的测定值 。
通常采用离群值检测的方法对异常值进行检测
此处也在源代码的基础上增加了不同的展示(直方图+箱线图):
# 1.RevolvingUtilizationOfUnsecuredLinesf,[ax1,ax2]=plt.subplots(1,2,figsize=(12,5))sns.distplot(df['RevolvingUtilizationOfUnsecuredLines'],ax=ax1)sns.boxplot(y='RevolvingUtilizationOfUnsecuredLines',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['RevolvingUtilizationOfUnsecuredLines'].describe())
可知,数据分布及其不正常,中位数和四分之三位数都小于1,但是最大值确达到了50708,可用额度比值应该小于1,所以后面将大于1的值当做异常值剔除 。
len(df[df['RevolvingUtilizationOfUnsecuredLines']>1]) # 3260条df = df[df['RevolvingUtilizationOfUnsecuredLines']<=1]
# 2、年龄分布f,[ax1,ax2]=plt.subplots(1,2,figsize=(12,5))sns.distplot(df['age'],ax=ax1)sns.boxplot(y='age',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['age'].describe())# 存在小于0的情况,明显异常可去除 。大于100的较多且连续,可保留print('count>100:', len(df[df.age>100]))df = df[df.age>0]
# 3、逾期30-59天 | 60-89天 | 90天笔数分布:f,[[ax1,ax2],[ax3,ax4],[ax5,ax6]] = plt.subplots(3,2,figsize=(24,10))sns.distplot(df['NumberOfTime30-59DaysPastDueNotWorse'],ax=ax1)sns.boxplot(y='NumberOfTime30-59DaysPastDueNotWorse',data=http://www.kingceram.com/post/df,ax=ax2)sns.distplot(df['NumberOfTime60-89DaysPastDueNotWorse'],ax=ax3)sns.boxplot(y='NumberOfTime60-89DaysPastDueNotWorse',data=df,ax=ax4)sns.distplot(df['NumberOfTimes90DaysLate'],ax=ax5)sns.boxplot(y='NumberOfTimes90DaysLate',data=df,ax=ax6)plt.show()
不够清晰,换一种:
df.boxplot(column=["NumberOfTime30-59DaysPastDueNotWorse", "NumberOfTime60-89DaysPastDueNotWorse", "NumberOfTimes90DaysLate"], rot=30)
上面的箱线图可以看出 -e,-e,ate三个特征都存在两个异常值,下面使用 () 方法查看具体的异常值:
print("NumberOfTime30-59DaysPastDueNotWorse:", df["NumberOfTime30-59DaysPastDueNotWorse"].unique())print("NumberOfTime60-89DaysPastDueNotWorse:", df["NumberOfTime60-89DaysPastDueNotWorse"].unique())print("NumberOfTimes90DaysLate:", df["NumberOfTimes90DaysLate"].unique())
此处可以直接删除,也可以用中位数等来代替,可以自行考虑并选择 。
若想删除:
df = df[df["NumberOfTime30-59DaysPastDueNotWorse"]<95]df = df[df["NumberOfTime60-89DaysPastDueNotWorse"]<95]df = df[df["NumberOfTimes90DaysLate"]<95]
此处用中位数代替:
# 用中位数替代异常值def replaceOutlier(data):New = []med = data.median()for val in data:if ((val == 98) | (val == 96)):New.append(med)else:New.append(val)return Newdf["NumberOfTime30-59DaysPastDueNotWorse"] = replaceOutlier(df["NumberOfTime30-59DaysPastDueNotWorse"])df["NumberOfTime60-89DaysPastDueNotWorse"] = replaceOutlier(df["NumberOfTime60-89DaysPastDueNotWorse"])df["NumberOfTimes90DaysLate"] = replaceOutlier(df["NumberOfTimes90DaysLate"])# 替换后的箱线图df.boxplot(column=["NumberOfTime30-59DaysPastDueNotWorse", "NumberOfTime60-89DaysPastDueNotWorse", "NumberOfTimes90DaysLate"],rot=30)plt.show()
正常多了,下一个:
#4、DebtRatio负债率特征分布f,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))sns.distplot(df['DebtRatio'],ax=ax1)sns.boxplot(y='DebtRatio',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['DebtRatio'].describe())
此处可自行选择方法,这里采用了中位数绝对偏差检测出最小的异常值去取代 其余异常值 。
若你认为那些点不算异常值 , 也可以跳过这一步,不做处理 。
# 使用中位数绝对偏差 MAD(median absolute deviation)方法进行异常值的检测from scipy.stats import normdef mad_based_outlier(points, thresh=3.5):if type(points) is list:points = np.asarray(points)if len(points.shape) == 1:points = points[:, None]med = np.median(points, axis=0)abs_dev = np.absolute(points - med)med_abs_dev = np.median(abs_dev)mod_z_score = norm.ppf(0.75) * abs_dev / med_abs_devreturn mod_z_score > thresh# 检测出最小的异常值,用于替换异常值minUpperBound = min([val for (val, out) in zip(df.DebtRatio, mad_based_outlier(df.DebtRatio)) if out == True])newDebtRatio = []for val in df.DebtRatio:if val > minUpperBound:newDebtRatio.append(minUpperBound)else:newDebtRatio.append(val)df.DebtRatio = newDebtRatiof,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))sns.distplot(df['DebtRatio'],ax=ax1)sns.boxplot(y='DebtRatio',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['DebtRatio'].describe())

【风控】评分卡模型的代码实践 超详细可运行

文章插图
# 5、月收入特征分布f,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))sns.kdeplot(df['MonthlyIncome'],ax=ax1)sns.boxplot(y='MonthlyIncome',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['MonthlyIncome'].describe())
# 采用中位数绝对偏差 MAD来处理minUpperBound_MonthlyIncome = min([val for (val, out) in zip(df.MonthlyIncome, mad_based_outlier(df.MonthlyIncome)) if out == True])newMonthlyIncome = []for val in df.MonthlyIncome:if val > minUpperBound_MonthlyIncome:newMonthlyIncome.append(minUpperBound_MonthlyIncome)else:newMonthlyIncome.append(val)df.MonthlyIncome = newMonthlyIncomef,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))sns.kdeplot(df['MonthlyIncome'],ax=ax1)sns.boxplot(y='MonthlyIncome',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['MonthlyIncome'].describe())
# 6、NumberOfOpenCreditLinesAndLoans 信贷数量特征分布f,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))sns.distplot(df['NumberOfOpenCreditLinesAndLoans'],ax=ax1)sns.boxplot(y='NumberOfOpenCreditLinesAndLoans',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['NumberOfOpenCreditLinesAndLoans'].describe())#由于箱型图的上界值挺连续,所以可能不是异常值
# 7、NumberRealEstateLoansOrLines 固定资产贷款数量f,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))sns.distplot(df['NumberRealEstateLoansOrLines'],ax=ax1)sns.boxplot(y='NumberRealEstateLoansOrLines',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['NumberRealEstateLoansOrLines'].describe())#查看箱型图发现最上方有异常值print('----------------------')print(df[df['NumberRealEstateLoansOrLines']>32].count())#固定资产贷款数量大于28的有两个,大于32有一个为54,所以决定把>32的当做异常值剔除df = df[df['NumberRealEstateLoansOrLines']<=32]
# 8、NumberOfDependents 家属数量分布f,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))sns.kdeplot(df['NumberOfDependents'],ax=ax1)sns.boxplot(y='NumberOfDependents',data=http://www.kingceram.com/post/df,ax=ax2)plt.show()print(df['NumberOfDependents'].describe())print('----------------------')print(df[df['NumberOfDependents']>15].count())#由箱型图和描述性统计可以看出,20为异常值,可删除df = df[df['NumberOfDependents']<=15]
缺失值与异常值分析与处理完毕后:我们可以看出,Age、、大致呈正太分布 , 符合统计分析 。
3、特征选择——分箱
md ### 特征共线性1、特征间共线性:两个或多个特征包含了相似的信息,期间存在强烈的相关关系 2、常用判断标准:两个或两个以上的特征间的相关性系数高于0.8 3、共线性的影响: 1)降低运算效率 2)降低一些模型的稳定性 3)弱化一些模型的预测能力4、查看共线性的方式:建立共线性表格或热力图5、处理方式:删除或变换
# 特征共线性#建立共线性表格correlation_table = pd.DataFrame(df.corr())#热力图sns.heatmap(correlation_table)#可以看到各个变量间的相关性都不大,所以无需剔除变量
md### 特征选择变量分箱:将连续变量离散化将多状态的离散变量合并成少状态变量分箱的重要性:1、稳定性:避免特征中无意义的波动对评分带来波动2、健壮性:避免极端值的影响变量分箱的优势:1、可以将缺失值作为一个独立的箱带入模型中2、将所有的变量变换到相似的尺度上变量分箱的劣势:1、计算量大2、分箱之后需要编码变量分箱常用的方法:有监督的:1、Best-KS; 2、ChiMerge(卡方分箱法)无监督的:1、等距; 2、等频; 3、聚类
WOE
WOE的全称是“ of ” , 即证据权重,WOE是对原始自变量的一种编码形式 。
要对一个变量进行WOE编码,需要首先把这个变量进行分组处理(也叫离散化、分箱等等,说的都是一个意思) 。分组后 , 对于第i组,WOE的计算公式如下:
WOE表示的实际上是“当前分箱中坏客户占所有坏客户的比例”和“当前分箱中好客户占所有好客户的比例”的差异 。
【【风控】评分卡模型的代码实践 超详细可运行】变换以后可以看出,WOE也可以理解为当前分箱中坏客户和好客户的比值,和所有样本中这个比值的差异 (也就是我们随机的坏客户和好客户的比例) 。
WOE越大,这种差异越大 , 当前分组里的坏客户的可能性就越大 , WOE越小 , 差异越小 , 这个分组里的样本响应的可能性就越小 。当分箱中坏客户和好客户的比例等于随机坏客户和好客户的比值时,说明这个分箱没有预测能力 , 即WOE=0 。
WOE编码的优势:
可提升模型的预测效果
将自变量规范到同一尺度上
WOE能反映自变量取值的贡献情况
有利于对变量的每个分箱进行评分
转化为连续变量之后,便于分析变量与变量之间的相关性
与独热向量编码相比,可以保证变量的完整性,同时避免稀疏矩阵和维度灾难
VI
IV的全称是 Value,中文意思是信息价值,或者信息量;用来衡量自变量的预测能力;类似的指标还有信息增益、基尼系数等等 。
IV计算公式 , 对于分组i,会有一个对应的IV值,计算公式如下:
#连续性变量--- 定义自动分箱函数---最优分箱def mono_bin(Y, X, n=10):# X为待分箱的变量,Y为target变量,n为分箱数量r = 0#设定斯皮尔曼 初始值badnum=Y.sum()#计算坏样本数goodnum=Y.count()-badnum#计算好样本数#下面这段就是分箱的核心,就是机器来选择指定最优的分箱节点,代替我们自己来设置while np.abs(r) < 1:d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.qcut(X.rank(method="first"), n)})#用pd.qcut实现最优分箱 , Bucket:将X分为n段,n由斯皮尔曼系数决定d2 = d1.groupby('Bucket', as_index = True)# 按照分箱结果进行分组聚合r, p = stats.spearmanr(d2.mean().X, d2.mean().Y)# 以斯皮尔曼系数作为分箱终止条件n = n - 1d3 = pd.DataFrame(d2.X.min(), columns = ['min'])d3['min']=d2.min().X#箱体的左边界d3['max'] = d2.max().X#箱体的右边界d3['bad'] = d2.sum().Y#每个箱体中坏样本的数量d3['total'] = d2.count().Y#每个箱体的总样本数d3['rate'] = d2.mean().Yprint(d3['rate'])print('----------------------')d3['woe']=np.log((d3['bad']/badnum)/((d3['total'] - d3['bad'])/goodnum))# 计算每个箱体的woe值d3['badattr'] = d3['bad']/badnum#每个箱体中坏样本所占坏样本总数的比例d3['goodattr'] = (d3['total'] - d3['bad'])/goodnum# 每个箱体中好样本所占好样本总数的比例iv = ((d3['badattr']-d3['goodattr'])*d3['woe']).sum()# 计算变量的iv值print('分箱结果:')print(d3)print('IV值为:')print(iv)woe=list(d3['woe'].round(3))cut=[]#cut 存放箱段节点cut.append(float('-inf'))# 在列表前加-inffor i in range(1,n+1):# n是前面的分箱的分割数 , 所以分成n+1份qua=X.quantile(i/(n+1))#quantile 分为数得到分箱的节点cut.append(round(qua,4))# 保留4位小数#返回cutcut.append(float('inf'))# 在列表后加infreturn d3,iv,cut,woex1_d,x1_iv,x1_cut,x1_woe = mono_bin(df['SeriousDlqin2yrs'],df.RevolvingUtilizationOfUnsecuredLines)x2_d,x2_iv,x2_cut,x2_woe = mono_bin(df['SeriousDlqin2yrs'],df.age)x4_d,x4_iv,x4_cut,x4_woe = mono_bin(df['SeriousDlqin2yrs'],df.DebtRatio)x5_d,x5_iv,x5_cut,x5_woe = mono_bin(df['SeriousDlqin2yrs'],df.MonthlyIncome)
#离散型变量-手动分箱def self_bin(Y,X,cut):badnum=Y.sum()# 坏用户数量goodnum=Y.count()-badnum#好用户数量d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.cut(X, cut)})#建立个数据框 X-- 各个特征变量 , Y--用户好坏标签 ,Bucket--各个分箱d2 = d1.groupby('Bucket', as_index = True)# 按照分箱结果进行分组聚合d3 = pd.DataFrame(d2.X.min(), columns = ['min'])#添加 min 列 ,不用管里面的 d2.X.min()d3['min']=d2.min().Xd3['max'] = d2.max().Xd3['bad'] = d2.sum().Yd3['total'] = d2.count().Yd3['rate'] = d2.mean().Yd3['woe']=np.log((d3['bad']/badnum)/((d3['total'] - d3['bad'])/goodnum))# 计算每个箱体的woe值d3['badattr'] = d3['bad']/badnum#每个箱体中坏样本所占坏样本总数的比例d3['goodattr'] = (d3['total'] - d3['bad'])/goodnum# 每个箱体中好样本所占好样本总数的比例iv = ((d3['badattr']-d3['goodattr'])*d3['woe']).sum()# 计算变量的iv值woe=list(d3['woe'].round(3))return d3,iv,woeninf = float('-inf')#负无穷大pinf = float('inf')#正无穷大cutx3 = [ninf, 0, 1, 3, 5, pinf]cutx6 = [ninf, 1, 2, 3, 5, pinf]cutx7 = [ninf, 0, 1, 3, 5, pinf]cutx8 = [ninf, 0,1,2, 3, pinf]cutx9 = [ninf, 0, 1, 3, pinf]cutx10 = [ninf, 0, 1, 2, 3, 5, pinf]dfx3,ivx3,woex3 = self_bin(df.SeriousDlqin2yrs,df['NumberOfTime30-59DaysPastDueNotWorse'], cutx3)dfx6,ivx6 ,woex6= self_bin(df.SeriousDlqin2yrs, df['NumberOfOpenCreditLinesAndLoans'], cutx6)dfx7,ivx7,woex7 = self_bin(df.SeriousDlqin2yrs, df['NumberOfTimes90DaysLate'], cutx7)dfx8, ivx8,woex8 = self_bin(df.SeriousDlqin2yrs, df['NumberRealEstateLoansOrLines'], cutx8)dfx9, ivx9,woex9 = self_bin(df.SeriousDlqin2yrs, df['NumberOfTime60-89DaysPastDueNotWorse'], cutx9)dfx10,ivx10,woex10 = self_bin(df.SeriousDlqin2yrs, df['NumberOfDependents'], cutx10)
训练集相关性分析与IV值筛选
# 特征选择---相关系数矩阵corr = df.corr()#计算各变量的相关性系数xticks = ['x0','x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']#x轴标签yticks = list(corr.index)#y轴标签fig = plt.figure(figsize=(10,8))ax1 = fig.add_subplot(1, 1, 1)sns.heatmap(corr, annot=True, cmap='rainbow', ax=ax1, annot_kws={'size': 12, 'weight': 'bold', 'color': 'black'})#绘制相关性系数热力图ax1.set_xticklabels(xticks, rotation=0, fontsize=14)ax1.set_yticklabels(yticks, rotation=0, fontsize=14)plt.show()
可知,我红框圈起来的几个特征对于我们所要预测的值(因变量)有较强的相关性 。
接下来,进一步检查模型的VI(证据权重)作为变量筛选的依据 。
# IV值筛选#通过IV值判断变量预测能力的标准是:小于 0.02: unpredictive;0.02 to 0.1: weak;0.1 to 0.3: medium; 0.3 to 0.5: strongivlist=[x1_iv,x2_iv,ivx3,x4_iv,x5_iv,ivx6,ivx7,ivx8,ivx9,ivx10]#各变量IVindex=['x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']#x轴的标签fig1 = plt.figure(1,figsize=(8,5))ax1 = fig1.add_subplot(1, 1, 1)x = np.arange(len(index))+1ax1.bar(x,ivlist,width=.4) #ax1.bar(range(len(index)),ivlist, width=0.4)#生成柱状图#ax1.bar(x,ivlist,width=.04)ax1.set_xticks(x)ax1.set_xticklabels(index, rotation=0, fontsize=15)ax1.set_ylabel('IV', fontsize=16)#IV(Information Value),#在柱状图上添加数字标签for a, b in zip(x, ivlist):plt.text(a, b + 0.01, '%.4f' % b, ha='center', va='bottom', fontsize=12)plt.show()'''可以看出 , DebtRatio (x4)、MonthlyIncome(x5)、NumberOfOpenCreditLinesAndLoans(x6)、NumberRealEstateLoansOrLines(x8)和NumberOfDependents(x10)变量的IV值明显较低,所以予以删除 。故选择特征:RevolvingUtilizationOfUnsecuredLines(x1)、age(x2)、NumberOfTime30-59DaysPastDueNotWorse(x3)、NumberOfTimes90DaysLate(x7)、NumberOfTime60-89DaysPastDueNotWorse(x9)作为后续评分模型建立的对象 。'''
故选择特征:(x1)、age(x2)、-e(x3)、ate(x7)、-e(x9)作为后续评分模型建立的对象 。
【风控】评分卡模型的代码实践 超详细可运行

文章插图
4、建立模型 WOE转换
WOE转换:
证据权重( of ,WOE)转换可以将回归模型转变为标准评分卡格式
# 替换成woe函数def trans_woe(var,var_name,woe,cut):woe_name=var_name+'_woe'for i in range(len(woe)):# len(woe) 得到woe里 有多少个数值if i==0:var.loc[(var[var_name]<=cut[i+1]),woe_name]=woe[i]#将woe的值按 cut分箱的下节点,顺序赋值给var的woe_name 列,分箱的第一段elif (i>0) and(i<=len(woe)-2):var.loc[((var[var_name]>cut[i])&(var[var_name]<=cut[i+1])),woe_name]=woe[i] #中间的分箱区间, , 数手指头就很清楚了else:var.loc[(var[var_name]>cut[len(woe)-1]),woe_name]=woe[len(woe)-1]# 大于最后一个分箱区间的 上限值,最后一个值是正无穷return varx1_name='RevolvingUtilizationOfUnsecuredLines'x2_name='age'x3_name='NumberOfTime30-59DaysPastDueNotWorse'x7_name='NumberOfTimes90DaysLate'x9_name='NumberOfTime60-89DaysPastDueNotWorse'df=trans_woe(df,x1_name,x1_woe,x1_cut)df=trans_woe(df,x2_name,x2_woe,x2_cut)df=trans_woe(df,x3_name,woex3,cutx3)df=trans_woe(df,x7_name,woex7,cutx7)df=trans_woe(df,x9_name,woex9,cutx9)df
feature_cols = ['RevolvingUtilizationOfUnsecuredLines','age','NumberOfTime30-59DaysPastDueNotWorse', 'NumberOfTimes90DaysLate', 'NumberOfTime60-89DaysPastDueNotWorse']# 实际训练的特征(转换为woe后的)feature_woe_cols = [c for c in list(df.columns) if 'woe' in c]
# 查看选中特征的分箱情况(后面转换分数会用到)x1_d['features'] = x1_namex2_d['features'] = x2_namedfx3['features'] = x3_namedfx7['features'] = x7_namedfx9['features'] = x9_namedf_bin_to_woe = pd.concat((x1_d.loc[:,['woe','features']], x2_d.loc[:,['woe','features']], dfx3.loc[:,['woe','features']], dfx7.loc[:,['woe','features']], dfx9.loc[:,['woe','features']]))df_bin_to_woe = df_bin_to_woe.reset_index()
模型训练与评估
from sklearn.model_selection import train_test_splitY = df["SeriousDlqin2yrs"]X = df.iloc[:, 1:]# 测试和训练数据进行3:7的比例进行切分 random_state定一个值是的每次运行的时候不会被随机分X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, random_state=123)train = pd.concat([Y_train, X_train], axis=1)test = pd.concat([Y_test, X_test], axis=1)from sklearn.linear_model import LogisticRegressionlrMod = LogisticRegression(penalty='l1', dual=False, tol=0.0001, C=1.0, fit_intercept=True,intercept_scaling=1, class_weight=None, random_state=None, solver='liblinear', max_iter=100,multi_class='ovr', verbose=2)model = lrMod.fit(X_train[feature_woe_cols], Y_train)# 查看在测试集上的性能model.score(X_test[feature_woe_cols], Y_test)
# 模型的AUC 。业内的经验是,0.80以上就算是可以投入产品线使用的模型 。#评估from sklearn import metricsprobs = model.predict_proba(X_test[feature_woe_cols])preds = probs[:,1]fpr,tpr,threshold = metrics.roc_curve(Y_test,preds)#评估算法roc_auc = metrics.auc(fpr,tpr)#计算AUCplt.figure(figsize=(8,5))#只能在这里面设置plt.title('Receiver Operating Characteristic(ROC)')plt.plot(fpr,tpr,'b',label='AUC=%0.2f'% roc_auc)plt.legend(loc='lower right',fontsize=14)plt.plot([0.0, 1.0], [0.0, 1.0], 'r--')plt.xlim=([0.0, 1.0])plt.ylim=([0.0, 1.0])plt.xticks(fontsize=14)plt.yticks(fontsize=14)plt.ylabel('TPR-真正率',fontsize=16)plt.xlabel('FPR-假正率',fontsize=16)plt.show()
5、评分卡转换
评分卡中不直接用客户违约率p,而是用违约概率与正常概率的比值,称为Odds,即
根据逻辑回归原理:

评分卡的背后逻辑是Odds的变动与评分变动的映射(把Odds映射为评分) 。我们可以设计这个一个公式:
其中A与B是常数,B前面取负号的原因,是让违约概率越低,得分越高 。因为实际业务里,分数也高风险越低,风控里还是默认高分高信用低风险 。
基准分:P0。业界某些风控策略基准分都设置为500/600/650 。基准分为 A-B*ln(odds)
PDO(point of ),比率翻番时分数的变动值 。假设我们设置为当odds翻倍时,分值减少30 。
易知:
例子:
代码:
# 生成评分卡import mathB = 50/math.log(2)A = 650 + B* math.log(1/1)def generate_scorecard(model_coef, binning_df, features, B):lst = []cols = ['Variable', 'Binning', 'woe', 'coef', 'Score']coef = model_coef[0]for i in range(len(features)):f = features[i]df = binning_df[binning_df['features']==f]for index, row in df.iterrows():lst.append([f, row['Bucket'], row['woe'],coef[i], int(round(-coef[i]*row['woe']*B))])data = http://www.kingceram.com/post/pd.DataFrame(lst, columns=cols)return datascore_card = generate_scorecard(model.coef_, df_bin_to_woe, feature_cols, B)score_card
6、测试
为了能够显示出最终评分,很多地方的都不是很清晰或者运行有问题,我自写了一个,主要参考来源于函数(但有点傻,若特征很多的话就最好重新优化一下):
score1 = list(score_card[score_card['Variable']==x1_name]['Score'])score2 = list(score_card[score_card['Variable']==x2_name]['Score'])score3 = list(score_card[score_card['Variable']==x3_name]['Score'])score7 = list(score_card[score_card['Variable']==x7_name]['Score'])score9 = list(score_card[score_card['Variable']==x9_name]['Score'])# 替换成score函数def trans_score(var,var_name,woe,cut):woe_name=var_name+'_score'for i in range(len(woe)):# len(woe) 得到woe里 有多少个数值if i==0:var.loc[(var[var_name]<=cut[i+1]),woe_name]=woe[i]#将woe的值按 cut分箱的下节点,顺序赋值给var的woe_name 列,分箱的第一段elif (i>0) and(i<=len(woe)-2):var.loc[((var[var_name]>cut[i])&(var[var_name]<=cut[i+1])),woe_name]=woe[i] #中间的分箱区间,,数手指头就很清楚了else:var.loc[(var[var_name]>cut[len(woe)-1]),woe_name]=woe[len(woe)-1]# 大于最后一个分箱区间的 上限值,最后一个值是正无穷return var# 小测试good_sample = df[df['SeriousDlqin2yrs']==0].sample(5, random_state=1)[feature_cols]bad_sample = df[df['SeriousDlqin2yrs']==1].sample(5, random_state=1)[feature_cols]# 测试集也需要转换一下 , 才能够进行预测呀test = good_sampletest=trans_score(test,x1_name,score1,x1_cut)test=trans_score(test,x2_name,score2,x2_cut)test=trans_score(test,x3_name,score3,cutx3)test=trans_score(test,x7_name,score7,cutx7)test=trans_score(test,x9_name,score9,cutx9)test['score'] = A + test['RevolvingUtilizationOfUnsecuredLines_score'] + test['age_score'] + test['NumberOfTime30-59DaysPastDueNotWorse_score'] + test['NumberOfTimes90DaysLate_score'] +test['NumberOfTime60-89DaysPastDueNotWorse_score']test
正例的评分:
负例的评分:
总结
总体流程就是:
1、数据预处理(缺失值与异常值)
2、通过分箱得WOE、IV,并结合相关性分析选定特征
3、完成WOE的转换、划分数据集,建立模型(训练的是转换为WOE的那些特征) 。
4、转换为评分卡
通过最后的评分,可以看出还是可以再优化的,可以从特征工程、参数的选择方面再优化 。代码方面也可以再简洁一些 。
: