當前位置:網站首頁>機器學習之金融風控

機器學習之金融風控

2022-05-14 08:48:55西西先生666

一、評分卡

1.1 評分卡原理

  1. 根據風控時間點的”前中後”,一般風評分卡可以分為下面三類:
    1)A卡(Application score card):目的在於預測申請時(申請信用卡、申請貸款)對申請人進行量化評估;
    2)B卡(Behavior score card):目的在於預測使用時點(獲得貸款、信用卡的使用期間)未來一定時間內逾期的概率;
    3)C卡(Collection score card):目的在於預測已經逾期並進入催收階段後未來一定時間內還款的概率。

1.2 評分卡優缺點

  1. 優點:
    1)易於使用。業務人員在操作時,只需要按照評分卡每樣打分然後算個總分就能操作,不需要接受太多專業訓練;
    2)直觀透明。客戶和審核人員都能知道看到結果,以及結果是如何產生的;
    3)應用範圍廣。如支付寶的芝麻信用分,或者知乎鹽值。
  2. 缺點:
    1)當信息維度高時,評分卡建模會變得非常困難;

1.3 評分卡模型搭建步驟

  • 0.數據探究。研究數據都包含哪些信息;
  • 1.樣本選取。選取一定時間周期內該平臺上的信貸樣本數據,劃分訓練集和測試集;
  • 2.變量選取。也就是特征篩選。需要一定的業務理解。一般這部分費時較久;
  • 3.邏輯回歸。根據篩選後的特征,構建邏輯回歸模型;
  • 4.評分卡轉換。根據一定的公式轉換;
  • 5.驗證並上線。驗證評分卡效果,並上線持續監測。

1.4 IV值和WOE值詳解

  • WOE:Weight of Evidence,即證據權重。WOE是對原始自變量的一種編碼形式。
    1)對變量進行WOE編碼時,首先需要對變量進行分組處理(即離散化、分箱等操作),分組後,對於第 i i i組,WOE的計算公式為:
    W O E i = l n ( p y i p n i ) = l n ( y i y T n i n T ) (1) WOE_i=ln(\frac{py_i}{pn_i})=ln(\frac{\frac{y_i}{y_T}}{\frac{n_i}{n_T}})\tag{1} WOEi=ln(pnipyi)=ln(nTniyTyi)(1)
    2)其中 p y i py_i pyi是組中響應的客戶數( y i y_i yi,在風險模型中,對應違約的客戶,即label=1的客戶)占所有樣本中所有響應客戶( y T y_T yT)的比例; p n i pn_i pni是組中未響應的客戶( n i n_i ni,在風險模型中,對應未違約的客戶,即label=0的客戶)占所有樣本中所有未響應客戶( n T n_T nT)的比例。
    3)WOE越大,組中響應的客戶比例和未響應客戶比例之間的比值差异越大,則在這個分組裏的樣本響應的可能性就越大,WOE越小,差异越小,這個分組裏的樣本響應的可能性就越小。

  • IV:Information Value,即信息值或信息量。判斷特征對結果的重要程度。
    1)IV的計算公式為:
    I V i = ( p y i − p n i ) ∗ W O E i = ( p y i − p n i ) ∗ l n ( p y i p n i ) = ( p y i − p n i ) ∗ l n ( y i y T n i n T ) (2) \begin{aligned} IV_i&=(py_i-pn_i)*WOE_i\\ &=(py_i-pn_i)*ln(\frac{py_i}{pn_i})\\ &=(py_i-pn_i)*ln(\frac{\frac{y_i}{y_T}}{\frac{n_i}{n_T}})\tag{2} \end{aligned} IVi=(pyipni)WOEi=(pyipni)ln(pnipyi)=(pyipni)ln(nTniyTyi)(2)
    有了一個變量的各個分組的 I V i IV_i IVi值,我們可以計算整個變量的IV值,如下所示:
    I V = ∑ i n I V i (3) IV=\sum_i^n IV_i\tag{3} IV=inIVi(3)
    其中 n n n為變量分組的個數。

  • IV值缺點:不能自動處理變量的分組中出現響應比例為0或100%的情况。那麼,遇到響應比例為0或者100%的情况,我們應該怎麼做呢?建議如下:
    (1)如果可能,直接把這個分組做成一個規則,作為模型的前置條件或補充條件;
    (2)重新對變量進行離散化或分組,使每個分組的響應比例都不為0且不為100%,尤其是當一個分組個體數很小時(比如小於100個),强烈建議這樣做,因為本身把一個分組個體數弄得很小就不是太合理。
    (3)如果上面兩種方法都無法使用,建議人工把該分組的響應數和非響應的數量進行一定的調整。如果響應數原本為0,可以人工調整響應數為1,如果非響應數原本為0,可以人工調整非響應數為1。

