信用評分卡模型總結

本文主要介紹了信用評分卡模型的概念,分析流程,使用場景,WOE,IV值等的詳細介紹,還包括一個案例分析。

本文主要參考:https://www.jianshu.com/p/f931a4df202c

https://blog.csdn.net/goodhuajun/article/details/39582761blog.csdn.net

1、基本概念

img

**WOE值:**WOE的全稱是“Weight of Evidence”,即證據權重。WOE是對原始自變量的一種編碼形式。

將模型目標標量爲1記爲違約用戶,對於目標變量爲0記爲正常用戶;則WOE(weight of Evidence)其實就是自變量取某個值的時候對違約比例的一種影響。

Woe公式如下:

img

eg:

img

表中以age年齡爲某個自變量,由於年齡是連續型自變量,需要對其進行離散化處理,假設離散化分爲5組,#bad和#good表示在這五組中違約用戶和正常用戶的數量分佈,最後一列是woe值的計算,通過後面變化之後的公式可以看出,**woe反映的是在自變量每個分組下違約用戶對正常用戶佔比和總體中違約用戶對正常用戶佔比之間的差異;從而可以直觀的認爲woe蘊含了自變量取值對於目標變量(違約概率)的影響。**再加上woe計算形式與logistic迴歸中目標變量的logistic轉換(logist_p=ln(p/1-p))如此相似,因而可以將自變量woe值替代原先的自變量值.

WOE越大,差異越大,這個分組裏的樣本響應的可能性就越大,WOE越小,差異越小,這個分組裏的樣本響應的可能性就越小。

  • 當前分組中,響應的比例越大,WOE值越大;
  • 當前分組WOE的正負,由當前分組響應和未響應的比例,與樣本整體響應和未響應的比例的大小關係決定,當前分組的比例小於樣本整體比例時,WOE爲負,當前分組的比例大於整體比例時,WOE爲正,當前分組的比例和整體比例相等時,WOE爲0。
  • WOE的取值範圍是全體實數。

**IV值:**IV的全稱是Information Value,中文意思是信息價值,或者信息量。

IV公式如下:

img

IV衡量的是某一個變量的信息量,從公式來看的話,相當於是自變量woe值的一個加權求和,其值的大小決定了自變量對於目標變量的影響程度;從直觀邏輯上大體可以這樣理解“用IV去衡量變量預測能力”這件事情:我們假設在一個分類問題中,目標變量的類別有兩類:Y1,Y2。對於一個待預測的個體A,要判斷A屬於Y1還是Y2,我們是需要一定的信息的,假設這個信息總量是I,而這些所需要的信息,就蘊含在所有的自變量C1,C2,C3,……,Cn中,那麼,對於其中的一個變量Ci來說,其蘊含的信息越多,那麼它對於判斷A屬於Y1還是Y2的貢獻就越大,Ci的信息價值就越大,Ci的IV就越大,它就越應該進入到入模變量列表中。

  • 對於變量的一個分組,這個分組的響應和未響應的比例與樣本整體響應和未響應的比例相差越大,IV值越大,否則,IV值越小;
  • 極端情況下,當前分組的響應和未響應的比例和樣本整體的響應和未響應的比例相等時,IV值爲0;
  • IV值的取值範圍是[0,+∞),且,噹噹前分組中只包含響應客戶或者未響應客戶時,IV = +∞。

事實上,爲了理解WOE的意義,需要考慮對評分模型效果的評價。因爲我們在建模時對模型自變量的所有處理工作,本質上都是爲了提升模型的效果。在這種二分類模型效果的評價方法,尤其是其中的ROC曲線。爲了描述WOE的意義,還真的需要從ROC說起。仍舊是先畫個表格。

img

數據來自於著名的German credit dataset,取了其中一個自變量來說明問題。第一列是自變量的取值,N表示對應每個取值的樣本數,n1和n0分別表示了違約樣本數與正常樣本數,p1和p0分別表示了違約樣本與正常樣本佔各自總體的比例,cump1和cump0分別表示了p1和p0的累計和,woe是對應自變量每個取值的WOE(ln(p1/p0)),iv是woe*(p1-p0)。對iv求和(可以看成是對WOE的加權求和),就得到IV(information value信息值),是衡量自變量對目標變量影響的指標之一(類似於gini,entropy那些),此處是0.666,貌似有點太大了。

就以上面這個表格爲例,其中的cump1和cump0,從某種角度看就是我們做ROC曲線時候的TPR與FPR。例如,此時的評分排序爲A12,A11,A14,A13,若以A14爲cutoff,則此時的TPR=cumsum(p1)[3]/(sum(p1)),FPR=cumsum(p0)[3]/(sum(p0)),就是cump1[3]和cump0[3]。於是我們可以畫出相應的ROC曲線。

img

橫軸FPR:1-TNR,1-Specificity,FPR越大,預測正類中實際負類越多。

縱軸TPR:Sensitivity(正類覆蓋率),TPR越大,預測正類中實際正類越多。

