申請者評分模型(A卡)開發(基于邏輯回歸)
1項目背景
申請者評分模型應用在信貸場景中的貸款申請環節,主要是以申請者的曆史資訊為基礎,預測未來放款後逾期或者違約的機率,為銀行客戶關系管理提供資料依據,進而有效的控制違約風險。
2開發流程
本次模組化基本流程:
1.資料準備:收集并整合在庫客戶的資料,定義目标變量,排除特定樣本。
2.探索性資料分析:評估每個變量的值分布情況,處理異常值和缺失值。
3.資料預處理:變量篩選,變量分箱,WOE轉換、分割訓練集測試集。
4.模型開發:邏輯回歸拟合模型。
5.模型評估:常見幾種評估方法,ROC、KS等。
6.生成評分卡
标準評分卡開發流程如下圖所示(基于邏輯回歸):
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
#檢視缺失值所占比例
sample_window.isnull().sum()/sample_window.shape[0]
# 選取某一個ID檢視資料結構
sample_record = sample_window[sample_window.CID == sample_window.iat[4,0]]
sample_record.sort_values('START_DATE')
#去掉重複值
sample_window.drop_duplicates(inplace=True)
#去掉沒有逾期階段記錄的資訊
sample_window.dropna(subset=['STAGE_BEF','STAGE_AFT'],inplace=True)
sample_window.shape
#取每個 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
# 提取 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)
建構轉移矩陣,橫坐标(行)表示轉移前狀态,縱坐标(列)表示下一個月狀态(轉移後) , 選取連續兩個月的有記錄的,記錄逾期階段遷移,計算 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)
僅僅從轉移矩陣來看,在逾期階段到了 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()
#檢視異常資料
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()
#逾期月份累計分布
month_count.cumsum().plot()
理想情況下,累計分布曲線會在某個月開始收斂。
在這裡不收斂,根據業務通常定義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
(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)
删除異常值
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])}))
如果不希望改變原始資料的分布,選擇模型補全的方法。
5資料預處理
5.1資料集劃分
原始資料集已經分好
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)
删除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
可以看出這些變量在不同的樣本中都是比較穩定的。
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()
從上表可以看出,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()
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
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")
為什麼有兩條線呢?
看你如何去定義好壞,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")
從同時間的測試集無法看出模型是否過拟合,還要看跨時間的測試集。
#跨時間測試集
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")
在現實中,跨時間測試集的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)
#根據壞賬率選擇門檻值
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')
根據業務部門期望的通過率或者是能夠忍受的壞賬率來選擇對應的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")
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")