天天看點

風控模組化一、初步認識風控一、基礎知識二、工業模組化三、規則模組化四、參考文獻

   風險控制(Risk Control),簡稱風控,是網際網路金融的核心。

一、基礎知識

1.1 A/B/C卡

  • A卡(Application score card)即申請評分模型(貸款前),用于預測申請時點(申請信用卡、申請貸款)未來一定時間内逾期的機率。
  • B卡(Behavior score card)即行為評分模型(貸款中),用于預測使用時點(獲得貸款、信用卡的使用期間)未來一定時間内逾期的機率。
  • C卡(Collection score card)即催收評分模型(貸款後),用于預測進入催收階段後,未來一定時間内還款的機率。

1.2 信貸業務

        信貸業務,是通過放款收回本金,獲得利息的,進而赢得利潤。

        有貸款的哥們,貸款平台對其未來還款能力進行預測,将資金優先借貸給有大機率償還的使用者。

1.3 評分卡

        評分卡是以分數的形式來衡量風險幾率的一種手段,是對未來一段時間内違約/逾期/失聯機率的預測。 有一個明确的正區間,分數越高越安全,有反欺詐評分卡,申請評分卡,行為評分卡,催收評分卡。

        評分卡的特性:穩定性,區分性,預測能力,和逾期機率等價。

1.4 信貸風險與控制

信貸領域有兩類風險:

        信用風險,是還款能力和還款意願在貸款後出現的風險。由于一些不可抗力使使用者經濟和思想狀态發生變化。可以通過風險定價政策等手段可控。

        欺詐風險,貸款目的不正當,沒有還款計劃。可控性差。

風險管控由兩大類系統組成:信用評分系統,欺詐檢測系統。

1.5 網際網路金融風控體系

        網際網路金融風控體系主要由資料資訊,政策體系,人工智能模型三部分構成。與傳統人工信審相比,人工智能風控,可批量,迅速,準确地處理貸款申請。解放在中小額貸款的勞動力。

風控模組化一、初步認識風控一、基礎知識二、工業模組化三、規則模組化四、參考文獻

二、工業模組化

2.1 基本定義

在風控場景下遇到的問題,通常都會轉化為二分類問題,并将響應變量作為負樣本。比如:

  • 信用評分模型,預測使用者是否會逾期。 負樣本:逾期使用者。
  • 營銷模型,預測使用者被營銷是否會貸款。負樣本:貸款使用者。
  • 失聯模型,預測使用者是否會失聯。負樣本:失聯使用者。

        信貸評分系統中,負樣本标簽: 逾期超過15天的客戶。正樣本标簽: 未逾期+逾期少于5天的客戶。從分布角度來講,二分類問題一般會假設樣本服從二項分布。如果保留5-15天内逾期的使用者(灰樣本),會讓正負樣本的實際界限很模糊,去掉中間樣本,使樣本分布更趨于二項分布,對模型的訓練更加有利。不過其中灰樣本也會作為測試集,確定模型在訓練結束後,對該部分樣本也有區分能力。

2.2 資料樣本

樣本選取時,滿足原則:

  • 代表性,樣本必須能代表總體。
  • 充分性,樣本集數量滿足一定要求,少樣本無法滿足統計的顯著性。評分卡模組化通常炫耀正負樣本不少于1500個。神經網絡需要樣本量在50萬個以上,否則很難保證穩定性。
  • 時效性,樣本的觀測期與實際應用的時間節點越近越好。然而很多平台,很難保證樣本都處于穩定的較近期時間點上。此時可以通過遷移學習(transfer learning)對樣本挑選或者對變量進行映射,使得早期樣本與近期樣本有相似的資料分布。
  • 排除性。不滿足目前場景貸款需要的使用者不應該作為樣本,比如判定為欺詐的使用者不應該放在目前樣本集中。

        樣本大時,做欠采樣(subsampling)。負樣本一般較少,是以通常隻對正樣本做欠采樣。方法有三種方法:

  • 随機欠采樣,正樣本的比例随機抽。
  • 分層抽樣,保證抽樣後,訓練,驗證,測試的正負樣本比例相同。
  • 等比例抽樣,正負樣本之間的比例進行抽樣。

