在借贷场景中,评分卡是用分数形式来衡量一个客户信用风险的大小,分数越高代表信用风险越小。
针对个人客户而言,评分卡分为三类,分别是: A卡(Application score card)申请评分卡 B卡(Behavior score card)行为评分卡 C卡(Collection score card)催收评分卡。
而众人常说的“评分卡”其实是指A卡,又称为申请者评级模型,主要应用于相关融资类业务中新用户的主体评级。 一个完整的模型开发,有以下流程:
获取数据 数据清洗 模型开发 模型检验与评估 模型上线 检测与报告接下来,使用Give Me Some Credit数据集,总共15万条训练数据,介绍使用逻辑回归建立A卡的方法。
变量表:
变量名变量解释SeriousDlqin2yrs是否有超过 90 天或更糟的逾期拖欠RevolvingUtilizationOfUnsecuredLines贷款以及信用卡可用额度与总额度比例age借款人的年龄NumberOfTime30-59DaysPastDueNotWorse35-59 天逾期但不糟糕次数DebtRatio负债比率MonthlyIncome月收入NumberOfOpenCreditLinesAndLoans未偿还贷款数量和信贷额度NumberOfTimes90DaysLate借款人逾期 90 天或以上的次数NumberRealEstateLoansOrLines不动产贷款或额度数量NumberOfTime60-89DaysPastDueNotWorse借款人已超过 60-89 天的次数,但在过去两年中没有更糟。NumberOfDependents家庭中的家属人数(配偶,子女等)观察缺失数据可以发现,需要填补的特征是“收入”和“家属人数”。“家属人数”缺失很少,仅缺失了大约2.5%,使用均值来填补。“收入”缺失了几乎20%,并且“收入”对信用评分来说是很重要的因素,因此使用随机森林填补“收入”。
data.isnull().sum() data['NumberOfDependents'].fillna(int(data['NumberOfDependents'].mean()),inplace=True) def fill_missing_rf(x,y,to_fill): df = x.copy() fill = df.loc[:,to_fill] df = pd.concat([df.loc[:,df.columns!=to_fill],pd.DataFrame(y)],axis=1) y_train = fill[fill.notnull()] y_test = fill[fill.isnull()] x_train = df.iloc[y_train.index,:] x_test = df.iloc[y_test.index,:] from sklearn.ensemble import RandomForestRegressor as rfr rfr = rfr(n_estimators=200,random_state=0,n_jobs=-1) rfr = rfr.fit(x_train,y_train) y_predict = rfr.predict(x_test) return y_predict x = data.iloc[:,1:] y = data['SeriousDlqin2yrs'] y_pred = fill_missing_rf(x,y,'MonthlyIncome') data.loc[data.loc[:,'MonthlyIncome'].isnull(),'MonthlyIncome'] = y_pred对于age变量,大于100岁小于等于0岁的可以认为是异常值,由箱线图可知,异常值样本不多,故直接删除
x1=data['age'] fig,axes = plt.subplots() axes.boxplot(x1) axes.set_xticklabels(['age']) data = data[data['age']>0] data = data[data['age']<100]可以看到这三个变量都有明显的异常值
x1=data['NumberOfTime30-59DaysPastDueNotWorse'] x2=data['NumberOfTimes90DaysLate'] x3=data['NumberOfTime60-89DaysPastDueNotWorse'] fig,axes = plt.subplots() axes.boxplot([x1,x2,x3]) axes.set_xticklabels(('x1','x2','x3'))查看这三列的异常值,分别为96和98,把他们删除
data.loc[:,'NumberOfTime30-59DaysPastDueNotWorse'].value_counts() data.loc[:,'NumberOfTimes90DaysLate'].value_counts() data.loc[:,'NumberOfTime60-89DaysPastDueNotWorse'].value_counts() data = data[data.loc[:,'NumberOfTime30-59DaysPastDueNotWorse']<90] data.index=range(data.shape[0])检验各个变量之间的相关性,如果各个变量之间拥有较强的相关性,会明显影响模型的泛化能力。 可以看出各变量间相关性较小,可进行后续建模。
corr = data.corr() cmap = sns.diverging_palette(200,20,sep=20,as_cmap=True) sns.heatmap(corr,annot=True,cmap=cmap,annot_kws={'size':5})很明显,样本不均衡,样本总数:149152,1占:6.62%,0占:93.38%,这里采用上采样的方法来平衡样本。
from imblearn.over_sampling import SMOTE sm = SMOTE(random_state=0) x,y = sm.fit_sample(x,y)画出IV曲线,得到最佳分箱数
for i in model_data.columns[1:-1]: print(i) graphforbestbin(model_data,i,'SeriousDlqin2yrs',n=2,q=20,graph=True)以DebtRatio为例,根据IV值的变化幅度由大变小的点,选择最佳分箱数为4,其他变量以此类推。 根据IV曲线找出的分箱结果如下:
auto_bins = {'RevolvingUtilizationOfUnsecuredLines':5 ,'age':6 ,'DebtRatio':4 ,'MonthlyIncome':3 ,'NumberOfOpenCreditLinesAndLoans':7 }但不是所有特征都能用这个方法,不能自动分箱的变量可以对变量进行观察手动分箱。
# 以下变量手动分箱 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()} # 对所有特征分箱: bins_of_col = {} for col in auto_bins: bins_df = graphforbestbin(model_data,col,'SeriousDlqin2yrs',n = auto_bins[col],q=20,graph=False) bins_list = sorted(set(bins_df['min']).union(bins_df['max'])) #保证区间覆盖使用np.inf替换最大值-np.inf替换最小值 bins_list[0],bins_list[-1] = -np.inf,np.inf bins_of_col[col] = bins_list # 合并手动分箱的结果 bins_of_col.update(hand_bins) bins_of_col画出各特征的IV值
index = ['x1','x2','x3','x4','x5','x6','x7','x8','x9','x10'] index_num = range(len(index)) plt.figure() plt.bar(index,IV.values()) plt.show()x1 ‘RevolvingUtilizationOfUnsecuredLines’: 1.0949541172484247, x2 ‘age’: 0.2503443210341437, x3 ‘DebtRatio’: 0.06638925056198089, x4 ‘MonthlyIncome’: 0.09548586296415988, x5 ‘NumberOfOpenCreditLinesAndLoans’: 0.07991090090702914, x6 ‘NumberOfTime30-59DaysPastDueNotWorse’: 0.7020812947616354, x7 ‘NumberOfTimes90DaysLate’: 0.8396562353357999, x8 ‘NumberRealEstateLoansOrLines’: 0.06350368034698911, x9 ‘NumberOfTime60-89DaysPastDueNotWorse’: 0.5623219236223598, x10 ‘NumberOfDependents’: 0.03414718377941136} 一般认为,IV值小于0.03的特征几乎不带有有效信息,对模型没有贡献,可以删除,这组特征中最低值为’NumberOfDependents’为0.034,所有特征都可以保留。
接下来,将WOE映射到原始数据中,形成建模数据。
model_woe = pd.DataFrame(index=model_data.index) for col in bins_of_col: model_woe[col] = pd.cut(model_data[col],bins_of_col[col]).map(woeall[col]) model_woe["SeriousDlqin2yrs"] = model_data["SeriousDlqin2yrs"] model_woe #这就是建模数据计算测试集vali_woe,利用建立好了的get_woe函数:
woeall_vali = {} for col in bins_of_col: woeall_vali[col] = get_woe(vali_data,col,"SeriousDlqin2yrs",bins_of_col[col]) vali_woe = pd.DataFrame(index=vali_data.index) for col in bins_of_col: vali_woe[col] = pd.cut(vali_data[col],bins_of_col[col]).map(woeall_vali[col]) vali_woe["SeriousDlqin2yrs"] = vali_data["SeriousDlqin2yrs"] vali_x = vali_woe.iloc[:,:-1] vali_y = vali_woe.iloc[:,-1]接下来就可以建模了
from sklearn.linear_model import LogisticRegression as LR x = model_woe.iloc[:,:-1] y = model_woe.iloc[:,-1] lr = LR().fit(x,y) lr.score(vali_x,vali_y)模型得分为0.8652
混淆矩阵
from sklearn.metrics import confusion_matrix C2 = confusion_matrix(y_true,y_pred) sns.heatmap(C2,annot=True,fmt='d')准确率 = TP \ (TP+FP) = 0.85 召回率 = TP \ (TP+FN) = 0.89 绘制ROC曲线,ROC=0.94,曲线越往左上凸,True Positive 就越高,对应的False Positive越低。
import scikitplot as skplt vali_proba_df = pd.DataFrame(lr.predict_proba(vali_x)) skplt.metrics.plot_roc(vali_y,vali_proba_df, plot_micro=False,figsize=(6,6), plot_macro=False)求出A、B和base_score 将所有特征的评分卡内容全部一次性写往一个本地文件ScoreData.csv:
with open(file,"w") as fdata: fdata.write("base_score,{}\n".format(base_score)) for i,col in enumerate(x.columns): score = woeall[col] * (-B*lr.coef_[0][i]) score.name = "Score" score.index.name = col score.to_csv(file,header=True,mode="a")