1.5 評分卡轉換

  • 評分卡模型中不直接采用客戶違約概率p,而是采用違約概率與正常概率之間的比值,稱為odds,即
    o d d s = p 1 − p (4) odds=\frac{p}{1-p}\tag{4} odds=1pp(4)
    p = o d d s 1 + o d d s (5) p=\frac{odds}{1+odds}\tag{5} p=1+oddsodds(5)
  • 為什麼不直接采用p,而是采用odds呢?
    1)根據邏輯斯蒂回歸原理:
    p = 1 1 + e − θ T x (6) p=\frac{1}{1+e^{-\theta^Tx}}\tag{6} p=1+eθTx1(6)
    經過變換可得:
    l n ( p 1 − p ) = θ T x (7) ln(\frac{p}{1-p})=\theta^Tx\tag{7} ln(1pp)=θTx(7)
    2)有了邏輯斯蒂的原理,我們可得:
    l n ( o d d s ) = θ T x (8) ln(odds)=\theta^Tx\tag{8} ln(odds)=θTx(8)
  • 評分卡邏輯的背後是odds的變動與評分變動之間的映射(把odds映射為評分),可以設計一個公式:
    S c o r e = A − B ∗ l n ( o d d s ) (9) Score=A-B*ln(odds)\tag{9} Score=ABln(odds)(9)
    其中A、B是出常數,B前面取負號的原因在於:違約率越低,得分越高。
  • 計算A、B的方法如下,首先包含2個假設:
    1)基准分:當 θ 0 \theta_0 θ0為某個比率時的得分 P 0 P_0 P0,業界風控策略基准分都設置為500/600/650,基准分 P 0 P_0 P0為:
    P 0 = A − B ∗ l n ( θ 0 ) (10) P_0=A-B*ln(\theta_0)\tag{10} P0=ABln(θ0)(10)
    2)PDO(point of double):比率翻倍時分數的變動值,我們這裏假設當odds翻倍時,分值减少50,則有:
    P 0 − P D O = A − B ∗ l n ( 2 θ 0 ) (11) P_0-PDO=A-B*ln(2\theta_0)\tag{11} P0PDO=ABln(2θ0)(11)
    由公式(9)、(10)可得A、B的值為:
    B = P D O l n 2 (12) B=\frac{PDO}{ln2}\tag{12} B=ln2PDO(12)
    A = P 0 + B ∗ l n ( θ 0 ) (13) A=P_0+B*ln(\theta_0)\tag{13} A=P0+Bln(θ0)(13)
  • 評分卡裏每一個變量的每一個分箱有一個對應分值:
    S c o r e = A − B { θ 0 + θ 1 x 1 + θ n x n } (14) Score=A-B\{\theta_0+\theta_1x_1+\theta_nx_n\}\tag{14} Score=AB{ θ0+θ1x1+θnxn}(14)
    其中變量 x 1 , x 2 x_1, x_2 x1,x2都是最終模型的輸入變量。由於所有輸入變量都進行了WOE編碼,所以這些變量可以寫為 ( θ i w i j ) δ i j (\theta_i w_{ij})\delta_{ij} (θiwij)δij的形式,其中 w i j w_{ij} wij為第 i i i個特征的第 j j j個分箱的WOE值, δ i j \delta_{ij} δij是取值為0,1的變量,當 δ i j = 1 \delta_{ij}=1 δij=1時,錶示特征 i i i取第 j j j個分箱值, δ i j = 0 \delta_{ij}=0 δij=0時,錶示特征 i i i不取第 j j j個分箱值,最終得到評分卡模型為:
    S c o r e = A − B { θ 0 + ( θ 1 w 11 ) δ 11 + ( θ 1 w 12 ) δ 12 + . . . . . . . . . + ( θ n w n 1 ) δ n 1 + ( θ n w n 2 ) δ n 2 + . . . } (15) Score=A-B\begin{Bmatrix} \theta_0\\ +(\theta_1 w_{11})\delta_{11}+(\theta_1 w_{12})\delta_{12}+...\\ ......\\ +(\theta_n w_{n1})\delta_{n1}+(\theta_n w_{n2})\delta_{n2}+... \end{Bmatrix}\tag{15} Score=ABθ0+(θ1w11)δ11+(θ1w12)δ12+.........+(θnwn1)δn1+(θnwn2)δn2+...(15)