理想目標:TPR=1,FPR=0,即圖中(0,1)點,故ROC曲線越靠攏(0,1)點,越偏離45度對角線越好,Sensitivity、Specificity越大效果越好。

ROC的全名叫做Receiver Operating Characteristic,其主要分析工具是一個畫在二維平面上的曲線——ROC curve。平面的橫座標是false positive rate(FPR),縱座標是true positive rate(TPR)。對某個分類器而言,我們可以根據其在測試樣本上的表現得到一個TPR和FPR點對。這樣,此分類器就可以映射成ROC平面上的一個點。調整這個分類器分類時候使用的閾值,我們就可以得到一個經過(0, 0),(1, 1)的曲線,這就是此分類器的ROC曲線。一般情況下,這個曲線都應該處於(0, 0)和(1, 1)連線的上方。因爲(0, 0)和(1, 1)連線形成的ROC曲線實際上代表的是一個隨機分類器。

AUC(Area under Curve):Roc曲線下的面積,介於0.1和1之間。Auc作爲數值可以直觀的評價分類器的好壞,值越大越好。

首先AUC值是一個概率值,當你隨機挑選一個正樣本以及負樣本,當前的分類算法根據計算得到的Score值將這個正樣本排在負樣本前面的概率就是AUC值,AUC值越大,當前分類算法越有可能將正樣本排在負樣本前面,從而能夠更好地分類。

至此應該已經可以總結到:評價評分模型的效果可以從“條件分佈函數距離”與“條件密度函數距離”這兩個角度出發進行考慮,從而分別得到AUC和IV這兩個指標。這兩個指標當然也可以用來作爲篩選自變量的指標,IV似乎更加常用一些。而WOE就是IV的一個主要成分。

那麼,到底爲什麼要用WOE來對自變量做編碼呢?主要的兩個考慮是:提升模型的預測效果,提高模型的可理解性。

首先,對已經存在的一個評分規則,例如上述的A12,A11,A14,A13,對其做各種函數變化,可以得到不同的ROC結果。但是,如果這種函數變化是單調的,那麼ROC曲線事實上是不發生變化的。因此,想要提高ROC,必須寄希望於對評分規則做非單調的變換。傳說中的NP引理證明了,使得ROC達到最優的變換就是計算現有評分的一個WOE,這似乎叫做“條件似然比”變換。用上述例子,我們根據計算出的WOE值,對評分規則(也就是第一列的value)做排序,得到新的一個評分規則。

img

此處按照WOE做了逆序排列(因爲WOE越大則違約概率越大),照例可以畫出ROC線。

img

可以看出來,經過WOE的變化之後,模型的效果好多了。事實上,WOE也可以用違約概率來代替,兩者沒有本質的區別。用WOE來對自變量做編碼的一大目的就是實現這種“條件似然比”變換,極大化辨識度。

總結起來就是,做信用評分模型時,自變量的處理過程(包括編碼與篩選)很大程度上是基於對單變量模型效果的評價。而在這個評價過程中,ROC與IV是從不同角度考察自變量對目標變量的影響力,基於這種考察,我們用WOE值對分類自變量進行編碼,從而能夠更直觀地理解自變量對目標變量的作用效果及方向,同時提升預測效果。

ROC的全名叫做Receiver Operating Characteristic,其主要分析工具是一個畫在二維平面上的曲線——ROC curve。平面的橫座標是false positive rate(FPR),縱座標是true positive rate(TPR)。對某個分類器而言,我們可以根據其在測試樣本上的表現得到一個TPR和FPR點對。這樣,此分類器就可以映射成ROC平面上的一個點。

2、項目流程

img

3、數據獲取

數據屬於個人消費類貸款,只考慮信用評分最終實施時能夠使用到的數據應從如下一些方面獲取數據: – 基本屬性:包括了借款人當時的年齡。 – 償債能力:包括了借款人的月收入、負債比率。 – 信用往來:兩年內35-59天逾期次數、兩年內60-89天逾期次數、兩年內90 天或高於90天逾期的次數。 – 財產狀況:包括了開放式信貸和貸款數量、不動產貸款或額度數量。 – 貸款屬性:暫無。 – 其他因素:包括了借款人的家屬數量(不包括本人在內)。 – 時間窗口:自變量的觀察窗口爲過去兩年,因變量表現窗口爲未來兩年。

img

4、數據處理

import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import scipy.stats.stats as stats
import matplotlib.pyplot as plt
import statsmodels.api as sm
import math
#列全部顯示
pd.set_option('display.max_rows',1000)
pd.set_option('display.max_columns',1000)
#載入數據
data = pd.read_csv('C:\\Users\\nanafighting\\Desktop\\cs-training.csv')
#數據集確實和分佈情況
data.describe().to_csv('DataDescribe.csv')
#print(data.head())

4.1 缺失值處理

變量MonthlyIncome缺失率比較大,所以我們根據變量之間的相關關係填補缺失值,我們採用隨機森林法:

# 用隨機森林對缺失值預測填充函數
def set_missing(df):
    # 把已有的數值型特徵取出來
    process_df = df.ix[:,[5,0,1,2,3,4,6,7,8,9]]
    # 分成已知該特徵和未知該特徵兩部分
    #as_matrix()將表格轉換爲矩陣
    known = process_df[process_df.MonthlyIncome.notnull()].as_matrix()
    unknown = process_df[process_df.MonthlyIncome.isnull()].as_matrix()
    # known = process_df[process_df.MonthlyIncome.notnull()].values
    # unknown = process_df[process_df.MonthlyIncome.isnull()].values
    # X爲特徵屬性值
    X = known[:, 1:]
    # y爲結果標籤值
    y = known[:, 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[:, 1:]).round(0)
    print(predicted)
    # 用得到的預測結果填補原缺失數據
    df.loc[(df.MonthlyIncome.isnull()), 'MonthlyIncome'] = predicted
    return df

NumberOfDependents變量缺失值比較少,直接刪除,對總體模型不會造成太大影響。對缺失值處理完之後,刪除重複項。

    data=set_missing(data)#用隨機森林填補比較多的缺失值
    data=data.dropna()#刪除比較少的缺失值
    data = data.drop_duplicates()#刪除重複項
    data.to_csv('MissingData.csv',index=False)

4.2 異常值處理

我們發現變量age中存在0,顯然是異常值,直接剔除

# 年齡等於0的異常值進行剔除
    data = data[data['age'] > 0]

數據集中好客戶爲0,違約客戶爲1,考慮到正常的理解,能正常履約並支付利息的客戶爲1,所以我們將其取反。

#剔除異常值
    data = data[data['NumberOfTime30-59DaysPastDueNotWorse'] < 90]
    #變量SeriousDlqin2yrs取反
    data['SeriousDlqin2yrs']=1-data['SeriousDlqin2yrs']

4.3 數據切分

from sklearn.cross_validation import train_test_split
    Y = data['SeriousDlqin2yrs']
    X = data.ix[:, 1:]
    #測試集佔比30%
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, random_state=0)
    # print(Y_train)
    train = pd.concat([Y_train, X_train], axis=1)
    test = pd.concat([Y_test, X_test], axis=1)
    clasTest = test.groupby('SeriousDlqin2yrs')['SeriousDlqin2yrs'].count()
    train.to_csv('TrainData.csv',index=False)
    test.to_csv('TestData.csv',index=False)

5、探索性分析

在建立模型之前,我們一般會對現有的數據進行 探索性數據分析(Exploratory Data Analysis) 。 EDA是指對已有的數據(特別是調查或觀察得來的原始數據)在儘量少的先驗假定下進行探索。常用的探索性數據分析方法有:直方圖、散點圖和箱線圖等。 客戶年齡分佈如圖所示,可以看到年齡變量大致呈正態分佈,符合統計分析的假設。

import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import scipy.stats.stats as stats
import matplotlib.pyplot as plt
    #異常值處理
    #年齡等於0的異常值進行剔除
    data=data[data['age']>0]
    df=DataFrame(data['age'],columns=['age'])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.hist(df, bins=50)
    plt.title('Age distribution')
    plt.xlabel('Age')
    plt.ylabel('density')
    plt.show()

收入

import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import scipy.stats.stats as stats
import matplotlib.pyplot as plt
    #異常值處理
    #年齡等於0的異常值進行剔除
    data=data[data['MonthlyIncome']>0]
    df=DataFrame(data['MonthlyIncome'],columns=['MonthlyIncome'])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.hist(df, bins=50)
    plt.title('MonthlyIncomedistribution')
    plt.xlabel('MonthlyIncome')
    plt.ylabel('density')
    plt.show()

6、變量選擇

我們採用信用評分模型的變量選擇方法,通過WOE分析方法,即是通過比較指標分箱和對應分箱的違約概率來確定指標是否符合經濟意義。首先我們對變量進行離散化(分箱)處理。

6.1 分箱處理

變量分箱(binning)是對連續變量離散化(discretization)的一種稱呼。信用評分卡開發中一般有常用的等距分段、等深分段、最優分段。其中等距分段(Equval length intervals)是指分段的區間是一致的,比如年齡以十年作爲一個分段;等深分段(Equal frequency intervals)是先確定分段數量,然後令每個分段中數據數量大致相等;最優分段(Optimal Binning)又叫監督離散化(supervised discretizaion),使用遞歸劃分(Recursive Partitioning)將連續變量分爲分段,背後是一種基於條件推斷查找較佳分組的算法。 我們首先選擇對連續變量進行最優分段,在連續變量的分佈不滿足最優分段的要求時,再考慮對連續變量進行等距分段。最優分箱的代碼如下:

import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import scipy.stats.stats as stats
import matplotlib.pyplot as plt
import statsmodels.api as sm
import math