2.3 模組化的具體流程:

  1. 業務抽象為分類或回歸問題。
  2. 定義标簽
  3. 選取合适的樣本,比對出全部資訊作為特征。
  4. 特征工程+模型訓練+模型評價+模型調優。
  5. 輸出模型報告。
  6. 上線+監控

三、規則模組化

        風控領域有兩種常見的風險規避手段:規則模型和人工智能模型。本小節首先通過規則模型來簡單了解一下模組化的流程,後面所有的章節也會圍繞着人工智能模組化展開。

#!/usr/bin/env python
# coding: utf-8

# In[4]:


import pandas as pd
import numpy as np
import os
# os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'

# In[5]:


path = './'

data = pd.read_excel(path + 'oil_data_for_tree.xlsx')
data.head()

# In[6]:


set(data.class_new)

# In[7]:


data.shape

# org_lst 不需要做特殊變換,直接去重  
# agg_lst 數值型變量做聚合  
# dstc_lst 文本型變量做cnt  

# In[8]:


org_lst = ['uid','create_dt','oil_actv_dt','class_new','bad_ind']
agg_lst = ['oil_amount','discount_amount','sale_amount','amount','pay_amount','coupon_amount','payment_coupon_amount']
dstc_lst = ['channel_code','oil_code','scene','source_app','call_source']

# 資料重組

# In[9]:


df = data[org_lst].copy()
df[agg_lst] = data[agg_lst].copy()
df[dstc_lst] = data[dstc_lst].copy()
df.head()

# 看一下缺失情況

# In[10]:


df.isna().sum()

# 看一下基礎變量的describe

# In[11]:


df.describe()

# 對creat_dt做補全,用oil_actv_dt來填補,并且截取6個月的資料。  
# 構造變量的時候不能直接對曆史所有資料做累加。  
# 否則随着時間推移,變量分布會有很大的變化。

# In[12]:


df2 = df.sort_values(['uid','create_dt'],ascending = False)
df2.head()

# In[13]:


def time_isna(x,y):
    if str(x) == 'NaT':
        x = y
    else:
        x = x
    return x
df2['create_dt'] = df2.apply(lambda x: time_isna(x.create_dt,x.oil_actv_dt),axis = 1)
df2['dtn'] = (df2.oil_actv_dt - df2.create_dt).apply(lambda x :x.days)
df = df2[df2['dtn']<180]
df.head()

# 對org_list變量求曆史貸款天數的最大間隔,并且去重

# In[23]:


base = df[org_lst]
base['dtn'] = df['dtn']
base = base.sort_values(['uid','create_dt'],ascending = False)
base.tail(30)

# In[21]:


base.shape

# 重複uid,保留日期最近的uid。

# In[24]:


base = base.drop_duplicates(['uid'],keep = 'first')
base.tail(30)

# In[18]:


base.shape

# In[26]:


agg_lst

# In[27]:


df.head()

# 做變量衍生

# In[28]:


gn = pd.DataFrame()
for i in agg_lst:
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:len(df[i])).reset_index())
    tp.columns = ['uid',i + '_cnt']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.where(df[i]>0,1,0).sum()).reset_index())
    tp.columns = ['uid',i + '_num']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nansum(df[i])).reset_index())
    tp.columns = ['uid',i + '_tot']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmean(df[i])).reset_index())
    tp.columns = ['uid',i + '_avg']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i])).reset_index())
    tp.columns = ['uid',i + '_max']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmin(df[i])).reset_index())
    tp.columns = ['uid',i + '_min']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanvar(df[i])).reset_index())
    tp.columns = ['uid',i + '_var']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i]) -np.nanmin(df[i]) ).reset_index())
    tp.columns = ['uid',i + '_var']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmean(df[i])/max(np.nanvar(df[i]),1)).reset_index())
    tp.columns = ['uid',i + '_var']
    if gn.empty == True:
        gn = tp
    else:
        gn = pd.merge(gn,tp,on = 'uid',how = 'left')

