python信用評分卡建模(附代碼,博主錄制)
GBDT模型用於評分卡模型
https://blog.csdn.net/LuYi_WeiLin/article/details/88397303 轉載
本文主要總結以下內容:
GBDT模型基本理論介紹
GBDT模型如何調參數
GBDT模型對樣本違約概率進行估計(GBDT模型用於評分卡python代碼實現請看下一篇博客)
GBDT模型挑選變量重要性
GBDT模型如何進行變量的衍生
GBDT模型基本理論介紹
GBDT(Gradient Boosting Decision Tree) 又叫 MART(Multiple Additive Regression Tree),是一種迭代的決策樹算法,該算法由多棵決策樹組成,所有樹的結論累加起來做最終答案。它在被提出之初就和SVM一起被認為是泛化能力較強的算法。GBDT中的樹是回歸樹(不是分類樹),GBDT用來做回歸預測,調整后也可以用於分類。
GBDT模型是集成學習框架Boosting的一種
一句話解釋版本:
Bagging是決策樹的改進版本,通過擬合很多決策樹來實現降低方差
隨機森林(Random Forrest)是Bagging的改進版本,通過限制節點可選特征范圍優化Bagging
Boosting是Bagging的改進版本,通過吸取之前樹的經驗建立后續樹優化Bagging
Boosting模型工作原理

- GBDT模型的原理
Y標簽類別可以是(連續型[基模型:回歸]、離散無序型[基模型:分類]、離散有序型[基模型:排序])

不同的Y標簽類型選擇不一樣的損失函數,上式中損失函數L(F(X),Y),其中Y是固定的,F(X)的針對不同問題,函數結構也是可以定下來的,唯一要確定的是函數所對應的參數使得該損失函數最小,所以我們把問題從函數空間搜索問題轉換為參數空間搜索問題
常見的損失函數(針對不同Y標簽類型選擇不一樣的損失函數)
Y標簽類別可以是(連續型[基模型:回歸]、離散無序型[基模型:分類]、離散有序型[基模型:排序])
下面三個分別對應:回歸型損失函數、分類型損失函數、邏輯回歸損失函數(當然了這里只是舉了常見的三個,比如回歸型損失函數我們也可以使用均方差等損失函數,這里不過多去展開)
針對GBDT模型我們如何尋找最優參數呢?

運用最優化的思想,采用迭代的方法求出其參數(因為現實中是很難求其精確解的,就算可以求解也要花費大量的時間,我們可以問題簡化,求出P的近似解即可,無限的逼近到一個我們可以接受的范圍即可),如何迭代呢?我們先了解一下梯度法
- 梯度法

梯度提升法

