金融申請評分卡(2)
金融申請評分卡的資料預處理和特徵衍生
1、模型處理的一般流程
以上為模型的一般處理辦法;在本次資料欄位有:
欄位 | 名稱 |
---|---|
member_id | ID |
loan_amnt | 申請額度 |
term | 產品期限 |
int_rate | 利率 |
emp_length | 工作期限 |
home_ownership | 是否有自有住宅 |
annual_inc | 年收入 |
verification_status | 收入核驗狀態 |
desc | 描述 |
purpose | 貸款目的 |
title | 貸款目的描述 |
zip_code | 聯絡地址郵政編碼 |
addr_state | 聯絡地址所屬州 |
delinq_2yrs | 申貸日期前2年逾期次數 |
inq_last_6mths | 申請日前6個月諮詢次數 |
mths_since_last_delinq | 上次逾期距今月份數 |
mths_since_last_record | 上次登記公眾記錄距今的月份數 |
open_acc | 徵信局中記錄的信用產品數 |
pub_rec | 公眾不良記錄數 |
total_acc | 正在使用的信用產品數 |
pub_rec_bankruptcies | 公眾破產記錄數 |
earliest_cr_line | 第一次借貸時間 |
loan_status | 貸款狀態—目標變數 |
2、資料的預處理
2.1、基本處理辦法
- 利率方面的處理辦法:帶%的百分比,需要轉化為浮點數
- 工作年限“<1 year”轉化為0,“>10 year”的轉化為1
- 日期方面:直接轉化為標準日期
- 文字資訊:欄位中的desc就是客戶申請期間的申請原因等資訊,這裡處理採用最簡單的辦法,如果裡面有資訊,則為1,無資訊則為0,其他例如採用NLP的辦法,做其他處理,暫時不做,因為涉及分詞等等,處理其他麻煩,不是寫這次部落格的主要目的。
2.2、缺失值的處理辦法
缺失值的種類情況:
- 完全隨機缺失
- 隨機缺失
- 完全非隨機缺失
處理的辦法一般為以下幾種:
- 補缺
- 作為一種狀態,例如,空的為0,非空為1,處理起來簡單,如果缺失值不多,效果不錯
- 刪除本行的記錄,這種處理辦法最簡單,尤其在資料量較大的情況下,刪除部分資料,對整體基本無影響。
2.3、資料特徵構-特徵衍生
因為在原有的特徵上面,也就是直接特徵方面的資訊含量不足以很好的建立申請評分卡模型,所以一般都會去構建新的特徵,進行特徵的衍生。那麼經常接觸到的特徵衍生辦法如下:
- 計數:過去1年內申請貸款的總次數
- 求和:過去1年內的在線上的消費金額
- 比例:貸款申請額度和年收入的佔比
- 時間差:第一次開戶距離今天的時間長度
- 波動率:過去3年內每份工作的時間的標準差
以上構建的辦法均基於經驗的構建,不包含了因子分析等辦法
2.4、特徵的分箱
特徵分箱的目的:
- 將連續變數離散化
- 將多狀態的離散變數合併成少狀態
分箱的通俗解釋:
- 穩定性:避免了特徵中的無意義的波動對評分帶來的不好的影響
- 加強了模型的健壯性:避免了模型受到極端值的影響
舉個例子:例如未進行分箱之前,樣本資料裡面沒有一個高二年級的學生,那麼假定做好分箱之後,高一到高三均屬於高中,因此出現一個高二年級的學生後,就會被劃入高中這個“箱”,模型的穩定性就得到了加強;在健壯性方面,例如我的收入是1000,在申請貸款的時候給予的評分很低,假定就20分,經過我的不斷努力,跳槽7-8次之後,薪水漲到1500左右,這個時候,還是屬於低收入的困難人群,那麼給予的評分還是20分左右,這樣模型的健壯性就得到了體現,模型不需要根據一些小的變化就進行調整。
分箱簡單的解釋是:分箱就是為了做到同組之間的差異儘可能的小,不同組之間的差異儘可能的大。
分箱的好處:
- 可以把缺失值作為一個獨立的箱帶入到模型中去
- 將所有的變數變換到相似的尺度上(例如:一個變數是年齡,一個變數是月收入,不做分箱,2者之間的變化差距太大)
分箱的缺點:
- 計算量比較大,處理資料過程較為繁瑣。
- 編碼之後容易導致資訊的丟失。
2.5、特徵的分箱方法
分箱的辦法主要接觸到很多,等距、等頻、卡方分箱、決策樹分箱法,這裡只具體展示卡方分箱法,決策樹分箱的程式碼如下,其他的分箱僅說明原理:
coding=utf-8
import operator
from math import log
import time
class InformationGainSplitDiscretization(object):
def __init__(self):
self.minInfoGain_epos = 1e-8 #停止條件之一:最小資訊增益,當某資料集的最優分裂對應的資訊增益(即最大資訊增益)小於這個值,則此資料集停止進一步的分裂。
self.splitPiontsList = [] #分裂點列表,最終要依分裂點的值升序排列。以便後續的離散化函式(輸入:待離散的資料集)使用。 #self.totalGain = ()
self.tree_deep = 3
def splitDataSet(self,dataSet, splitpoint_idx):
leftSubDataSet = []
rightSubDataSet = []
for leftSubSet in dataSet[:(splitpoint_idx+1)]:
leftSubDataSet.append(leftSubSet)
for rightSubSet in dataSet[(splitpoint_idx+1):]:
rightSubDataSet.append(rightSubSet)
leftSubDataSet.sort(key=lambda x : x[0], reverse=False)
rightSubDataSet.sort(key=lambda x : x[0], reverse=False)
return (leftSubDataSet,rightSubDataSet)
def calcInfoGain(self,dataSet):
lable1_sum = 0
total_sum = 0
infoGain = 0
if dataSet == []:
pass
else :
for i in range(len(dataSet)):
lable1_sum += dataSet[i][1]
total_sum += dataSet[i][1] + dataSet[i][2]
p1 = (lable1_sum*1.0) / (total_sum*1.0)
p0 = 1 - p1
if p1 == 0 or p0 == 0:
infoGain = 0
else:
infoGain = - p0 * log(p0) - p1 * log(p1)
return infoGain,total_sum
def getMaxInfoGain(self,dataSet):
gainList = []
totalGain = self.calcInfoGain(dataSet)
maxGain = 0
maxGainIdx = 0
for i in range(len(dataSet)):
leftSubDataSet_info = self.calcInfoGain(self.splitDataSet(dataSet, i)[0])
rightSubDataSet_info = self.calcInfoGain(self.splitDataSet(dataSet, i)[1])
gainList.append(totalGain[0]
- ((leftSubDataSet_info[1]*1.0)/(totalGain[1]*1.0)) * leftSubDataSet_info[0]
- ((rightSubDataSet_info[1]*1.0)/(totalGain[1]*1.0)) * rightSubDataSet_info[0])
maxGain = max(gainList)
maxGainIdx = gainList.index(max(gainList))
splitPoint = dataSet[maxGainIdx][0]
return splitPoint,maxGain,maxGainIdx
def getSplitPointList(self,dataSet,maxdeeps,begindeep):
if begindeep >= maxdeeps:
pass
else:
maxInfoGainList = self.getMaxInfoGain(dataSet)
if maxInfoGainList[1] <= self.minInfoGain_epos:
pass
else:
self.splitPiontsList.append(maxInfoGainList[0])
begindeep += 1
subDataSet = self.splitDataSet(dataSet, maxInfoGainList[2])
self.getSplitPointList(subDataSet[0],maxdeeps,begindeep)
self.getSplitPointList(subDataSet[1],maxdeeps,begindeep)
def fit(self, x, y,deep = 3, epos = 1e-8):
self.minInfoGain_epos = epos
self.tree_deep = deep
bin_dict = {}
bin_list = []
for i in range(len(x)):
pos = x[i]
target = y[i]
bin_dict.setdefault(pos,[0,0])
if target == 1:
bin_dict[pos][0] += 1
else:
bin_dict[pos][1] += 1
for key ,val in bin_dict.items():
t = [key]
t.extend(val)
bin_list.append(t)
bin_list.sort( key=lambda x : x[0], reverse=False)
self.getSplitPointList(bin_list,self.tree_deep,0)
self.splitPiontsList = [elem for elem in self.splitPiontsList if elem != []]
self.splitPiontsList.sort()
def transform(self,x):
res = []
for e in x :
index = self.get_Discretization_index(self.splitPiontsList, e)
res.append(index)
return res
def get_Discretization_index(self, Discretization_vals, val):
index = len(Discretization_vals) + 1
for i in range(len(Discretization_vals)):
bin_val = Discretization_vals[i]
if val <= bin_val:
index = i + 1
break
return index
無監督分箱方法(一般不推薦,好不好用,得看人品,一般比卡方和決策樹的效果要差點)
等距劃分:
從最小值到最大值之間,均分為 N 等份, 這樣, 如果 A,B 為最小最大值, 則每個區間的長度為 W=(B−A)/N , 則區間邊界值為A+W,A+2W,….A+(N−1)W 。這裡只考慮邊界,每個等份裡面的例項數量可能不等。等頻分箱:
區間的邊界值要經過選擇,使得每個區間包含大致相等的例項數量。比如說 N=10 ,每個區間應該包含大約10%的例項。比較:
比如,等寬區間劃分,劃分為5區間,最高工資為50000,則所有工資低於10000的人都被劃分到同一區間。等頻區間可能正好相反,所有工資高於50000的人都會被劃分到50000這一區間中。這兩種演算法都忽略了例項所屬的型別,落在正確區間裡的偶然性很大。
對特徵進行分箱後,需要對分箱後的每組(箱)進行woe編碼,然後才能放進模型訓練。
有監督分箱方法
Best-KS(非常類似決策樹的分箱,決策樹分箱的標準是基尼指數,這裡就只考慮KS值):
讓分箱後組別的分佈的差異最大化。
步驟:對於連續變數- 排序
- 計算每一點的KS值
- 選取最大的KS值對應的特徵值,用該特徵值將特徵分為大於該值和小於該值兩端
- 對於每一部分,迴圈b、c步驟,直到滿足終止條件
終止條件,繼續回滾到上一步:
- 下一步分箱,最小的箱的佔比低於設定的閾值(0.05)
- 下一步分箱後,有一箱的對應的y的類別全部為0或者1
- 下一步分箱後,bad rate不單調
步驟:對於離散很高的分類變數
- 編碼(類別變數個數很多,先編碼,再分箱。)
- 依據連續變數的方式進行分箱
分箱以後變數必須單調,具體的例子如下圖:
假定變數被分成了6個箱,假定X軸為年齡,Y軸為壞樣本率,這樣就可以解釋了,年齡越大,壞客戶的比例約多。如果分箱之後不單調,那麼模型在這個變數上的可解釋性就成問題了。所以在分箱期間要注意變數的單調性。
卡方分箱:
這裡copy一段官方解釋(比較長):自底向上的(即基於合併的)資料離散化方法。它依賴於卡方檢驗:具有最小卡方值的相鄰區間合併在一起,直到滿足確定的停止準則。通俗的講,即讓組內成員相似性強,讓組間的差異大。基本思想:對於精確的離散化,相對類頻率在一個區間內應當完全一致。因此,如果兩個相鄰的區間具有非常類似的類分佈,則這兩個區間可以合併;否則,它們應當保持分開。而低卡方值表明它們具有相似的類分佈。
忘記上面,直接實踐一下,步驟如下:
- 預先我們設定一個卡方的閾值
- 根據離散化的屬性對例項進行排序,每個例項屬於一個區間
- 開始合併,具體分2步:
- 計算每一對相鄰區間的卡方數值
- 卡方值最小的一對區間直接合並
:第i區間第j類的例項的數量
的期望頻率,為,N是總樣本,是第i組的樣本數,是第j類樣本在全體中的比例
接下來就百度一下卡方檢驗閾值,直接看裡面的數值,找到顯著水平和自由度,自由度為2,90%置信度的情況下,卡方為4.6;如果忘記了卡方檢驗的意義,直接百度卡方檢驗。
目前一般分箱5個或者6個,置信度在0.95左右,區間為10-15之間。主要是因為分箱太多,操作起來太麻煩,對模型的提高也不大,分箱5個一般就不錯。
卡方分箱的終止條件很簡單,基本就是2條:
- 預設分到多少箱,如果已經分到了這個數值了,那就第2步
- 檢查一下單調性,滿足就完成分箱了,如果不滿足,相鄰的箱就合併,直到單調了為止,因為最後合併到2個箱的時候,是一定單調的。
- 補充:分箱之前要切分,通常50-100個切分點,看資料量的大小,最最最重要的,千萬不要用等距劃分,因為比如收入、年齡這些欄位成偏態分步,資料沒有平均分佈,要用等頻劃分。
- 類別變數,類別較少,就不用在分箱了,如果有那個類別是全部為壞樣本,需要和最小的不是壞樣本的合併一下,因為不合並等會WOE不能計算了。
- 最後補充:在評分卡模型中,能不用熱編碼就不要用熱編碼,因為熱編碼膨脹了資料量,在選擇變數是不是進入模型當中去,也是存在問題了,例如逐步迴歸就不好搞,業務方面的解釋性也差,沒直接的業務邏輯關係。總之,能不用就不用,要是沒變量了,還是可以考慮用一下。
3、WOE編碼
WOE編碼官方解釋:一種有監督的編碼方式,將預測類別的集中度的屬性作為編碼的數值;優勢是:將特徵的值規範到相近的尺度上。缺點是:需要分箱後每箱都同時有好壞樣本(例如,預測違約和不違約可是使用WOE編碼,如果去預測中度違約、重度違約、輕度違約等等情況,這個時候WOE編碼就不行了)。通常意義上,WOE的絕對值在0.1-3之間。
編碼的意義在於符號與好樣本的比例有關;當好樣本為分子,壞樣本為分母的時候,可以要求迴歸模型的係數為負。
具體的WOE編碼這裡就不找材料了,CSDN部落格上,有很多寫的很好的,這裡引用一篇部落格在這裡,請猛擊。
這裡簡單引用一下其他人成熟的比較正式說法,WOE公式如下:
例如,以年齡作為一個變數,由於年齡是連續型自變數,需要對其進行離散化處理,假設離散化分為5組(如何分箱,上面已經介紹,後面將繼續介紹),#bad和#good表示在這五組中違約使用者和正常使用者的數量分佈,最後一列是woe值的計算,通過後面變化之後的公式可以看出,woe反映的是在自變數每個分組下違約使用者對正常使用者佔比和總體中違約使用者對正常使用者佔比之間的差異;從而可以直觀的認為woe蘊含了自變數取值對於目標變數(違約概率)的影響。再加上woe計算形式與logistic迴歸中目標變數的logistic轉換(logist_p=ln(p/1-p))如此相似,因而可以將自變數woe值替代原先的自變數值;,具體的計算情況如下:
Age | bad | good | WOE |
---|---|---|---|
0-10 | 50 | 200 | =ln((50/100)/(200/1000))=ln((50/200)/(100/1000)) |
10-18 | 20 | 200 | =ln((20/100)/(200/1000))=ln((20/200)/(100/1000)) |
18-35 | 5 | 200 | =ln((5/100)/(200/1000))=ln((5/200)/(100/1000)) |
35-50 | 15 | 200 | =ln((15/100)/(200/1000))=ln((15/200)/(100/1000)) |
50以上 | 10 | 200 | =ln((10/100)/(200/1000))=ln((10/200)/(100/1000)) |
彙總 | 100 | 1000 |
4、IV值
IV值的官方解釋為:IV(Information Value), 衡量特徵包含預測變數濃度的一種指標。
計算公式如下: