一、信贷风控简介
信贷风控是数据挖掘算法最成功的应用之一,这在于金融信贷行业的数据量很充足,需求场景清晰及丰富。
信贷风控简单来说就是判断一个人借了钱后面(如下个月的还款日)会不会按期还钱。更专业来说,信贷风控是还款能力及还款意愿的综合考量,根据这预先的判断为信任依据进行放贷,以此大大提高了金融业务效率。
与其他机器学习的工业场景不同,金融是极其厌恶风险的领域,其特殊性在于非常侧重模型的解释性及稳定性。业界通常的做法是基于挖掘多维度的特征建立一套可解释及效果稳定的规则及风控模型对每笔订单/用户/行为做出判断决策。
其中,对于(贷前)申请前的风控模型,也称为申请评分卡--A卡。A卡是风控的关键模型,业界共识是申请评分卡可以覆盖80%的信用风险。此外还有贷中行为评分卡B卡、催收评分卡C卡,以及反欺诈模型等等。
A卡(Application score card)。目的在于预测申请时(申请信用卡、申请贷款)对申请人进行量化评估。
B卡(Behavior score card)。目的在于预测使用时点(获得贷款、信用卡的使用期间)未来一定时间内逾期的概率。
C卡(Collection score card)。目的在于预测已经逾期并进入催收阶段后未来一定时间内还款的概率。
一个好的特征,对于模型和规则都是至关重要的。像申请评分卡--A卡,主要可以归到以下3方面特征:
1、信贷历史类: 信贷交易次数及额度、收入负债比、查询征信次数、信贷历史长度、新开信贷账户数、额度使用率、逾期次数及额度、信贷产品类型、被追偿信息。(信贷交易类的特征重要程度往往是最高的,少了这部分历史还款能力及意愿的信息,风控模型通常直接就废了。)
2、基本资料及交易记录类:年龄、婚姻状况、学历、工作类型及年薪、工资收入、存款AUM、资产情况、公积金及缴税、非信贷交易流水等记录(这类主要是从还款能力上面综合考量的。还可以结合多方核验资料的真伪以及共用像手机号、身份证号等团伙欺诈信息,用来鉴别欺诈风险。需要注意的,像性别、肤色、地域、种族、宗教信仰等类型特征使用要谨慎,可能模型会有效果,但也会导致算法歧视问题。)
3、公共负面记录类: 如破产负债、民事判决、行政处罚、法院强制执行、涉赌涉诈黑名单等(这类特征不一定能拿得到数据,且通常缺失度比较高,对模型贡献一般,更多的是从还款意愿/欺诈维度的考虑)
二、申请评分卡(A卡)全流程
实战部分我们以经典的申请评分卡为例,使用的中原银行个人贷款违约预测比赛的数据集,使用信用评分python库--toad、树模型Lightgbm及逻辑回归LR做申请评分模型。(注:文中所涉及的一些金融术语,由于篇幅就不展开解释了,疑问之处 可以谷歌了解下哈。)
2.1 模型定义
申请评分模型定义主要是通过一系列的数据分析确定建模的样本及标签。
首先,补几个金融风控的术语的说明。概念模糊的话,可以回查再理解下:
逾期期数(M) :指实际还款日与应还款日之间的逾期天数,并按区间划分后的逾期状态。M取自Month on Book的第一个单词。(注:不同机构所定义的区间划分可能存在差异)
M0:当前未逾期(或用C表示,取自Current)
M1: 逾期1-30日
M2:逾期31-60日
M3:逾期61-90日
M4:逾期91-120日
M5:逾期121-150日
M6:逾期151-180日
M7+:逾期180日以上
观察点:样本层面的时间窗口。 用于构建样本集的时间点(如2010年10月申请贷款的用户),不同环节定义不同,比较抽象,这里举例说明:如果是申请模型,观察点定义为用户申贷时间,取19年1-12月所有的申贷订单作为构建样本集;如果是贷中行为模型,观察点定义为某个具体日期,如取19年6月15日在贷、没有发生逾期的申贷订单构建样本集。
观察期:特征层面的时间窗口。构造特征的相对时间窗口,例如用户申请贷款订前12个月内(2009年10月截至到2010年10月申请贷款前的数据都可以用, 可以有用户平均消费金额、次数、贷款次数等数据特征)。设定观察期是为了每个样本的特征对齐,长度一般根据数据决定。一个需要注意的点是,只能用此次申请前的特征数据,不然就会数据泄露(时间穿越,用未来预测过去的现象)。
表现期:标签层面的时间窗口。定义好坏标签Y的时间窗口,信贷风险具有天然的滞后性,因为用户借款后一个月(第一期)才开始还钱,有得可能还了好几期才发生逾期。
对于现成的比赛数据,数据特征的时间跨度(观察期)、数据样本、标签定义都是已经提前分析确定下来的。但对于实际的业务来说,数据样本及模型定义其实也是申请评分卡的关键之处。毕竟实际场景里面可能没有人扔给你现成的数据及标签(好坏定义,有些公司的业务会提前分析好给建模人员),然后只是跑个分类模型那么简单。
确定建模的样本量及标签,也就是模型从多少的数据样本中学习如何分辨其中的好、坏标签样本。如果样本量稀少、标签定义有问题,那学习的结果可想而知也会是差的。(对于建模样本量的确定,经验上肯定是满足建模条件的样本越多越好,一个类别最好有几千以上的样本数。)
但对于标签的定义,可能我们直观感觉是比较简单,比如“好用户就是没有逾期的用户, 坏用户就是在逾期的用户”,但具体做量化起来会发现并不简单,有两个方面的主要因素需要考量:
- 【坏的定义】逾期多少天算是坏客户。比如:只逾期2天算是建模的坏客户?
根据巴塞尔协议的指导,一般逾期超过90天(M4+)的客户,即定义为坏客户。更为通用的,可以使用“滚动率”分析方法(Roll Rate Analysis)确定多少天算是“坏”,基本方法是统计分析出逾期M期的客户多大概率会逾期M+1期(同样的,我们不太可能等着所有客户都逾期一年才最终确定他就是坏客户。一来时间成本太高,二来这数据样本会少的可怜)。如下示例,我们通过滚动率分析各期逾期的变坏概率。当前未逾期(M0)下个月保持未逾期的概率99.71%; 当前逾期M1,下个月继续逾期概率为54.34%;当前M2下个月继续逾期概率就高达90.04%。我们可以看出M2是个比较明显的变坏拐点,可以以M2+作为坏样本的定义。
- 【表现期】借贷申请的时间点(即:观察点)之后要在多久的时间暴露表现下,才能比较彻底的确定客户是否逾期。比如:借贷后观察了一个客户借贷后60天的那几个分期的表现都是按时还款,就可以判断他是好/坏客户?
这也就是确定表现期,常用的分析方法是Vintage分析(Vintage在信贷领域不仅可以用它来评估客户好坏充分暴露所需的时间,即成熟期,还可以用它分析不同时期风控策略的差异等),通过分析历史累计坏用户暴露增加的趋势,来确定至少要多少期可以比较全面的暴露出大部分的坏客户。如下示例的坏定义是M4+,我们可以看出各期的M4+坏客户经过9或者10个月左右的表现,基本上可以都暴露出来,后面坏客户的总量就比较平稳了。这里我们就可以将表现期定位9或者10个月~
确定了坏的定义以及需要的表现期,我们就可以确定样本的标签,最终划定的建模样本:
- 好用户:表现期(如9个月)内无逾期的用户样本。
- 坏用户:表现期(如9个月)内逾期(如M2+)的用户样本。
- 灰用户:表现期内有过逾期行为,但不到坏定义(如M2+)的样本。注:实践中经常会把只逾期3天内的用户也归为好用户。
比如现在的时间是2022-10月底,表现期9个月的话,就可以取2022-01月份及之前申请的样本(这也称为 观察点),打上好坏标签,建模。
通过上面信用评分的介绍,很明显的好用户通常远大于坏用户的,这是一个类别极不均衡的典型场景,不均衡处理方法下文会谈到。
2.2 读取数据及预处理
本数据集的数据字典文档、比赛介绍及本文代码,可以到https://github.com/aialgorithm/Blog项目相应的代码目录下载
该数据集为中原银行的个人贷款违约预测数据集,个别字段有做了脱敏(金融的数据大都涉及机密)。主要的特征字段有个人基本信息、经济能力、贷款历史信息等等
数据有10000条样本,38维原始特征,其中isDefault为标签,是否逾期违约。
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()
数据预处理主要是对日期信息、噪音数据做下处理,并划分下类别、数值类型的特征。
# 日期类型:issueDate 转换为pandas中的日期类型,加工出数值特征
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
# 提取时间diff # 转换为天为单位
base_time = datetime.datetime.strptime('2000-01-01', '%Y-%m-%d') # 随机设置初始的基准时间
train_bank['issue_date_diff'] = train_bank['issue_date'].apply(lambda x: x-base_time).dt.days
# 可以发现earlies_credit_mon应该是年份-月的格式,这里简单提取年份
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')
# 区分 数值 或类别特征
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]) # 转为数值
num_feas.append(col)
except:
train_bank[col] = train_bank[col].astype('category')
cate_feas.append(col)
print(cate_feas)
print(num_feas)
2.3 lightgbm评分卡建模
如果是用Lightgbm建模做违约预测,简单的数据处理,基本上代码就结束了。lgb树模型是集成学习的强模型,自带缺失、类别变量的处理,特征上面不用做很多处理,建模非常方便,模型效果通常不错,还可以输出特征的重要性。
(By the way,申请评分卡业界用逻辑回归LR会比较多,因为模型简单,解释性也比较好)。
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]) # 设置x、y轴的上下限,以免和边缘重合,更好的观察图像的整体
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate') # 可以使用中文,但需要导入一些库即字体
plt.title('ROC Curve')
plt.legend(loc="lower right")
return metrics
# 划分数据集:训练集和测试集
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)
# 训练模型
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)
2.4 LR评分卡建模
LR即逻辑回归,是一种广义线性模型,因为其模型简单、解释性良好,在金融行业是最常用的。
也正因为LR过于简单,没有非线性能力,所以我们往往需要通过比较复杂的特征工程,如分箱WOE编码的方法,提高模型的非线性能力。
关于LR的原理及优化方法,强烈推荐阅读下:
下面我们通过toad实现特征分析、特征选择、特征分箱及WOE编码
2.4.1 特征选择
# 数据EDA分析
toad.detector.detect(train_bank)
# 特征选择,根据相关性 缺失率、IV 等指标
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)
# 划分训练集 测试集
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)
2.4.2 卡方分箱
# 特征的卡方分箱
combiner = toad.transform.Combiner()
# 训练数据并指定分箱方法
combiner.fit(pd.concat([train_x,train_y], axis=1), y='isDefault',method= 'chi',min_samples = 0.05,exclude=[])
# 以字典形式保存分箱结果
bins = combiner.export()
bins
通过特征分箱,每一个特征被离散化为各个分箱。
接下来就是LR特征工程的特色处理了--手动调整分箱的单调性。
这一步的意义更多在于特征的业务解释性的约束,对于模型的拟合效果影响不一定是正面的。这里我们主观认为大多数特征的不同分箱的坏账率badrate应该是满足某种单调关系的,而起起伏伏是不太好理解的。如征信查询次数这个特征,应该是分箱数值越高,坏账率越大。(注:如年龄特征可能就不满足这种单调关系)
我们可以查看下ebt_loan_ratio这个变量的分箱情况,根据bad_rate趋势图,并保证单个分箱的样本占比不低于0.05,去调整分箱,达到单调性。(其他的特征可以按照这个方法继续调整,单调性调整还是挺耗时的)
adj_var = 'scoring_low'
#调整前原来的分箱 [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)
-
调整前
-
调整后
# 更新调整后的分箱
combiner.set_rules(adj_bin)
combiner.export()
2.4.3 WOE编码
接下来就是对各个特征的分箱做WOE编码,通过WOE编码给各个分箱不同的权重,提升LR模型的非线性。
#计算WOE,仅在训练集计算WOE,不然会标签泄露
transer = toad.transform.WOETransformer()
binned_data = combiner.transform(pd.concat([train_x,train_y], axis=1))
#对WOE的值进行转化,映射到原数据集上。对训练集用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的值进行转化,映射到原数据集上。测试集用transform.
data_test_woe = transer.transform(binned_data)
data_test_woe.head()
2.4.4 训练LR
使用woe编码后的train数据训练模型。对于金融风控这种极不平衡的数据集,比较常用的做法是做下极少类的正采样或者使用代价敏感学习class_weight='balanced',以增加极少类的学习权重。可见:《一文解决样本不均衡(全)》
对于LR等弱模型,通常会发现训练集与测试集的指标差异(gap)是比较少的,即很少过拟合现象。
# 训练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))
2.4.5 评分卡应用
利用训练好的LR模型,输出(概率)分数分布表,结合误杀率、召回率以及业务需要可以确定一个合适分数阈值cutoff (注:在实际场景中,通常还会将概率非线性转化为更为直观的整数分score=A-B*ln(odds),方便评分卡更直观、统一的应用。)
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')
当预测这用户的概率大于设定阈值,意味这个用户的违约概率很高,就可以拒绝他的贷款申请。