可能看到這里有的小伙伴已經雲里霧里了,說的是什么呢?不急我們先來引用一下別人的例子(https://blog.csdn.net/zhangbaoanhadoop/article/details/81840669文章不錯,建議大家去看一下)
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
讓損失函數沿着梯度方向的下降。這個就是gbdt 的 gb的核心。gbdt 每輪迭代的時候,都去擬合損失函數在當前模型下的負梯度。(如果損失函數使用的是平方誤差損失函數,則這個損失函數的負梯度就可以用殘差來代替,以下所說的殘差擬合,便是使用了平方誤差損失函數)
Boosting,迭代,即通過迭代多棵樹來共同決策。這怎么實現呢?難道是每棵樹獨立訓練一遍,比如A這個人,第一棵樹認為是10歲,第二棵樹認為是0歲,第三棵樹認為是20歲,我們就取平均值10歲做最終結論?--當然不是!且不說這是投票方法並不是GBDT,只要訓練集不變,獨立訓練三次的三棵樹必定完全相同,這樣做完全沒有意義。之前說過,GBDT是把所有樹的結論累加起來做最終結論的,所以可以想到每棵樹的結論並不是年齡本身,而是年齡的一個累加量。GBDT的核心就在於,每一棵樹學的是之前所有樹結論和的殘差,這個殘差就是一個加預測值后能得真實值的累加量。比如A的真實年齡是18歲,但第一棵樹的預測年齡是12歲,差了6歲,即殘差為6歲。那么在第二棵樹里我們把A的年齡設為6歲去學習,如果第二棵樹真的能把A分到6歲的葉子節點,那累加兩棵樹的結論就是A的真實年齡;如果第二棵樹的結論是5歲,則A仍然存在1歲的殘差,第三棵樹里A的年齡就變成1歲,繼續學。
還是年齡預測,簡單起見訓練集只有4個人,A,B,C,D,他們的年齡分別是14,16,24,26。其中A、B分別是高一和高三學生;C,D分別是應屆畢業生和工作兩年的員工。如果是用一棵傳統的回歸決策樹來訓練,會得到如下圖2所示結果:
圖2:傳統回歸決策樹
現在我們使用GBDT來做這件事,由於數據太少,我們限定葉子節點做多有兩個,即每棵樹都只有一個分枝,並且限定只學兩棵樹。我們會得到如下圖3所示結果:

圖3:GBDT模型
在第一棵樹分枝和圖2一樣,由於A,B年齡較為相近,C,D年齡較為相近,他們被分為兩撥,每撥用平均年齡作為預測值。此時計算殘差(殘差的意思就是: A的預測值 + A的殘差 = A的實際值),所以A的殘差就是16-15=1(注意,A的預測值是指前面所有樹累加的和,這里前面只有一棵樹所以直接是15,如果還有樹則需要都累加起來作為A的預測值)。進而得到A,B,C,D的殘差分別為-1,1,-1,1。然后我們拿殘差替代A,B,C,D的原值,到第二棵樹去學習,如果我們的預測值和它們的殘差相等,則只需把第二棵樹的結論累加到第一棵樹上就能得到真實年齡了。這里的數據顯然是我可以做的,第二棵樹只有兩個值1和-1,直接分成兩個節點。此時所有人的殘差都是0,即每個人都得到了真實的預測值。
換句話說,現在A,B,C,D的預測值都和真實年齡一致了。Perfect!:
A: 14歲高一學生,購物較少,經常問學長問題;預測年齡A = 15 – 1 = 14
B: 16歲高三學生;購物較少,經常被學弟問問題;預測年齡B = 15 + 1 = 16
C: 24歲應屆畢業生;購物較多,經常問師兄問題;預測年齡C = 25 – 1 = 24
D: 26歲工作兩年員工;購物較多,經常被師弟問問題;預測年齡D = 25 + 1 = 26
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
看完例子,想必大家已經基本了解GBDT的原理了,那我們下面看梯度提升法在不同損失函數用法
把梯度提升法的思想帶入邏輯回歸損失函數
把梯度提升法的思想帶入分類類型的損失函數

GBDT模型如何調整參數
GBDT該調整哪些參數
一般集成模型的參數分為兩種類別:
- 框架層面參數
- 基模型層面參數
當然啦,GBDT模型框架層面參數和基模型層面參數是有非常多參數的,經驗得出我們只需要關注以下幾個變量即可(當然,特殊情況特殊處理)
- 框架層面參數

基模型層面參數


如何進行參數調節
我們使用到的方法是:K折交叉驗證(CV)

其優缺點:

基於k折交叉驗證的網格搜索法(GridSearchCV)

每變一個參數,模型都要擬合一遍,大樣本就會很卡。使用貪心算法,把n1*n2*n3*n4*......*nk復雜度變為n1+n2+n3+n4+......+nk,求其次優解。按照變量(框架層面參數、基模型層面參數)的重要程度來順序調參。
GBDT模型對樣本違約概率進行估計
代碼請看下一篇博客
GBDT模型挑選變量重要性
集成模型幾乎都能給出變量重要性的估計,因為有抽樣成分在里面
GBDT模型如何進行變量的衍生
獨熱編碼
在講解變量衍生之前我們來了解一下獨熱編碼(引用一下別人的博客介紹一下獨熱編碼)
----------------------------------------------------------------------------------------------
原文:https://blog.csdn.net/windowsyun/article/details/78277880
簡單介紹
有一組數據,其中有個特征是性別。既然是性別,那它的值顯然只有兩個選擇,要么男性(用1表示)要么女性(用0表示)。
獨熱編碼就是將這一個特征變成兩個特征:是男性、是女性。
我是男的,我的特征就變成了 [1, 0],1代表我是男的,0代表我不是女的。同樣,女性的特征變為[0, 1]。
用處
為什么用獨熱編碼?
假設一個特征是顏色,選項有:黃色、紅色、綠色等等。如果我們不采用獨熱編碼,用0表示黃色,用1表示綠色,用2表示紅色,以此類推。從數學上看,它們之間的距離不一樣了,0和1的距離顯然比0和2的距離小,可是不能認為黃色與紅色的關系比綠色更接近。
采用獨熱編碼后,黃色變成[1, 0, 0 , … ],紅色變成[0, 1, 0, … ],綠色變成[0, 0, 1, … ],這樣它們的相似度就一樣了,這對機器學習算法很重要。
獨熱編碼注意事項



python代碼如下
import pandas as pd
import time
import numpy as np
import re
from sklearn.ensemble import GradientBoostingClassifier
from sklearn import cross_validation, metrics
from sklearn.model_selection import GridSearchCV, train_test_split
import matplotlib.pylab as plt
import datetime
from dateutil.relativedelta import relativedelta
from numpy import log
from sklearn.metrics import roc_auc_score
from sklearn.feature_extraction import DictVectorizer
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model.logistic import LogisticRegression
def CareerYear(x):
#對工作年限進行轉換
if str(x).find('nan') > -1:
return -1
elif str(x).find("10+")>-1: #將"10+years"轉換成 11
return 11
elif str(x).find('< 1') > -1: #將"< 1 year"轉換成 0
return 0
else:
return int(re.sub("\D", "", x)) #其余數據,去掉"years"並轉換成整數
def DescExisting(x):
#將desc變量轉換成有記錄和無記錄兩種
if type(x).__name__ == 'float':
return 'no desc'
else:
return 'desc'
def ConvertDateStr(x):
mth_dict = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10,
'Nov': 11, 'Dec': 12}
if str(x) == 'nan':
return datetime.datetime.fromtimestamp(time.mktime(time.strptime('9900-1','%Y-%m')))
#time.mktime 不能讀取1970年之前的日期
else:
yr = int(x[4:6])
if yr <=17:
yr = 2000+yr
else:
yr = 1900 + yr
mth = mth_dict[x[:3]]
return datetime.datetime(yr,mth,1)
def MonthGap(earlyDate, lateDate):
if lateDate > earlyDate:
gap = relativedelta(lateDate,earlyDate)
yr = gap.years
mth = gap.months
return yr*12+mth
else:
return 0
def MakeupMissing(x):
if np.isnan(x):
return -1
else:
return x
'''
第一步:數據准備
'''
folderOfData = foldOfData = 'H:/'
allData = pd.read_csv(folderOfData + '數據集.csv',header = 0, encoding = 'latin1',engine ='python')
allData['term'] = allData['term'].apply(lambda x: int(x.replace(' months','')))
# 處理標簽:Fully Paid是正常用戶;Charged Off是違約用戶
allData['y'] = allData['loan_status'].map(lambda x: int(x == 'Charged Off'))
'''
由於存在不同的貸款期限(term),申請評分卡模型評估的違約概率必須要在統一的期限中,且不宜太長,所以選取term=36months的行本
'''
allData1 = allData.loc[allData.term == 36]
trainData, testData = train_test_split(allData1,test_size=0.4)
'''
第二步:數據預處理
'''
# 將帶%的百分比變為浮點數
trainData['int_rate_clean'] = trainData['int_rate'].map(lambda x: float(x.replace('%',''))/100)
# 將工作年限進行轉化,否則影響排序
trainData['emp_length_clean'] = trainData['emp_length'].map(CareerYear)
# 將desc的缺失作為一種狀態,非缺失作為另一種狀態
trainData['desc_clean'] = trainData['desc'].map(DescExisting)
# 處理日期。earliest_cr_line的格式不統一,需要統一格式且轉換成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))
'''
第三步:變量衍生
'''
# 考慮申請額度與收入的占比
trainData['limit_income'] = trainData.apply(lambda x: x.loan_amnt / x.annual_inc, axis = 1)
# 考慮earliest_cr_line到申請日期的跨度,以月份記
trainData['earliest_cr_to_app'] = trainData.apply(lambda x: MonthGap(x.earliest_cr_line_clean,x.app_date_clean), axis = 1)
'''
對於類別型變量,需要onehot(獨熱)編碼,再訓練GBDT模型
'''
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']
#獨熱編碼
v = DictVectorizer(sparse=False)
X1 = v.fit_transform(trainData[cat_features].to_dict('records'))
#將獨熱編碼和數值型變量放在一起進行模型訓練
X2 = np.matrix(trainData[num_features])
X = np.hstack([X1,X2])
y = trainData['y']
# 未經調參進行GBDT模型訓練
gbm0 = GradientBoostingClassifier(random_state=10)
gbm0.fit(X,y)
y_pred = gbm0.predict(X)
y_predprob = gbm0.predict_proba(X)[:,1].T
print("Accuracy : %.4g" % metrics.accuracy_score(y, y_pred))
print("AUC Score (Train): %f" % metrics.roc_auc_score(np.array(y.T), y_predprob))
'''
第四步:在測試集上測試模型的性能
'''
# 將帶%的百分比變為浮點數
testData['int_rate_clean'] = testData['int_rate'].map(lambda x: float(x.replace('%',''))/100)
# 將工作年限進行轉化,否則影響排序
testData['emp_length_clean'] = testData['emp_length'].map(CareerYear)
# 將desc的缺失作為一種狀態,非缺失作為另一種狀態
testData['desc_clean'] = testData['desc'].map(DescExisting)
# 處理日期。earliest_cr_line的格式不統一,需要統一格式且轉換成python的日期
testData['app_date_clean'] = testData['issue_d'].map(lambda x: ConvertDateStr(x))
testData['earliest_cr_line_clean'] = testData['earliest_cr_line'].map(lambda x: ConvertDateStr(x))
# 處理mths_since_last_delinq。注意原始值中有0,所以用-1代替缺失
testData['mths_since_last_delinq_clean'] = testData['mths_since_last_delinq'].map(lambda x:MakeupMissing(x))
testData['mths_since_last_record_clean'] = testData['mths_since_last_record'].map(lambda x:MakeupMissing(x))
testData['pub_rec_bankruptcies_clean'] = testData['pub_rec_bankruptcies'].map(lambda x:MakeupMissing(x))
# 考慮申請額度與收入的占比
testData['limit_income'] = testData.apply(lambda x: x.loan_amnt / x.annual_inc, axis = 1)
# 考慮earliest_cr_line到申請日期的跨度,以月份記
testData['earliest_cr_to_app'] = testData.apply(lambda x: MonthGap(x.earliest_cr_line_clean,x.app_date_clean), axis = 1)
#用訓練集里的onehot編碼方式進行編碼
X1_test = v.transform(testData[cat_features].to_dict('records'))
X2_test = np.matrix(testData[num_features])
X_test = np.hstack([X1_test,X2_test])
y_test = np.matrix(testData['y']).T
### 計算KS值
def KS(df, score, target):
'''
:param df: 包含目標變量與預測值的數據集,dataframe
:param score: 得分或者概率,str
:param target: 目標變量,str
:return: KS值
'''
total = df.groupby([score])[target].count()
bad = df.groupby([score])[target].sum()
all = pd.DataFrame({'total':total, 'bad':bad})
all['good'] = all['total'] - all['bad']
all[score] = all.index
all = all.sort_values(by=score,ascending=False)
all.index = range(len(all))
all['badCumRate'] = all['bad'].cumsum() / all['bad'].sum()
all['goodCumRate'] = all['good'].cumsum() / all['good'].sum()
KS = all.apply(lambda x: x.badCumRate - x.goodCumRate, axis=1)
return max(KS)
#在測試集上測試GBDT性能
y_pred = gbm0.predict(X_test)
y_predprob = gbm0.predict_proba(X_test)[:,1].T
testData['predprob'] = list(y_predprob)
print("Accuracy : %.4g" % metrics.accuracy_score(y_test, y_pred))
print("AUC Score (Test): %f" % metrics.roc_auc_score(np.array(y_test)[:,0], y_predprob))
print("KS is :%f" % KS(testData, 'predprob', 'y'))
'''
GBDT調參
'''
# 1, 選擇較小的步長(learning rate)后,對迭代次數(n_estimators)進行調參
X = pd.DataFrame(X)
param_test1 = {'n_estimators':range(80,81,10)}
gsearch1 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, min_samples_split=30,min_samples_leaf=5,max_depth=8,max_features='sqrt', subsample=0.8,random_state=10),param_grid = param_test1, scoring='roc_auc',iid=False,cv=5)
gsearch1.fit(X,y)
gsearch1.best_params_, gsearch1.best_score_
best_n_estimator = gsearch1.best_params_['n_estimators']
# 2, 對決策樹最大深度max_depth和內部節點再划分所需最小樣本數min_samples_split進行網格搜索
param_test2 = {'max_depth':range(3,4), 'min_samples_split':range(6,7)}
gsearch2 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=best_n_estimator, min_samples_leaf=20, max_features='sqrt', subsample=0.8, random_state=10),param_grid = param_test2, scoring='roc_auc',iid=False, cv=5)
gsearch2.fit(X,y)
gsearch2.best_params_, gsearch2.best_score_
best_max_depth = gsearch2.best_params_['max_depth']
#3, 再對內部節點再划分所需最小樣本數min_samples_split和葉子節點最少樣本數min_samples_leaf一起調參
param_test3 = {'min_samples_split':range(80,81,10), 'min_samples_leaf':range(50,51,5)}
gsearch3 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=best_n_estimator,max_depth=best_max_depth,max_features='sqrt', subsample=0.8, random_state=10),param_grid = param_test3, scoring='roc_auc',iid=False, cv=5)
gsearch3.fit(X,y)
gsearch3.best_params_, gsearch3.best_score_
best_min_samples_split, best_min_samples_leaf = gsearch3.best_params_['min_samples_split'],gsearch3.best_params_['min_samples_leaf']
#4, 對最大特征數max_features進行網格搜索
param_test4 = {'max_features':range(int(np.sqrt(X.shape[0])),int(np.sqrt(X.shape[0]))+1,5)}
gsearch4 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=best_n_estimator,max_depth=best_max_depth, min_samples_leaf =best_min_samples_leaf,min_samples_split =best_min_samples_split, subsample=0.8, random_state=10),param_grid = param_test4, scoring='roc_auc',iid=False, cv=5)
gsearch4.fit(X,y)
gsearch4.best_params_, gsearch4.best_score_
best_max_features = gsearch4.best_params_['max_features']
#5, 對采樣比例進行網格搜索
param_test5 = {'subsample':[0.6+i*0.05 for i in range(1)]}
gsearch5 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=best_n_estimator,max_depth=best_max_depth,min_samples_leaf =best_min_samples_leaf, max_features=best_max_features,random_state=10),param_grid = param_test5, scoring='roc_auc',iid=False, cv=5)
gsearch5.fit(X,y)
gsearch5.best_params_, gsearch5.best_score_
best_subsample = gsearch5.best_params_['subsample']
gbm_best = GradientBoostingClassifier(learning_rate=0.1, n_estimators=best_n_estimator,max_depth=best_max_depth,min_samples_leaf =best_min_samples_leaf, max_features=best_max_features,subsample =best_subsample, random_state=10)
gbm_best.fit(X,y)
#在測試集上測試並計算性能
y_pred = gbm_best.predict(X_test)
y_predprob = gbm_best.predict_proba(X_test)[:,1].T
testData['predprob'] = list(y_predprob)
#准確性
print("Accuracy : %.4g" % metrics.accuracy_score(y_test, y_pred))
print("AUC Score (Test): %f" % metrics.roc_auc_score(np.array(y_test)[:,0], y_predprob))
print("KS is :%f"%KS(testData, 'predprob', 'y'))
###########概率轉換為分數########################
def Prob2Score(prob, basePoint, PDO):
#將概率轉化成分數且為正整數
y = np.log(prob/(1-prob))
return int(basePoint+PDO/np.log(2)*(-y))
basePoint = 250
PDO = 50
testData['score'] = testData['predprob'].map(lambda x:Prob2Score(x, basePoint, PDO))
testData = testData.sort_values(by = 'score')
#畫出分布圖
plt.hist(testData['score'], 100)
plt.xlabel('score')
plt.ylabel('freq')
plt.title('distribution')
python金融風控評分卡模型和數據分析微專業課(博主親自錄制視頻):http://dwz.date/b9vv
微信掃二維碼,免費學習更多python資源



