本案例所用数据来自一家银行的个人金融业务数据集,可以作为银行场景下进行个人客户业务分析和数据挖掘的示例。这份数据中涉及 5300 个银行客户的 100 万笔交易,而且涉及 700 份贷款信息与近 900 张信用卡的数据。通过分析这份数据可以获取与银行服务相关的业务知识。例如,对于提供增值服务的银行客户经理来说,希望明确哪些客户有更多的业务需求,而风险管理的业务人员可以及早发现贷款的潜在损失
(1)账户表(Accounts):每条记录描述了一个账户(account_id)的静态信息,共 4500 条记录,如表 1-1 所示
名称 | 标签 |
---|---|
account_id | 账户号,主键 |
district_id | 开户分行行政区号,外键 |
date | 开户日期 |
frequency | 结算频度(月,周,交易之后马上) |
(2)顾客信息表(Clients): 每条记录描述了一个客户(client_id)的特征信息,共5369 条记录,如表 1-2 所示
名称 | 标签 |
---|---|
client_id | 客户号,主键 |
Sex | 性别 |
birth_date | 出生日期 |
district_id | 地区号(客户所属地区),外键 |
(3)权限分配表(Disp): 每条记录描述了顾客(client_id)和账号(account_id)之间的关系,以及客户操作账号的权限,共5369 条记录,如表 1-3 所示
名称 | 标签 | 说明 |
---|---|---|
disp_id | 权限设置号 | 主键 |
client_id | 客户号 | 外键 |
account_id | 账户号 | 外键 |
type | 权限类型 | 分为“所有者”和“用户”,只用“所有者”身份 可以进行增值业务操作和贷款 |
(4)支付订单表(Order):每条记录代表一个支付命令,共 6471 条记录,如表 1-4 所示
名称 | 标签 | 说明 |
---|---|---|
order_id | 订单号 | 主键 |
account_id | 发起订单的账户号 | 外键 |
bank_to | 收款银行 | 银行名称用字母代替 |
account_to | 收款账户号 | |
amount | 金额 | 单位/元 |
K_symbol | 支付方式 |
(5)交易表(Trans): 每条记录代表每个账户(account_id)上的一条交易,共1056320条记录,如表 1-5 所示
名称 | 标签 |
---|---|
trans_id | 交易序列号,主键 |
account_id | 发起交易的账户号,外键 |
date | 交易日期 |
type | 借贷类型 |
operation | 交易类型 |
amount | 金额 |
balance | 账户余额 |
K_symbol | 交易特征 |
Bank | 对方银行 |
account | 对方账户号 |
(6)贷款表(Loans): 每条记录代表某个账户(account_id)上的一条贷款信息,共 682 条记录,如表 1-6 所示
名称 | 标签 | 说明 |
---|---|---|
loan_id | 贷款号 | 主键 |
account_id | 账户号 | 外键 |
date | 发放贷款日期 | |
amount | 贷款金额 | 单位/元 |
duration | 贷款期限 | |
payments | 每月归还额 | 单位/元 |
status | 还款状态 | A 代表合同终止,没问题 B 代表合同终止,贷款没有支付 C 代表合同处于执行期,至今正常 D 代表合同处于执行期,为欠款状态 |
(7)信用卡(Cards): 每条记录描述一个顾客号的信用卡信息,共 892 条记录,如表 1-7 所示
名称 | 标签 |
---|---|
card_id | 信用卡ID,主键 |
disp_id | 账户权限号,外键 |
type | 卡类型 |
issued | 发卡日期 |
(8)人口地区统计表(District):每条记录了描述一个地区的人口统计学信息,共 77 条,如表 1-8 所示
名称 | 标签 |
---|---|
A1-district_id | 地区号,主键 |
GDP | GDP总量 |
A4 | 居住人口 |
A10 | 城镇人口比例 |
A11 | 平均工资 |
A12 | 1995年失业率 |
A13 | 1996年失业率 |
A14 | 1000人中有多少企业家 |
A15 | 1995年犯罪率(千人) |
A16 | 1996年犯罪率(千人) |
实际业务中的一个人可以拥有多个账户号(account_id), 一个账户号(account_id)可以对应多个顾客(client_id),即多个顾客共享一个账户号(account_id),但是每个账户号(account id)的所有者(即最高权限者)只能是一个人。账户号(account_id)与客户号(client id)的对应关系,在表 Disposition”中进行展示;表“Credit card”表述了银行提供给顾客(client_id)的服务,每个客户可以申请一张信用卡;贷款为基于账户的服务,一个账户(account_id)在一个时点最多只能有一笔贷款。
关系实体图(E-R 图)可以直观地描述表间关系,如图 1-1 所示。图中将每张表的主键与外键通过实线相连接,可以明确指导我们将表进行横向连接。比如要知道贷款客户的性别,就需要使用贷款表(Loan)中的 account_id 先与权限分配表(Disposition)中的 account id 连接,然后再拿 client_id 和客户表(Client)中的 client id 连接
在贷款审批方面,可以通过构建量化模型对客户的信用等级进行金管理方面,得知了每个账户的违约概率后,可以预估未来的坏账比例,及时做好资金安排。也可以对违约可能性较高的客户更加频繁地“关怀”,及时发现问题,以避免损失。在这个量化模型中,被解释变量为二分类变量,因此需要构建一个排序类分类模型。而排序类分类模型中最常使用的算法是逻辑回归
(1) 属性表征信息:在分析个人客户时,又称人口统计信息。主要涉及最基本的性别、出生日期等信息。这类指标对客户的行为预测并不具有因果关系,只是根据历史数据统计可得到一些规律。比如,随着客户年龄的提高,会对房贷、消费贷款、教育储蓄、个人理财等产品依次产生需求,但是年龄并不是对产品有需求的根本原因,其实婚龄才是其原因。只不过婚龄和年龄在同时期人群中是高度相关的。同理,性别和某种业务表现的高相关性,很多也来自于外部世界对性别类型的一种行为期望。对于银行、汽车 4S 店这类需要客户临柜填写表格的公司而言,是可以获取这方面的“真实”信息的,而对于电商而言,是难以获取“真实”信息的。但是电商的分析人员也不必气馁,其实“真实”这个概念是有很多内涵的,根据电商数据虽然不能知道客户人口学上的“真实”年龄,但是根据其消费行为完全可以刻画出其消费心理上的“真实”年龄,而后者在预测客户需求和行为方面更有效。
(2) 行为信息:行为是内部需求在外部特定环境下的一种表现。首先,行为是内部需求的结果。比如,活期存款的客户将手头的钱存起来,以应付不时之需的需求。其次,这必行为是在特定环境下表现出来的,在活期理财产品推出之前,活期存款是唯一的选择。对于银行而言,行为数据仅限于业务数据,而电信公司可以获取的行为数据更加广泛,不仅可以获取通话行为、上网行为等业务信息,还可以获取周末出行、业余生活等个人行为信息。获取的客户行为信息越多,对客户的了解越深入。在这方面,各类企业都具有很大的深挖潜力。由于行为数据均为详细记录,数量庞大,而建模数据是一个样本只能有一条记录,因此需要对行为数据依照 RFM 方法进行行为信息的提取,比如过去年的账户余额就是按照 “M” 计算得到的,这类变量称为一级衍生变量。这还不够,比如,要看账户余额是否有增长趋势,就要计算过去一年每月的平均账户余额,然后计算前后两月平均账户余额增长率的均值,这个变量就称为二级衍生变量。行为信息的提取可以按照 RFM 方法做到三级至四级衍生变量
(3) 状态信息:指客户的社会经济状态和社会网络关系。社会学认为,人之所以为特定的人,就在于其被固化在特定的关系之中,这被称为嵌入理论。了解客户的社会关系,就了解了外界对该客户的期望,进而推断出其需求。通过深入分析,甚至可以推断出客户未来的需求,达到比客户更了解客户的状态。在这方面,有些企业走在了前面,比如,电信企业通过通话和短信行为确定客户的交友圈,通过信号地理信息定位客户的工作、生活和休闲区域以此推测其工作类型和社交网络类型等。有些企业刚刚起步,只是通过客户住址大致下客户居住小区的档次,以确定其社会经济地位。这类信息是值得每个以客户为中心的企业花时间和精力去深挖的。
(4) 利益信息:如果可以知道客户的内在需求,这当然是最理想的,而这类数据获取方式是很匮乏的。传统方式只能通过市场调研、客户呼入或客户投诉得到相关数据。现在利用客服、微信公众号、微博、论坛等留言信息,可以便捷地获取客户评价信息。
以上构建变量的准则是放之四海而皆准的,而具体到违约预测这个主题,还需要更有针对性的分析。以往的研究认为,影响违约的主要因素有还款能力不足和还款意愿不足两个方面。还款意愿不足有可能是欲望大于能力、生活状态不稳定。以上是概念分析,之后就需要量化,比如使用“资产余额的变异系数”作为生活状态不稳定的代理指标。
在建模过程中,有预测价值的变量基本都是衍生变量,比如:
根据上述的维度分析的框架创建建模使用的变量。不过我们不要期望可以创建全部四个维度的变量,一般创建前三个维度足矣。首先生成被解释主要方面。在贷款(loans)表中还款状态(status)变量记录了客户的贷款偿还情况,其中
我们以此构造一个客户行为信用评级模型,以预测其他客户违约的概率
我们分析的变量按照时间变化情况可以分为动态变量和静态变量
动态变量还可分为时点变量和区间变量
在建模过程中,需要按照图 4-1 所示的取数窗口提取变量。其中有两个重要的时间窗口——观察窗口和预测窗口
模型框架
取数窗口期的长短和模型易用性是一对矛盾体:窗口期越短,缺失值越少,可分析的样本就越多、越便于使用。但是区间变量中单个变量的观测期越短,数据越不稳定,这样难以获得稳健的参数。但是取数窗口期越长,新的客户就会因为变量缺失而无法纳入研究样本。因此取数窗口的长短是需要根据建模面临的任务灵活调整的。本案例中的观测窗口定为一年
同样,预测窗口可长可短,取决于构建什么样的模型,以及目标变量是什么。比如营销响应模型,预测窗口取三天至一周就足够了;而信用卡信用违约模型,须要观测一年的时间。通常,越长的预测窗口样本量越少。而预测窗口过短则会导致有些样本的被解释变量的最终状态还没有表现出来。本文没有严格按照信用评级模型的取数窗口进行设置,需要深人学习的读者请参考《信用风险评分卡研究:基于 SAS 的开发与实施》
利用pandas导入可用于建模的样本数据,利用loan表生成被解释的变量
import pandas as pd
import numpy as np
import os
loanfile = os.listdir()
createVar = locals()
for i in loanfile:
if i.endswith("csv"):
createVar[i.split('.')[0]]=pd.read_csv(i,encoding='gbk')
print(i.split('.')[0])
创建被解释变量
bad_good={'B':1, 'D':1, 'A':0, 'C': 2}
loans['bad_good']=loans.status.map(bad_good)
loans.head()
loan_id | account_id | date | amount | duration | payments | status | bad_good | |
---|---|---|---|---|---|---|---|---|
0 | 5314 | 1787 | 1993-07-05 | 96396 | 12 | 8033 | B | 1 |
1 | 5316 | 1801 | 1993-07-11 | 165960 | 36 | 4610 | A | 0 |
2 | 6863 | 9188 | 1993-07-28 | 127080 | 60 | 2118 | A | 0 |
3 | 5325 | 1843 | 1993-08-03 | 105804 | 36 | 2939 | A | 0 |
4 | 7240 1 | 1013 | 1993-09-06 | 274740 | 60 | 4579 | A | 0 |
将所有维度的信息归结到贷款表(LOANS)上,每个贷款账户只有一条记录。寻找有预测能力的指标。首先是寻找客户表征信息,如性别、年龄。客户的人口信息保存在客户信息表(CIENTS)中,但是该表是以客户为主键的,需要和权限分配表(DISP)相连接才可以获得账号级别的信息
data2=pd.merge(loans,disp,on='account_id',how='left')
data2=pd.merge(data2,clients,on='client_id',how='left')
data2=data2[data2.type=='所有者']
data2.head()
提取借款人居住地情况,如居住地失业率等变量。与 district 表进行连接
data3 = pd.merge(data2, district,
left_on = 'district_id',
right_on = 'A1',
how = 'left')
data3.head()
根据客户的账户变动的行为信息,考察借款人还款能力,如账户平均余额、余额的标准差、变异系数、平均入账和平均支出的比例、存贷比等
首先将贷款表和交易表按照 account_id 内连接
data_4temp1=pd.merge(loans[['account_id','date']],
trans[['account_id','type','amount','balance','date']],
on='account_id')
data_4temp1.columns=['account_id','date','type','amount','balance','t_date']
data_4temp1=data_4temp1.sort_values(by=['account_id','t_date'])
然后将来自贷款表和交易表中的两个字符串类型的日期变量转换为日期,为窗口取数做准备
data_4temp1['date']=pd.to_datetime(data_4temp1['date'])
data_4temp1['t_date']=pd.to_datetime(data_4temp1['t_date'])
账户余额和交易额度为字符变量,有千分位符,需要进行数据清洗,并转换为数值类型
data_4temp1['balance2']=data_4temp1['balance'].map(lambda x:int(''.join(x[1:].split(','))))
data_4temp1['amount2']=data_4temp1['amount'].map(lambda x:int(''.join(x[1:].split(','))))
现对窗口进行取数据,只保留贷款日期前365天至贷款前1天的交易数据
import datetime
data_4temp2=data_4temp1[data_4temp1.date>data_4temp1.t_date][data_4temp1.date<data_4temp1.t_date+datetime.timedelta(days=365)]
data_4temp2.head()
计算每个贷款账户贷款前一年的平均账户余额(财富水平),账户余额的标准差(代表财富稳定情况)和变异系数(代表财富稳定情况的另一指标)
data_4temp3=data_4temp2.groupby('account_id')['balance2'].agg([('avg_balance','mean'),('stdev_balance','std')])
data_4temp3['cv_balance']=data_4temp3[['avg_balance','stdev_balance']].apply(lambda x:x[1]/x[0],axis=1)
计算平均入账和平均支出的比例。首先以上一步时间窗口取数得到的数据集为基础,对每一个账户“借-贷”类型进行交易金额汇总
type_dict={'借':'out','贷':'income'}
data_4temp2['type1']=data_4temp2.type.map(type_dict)
data_4temp4=data_4temp2.groupby(['account_id','type1'])[['amount2']].sum()
data_4temp4.head(2)
对于上一步汇总后的数据,每个账户会有两条记录,需要对其进行拆分列操作,将每个账户的两条观测转换为每个账户一条观测。以下语句中pd.pivot_table函数进行堆叠列
data_4temp5=pd.pivot_table(data_4temp4,values='amount2',index='account_id',columns='type1')
data_4temp5.fillna(0,inplace=True)
data_4temp5['r_out_in']=data_4temp5[['out','income']].apply(lambda x:x[0]/x[1],axis=1)
data_4temp5.head(2)
以下语句讲分别计算的平均账户余额,账户余额的标准差、变异系数、平均入账和平均支出的比例等变量与之前的 data3 数据合并
data4=pd.merge(data3,data_4temp3,left_on='account_id',right_index=True,how='left')
data4=pd.merge(data4,data_4temp5,left_on='account_id',right_index=True,how='left')
最后计算存贷比、贷收比
data4['r_lb']=data4[['amount','avg_balance']].apply(lambda x:x[0]/x[1],axis=1)
data4['r_lincome']=data4[['amount','income']].apply(lambda x:x[0]/x[1],axis=1)
这部分是从信息中获取知识的过程。数据挖掘的方法分为分类和描述两大类,其中预测账户的违约情况属于分类模型。使用逻辑回归为刚才创建的数据建模
提取状态为C的样本用于预测。其他样本随机抽样,建立训练集与测试集:
data_model=data4[data4.status!='C']
for_predict=data4[data4.status=='C']
train=data_model.sample(frac=0.7,random_state=1235).copy()
test=data_model[~data_model.index.isin(train.index)].copy()
print('训练集样本量:%i\n测试集样本量:%i'%(len(train),len(test)))
训练集样本量:195
测试集样本量:84
使用向前逐步法进行逻辑回归建模
import statsmodels.formula.api as smf
import statsmodels.api as sm
def forward_select(data, response):
import statsmodels.api as sm
import statsmodels.formula.api as smf
remaining = set(data.columns)
remaining.remove(response)
selected = []
current_score, best_new_score = float('inf'), float('inf')
while remaining:
aic_with_candidates=[]
for candidate in remaining:
formula = "{} ~ {}".format(
response,' + '.join(selected + [candidate]))
aic = smf.glm(
formula=formula, data=data,
family=sm.families.Binomial(sm.families.links.logit)
).fit().aic
aic_with_candidates.append((aic, candidate))
aic_with_candidates.sort(reverse=True)
best_new_score, best_candidate=aic_with_candidates.pop()
if current_score > best_new_score:
remaining.remove(best_candidate)
selected.append(best_candidate)
current_score = best_new_score
print ('aic is {},continuing!'.format(current_score))
else:
print ('forward selection over!')
break
formula = "{} ~ {} ".format(response,' + '.join(selected))
print('final formula is {}'.format(formula))
model = smf.glm(
formula=formula, data=data,
family=sm.families.Binomial(sm.families.links.logit)
).fit()
return(model)
candidates=['bad_good','A1','GDP','A4','A10','A11','A12','amount','duration',
'A13','A14','A15','a16','avg_balance','stdev_balance','cv_balance',
'income','out','r_out_in','r_lb','r_lincome']
data_for_select=train[candidates]
lg_m1=forward_select(data=data_for_select,response='bad_good')
lg_m1.summary().tables[1]
coef | std err | z | P > z P>z P>z | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
Intercept | -7.2409 | 1.416 | -5.115 | 0.000 | -10.016 | -4.466 |
cv_balance | 10.7670 | 2.175 | 4.951 | 0.000 | 6.505 | 15.029 |
duration | 0.0462 | 0.021 | 2.239 | 0.025 | 0.006 | 0.087 |
r_lb | 0.2998 | 0.106 | 2.831 | 0.005 | 0.092 | 0.507 |
A10 | -0.0193 | 0.012 | -1.586 | 0.113 | -0.043 | 0.005 |
通过以上语句得到相关结果,表 5-1 列出逻辑回归的模型参数,其中申请贷款前一年的存贷比(r_lb),变异系数(cv_balance),贷款期限(duration)与违约正相关,城镇人口比例(A10)与违约负相关
以下使用测试数据进行模型效果评估。此时调用了 scikit-learn 的评估模块绘制 R O C ROC ROC 曲线
import sklearn.metrics as metrics
import matplotlib.pyplot as plt
fpr, tpr, th = metrics.roc_curve(test.bad_good, lg_m1.predict(test))
plt.figure(figsize=[6, 6])
plt.plot(fpr, tpr, 'b--')
plt.title('ROC curve')
plt.show()
print('AUC = %.4f' %metrics.auc(fpr, tpr))
可以看到模型的ROC曲线非常接近左上角,其曲线下面积(AUC)为 0.8689,这说明模型的排序能力很强
在这个案例中,贷款状态为 C 的账户是尚没有出现违约且合同未到期的客户。这些贷款客户中有些人的违约可能性较高,需要业务人员重点关注。一发现问题时,可以及时处理挽回损失。可以通过以下语句得到每笔贷款的违约概率
for_predict['prob']=lg_m1.predict(for_predict)
for_predict[['account_id','prob']].head()
account_id | prob | |
---|---|---|
23 | 1071 | 0.900833 |
30 | 5313 | 0.914699 |
38 | 10079 | 0.502645 |
39 | 5385 | 0.477389 |
42 | 8321 | 0.117526 |
输出结果见表 6-1 。这里需要强调的是,此处的概率仅是代表违约可能性的相对值,并业务人员知道哪些客户为重点关注的即可。不代表其真实违约概率。比如预测概率为 0.90 的违约可能性高于 0.47, 这已经足够了,因为业务人员知道哪些客户为重点关注即可
案例的建模流程。本案例中,我们遵照数据挖掘项目通用的流程 CRISP-DM 进行建模。最后回顾一下本案例的建模流程
(1) 业务分析:需要构建一个分类模型预测每个客户的违约概率,其实是对客户的信用进行一个排序。分类模型有很多种,其中逻辑回归是最常用到的。
(2) 数据解读:从业务需求出发,了解、熟悉现有的数据结构、数据质量等信息。主要寻找对客户违约成本、还款意愿、还款能力(资产规模和稳定性)有代表意义的变量。
(3) 数据准备:结合数据的内在价值与业务分析,提 I 取各类有价值的信息,构建被解释变量和解释变量。
(4) 模型构建与评价:该步骤按照 SEMMA 标准算法,分为数据采样、变量分布探索、修改变量、构建逻辑回归、评价模型的优劣。
(5) 模型监控:当模型上线后,对模型的表现进行长期监控,主要检验模型预测准确性与数据的稳定性。
在实际的工作中,上面提供流程的第 1~3 步并不一定一次性做好,很多时候这部分需要反复验证、反复解读。因为我们往往需要多次分析审核,所以可以较好地理解拿到的数据并且能够识别出数据中的异常或错误的内容。而此部分若纳人了错误的数据,则会导致后面的步骤,如建模等工作完全没有意义