Python之信用評分卡模型實現

信用評分技術是一種應用統計模型,其作用是對貸款申請人(信用卡申請人)做風險評估分值的方法。信用評分卡模型是一種成熟的預測方法,尤其在信用風險評估以及金融風險控制領域更是得到了比較廣泛的使用。信用評分卡可以根據客戶提供的資料、客戶的歷史數據、第三方平台(芝麻分、京東、微信等)的數據,對客戶的信用進行評估。信用評分卡的建立是以對大量數據的統計分析結果為基礎,具有較高的準確性和可靠性。

客戶申請評分卡是一種統計模型,它可基於對當前申請人的各項資料進行評估並給出一個分數,該評分能定量對申請人的償債能力作出預判。客戶申請評分卡由一系列特徵項組成,如,年齡、銀行流水、收入等,逾期次數。如果申請人的信用評分大於等於金融放款機構所設定的界限分數,此申請處於可接受的風險水平並將被批准;低於界限分數的申請人將被拒絕或給予標示以便進一步審查。每一個特徵項都有值域範圍,在開發評分卡系統模型中,這些特徵的值域範圍與申請人未來信用表現之間存在一定關係,然後給特徵的值域分配適當的分數權重,分配的分數權重要反映這種相互關係。分數權重越大,說明該屬性表示的信用表現越好。一個申請人的得分是其特徵的值對應的權重得分的簡單求和。

信用風險評級模型的主要開發流程如下:

1.數據獲取,包括獲取存量客戶及潛在客戶的數據。存量客戶是指已開展相關融資類業務的客戶,包括個人客戶和機構客戶;潛在客戶是指未來擬開展相關融資類業務的客戶。

2.數據預處理,主要工作包括數據清洗、缺失值處理、異常值處理,主要是為了將獲取的原始數據轉化為可用作模型開發的格式化數據。

3.探索性數據分析,該步驟主要是獲取樣本總體的大概情況,描述樣本總體情況的指標主要有直方圖、箱形圖等。

4.變數選擇,該步驟主要是通過統計學的方法,篩選出對違約狀態影響最顯著的指標。主要有單變數特徵選擇方法和基於機器學習模型的方法。

5.模型開發,該步驟主要包括變數分段、變數的WOE(證據權重)變換和邏輯回歸估算三部分。

6.模型評估,該步驟主要是評估模型的區分能力、預測能力、穩定性,並形成模型評估報告,得出模型是否可以使用的結論。

7.信用評分,根據邏輯回歸的係數和WOE等確定信用評分的方法。將Logistic模型轉換為標準評分的形式。

8.建立評分系統,根據信用評分方法,建立自動信用評分系統。

本文通過對Kaggle上的

Give Me Some Credit?

www.kaggle.com

項目共計15w條數據的挖掘分析,結合信用評分卡的建立原理,完成了數據處理、特徵變數選擇、變數WOE編碼離散化、logistic回歸模型開發評估、信用評分卡和自動評分系統創建等,為金融放款機構等的風險水平控制給予了參考。

1.數據來源:

在Kaggle上下載到對應的數據集,導入文件路徑

讀取數據:第1列為序數列,無實際意義,刪除

data=pd.read_csv("cs-training.csv")
data=data.iloc[:,1:]#去掉第一列無用序號
data.info() #查看數據集信息

我們發現MonthlyIncome、NumberOfDependents列存在缺失值

缺失值的處理:

1.直接刪除含有缺失值的樣本;

2.根據樣本之間的相似性填補缺失值;

3.根據變數之間的相關關係填補缺失值。

這裡我們利用相關關係填補:

許鐵-巡洋艦科技:說說隨機森林?

zhuanlan.zhihu.com圖標

# 用隨機森林對缺失值預測填充函數
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import make_regression

def set_missing(df):
# 把已有的數值型特徵取出來,MonthlyIncome位於第5列,NumberOfDependents位於第10列
process_df = df.ix[:,[5,0,1,2,3,4,6,7,8,9]]
# 分成已知該特徵和未知該特徵兩部分
known = process_df[process_df.MonthlyIncome.notnull()].as_matrix()
unknown = process_df[process_df.MonthlyIncome.isnull()].as_matrix()
X = known[:, 1:] # X為特徵屬性值
y = known[:, 0] # y為結果標籤值
rfr = RandomForestRegressor(random_state=0, n_estimators=200,max_depth=3,n_jobs=-1)
rfr.fit(X,y)
# 用得到的模型進行未知特徵值預測月收入
predicted = rfr.predict(unknown[:, 1:]).round(0)
print(predicted)
# 用得到的預測結果填補原缺失數據
df.loc[df.MonthlyIncome.isnull(), MonthlyIncome] = predicted
return df
data=set_missing(data)#用隨機森林填補比較多的缺失值
data=data.dropna()#刪除比較少的缺失值 :NumberOfDependents
data = data.drop_duplicates()#刪除重複項
data.info()