二、實現

2.1 數據導入與預處理

#導入所需要的包
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
import seaborn as sns
import math
import warnings
warnings.filterwarnings('ignore')
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import SMOTE
from sklearn.linear_model import LogisticRegression
import sklearn.metrics as metrics

#數據讀取與展示
test=pd.read_csv('./data/give me some credit/cs-test.csv')
del test['Unnamed: 0'] #删除無用的列
train=pd.read_csv('./data/give me some credit/cs-train.csv')
del train['Unnamed: 0']
train.head()

在這裏插入圖片描述

在這裏插入圖片描述
變量SeriousDlqin2yrs是模型的label。其中1為壞,0為好。這個變量是意思是Serious Delinquent in 2 year,也就是2年內發生嚴重逾期,其中”嚴重“定義為逾期超過90天。例如你2019年4月1號是你的還款日,然後你在7月1號前都沒還錢,那這時候逾期就超過90天了,你的數據標簽就為1。

#查看數據是否缺失
train.info()

在這裏插入圖片描述

#數據描述性統計
train.describe()

在這裏插入圖片描述

#定義函數,計算缺失值及缺失比例
def cal_miss_value(df):
    null_val_sum=df.isnull().sum()
    null_df=pd.DataFrame({
    'Columns':null_val_sum.index,
                          'Number of null values':null_val_sum.values,
                          'Proportion':null_val_sum.values/len(df)})
    null_df['Proportion']=null_df['Proportion'].apply(lambda x:format(x, '.2%'))
    return null_df
train_null_df=cal_miss_value(train)
train_null_df

在這裏插入圖片描述

#缺失值處理-中比特數填充
train['NumberOfDependents']=train['NumberOfDependents'].fillna(train['NumberOfDependents'].median())
print('Train data:')
print(train.isnull().sum())

test['NumberOfDependents']=test['NumberOfDependents'].fillna(test['NumberOfDependents'].median())
print('\nTest data:')
print(test.isnull().sum())

在這裏插入圖片描述

#使用隨機森林對收入NumberOfDependents進行預測,來填充缺失值
names=list(train.columns)
names.remove('NumberOfDependents')
def rf_fill_nan(df, null_col):
    #分成已知該特征和未知該特征2部分
    known=df[df[null_col].notnull()]
    unknown=df[df[null_col].isnull()]
    #指定X和y
    X=known.drop([null_col], axis=1)
    y=known[null_col]
    model=RandomForestRegressor(random_state=0, n_jobs=-1).fit(X, y)
    pred=model.predict(unknown.drop([null_col], axis=1)).round(0)  #四舍五入取整
    df.loc[df[null_col].isnull(), null_col]=pred #缺失值進行填充
    return df
train=rf_fill_nan(train, 'MonthlyIncome')

#异常值處理--剔除age等於0的數據
train=train.loc[train['age']>0]

2.2 可視化分析

#可視化分析
sns.countplot(x='SeriousDlqin2yrs', data=train)
print('Default rate=%.4f'%(train['SeriousDlqin2yrs'].sum()/len(train)))
#結果
Default rate=0.0668

在這裏插入圖片描述

#客戶年齡分布
sns.distplot(train['age'])

在這裏插入圖片描述

2.3 數據分箱-計算IV值和WOE值