# 定義自動分箱函數
def mono_bin(Y, X, n = 20):
    r = 0
    good=Y.sum()
    bad=Y.count()-good
    while np.abs(r) < 1:
        d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.qcut(X, 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
    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()
    d4 = (d3.sort_index(by = 'min'))
    print("=" * 60)
    print(d4)
    cut=[]
    cut.append(float('-inf'))
    for i in range(1,n+1):
        qua=X.quantile(i/(n+1))
        cut.append(round(qua,4))
    cut.append(float('inf'))
    woe=list(d4['woe'].round(3))
    return d4,iv,cut,woe

優化

#自定義分箱函數
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)})
    d2=d1.groupby('Bucket', as_index = True)
    d3 = pd.DataFrame(d2.X.min(), columns=['min'])
    d3['min'] = d2.min().X
    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()
    d4 = (d3.sort_index(by='min'))
    print("=" * 60)
    print(d4)
    woe = list(d4['woe'].round(3))
    return d4, iv,woe

針對不能最優分箱的變量,分箱如下

# 連續變量離散化
    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]

6.2 WOE值

在進行分析時,我們需要對各指標從小到大排列,並計算出相應分檔的WoE值。其中正向指標越大,WoE值越小;反向指標越大,WoE值越大。正向指標的WoE值負斜率越大,反響指標的正斜率越大,則說明指標區分能力好。WoE值趨近於直線,則意味指標判斷能力較弱。若正向指標和WoE正相關趨勢、反向指標同WoE出現負相關趨勢,則說明此指標不符合經濟意義,則應當予以去除。

d3['woe']=np.log((d3['rate']/(1-d3['rate']))/(good/bad))

6.3相關性分析和IV篩選

接下來,我們會用經過清洗後的數據看一下變量間的相關性。注意,這裏的相關性分析只是初步的檢查,進一步檢查模型的VI(證據權重)作爲變量篩選的依據。

相關性圖我們通過Python裏面的seaborn包,調用heatmap()繪圖函數進行繪製,實現代碼如下:

    corr = data.corr()#計算各變量的相關性係數
    xticks = ['x0','x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']#x軸標籤
    yticks = list(corr.index)#y軸標籤
    fig = plt.figure()
    ax1 = fig.add_subplot(1, 1, 1)
    sns.heatmap(corr, annot=True, cmap='rainbow', ax=ax1, annot_kws={'size': 9, 'weight': 'bold', 'color': 'blue'})#繪製相關性係數熱力圖
    ax1.set_xticklabels(xticks, rotation=0, fontsize=10)
    ax1.set_yticklabels(yticks, rotation=0, fontsize=10)
    plt.show()

接下來,我進一步計算每個變量的Infomation Value(IV)。IV指標是一般用來確定自變量的預測能力。 其公式爲: IV=sum((goodattribute-badattribute)*ln(goodattribute/badattribute)) 通過IV值判斷變量預測能力的標準是: < 0.02: unpredictive 0.02 to 0.1: weak 0.1 to 0.3: medium 0.3 to 0.5: strong > 0.5: suspicious IV的實現放在mono_bin()函數裏面,代碼實現如下:

# 定義自動分箱函數
def mono_bin(Y, X, n = 20):
    r = 0
    good=Y.sum()
    bad=Y.count()-good
    while np.abs(r) < 1:
        d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.qcut(X, 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
     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()
     d4 = (d3.sort_index(by = 'min')).reset_index(drop=True)
     print("=" * 60)
     print(d4)
     cut=[]
     cut.append(float('-inf'))
     for i in range(1,n+1):
         qua=X.quantile(i/(n+1))
         cut.append(round(qua,4))
     cut.append(float('inf'))
     woe=list(d4['woe'].round(3))
     return d4,iv,cut,woe

生成的IV圖代碼:

    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)
    ax1 = fig1.add_subplot(1, 1, 1)
    x = np.arange(len(index))+1
    ax1.bar(x, ivlist, width=0.4)#生成柱狀圖
    ax1.set_xticks(x)
    ax1.set_xticklabels(index, rotation=0, fontsize=12)
    ax1.set_ylabel('IV(Information Value)', fontsize=14)
    #在柱狀圖上添加數字標籤
    for a, b in zip(x, ivlist):
        plt.text(a, b + 0.01, '%.4f' % b, ha='center', va='bottom', fontsize=10)
    plt.show()

7、模型分析

證據權重(Weight of Evidence,WOE)轉換可以將Logistic迴歸模型轉變爲標準評分卡格式。引入WOE轉換的目的並不是爲了提高模型質量,只是一些變量不應該被納入模型,這或者是因爲它們不能增加模型值,或者是因爲與其模型相關係數有關的誤差較大,其實建立標準信用評分卡也可以不採用WOE轉換。這種情況下,Logistic迴歸模型需要處理更大數量的自變量。儘管這樣會增加建模程序的複雜性,但最終得到的評分卡都是一樣的。 在建立模型之前,我們需要將篩選後的變量轉換爲WoE值,便於信用評分。