數據已經填充完畢!

製作箱線圖觀察數據中是否存在異常值:

#NumberOfTime30-59DaysPastDueNotWorse,NumberOfTimes90DaysLate貸款數量 ,NumberOfTime60-89DaysPastDueNotWorse
train_box=data.iloc[:,[3,7,9]]
train_box.boxplot(figsize=(10,4))

觀察到2個異常值點,刪除,此外年齡應該大於0:

data=data[data[age]>0]
data=data[data[NumberOfTime30-59DaysPastDueNotWorse]<90]
data[SeriousDlqin2yrs]=1-data[SeriousDlqin2yrs] # 使好客戶為1 , 壞客戶為0,方便計數

探索性分析,觀察數據分布:

age=data[age]
sns.distplot(age)

mi=data[data[MonthlyIncome]<50000][MonthlyIncome]
sns.distplot(mi)

年齡和收入分布皆近似正態分布!

#數據切割,將數據分為訓練集和測試集:3-7

from sklearn.cross_validation import train_test_split
Y=data[SeriousDlqin2yrs]
X=data.ix[:,1:]
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, random_state=0)
trainDf = pd.concat([Y_train, X_train], axis=1)
testDf = pd.concat([Y_test, X_test], axis=1)
clasTest = testDf.groupby(SeriousDlqin2yrs)[SeriousDlqin2yrs].count()

變數分箱(binning)是對連續變數離散化(discretization)的一種稱呼。信用評分卡開發中一般有常用的等距分段、等深分段、最優分段。其中等距分段(Equval length intervals)是指分段的區間是一致的,比如年齡以十年作為一個分段;等深分段(Equal frequency intervals)是先確定分段數量,然後令每個分段中數據數量大致相等;最優分段(Optimal Binning)又叫監督離散化(supervised discretizaion),使用遞歸劃分(Recursive Partitioning)將連續變數分為分段,背後是一種基於條件推斷查找較佳分組的演算法。
#調用庫包:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
%matplotlib inline

def mono_bin(Y, X, n ):
r = 0 #設定斯皮爾曼初始值
good=Y.sum() #好客戶的人數
bad=Y.count()-good #壞客戶的人數
#分箱的核心是用機器來選最優的分箱節點
while np.abs(r) < 1: #while ,不滿足條件時,跳出循環
d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.qcut(X, 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 - 1
d3 = pd.DataFrame(d2.X.min(), columns = [min])
d3[min]=d2.min().X # min 就是分箱的節點
d3[max] = d2.max().X
d3[sum] = d2.sum().Y
d3[total] = d2.count().Y
d3[rate] = d2.mean().Y
d3[woe]=np.log((d3[rate]/(1-d3[rate]))/(good/bad))
d3[goodattribute]=d3[sum]/good
d3[badattribute]=(d3[total]-d3[sum])/bad
iv=((d3[goodattribute]-d3[badattribute])*d3[woe]).sum() #返回 iv
d4 = (d3.sort_index(by = min)).reset_index(drop=True) # 返回 d
woe=list(d4[woe].round(3)) #返回 woe
cut=[] # cut 存放箱段節點
cut.append(float(-inf)) # 在列表前加 -inf
for i in range(1,n+1): # n 是前面分箱的分割數 ,所以分成n+1份
qua=X.quantile(i/(n+1)) #quantile 分為數 得到分箱的節點
cut.append(round(qua,4)) # 保留4位小數,返回cut
cut.append(float(inf)) # 在列表後加inf
return d4,iv,cut,wo

Python 相關分析 - zhide2011bgs3的博客 - CSDN博客?

blog.csdn.net圖標

dfx1,ivx1,cutx1,woex1=mono_bin(trainDf.SeriousDlqin2yrs,trainDf.RevolvingUtilizationOfUnsecuredLines,n=10) #貸款以及信用卡可用額度與總額度比例
dfx2, ivx2,cutx2,woex2=mono_bin(trainDf.SeriousDlqin2yrs,trainDf.age,n=10) # 年齡
dfx4, ivx4,cutx4,woex4=mono_bin(trainDf.SeriousDlqin2yrs,trainDf.DebtRatio,n=20) #負債比率
dfx5, ivx5,cutx5,woex5=mono_bin(trainDf.SeriousDlqin2yrs,trainDf.MonthlyIncome,n=10) #月收入對

對於上述分箱方法不能合理拆分的特徵,採用無監督分箱的手動分箱:

pinf = float(inf)#正無窮大
ninf = float(-inf)#負無窮大
cutx3 = [ninf, 0, 1, 3, 5, pinf]
cutx6 = [ninf, 1, 2, 3, 5, 7, 9, pinf] #加了 7,9
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]

