摘要
本文是利用Python語(yǔ)言對(duì)Kaggle上Give Me Some Credit網(wǎng)貸數(shù)據(jù)集進(jìn)行申請(qǐng)?jiān)u分卡(a卡)的數(shù)據(jù)清洗姐霍,建模镊折,熟悉評(píng)分卡構(gòu)建流程恨胚。結(jié)合信貸評(píng)分卡原理赃泡,從數(shù)據(jù)預(yù)處理升熊、建模分析级野、創(chuàng)建信用評(píng)分卡到建立自動(dòng)評(píng)分機(jī)制蓖柔,創(chuàng)立了一個(gè)簡(jiǎn)單的信用準(zhǔn)入評(píng)分系統(tǒng)况鸣。并對(duì)建立基于AI 的機(jī)器學(xué)習(xí)評(píng)分卡系統(tǒng)的路徑進(jìn)行思考和總結(jié)潜索。
工作原理
信用評(píng)分卡是一種成熟的預(yù)測(cè)方法竹习,在信用風(fēng)險(xiǎn)評(píng)估以及金融風(fēng)險(xiǎn)控制領(lǐng)域更是得到了比較廣泛的使用由驹,其原理是將模型變量利用WOE-IV編碼離散化之后運(yùn)用logistic回歸模型進(jìn)行的一種二分類(lèi)廣義線性模型。
評(píng)分卡由一系列特征組成默刚,每個(gè)特征對(duì)應(yīng)申請(qǐng)表上的一個(gè)數(shù)據(jù)(年齡荤西、銀行流水勉躺、收入等)饵溅。并且一個(gè)特征項(xiàng)都會(huì)存在一串屬性(對(duì)于年齡這個(gè)問(wèn)題蜕企,答案可能就有30歲以下轻掩、30到40歲等)唇牧。開(kāi)發(fā)評(píng)分卡模型的過(guò)程中奋构,必須先確定屬性與申請(qǐng)人未來(lái)信用表現(xiàn)之間的相關(guān)關(guān)系,分配適當(dāng)?shù)?strong>權(quán)重分值径缅。分值越大纳猪,說(shuō)明該屬性蘊(yùn)含的信用表現(xiàn)越佳氏堤。而申請(qǐng)得分就是其分值的簡(jiǎn)單求和鼠锈。一旦評(píng)分大于等于金融放款機(jī)構(gòu)所設(shè)定的閾值,表示該申請(qǐng)人的風(fēng)險(xiǎn)水平是可接受的范圍同欠,可以被批準(zhǔn)铺遂。
數(shù)據(jù)處理
載入庫(kù)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use("bmh")
plt.rc('font', family='SimHei', size=13)
%matplotlib inline
import os
導(dǎo)入數(shù)據(jù)
data_train = pd.read_csv('cs-training.csv')
data_test = pd.read_csv('cs-test.csv')
探索性數(shù)據(jù)分析
特征解釋?zhuān)?/h3>
data_train.info()
data_train.info()
有缺失值的特征:MonthlyIncome炕檩,NumberOfDependents笛质。
修改列名:
columns = ({'SeriousDlqin2yrs':'Label',
'RevolvingUtilizationOfUnsecuredLines':'Revol',
'NumberOfOpenCreditLinesAndLoans':'NumOpen',
'NumberOfTimes90DaysLate':'Num90late',
'NumberRealEstateLoansOrLines':'NumEstate',
'NumberOfTime60-89DaysPastDueNotWorse':'Num60-89late',
'NumberOfDependents':'NumDependents',
'NumberOfTime30-59DaysPastDueNotWorse':'Num30-59late'}
)
data_train.rename(columns=columns,inplace = True)
data_test.rename(columns=columns,inplace = True)
badrate
bad = data_train.loc[data_train['Label']==1].shape[0]
good = data_train.loc[data_train['Label']==0].shape[0]
print('badrate為:{:.2f}%'.format(round(bad*100/(good+bad),2)))
壞客戶只占用戶6.68%。
異常值探索
特征 年齡
年齡分布
f,[ax1,ax2]=plt.subplots(1,2,figsize=(20,6))
sns.distplot(data_train['age'],ax=ax1)
sns.boxplot(y='age',data=data_train,ax=ax2)
plt.show()
從圖可以發(fā)現(xiàn),年齡基本呈正態(tài)分布肩杈。
異常值處理:
處理數(shù)據(jù)異常值一般有:3倍標(biāo)準(zhǔn)差區(qū)分異常值扩然、分箱四分位距區(qū)分異常值或者直接去除首尾各1%的數(shù)據(jù)這三種方法。
數(shù)據(jù)現(xiàn)在是處于正態(tài)分布兵拢,故采用3倍標(biāo)準(zhǔn)差去除異常说铃。
data_train = data_train[data_train['age']!=0]
age_Mean = np.mean(data_train['age'])
age_Std = np.std(data_train['age'])
UpLimit = round((age_Mean + 3*age_Std),2)
DownLimit = round((age_Mean - 3*age_Std),2)
data_train = data_train[(data_train['age']>=DownLimit)&(data_train['age']<=UpLimit)]
觀察年齡大小對(duì)label值的影響
##
#先做一個(gè)等距粗分箱
data_train['ageCat'] = np.nan
data_train.loc[(data_train['age']>18)&(data_train['age']<40),'ageCat'] = 1
data_train.loc[(data_train['age']>=40)&(data_train['age']<60),'ageCat'] = 2
data_train.loc[(data_train['age']>=60)&(data_train['age']<80),'ageCat'] = 3
data_train.loc[(data_train['age']>=80),'ageCat'] = 4
BadrateList = {}
for m in data_train['ageCat'].value_counts().index:
data = data_train.loc[data_train['ageCat']==m]
Badrate = data.loc[data['Label']==0].shape[0]/data.loc[data['Label']==1].shape[0]
BadrateList[m] = Badrate
BadrateList=dict(sorted(BadrateList.items(),key=lambda x:x[0]))
f,ax = plt.subplots(figsize=(8,5))
sns.countplot('ageCat', hue='Label', data=data_train, ax=ax)
ax2 = ax.twinx()
ax2.plot([x-1 for x in BadrateList.keys()], [round(x,2) for x in BadrateList.values()],'ro--',linewidth=2)
可以看到年齡越大植捎,badrate越大焰枢,呈現(xiàn)單調(diào)性济锄。為后面的等頻分箱求WOE提供參考荐绝。
特征 Revol
該特征的意義是:除了房貸車(chē)貸之外的信用卡金額(即貸款金額額度)/信用卡總額度低滩。即通常是小于等于1的恕沫,一般大于1是屬于透支鲸阔。
print(data_train['Revol'][data_train['Revol']>1].count())
print(data_train['Revol'].max())
print(data_train['Revol'].min())
3321
50708.0
0.0
特征值超過(guò)幾萬(wàn)顯然不太正常褐筛,估計(jì)很有可能是沒(méi)有除以分母信用卡總額度渔扎,而是分子的純信用卡賬面貸款金額。
現(xiàn)在嘗試確定屬于透支的最大數(shù)字和異常值的范圍:
#四分位距法
Percentile = np.percentile(data_train['Revol'],[0,25,50,75,100])
IQR = Percentile[3] - Percentile[1]
UpLimit = Percentile[3]+IQR*1.5
DownLimit = Percentile[1]-IQR*1.5
print('上限為:{0}, 下限為:{1}'.format(UpLimit,DownLimit))
print('上界異常值占比:{0}%'.format(round(data_train[data_train['Revol']>UpLimit].shape[0]*100/data_train.shape[0],2)))
上限為:1.3529709321249999, 下限為:-0.763938468875
上界異常值占比:0.51%
#分段觀察Revol分布
f,[[ax1,ax2],[ax3,ax4]]=plt.subplots(2,2,figsize=(20,10))
sns.distplot(data_train['Revol'][data_train['Revol']<1],bins=10,ax=ax1)
sns.distplot(data_train['Revol'][(data_train['Revol']>=1)&(data_train['Revol']<100)],bins=10,ax=ax2)
sns.distplot(data_train['Revol'][(data_train['Revol']>=100)&(data_train['Revol']<10000)],bins=10,ax=ax3)
sns.distplot(data_train['Revol'][(data_train['Revol']>=10000)&(data_train['Revol']<51000)],bins=10,ax=ax4)
ax1.set_title('0=<Revol<1')
ax2.set_title('1=<Revol<100')
ax3.set_title('100=<Revol<10000')
ax4.set_title('10000=<Revol<51000')
plt.show()
print('大于等于1的數(shù)量為:{0}'.format(data_train.loc[data_train['Revol']>=1].shape[0]))
print('大于等于1的數(shù)量為:{0}'.format(data_train.loc[data_train['Revol']>=1].shape[0]))
- Revol小于1的分布中,大部分?jǐn)?shù)據(jù)都處于(0,0.1)區(qū)間,而隨著Revol特征值變大琼蚯,數(shù)量遞減遭庶;
- 在Revol大于1的數(shù)值分布中峦睡,都明顯的呈現(xiàn)了遞減趨勢(shì)煎谍,主要集中在100以?xún)?nèi)的范圍內(nèi)呐粘;
將1~50000等頻分箱作岖,計(jì)算相應(yīng)的badrate進(jìn)行觀察
#qcut等頻分箱
data2 = data_train[(data_train['Revol']>1)&(data_train['Revol']<51000)]
data2['Revol_100'] = pd.qcut(data2['Revol'],10)
badrate ={}
revol_100_list = list(set(data2['Revol_100']))
for x in revol_100_list:
a = data2[data2['Revol_100'] == x]
bad = a['Label'][a['Label']==1].count()
good = a['Label'][a['Label']==0].count()
badrate[x]=bad/(good+bad)
f = zip(badrate.keys(),badrate.values())
badrate = pd.DataFrame(f)
badrate.columns = ['cut','badrate']
badrate = badrate.sort_values('cut')
print(badrate)
badrate.plot('cut','badrate')
cut badrate
7 (0.999, 1.007] 0.282282
3 (1.007, 1.014] 0.313253
6 (1.014, 1.026] 0.340361
5 (1.026, 1.045] 0.406627
0 (1.045, 1.075] 0.409639
1 (1.075, 1.118] 0.439759
4 (1.118, 1.215] 0.493976
2 (1.215, 1.442] 0.481928
8 (1.442, 2.198] 0.427711
9 (2.198, 50708.0] 0.129518
data_train['Revol'].loc[data_train['Revol']>=2].count()
371
從業(yè)務(wù)角度出發(fā),隨著信用卡貸款比例增高与柑,客戶風(fēng)險(xiǎn)越高价捧,badrate就會(huì)越大结蟋。Revol大于1推正,客戶就已經(jīng)處于“透支”狀態(tài)植榕,但是"透支"比例總是有上限的尊残。當(dāng)Revol大于2時(shí)寝衫,badrate顯著下降至12.95%慰毅,接近總體badrate的6.68%汹胃,那么可以將Revol大于2的371個(gè)客戶的Revol值看作屬于0~1之間统台。
特征 DebtRatio
該特征情況經(jīng)過(guò)探索,情況和Revol類(lèi)似:也是疑似存在比率分母缺失贵扰。那么用相似的方法處理。
print(data_train['DebtRatio'][data_train['DebtRatio']>1].count())
print(data_train['DebtRatio'].max())
print(data_train['DebtRatio'].min())
35122
329664.0
0.0
data3['D_100'] = pd.qcut(data3['DebtRatio'],10)
badrate ={}
D100_list = list(set(data3['D_100']))
for x in D100_list:
a = data3[data3['D_100'] == x]
bad = a['Label'][a['Label']==1].count()
good = a['Label'][a['Label']==0].count()
badrate[x]=bad/(good+bad)
f = zip(badrate.keys(),badrate.values())
badrate = pd.DataFrame(f)
badrate.columns = ['cut','badrate']
badrate = badrate.sort_values('cut')
print(badrate)
badrate.plot('cut','badrate',figsize=(20,8))
data_train['DebtRatio'].loc[data_train['DebtRatio']>=2].count()
31200
經(jīng)過(guò)比對(duì),當(dāng)DebtRatio>2時(shí)球切,badrate就下降至與總體一致绒障,故后面將DebtRatio>2與DebtRatio<1分成一箱吨凑。
缺失值探索
特征 NumDependents
NumDependents中缺失值占比
print(data_train[data_train['NumDependents'].isnull()].shape[0])
print(round(data_train[data_train['NumDependents'].isnull()].shape[0]/data_train.shape[0],4))
3911
0.0261
計(jì)算缺失值和未缺失值的badrate
d_Null = data_train.loc[data_train['NumDependents'].isnull()]
d_notNull = data_train.loc[data_train['NumDependents'].notnull()]
print('缺失值為{0}%'.format(round(d_Null[d_Null['Label']==1].shape[0]*100/d_Null.shape[0],2)))
print('未缺失值為{0}%'.format(round(d_notNull[d_notNull['Label']==1].shape[0]*100/d_notNull.shape[0],2)))
缺失值為4.55%
未缺失值為6.74%
說(shuō)明badrate在NumDependents的缺失值下并沒(méi)有區(qū)別,不能將缺失值看作一種特殊的狀態(tài)對(duì)待户辱,那么可以填充缺失值
print(data_train.loc[data_train['MonthlyIncome'].isnull()].shape[0])
print(data_train.loc[(data_train['NumDependents'].isnull())&(data_train['MonthlyIncome'].isnull())].shape[0])
29708
3911
從上面得知MonthlyIncome為空的客戶鸵钝,NumDependents也為空。
下面觀察MonthlyIncome為空時(shí)庐镐,NumDependents不為空的情況。并期望用相似的情況解決NumDependents的空值
D_Null = data_train.loc[(data_train['MonthlyIncome'].isnull())&(data_train['NumDependents'].notnull()),'NumDependents']
D_Null.value_counts().plot(kind='bar', figsize=(10,5))
該情況下必逆,數(shù)據(jù)絕大部分集中于0處痕届,這也和NumDependents總體分布相似,那么填充為0值末患。
data_train['NumDependents'] = data_train['NumDependents'].fillna(0)
特征 MonthlyIncome
缺失情況
print(data_train[data_train['MonthlyIncome'].isnull()].shape[0])
print(round(data_train[data_train['MonthlyIncome'].isnull()].shape[0]/data_train.shape[0],4))
29708
0.1981
缺失比例接近20%研叫,考慮使用隨機(jī)森林算法填充:
data_train.drop(['Unnamed: 0'],axis=1)
from sklearn.ensemble import RandomForestRegressor
process_miss = data_train.iloc[:,[5,0,1,2,3,4,6,7,8,9]]
known = process_miss[process_miss.MonthlyIncome.notnull()].values
unknown = process_miss[process_miss.MonthlyIncome.isnull()].values
x = known[:,1:]
y = known[:,0]
rf = RandomForestRegressor(random_state=0,n_estimators=100,max_depth=3,max_features=3,n_jobs=-1)
rf.fit(x,y)
pre = rf.predict(unknown[:,1:]).round(0)
data_train.loc[data_train['MonthlyIncome'].isnull(),'MonthlyIncome'] = pre
特征 Num30-59、 60-89 璧针、90 late
f,[ax,ax1,ax2]=plt.subplots(1,3,figsize=(20,5))
ax.boxplot(x=data_train['Num30-59late'])
ax1.boxplot(x=data_train['Num60-89late'])
ax2.boxplot(x=data_train['Num90late'])
ax.set_xlabel('Num30-59late')
ax1.set_xlabel('Num60-89late')
ax2.set_xlabel('Num90late')
plt.show()
for i in ['Num30-59late','Num60-89late','Num90late']:
print(data_train[i][data_train[i]>20].count())
269
269
269
從圖中看出嚷炉,三個(gè)特征均有大于20的異常值。一般來(lái)說(shuō)逾期不會(huì)存在這么多次數(shù)探橱,并且異常個(gè)數(shù)很少申屹,可以考慮刪去绘证。
特征 NumOpen和NumEstate
分布和上個(gè)特征類(lèi)似,都是存在少量上界異常值:大于50和大于30
f,[ax1,ax2]=plt.subplots(1,2,figsize=(20,10))
sns.boxplot(y='NumOpen',data=data_train,ax = ax1)
sns.boxplot(y='NumEstate',data=data_train,ax =ax2)
建模部分
導(dǎo)入庫(kù)及數(shù)據(jù)
import numpy as np
import pandas as pd
import scipy.stats as stats
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use("bmh")
plt.rc('font', family='SimHei', size=13)
%matplotlib inline
import math
import os
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_predict
from sklearn.metrics import roc_curve, roc_auc_score
from sklearn.preprocessing import StandardScaler
# 導(dǎo)入數(shù)據(jù)
data_train = pd.read_csv('cs-training.csv')
data_test = pd.read_csv('cs-test.csv')
columns = ({'SeriousDlqin2yrs':'Label',
'RevolvingUtilizationOfUnsecuredLines':'Revol',
'NumberOfOpenCreditLinesAndLoans':'NumOpen',
'NumberOfTimes90DaysLate':'Num90late',
'NumberRealEstateLoansOrLines':'NumEstate',
'NumberOfTime60-89DaysPastDueNotWorse':'Num60-89late',
'NumberOfDependents':'NumDependents',
'NumberOfTime30-59DaysPastDueNotWorse':'Num30-59late'}
)
data_train.rename(columns=columns,inplace = True)
data_test.rename(columns=columns,inplace = True)
處理測(cè)試集異常值
data_train = data_train[data_train['age']!=0]
data_train = data_train[data_train['Num30-59late']<20]
data_train = data_train[data_train['Num60-89late']<20]
data_train = data_train[data_train['Num90late']<20]
data_train= data_train[data_train['NumOpen']<50]
data_train= data_train[data_train['NumEstate']<30]
data_train = data_train.drop(['Unnamed: 0'],axis=1)
data_test = data_test.drop(['Unnamed: 0'],axis=1)
補(bǔ)全測(cè)試集和驗(yàn)證集缺失值
#隨機(jī)森林補(bǔ)全income
def missfill(df):
process_miss = df.iloc[:,[5,1,2,3,4,6,7,8,9]]
known = process_miss[process_miss.MonthlyIncome.notnull()].as_matrix()
unknown = process_miss[process_miss.MonthlyIncome.isnull()].as_matrix()
X = known[:,1:]
y = known[:,0]
rfr = RandomForestRegressor(random_state=0,n_estimators=100,max_depth=3,n_jobs=-1哗讥,max_features = 3)
rfr.fit(X,y)
pre= rfr.predict( unknown[:,1:]).round(0)
return pre
# 預(yù)處理
def processing(data):
data["NumDependents"] = np.abs(data["NumDependents"])
data["NumDependents"] = data["NumDependents"].fillna(0)
data["NumDependents"] = data["NumDependents"].astype('int64')
data.loc[(data['NumDependents']>=7), 'NumDependents'] = 7
pre = missfill(data)
data.loc[data['MonthlyIncome'].isnull(),'MonthlyIncome'] = pre
#構(gòu)造新的特征
data["Defaulted"] = data["Num90late"] + data["Num60-89late"] + data["Num30-59late"]
data["Loans"] = data["NumOpen"] + data["NumEstate"]
data["DebtPay"] =np.absolute( data["DebtRatio"] * data["MonthlyIncome"])
data["DebtPay"] = data["DebtPay"].astype('int64')
data["age"] = data["age"].astype('int64')
data["MonthlyIncome"] = data["MonthlyIncome"].astype('int64')
data["age_map"] = data["age"]
data["WithDependents"] = data["NumDependents"]
return data
def hand_bin(data):
# 手動(dòng)分箱
# Revol特征分箱
data.loc[(data['Revol']>=0)&(data['Revol']<1), 'Revol'] = 0
data.loc[(data['Revol']>1)&(data['Revol']<=30), 'Revol'] = 1
data.loc[(data['Revol']>30), 'Revol'] = 0
# DebtRatio特征分箱
data.loc[(data['DebtRatio']>=0)&(data['DebtRatio']<1), 'DebtRatio'] = 0
data.loc[(data['DebtRatio']>=1)&(data['DebtRatio']<2), 'DebtRatio'] = 1
data.loc[(data['DebtRatio']>=2), 'DebtRatio'] = 0
# Num30-59late, Num60-89late, Num90late,NumOpen
data.loc[(data['Num30-59late']>=8), 'Num30-59late'] = 8
data.loc[(data['Num60-89late']>=7), 'Num60-89late'] = 7
data.loc[(data['Num90late']>=10), 'Num90late'] = 10
data.loc[(data['NumEstate']>=8), 'NumEstate'] = 8
# 衍生變量分箱
data.loc[(data["Defaulted"] >= 1), "Defaulted"] = 1
data.loc[(data["Loans"] <= 5), "Loans"] = 0
data.loc[(data["Loans"] > 5), "Loans"] = 1
data.loc[(data["WithDependents"] >= 1), "WithDependents"] = 1
data.loc[(data["age"] >= 18) & (data["age"] < 60), "age_map"] = 1
data.loc[(data["age"] >= 60), "age_map"] = 0
data["age_map"] = data["age_map"].replace(0, "older")
data["age_map"] = data["age_map"].replace(1, "senior")
data = pd.concat([data, pd.get_dummies(data.age_map,prefix='is')], axis=1)
return data
Ytest = data_test['Label']
Xtest = data_test.drop(['Label'], axis=1)
Ytrain = data_train['Label']
Xtrain = data_train.drop(['Label'], axis=1)
data_train = processing(data_train)
data_test = processing(data_test)
data_train = hand_bin(data_train)
data_test = hand_bin(data_test)
data_train.drop('age_map',axis=1,inplace=True)
data_test.drop('age_map',axis=1,inplace=True)
特征相關(guān)性熱力圖
corr = data_train.corr()
plt.figure(figsize=(14,12))
sns.heatmap(corr, annot=True)
將變量分成離散變量和連續(xù)變量分別進(jìn)行進(jìn)行woe,iv值計(jì)算:
dvar = ['Revol','DebtRatio','Num30-59late', 'Num60-89late','Num90late','Defaulted','WithDependents',
'NumEstate','NumDependents','Loans','is_senior','is_older']
svar = ['MonthlyIncome','age','NumOpen','DebtPay']
def bin_woe(tar, var, n=None, cat=None):
total_bad = tar.sum()
total_good =tar.count()-total_bad
totalRate = total_good/total_bad
if cat == 's':
msheet = pd.DataFrame({tar.name:tar,var.name:var,'bins':pd.qcut(var, n, duplicates='drop')})
grouped = msheet.groupby(['bins'])
elif (cat == 'd') and (n is None):
msheet = pd.DataFrame({tar.name:tar,var.name:var})
grouped = msheet.groupby([var.name])
groupBad = grouped.sum()[tar.name]
groupTotal = grouped.count()[tar.name]
groupGood = groupTotal - groupBad
groupRate = groupGood/groupBad
groupBadRate = groupBad/groupTotal
groupGoodRate = groupGood/groupTotal
woe = np.log(groupRate/totalRate)
iv = np.sum((groupGood/total_good-groupBad/total_bad)*woe)
if cat == 's':
new_var, cut = pd.qcut(var, n, duplicates='drop',retbins=True, labels=woe.tolist())
elif cat == 'd':
dictmap = {}
for x in woe.index:
dictmap[x] = woe[x]
new_var, cut = var.map(dictmap), woe.index
return woe.tolist(), iv, cut, new_var
def woe_vs(data):
cutdict = {}
ivdict = {}
woe_dict = {}
woe_var = pd.DataFrame()
for var in data.columns:
if var in dvar:
woe, iv, cut, new = bin_woe(data['Label'], data[var], cat='d')
woe_dict[var] = woe
woe_var[var] = new
ivdict[var] = iv
cutdict[var] = cut
elif var in svar:
woe, iv, cut, new = bin_woe(data['Label'], data[var], n=5, cat='s')
woe_dict[var] = woe
woe_var[var] = new
ivdict[var] = iv
cutdict[var] = cut
ivdict = sorted(ivdict.items(), key=lambda x:x[1], reverse=False)
iv_vs = pd.DataFrame([x[1] for x in ivdict],index=[x[0] for x in ivdict],columns=['IV'])
ax = iv_vs.plot(kind='bar',
figsize=(12,12),
title='Feature Importance',
fontsize=10,
width=0.8)
ax.set_ylabel('Features')
ax.set_xlabel('IV of Features')
return ivdict, woe_var, woe_dict, cutdict
iv_info, woe_data, woe_dict, cut_dict = woe_vs(data_train)
由此可以開(kāi)始選擇選擇變量
選擇變量
選擇變量的基本規(guī)則是根據(jù)熱力圖和上面的IV圖和在特征相關(guān)性高的變量中嚷那,選擇IV值高的規(guī)則,可以選擇如下變量:
is_senior | is_older | age:選age
WithDependents | NumDependents:選NumDependents
Defaulted | Num30-59late:選Defaulted
NumOpen |Loans:選NumOpen
最后保留的變量
ivinfo_keep = ['Revol', 'age', 'DebtRatio', 'MonthlyIncome',
'NumOpen', 'Num90late', 'NumEstate', 'Num60-89late',
'NumDependents','Defaulted','DebtPay']
再次計(jì)算相關(guān)系數(shù):
可以判斷各變量之間不存在明顯的相關(guān)性
邏輯回歸建模
X = woe_data[ivinfo_keep]
y = Ytrain
X_train, X_val, y_train, y_val = train_test_split(X,y,random_state=42)
logit = LogisticRegression(random_state=0,
solver="sag",
penalty="l2",
class_weight="balanced",
C=1.0,
max_iter=500)
logit.fit(X_train, y_train)
logit_scores_proba = logit.predict_proba(X_train)
logit_scores = logit_scores_proba[:,1]
利用ROC曲線判斷效果:
def plot_roc_curve(fpr, tpr, auc_score, label=None):
plt.figure(figsize=(10,8))
plt.plot(fpr, tpr, linewidth=2, label='AUC = %0.2f'%auc_score)
plt.plot([0,1],[0,1], "k--")
plt.axis([0,1,0,1])
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive rate")
plt.legend()
fpr_logit, tpr_logit, thresh_logit = roc_curve(y_train, logit_scores)
auc_score = roc_auc_score(y_train, logit_scores)
print("AUC: {}".format(auc_score))
plot_roc_curve(fpr_logit,tpr_logit, auc_score)
AUC: 0.8290290455012129
從上面圖形和auc值可以看出擬合效果較好杆煞,接下來(lái)開(kāi)始計(jì)算評(píng)分卡:
coe = logit.coef_
intercept = logit.intercept_
B = 20 / math.log(2)
A = 600 + B / math.log(20)
# 基礎(chǔ)分
base = round(A-B * intercept[0], 0)
featurelist = []
woelist = []
coelist = []
cutlist = []
for k,v in woe_dict.items():
if k in ivinfo_keep:
for n in range(0,len(v)):
featurelist.append(k)
woelist.append(v[n])
coelist.append(coe[0][n])
cutlist.append(cut_dict[k][n])
scoreboard = pd.DataFrame({'feature':featurelist, 'woe':woelist, 'coe':coelist, 'cut':cutlist},
columns=['feature','cut','woe','coe'])
scoreboard['score'] = round(-scoreboard['woe']*scoreboard['coe']*B,0).astype('int64')
scoreboard
結(jié)果如下:
總結(jié):
1.用戶的屬性有千千萬(wàn)萬(wàn)個(gè)維度魏宽,而評(píng)分卡模型所選用的特征很有限,這時(shí)候就需要一定的原則對(duì)變量篩選:預(yù)測(cè)能力决乎、變量相關(guān)性等队询。
- 評(píng)分卡模型采用的是對(duì)每個(gè)字段的分段進(jìn)行評(píng)分,對(duì)于特征的分段處理就是采用變量分箱构诚。
- 對(duì)字段的每個(gè)分段進(jìn)行評(píng)分的方式是采用WOE-IV編碼蚌斩,將預(yù)測(cè)概率值轉(zhuǎn)化為評(píng)分,利用變量相關(guān)性分析確保其合理性范嘱。
4.本質(zhì)上是采用邏輯回歸進(jìn)行二分類(lèi)送膳,但重點(diǎn)在于特征篩選和處理方面。