7.1 WOE轉換

#替換成woe函數
def replace_woe(series,cut,woe):
    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(woe[m])
        i += 1
    return list

我們將每個變量都進行替換,並將其保存到WoeData.csv文件中:

# 替換成woe
    data['RevolvingUtilizationOfUnsecuredLines'] = Series(replace_woe(data['RevolvingUtilizationOfUnsecuredLines'], cutx1, woex1))
    data['age'] = Series(replace_woe(data['age'], cutx2, woex2))
    data['NumberOfTime30-59DaysPastDueNotWorse'] = Series(replace_woe(data['NumberOfTime30-59DaysPastDueNotWorse'], cutx3, woex3))
    data['DebtRatio'] = Series(replace_woe(data['DebtRatio'], cutx4, woex4))
    data['MonthlyIncome'] = Series(replace_woe(data['MonthlyIncome'], cutx5, woex5))
    data['NumberOfOpenCreditLinesAndLoans'] = Series(replace_woe(data['NumberOfOpenCreditLinesAndLoans'], cutx6, woex6))
    data['NumberOfTimes90DaysLate'] = Series(replace_woe(data['NumberOfTimes90DaysLate'], cutx7, woex7))
    data['NumberRealEstateLoansOrLines'] = Series(replace_woe(data['NumberRealEstateLoansOrLines'], cutx8, woex8))
    data['NumberOfTime60-89DaysPastDueNotWorse'] = Series(replace_woe(data['NumberOfTime60-89DaysPastDueNotWorse'], cutx9, woex9))
    data['NumberOfDependents'] = Series(replace_woe(data['NumberOfDependents'], cutx10, woex10))
    data.to_csv('WoeData.csv', index=False)

7.2 Logisic模型建立

#導入數據
    data = pd.read_csv('WoeData.csv')
    #應變量
    Y=data['SeriousDlqin2yrs']
    #自變量,剔除對因變量影響不明顯的變量
    X=data.drop(['SeriousDlqin2yrs','DebtRatio','MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines','NumberOfDependents'],axis=1)
    X1=sm.add_constant(X)
    logit=sm.Logit(Y,X1)
    result=logit.fit()
    print(result.summary())

7.3 模型檢驗

我們需要驗證一下模型的預測能力如何。我們使用在建模開始階段預留的test數據進行檢驗。通過ROC曲線和AUC來評估模型的擬合能力。

在Python中,可以利用sklearn.metrics,它能方便比較兩個分類器,自動計算ROC和AUC。

實現代碼:

#應變量
    Y_test = test['SeriousDlqin2yrs']
    #自變量,剔除對因變量影響不明顯的變量,與模型變量對應
    X_test = test.drop(['SeriousDlqin2yrs', 'DebtRatio', 'MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines', 'NumberOfDependents'], axis=1)
    X3 = sm.add_constant(X_test)
    resu = result.predict(X3)#進行預測
    fpr, tpr, threshold = roc_curve(Y_test, resu)
    rocauc = auc(fpr, tpr)#計算AUC
    plt.plot(fpr, tpr, 'b', label='AUC = %0.2f' % rocauc)#生成ROC曲線
    plt.legend(loc='lower right')
    plt.plot([0, 1], [0, 1], 'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('真正率')
    plt.xlabel('假正率')
    plt.show()

從上圖可知,AUC值爲0.85,說明該模型的預測效果還是不錯的,正確率較高。

八、信用評分

我們已經基本完成了建模相關的工作,並用ROC曲線驗證了模型的預測能力。接下來的步驟,就是將Logistic模型轉換爲標準評分卡的形式。

8.1 評分標準

img

img

依據以上論文資料得到: a=log(p_good/P_bad) Score = offset + factor * log(odds) 在建立標準評分卡之前,我們需要選取幾個評分卡參數:基礎分值、 PDO(比率翻倍的分值)和好壞比。 這裏, 我們取600分爲基礎分值,PDO爲20 (每高20分好壞比翻一倍),好壞比取20。

 # 我們取600分爲基礎分值,PDO爲20(每高20分好壞比翻一倍),好壞比取20。
    p = 20 / math.log(2)
    q = 600 - 20 * math.log(20) / math.log(2)
    baseScore = round(q + p * coe[0], 0)

個人總評分=基礎分+各部分得分

8.2 部分評分

下面計算各變量部分的分數。各部分得分函數:

#計算分數函數
def get_score(coe,woe,factor):
    scores=[]
    for w in woe:
        score=round(coe*w*factor,0)
        scores.append(score)
    return scores

計算各變量得分情況:

# 各項部分分數
    x1 = get_score(coe[1], woex1, p)
    x2 = get_score(coe[2], woex2, p)
    x3 = get_score(coe[3], woex3, p)
    x7 = get_score(coe[4], woex7, p)
    x9 = get_score(coe[5], woex9, p)

我們可以得到各部分的評分卡如圖所示:

img

九、自動評分系統

根據變量來計算分數,實現如下:

#根據變量計算分數
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

我們來計算test裏面的分數:

    test1 = pd.read_csv('TestData.csv')
    test1['BaseScore']=Series(np.zeros(len(test1)))+baseScore
    test1['x1'] = Series(compute_score(test1['RevolvingUtilizationOfUnsecuredLines'], cutx1, x1))
    test1['x2'] = Series(compute_score(test1['age'], cutx2, x2))
    test1['x3'] = Series(compute_score(test1['NumberOfTime30-59DaysPastDueNotWorse'], cutx3, x3))
    test1['x7'] = Series(compute_score(test1['NumberOfTimes90DaysLate'], cutx7, x7))
    test1['x9'] = Series(compute_score(test1['NumberOfTime60-89DaysPastDueNotWorse'], cutx9, x9))
    test1['Score'] = test1['x1'] + test1['x2'] + test1['x3'] + test1['x7'] +test1['x9']  + baseScore
    test1.to_csv('ScoreData.csv', index=False)

完整代碼:

import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import scipy.stats.stats as stats
import matplotlib.pyplot as plt
import statsmodels.api as sm
import math
#列全部顯示
pd.set_option('display.max_rows',1000)
pd.set_option('display.max_columns',1000)
#載入數據
data = pd.read_csv('C:\\Users\\nanafighting\\Desktop\\cs-training.csv')
#數據集確實和分佈情況
data.describe().to_csv('DataDescribe.csv')
#print(data.head())



import pandas as pd
import matplotlib.pyplot as plt #導入圖像庫
from sklearn.ensemble import RandomForestRegressor

# 用隨機森林對缺失值預測填充函數
def set_missing(df):
    # 把已有的數值型特徵取出來
    process_df = df.ix[:,[5,0,1,2,3,4,6,7,8,9]]
    # 分成已知該特徵和未知該特徵兩部分
    known = process_df[process_df.MonthlyIncome.notnull()].values
    unknown = process_df[process_df.MonthlyIncome.isnull()].values
    # X爲特徵屬性值
    X = known[:, 1:]
    # y爲結果標籤值
    y = known[:, 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[:, 1:]).round(0)
    print(predicted)
    # 用得到的預測結果填補原缺失數據
    df.loc[(df.MonthlyIncome.isnull()), 'MonthlyIncome'] = predicted
    return df

if __name__ == '__main__':
    #載入數據
    data = pd.read_csv('C:\\Users\\nanafighting\\Desktop\\cs-training.csv')
    #數據集確實和分佈情況
    data=set_missing(data)#用隨機森林填補比較多的缺失值
    data=data.dropna()#刪除比較少的缺失值
    data = data.drop_duplicates()#刪除重複項
    data.to_csv('MissingData.csv',index=False)
    data.describe().to_csv('MissingDataDescribe.csv')
    #print(data.head())

    #異常值處理
    #年齡等於0的異常值進行剔除
    data=data[data['age']>0]
    # 箱形圖
    data379=data[['NumberOfTime30-59DaysPastDueNotWorse','NumberOfTimes90DaysLate','NumberOfTime60-89DaysPastDueNotWorse']]
    data379.boxplot()
    data = data[data['NumberOfTime30-59DaysPastDueNotWorse'] < 90]
    data379 = data[['NumberOfTime30-59DaysPastDueNotWorse', 'NumberOfTimes90DaysLate', 'NumberOfTime60-89DaysPastDueNotWorse']]
    #data379.boxplot()
    plt.show()
    data.to_csv('PretreatmentData.csv')
import pandas as pd
import matplotlib.pyplot as plt #導入圖像庫
from sklearn.cross_validation import train_test_split

def outlier_processing(df,col):
    s=df[col]
    oneQuoter=s.quantile(0.25)
    threeQuote=s.quantile(0.75)
    irq=threeQuote-oneQuoter
    min=oneQuoter-1.5*irq
    max=threeQuote+1.5*irq
    df=df[df[col]<=max]
    df=df[df[col]>=min]
    return df

if __name__ == '__main__':
    data = pd.read_csv('MissingData.csv')
    # 年齡等於0的異常值進行剔除
    data = data[data['age'] > 0]
    data = data[data['NumberOfTime30-59DaysPastDueNotWorse'] < 90]#剔除異常值
    data['SeriousDlqin2yrs']=1-data['SeriousDlqin2yrs']
    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)
    # print(Y_train)
    train = pd.concat([Y_train, X_train], axis=1)
    test = pd.concat([Y_test, X_test], axis=1)
    clasTest = test.groupby('SeriousDlqin2yrs')['SeriousDlqin2yrs'].count()
    train.to_csv('TrainData.csv',index=False)
    test.to_csv('TestData.csv',index=False)
    print(train.shape)
    print(test.shape)
import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import scipy.stats.stats as stats
import matplotlib.pyplot as plt
import statsmodels.api as sm
import math