def self_bin(Y,X,cat):#自定義人工分箱函數
good=Y.sum() #好用戶數量
bad=Y.count()-good # 壞用戶數量
d1=pd.DataFrame({X:X,Y:Y,Bucket:pd.cut(X,cat)}) #建立個數據框 X-- 各個特徵變數 , Y--用戶好壞標籤 , Bucket--各個分箱
d2=d1.groupby(Bucket, as_index = True) # 按分箱分組聚合 ,並且設為 Index
d3 = pd.DataFrame(d2.X.min(), columns=[min]) # 添加 min 列 ,不用管裡面的 d2.X.min()
d3[min] = d2.min().X #求每個箱段內 X 比如家庭人數的最小值
d3[max] = d2.max().X #求每個箱段內 X 比如家庭人數的最大值
d3[sum] = d2.sum().Y #求每個箱段內 Y 好客戶的個數
d3[total] = d2.count().Y #求每個箱段內 總共客戶數
d3[rate] = d2.mean().Y # 好客戶率
#WOE的全稱是「Weight of Evidence」,即證據權重。WOE是對原始自變數的一種編碼形式。是為了 計算 iv 準備的
#要對一個變數進行WOE編碼,需要首先把這個變數進行分組處理(也叫離散化、分箱等等,說的都是一個意思)
d3[woe] = np.log((d3[rate] / (1 - d3[rate])) / (good / bad))
d3[goodattribute] = d3[sum] / good # 每個箱段內好用戶佔總好用戶數的比率
d3[badattribute] = (d3[total] - d3[sum]) / bad # 每個箱段內壞用戶佔總壞用戶數的比率
#IV的全稱是Information Value,中文意思是信息價值,或者信息量。通俗的說就是變數的預測能力
iv = ((d3[goodattribute] - d3[badattribute]) * d3[woe]).sum()
d4 = (d3.sort_index(by=min)) #數據框的按min升序排列
woe = list(d4[woe].round(3))
return d4, iv,woe
#對他們就行分箱處理:

dfx3,ivx3,woex3 = self_bin(trainDf.SeriousDlqin2yrs,trainDf[NumberOfTime30-59DaysPastDueNotWorse], cutx3)
dfx6,ivx6 ,woex6= self_bin(trainDf.SeriousDlqin2yrs, trainDf[NumberOfOpenCreditLinesAndLoans], cutx6)
dfx7,ivx7,woex7 = self_bin(trainDf.SeriousDlqin2yrs, trainDf[NumberOfTimes90DaysLate], cutx7)
dfx8, ivx8,woex8 = self_bin(trainDf.SeriousDlqin2yrs, trainDf[NumberRealEstateLoansOrLines], cutx8)
dfx9, ivx9,woex9 = self_bin(trainDf.SeriousDlqin2yrs, trainDf[NumberOfTime60-89DaysPastDueNotWorse], cutx9)
dfx10,ivx10,woex10 = self_bin(trainDf.SeriousDlqin2yrs, trainDf[NumberOfDependents], cutx10)

自變數較多,判斷有無多重共線性問題:

#相關性分析:
import seaborn as sns
corr = trainDf.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.savefig(矩陣熱力圖.png,dpi=200)
plt.show()

變數之間的相關性非常低!

如果存在,如何消除?

增大樣本量;嶺回歸(Ridge Regression);逐步回歸法;主成分回歸;人工去除。

