鍵.png)
使用這些基本 REST API 最佳實(shí)踐構(gòu)建出色的 API
與其他機(jī)器學(xué)習(xí)的工業(yè)場景不同,金融是極其厭惡風(fēng)險(xiǎn)的領(lǐng)域,其特殊性在于非常側(cè)重模型的解釋性及穩(wěn)定性。業(yè)界通常的做法是基于挖掘多維度的特征建立一套可解釋及效果穩(wěn)定的規(guī)則及風(fēng)控模型對每筆訂單/用戶/行為做出判斷決策。
其中,對于(貸前)申請前的風(fēng)控模型,也稱為申請?jiān)u分卡–A卡。A卡是風(fēng)控的關(guān)鍵模型,業(yè)界共識是申請?jiān)u分卡可以覆蓋80%的信用風(fēng)險(xiǎn)。此外還有貸中行為評分卡B卡、催收評分卡C卡,以及反欺詐模型等等。
A卡(Application score card)。目的在于預(yù)測申請時(shí)(申請信用卡、申請貸款)對申請人進(jìn)行量化評估。B卡(Behavior score card)。目的在于預(yù)測使用時(shí)點(diǎn)(獲得貸款、信用卡的使用期間)未來一定時(shí)間內(nèi)逾期的概率。C卡(Collection score card)。目的在于預(yù)測已經(jīng)逾期并進(jìn)入催收階段后未來一定時(shí)間內(nèi)還款的概率。
一個(gè)好的特征,對于模型和規(guī)則都是至關(guān)重要的。像申請?jiān)u分卡–A卡,主要可以歸到以下3方面特征:
實(shí)戰(zhàn)部分我們以經(jīng)典的申請?jiān)u分卡為例,使用的中原銀行個(gè)人貸款違約預(yù)測比賽的數(shù)據(jù)集,使用信用評分python庫–toad、樹模型Lightgbm及邏輯回歸LR做申請?jiān)u分模型。(注:文中所涉及的一些金融術(shù)語,由于篇幅就不展開解釋了,疑問之處 可以谷歌了解下哈。)
申請?jiān)u分模型定義主要是通過一系列的數(shù)據(jù)分析確定建模的樣本及標(biāo)簽。
首先,補(bǔ)幾個(gè)金融風(fēng)控的術(shù)語的說明。概念模糊的話,可以回查再理解下:
逾期期數(shù)(M) :指實(shí)際還款日與應(yīng)還款日之間的逾期天數(shù),并按區(qū)間劃分后的逾期狀態(tài)。M取自Month on Book的第一個(gè)單詞。(注:不同機(jī)構(gòu)所定義的區(qū)間劃分可能存在差異) M0:當(dāng)前未逾期(或用C表示,取自Current) M1:逾期1-30日 M2:逾期31-60日 M3:逾期61-90日 M4:逾期91-120日 M5:逾期121-150日 M6:逾期151-180日 M7+:逾期180日以上
觀察點(diǎn):樣本層面的時(shí)間窗口。 用于構(gòu)建樣本集的時(shí)間點(diǎn)(如2010年10月申請貸款的用戶),不同環(huán)節(jié)定義不同,比較抽象,這里舉例說明:如果是申請模型,觀察點(diǎn)定義為用戶申貸時(shí)間,取19年1-12月所有的申貸訂單作為構(gòu)建樣本集;如果是貸中行為模型,觀察點(diǎn)定義為某個(gè)具體日期,如取19年6月15日在貸、沒有發(fā)生逾期的申貸訂單構(gòu)建樣本集。
觀察期:特征層面的時(shí)間窗口。構(gòu)造特征的相對時(shí)間窗口,例如用戶申請貸款訂前12個(gè)月內(nèi)(2009年10月截至到2010年10月申請貸款前的數(shù)據(jù)都可以用, 可以有用戶平均消費(fèi)金額、次數(shù)、貸款次數(shù)等數(shù)據(jù)特征)。設(shè)定觀察期是為了每個(gè)樣本的特征對齊,長度一般根據(jù)數(shù)據(jù)決定。一個(gè)需要注意的點(diǎn)是,只能用此次申請前的特征數(shù)據(jù),不然就會數(shù)據(jù)泄露(時(shí)間穿越,用未來預(yù)測過去的現(xiàn)象)。
表現(xiàn)期:標(biāo)簽層面的時(shí)間窗口。定義好壞標(biāo)簽Y的時(shí)間窗口,信貸風(fēng)險(xiǎn)具有天然的滯后性,因?yàn)橛脩艚杩詈笠粋€(gè)月(第一期)才開始還錢,有得可能還了好幾期才發(fā)生逾期。
對于現(xiàn)成的比賽數(shù)據(jù),數(shù)據(jù)特征的時(shí)間跨度(觀察期)、數(shù)據(jù)樣本、標(biāo)簽定義都是已經(jīng)提前分析確定下來的。但對于實(shí)際的業(yè)務(wù)來說,數(shù)據(jù)樣本及模型定義其實(shí)也是申請?jiān)u分卡的關(guān)鍵之處。畢竟實(shí)際場景里面可能沒有人扔給你現(xiàn)成的數(shù)據(jù)及標(biāo)簽(好壞定義,有些公司的業(yè)務(wù)會提前分析好給建模人員),然后只是跑個(gè)分類模型那么簡單。
確定建模的樣本量及標(biāo)簽,也就是模型從多少的數(shù)據(jù)樣本中學(xué)習(xí)如何分辨其中的好、壞標(biāo)簽樣本。如果樣本量稀少、標(biāo)簽定義有問題,那學(xué)習(xí)的結(jié)果可想而知也會是差的。
對于建模樣本量的確定,經(jīng)驗(yàn)上肯定是滿足建模條件的樣本越多越好,一個(gè)類別最好有幾千以上的樣本數(shù)。但對于標(biāo)簽的定義,可能我們直觀感覺是比較簡單,比如“好用戶就是沒有逾期的用戶, 壞用戶就是在逾期的用戶”,但具體做量化起來會發(fā)現(xiàn)并不簡單,有兩個(gè)方面的主要因素需要考量:
根據(jù)巴塞爾協(xié)議的指導(dǎo),一般逾期超過90天(M4+)的客戶,即定義為壞客戶。更為通用的,可以使用“滾動率”分析方法(Roll Rate Analysis)確定多少天算是“壞”,基本方法是統(tǒng)計(jì)分析出逾期M期的客戶多大概率會逾期M+1期(同樣的,我們不太可能等著所有客戶都逾期一年才最終確定他就是壞客戶。一來時(shí)間成本太高,二來這數(shù)據(jù)樣本會少的可憐)。如下示例,我們通過滾動率分析各期逾期的變壞概率。當(dāng)前未逾期(M0)下個(gè)月保持未逾期的概率99.71%;當(dāng)前逾期M1,下個(gè)月繼續(xù)逾期概率為54.34%;當(dāng)前M2下個(gè)月繼續(xù)逾期概率就高達(dá)90.04%。我們可以看出M2是個(gè)比較明顯的變壞拐點(diǎn),可以以M2+作為壞樣本的定義。
這也就是確定表現(xiàn)期,常用的分析方法是Vintage分析(Vintage在信貸領(lǐng)域不僅可以用它來評估客戶好壞充分暴露所需的時(shí)間,即成熟期,還可以用它分析不同時(shí)期風(fēng)控策略的差異等),通過分析歷史累計(jì)壞用戶暴露增加的趨勢,來確定至少要多少期可以比較全面的暴露出大部分的壞客戶。如下示例的壞定義是M4+,我們可以看出各期的M4+壞客戶經(jīng)過9或者10個(gè)月左右的表現(xiàn),基本上可以都暴露出來,后面壞客戶的總量就比較平穩(wěn)了。這里我們就可以將表現(xiàn)期定位9或者10個(gè)月~
確定了壞的定義以及需要的表現(xiàn)期,我們就可以確定樣本的標(biāo)簽,最終劃定的建模樣本:
比如現(xiàn)在的時(shí)間是2022-10月底,表現(xiàn)期9個(gè)月的話,就可以取2022-01月份及之前申請的樣本(這也稱為 觀察點(diǎn)),打上好壞標(biāo)簽,建模。
通過上面信用評分的介紹,很明顯的好用戶通常遠(yuǎn)大于壞用戶的,這是一個(gè)類別極不均衡的典型場景,不均衡處理方法下文會談到。
本數(shù)據(jù)集的數(shù)據(jù)字典文檔、比賽介紹及本文代碼,可以到https://github.com/aialgorithm/Blog項(xiàng)目相應(yīng)的代碼目錄下載
該數(shù)據(jù)集為中原銀行的個(gè)人貸款違約預(yù)測數(shù)據(jù)集,個(gè)別字段有做了脫敏(金融的數(shù)據(jù)大都涉及機(jī)密)。主要的特征字段有個(gè)人基本信息、經(jīng)濟(jì)能力、貸款歷史信息等等
數(shù)據(jù)有10000條樣本,38維原始特征,其中isDefault為標(biāo)簽,是否逾期違約。
import pandas as pd
pd.set_option("display.max_columns",50)
train_bank = pd.read_csv('./train_public.csv')
print(train_bank.shape)
train_bank.head()
數(shù)據(jù)預(yù)處理主要是對日期信息、噪音數(shù)據(jù)做下處理,并劃分下類別、數(shù)值類型的特征。
# 日期類型:issueDate 轉(zhuǎn)換為pandas中的日期類型,加工出數(shù)值特征
train_bank['issue_date'] = pd.to_datetime(train_bank['issue_date'])
# 提取多尺度特征
train_bank['issue_date_y'] = train_bank['issue_date'].dt.year
train_bank['issue_date_m'] = train_bank['issue_date'].dt.month
# 提取時(shí)間diff # 轉(zhuǎn)換為天為單位
base_time = datetime.datetime.strptime('2000-01-01', '%Y-%m-%d') # 隨機(jī)設(shè)置初始的基準(zhǔn)時(shí)間
train_bank['issue_date_diff'] = train_bank['issue_date'].apply(lambda x: x-base_time).dt.days
# 可以發(fā)現(xiàn)earlies_credit_mon應(yīng)該是年份-月的格式,這里簡單提取年份
train_bank['earlies_credit_mon'] = train_bank['earlies_credit_mon'].map(lambda x:int(sorted(x.split('-'))[0]))
train_bank.head()
# 工作年限處理
train_bank['work_year'].fillna('10+ years', inplace=True)
work_year_map = {'10+ years': 10, '2 years': 2, '< 1 year': 0, '3 years': 3, '1 year': 1,
'5 years': 5, '4 years': 4, '6 years': 6, '8 years': 8, '7 years': 7, '9 years': 9}
train_bank['work_year'] = train_bank['work_year'].map(work_year_map)
train_bank['class'] = train_bank['class'].map({'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6})
# 缺失值處理
train_bank = train_bank.fillna('9999')
# 區(qū)分 數(shù)值 或類別特征
drop_list = ['isDefault','earlies_credit_mon','loan_id','user_id','issue_date']
num_feas = []
cate_feas = []
for col in train_bank.columns:
if col not in drop_list:
try:
train_bank[col] = pd.to_numeric(train_bank[col]) # 轉(zhuǎn)為數(shù)值
num_feas.append(col)
except:
train_bank[col] = train_bank[col].astype('category')
cate_feas.append(col)
print(cate_feas)
print(num_feas)
如果是用Lightgbm建模做違約預(yù)測,簡單的數(shù)據(jù)處理,基本上代碼就結(jié)束了。lgb樹模型是集成學(xué)習(xí)的強(qiáng)模型,自帶缺失、類別變量的處理,特征上面不用做很多處理,建模非常方便,模型效果通常不錯(cuò),還可以輸出特征的重要性。
(By the way,申請?jiān)u分卡業(yè)界用邏輯回歸LR會比較多,因?yàn)槟P秃唵?,解釋性也比較好)。
def model_metrics(model, x, y):
""" 評估 """
yhat = model.predict(x)
yprob = model.predict_proba(x)[:,1]
fpr,tpr,_ = roc_curve(y, yprob,pos_label=1)
metrics = {'AUC':auc(fpr, tpr),'KS':max(tpr-fpr),
'f1':f1_score(y,yhat),'P':precision_score(y,yhat),'R':recall_score(y,yhat)}
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, 'k--', label='ROC (area = {0:.2f})'.format(roc_auc), lw=2)
plt.xlim([-0.05, 1.05]) # 設(shè)置x、y軸的上下限,以免和邊緣重合,更好的觀察圖像的整體
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate') # 可以使用中文,但需要導(dǎo)入一些庫即字體
plt.title('ROC Curve')
plt.legend(loc="lower right")
return metrics
# 劃分?jǐn)?shù)據(jù)集:訓(xùn)練集和測試集
train_x, test_x, train_y, test_y = train_test_split(train_bank[num_feas + cate_feas], train_bank.isDefault,test_size=0.3, random_state=0)
# 訓(xùn)練模型
lgb=lightgbm.LGBMClassifier(n_estimators=5,leaves=5, class_weight= 'balanced',metric = 'AUC')
lgb.fit(train_x, train_y)
print('train ',model_metrics(lgb,train_x, train_y))
print('test ',model_metrics(lgb,test_x,test_y))
from lightgbm import plot_importance
plot_importance(lgb)
LR即邏輯回歸,是一種廣義線性模型,因?yàn)槠淠P秃唵?、解釋性良好,在金融行業(yè)是最常用的。
也正因?yàn)長R過于簡單,沒有非線性能力,所以我們往往需要通過比較復(fù)雜的特征工程,如分箱WOE編碼的方法,提高模型的非線性能力。
下面我們通過toad實(shí)現(xiàn)特征分析、特征選擇、特征分箱及WOE編碼
# 數(shù)據(jù)EDA分析
toad.detector.detect(train_bank)
# 特征選擇,根據(jù)相關(guān)性 缺失率、IV 等指標(biāo)
train_selected, dropped = toad.selection.select(train_bank,target = 'isDefault', empty = 0.5, iv = 0.05, corr = 0.7, return_drop=True, exclude=['earlies_credit_mon','loan_id','user_id','issue_date'])
print(dropped)
print(train_selected.shape)
# 劃分訓(xùn)練集 測試集
train_x, test_x, train_y, test_y = train_test_split(train_selected.drop(['loan_id','user_id','isDefault','issue_date','earlies_credit_mon'],axis=1), train_selected.isDefault,test_size=0.3, random_state=0)
# 特征的卡方分箱
combiner = toad.transform.Combiner()
# 訓(xùn)練數(shù)據(jù)并指定分箱方法
combiner.fit(pd.concat([train_x,train_y], axis=1), y='isDefault',method= 'chi',min_samples = 0.05,exclude=[])
# 以字典形式保存分箱結(jié)果
bins = combiner.export()
bins
通過特征分箱,每一個(gè)特征被離散化為各個(gè)分箱。
接下來就是LR特征工程的特色處理了–手動調(diào)整分箱的單調(diào)性。
這一步的意義更多在于特征的業(yè)務(wù)解釋性的約束,對于模型的擬合效果影響不一定是正面的。這里我們主觀認(rèn)為大多數(shù)特征的不同分箱的壞賬率badrate應(yīng)該是滿足某種單調(diào)關(guān)系的,而起起伏伏是不太好理解的。如征信查詢次數(shù)這個(gè)特征,應(yīng)該是分箱數(shù)值越高,壞賬率越大。(注:如年齡特征可能就不滿足這種單調(diào)關(guān)系)
我們可以查看下ebt_loan_ratio這個(gè)變量的分箱情況,根據(jù)bad_rate趨勢圖,并保證單個(gè)分箱的樣本占比不低于0.05,去調(diào)整分箱,達(dá)到單調(diào)性。(其他的特征可以按照這個(gè)方法繼續(xù)調(diào)整,單調(diào)性調(diào)整還是挺耗時(shí)的)
adj_var = 'scoring_low'
#調(diào)整前原來的分箱 [560.4545455, 621.8181818, 660.0, 690.9090909, 730.0, 775.0]
adj_bin = {adj_var: [ 660.0, 700.9090909, 730.0, 775.0]}
c2 = toad.transform.Combiner()
c2.set_rules(adj_bin)
data_ = pd.concat([train_x,train_y], axis=1)
data_['type'] = 'train'
temp_data = c2.transform(data_[[adj_var,'isDefault','type']], labels=True)
from toad.plot import badrate_plot, proportion_plot
# badrate_plot(temp_data, target = 'isDefault', x = 'type', by = adj_var)
# proportion_plot(temp_data[adj_var])
from toad.plot import bin_plot,badrate_plot
bin_plot(temp_data, target = 'isDefault',x=adj_var)
# 更新調(diào)整后的分箱
combiner.set_rules(adj_bin)
combiner.export()
接下來就是對各個(gè)特征的分箱做WOE編碼,通過WOE編碼給各個(gè)分箱不同的權(quán)重,提升LR模型的非線性。
#計(jì)算WOE,僅在訓(xùn)練集計(jì)算WOE,不然會標(biāo)簽泄露
transer = toad.transform.WOETransformer()
binned_data = combiner.transform(pd.concat([train_x,train_y], axis=1))
#對WOE的值進(jìn)行轉(zhuǎn)化,映射到原數(shù)據(jù)集上。對訓(xùn)練集用fit_transform,測試集用transform.
data_tr_woe = transer.fit_transform(binned_data, binned_data['isDefault'], exclude=['isDefault'])
data_tr_woe.head()
## test woe
# 先分箱
binned_data = combiner.transform(test_x)
#對WOE的值進(jìn)行轉(zhuǎn)化,映射到原數(shù)據(jù)集上。測試集用transform.
data_test_woe = transer.transform(binned_data)
data_test_woe.head()
使用woe編碼后的train數(shù)據(jù)訓(xùn)練模型。對于金融風(fēng)控這種極不平衡的數(shù)據(jù)集,比較常用的做法是做下極少類的正采樣或者使用代價(jià)敏感學(xué)習(xí)class_weight=’balanced’,以增加極少類的學(xué)習(xí)權(quán)重。
對于LR等弱模型,通常會發(fā)現(xiàn)訓(xùn)練集與測試集的指標(biāo)差異(gap)是比較少的,即很少過擬合現(xiàn)象。
# 訓(xùn)練LR模型
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(class_weight='balanced')
lr.fit(data_tr_woe.drop(['isDefault'],axis=1), data_tr_woe['isDefault'])
print('train ',model_metrics(lr,data_tr_woe.drop(['isDefault'],axis=1), data_tr_woe['isDefault']))
print('test ',model_metrics(lr,data_test_woe,test_y))
利用訓(xùn)練好的LR模型,輸出(概率)分?jǐn)?shù)分布表,結(jié)合誤殺率、召回率以及業(yè)務(wù)需要可以確定一個(gè)合適分?jǐn)?shù)閾值cutoff (注:在實(shí)際場景中,通常還會將概率非線性轉(zhuǎn)化為更為直觀的整數(shù)分score=A-B*ln(odds),方便評分卡更直觀、統(tǒng)一的應(yīng)用。)
train_prob = lr.predict_proba(data_tr_woe.drop(['isDefault'],axis=1))[:,1]
test_prob = lr.predict_proba(data_test_woe)[:,1]
# Group the predicted scores in bins with same number of samples in each (i.e. "quantile" binning)
toad.metrics.KS_bucket(train_prob, data_tr_woe['isDefault'], bucket=10, method = 'quantile')
當(dāng)預(yù)測這用戶的概率大于設(shè)定閾值,意味這個(gè)用戶的違約概率很高,就可以拒絕他的貸款申請。
文章轉(zhuǎn)自微信公眾號@算法進(jìn)階