# 定義自動分箱函數
def mono_bin(Y, X, n = 20):
    r = 0
    good=Y.sum()
    bad=Y.count()-good
    while np.abs(r) < 1:
        d1 = pd.DataFrame({"X": X, "Y": Y, "Bucket": pd.qcut(X, 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
    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()
    d4 = (d3.sort_index(by = 'min'))
    print("=" * 60)
    print(d4)
    cut=[]
    cut.append(float('-inf'))
    for i in range(1,n+1):
        qua=X.quantile(i/(n+1))
        cut.append(round(qua,4))
    cut.append(float('inf'))
    woe=list(d4['woe'].round(3))
    return d4,iv,cut,woe
#自定義分箱函數
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)})
    d2=d1.groupby('Bucket', as_index = True)
    d3 = pd.DataFrame(d2.X.min(), columns=['min'])
    d3['min'] = d2.min().X
    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()
    d4 = (d3.sort_values(by='min'))
    print("=" * 60)
    print(d4)
    woe = list(d4['woe'].round(3))
    return d4, iv,woe
#用woe代替
def replace_woe(series,cut,woe):
    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(woe[m])
        i += 1
    return list
#計算分數函數
def get_score(coe,woe,factor):
    scores=[]
    for w in woe:
        score=round(coe*w*factor,0)
        scores.append(score)
    return scores

#根據變量計算分數
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

if __name__ == '__main__':
    data = pd.read_csv('TrainData.csv')
    pinf = float('inf')#正無窮大
    ninf = float('-inf')#負無窮大
    dfx1, ivx1,cutx1,woex1=mono_bin(data.SeriousDlqin2yrs,data.RevolvingUtilizationOfUnsecuredLines,n=10)
    dfx2, ivx2,cutx2,woex2=mono_bin(data.SeriousDlqin2yrs, data.age, n=10)
    dfx4, ivx4,cutx4,woex4 =mono_bin(data.SeriousDlqin2yrs, data.DebtRatio, n=20)
    dfx5, ivx5,cutx5,woex5 =mono_bin(data.SeriousDlqin2yrs, data.MonthlyIncome, n=10)
    # 連續變量離散化
    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(data.SeriousDlqin2yrs, data['NumberOfTime30-59DaysPastDueNotWorse'], cutx3)
    dfx6, ivx6 ,woex6= self_bin(data.SeriousDlqin2yrs, data['NumberOfOpenCreditLinesAndLoans'], cutx6)
    dfx7, ivx7,woex7 = self_bin(data.SeriousDlqin2yrs, data['NumberOfTimes90DaysLate'], cutx7)
    dfx8, ivx8,woex8 = self_bin(data.SeriousDlqin2yrs, data['NumberRealEstateLoansOrLines'], cutx8)
    dfx9, ivx9,woex9 = self_bin(data.SeriousDlqin2yrs, data['NumberOfTime60-89DaysPastDueNotWorse'], cutx9)
    dfx10, ivx10,woex10 = self_bin(data.SeriousDlqin2yrs, data['NumberOfDependents'], cutx10)
    ivlist=[ivx1,ivx2,ivx3,ivx4,ivx5,ivx6,ivx7,ivx8,ivx9,ivx10]
    index=['x1','x2','x3','x4','x5','x6','x7','x8','x9','x10']
    fig1 = plt.figure(1)
    ax1 = fig1.add_subplot(1, 1, 1)
    x = np.arange(len(index))+1
    ax1.bar(x, ivlist, width=0.4)
    ax1.set_xticks(x)
    ax1.set_xticklabels(index, rotation=0, fontsize=12)
    ax1.set_ylabel('IV(Information Value)', fontsize=14)
    for a, b in zip(x, ivlist):
        plt.text(a, b + 0.01, '%.4f' % b, ha='center', va='bottom', fontsize=10)
    # 替換成woe
    data['RevolvingUtilizationOfUnsecuredLines'] = Series(replace_woe(data['RevolvingUtilizationOfUnsecuredLines'], cutx1, woex1))
    data['age'] = Series(replace_woe(data['age'], cutx2, woex2))
    data['NumberOfTime30-59DaysPastDueNotWorse'] = Series(replace_woe(data['NumberOfTime30-59DaysPastDueNotWorse'], cutx3, woex3))
    data['DebtRatio'] = Series(replace_woe(data['DebtRatio'], cutx4, woex4))
    data['MonthlyIncome'] = Series(replace_woe(data['MonthlyIncome'], cutx5, woex5))
    data['NumberOfOpenCreditLinesAndLoans'] = Series(replace_woe(data['NumberOfOpenCreditLinesAndLoans'], cutx6, woex6))
    data['NumberOfTimes90DaysLate'] = Series(replace_woe(data['NumberOfTimes90DaysLate'], cutx7, woex7))
    data['NumberRealEstateLoansOrLines'] = Series(replace_woe(data['NumberRealEstateLoansOrLines'], cutx8, woex8))
    data['NumberOfTime60-89DaysPastDueNotWorse'] = Series(replace_woe(data['NumberOfTime60-89DaysPastDueNotWorse'], cutx9, woex9))
    data['NumberOfDependents'] = Series(replace_woe(data['NumberOfDependents'], cutx10, woex10))
    data.to_csv('WoeData.csv', index=False)

    test= pd.read_csv('TestData.csv')
    # 替換成woe
    test['RevolvingUtilizationOfUnsecuredLines'] = Series(replace_woe(test['RevolvingUtilizationOfUnsecuredLines'], cutx1, woex1))
    test['age'] = Series(replace_woe(test['age'], cutx2, woex2))
    test['NumberOfTime30-59DaysPastDueNotWorse'] = Series(replace_woe(test['NumberOfTime30-59DaysPastDueNotWorse'], cutx3, woex3))
    test['DebtRatio'] = Series(replace_woe(test['DebtRatio'], cutx4, woex4))
    test['MonthlyIncome'] = Series(replace_woe(test['MonthlyIncome'], cutx5, woex5))
    test['NumberOfOpenCreditLinesAndLoans'] = Series(replace_woe(test['NumberOfOpenCreditLinesAndLoans'], cutx6, woex6))
    test['NumberOfTimes90DaysLate'] = Series(replace_woe(test['NumberOfTimes90DaysLate'], cutx7, woex7))
    test['NumberRealEstateLoansOrLines'] = Series(replace_woe(test['NumberRealEstateLoansOrLines'], cutx8, woex8))
    test['NumberOfTime60-89DaysPastDueNotWorse'] = Series(replace_woe(test['NumberOfTime60-89DaysPastDueNotWorse'], cutx9, woex9))
    test['NumberOfDependents'] = Series(replace_woe(test['NumberOfDependents'], cutx10, woex10))
    test.to_csv('TestWoeData.csv', index=False)

    #計算分數
    #coe爲邏輯迴歸模型的係數
    coe=[9.738849,0.638002,0.505995,1.032246,1.790041,1.131956]
    # 我們取600分爲基礎分值,PDO爲20(每高20分好壞比翻一倍),好壞比取20。
    p = 20 / math.log(2)
    q = 600 - 20 * math.log(20) / math.log(2)
    baseScore = round(q + p * coe[0], 0)
    # 各項部分分數
    x1 = get_score(coe[1], woex1, p)
    x2 = get_score(coe[2], woex2, p)
    x3 = get_score(coe[3], woex3, p)
    x7 = get_score(coe[4], woex7, p)
    x9 = get_score(coe[5], woex9, p)
    print(x1,x2, x3, x7, x9)
    test1 = pd.read_csv('TestData.csv')
    test1['BaseScore']=Series(np.zeros(len(test1)))+baseScore
    test1['x1'] = Series(compute_score(test1['RevolvingUtilizationOfUnsecuredLines'], cutx1, x1))
    test1['x2'] = Series(compute_score(test1['age'], cutx2, x2))
    test1['x3'] = Series(compute_score(test1['NumberOfTime30-59DaysPastDueNotWorse'], cutx3, x3))
    test1['x7'] = Series(compute_score(test1['NumberOfTimes90DaysLate'], cutx7, x7))
    test1['x9'] = Series(compute_score(test1['NumberOfTime60-89DaysPastDueNotWorse'], cutx9, x9))
    test1['Score'] = test1['x1'] + test1['x2'] + test1['x3'] + test1['x7'] +test1['x9']  + baseScore
    test1.to_csv('ScoreData.csv', index=False)
    plt.show()
