(一)python-申請?jiān)u分卡模型

簡介

本文通過使用LendingClub的數(shù)據(jù),采用卡方分箱(ChiMerge)、WOE編碼、計(jì)算IV值、單變量和多變量(VIF)分析,然后使用邏輯回歸模型進(jìn)行訓(xùn)練,在變量篩選時(shí)也可嘗試添加L1約束或通過隨機(jī)森林篩選變量,最后進(jìn)行模型評(píng)估。

關(guān)鍵詞:卡方分箱,WOE,IV值,變量分析,邏輯回歸

一、數(shù)據(jù)預(yù)處理

數(shù)據(jù)清洗:數(shù)據(jù)選擇、格式轉(zhuǎn)換、缺失值填補(bǔ)
由于貸款期限(term)有多個(gè)種類,申請?jiān)u分卡模型評(píng)估的違約概率必須在統(tǒng)一的期限中,并且不宜太長,因此選擇36months的數(shù)據(jù)作為本次建模數(shù)據(jù),60%訓(xùn)練,40%測試。

folderOfData = os.path.join(os.getcwd(), 'data')
allData = pd.read_csv(os.path.join(folderOfData,'application.csv'),header = 0, encoding = 'latin1')
allData['term'] = allData['term'].apply(lambda x: int(x.replace(' months','')))
# 處理標(biāo)簽:Fully Paid是正常用戶;Charged Off是違約用戶
allData['y'] = allData['loan_status'].map(lambda x: int(x == 'Charged Off'))

allData1 = allData.loc[allData.term == 36]
trainData, testData = train_test_split(allData1,test_size=0.4)

進(jìn)一步清洗:

  1. 將int_rate利息轉(zhuǎn)換為小數(shù)形式
  2. 將emp_length處理為:10+為11,<1為0,空為-1
  3. desc為有記錄和無記錄兩種情況
  4. 日期處理
  5. 兩個(gè)日期之間月數(shù)計(jì)算
# 將帶%的百分比變?yōu)楦↑c(diǎn)數(shù)
trainData['int_rate_clean'] = trainData['int_rate'].map(lambda x: float(x.replace('%',''))/100)

# 將工作年限進(jìn)行轉(zhuǎn)化,否則影響排序
trainData['emp_length_clean'] = trainData['emp_length'].map(CareerYear)

# 將desc的缺失作為一種狀態(tài),非缺失作為另一種狀態(tài)
trainData['desc_clean'] = trainData['desc'].map(DescExisting)

# 處理日期。earliest_cr_line的格式不統(tǒng)一,需要統(tǒng)一格式且轉(zhuǎn)換成python的日期
trainData['app_date_clean'] = trainData['issue_d'].map(lambda x: ConvertDateStr(x))
trainData['earliest_cr_line_clean'] = trainData['earliest_cr_line'].map(lambda x: ConvertDateStr(x))

# 處理mths_since_last_delinq。注意原始值中有0,所以用-1代替缺失
trainData['mths_since_last_delinq_clean'] = trainData['mths_since_last_delinq'].map(lambda x:MakeupMissing(x))

trainData['mths_since_last_record_clean'] = trainData['mths_since_last_record'].map(lambda x:MakeupMissing(x))

trainData['pub_rec_bankruptcies_clean'] = trainData['pub_rec_bankruptcies'].map(lambda x:MakeupMissing(x))

二、變量衍生和挑選

  • 衍生:
  1. 考慮申請額度與收入的占比
  2. 考慮earliest_cr_line到申請日期的跨度,計(jì)算月份數(shù)
# 考慮申請額度與收入的占比
trainData['limit_income'] = trainData.apply(lambda x: x.loan_amnt / x.annual_inc, axis = 1)
# 考慮earliest_cr_line到申請日期的跨度,計(jì)算月份數(shù)
trainData['earliest_cr_to_app'] = trainData.apply(lambda x: MonthGap(x.earliest_cr_line_clean,x.app_date_clean), axis = 1)
  • 挑選:
    我們初步挑選變量如下,分為兩類:數(shù)值型(連續(xù)型)的和類別型的變量。