#數據分箱
''' 需要 qcut的: 等頻分箱,每個箱包含的樣本數是相同的 RevolvingUtilizationOfUnsecuredLines DebtRatio MonthlyIncome NumberOfOpenCreditLinesAndLoans NumberRealEstateLoansOrLines 需要 cut的: 等間隔分箱或者自定義分箱邊界 age NumberOfDependents NumberOfTime30-59DaysPastDueNotWorse NumberOfTimes90DaysLate NumberOfTime60-89DaysPastDueNotWorse '''
age_bins = [-math.inf, 25, 40, 50, 60, 70, math.inf]
train['bin_age'] = pd.cut(train['age'],bins=age_bins).astype(str)
dependent_bin = [-math.inf,2,4,6,8,10,math.inf]
train['bin_NumberOfDependents'] = pd.cut(train['NumberOfDependents'],bins=dependent_bin).astype(str)
dpd_bins = [-math.inf,1,2,3,4,5,6,7,8,9,math.inf]
train['bin_NumberOfTimes90DaysLate'] = pd.cut(train['NumberOfTimes90DaysLate'],bins=dpd_bins)
train['bin_NumberOfTime30-59DaysPastDueNotWorse'] = pd.cut(train['NumberOfTime30-59DaysPastDueNotWorse'], bins=dpd_bins)
train['bin_NumberOfTime60-89DaysPastDueNotWorse'] = pd.cut(train['NumberOfTime60-89DaysPastDueNotWorse'], bins=dpd_bins)

train['bin_RevolvingUtilizationOfUnsecuredLines'] = pd.qcut(train['RevolvingUtilizationOfUnsecuredLines'],q=5,duplicates='drop').astype(str)#q為分箱數,當有重複的邊界時,會報錯,將報錯drop掉
train['bin_DebtRatio'] = pd.qcut(train['DebtRatio'],q=5,duplicates='drop').astype(str)
train['bin_MonthlyIncome'] = pd.qcut(train['MonthlyIncome'],q=5,duplicates='drop').astype(str)
train['bin_NumberOfOpenCreditLinesAndLoans'] = pd.qcut(train['NumberOfOpenCreditLinesAndLoans'],q=5,duplicates='drop').astype(str)
train['bin_NumberRealEstateLoansOrLines'] = pd.qcut(train['NumberRealEstateLoansOrLines'],q=5,duplicates='drop').astype(str)
#寫一個計算IV的函數
def cal_IV(df, feature, target):
    lst=[]
    for i in df[feature].unique():
        lst.append([feature, 
                    i, 
                    len(df.loc[df[feature]==i]), 
                    len(df.loc[(df[feature]==i)&(df[target]==1)])])
    data=pd.DataFrame(lst, columns=['特征', '特征取值', '該特征取值下樣本數', '該特征取值下bad樣本數'])
    data=data.loc[data['該特征取值下bad樣本數']>0] #如果該取值下沒有bad樣本,則無法計算woe值
    data['woe的分子']=data['該特征取值下bad樣本數']/len(df.loc[df[target]==1])
    data['woe的分母']=(data['該特征取值下樣本數']-data['該特征取值下bad樣本數'])/len(df.loc[df[target]==0])
    data['woe']=np.log(data['woe的分子']/data['woe的分母'])
    data['iv']=(data['woe']*(data['woe的分子']-data['woe的分母'])).sum()
    return data
lst=[]
for j in train.columns:
    if j.startswith('bin_'): #判斷字符串以bin_開頭,endswith是判斷結尾
        lst.append([j, cal_IV(train, j, 'SeriousDlqin2yrs')['iv'][0]])
data_iv=pd.DataFrame(lst, columns=['feature', 'iv'])
data_iv=data_iv.sort_values(by='iv', ascending=False).reset_index(drop=True) #按IV值進行降序排列
data_iv

在這裏插入圖片描述
在這裏插入圖片描述
我們選擇IV值之大於0.1的特征進行預測。

used_cols=data_iv.loc[data_iv['iv']>0.1]['feature'].tolist()
used_cols
#結果
['bin_RevolvingUtilizationOfUnsecuredLines',
 'bin_NumberOfTime30-59DaysPastDueNotWorse',
 'bin_NumberOfTimes90DaysLate',
 'bin_NumberOfTime60-89DaysPastDueNotWorse',
 'bin_age']
