大家好,我是东哥。
信贷风控领域中,经常用到账龄Vintage报表,这是入门初学者的难点之一,因为它涉及到用户还款、逾期等多种行为以及业务上的多种统计口径,因此很多朋友一直无法将逻辑梳理清楚。本次来给大家详细介绍Vintage报表的底层计算逻辑是什么样的。
一、4个统计时点
以2022-11放款月份为例,各个MOB对应的M2+逾期率为:
MOB1的M2+逾期率=MOB1的M2+逾期金额/2022年11月总放款金额=0
MOB2的M2+逾期率=MOB2的M2+逾期金额/2022年11月总放款金额=0.95%
…
MOB12的M2+逾期率=MOB12的M2+逾期金额/2022年11月总放款金额=3.22%
通用公式:MOB(N)的M2+逾期率=MOB(N)的M2+逾期金额/xx放款月份的总放款金额
要计算每个单元格的逾期率,需要首先了解4个统计时点:应还款日、实际还款日、MOB观察日,当前观察日。
- 应还款日:还款计划生成后,确定了每个月的还款日。有两个方式,第一种是还款日根据放款日而定,比如2022年11月10日放款,那么后续每个月10号还款,第二种是所有客户都是同一还款日,比如所有客户都在每个月的21号还款。
- 实际还款日:客户实际的还款日,由客户还款行为决定,与应还款日比较以后可有三种方式,提前还款、按时还款、逾期不还。
- MOB观察日:每个MOB月的观察时点,也分为两种,一种是期末时点,一种是月末时点。
- 当前观察日:就是假设你站在了某个时点,然后对历史每个月放款后各MOB逾期数据的回看。与前三个时点不同,当前观察日不是周期性产生的,而是固定不变的,对于所有放款月都一样。
总结一下,在进行Vintage计算之前需要确认几个事项:
- 当前观察日是哪天?
- MOB观察日的口径,是月末时点,还是期末时点?
- 观察逾期的口径,是当前current逾期,还是曾经ever逾期?
- 金额口径还是订单口径?
实际业务场景中,比较常用的是“MOB月末时点观测+当前逾期口径+逾期未结清余额”的逾期率口径。
以上4个都确定以后,剩下就看应还款日和实际还款日了,而应还款日是根据产品设计而定的,因此只有实际还款日是不确定的。实际还款日是由客户行为决定的,可以发生在任何的时间点,所以根据实际还款日的不同发生位置,就会产生多种情况。
二、逾期天数计算
第一种是,当应还日超过当前观察日的时候,也就是应还日还在未来,是未发生的事,因此我们无法判断。
第二种是,应还日在当前观察日之内了,属于我们可以观察到的历史数据了。此时,如果实际还款日在应还日当天或者之前,说明是正常还款,未发生逾期,因此逾期天数为0。
第三种是,实际还款日在应还日和mob观察日之间,说明虽然逾期了,但在mob观察日之前还上了。此时如果是当前逾期的口径,那么在mob月底观察是未发生逾期的,那么逾期天数为0;如果是曾经逾期口径,那么就发生过逾期了,逾期天数=实还日-应还日=5
第四种是,实际还款日在mob观察日之后,虽然也还了,但晚于mob观察点,因此当前逾期与曾经逾期口径是一样的,逾期天数都=MOB观察日-应还款日=21
第五种是,从应还日一直到当前观察日,客户一直没有还款动作,也就是一直未结清。因此当前逾期与曾经逾期口径也是一样的,逾期天数都=MOB观察日-应还款日=21
三、逾期金额计算
前面我们根据4个统计时点,计算出每个客户在各个mob下的逾期状态和逾期天数。
- 逾期天数可以转化为逾期期数,比如M1+/M2+/M3+等等,因此我们就可以观察M1+/M2+/M3+的逾期率在vintage账龄下的趋势。
- 通过各mob的逾期状态判断,我们也可以统计出逾期的剩余未还本金,也就是我们前面所要求的金额逾期率口径的分子。
四、逾期率计算逻辑
五、Python代码实操
对于核心部分逾期天数和金额计算的Python代码展示如下。
############################逾期标识#########################################
# 加工出逾期标志
data_all_1['odu_flag'] = 0
data_all_1.loc[(data_all_1['actual_repay_date']>data_all_1['obser_month_end'])|(data_all_1['actual_repay_date'].isnull()),'odu_flag'] = 1
data_all_1['odu_flag_sft'] = data_all_1.groupby('order_id')['odu_flag'].transform(lambda x:x.shift(1))
data_all_1['actual_repay_date_sft'] = data_all_1.groupby('order_id')['actual_repay_date'].transform(lambda x:x.shift(1))
data_all_1['odu_first_flag'] = 0
data_all_1.loc[(data_all_1['odu_flag_sft']==1)&(data_all_1['odu_flag']==1)&(data_all_1['actual_repay_date_sft']>data_all_1['obser_month_end']),'odu_first_flag'] = 2
data_all_1.loc[(data_all_1['odu_flag_sft']==1)&(data_all_1['odu_flag']==1)&(data_all_1['actual_repay_date_sft']<=data_all_1['obser_month_end']),'odu_first_flag'] = 1
# 更新剩余本金
data_all_1.loc[data_all_1['odu_first_flag']==1,'balance'] = data_all_1['balance_sft']
data_all_1.loc[(data_all_1['odu_first_flag']==2),'balance'] = np.nan
data_all_1['balance'] = data_all_1.groupby('order_id')['balance'].transform(lambda x:x.ffill())
############################逾期天数#########################################
# 实还未超过月底观测日
data_all_1.loc[(data_all_1['actual_repay_date'].isnull()==False)&(data_all_1['actual_repay_date']<=data_all_1['obser_month_end']),'odu_days'] = 0
# 逾期-首次逾期的
data_all_1.loc[(data_all_1['odu_first_flag']==1),'odu_days'] = (data_all_1['obser_month_end']-data_all_1['expected_date']).dt.days
# 继续逾期的
data_all_1['expected_date_2'] = data_all_1['expected_date']
data_all_1.loc[(data_all_1['odu_first_flag']==2),'expected_date_2'] = np.nan
data_all_1['expected_date_2'] = data_all_1.groupby('order_id')['expected_date_2'].transform(lambda x:x.ffill())
data_all_1.loc[(data_all_1['odu_first_flag']==2),'odu_days'] = (data_all_1['obser_month_end']-data_all_1['expected_date_2']).dt.days
以上是全部。
完整代码如下(真实业务数据+代码实操):