import pandas as pd
import matplotlib.pyplot as plt #導入圖像庫
import matplotlib
import seaborn as sns
import statsmodels.api as sm
from sklearn.metrics import roc_curve, auc

if __name__ == '__main__':
    matplotlib.rcParams['axes.unicode_minus'] = False
    data = pd.read_csv('WoeData.csv')
    Y=data['SeriousDlqin2yrs']
    X=data.drop(['SeriousDlqin2yrs','DebtRatio','MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines','NumberOfDependents'],axis=1)
    X1=sm.add_constant(X)
    logit=sm.Logit(Y,X1)
    result=logit.fit()
    print(result.params)

    test = pd.read_csv('TestWoeData.csv')
    Y_test = test['SeriousDlqin2yrs']
    X_test = test.drop(['SeriousDlqin2yrs', 'DebtRatio', 'MonthlyIncome', 'NumberOfOpenCreditLinesAndLoans','NumberRealEstateLoansOrLines', 'NumberOfDependents'], axis=1)
    X3 = sm.add_constant(X_test)
    resu = result.predict(X3)
    fpr, tpr, threshold = roc_curve(Y_test, resu)
    rocauc = auc(fpr, tpr)
    plt.plot(fpr, tpr, 'b', label='AUC = %0.2f' % rocauc)
    plt.legend(loc='lower right')
    plt.plot([0, 1], [0, 1], 'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('真正率')
    plt.xlabel('假正率')
    plt.show()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章