#定義一個函數cal_WOE,用以把分箱轉成WOE值
def cal_WOE(df, features, target):
    df_new=df
    for f in features:
        df_woe=df_new.groupby(f).agg({
    target:['sum', 'count']})
        df_woe.columns = list(map(''.join, df_woe.columns))
        df_woe = df_woe.reset_index()
        df_woe = df_woe.rename(columns = {
    target+'sum':'bad'})
        df_woe = df_woe.rename(columns = {
    target+'count':'all'})
        df_woe['good']=df_woe['all']-df_woe['bad']
        df_woe['bad_rate']=df_woe['bad']/df_woe['bad'].sum()
        df_woe['good_rate']=df_woe['good']/df_woe['good'].sum()
        df_woe['woe'] = df_woe['bad_rate'].divide(df_woe['good_rate'],fill_value=1)#分母為0時,用1填充
        #此woe值未取對數,對數轉換後可能造成出現無窮小
        df_woe.columns = [c if c==f else c+'_'+f for c in list(df_woe.columns)]
        df_new=df_new.merge(df_woe, on=f, how='left')
    return df_new

df_woe = cal_WOE(train,used_cols,'SeriousDlqin2yrs')
woe_cols = [c for c in list(df_woe.columns) if 'woe' in c]
df_woe[woe_cols]

在這裏插入圖片描述

d=pd.DataFrame()
for j in train.columns:
    if j.startswith('bin_'): #判斷字符串以bin_開頭,endswith是判斷結尾
        d=d.append(cal_IV(train, j, 'SeriousDlqin2yrs'))
d

在這裏插入圖片描述

2.4 baseline模型搭建與評估

#建立邏輯斯蒂回歸-取出20%的數據作為驗證集
X_train, X_validation, y_train, y_validation=train_test_split(df_woe[woe_cols],  df_woe['SeriousDlqin2yrs'], test_size=0.2, random_state=42)
print("train set's bad rate is: %.4f"% (y_train.sum()/y_train.count()))
print("validation set's bad rate is: %.4f"% (y_validation.sum()/y_validation.count()))
#結果
train set's bad rate is: 0.0672
validation set's bad rate is: 0.0653
print(np.isinf(X_train).any()) #判斷是否為無窮大
print(np.isfinite(X_train).any()) #判斷是否為有限的數字
print(np.isnan(X_train).any()) #判斷是否存在缺失值

在這裏插入圖片描述

#采用SMOTE進行數據平衡,效果不好,所以未采用
# X_train_smote, y_train_smote = SMOTE(random_state=42).fit_resample(X_train, y_train)
# print("After SMOTE, train set's bad rate is: %.4f"% (y_train_smote.sum()/y_train_smote.count()))
#Logistic Regression作為baseline模型,常用於風控領域
#model_smote=LogisticRegression(random_state=42).fit(X_train_smote, y_train_smote)
model=LogisticRegression(random_state=42).fit(X_train, y_train)
print("Model's parameters:", model.coef_)
prob=model.predict_proba(X_validation)[:,1] #返回2列數據,第0列是0類別的概率,第1列是1類別的概率
fpr,tpr, threshold=metrics.roc_curve(y_validation, prob)
roc_auc=metrics.auc(fpr,tpr)

#繪制ROC曲線
plt.plot(fpr,tpr,'b',label="AUC=%.2f"%roc_auc)
plt.title('ROC Curve')
plt.legend(loc='best')
plt.plot([0,1],[0,1],'r--')
plt.xlim([0,1])
plt.ylim([0,1])
plt.xlabel('fpr')
plt.ylabel('tpr')
plt.show()

#計算混淆矩陣
y_pred=model.predict(X_validation)
print('Confusion_matrix:\n',metrics.confusion_matrix(y_validation, y_pred))
#計算分類指標
print('\nAccuracy:\n %.4f'%metrics.accuracy_score(y_validation, y_pred))
target_names = ['label=0','label=1']
print('\nPrecision_recall_f1-score:\n',metrics.classification_report(y_validation,y_pred,target_names = target_names))

在這裏插入圖片描述

2.5 評分卡轉換

#評分卡轉換
#設置基礎分P0為650,PDO為50,我們定義theta_0為1:1(錶示p/1-p=1:1),也可以采用其他值
B=50/np.log(2)
A=650+B*np.log(1/1)

