天天看點

申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

申請者評分模型(A卡)開發(基于邏輯回歸)

1項目背景

申請者評分模型應用在信貸場景中的貸款申請環節,主要是以申請者的曆史資訊為基礎,預測未來放款後逾期或者違約的機率,為銀行客戶關系管理提供資料依據,進而有效的控制違約風險。

2開發流程

本次模組化基本流程:

1.資料準備:收集并整合在庫客戶的資料,定義目标變量,排除特定樣本。

2.探索性資料分析:評估每個變量的值分布情況,處理異常值和缺失值。

3.資料預處理:變量篩選,變量分箱,WOE轉換、分割訓練集測試集。

4.模型開發:邏輯回歸拟合模型。

5.模型評估:常見幾種評估方法,ROC、KS等。

6.生成評分卡

标準評分卡開發流程如下圖所示(基于邏輯回歸):

申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

3資料準備

3.1樣本選取

首先根據準入規則(如年齡、在網時長等)、行内黑名單過濾客戶,再通過反欺詐模型過濾客戶,得到用于建立信用評分卡的樣本。

3.2資料說明

本次模組化資料一共用到三個表:

“CreditFirstUse”:客戶首次使用信用卡時間資訊表

“CreditSampleWindow”:客戶曆史違約資訊表(基于客戶編号)

“Data_Whole”:客戶基本資訊表

3.3定義目标變量

申請者評分模型需要解決的問題是未來一段時間(如12個月)客戶出現違約(如至少一次90天或90天以上逾期)的機率。在這裡“12個月”為“觀察時間視窗”,“至少一次90天或90天以上逾期”為表現時間視窗即違約日期時長,那麼我們如何确定觀察時間視窗和違約日期時長(如M2算違約,還是M3算違約)呢?

3.3.1定義違約日期時長(表現時間視窗)

sample_window=pd.read_csv("CreditSampleWindow.csv")
sample_window.head()
sample_window.shape
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
#檢視缺失值所占比例
sample_window.isnull().sum()/sample_window.shape[0]
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
# 選取某一個ID檢視資料結構
sample_record = sample_window[sample_window.CID == sample_window.iat[4,0]]
sample_record.sort_values('START_DATE')
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
#去掉重複值
sample_window.drop_duplicates(inplace=True)
           