num_features = ['int_rate_clean','emp_length_clean','annual_inc', 'dti', 'delinq_2yrs', 'earliest_cr_to_app','inq_last_6mths', \
                'mths_since_last_record_clean', 'mths_since_last_delinq_clean','open_acc','pub_rec','total_acc','limit_income','earliest_cr_to_app']

cat_features = ['home_ownership', 'verification_status','desc_clean', 'purpose', 'zip_code','addr_state','pub_rec_bankruptcies_clean']

三、卡方分箱法

采用卡方(ChiMerge)分箱,要求分箱完成之后:

  1. 不超過5箱(本模型默認(rèn)不超過5箱)
  2. 壞樣本率(Bad Rate)單調(diào)
  3. 每箱同時(shí)包含好壞樣本
  4. 如有特殊值如-1單獨(dú)成一箱,此箱不參與Bad Rate單調(diào)性檢驗(yàn)

連續(xù)型的變量可以直接進(jìn)行分箱,對于類別型的變量分為以下幾種情況:

  1. 當(dāng)類別型變量取值比較多時(shí)(本例中大于5),先用bad rate 進(jìn)行編碼,然后放入連續(xù)型變量列表中,使用連續(xù)型變量分箱的方法進(jìn)行分箱。
  2. 當(dāng)取值較少時(shí)(本例中小于等于5),分兩種情況:
    (1)如果每種類別同時(shí)包含好壞樣本,則無需分箱;
    (2)如果有類別只包含好壞樣本的一種,則需要合并;

具體操作如下:
第一步,檢查類別型變量中,哪些變量取值超過5。

more_value_features = []
less_value_features = []
# 第一步,檢查類別型變量中,哪些變量取值超過5
for var in cat_features:
    valueCounts = len(set(trainData[var]))
    print valueCounts
    if valueCounts > 5:
        more_value_features.append(var)  #取值超過5的變量,需要bad rate編碼,再用卡方分箱法進(jìn)行分箱
    else:
        less_value_features.append(var)

第二步,當(dāng)取值<5時(shí):如果每種類別同時(shí)包含好壞樣本,無需分箱;如果有類別只包含好壞樣本的一種,需要合并。

merge_bin_dict = {}  #存放需要合并的變量,以及合并方法
var_bin_list = []   #由于某個(gè)取值沒有好或者壞樣本而需要合并的變量
for col in less_value_features:
    binBadRate = BinBadRate(trainData, col, 'y')[0]
    if min(binBadRate.values()) == 0 :  #由于某個(gè)取值沒有壞樣本而進(jìn)行合并
        print '{} need to be combined due to 0 bad rate'.format(col)
        combine_bin = MergeBad0(trainData, col, 'y')
        merge_bin_dict[col] = combine_bin
        newVar = col + '_Bin'
        trainData[newVar] = trainData[col].map(combine_bin)
        var_bin_list.append(newVar)
    if max(binBadRate.values()) == 1:    #由于某個(gè)取值沒有好樣本而進(jìn)行合并
        print '{} need to be combined due to 0 good rate'.format(col)
        combine_bin = MergeBad0(trainData, col, 'y',direction = 'good')
        merge_bin_dict[col] = combine_bin
        newVar = col + '_Bin'
        trainData[newVar] = trainData[col].map(combine_bin)
        var_bin_list.append(newVar)

第三步,當(dāng)取值>5時(shí):用bad rate進(jìn)行編碼,放入連續(xù)型變量里。

br_encoding_dict = {}   #記錄按照bad rate進(jìn)行編碼的變量,及編碼方式
for col in more_value_features:
    br_encoding = BadRateEncoding(trainData, col, 'y')
    trainData[col+'_br_encoding'] = br_encoding['encoding']
    br_encoding_dict[col] = br_encoding['bad_rate']
    num_features.append(col+'_br_encoding')

第四步,分箱,對連續(xù)型變量列表num_features進(jìn)行卡方分箱。本文分箱后的最多的箱數(shù)為5箱。