# In[29]:


gn.head()

# 對dstc_lst變量求distinct個數

# In[30]:


gc = pd.DataFrame()
for i in dstc_lst:
    tp = pd.DataFrame(df.groupby('uid').apply(lambda df: len(set(df[i]))).reset_index())
    tp.columns = ['uid',i + '_dstc']
    if gc.empty == True:
        gc = tp
    else:
        gc = pd.merge(gc,tp,on = 'uid',how = 'left')
gc.head()

# In[33]:


gc.shape,gn.shape, base.shape

# 将變量組合在一起

# In[34]:


fn = pd.merge(base,gn,on= 'uid')
fn = pd.merge(fn,gc,on= 'uid') 
fn.shape

# In[35]:


fn = fn.fillna(0)

# In[36]:


fn.head(100)

# 訓練決策樹模型

# In[37]:


x = fn.drop(['uid','oil_actv_dt','create_dt','bad_ind','class_new'],axis = 1)
y = fn.bad_ind.copy()
from sklearn import tree

dtree = tree.DecisionTreeRegressor(max_depth = 2,min_samples_leaf = 500,min_samples_split = 5000)
dtree = dtree.fit(x,y)

# 輸出決策樹圖像,并作出決策

# In[38]:


import pydotplus 
from IPython.display import Image
from sklearn.externals.six import StringIO

with open(path + "dt.dot", "w") as f:
    tree.export_graphviz(dtree, out_file=f)
dot_data = StringIO()
tree.export_graphviz(dtree, out_file=dot_data,
                         feature_names=x.columns,
                         class_names=['bad_ind'],
                         filled=True, rounded=True,
                         special_characters=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue()) 
Image(graph.create_png())

# value = badrate

# In[39]:


sum(fn.bad_ind),len(fn.bad_ind), sum(fn.bad_ind)/len(fn.bad_ind)


           

       其中資料和代碼源于GitHub - CourteousWood/Risk_control中introduction,最後畫出來的圖像為:

風控模組化一、初步認識風控一、基礎知識二、工業模組化三、規則模組化四、參考文獻

        如果畫圖失敗,centos可以采用下面三行解決。

sudo yum -y install graphviz
python3 -m pip install graphviz -i https://pypi.douban.com/simple/
python3 -m pip install pydotplus -i https://pypi.douban.com/simple/
           

        表中value計算的是葉節點中正負樣本标簽的均值,在二分類中,均值 等價于  标簽為1的樣本在總樣本的比例。可以看到樣本被兩個特征劃分為三個群體,負樣本占比逐漸減少,分别為0.074,0.03,0.012。

dff1 = fn.loc[(fn.pay_amount_tot>240387.5)&(fn.amount_cnt>=3.5)].copy()  
dff1['level'] = 'past_A'  
dff2 = fn.loc[(fn.pay_amount_tot>240387.55)&(fn.amount_cnt<=3.5)].copy()  
dff2['level'] = 'past_B'  
dff3 = fn.loc[fn.pay_amount_tot<=240387.5].copy()  
dff3['level'] = 'past_C'
           

        通過簡單的分群,對三個群體分别采用不同的政策去處理,大大減少業務損失。

從下一節開始,将用人工智能方面模型去解決風控的業務。有其他疑問,歡迎留言,一起讨論+進步!

四、參考文獻

  1. 梅子行 <<智能風控原理、算法與工程實踐>>
  2. 七月線上之金融風控實戰入門
  3. 風控模型的A卡、B卡、C卡 - 知乎

繼續閱讀