利用logistic回归构建申请信用评级案例
申請評分卡對于從事信貸風控行業的人來說肯定不陌生,甚至每天都會應用到。申請評分,即對申請客戶打分,對于業務專家來說可以是基于經驗對客戶資質進行評估,最終決定是否給予通過申請,首先基于經驗的評估很難量化,可能還受各種主觀因素的影響導致評估標準頻繁波動,而基于數據的評估是直接以分數的形式來展現,更容易進行比較,且在建立好模型之后,這套評分標準就已經確定,除非重新構建模型,穩定性更勝一籌。這篇文章主要介紹自己的一些理解以及建立評分卡的過程,第一次嘗試建模,如有錯誤的地方,請指出。
申請評分卡
- 評分卡中的一種,其他還有行為評分卡、催收評分卡等
- 原理是基于客戶在過去某個時間點截止到本次貸款或信用卡申請時的各項數據,預測其未來某一段時間內的違約概率,而評分則是以分數的形式來體現這個違約概率,即違約概率越高,對應的評分越低
- 過去某個時間點截止到本次申請即觀察期,未來某一段時間即表現期
Logistic回歸
- 二分類問題中常用的一種算法,具體內容:機器學習算法系列(一):Logistic回歸
本次建模流程目錄
一、數據獲取與目標變量定義
二、探索性數據分析
三、數據預處理
四、特征工程
五、建立模型
六、模型評估
七、建立評分卡
八、拒絕推論
一、數據獲取與目標變量定義
- 導入需要用到的python庫
?
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from datetime import datetime from mpl_toolkits.mplot3d import Axes3D import woe import woe.feature_process as fp import woe.eval as eva from statsmodels.stats.outliers_influence import variance_inflation_factor from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegressionCV from sklearn.metrics import roc_curve,auc import warnings warnings.filterwarnings('ignore') %matplotlib inline- 數據獲取:取自美國一家P2P平臺Lending Club的官網,選取2017年三季度-2018年二季度的放貸數據,數據更新至2019年5月
?
data17Q3=pd.read_csv('C:\\Users\\honglihui\\Desktop\\LendingClubLoanData-UpdateTime201905\\LoanStats_2017Q3.csv',low_memory=False,header=1) data17Q4=pd.read_csv('C:\\Users\\honglihui\\Desktop\\LendingClubLoanData-UpdateTime201905\\LoanStats_2017Q4.csv',low_memory=False,header=1) data18Q1=pd.read_csv('C:\\Users\\honglihui\\Desktop\\LendingClubLoanData-UpdateTime201905\\LoanStats_2018Q1.csv',low_memory=False,header=1) data18Q2=pd.read_csv('C:\\Users\\honglihui\\Desktop\\LendingClubLoanData-UpdateTime201905\\LoanStats_2018Q2.csv',low_memory=False,header=1) data=pd.concat([data17Q3,data17Q4,data18Q1,data18Q2],ignore_index=True)#總數據集?
data.info()?
<共48萬條數據,144個變量,其中float64類變量107個,object類變量37個>
?
- 目標變量定義:
建立模型預測客戶在未來一段時間內是否會違約,那必然需要先定義違約,逾期多長時間算違約,不同業務標準不同。與數據集中還款表現loan_status字段相關,作為目標變量,剩下的字段則作為解釋變量 - 還款狀態數據分布:
?
data['loan_status'].groupby(data.loan_status).count()正常還款-M1為5%
M1-M2為30%
M2-M3為70%
M3-M4為85%
M4-M5為98%
可定義表現期內出現M3一次及以上、M2兩次及以上為壞客戶,
定義表現期內正常還款、M1不超過一次的為好客戶,剩下的定義為不確定,從數據集中剔除;本次數據集缺少歷史詳細逾期數據,因此這里基于業務直接采取如下定義:其中壞客戶標記為1,好客戶標記為0,不確定的標記為Undefined:
?
loan_status_dict={'Charged Off':1,'Default':1,'Late (31-120 days)':1,'Fully Paid':0,'Current':0, 'In Grace Period':'Undefined','Late (16-30 days)':'Undefined'}替換原始還款狀態為標記值{0,1,Undefined}:
?
data.loan_status.replace(to_replace=loan_status_dict,inplace=True)值替換后的貸款狀態字段:
?
data['loan_status'].groupby(data.loan_status).count()更新數據,只保留{0,1}數據,并修改目標變量名為target:
?
data=data[data.loan_status.isin([1,0])] data.rename(columns={'loan_status':'target'},inplace=True)重置index:
?
data.reset_index(drop=True,inplace=True)剩余475404組數據:
?
data.info()- 好壞客戶分布與占比:
?
sns.set_style('darkgrid') plt.rcParams['font.sans-serif']='Microsoft YaHei' plt.rcParams['figure.dpi']=150plt.subplot(121) bad=data.target.sum() #正樣本量 good=data.shape[0]-bad #負樣本量 sns.barplot(x=['good','bad'],y=[good,bad]) plt.text(0,good+10000,good,ha='center',va='baseline',fontsize=10) plt.text(1,bad+10000,bad,ha='center',va='baseline',fontsize=10)plt.subplot(122) bad_ratio=data.target.sum()/data.shape[0]#正樣本占比 good_ratio=1-bad#負樣本占比 plt.pie([good,bad],labels=['good','bad'],autopct='%1.1f%%',startangle=30,explode=[0,0.3],textprops={'fontsize':10},shadow=True) plt.axis('equal')好壞客戶分布與占比
二、探索性數據分析
- 數據基本分布
?
sns.barplot(x=('2017Q3','2017Q4','2018Q1','2018Q2'),y=[data17Q3.shape[0],data17Q4.shape[0],data18Q1.shape[0],data18Q2.shape[0]])?
各季度放款量
<各季度放款量基本一致,在12萬左右>
?
?
draw=data.copy()#用于作圖的數據副本 sns.distplot(a=draw.loan_amnt,bins=20)?
貸款金額分布
<申請金額均不超過4萬,小額貸款占多數,金額分布主要集中在5000-20000>
?
?
draw['term']=draw['term'].str.replace('months',' ').str.strip().astype('int') sns.violinplot(x=draw['term'],y=draw['loan_amnt'])?
借款期限、貸款金額分布
<期限有36期和60期,36期的金額主要在10000及以下,60期的金額主要在10000以上>
?
?
draw['int_rate']=draw['int_rate'].str.replace('%',' ').str.strip().astype('float')plt.subplot(211) sns.violinplot(x=draw['grade'],y=draw['loan_amnt'])plt.subplot(212) sns.violinplot(x=draw['grade'],y=draw['int_rate']) plt.ylabel('int_rate(%)',fontdict={'fontsize':11},labelpad=20)?
借款金額、利率隨客戶等級的變化
<A/B/C等級客戶申請金額主要集中在10000左右,F/G等級客戶申請金額分布較其他等級的要高>
<借款利率大致隨等級的降低而增加,F等級利率分布比較特殊,這可能與它的借款金額有一定關系,主要集中在10000以上,對應的借款期限多為60期,利率也會偏高>
?
?
draw['emp_length']=draw.emp_length.str.replace('years',' ').str.strip() draw['emp_length']=draw.emp_length.str.replace('year',' ').str.strip() #data['emp_length']=data.emp_length.replace({'10+':'10','< 1':'0','n/a':'-1'}).astype('float64')plt.figure(figsize=(8,8))plt.subplot(221) el=draw.emp_length.groupby(draw.emp_length).count() sns.barplot(x=el.index,y=el,order=['n/a','< 1','1','2','3','4','5','6','7','8','9','10+']) plt.ylabel('count')plt.subplot(222) ho=draw.home_ownership.groupby(draw.home_ownership).count() ho_ratio=(ho/draw.shape[0]).sort_values(ascending=False) plt.pie(ho_ratio) plt.legend(ho_ratio.index,loc=9,ncol=2,labelspacing=0.05,columnspacing=10) plt.text(-0.5,-1.2,'home_ownership',fontsize=10)plt.subplot(212) pp=draw.purpose.groupby(draw.purpose).count() pp_ratio=(pp/draw.purpose.shape[0]).sort_values(ascending=False) plt.pie(pp_ratio) plt.legend(pp_ratio.index,loc=0,labelspacing=0.05) plt.text(-0.2,-1.2,'purpose',fontsize=10)plt.axis('equal')?
工作年限、住房類型、貸款用途分布
<客戶群體工作年限在10年以上占多數>
<住房類型主要有按揭、租住、自有三種>
<貸款用途主要集中在債務合并、償還信用卡、其他、房屋裝修四種>
?
?
plt.figure(figsize=(11,4))plt.subplot(121) sns.violinplot(draw.annual_inc)plt.subplot(122) sns.violinplot(draw.annual_inc[draw.annual_inc<=200000])?
年收入分布
<左圖分析可知群體收入存在較大差異,大部分年收入在20萬以下>
<右圖是20萬及以下的收入分布,可以看出大部分客戶的年收入在5萬左右,中位值65000>
?
- 各個因素與還款表現之間的關系
?
plt.figure(figsize=(8,8))plt.subplot(211) sns.violinplot(x=draw.term,y=draw.loan_amnt,hue=draw.target) plt.legend(loc=0)plt.subplot(212) bad_ratio=draw.target.groupby(draw.term).sum()/draw.target.groupby(draw.term).count() sns.barplot(x=bad_ratio.index,y=bad_ratio,color='#e6daa6') plt.subplots_adjust(hspace=0.1) plt.ylabel('bad_ratio')?
貸款金額、借款期限與還款表現
<好、壞客戶在借款金額分布上沒有明顯的區分度>
<借款期限越長,越可能出現違約>
?
?
bad_ratio=draw.target.groupby(draw.grade).sum()/draw.target.groupby(draw.grade).count()#壞客戶占比 bad_ratio.plot(kind='line') plt.ylabel('bad_ratio')客戶等級與還款表現
<相關性較強,客戶等級越低,還款表現越差>
?
draw['emp_length']=draw.emp_length.replace({'10+':'10','< 1':'0','n/a':'-1'}).astype('float64') grouped=draw.target.groupby(draw.emp_length) bad_ratio=grouped.sum()/grouped.count() bad_ratio.plot() plt.xticks(range(-1,11),('n/a','< 1','1','2','3','4','5','6','7','8','9','10+')) plt.ylabel('bad_ratio')?
工作年限與還款表現
<工作年限大致可以反映一個客戶的還款能力,工作年限越長,還款表現越好>
<工作年限為空值的群體壞客戶占比存在明顯的偏離,猜測這部分群體在申請時未填寫此記錄可能是因為沒有工作>
?
?
draw.dti.fillna(draw.dti.median(),inplace=True)#缺失值處理 dt=draw[['dti','target']][draw.dti<=40] #dti取值主要集中在40以下,取該部分值來分析 grouped=pd.cut(dt.dti,10)#區間分段 bad_ratio=dt['target'].groupby(grouped).sum()/dt['target'].groupby(grouped).count()#壞客戶占比 bad_ratio.plot(figsize=(12,5)) plt.ylabel('bad_ratio')?
負債收入比(dti)與還款表現
<以業務的角度分析,dti越高則對應的還款壓力越大,違約的概率就越高,趨勢圖大致是單調遞增的,但在這個數據集中并不是這樣的分布,可以看出dti在4-16之間的客戶群還款表現最好,而dit<4的群體中壞客戶占比明顯偏高>
?
?
bad_ratio=draw.target.groupby(draw.home_ownership).sum()/draw.target.groupby(draw.home_ownership).count() bad_ratio=bad_ratio[['RENT','MORTGAGE','OWN']] #只選取數據量較多的類別plt.figure(figsize=(15,10))#設置畫布大小point=np.linspace(0, 2*np.pi,bad_ratio.shape[0],endpoint=False) values=bad_ratio.values point=np.concatenate((point,[point[0]])) values=np.concatenate((values,[values[0]]))plt.subplot(121,polar=True)#圖1 plt.plot(point,values) plt.thetagrids(point*180/np.pi,bad_ratio.index) plt.fill(point,values,'g',alpha=0.3) plt.xlabel('HOME_OWNERSHIP',labelpad=27,fontdict={'fontsize':15})#-------------------------------------------------------------------------------------------------bad_ratio=draw.target.groupby(draw.purpose).sum()/draw.target.groupby(draw.purpose).count() bad_ratio.sort_values(inplace=True) index=~bad_ratio.index.isin(['wedding','educational','renewable_energy'])#這三個數據量較少,不考慮在內 bad_ratio=bad_ratio[index]point=np.linspace(0, 2*np.pi,bad_ratio.shape[0],endpoint=False) values=bad_ratio.values point=np.concatenate((point,[point[0]])) values=np.concatenate((values,[values[0]]))plt.subplot(122,polar=True)#圖2 plt.plot(point,values) plt.thetagrids(point*180/np.pi,bad_ratio.index) plt.fill(point,values,'b',alpha=0.3) plt.xlabel('PURPOSE',labelpad=10,fontdict={'fontsize':15})?
住房類型、借款用途與還款表現
<房產類型為按揭的客戶群還款表現好于其他>
<借款用于償還信用卡的客戶群體還款好于其他,而借款用途為經商的風險最高>
?
?
bad=draw.target.groupby(draw.addr_state).sum()#按地區分組的壞客戶 good=draw.target.groupby(draw.addr_state).count()-bad#按地區分組的好客戶 bad_ratio=bad/(bad+good)#各地區壞客戶占比 good.sort_values(ascending=False,inplace=True) bad_ratio.sort_values(ascending=False,inplace=True)plt.figure(figsize=(20,8)) plt.subplot(211)#圖1 plt.bar(good.index,good) plt.bar(bad.index,bad,color='#B22222') plt.legend(['good','bad']) plt.ylabel('count') plt.subplot(212)#圖2 plt.bar(bad_ratio.index,bad_ratio) plt.ylabel('bad_ratio')?
區域成交量分布與還款表現
<成交的客戶主要來自CA(加利福尼亞州)、TX(得克薩斯州)、NY(紐約州)、FL(佛羅里達州),占30%以上>
<還款表現最差的三個州是MS(密西西比州)、AL(亞拉巴馬州)、AR(阿肯色州)>
?
?
fig=plt.figure(figsize=(15,15)) ax=fig.add_subplot(111,projection='3d') draw=draw[draw.annual_inc<'155000']#選取收入分布比較集中的部分 draw_bad=draw[draw.target==1]#選取壞客戶 draw_good=draw[draw.target==0]#選取好客戶ax.scatter(draw_bad.annual_inc,draw_bad.installment,c='#d8863b',zs=0.1) ax.scatter(draw_good.annual_inc,draw_good.installment,c='#49759c') ax.set_xlabel('annual_inc',labelpad=13) ax.set_ylabel('installment',labelpad=13) ax.legend(['bad','good'])好壞客戶在收入、月供這兩個維度上的分布情況
三、數據預處理
- 變量預處理
(1)計算每個變量缺失率
?
num_of_null=(data.isnull().sum()/data.shape[0]).sort_values(ascending=False) num_of_null.plot(kind='bar',figsize=(50,15),fontsize=23)各變量數據缺失率
缺失達90%的變量包含的信息太少,直接刪除,缺失20%-90%之間的待定,可能存在一些比較有用的信息,這里選擇做進一步分析,缺失20%以下的進行填充
刪除缺失率90%以上的變量:
?
thresh=data.shape[0]*0.1 data.dropna(thresh=thresh,axis=1,inplace=True)缺失20%-90%之間的變量:
?
num_of_null.loc[(num_of_null>0.2) & (num_of_null<=0.9)]缺失20%-90%之間的變量
?
通過分析可知缺失率為86%以上的變量相似,都是關于共同借款人的信息,由于原表中已有變量application_type用于區分單人申請還是共同申請,所以與共同申請人有關的變量全部刪除,只保留application_type用于區分是否有共同申請人:
?
data.drop(['sec_app_revol_util','verification_status_joint','revol_bal_joint','annual_inc_joint','sec_app_earliest_cr_line','sec_app_inq_last_6mths','sec_app_mort_acc','sec_app_open_acc','sec_app_open_act_il','sec_app_num_rev_accts','sec_app_chargeoff_within_12_mths','sec_app_collections_12_mths_ex_med','dti_joint'],axis=1,inplace=True)剩下的mths_*變量均表示自客戶在銀行、公開記錄、借貸平臺的最近一次違約以來的月份數,比如mths_since_last_major_derog表示自最近一次出現90+以來的月份數,考慮到這些變量與目標變量之間存在一定關聯,因此先保留,后續將缺失值歸為一類
最后next_pymnt_d屬于貸后變量,申請階段是沒有的,不應存在于申請評分卡中,其他的貸后變量也一并刪除:
?
data.drop(['next_pymnt_d','last_pymnt_amnt','last_pymnt_d','pymnt_plan','last_credit_pull_d','collection_recovery_fee','recoveries','debt_settlement_flag','hardship_flag','total_rec_late_fee','total_rec_int','total_rec_prncp','total_pymnt_inv','total_pymnt','out_prncp_inv','out_prncp','initial_list_status'],axis=1,inplace=True)(2)變量取值分布分析:若數據集在某一個屬性上取值都相同或接近,那區分度就越小
計算出各個變量中取同值的占比情況:
?
column=[] samerate=[] columns=data.columns for col in columns:grouped_max=data[col].groupby(data[col]).count().max()grouped_sum=data[col].groupby(data[col]).count().sum()same_rate=grouped_max/grouped_sumcolumn.append(col)samerate.append(same_rate) sr=pd.Series(samerate,index=column) sr.sort_values(ascending=False)部分同值率較高的變量
?
可見變量policy_code所有取值都一樣,無區分度,應剔除,考慮到后面會統一用IV值進行變量選擇,所以這里剩下的暫時先不處理:
?
data.drop('policy_code',axis=1,inplace=True)#剔除變量policy_code(3)基于業務理解對變量進行簡單剔除:
funded_amnt:該時間點籌集的總貸款金額,與loan_amnt完全一致
funded_amnt_inv:該時間點籌集款中來自于投資人的金額,與loan_amnt相差不大
sub_grade:grade的細分等級
title:貸款用途,與purpose大致相同
zip_code:郵政編碼
emp_title:借款人自己填寫的工作職位且分類雜亂,有效性不高
?
data.drop(['funded_amnt','funded_amnt_inv','sub_grade','title','zip_code','emp_title'],axis=1,inplace=True)(4)特征衍生:
earliest_cr_line:擁有信用額度的最早日期
issue_d:放款日期
構造衍生變量1:客戶在申請時已擁有的信用歷史月數mths_since_earliest_cr_line=issue_d-earliest_cr_line,并刪除原始變量;
installment:貸款月供
annual_inc:年收入
構造衍生變量2:貸款月供與月收入比installment_income_ratio,與原始數據集中變量dti不同的是負債項只考慮本次貸款負債;
?
#構造衍生變量1: mths_since_earliest_cr_line=[] datediff=pd.to_datetime(data.issue_d)-pd.to_datetime(data.earliest_cr_line) for i in datediff:mthsdiff=round(i.days/30)mths_since_earliest_cr_line.append(mthsdiff) data.insert(10,column='mths_since_earliest_cr_line',value=mths_since_earliest_cr_line)#插入新增變量1數據 data['mths_since_earliest_cr_line']=data['mths_since_earliest_cr_line'].astype('float64') data.drop(['issue_d','earliest_cr_line'],axis=1,inplace=True)#構造衍生變量2: monthly_income=data.annual_inc/12 installment_income_ratio=data.installment/monthly_income installment_income_ratio=round(installment_income_ratio,2) data.insert(8,'installment_income_ratio',installment_income_ratio)#插入新增變量2數據 data['installment_income_ratio']=data['installment_income_ratio'].astype('float64')剩余變量:
?
data.info()<經過一系列特征初篩和特征衍生,變量已經從144個降低至82個>
(5)變量分類:為了便于后續的自動分箱操作,這里先對數據做一些處理和分類
?
data['int_rate']=data.int_rate.str.replace('%',' ').str.strip().astype('float64') data['revol_util']=data.revol_util.str.replace('%',' ').str.strip().astype('float64') data['term']=data.term.str.replace('months',' ').str.strip()data['emp_length']=data.emp_length.str.replace('years',' ').str.strip() data['emp_length']=data.emp_length.str.replace('year',' ').str.strip() data['emp_length']=data.emp_length.replace({'10+':10,'< 1':0,'n/a':-1}).astype('float64')discrete_col=data.columns[data.dtypes=='object'].drop('target')#離散型變量,不包含目標變量target continuous_col=data.columns[data.dtypes=='float64']#連續型變量- 缺失值處理
各變量缺失情況:
?
num_of_null_new=(data.isnull().sum()/data.shape[0]).sort_values(ascending=False) has_null=num_of_null_new[num_of_null_new>0] has_null各變量缺失情況
?
含缺失值的變量數據概況:可以大致了解各個含缺失值的變量分布情況
?
data[has_null.index].describe().T含缺失值的變量數據概況
<由于這部分缺失值占多數,不能簡單地以均值、中位數、眾數等方法填充。基于字段含義與業務將這些變量統一理解為:自上一次不良記錄或查詢以來的月份數,月份數與客戶申請貸款時的資質應大致呈現正相關關系,在這些變量上出現缺失很可能代表客戶從未出現過該類不良記錄或查詢記錄,后續分箱中positive_rate呈現單調性也驗證了自己的猜測。這里將缺失值單獨歸為一類,且取值與其他群體要有明顯的區分>
?
data['mths_since_recent_inq'].fillna(value=99,inplace=True) data['mths_since_last_delinq'].fillna(value=999,inplace=True) data['mths_since_last_record'].fillna(value=999,inplace=True) data['mths_since_recent_bc_dlq'].fillna(value=999,inplace=True) data['mths_since_last_major_derog'].fillna(value=999,inplace=True) data['mths_since_recent_revol_delinq'].fillna(value=999,inplace=True)?
missing=['il_util','num_tl_120dpd_2m','mo_sin_old_il_acct','mths_since_rcnt_il','bc_util','percent_bc_gt_75','bc_open_to_buy','mths_since_recent_bc','dti', 'revol_util','all_util','avg_cur_bal','pct_tl_nvr_dlq'] data[missing]=data[missing].apply(lambda x:x.fillna(x.median()))四、特征工程
- 數據分箱
<<關于IV和下文中的WOE:數據挖掘模型中的IV和WOE詳解
?
continuous=[] for col in continuous_col:con=fp.proc_woe_continuous(df=data,var=col,#變量名global_bt=data.target.sum(),#正樣本總量global_gt=data.shape[0]-data.target.sum(),#負樣本總量min_sample=0.05*data.shape[0],#單個區間最小樣本量閾值alpha=0.01)continuous.append(con)?
discrete=[] for col in discrete_col:dis=fp.proc_woe_discrete(df=data,var=col,#變量名global_bt=data.target.sum(),#正樣本總量global_gt=data.shape[0]-data.target.sum(),#負樣本總量min_sample=0.05*data.shape[0])#單個區間最小樣本量閾值discrete.append(dis)?
df_continuous=eva.eval_feature_detail(Info_Value_list=continuous)#連續型變量分箱結果 df_discrete=eva.eval_feature_detail(Info_Value_list=discrete)#離散型變量分箱結果 bined=pd.concat([df_continuous,df_discrete])#合并 bined.to_csv('C:\\Users\\honglihui\\Desktop\\LendingClubLoanData-UpdateTime201905\\bined.csv',index=False)分箱調整:基于positive_rate對相鄰分箱進行合并,使其呈現單調性,更貼近業務邏輯,滿足模型解釋性要求,選擇IV值相對較高的變量進行分箱調整
<部分分箱結果>
調整前
?調整后
?
導入調整后的分箱結果:
?
bined_adjust=pd.read_csv('C:\\Users\\honglihui\\Desktop\\LendingClubLoanData-UpdateTime201905\\bined_adjust.csv',low_memory=False) bined_adjust.dropna(axis=[0,1],how='all',inplace=True)- 變量數值編碼
以WOE值代替原始數據:
?
bined_adjust_con=bined_adjust[:489]#調整后的連續型變量分箱結果 bined_adjust_dis=bined_adjust[489:]#調整后的離散型變量分箱結果#連續型變量WOE轉化: for var in continuous_col:#連續型變量池con=bined_adjust_con['split_list'][bined_adjust_con.var_name==var]#取出相應變量的split_listidx=data.index#初始化indexfor i in bined_adjust_con[bined_adjust_con.var_name==var].index:#該變量所對應的分箱index區間con[i]=con[i].replace('(',' ').replace(')',' ').replace(']',' ').strip().split(',')#將split_list處理成包含區間最大最小值的listupper=np.float64(con[i][1])#右邊值設為區間最大值lower=np.float64(con[i][0])#左邊值設為區間最小值idx1=data[var][idx][(data[var][idx]>lower) & (data[var][idx]<=upper)].index#該段對應的indexdata[var][idx1]=bined_adjust_con.woe_list[i]#選出該段index對應的值以woe值替換idx=idx[~idx.isin(idx1)]#更新index,已替換過值的index不進入下一次值替換循環#離散型變量WOE轉化: for var in discrete_col:#離散型變量池dis=bined_adjust_dis['split_list'][bined_adjust_dis.var_name==var]#取出相應變量的split_listfor i in bined_adjust_dis[bined_adjust_dis.var_name==var].index:#該變量所對應的分箱index區間dis[i]=dis[i].replace('[','').replace(']','').replace("'",'').strip().split(',')#將split_list處理成包含所有子元素的listdata[var][data[var].isin(dis[i])]=bined_adjust_dis.woe_list[i]#以woe值替換原值數值編碼后的數據集:
?
data.head()部分數據
- 變量選擇
?
IV=bined_adjust[['var_name','iv']].drop_duplicates()#每個解釋變量對應的iv值 vas=IV[['var_name','iv']][IV.iv>0.02].sort_values(by='iv',ascending=False)#篩去iv小于0.02無預測能力的變量 vas.set_index(keys='var_name',inplace=True) vas.plot(kind='bar',figsize=(10,5))IV值大于0.02的變量
共篩選出33個解釋變量,其中grade與int_rate兩個變量的預測能力較強,其余較一般,可以預見最后的模型效果并不會太好
?
r=data[vas.index].corr()#相關系數 r=r.round(2) plt.figure(figsize=(15,10)) sns.heatmap(data=r,annot=True)變量的相關性分析
可以看出有幾個變量之間相關性很強,其中tot_cur_bal與tot_hi_cred_lim相關系數0.95,loan_amnt與installment相關系數0.91,均呈現強正相關關系,需要考慮剔除其中一個,以減少多重共線性對Logistic回歸的影響
2.變量的多重共線性分析:Logistic回歸對多重共線性敏感,利用方差膨脹因子(VIF)來檢測多重共線性:若VIF>5,則說明變量間存在較嚴重的多重共線性
?
a=data[r.index]#取上面相關分析中用到的變量 b=np.array(a) VIF=[] for i in range(a.shape[1]):vifi=variance_inflation_factor(exog=b,exog_idx=i)#計算方差膨脹因子VIF.append(vifi) VIF方差膨脹因子(VIF)
最終基于使模型輸出的變量系數均為正以保證解釋性、盡可能地縮減變量的數量、選擇便于業務解釋的變量等原則決定剔除以下這些變量:
?
v=vas.index v=v[~v.isin(['tot_cur_bal','loan_amnt','total_bc_limit','avg_cur_bal','open_rv_24m','open_rv_12m','num_tl_op_past_12m','total_rev_hi_lim','term','open_acc_6m','mo_sin_old_rev_tl_op','all_util','inq_last_12m','bc_open_to_buy','mo_sin_rcnt_tl','mort_acc','il_util','mo_sin_rcnt_rev_tl_op','inq_fi'])]#刪除這些變量最終篩選出來用于建模的變量一共14個
五、建立模型
- 訓練模型
?
v=v.insert(0,'target') data=data[v]#建模數據集,包含14個解釋變量,1個目標變量X=data.drop('target',axis=1) y=data['target'].astype('int') X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2,random_state=0)#80%訓練集,20%測試集 clf=LogisticRegressionCV(Cs=[0.001,0.01,0.1,1,10,100,1000],#正則化強度備選集cv=10,#10折交叉驗證class_weight='balanced',#自動調整類別權重penalty='l2',#選用L2正則化random_state=0,#設置一個固定的隨機數種子)#其余為默認參數 clf.fit(X_train,y_train)#訓練模型六、模型評估
輸出各閾值對應的FPR、TPR:
?
y_score=clf.predict_proba(X_test)[:,1]#模型輸出的正樣本概率 fpr,tpr,thresholds=roc_curve(y_test,y_score)- ROC曲線
?
AUC=auc(fpr,tpr) plt.figure(figsize=(7,7)) plt.plot(fpr,tpr,label='AUC=%.2f'%AUC) plt.plot([0,1],[0,1],'--') plt.xlim([0,1]) plt.ylim([0,1]) plt.xlabel('False Positive Rate',fontdict={'fontsize':12},labelpad=10) plt.ylabel('True Positive Rate',fontdict={'fontsize':12},labelpad=10) plt.title('ROC curve',fontdict={'fontsize':20}) plt.legend(loc=0,fontsize=11)?
ROC曲線
<AUC為0.72,效果一般>
?
- K-S曲線
?
KS=max(tpr-fpr)#KS值 TF_diff=pd.Series(tpr)-pd.Series(fpr) tpr_best=tpr[TF_diff==TF_diff.max()] fpr_best=fpr[TF_diff==TF_diff.max()] thr_best=thresholds[TF_diff==TF_diff.max()]#KS值對應的概率閾值 plt.figure(figsize=(7,7)) plt.plot(thresholds,tpr) plt.plot(thresholds,fpr) plt.plot([thr_best,thr_best],[fpr_best,tpr_best],'--') plt.xlim([thresholds.min(),thresholds.max()-1]) plt.ylim([0,1]) plt.text(0.6,0.75,s='KS=%.2f'%KS,fontdict={'fontsize':12}) plt.text(0.6,0.7,s='thr_best=%.3f'%thr_best,fontdict={'fontsize':12}) plt.xlabel('Threshold',fontdict={'fontsize':12},labelpad=10) plt.ylabel('Rate',fontdict={'fontsize':12},labelpad=10) plt.title('K-S curve',fontdict={'fontsize':20}) plt.legend(['True Positive Rate','False Positive Rate'],loc=3)?
K-S曲線
<KS值為0.32,好壞區分效果不錯>
<對應的最佳閾值為0.503,定義壞客戶為違約客戶,則預測是否違約的最佳判定原則為:違約估計概率>=0.503判定為違約,違約估計概率<0.503判定為正常>
?
七、建立評分卡
p為違約概率估計,則:
?
其中:
- A、B為常數
- β0為模型輸出的截距
- β1,…,βn為模型輸出的變量系數
假設:Odds為1/20所對應的分值為800分,2Odds時對應分值減少50分
?
?
代入:
800=A-B*log(1/20)
800-50=A-B*log(2*1/20)
可求得A、B,最后求得總評分Score
?
b=50/(np.log(2)) a=800+b*np.log(1/20)?
v=v.drop('target')#參與建模的解釋變量集v coef=clf.coef_#模型輸出系數 intercept=clf.intercept_#模型輸出截距#變量集v與對應系數coef組合:v_coef df_v=pd.DataFrame(v) df_coef=pd.DataFrame(coef).T v_coef=pd.concat([df_v,df_coef],axis=1) v_coef.columns=['var_name','coef']#變量集v與對應woe組合:v_woe v_woe=bined_adjust[bined_adjust.var_name.isin(v)][['var_name','split_list','woe_list']].reset_index(drop=True)#v_coef與v_woe組合:v_coef_woe v_coef_woe=pd.merge(v_woe,v_coef)#構建新列wf:變量系數coef與變量各分組woe的乘積 wf=v_coef_woe['woe_list']*v_coef_woe['coef'] v_coef_woe.insert(loc=4,column='wf',value=wf)#構建新列score_woe:每個分組對應的子評分 score_woe=v_coef_woe['wf']*(-b) score_woe=round(score_woe,0)#變量各分段得分 v_coef_woe.insert(loc=5,column='score_woe',value=score_woe)?
score_0=round(float(a-b*intercept),0)#基準分 score_X=v_coef_woe[['var_name','split_list','woe_list','score_woe']]#變量中各分段得分 df_score_0=pd.DataFrame(['score_0','—','—','%.f'%score_0],index=['var_name','split_list','woe_list','score_woe']).T score_card=df_score_0.append(score_X,ignore_index=True)#總評分卡 score_card['score_woe']=score_card['score_woe'].astype('float64')score_card#輸出評分卡<評分卡部分字段評分>
評分卡部分字段評分
?
?
data_score=data.copy()#備份data?
data_score=data_score.drop('target',axis=1)for i in v:coef=v_coef.coef[v_coef.var_name==i].values#變量對應系數score_woe=(data_score[i]*coef*(-b)).astype('float64')#變量分段對應分數data_score[i]=round(score_woe,0)data_score.insert(loc=0,column='score_0',value='%.f'%score_0)#插入基礎分值字段 data_score['score_0']=data_score['score_0'].astype('float64') scores=data_score.sum(axis=1)#計算每個客戶總評分 data_score.insert(loc=15,column='scores',value=scores)#插入總評分字段data_score#輸出以評分替換woe后的數據<部分評分結果>
?
draw_score=data_score.copy()#用于畫圖分析的評分數據副本?
proba=clf.predict_proba(X)[:,1]#違約概率估計 target=data['target']#真實標記 draw_score.insert(loc=16,column='proba',value=proba) draw_score.insert(loc=17,column='target',value=target)- 好壞客戶評分分布情況
?
bad=draw_score.scores[draw_score.target==1] good=draw_score.scores[draw_score.target==0] plt.figure(figsize=(7,5)) sns.distplot(bad,bins=50,hist=False,label='bad') sns.distplot(good,bins=50,hist=False,label='good') plt.xlabel('Score',fontdict={'fontsize':12},labelpad=10) plt.legend(loc=0,fontsize=11)?
好壞客戶評分分布
<壞客戶主要分布在550分左右,好客戶主要分布在600分左右,有一定區分度,但不明顯>
?
- 評分與違約概率估計
?
matplotlib.rcParams['agg.path.chunksize']=1000 plt.rcParams['figure.dpi']=150 plt.plot(draw_score.proba,draw_score.scores) plt.xlabel('Probability estimates') plt.ylabel('Score')?
評分與違約概率估計
<評分只是違約概率估計的一個更直觀的展現形式,更易于理解和應用,預測的違約概率越低,對應的評分越高>
?
八、拒絕推論
- 由于從頭到尾所用的數據都是已放款的客戶數據,模型的建立基于通過的貸款,被拒絕的貸款數據未納入,因此存在群體上的偏差,若要使模型在整個群體上的預測都有好的表現則需要再進一步優化
- 比如用建立好的模型對拒絕客戶群體輸出對應分值,挑選部分得分較高的客戶標記為好客戶,剩下的標記為壞客戶,再將所有通過和拒絕的客戶數據融合重新建立一個更全面的模型。
作者:紅栗灰
鏈接:https://www.jianshu.com/p/72b4b8fed525
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的利用logistic回归构建申请信用评级案例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 模型验证的常用武器k-s
- 下一篇: 信用评分卡模型开发及评估指标