continous_merged_dict = {}
for col in num_features:
    max_interval = 5  # 分箱后的最多的箱數(shù)
    print "{} is in processing".format(col)
    if -1 not in set(trainData[col]):   #-1會(huì)當(dāng)成特殊值處理。如果沒有-1,則所有取值都參與分箱
        cutOff = ChiMerge(trainData, col, 'y', max_interval=max_interval,special_attribute=[],minBinPcnt=0)
        trainData[col+'_Bin'] = trainData[col].map(lambda x: AssignBin(x, cutOff,special_attribute=[]))
        monotone = BadRateMonotone(trainData, col+'_Bin', 'y')   # 檢驗(yàn)分箱后的單調(diào)性是否滿足
        while(not monotone):
            # 檢驗(yàn)分箱后的單調(diào)性是否滿足。如果不滿足,則縮減分箱的個(gè)數(shù)。
            max_interval -= 1
            cutOff = ChiMerge(trainData, col, 'y', max_interval=max_interval, special_attribute=[],
                                          minBinPcnt=0)
            trainData[col + '_Bin'] = trainData[col].map(lambda x: AssignBin(x, cutOff, special_attribute=[]))
            if max_interval == 2:
                # 當(dāng)分箱數(shù)為2時(shí),必然單調(diào)
                break
            monotone = BadRateMonotone(trainData, col + '_Bin', 'y')
        newVar = col + '_Bin'
        trainData[newVar] = trainData[col].map(lambda x: AssignBin(x, cutOff, special_attribute=[]))
        var_bin_list.append(newVar)
    else:
        # 如果有-1,則除去-1后,其他取值參與分箱
        cutOff = ChiMerge(trainData, col, 'y', max_interval=max_interval, special_attribute=[-1],
                                      minBinPcnt=0)
        trainData[col + '_Bin'] = trainData[col].map(lambda x: AssignBin(x, cutOff, special_attribute=[-1]))
        monotone = BadRateMonotone(trainData, col + '_Bin', 'y',['Bin -1'])
        while (not monotone):
            max_interval -= 1
            # 如果有-1,-1的bad rate不參與單調(diào)性檢驗(yàn)
            cutOff = ChiMerge(trainData, col, 'y', max_interval=max_interval, special_attribute=[-1],
                                          minBinPcnt=0)
            trainData[col + '_Bin'] = trainData[col].map(lambda x: AssignBin(x, cutOff, special_attribute=[-1]))
            if max_interval == 3:
                # 考慮特殊值,當(dāng)分箱數(shù)為3-1=2時(shí),必然單調(diào)
                break
            monotone = BadRateMonotone(trainData, col + '_Bin', 'y',['Bin -1'])
        newVar = col + '_Bin'
        trainData[newVar] = trainData[col].map(lambda x: AssignBin(x, cutOff, special_attribute=[-1]))
        var_bin_list.append(newVar)
    continous_merged_dict[col] = cutOff

四、WOE編碼和IV值

經(jīng)常上一步的分箱后,分箱后的變量有如下幾種情況:

  1. 初始取值個(gè)數(shù)小于5,且不需要合并的類別型變量。
  2. 初始取值個(gè)數(shù)小于5,需要合并的類別型變量,并且合并后的新變量不再需要合并。
  3. 初始取值個(gè)數(shù)超過5,需要合并的類別型變量,并且合并后的新變量不再需要合并。
  4. 連續(xù)型變量進(jìn)行卡方分箱。

如下取到每個(gè)變量分箱后的WOE和該變量的IV值:

WOE_dict = {}
IV_dict = {}
for var in all_var:
    woe_iv = CalcWOE(trainData, var, 'y')
    WOE_dict[var] = woe_iv['WOE']
    IV_dict[var] = woe_iv['IV']

將變量IV值進(jìn)行降序排列,得到結(jié)果如下:

IV_dict_sorted = sorted(IV_dict.items(), key=lambda x: x[1], reverse=True)

IV_values = [i[1] for i in IV_dict_sorted]
IV_name = [i[0] for i in IV_dict_sorted]
plt.title('feature IV')
plt.bar(range(len(IV_values)),IV_values)

得到的IV值如下圖所示:


image.png

五、變量分析

單變量分析和多變量分析,均基于WOE編碼后的值。

  1. 選擇IV值大于等于0.01的變量
  2. 比較兩兩線性相關(guān)性。如果相關(guān)系數(shù)的絕對值高于閾值,剔除IV較低的一個(gè)。
#選取IV>=0.01的變量
high_IV = {k:v for k, v in IV_dict.items() if v >= 0.01}
high_IV_sorted = sorted(high_IV.items(),key=lambda x:x[1],reverse=True)

