(金融)线上消费贷风控优化(单变量分析)与用户留存项目(同期群分析)——数据分析
一、首逾率单变量分析
案例背景:
日常监控发现某款消费贷产品首逾率有逐渐升高的趋势,需要把首逾率降下来以减少产品带来的损失。
分析目标:
分析思路:
- 因为我们所要分析的策略时将要在客户申请时用来判断客户是否会逾期的条件,所以策略分析时的基本思路就是还原这些有首逾表现的客户在申请时的数据(这个还原是指提取出客户在申请时点各个维度的数据,越多越好) ,然后利用这些数据去找出能够区分好坏客户的变量,制定策略。
申请类指标:
逾期类指标:
- 首逾率:首期逾期的合同数/首期到了应还款的总合同数
- Vintage-账龄:指公司尚未收回的应收账款的时间长度
- Vintage30+:每个申请月份在随后的月份逾期30+的贷款余额占该审批月份的放款额的比例。
- mob4vintage30+:某一月内贷款合同签署4个月后时,当前逾期超过30天的合同的未还金额/总放款合同金额。
import pandas as pd
import numpy as np
import sqlalchemy
1.0 数据读取
engine = sqlalchemy.create_engine('mysql+pymysql://id:**********@xxx.xxx.xx.xx:3306/db')
sql_cmd = 'select * from load_datas'
dt=pd.read_sql(sql=sql_cmd,con=engine)
dt
| user_id | age | occupation | work_times | credit_score | credit_level | credit_check_times | credit_card_use_rate | is_overdue |
---|
0 | 1 | 22 | 个体户 | 45.0 | 650.0 | A | 23 | 0.00 | 0 |
1 | 2 | 28 | 职员 | 34.0 | 680.0 | A | 33 | 0.32 | 0 |
2 | 3 | 39 | 其他 | 113.0 | 530.0 | A | 32 | 0.05 | 0 |
3 | 4 | 24 | 职员 | 44.0 | 800.0 | A | 35 | 0.01 | 0 |
4 | 5 | 30 | 职员 | 82.0 | NaN | A | 28 | 0.00 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
56451 | 56452 | 40 | None | NaN | NaN | 缺失 | 缺失 | NaN | 0 |
56452 | 56453 | 21 | None | NaN | NaN | 缺失 | 缺失 | NaN | 0 |
56453 | 56454 | 43 | None | NaN | NaN | 缺失 | 缺失 | NaN | 0 |
56454 | 56455 | 20 | None | NaN | NaN | 缺失 | 缺失 | NaN | 0 |
56455 | 56456 | 33 | None | NaN | NaN | 缺失 | 缺失 | NaN | 1 |
56456 rows × 9 columns
dt.rename(columns={'user_id':'用户id',
'age':'年龄',
'occupation':'职业',
'work_times':'工作时间',
'credit_score':'芝麻信用分',
'credit_level':'信用评级',
'credit_check_times':'近半年征信查询次数',
'credit_card_use_rate':'信用卡额度使用率',
'is_overdue':'是否有逾期记录',
},inplace=True)
dt.head()
| 用户id | 年龄 | 职业 | 工作时间 | 芝麻信用分 | 信用评级 | 近半年征信查询次数 | 信用卡额度使用率 | 是否有逾期记录 |
---|
0 | 1 | 22 | 个体户 | 45.0 | 650.0 | A | 23 | 0.00 | 0 |
1 | 2 | 28 | 职员 | 34.0 | 680.0 | A | 33 | 0.32 | 0 |
2 | 3 | 39 | 其他 | 113.0 | 530.0 | A | 32 | 0.05 | 0 |
3 | 4 | 24 | 职员 | 44.0 | 800.0 | A | 35 | 0.01 | 0 |
4 | 5 | 30 | 职员 | 82.0 | NaN | A | 28 | 0.00 | 0 |
dt.shape
(56456, 9)
1.1统计总体逾期情况
总体首逾率=dt.是否有逾期记录.sum()/dt.是否有逾期记录.count()
总体首逾率
0.307584667705824
1.2 筛选出有效变量:
这里用到单变量分析的方法,单变量分析的主要目的是筛选出好坏区分度较好的变量以便制定策略。在消金公司的日常工作中,会有专门负责爬取变量和计算加工变量数据的团队,他们在不断的去获取加工很多可能对风险控制有帮助的数据提供给我们风控团队,而我们风控人员就需要从这成千上万个变量中探查出能够控制逾期风险但同时又不会误拒很多好客户的变量。
拿到数据的第一步就是针对每个变量单独分析,查看其对逾期的影响。
1.2.1 征信查询次数分组
def group_check(n):
try:
n=int(n)
if n>=0 and n<3:
return '1:[0,3)'
elif n>=3 and n<6:
return '2:[3,6)'
elif n>=6 and n<12:
return '3:[6,12)'
elif n>=12 and n<21:
return '4:[12:21'
elif n>=21:
return '5:[21,+∞)'
except Exception as e:
return '6:缺失'
dt['征信分组']=dt.近半年征信查询次数.apply(group_check)
dt.head()
| 用户id | 年龄 | 职业 | 工作时间 | 芝麻信用分 | 信用评级 | 近半年征信查询次数 | 信用卡额度使用率 | 是否有逾期记录 | 征信分组 |
---|
0 | 1 | 22 | 个体户 | 45.0 | 650.0 | A | 23 | 0.00 | 0 | 5:[21,+∞) |
1 | 2 | 28 | 职员 | 34.0 | 680.0 | A | 33 | 0.32 | 0 | 5:[21,+∞) |
2 | 3 | 39 | 其他 | 113.0 | 530.0 | A | 32 | 0.05 | 0 | 5:[21,+∞) |
3 | 4 | 24 | 职员 | 44.0 | 800.0 | A | 35 | 0.01 | 0 | 5:[21,+∞) |
4 | 5 | 30 | 职员 | 82.0 | NaN | A | 28 | 0.00 | 0 | 5:[21,+∞) |
dt_info=dt.groupby('征信分组').agg({'用户id':'count',
'是否有逾期记录':sum}).reset_index().rename(columns={'用户id':'区间客户数',
'是否有逾期记录':'区间逾期客户数'})
dt_info
| 征信分组 | 区间客户数 | 区间逾期客户数 |
---|
0 | 1:[0,3) | 14667 | 3352 |
1 | 2:[3,6) | 15469 | 4157 |
2 | 3:[6,12) | 15316 | 4786 |
3 | 4:[12:21 | 6577 | 2739 |
4 | 5:[21,+∞) | 3213 | 1923 |
5 | 6:缺失 | 1214 | 408 |
dt_info['区间未逾期客户数']=dt_info.区间客户数-dt_info.区间逾期客户数
dt_info['区间客户占比']=dt_info.区间客户数/dt_info.区间客户数.sum()
dt_info['区间首逾率']=dt_info.区间逾期客户数/dt_info.区间客户数
dt_info
| 征信分组 | 区间客户数 | 区间逾期客户数 | 区间未逾期客户数 | 区间客户占比 | 区间首逾率 |
---|
0 | 1:[0,3) | 14667 | 3352 | 11315 | 0.259795 | 0.228540 |
1 | 2:[3,6) | 15469 | 4157 | 11312 | 0.274001 | 0.268731 |
2 | 3:[6,12) | 15316 | 4786 | 10530 | 0.271291 | 0.312484 |
3 | 4:[12:21 | 6577 | 2739 | 3838 | 0.116498 | 0.416451 |
4 | 5:[21,+∞) | 3213 | 1923 | 1290 | 0.056912 | 0.598506 |
5 | 6:缺失 | 1214 | 408 | 806 | 0.021503 | 0.336079 |
1.2.2 信用评级分组
dt.信用评级.unique()
array(['A', 'AA', 'B', 'C', 'D', 'E', 'HR', 'NC', '缺失'], dtype=object)
def group_score(level):
if level=='A':
return 'A'
elif level=='AA':
return 'AA'
elif level in ('B','C','D'):
return 'BCD'
elif level in ('E','HR','NC'):
return 'ERC'
else:
return '缺失'
dt['信用评级分组']=dt.信用评级.apply(group_score)
dt.head()
| 用户id | 年龄 | 职业 | 工作时间 | 芝麻信用分 | 信用评级 | 近半年征信查询次数 | 信用卡额度使用率 | 是否有逾期记录 | 征信分组 | 信用评级分组 |
---|
0 | 1 | 22 | 个体户 | 45.0 | 650.0 | A | 23 | 0.00 | 0 | 5:[21,+∞) | A |
1 | 2 | 28 | 职员 | 34.0 | 680.0 | A | 33 | 0.32 | 0 | 5:[21,+∞) | A |
2 | 3 | 39 | 其他 | 113.0 | 530.0 | A | 32 | 0.05 | 0 | 5:[21,+∞) | A |
3 | 4 | 24 | 职员 | 44.0 | 800.0 | A | 35 | 0.01 | 0 | 5:[21,+∞) | A |
4 | 5 | 30 | 职员 | 82.0 | NaN | A | 28 | 0.00 | 0 | 5:[21,+∞) | A |
dt_info2=dt.groupby('信用评级分组').agg({'用户id':'count',
'是否有逾期记录':sum}).reset_index().rename(columns={'用户id':'区间客户数',
'是否有逾期记录':'区间逾期客户数'})
dt_info2
| 信用评级分组 | 区间客户数 | 区间逾期客户数 |
---|
0 | A | 3369 | 851 |
1 | AA | 3784 | 640 |
2 | BCD | 16320 | 5920 |
3 | ERC | 8060 | 4251 |
4 | 缺失 | 24923 | 5703 |
dt_info2['区间未逾期客户数']=dt_info2.区间客户数-dt_info2.区间逾期客户数
dt_info2['区间客户占比']=dt_info2.区间客户数/dt_info2.区间客户数.sum()
dt_info2['区间首逾率']=dt_info2.区间逾期客户数/dt_info2.区间客户数
dt_info2
| 信用评级分组 | 区间客户数 | 区间逾期客户数 | 区间未逾期客户数 | 区间客户占比 | 区间首逾率 |
---|
0 | A | 3369 | 851 | 2518 | 0.059675 | 0.252597 |
1 | AA | 3784 | 640 | 3144 | 0.067026 | 0.169133 |
2 | BCD | 16320 | 5920 | 10400 | 0.289075 | 0.362745 |
3 | ERC | 8060 | 4251 | 3809 | 0.142766 | 0.527419 |
4 | 缺失 | 24923 | 5703 | 19220 | 0.441459 | 0.228825 |
1.3 计算提升度:
在进行变量分析之后,这时我们就要从中筛选中较为有效的变量了,这里涉及到一个衡量变量是否有效的指标,提升度。
提升度:通俗的来说就是衡量拒绝最坏那一部分的客户之后,对整体的风险控制的提升效果。
提升度越高,说明该变量可以更有效的区分好坏客户,能够更少的误拒好客户。
计算公式:提升度=最坏分箱的首逾客户占总首逾客户的比例 /该分箱的区间客户数占比
- 例如:上表中征信总查询次数的最坏分箱提升度就是(1923/17365)/(3213/56456)=11%/5.69%=1.93
提升度这个指标一般来说都是用来一批变量分析里做相对比较,很多时候都是在有限的变量里尽可能选提升度更高的变量来做策略。
如下,通过对所有变量的提升度进行倒序排列,发现个人征信总查询次数和客户信用评级的提升度最高,达到1.93和1.71。
dt_info.区间首逾率.idxmax()
4
worst_rate=dt_info.loc[dt_info.区间首逾率.idxmax()]['区间逾期客户数']/dt_info.区间逾期客户数.sum()
num_rate=dt_info.loc[dt_info.区间首逾率.idxmax()]['区间客户数']/dt_info.区间客户数.sum()
worst_rate/num_rate
1.9458254325820934
dt_info2.区间首逾率.idxmax()
3
worst_rate2=dt_info2.loc[dt_info2.区间首逾率.idxmax()]['区间逾期客户数']/dt_info2.区间逾期客户数.sum()
num_rate2=dt_info2.loc[dt_info2.区间首逾率.idxmax()]['区间客户数']/dt_info2.区间客户数.sum()
worst_rate2/num_rate2
1.7147127611157038
1.4 制定优化策略:
通过上一步的单变量分析,我们筛出了’征信查询次数’、‘信用评级’这两个提升度最高的变量。现在我们看一下如果将这两个变量的最坏分箱的客户都拒绝之后,对整体逾期的影响。
这个影响就是指假设我们将‘征信总查询次数>=21的3213位客户全部拒绝’之后,剩下的客户逾期率相比拒绝之前的逾期率降幅是多少。
1.4.1 依据征信查询次数优化风控策略
check_new_yuqi_kehu=dt_info.区间逾期客户数.sum()-dt_info.loc[dt_info.区间首逾率.idxmax()]['区间逾期客户数']
check_new_yuqi_rate=check_new_yuqi_kehu/dt_info.区间客户数.sum()
check_new_yuqi_rate
0.27352274337537197
check_old_yuqi_rate=dt_info.区间逾期客户数.sum()/dt_info.区间客户数.sum()
check_old_yuqi_rate
0.307584667705824
check_optimiz=check_new_yuqi_rate-check_old_yuqi_rate
check_optimiz
-0.03406192433045202
1.4.2 依据芝麻信用分优化风控策略
score_new_yuqi_kehu=dt_info2.区间逾期客户数.sum()-dt_info2.loc[dt_info2.区间首逾率.idxmax()]['区间逾期客户数']
score_new_yuqi_rate=score_new_yuqi_kehu/dt_info2.区间客户数.sum()
score_new_yuqi_rate
0.23228709083179822
score_old_yuqi_rate=dt_info2.区间逾期客户数.sum()/dt_info2.区间客户数.sum()
score_old_yuqi_rate
0.307584667705824
score_optimiz=score_new_yuqi_rate-score_old_yuqi_rate
score_optimiz
-0.07529757687402577
二、用户留存率的同期群分析
什么是同期群分析:
- 用户在产品使用中都有一个用户行为流程,不同时期的用户表现情况可能不一样,群组分析的主要目的是分析相似群体随时间的变化,核心就是对比、分析不同时间群组的用户,在相同周期下的行为差异, 所以也称同期群分析。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210312164142343.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Njk2MDk3MQ==,size_16,color_FFFFFF,t_70)
同期群分析的作用: - 对处于相同生命周期阶段的用户进行垂直分析(横向比较),从而比较得出相似群体随时间的变化(下图可以看到客单价随着时间推移在逐渐降低)。
- 通过比较不同的同期群,可以从总体上看到,应用的表现是否越来越好了。从而验证产品改进是否取得了效果。(下图可以看到客单价在逐渐提高)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210312164340674.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Njk2MDk3MQ==,size_16,color_FFFFFF,t_70)
2.0 读取数据
engine=sqlalchemy.create_engine('mysql+pymysql://frogdata05:Frogdata!1321@localhost:3306/froghd')
sql_cmd='select * from groups_data '
df=pd.read_sql(sql=sql_cmd,con=engine)
df[['orderid','orderdate','userid','totalcharges']].head()
| orderid | orderdate | userid | totalcharges |
---|
0 | 262 | 2009-01-11 | 12 | 50.67 |
1 | 278 | 2009-01-12 | 13 | 26.60 |
2 | 294 | 2009-01-13 | 14 | 38.71 |
3 | 301 | 2009-01-14 | 15 | 53.38 |
4 | 302 | 2009-01-15 | 16 | 14.28 |
df['order_month']=df.orderdate.apply(lambda x:x.strftime('%Y-%m'))
df.head()
| orderid | orderdate | userid | totalcharges | commonld | pupld | pickupdate | order_month |
---|
0 | 262 | 2009-01-11 | 12 | 50.67 | TRQKD | 2.0 | 2009-01-12 | 2009-01 |
1 | 278 | 2009-01-12 | 13 | 26.60 | 4HH2S | 3.0 | 2009-01-20 | 2009-01 |
2 | 294 | 2009-01-13 | 14 | 38.71 | 3TRDC | 2.0 | 2009-02-04 | 2009-01 |
3 | 301 | 2009-01-14 | 15 | 53.38 | NGAZJ | 2.0 | 2009-02-09 | 2009-01 |
4 | 302 | 2009-01-15 | 16 | 14.28 | FFYHD | 2.0 | 2009-02-09 | 2009-01 |
2.1构建用户群组并聚合总用户数、总订单数
df.set_index('userid',inplace= True)
df['cohort_group']=df.groupby(level=0)['orderdate'].min().apply(lambda x:x.strftime('%Y-%m'))
df.reset_index(inplace=True)
df.head()
| userid | orderid | orderdate | totalcharges | commonld | pupld | pickupdate | order_month | cohort_group |
---|
0 | 12 | 262 | 2009-01-11 | 50.67 | TRQKD | 2.0 | 2009-01-12 | 2009-01 | 2009-01 |
1 | 13 | 278 | 2009-01-12 | 26.60 | 4HH2S | 3.0 | 2009-01-20 | 2009-01 | 2009-01 |
2 | 14 | 294 | 2009-01-13 | 38.71 | 3TRDC | 2.0 | 2009-02-04 | 2009-01 | 2009-01 |
3 | 15 | 301 | 2009-01-14 | 53.38 | NGAZJ | 2.0 | 2009-02-09 | 2009-01 | 2009-01 |
4 | 16 | 302 | 2009-01-15 | 14.28 | FFYHD | 2.0 | 2009-02-09 | 2009-01 | 2009-01 |
group_month=df.groupby(['cohort_group','order_month'])
cohorts=group_month.agg({'userid':pd.Series.nunique,
'orderid':pd.Series.nunique,
'totalcharges':sum})
cohorts
cohorts.rename(columns={'userid':'totaluser',
'orderid':'totalorders'},inplace=True)
cohorts.head()
| | totaluser | totalorders | totalcharges |
---|
cohort_group | order_month | | | |
---|
2009-01 | 2009-01 | 20 | 21 | 326.44 |
2009-02 | 10 | 10 | 0.00 |
2009-03 | 8 | 11 | 0.00 |
2009-05 | 4 | 4 | 0.00 |
2009-02 | 2009-02 | 28 | 28 | 0.00 |
2.2 对每个用户群组重新编号,并统计各群组用户总数
def cohort_period(df):
df['cohort_period']=np.arange(len(df))+1
return df
cohorts=cohorts.groupby(level=0).apply(cohort_period)
cohorts.head()
| | totaluser | totalorders | totalcharges | cohort_period |
---|
cohort_group | order_month | | | | |
---|
2009-01 | 2009-01 | 20 | 21 | 326.44 | 1 |
2009-02 | 10 | 10 | 0.00 | 2 |
2009-03 | 8 | 11 | 0.00 | 3 |
2009-05 | 4 | 4 | 0.00 | 4 |
2009-02 | 2009-02 | 28 | 28 | 0.00 | 1 |
cohorts.reset_index(inplace=True)
cohorts.set_index(['cohort_group','cohort_period'],inplace=True)
cohorts.head()
| | order_month | totaluser | totalorders | totalcharges |
---|
cohort_group | cohort_period | | | | |
---|
2009-01 | 1 | 2009-01 | 20 | 21 | 326.44 |
2 | 2009-02 | 10 | 10 | 0.00 |
3 | 2009-03 | 8 | 11 | 0.00 |
4 | 2009-05 | 4 | 4 | 0.00 |
2009-02 | 1 | 2009-02 | 28 | 28 | 0.00 |
cohort_group_size=cohorts['totaluser'].groupby(level=0).first()
cohort_group_size.head()
cohort_group
2009-01 20
2009-02 28
2009-03 12
2009-04 8
Name: totaluser, dtype: int64
2.3 统计留存率
cohorts_v=cohorts['totaluser'].unstack(0)
cohorts_v
cohort_group | 2009-01 | 2009-02 | 2009-03 | 2009-04 |
---|
cohort_period | | | | |
---|
1 | 20.0 | 28.0 | 12.0 | 8.0 |
2 | 10.0 | 5.0 | 6.0 | 5.0 |
3 | 8.0 | 4.0 | NaN | NaN |
4 | 4.0 | NaN | NaN | NaN |
user_retention=cohorts_v/cohort_group_size
user_retention
cohort_group | 2009-01 | 2009-02 | 2009-03 | 2009-04 |
---|
cohort_period | | | | |
---|
1 | 1.0 | 1.000000 | 1.0 | 1.000 |
2 | 0.5 | 0.178571 | 0.5 | 0.625 |
3 | 0.4 | 0.142857 | NaN | NaN |
4 | 0.2 | NaN | NaN | NaN |
2.4绘图展示
import matplotlib.pyplot as plt
import matplotlib as mpl
pd.set_option('max_column',50)
mpl.rcParams['lines.linewidth']=2
%matplotlib inline
user_retention[['2009-01','2009-02','2009-03']].plot(figsize=(6,4))
plt.title('user retention')
plt.xticks(np.arange(1,13,1))
plt.xlim(1,12)
plt.ylabel('%of cohort')
Text(0, 0.5, '%of cohort')
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021031216521293.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Njk2MDk3MQ==,size_16,color_FFFFFF,t_70)
import seaborn as sns
sns.set(style='whitegrid')
plt.figure(figsize=(10,5))
plt.title('heat map:user_retention')
sns.heatmap(user_retention.T,mask=user_retention.T.isnull(),annot=True,annot_kws={'color':'tan','weight':'bold'},fmt='.2%')
<matplotlib.axes._subplots.AxesSubplot at 0x7f05ae875c90>
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021031216522238.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Njk2MDk3MQ==,size_16,color_FFFFFF,t_70)