def generate_scorecard(model_coef, bin_df, features, B):
    lst=[]
    cols=['Variable','Binning','Score']
    coef=model_coef[0]
    for i in range(len(features)):
        f=features[i]
        df=bin_df[bin_df['特征']==f]
        for index, row in df.iterrows():
            lst.append([f, row['特征取值'], int(-coef[i]*row['woe']*B)])
    data=pd.DataFrame(lst, columns=cols)
    return data

score_card = generate_scorecard(model.coef_, d, used_cols, B)
score_card

在這裏插入圖片描述

#進行排序
sort_scorecard=score_card.groupby(by='Variable').apply(lambda x:x.sort_values('Score', ascending=False))
sort_scorecard

在這裏插入圖片描述
總體來說評分符合預期。

2.6 驗證評分卡效果

#為了驗證評分卡的效果,我們各選五個SeriousDlqin2yrs == 0和SeriousDlqin2yrs == 1
#並固定一個random state。

def str_to_int(s):
    if s == '-inf':
        return -999999999.0
    elif s=='inf':
        return 999999999.0
    else:
        return float(s)

def map_value_to_bin(feature_value, feature_to_bin):
    for idx, row in feature_to_bin.iterrows():
        bins=str(row['Binning'])
        left_open=bins[0]=='('
        right_open=bins[-1]==')'
        binnings=bins[1:-1].split(',')
        in_range=True
        
        # check left bound
        if left_open:
            if feature_value<=str_to_int(binnings[0]):
                in_range=False
        else:
            if feature_value<str_to_int(binnings[0]):
                in_range=False
        
        #check right bound
        if right_open:
            if feature_value>= str_to_int(binnings[1]):
                in_range = False
        else:
            if feature_value> str_to_int(binnings[1]):
                in_range = False
        if in_range: #in_range==True時
            return row['Binning']
    return null

def map_to_score(df, score_card):
    scored_columns=list(i.split('_')[1] for i in score_card['Variable'].unique())
    score=0
    for col in scored_columns:
        bin_col='bin_'+col
        feature_to_bin=score_card[score_card['Variable']==bin_col]
        feature_value=df[col]
        selected_bin=map_value_to_bin(feature_value, feature_to_bin)
        selected_record_in_scorecard=feature_to_bin[feature_to_bin['Binning'] == selected_bin]
        score+=selected_record_in_scorecard['Score'].iloc[0]
    return score

def calculate_score_with_card(df, score_card, A):
    df['score']=df.apply(map_to_score, args=(score_card,), axis=1)
    df['score']=df['score']+A
    df['score']=df['score'].astype(int)
    return df

#生成樣本
row_cols=[i.split('_')[1] for i in used_cols]#取出未轉換為分箱的原始5個特征
pred_cols=row_cols+used_cols
good_sample=train[train['SeriousDlqin2yrs']==0].sample(5, random_state=1)[pred_cols]
bad_sample=train[train['SeriousDlqin2yrs']==1].sample(5, random_state=1)[pred_cols]
#開始計算評分--好客戶
calculate_score_with_card(good_sample, score_card, A)
calculate_score_with_card(good_sample, score_card, A)

在這裏插入圖片描述

#開始計算評分--壞客戶
calculate_score_with_card(bad_sample, score_card, A)
calculate_score_with_card(bad_sample, score_card, A)

在這裏插入圖片描述
可以看到,好樣本分數評分都比壞樣本分數高,說明了評分卡的有效性。

2.7 總結

  • 由於測試集label信息未知,所以無法計算IV值和WOE值
  • 分箱可以進行優化,可以才測試集上使用分箱結果

三、參考網址

  1. https://mp.weixin.qq.com/s/5BPb-wDauPvDZkTc2euROQ
  2. https://mp.weixin.qq.com/s/5cJ5Yix_3up2sAixJd79Zw
  3. https://mp.weixin.qq.com/s/eGjgCkgtupolyT4BccAANQ
  4. https://www.kaggle.com/orange90/credit-scorecard-example/notebook
  5. https://zhuanlan.zhihu.com/p/148102950

版權聲明
本文為[西西先生666]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/134/202205140750391853.html

隨機推薦