short_list = high_IV.keys()
short_list_2 = []
for var in short_list:
    newVar = var + '_WOE'
    trainData[newVar] = trainData[var].map(WOE_dict[var])
    short_list_2.append(newVar)

#對于上一步的結(jié)果,計(jì)算相關(guān)系數(shù)矩陣,并畫出熱力圖進(jìn)行數(shù)據(jù)可視化
trainDataWOE = trainData[short_list_2]
f, ax = plt.subplots(figsize=(10, 8))
corr = trainDataWOE.corr()
sns.heatmap(corr, mask=np.zeros_like(corr, dtype=np.bool), cmap=sns.diverging_palette(220, 10, as_cmap=True),square=True, ax=ax)
f.savefig('sns_heatmap_high_IV.png')

根據(jù)IV值挑選的變量的相關(guān)系數(shù)矩陣熱力圖:


image.png

單變量兩兩間的線性相關(guān)性檢驗(yàn):
(1)將候選變量按照IV進(jìn)行降序排列
(2)計(jì)算第i和第i+1的變量的線性相關(guān)系數(shù)
(3)對于系數(shù)超過閾值的兩個(gè)變量,剔除IV較低的一個(gè)
此處閾值為0.7,大于0.7則表示有相關(guān)性。見如下代碼:

deleted_index = []
cnt_vars = len(high_IV_sorted)
for i in range(cnt_vars):
    if i in deleted_index:
        continue
    x1 = high_IV_sorted[i][0]+"_WOE"
    for j in range(cnt_vars):
        if i == j or j in deleted_index:
            continue
        y1 = high_IV_sorted[j][0]+"_WOE"
        roh = np.corrcoef(trainData[x1],trainData[y1])[0,1]
        if abs(roh)>0.7:
            x1_IV = high_IV_sorted[i][1]
            y1_IV = high_IV_sorted[j][1]
            if x1_IV > y1_IV:
                deleted_index.append(j)
            else:
                deleted_index.append(i)

multi_analysis_vars_1 = [high_IV_sorted[i][0]+"_WOE" for i in range(cnt_vars) if i not in deleted_index]

多變量分析:VIF
一般要小于10,本次結(jié)果max_VIF為:1.5093709849027372,則多變量之間排除共線性。

X = np.matrix(trainData[multi_analysis_vars_1])
VIF_list = [variance_inflation_factor(X, i) for i in range(X.shape[1])]
max_VIF = max(VIF_list)
print max_VIF

六、邏輯回歸模型

要求:
1,變量顯著
2,符號(hào)為負(fù)
將多變量分析后的變量帶入LR模型中,

y = trainData['y']
X = trainData[multi_analysis]
X['intercept'] = [1]*X.shape[0]
LR = sm.Logit(y, X).fit()
summary = LR.summary()
pvals = LR.pvalues
pvals = pvals.to_dict()

逐步剔除p值不顯著的變量

varLargeP = {k: v for k,v in pvals.items() if v >= 0.1}
varLargeP = sorted(varLargeP.items(), key=lambda d:d[1], reverse = True)
while(len(varLargeP) > 0 and len(multi_analysis) > 0):
    # 每次迭代中,剔除最不顯著的變量,直到
    # (1) 剩余所有變量均顯著
    # (2) 沒有特征可選
    varMaxP = varLargeP[0][0]
    print varMaxP
    if varMaxP == 'intercept':
        print 'the intercept is not significant!'
        break
    multi_analysis.remove(varMaxP)
    y = trainData['y']
    X = trainData[multi_analysis]
    X['intercept'] = [1] * X.shape[0]

    LR = sm.Logit(y, X).fit()
    pvals = LR.pvalues
    pvals = pvals.to_dict()
    varLargeP = {k: v for k, v in pvals.items() if v >= 0.1}
    varLargeP = sorted(varLargeP.iteritems(), key=lambda d: d[1], reverse=True)

summary = LR.summary()

邏輯回歸結(jié)果如下:

                                        LLR p-value:                2.460e-280
========================================================================================================
                                           coef    std err          z      P>|z|      [0.025      0.975]
--------------------------------------------------------------------------------------------------------
zip_code_br_encoding_Bin_WOE            -0.9467      0.045    -21.258      0.000      -1.034      -0.859
int_rate_clean_Bin_WOE                  -0.8742      0.055    -15.779      0.000      -0.983      -0.766
annual_inc_Bin_WOE                      -0.7039      0.095     -7.383      0.000      -0.891      -0.517
purpose_br_encoding_Bin_WOE             -0.8559      0.087     -9.785      0.000      -1.027      -0.684
inq_last_6mths_Bin_WOE                  -0.7831      0.104     -7.537      0.000      -0.987      -0.579
addr_state_br_encoding_Bin_WOE          -0.2423      0.121     -1.997      0.046      -0.480      -0.005
limit_income_Bin_WOE                    -0.4409      0.134     -3.299      0.001      -0.703      -0.179
mths_since_last_record_clean_Bin_WOE    -0.7616      0.141     -5.416      0.000      -1.037      -0.486
total_acc_Bin_WOE                       -0.2963      0.173     -1.710      0.087      -0.636       0.043
dti_Bin_WOE                             -0.7897      0.196     -4.021      0.000      -1.175      -0.405
emp_length_clean_Bin_WOE                -0.7229      0.200     -3.611      0.000      -1.115      -0.331
intercept                               -2.1014      0.027    -78.645      0.000      -2.154      -2.049
========================================================================================================

可以看到p值均顯著,且系數(shù)為負(fù)。
計(jì)算auc值,結(jié)果為:0.74

trainData['prob'] = LR.predict(X)
auc = roc_auc_score(trainData['y'],trainData['prob'])  #AUC = 0.73

七、驗(yàn)證模型

用同樣的方法,對驗(yàn)證集數(shù)據(jù)進(jìn)行處理后,放入模型,如下得到
auc=0.65
ks = 0.22
表明模型有一定的預(yù)測能力和區(qū)分度

testData['intercept'] = [1]*testData.shape[0]
#預(yù)測數(shù)據(jù)集中,變量順序需要和LR模型的變量順序一致
#例如在訓(xùn)練集里,變量在數(shù)據(jù)中的順序是“負(fù)債比”在“借款目的”之前,對應(yīng)地,在測試集里,“負(fù)債比”也要在“借款目的”之前
testData2 = testData[list(LR.params.index)]
testData['prob'] = LR.predict(testData2)

#計(jì)算KS和AUC
auc = roc_auc_score(testData['y'],testData['prob'])
ks = KS(testData, 'prob', 'y')

計(jì)算評(píng)分:

basePoint = 250
PDO = 200
testData['score'] = testData['prob'].map(lambda x:Prob2Score(x, basePoint, PDO))
testData = testData.sort_values(by = 'score')

結(jié)果如下,分值與頻數(shù)的分布近似為正態(tài)分布。根據(jù)業(yè)務(wù)需要以及相應(yīng)的風(fēng)險(xiǎn)比例,劃分評(píng)分區(qū)間,合理應(yīng)用評(píng)分卡模型。


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • **2014真題Directions:Read the following text. Choose the be...
    又是夜半驚坐起閱讀 11,262評(píng)論 0 23
  • 維基經(jīng)濟(jì)學(xué)是十年前看的書,書中提出的互聯(lián)網(wǎng)四觀念:開放,對等,共享,全球運(yùn)作。 我覺得這本書應(yīng)該是互聯(lián)網(wǎng)思維的"圣...
    吳幸蓁閱讀 531評(píng)論 0 0
  • 千年思華佗,醫(yī)術(shù)人稱奇。 能救世人命,可解天下疾; 仰慕心不已,驚嘆無人敵! 蒼生多病疴,念及古神醫(yī)。
    妙手揮毫著文章閱讀 404評(píng)論 0 2
  • 01 前幾天和閨蜜打了視頻電話,因?yàn)槲覀兏髯远己苊?,所以很久沒見面也沒聊天了。 她和我一樣,今年大四。問起近況,她...
    此諾相惜閱讀 339評(píng)論 0 3
  • 上邪,我欲與君相知,長命無絕衰。山無陵,江水為竭。冬雷震震,夏雨雪。天地合,乃敢與君絕。 喜歡這樣的赤誠和勇敢,希...
    卻籬閱讀 399評(píng)論 0 0

友情鏈接更多精彩內(nèi)容