#IV值篩選:通過IV值判斷變數預測能力的標準是:
#小於 0.02: unpredictive;0.02 to 0.1: weak;0.1 to 0.3: medium; 0.3 to 0.5: strong
ivlist=[ivx1,ivx2,ivx3,ivx4,ivx5,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值明顯較低,所以予以刪除。

#建立模型之前,我們需要將篩選後的變數轉換為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

trainDf=trans_woe(trainDf,x1_name,woex1,cutx1)
trainDf=trans_woe(trainDf,x2_name,woex2,cutx2)
trainDf=trans_woe(trainDf,x3_name,woex3,cutx3)
trainDf=trans_woe(trainDf,x7_name,woex7,cutx7)
trainDf=trans_woe(trainDf,x9_name,woex9,cutx9)

Y=trainDf[SeriousDlqin2yrs] #因變數
#自變數,剔除對因變數影響不明顯的變數
X=trainDf.drop([SeriousDlqin2yrs,DebtRatio,MonthlyIncome, NumberOfOpenCreditLinesAndLoans,NumberRealEstateLoansOrLines,NumberOfDependents],axis=1)
X=trainDf.iloc[:,-5:]
X.head()

#回歸
import statsmodels.api as sm
X1=sm.add_constant(X)
logit=sm.Logit(Y,X1)
result=logit.fit()
print(result.summary())

#模型評估:我們需要驗證一下模型的預測能力如何。我們使用在建模開始階段預留的test數據進行檢驗。
#通過ROC曲線和AUC來評估模型的擬合能力。
#在Python中,可以利用sklearn.metrics,它能方便比較兩個分類器,自動計算ROC和AUC
testDf=trans_woe(testDf,x1_name,woex1,cutx1)
testDf=trans_woe(testDf,x2_name,woex2,cutx2)
testDf=trans_woe(testDf,x3_name,woex3,cutx3)
testDf=trans_woe(testDf,x7_name,woex7,cutx7)
testDf=trans_woe(testDf,x9_name,woex9,cutx9)
#構建測試集的特徵和標籤
test_X=testDf.iloc[:,-5:] #測試數據特徵
test_Y=testDf.iloc[:,0] #測試數據標籤

#評估
from sklearn import metrics
X3=sm.add_constant(test_X)
resu = result.predict(X3) #進行預測
fpr,tpr,threshold=metrics.roc_curve(test_Y,resu) #評估演算法
rocauc=metrics.auc(fpr,tpr) #計算AUC

plt.figure(figsize=(8,5)) #只能在這裡面設置
plt.plot(fpr,tpr,b,label=AUC=%0.2f% rocauc)
plt.legend(loc=lower right,fontsize=14)
plt.plot([0, 1], [0, 1], r--)
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel(TPR-真正率,fontsize=16)
plt.xlabel(FPR-假正率,fontsize=16)
plt.show()

AUC值為0.85,說明該模型的預測效果還是不錯的,正確率較高

#設定幾個評分卡參數:基礎分值、PDO(比率翻倍的分值)和好壞比。
#這裡, 我們取600分為基礎分值b,取20為PDO (每高20分好壞比翻一倍),好壞比O取20。
p=20/np.log(2)#比例因子
q=600-20*np.log(20)/np.log(2)#等於offset,偏移量
x_coe=[2.6084,0.6327,0.5151,0.5520,0.5747,0.4074]#回歸係數
baseScore=round(q+p*x_coe[0],0)
#個人總評分=基礎分+各部分得分
def get_score(coe,woe,factor):
scores=[]
for w in woe:
score=round(coe*w*factor,0)
scores.append(score)
return scores
#每一項得分
x1_score=get_score(x_coe[1],woex1,p)
x2_score=get_score(x_coe[2],woex2,p)
x3_score=get_score(x_coe[3],woex3,p)
x7_score=get_score(x_coe[4],woex7,p)
x9_score=get_score(x_coe[5],woex9,p)

def compute_score(series,cut,score):
list = []
i = 0
while i < len(series):
value = series[i]
j = len(cut) - 2
m = len(cut) - 2
while j >= 0:
if value >= cut[j]:
j = -1
else:
j -= 1
m -= 1
list.append(score[m])
i += 1
return list
test1 = pd.read_csv("cs-training.csv")
test1[BaseScore]=np.zeros(len(test1))+baseScore
test1[x1] =compute_score(test1[RevolvingUtilizationOfUnsecuredLines], cutx1, x1_score)
test1[x2] = compute_score(test1[age], cutx2, x2_score)
test1[x3] = compute_score(test1[NumberOfTime30-59DaysPastDueNotWorse], cutx3, x3_score)
test1[x7] = compute_score(test1[NumberOfTimes90DaysLate], cutx7, x7_score)
test1[x9] = compute_score(test1[NumberOfTime60-89DaysPastDueNotWorse],cutx9,x9_score)
test1[Score] = test1[x1] + test1[x2] + test1[x3] + test1[x7] +test1[x9] + baseScore

scoretable1=test1.iloc[:,[1,-7,-6,-5,-4,-3,-2,-1]] #選取需要的列,就是評分列
scoretable1.head()
scoretable1.to_csv(ScoreData簡化版.csv)
colNameDict={x1: RevolvingUtilizationOfUnsecuredLines ,x2:age,x3:NumberOfTime30-59DaysPastDueNotWorse,
x7:NumberOfTimes90DaysLate, x9:NumberOfTime60-89DaysPastDueNotWorse}
scoretable2=scoretable1.rename(columns=colNameDict,inplace=False)
scoretable2.to_csv(ScoreData.csv)


推薦閱讀:

TAG:互聯網金融 | 數據分析 | Python |