首页 >> 大全

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

2024-01-03 大全 25 作者:考证青年

写了好久,整理了好久,可运行,超详细的评分卡模型实践。

跟着做一遍一定大有收获!

文章目录 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 df
df = set_missing(df)# 删除所剩空值
df = df.dropna()
# 删除重复值
df = df.drop_duplicates()

查看并处理异常值

异常值:偏离大多数抽样数据的数值,通常指测定值中与平均值的偏差超过两倍标准差的测定值。

通常采用离群值检测的方法对异常值进行检测

此处也在源代码的基础上增加了不同的展示(直方图+箱线图):

# 1.RevolvingUtilizationOfUnsecuredLines
f,[ax1,ax2]=plt.subplots(1,2,figsize=(12,5))
sns.distplot(df['RevolvingUtilizationOfUnsecuredLines'],ax=ax1)
sns.boxplot(y='RevolvingUtilizationOfUnsecuredLines',data=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=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=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=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=df,ax=ax2)
plt.show()
print(df['DebtRatio'].describe())

评分卡模型woe_评分卡模型打分_

# 5、月收入特征分布
f,[ax1,ax2] = plt.subplots(1,2,figsize=(12,5))
sns.kdeplot(df['MonthlyIncome'],ax=ax1)
sns.boxplot(y='MonthlyIncome',data=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=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=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=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=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: strong
ivlist=[x1_iv,x2_iv,ivx3,x4_iv,x5_iv,ivx6,ivx7,ivx8,ivx9,ivx10]#各变量IV
index=['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))+1
ax1.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)作为后续评分模型建立的对象。

评分卡模型打分__评分卡模型woe

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 var
x1_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_name
x2_d['features'] = x2_name
dfx3['features'] = x3_name
dfx7['features'] = x7_name
dfx9['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 LogisticRegression
lrMod = 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 metrics
probs = 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 math
B = 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 = 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_sample
test=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、转换为评分卡

通过最后的评分,可以看出还是可以再优化的,可以从特征工程、参数的选择方面再优化。代码方面也可以再简洁一些。

:

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了