#去掉沒有逾期階段記錄的資訊
sample_window.dropna(subset=['STAGE_BEF','STAGE_AFT'],inplace=True)
sample_window.shape
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
#取每個 ID 每個月份的最高逾期記錄也就是STAGE_AFT作為該月份的逾期名額
sample_window['START_MONTH']=sample_window.START_DATE.apply(lambda x: int(x//100))  #取年月
sample_window['CLOSE_MONTH']=sample_window.CLOSE_DATE.apply(lambda x: int(x//100))
sample_window['AFT_FLAG']=sample_window.STAGE_AFT.apply(lambda x:int(x[-1]))  #取數字
sample_window.head()

#因為選取資料的時間是有一個節點的,由于系統原因,截至時間節點為0了
#是以将 CLOSE_DATE 為0的資料填補為 201806(根據缺失的業務背景确定)
sample_window.loc[sample_window.CLOSE_MONTH==0,'CLOSE_MONTH']=201806
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
# 提取 ID、月份、月份對應狀态作為新的資料
overdue = sample_window.loc[:,["CID","START_MONTH","AFT_FLAG"]]\
.rename(columns={"START_MONTH":"CLOSE_MONTH"})\
.append(sample_window.loc[:,["CID","CLOSE_MONTH","AFT_FLAG"]],ignore_index=True)

# 生成每個訂單的逾期資訊,以表格形式。提取當月最差的狀态
overdue = overdue.sort_values(by=["CID","CLOSE_MONTH","AFT_FLAG"])\
.drop_duplicates(subset=["CID","CLOSE_MONTH"],keep="last")\
.set_index(["CID","CLOSE_MONTH"]).unstack(1)  #unstack索引的級别,level=1
overdue.columns = overdue.columns.droplevel()   #删除列索引上的levels
overdue.head(2)
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

建構轉移矩陣,橫坐标(行)表示轉移前狀态,縱坐标(列)表示下一個月狀态(轉移後) , 選取連續兩個月的有記錄的,記錄逾期階段遷移,計算 count 錄入轉移矩陣。

import collections

def get_mat(df):
    trans_mat=pd.DataFrame(data=0,columns=range(10),index=range(10))
    counter=collections.Counter()
    for i,j in zip(df.columns,df.columns[1:]):
        select = (df[i].notnull()) & (df[j].notnull()) #選取連續兩個月有記錄的
        counter += collections.Counter(tuple(x) for x in df.loc[select, [i,j]].values)  #連續兩個月的預期階段轉移

    for key in counter.keys():
        trans_mat.loc[key[0],key[1]]=counter[key]  #将對應的值放進轉移矩陣

    trans_mat['all_count']=trans_mat.apply(sum,axis=1) #對行進行彙總

    bad_count = []
    for j in range(10):
        bad_count.append(trans_mat.iloc[j,j+1:10].sum()) #計算轉壞的數量,行表示上個月,清單示這個月
    trans_mat['bad_count']=bad_count     

    trans_mat['to_bad']=trans_mat.bad_count/trans_mat.all_count  #計算轉壞的比例
    return trans_mat

get_mat(overdue)
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

僅僅從轉移矩陣來看,在逾期階段到了 M2 時, 下一階段繼續轉壞的機率達到了 67%,逾期階段到達 M3 階段時,下一階段繼續轉壞的機率為 86%,可根據業務需要(營銷、風險等等)來考慮定義進入 M2 或M3 階段的使用者為壞客戶。這裡由于資料的原因我們暫定為 M4。

3.3.2定義觀察時間視窗

first_use=pd.read_csv("CreditFirstUse.csv",encoding="utf-8")
first_use.set_index("CID",inplace=True)
first_use["FST_USE_MONTH"]=first_use.FST_USE_DT.map(lambda x:x//100)

#計算每一筆訂單第一次出現逾期 M2的月份的索引的位置
def get_first_overdue(ser):
    array=np.where(ser>=2)[0]
    if array.size>0:
        return array[0]
    else:
        return np.nan


OVER_DUE_INDEX=overdue.apply(get_first_overdue,axis=1)
first_use['OVERDUE_INDEX']=OVER_DUE_INDEX
 
# FST_USE_MONTH在over_due的列索引中的index
first_use["START_INDEX"] =first_use.FST_USE_MONTH.map({k:v for v,k in   enumerate(overdue.columns)})
first_use.loc[first_use.OVERDUE_INDEX.notnull()].head()
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
#檢視異常資料
first_use.loc[first_use.OVERDUE_INDEX<first_use.START_INDEX]
sample_window.loc[sample_window.CID=="CID0164451"]

#在原始資料中最開始使用的時間是2015年
#删除這條資料
first_use.drop('CID0164451',inplace=True)

#計算使用信用卡和首次逾期之間的月份差,并計數
month_count=(first_use.OVERDUE_INDEX-first_use.START_INDEX).value_counts().sort_index()  #注意,畫圖的時候要排序
month_count.plot()
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
#逾期月份累計分布
month_count.cumsum().plot()
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

理想情況下,累計分布曲線會在某個月開始收斂。

在這裡不收斂,根據業務通常定義24個月。

3.3.4好壞客戶标簽(y)的定義

我們定義在24個月逾期90天的客戶為壞客戶。

4探索性資料分析

4.1異常值

通過表檢視異常值

train_data=pd.read_csv("Data_Whole.csv",index_col=0)
train_data.describe().T
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

(1)分析RevolvingUtilizationOfUnsecuredLines

一般情況下為0-1.有大于1的情況:

1.主動申請提額後額度回調,一般不會大于2

2.高風險欠款後貸款額度被調的很低時,這個比例會變大

(2)分析age

age最小值為0,去看age=0的有多少

(train_data.age==0).sum()
train_data=train_data[train_data.age>0]  #隻有一條資料,删除
           

通過箱線圖檢視異常值

columns = ["NumberOfTime30-59DaysPastDueNotWorse",
"NumberOfTime60-89DaysPastDueNotWorse",
"NumberOfTimes90DaysLate"]
train_data[columns].plot.box(vert=False)
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

删除異常值

for col in columns:
        train_data = train_data[train_data[col]<90]
           

4.2缺失值

缺失值大于50%或者70%的變量,不考慮這個變量。

缺失值特别小,0.1%,删除缺失值。

缺失值補全的方法:

1.單一值補全

數值型變量:樣本均值或中位數

分類型變量:新增一個類别

2.分組補全(利用與其相關性較強的變量)

數值型變量:各分組均值或各分組中位數

3.模型預測

各種變量:利用多變量組合模型預測缺失值

4.WOE補全

各種變量:直接計算WOE(僅限于LR模型)

train_data.notnull().sum()/train_data.shape[0]

#檢視皮爾遜相關系數
train_data.corr()
           

4.2.1單一值補全

def single_value_imp(df, var, fill):
    
    # df: 輸入資料名
    # var: 需要補全的變量名
    # fill: 填充種類 (1. mean; 2. median)
    
    out = df.copy()
    cnt = len(var)
    
    for i in range(cnt):
       x = var[i] 
       if fill[i] == 1:
           out[x].loc[out[x].isnull()] = out[x].describe()[1] 
       if fill[i] == 2:
           out[x].loc[out[x].isnull()] = out[x].describe()[5]
    
    return out   
    
temp_1 = single_value_imp(train_data, ['NumberOfDependents' , 'MonthlyIncome'], [2, 2])
temp_1.isnull().sum()
           

4.2.2分組補全

def grp_value_imp(df, var, col, bins, fill):
    
    # df: 輸入資料名
    # var: 需要補全的變量名
    # col: 分組的變量
    # bins:變量分組cutoff
    # fill: 填充種類 (1. mean; 2. median)
    
    temp = df.copy()
    
    #分箱
    temp[col + '_grp'] = pd.cut(temp[col], bins)
        
    #組内統計
    if fill == 1:
        grp_stat = pd.DataFrame(temp.groupby(col + '_grp')[var].mean()).rename(columns = {var: var + '_fill'})

    if fill == 2:
        grp_stat = pd.DataFrame(temp.groupby(col + '_grp')[var].median()).rename(columns = {var: var + '_fill'})
    
    #分組補全
    temp = pd.merge(temp, grp_stat, how = 'left', left_on = col + '_grp', right_index = True)
    temp[var] = temp[var].fillna(temp[var + '_fill'])
    
    result = temp.drop([col + '_grp', var + '_fill'], axis = 1)
    
    return result

train_data['age'].describe().T
sub_1 = grp_value_imp(df = train_data, var = 'NumberOfDependents', col = 'age', bins = [-np.inf, 30, 40, 50, 60, 70, np.inf], fill = 2)

train_data['NumberRealEstateLoansOrLines'].describe([.90, .95, .99]).T
temp_2 = grp_value_imp(df = sub_1, var = 'MonthlyIncome', col = 'NumberRealEstateLoansOrLines', bins = [-np.inf, 0, 1, 2, 3, np.inf], fill = 2)
temp_2.isnull().sum()
           

4.2.3模型預測補全

import lightgbm as lgb
def fill_missing(data, to_fill, fill_type):

# data: 輸入資料名
# to_fill: 需要補全的變量名
# fill_type: 填充種類 (1. 分類; 2. 回歸)

df = data.copy()
columns = data.columns.values.tolist()
columns.remove(to_fill)
X = df.loc[:,columns]
y = df.loc[:,to_fill]
X_train = X.loc[df[to_fill].notnull()]
X_pred = X.loc[df[to_fill].isnull()]
y_train = y.loc[df[to_fill].notnull()]
if fill_type == 1:
    model = lgb.LGBMClassifier()
else:
    model = lgb.LGBMRegressor()
model.fit(X_train,y_train)
pred = model.predict(X_pred).round()
df.loc[df[to_fill].isnull(), to_fill] = pred
return df
           

4.2.4三種補全方法的比較

print(pd.DataFrame({'Original': train_data['NumberOfDependents'].describe([.9, .95, .99]),
                    'Single_value': temp_1['NumberOfDependents'].describe([.9, .95, .99]),
                    'Group_value': temp_2['NumberOfDependents'].describe([.9, .95, .99]),
                    'Model_Prediction': temp_3['NumberOfDependents'].describe([.9, .95, .99])}))

print(pd.DataFrame({'Original': train_data['MonthlyIncome'].describe([.9, .95, .99]),
                    'Single_value': temp_1['MonthlyIncome'].describe([.9, .95, .99]),
                    'Group_value': temp_2['MonthlyIncome'].describe([.9, .95, .99]),
                    'Model_Prediction': temp_3['MonthlyIncome'].describe([.9, .95, .99])}))
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

如果不希望改變原始資料的分布,選擇模型補全的方法。

5資料預處理

5.1資料集劃分

原始資料集已經分好

申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
OOT = temp_3[temp_3.Sample == 2].drop(['Sample'], axis = 1)
DEV = temp_3[temp_3.Sample == 0].drop(['Sample'], axis = 1)
OOS = temp_3[temp_3.Sample == 1].drop(['Sample'], axis = 1)

#導出
DEV.to_csv("C:\Work Station\CDA\Spyder\Data\dev.csv")  
OOS.to_csv("C:\Work Station\CDA\Spyder\Data\oos.csv")  
OOT.to_csv("C:\Work Station\CDA\Spyder\Data\oot.csv")  
           

5.2變量篩選

5.2.1IV值篩選

dev = pd.read_csv("dev.csv", index_col = 0, engine = "python")
from auto_bin import auto_bin
## 對每一個變量進行分析,選擇合适的分箱個數
model_data.columns

# 自動分箱的添加
auto_col_bins = {"RevolvingUtilizationOfUnsecuredLines": 10,
                 "age": 7,
                 "DebtRatio": 10,
                 "MonthlyIncome": 9}

# 用來儲存每個分組的分箱資料
bins_of_col = {}

# 生成自動分箱的分箱區間和分箱後的 IV 值
for col in auto_col_bins:
    # print(col)
    bins_df = auto_bin(dev, col, "SeriousDlqin2yrs",
                   n = auto_col_bins[col],
                   iv=False,detail=False,q=20)
    bins_list = list(sorted(set(bins_df["min"])\
                .union(bins_df["max"])))
    # 保證區間覆寫使用 np.inf 替換最大值 -np.inf 替換最小值
    bins_list[0],bins_list[-1] = -np.inf,np.inf
    bins_of_col[col] = bins_list

# 手動分箱的添加
# 不能使用自動分箱的變量
hand_bins = {
  "NumberOfTime30-59DaysPastDueNotWorse": [0, 1, 2, 3],
  "NumberOfOpenCreditLinesAndLoans": [0, 1, 3],
  "NumberOfTimes90DaysLate": [0, 1],
  "NumberRealEstateLoansOrLines": [0],
  "NumberOfTime60-89DaysPastDueNotWorse": [0, 1],
  "NumberOfDependents":[0, 1, 2, 3]}

# 保證區間覆寫使用 np.inf 替換最大值 以及  -np.inf 
hand_bins = {k:[-np.inf,*v, np.inf] for k,v in hand_bins.items()}

# 合并手動分箱資料
bins_of_col.update(hand_bins)
           
# 計算分箱資料的 IV 值
def get_iv(df,col,y,bins):
    df = df[[col,y]].copy()
    df["cut"] = pd.cut(df[col],bins)
    bins_df = df.groupby("cut")[y].value_counts().unstack()
    bins_df["br"] = bins_df[1] / (bins_df[0] + bins_df[1])
    bins_df["woe"] = np.log((bins_df[0] / bins_df[0].sum()) /
                    (bins_df[1] / bins_df[1].sum()))
    iv = np.sum((bins_df[0] / bins_df[0].sum() -
    bins_df[1] / bins_df[1].sum())*bins_df.woe)
    return iv ,bins_df

# 儲存 IV 值資訊
info_values = {}
# 儲存 woe 資訊
woe_values = {}
for col in bins_of_col:
    iv_woe = get_iv(dev,col,
        "SeriousDlqin2yrs",
        bins_of_col[col])
    info_values[col], woe_values[col] = iv_woe

#畫IV值直方圖    
def plt_iv(info_values):
    keys,values = zip(*info_values.items())
    nums = range(len(keys))
    plt.barh(nums,values)
    plt.yticks(nums,keys)
    for i, v in enumerate(values):
        plt.text(v, i-.2, f"{v:.3f}")
plt_iv(info_values)
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

删除iv值小于0.03的變量,這裡不需要删除。

# DebtRatio為U型
#分析DebtRatio分布
sc.Eq_Bin_Plot(train_data = dev, test_data = dev, col = "DebtRatio", target = 'SeriousDlqin2yrs' , k = 10, special = 9999)
# For DebtRatio <= 1
sc.Eq_Bin_Plot(train_data = dev[dev.DebtRatio <= 1], test_data = dev[dev.DebtRatio <= 1], col = "DebtRatio", target = 'SeriousDlqin2yrs' , k = 5, special = 9999)   #單調上升
# For DebtRatio > 1
sc.Eq_Bin_Plot(train_data = dev[dev.DebtRatio > 1], test_data = dev[dev.DebtRatio > 1], col = "DebtRatio", target = 'SeriousDlqin2yrs' , k = 5, special = 9999)   #單調下降
# Sample Bias caused problem - DebtRatio資産負債越大風險越小?
#  資産負債高的已經在前面拒絕掉了,隻能剔除此變量
           
#分箱并WOE指派
dev_woe = dev.copy()
for col in bins_of_col:
    dev_woe[col + '_woe'] = pd.cut(dev[col], bins_of_col[col])\
        .map(woe_values[col]["woe"])

oos = pd.read_csv('oos.csv', encoding="utf8", index_col = 0, engine = "python")
oos_woe = oos.copy()
for col in bins_of_col:
    oos_woe[col + '_woe'] = pd.cut(oos[col], bins_of_col[col])\
        .map(woe_values[col]["woe"])

oot = pd.read_csv('oot.csv', encoding="utf8", index_col = 0, engine = "python")
oot_woe = oot.copy()
for col in bins_of_col:
    oot_woe[col + '_woe'] = pd.cut(oot[col], bins_of_col[col])\
        .map(woe_values[col]["woe"])
       
dev_woe.to_csv("dev_woe.csv")
oos_woe.to_csv("oos_woe.csv")
oot_woe.to_csv("oot_woe.csv")
           

5.2.2PSI篩選

dev_woe = pd.read_csv("dev_woe.csv", index_col = 0)
oos_woe = pd.read_csv("oos_woe.csv", index_col = 0)
oot_woe = pd.read_csv("oot_woe.csv", index_col = 0)

def PSI_Cal(df1,df2,var,grp):
    A=pd.DataFrame(df1.groupby(by=grp)[var].count()).rename(columns={var:var+'_1'})
    B=pd.DataFrame(df2.groupby(by=grp)[var].count()).rename(columns={var:var+'_2'})
    C=pd.merge(A,B,how='left',left_index=True,right_index=True)
    PSI_df=C/C.sum()
    PSI_df['log']=np.log(PSI_df[var+'_1'])/PSI_df[var+'_2']
    PSI_df['PSI']=(PSI_df[var+'_1']-PSI_df[var+'_2'])*PSI_df['log']
    return PSI_df['PSI'].sum()
    #變量名單
    v_list = dev.drop(['SeriousDlqin2yrs'], axis = 1).columns

#計算PSI
psi_list = []
for col in v_list:   
        psi = PSI_Cal(dev_woe, oos_woe, col, col + '_woe')
        psi_list.append(psi)
psi_df = pd.DataFrame(psi_list).rename(columns = {0: 'PSI'})
psi_df.index = v_list 
psi_df
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

可以看出這些變量在不同的樣本中都是比較穩定的。

6logistic模型的建立

6.1建立線性回歸模型

import statsmodels.api as sm
# 隻保留WOE資料
ll = []
for col in dev_woe.columns:
    if col.endswith("_woe"):
        ll.append(col)
data = dev_woe.loc[:,ll]
data["SeriousDlqin2yrs"] = dev_woe["SeriousDlqin2yrs"]
# Pearson Correlation
x_corr = data.drop('SeriousDlqin2yrs', axis = 1).corr()


import statsmodels.api as sm


Y = data['SeriousDlqin2yrs']
x_exclude = ["SeriousDlqin2yrs"]
x=data.drop(x_exclude,axis=1)
X=sm.add_constant(x)  #添加一個截距的列到現存的矩陣
result=sm.Logit(Y,X).fit()
result.summary()
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

從上表可以看出,NumberOfOpenCreditLinesAndLoans_woe系數是正數,是多重共線性導緻的,删除該變量。

x_exclude = ["SeriousDlqin2yrs","NumberOfOpenCreditLinesAndLoans_woe"]
x=data.drop(x_exclude,axis=1)
X=sm.add_constant(x)  #添加一個截距的列到現存的矩陣
result=sm.Logit(Y,X).fit()
result.summary()
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

NumberOfDependents_woe>0.05,要不要删除呢?

不需要。因為0.05本身隻是約定俗成的,不是大于0.05就一定重要,模型重不重要是由人來确定的;對于預測型模型重點關注預測的結果、是否過拟合等問題,對變量本身的重要性不是很關心。

6.2檢查多重共線性,VIF

from statsmodels.stats.outliers_influence import variance_inflation_factor
vif = {}
for i in range(x.shape[1]):
    vif[x.columns[i]] = variance_inflation_factor(np.array(x), i)
vif
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

VIF都很小,其實VIF在這裡檢查不出來多重共線性。很多傳統的方法,用到大資料上會失效。

在回歸中,為什麼不能有多重共線性?

因為在回歸中,有個很大的假設,回歸中的某個自變量X1系數w1表示擋其他自變量不變時,x1每增加1,因變量增加w1

但是在實際中,多重共線性很普遍

在樹的模型中就不用考慮多重共線性

7模型評估

7.1ROC曲線&KS

def app_pred(df, x_list,y_col,result):
     '''
    :param df: 包含目标變量的資料集,dataframe
    :param x_list: 所有自變量的列名
    :param y_col: 目标變量,str
    :param result:傳回包含預測值'prob'的df
    :return: KS值
    '''
    df=df.copy()
    ll = []
    for col in df.columns:
        if col.endswith("_woe"):
            ll.append(col)
    data = df.loc[:,ll]
    data[y_col] = df[y_col]
    x=data.drop([y_col],axis=1)
    x1=sm.add_constant(x)
    result=result.predict(x1)
    df['prob']=result
    return df
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.index = range(len(all))
    all = all.sort_values(by=score,ascending=False)
    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)


import scikitplot as skplt
def ROC_plt(df, score, target):
    '''
    :param df: 包含目标變量與預測值的資料集,dataframe
    :param score: 得分或者機率,str
    :param target: 目标變量,str
    '''
    proba_df=pd.DataFrame(df[score])
    proba_df.columns=[1]
    proba_df.insert(0,0,1-proba_df)
    skplt.metrics.plot_roc(df[target], #y真實值
                       proba_df,  #y預測值
                      plot_micro=False, #繪制微平均ROC曲線
                      plot_macro=False); ##繪制宏觀平均ROC曲線

#訓練集預測
dev_woe.drop(["NumberOfOpenCreditLinesAndLoans_woe",'NumberOfOpenCreditLinesAndLoans'],axis=1,inplace=True)
dev_pred = app_pred(dev_woe, x_list,"SeriousDlqin2yrs",result)

#計算KS(Compare TPR and FPR - 不同門檻值下檢測出壞樣本比例和檢測錯的好樣本比例)
#We'd like TRP to be large and FRP otherwise
print(KS(dev_pred, "prob", "SeriousDlqin2yrs"))

#畫ROC圖,并計算AUC
ROC_plt(dev_pred, "prob", "SeriousDlqin2yrs")
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

為什麼有兩條線呢?

看你如何去定義好壞,1是好還是0是好

#測試集預測


oos_woe.drop(["NumberOfOpenCreditLinesAndLoans_woe",'NumberOfOpenCreditLinesAndLoans'],axis=1,inplace=True)
oos_pred = app_pred(oos_woe, x_list, "SeriousDlqin2yrs", result)

# 計算OOS的KS
print(KS(oos_pred, "prob", "SeriousDlqin2yrs"))

# 預測結果為對應 1 的機率,轉換為數組用于繪圖
ROC_plt(oos_pred, "prob", "SeriousDlqin2yrs")
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

從同時間的測試集無法看出模型是否過拟合,還要看跨時間的測試集。

#跨時間測試集


oot_woe.drop(["NumberOfOpenCreditLinesAndLoans_woe",'NumberOfOpenCreditLinesAndLoans'],axis=1,inplace=True)
oot_pred = app_pred(oot_woe, x_list, "SeriousDlqin2yrs", result)
    
# 計算OOT的KS
print(KS(oot_pred, "prob", "SeriousDlqin2yrs"))
    
# 預測結果為對應 1 的機率,轉換為數組用于繪圖
ROC_plt(oot_pred, "prob", "SeriousDlqin2yrs")
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

在現實中,跨時間測試集的Gini系數下降在10%以内,都是正常的。

8門檻值選擇

from sklearn.metrics import confusion_matrix,accuracy_score,precision_score,recall_score,f1_score
    #根據通過率選門檻值
def ar_select(df,prob,ar):
    loc=int(df.shape[0]*ar)
    ordered=df.sort_values([prob]).reset_index()
    return ordered.loc[loc,prob]
#根據壞賬率選門檻值
def br_select(df,target,prob,br,ar=0.3,close=0.001):
    cutoff_list=sorted(list(set(df[prob])))
    ar_cutoff=ar_select(df,prob,ar)
    loc=cutoff_list.index(ar_cutoff)
    
    for i in range(loc,len(cutoff_list)):
        cutoff=cutoff_list[i]
        p=np.where(df[prob]>=cutoff,1,0)
        cm=confusion_matrix(df[target],p)
        bad_rate=cm[1][0]/(cm[0][0]+cm[1][0])
        if abs(bad_rate-br)<close:
            break
        return cutoff

#畫混淆矩陣
def cm(df,y,pred):
    print({'混淆矩陣':confusion_matrix(y_true=df[y],y_pred=pred),
           '準确率':accuracy_score(y_true=df[y],y_pred=pred),
           '精準率':precision_score(y_true=df[y],y_pred=pred),
           '召回率':recall_score(y_true=df[y],y_pred=pred),
           'F1score':f1_score(y_true=df[y],y_pred=pred)
        
    }
    )



#根據通過率選擇門檻值
cut_off1 = ar_select(dev_pred, 'prob', 0.4)  #0.4的通過率
cut_off1
p_dev = np.where(dev_pred['prob'] > cut_off1, 1, 0)  
cm(dev_pred, "SeriousDlqin2yrs", p_dev)
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
#根據壞賬率選擇門檻值
cut_off2 = br_select(dev_pred, "SeriousDlqin2yrs", 'prob', br = 0.03, close = 0.005) #0.03的壞賬率
cut_off2
p_dev2 = np.where(dev_pred['prob'] > cut_off2, 1, 0)
cm(dev_pred, "SeriousDlqin2yrs", p_dev2)




#畫通過率和壞賬率的圖
def plt_a_b(df,target,prob):
app_rate=np.linspace(0,0.99,100)
cut_off_list=[]
bad_rate=[]
for i in range(len(app_rate)):
    loc=int(df.shape[0]*app_rate[i])
    ordered=df.sort_values([prob]).reset_index()
    sub_cut_off=ordered.loc[loc,prob]
    cut_off_list.append(sub_cut_off)
    pre=np.where(df[prob]>=sub_cut_off,1,0)
    cm=confusion_matrix(df[target],pre)
    sub_bad_rate=cm[1][0]/(cm[0][0]+cm[1][0])
    bad_rate.append(sub_bad_rate)
data={'cut_off':cut_off_list,'app_rate':app_rate,'bad_rate':bad_rate}
ab_table=pd.DataFrame(data)
#設定rc動态參數
plt.rcParams['font.sans-serif']=['Simhei']  #顯示中文
plt.rcParams['axes.unicode_minus']=False    #顯示負号   
plt.plot(app_rate,bad_rate)
plt.xlabel("通過率")
plt.ylabel("壞賬率")
return ab_table

cut_off_tab = plt_a_b(dev_pred, "SeriousDlqin2yrs", 'prob')
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

根據業務部門期望的通過率或者是能夠忍受的壞賬率來選擇對應的cut_off

在這裡選擇通過率為73%,壞賬率接近2%的cut_off2 = 0.057394

注意:為了與業務相聯系,通常壞賬率轉換為金額。

#計算測試集
def apply_cutoff(df,target,prob,cut_off):
    df=df.copy()
    pre=np.where(df[prob]>=cut_off,1,0)
    df['pre']=pre
    return df

cut_off2 = 0.057394
apply_cutoff(dev_pred, "SeriousDlqin2yrs", 'prob', cut_off2)
apply_cutoff(oos_pred, "SeriousDlqin2yrs", 'prob', cut_off2)
apply_cutoff(oot_pred, "SeriousDlqin2yrs", 'prob', cut_off2)

#測試集畫通過率和壞賬率的圖
LR_ab_tab = plt_a_b(oos_pred, "SeriousDlqin2yrs", 'prob')

LR_ab_tab.to_csv("ab_tab_LR.csv")
           
申請者評分模型(A卡)開發(基于邏輯回歸)申請者評分模型(A卡)開發(基于邏輯回歸)

9信用評分

  • score=A-B*log(odds)
  • 求解A,B需要兩個假設:
  • 1.特定違約機率下的預期分值
  • 2.指定違約機率翻倍的分數PDO
base_odds=1/40
base_score=700
PDO=40
B=PDO/np.log(2)
A=base_score+B*np.log(base_odds)


del woe_values['NumberOfOpenCreditLinesAndLoans']

b_score = A - B*result.params["const"]

para=result.params[1:]
para.index=para.index.map(lambda x:x[:-4])


file = "ScoreData1.csv"
with open(file,"w") as fdata:
    fdata.write(f"base_score,{base_score}\n")
for col in para.index:
    score = woe_values[col]["woe"] * (-B*para[col])
    score.name = "Score"
    score.index.name = col
    score.to_csv(file,header=True,mode="a")
           

繼續閱讀