• 风控建模一、初步认识风控


       风险控制(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 互联网金融风控体系

            互联网金融风控体系主要由数据信息,策略体系,人工智能模型三部分构成。与传统人工信审相比,人工智能风控,可批量,迅速,准确地处理贷款申请。解放在中小额贷款的劳动力。

    7d48fe3f45f7f063eabaa89a38a9405f.png

    二、工业建模

    2.1 基本定义

    在风控场景下遇到的问题,通常都会转化为二分类问题,并将响应变量作为负样本。比如:

    • 信用评分模型,预测用户是否会逾期。 负样本:逾期用户。
    • 营销模型,预测用户被营销是否会贷款。负样本:贷款用户。
    • 失联模型,预测用户是否会失联。负样本:失联用户。

            信贷评分系统中,负样本标签: 逾期超过15天的客户。正样本标签: 未逾期+逾期少于5天的客户。从分布角度来讲,二分类问题一般会假设样本服从二项分布。如果保留5-15天内逾期的用户(灰样本),会让正负样本的实际界限很模糊,去掉中间样本,使样本分布更趋于二项分布,对模型的训练更加有利。不过其中灰样本也会作为测试集,确保模型在训练结束后,对该部分样本也有区分能力。

    2.2 数据样本

    样本选取时,满足原则:

    • 代表性,样本必须能代表总体。

    • 充分性,样本集数量满足一定要求,少样本无法满足统计的显著性。评分卡建模通常炫耀正负样本不少于1500个。神经网络需要样本量在50万个以上,否则很难保证稳定性。

    • 时效性,样本的观测期与实际应用的时间节点越近越好。然而很多平台,很难保证样本都处于稳定的较近期时间点上。此时可以通过迁移学习(transfer learning)对样本挑选或者对变量进行映射,使得早期样本与近期样本有相似的数据分布。

    • 排除性。不满足当前场景贷款需要的用户不应该作为样本,比如判定为欺诈的用户不应该放在当前样本集中。

            样本大时,做欠采样(subsampling)。负样本一般较少,因此通常只对正样本做欠采样。方法有三种方法:

    • 随机欠采样,正样本的比例随机抽。
    • 分层抽样,保证抽样后,训练,验证,测试的正负样本比例相同。
    • 等比例抽样,正负样本之间的比例进行抽样。

    2.3 建模的具体流程:

    1. 业务抽象为分类或回归问题。

    2. 定义标签

    3. 选取合适的样本,匹配出全部信息作为特征。

    4. 特征工程+模型训练+模型评价+模型调优。

    5. 输出模型报告。

    6. 上线+监控

    三、规则建模

            风控领域有两种常见的风险规避手段:规则模型和人工智能模型。本小节首先通过规则模型来简单了解一下建模的流程,后面所有的章节也会围绕着人工智能建模展开

    1. #!/usr/bin/env python
    2. # coding: utf-8
    3. # In[4]:
    4. import pandas as pd
    5. import numpy as np
    6. import os
    7. # os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'
    8. # In[5]:
    9. path = './'
    10. data = pd.read_excel(path + 'oil_data_for_tree.xlsx')
    11. data.head()
    12. # In[6]:
    13. set(data.class_new)
    14. # In[7]:
    15. data.shape
    16. # org_lst 不需要做特殊变换,直接去重
    17. # agg_lst 数值型变量做聚合
    18. # dstc_lst 文本型变量做cnt
    19. # In[8]:
    20. org_lst = ['uid','create_dt','oil_actv_dt','class_new','bad_ind']
    21. agg_lst = ['oil_amount','discount_amount','sale_amount','amount','pay_amount','coupon_amount','payment_coupon_amount']
    22. dstc_lst = ['channel_code','oil_code','scene','source_app','call_source']
    23. # 数据重组
    24. # In[9]:
    25. df = data[org_lst].copy()
    26. df[agg_lst] = data[agg_lst].copy()
    27. df[dstc_lst] = data[dstc_lst].copy()
    28. df.head()
    29. # 看一下缺失情况
    30. # In[10]:
    31. df.isna().sum()
    32. # 看一下基础变量的describe
    33. # In[11]:
    34. df.describe()
    35. # 对creat_dt做补全,用oil_actv_dt来填补,并且截取6个月的数据。
    36. # 构造变量的时候不能直接对历史所有数据做累加。
    37. # 否则随着时间推移,变量分布会有很大的变化。
    38. # In[12]:
    39. df2 = df.sort_values(['uid','create_dt'],ascending = False)
    40. df2.head()
    41. # In[13]:
    42. def time_isna(x,y):
    43. if str(x) == 'NaT':
    44. x = y
    45. else:
    46. x = x
    47. return x
    48. df2['create_dt'] = df2.apply(lambda x: time_isna(x.create_dt,x.oil_actv_dt),axis = 1)
    49. df2['dtn'] = (df2.oil_actv_dt - df2.create_dt).apply(lambda x :x.days)
    50. df = df2[df2['dtn']<180]
    51. df.head()
    52. # 对org_list变量求历史贷款天数的最大间隔,并且去重
    53. # In[23]:
    54. base = df[org_lst]
    55. base['dtn'] = df['dtn']
    56. base = base.sort_values(['uid','create_dt'],ascending = False)
    57. base.tail(30)
    58. # In[21]:
    59. base.shape
    60. # 重复uid,保留日期最近的uid。
    61. # In[24]:
    62. base = base.drop_duplicates(['uid'],keep = 'first')
    63. base.tail(30)
    64. # In[18]:
    65. base.shape
    66. # In[26]:
    67. agg_lst
    68. # In[27]:
    69. df.head()
    70. # 做变量衍生
    71. # In[28]:
    72. gn = pd.DataFrame()
    73. for i in agg_lst:
    74. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:len(df[i])).reset_index())
    75. tp.columns = ['uid',i + '_cnt']
    76. if gn.empty == True:
    77. gn = tp
    78. else:
    79. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    80. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.where(df[i]>0,1,0).sum()).reset_index())
    81. tp.columns = ['uid',i + '_num']
    82. if gn.empty == True:
    83. gn = tp
    84. else:
    85. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    86. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nansum(df[i])).reset_index())
    87. tp.columns = ['uid',i + '_tot']
    88. if gn.empty == True:
    89. gn = tp
    90. else:
    91. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    92. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmean(df[i])).reset_index())
    93. tp.columns = ['uid',i + '_avg']
    94. if gn.empty == True:
    95. gn = tp
    96. else:
    97. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    98. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i])).reset_index())
    99. tp.columns = ['uid',i + '_max']
    100. if gn.empty == True:
    101. gn = tp
    102. else:
    103. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    104. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmin(df[i])).reset_index())
    105. tp.columns = ['uid',i + '_min']
    106. if gn.empty == True:
    107. gn = tp
    108. else:
    109. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    110. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanvar(df[i])).reset_index())
    111. tp.columns = ['uid',i + '_var']
    112. if gn.empty == True:
    113. gn = tp
    114. else:
    115. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    116. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i]) -np.nanmin(df[i]) ).reset_index())
    117. tp.columns = ['uid',i + '_var']
    118. if gn.empty == True:
    119. gn = tp
    120. else:
    121. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    122. tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmean(df[i])/max(np.nanvar(df[i]),1)).reset_index())
    123. tp.columns = ['uid',i + '_var']
    124. if gn.empty == True:
    125. gn = tp
    126. else:
    127. gn = pd.merge(gn,tp,on = 'uid',how = 'left')
    128. # In[29]:
    129. gn.head()
    130. # 对dstc_lst变量求distinct个数
    131. # In[30]:
    132. gc = pd.DataFrame()
    133. for i in dstc_lst:
    134. tp = pd.DataFrame(df.groupby('uid').apply(lambda df: len(set(df[i]))).reset_index())
    135. tp.columns = ['uid',i + '_dstc']
    136. if gc.empty == True:
    137. gc = tp
    138. else:
    139. gc = pd.merge(gc,tp,on = 'uid',how = 'left')
    140. gc.head()
    141. # In[33]:
    142. gc.shape,gn.shape, base.shape
    143. # 将变量组合在一起
    144. # In[34]:
    145. fn = pd.merge(base,gn,on= 'uid')
    146. fn = pd.merge(fn,gc,on= 'uid')
    147. fn.shape
    148. # In[35]:
    149. fn = fn.fillna(0)
    150. # In[36]:
    151. fn.head(100)
    152. # 训练决策树模型
    153. # In[37]:
    154. x = fn.drop(['uid','oil_actv_dt','create_dt','bad_ind','class_new'],axis = 1)
    155. y = fn.bad_ind.copy()
    156. from sklearn import tree
    157. dtree = tree.DecisionTreeRegressor(max_depth = 2,min_samples_leaf = 500,min_samples_split = 5000)
    158. dtree = dtree.fit(x,y)
    159. # 输出决策树图像,并作出决策
    160. # In[38]:
    161. import pydotplus
    162. from IPython.display import Image
    163. from sklearn.externals.six import StringIO
    164. with open(path + "dt.dot", "w") as f:
    165. tree.export_graphviz(dtree, out_file=f)
    166. dot_data = StringIO()
    167. tree.export_graphviz(dtree, out_file=dot_data,
    168. feature_names=x.columns,
    169. class_names=['bad_ind'],
    170. filled=True, rounded=True,
    171. special_characters=True)
    172. graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
    173. Image(graph.create_png())
    174. # value = badrate
    175. # In[39]:
    176. sum(fn.bad_ind),len(fn.bad_ind), sum(fn.bad_ind)/len(fn.bad_ind)

           其中数据和代码源于GitHub - CourteousWood/Risk_control中introduction,最后画出来的图像为:

            如果画图失败,centos可以采用下面三行解决。

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

            表中value计算的是叶节点中正负样本标签的均值,在二分类中,均值 等价于  标签为1的样本在总样本的比例。可以看到样本被两个特征划分为三个群体,负样本占比逐渐减少,分别为0.074,0.03,0.012。

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

            通过简单的分群,对三个群体分别采用不同的策略去处理,大大减少业务损失。


    从下一节开始,将用人工智能方面模型去解决风控的业务。有其他疑问,欢迎留言,一起讨论+进步!


    四、参考文献

    1. 梅子行 <<智能风控原理、算法与工程实践>>
    2. 七月在线之金融风控实战入门
    3. 风控模型的A卡、B卡、C卡 - 知乎

  • 相关阅读:
    2小时开发《点球射门游戏》,动画演示思路(上),代码已开源
    MyBatis-Plus联表查询的短板,终于有一款工具补齐了
    【广州华锐互动】VR营销心理学情景模拟培训系统介绍
    IIS配置SSL,根据pem和key生成pfx,openssl的版本不能太高
    代码随想录 动态规划 14
    大数据在智慧城市的建设中起到了哪些作用?_光点科技
    【JavaScript】分支结构和循环结构
    Div3 cf1741
    人工智能数学课高等数学线性微积分数学教程笔记(4. 多元函数的微分学)
    比特熊直播间一周年,英雄集结令!邀你来合影!
  • 原文地址:https://blog.csdn.net/nanjifengzi/article/details/126805340