大家好,我是东哥。
信贷风控领域中,经常用到账龄Vintage报表,这是入门初学者的难点之一,因为它涉及到用户还款、逾期等多种行为以及业务上的多种统计口径,因此很多朋友一直无法将逻辑梳理清楚。本次来给大家详细介绍Vintage报表的底层计算逻辑是什么样的。
以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观察日,当前观察日。
总结一下,在进行Vintage计算之前需要确认几个事项:
实际业务场景中,比较常用的是“MOB月末时点观测+当前逾期口径+逾期未结清余额”的逾期率口径。
以上4个都确定以后,剩下就看应还款日和实际还款日了,而应还款日是根据产品设计而定的,因此只有实际还款日是不确定的。实际还款日是由客户行为决定的,可以发生在任何的时间点,所以根据实际还款日的不同发生位置,就会产生多种情况。
第一种是,当应还日超过当前观察日的时候,也就是应还日还在未来,是未发生的事,因此我们无法判断。
第二种是,应还日在当前观察日之内了,属于我们可以观察到的历史数据了。此时,如果实际还款日在应还日当天或者之前,说明是正常还款,未发生逾期,因此逾期天数为0。
第三种是,实际还款日在应还日和mob观察日之间,说明虽然逾期了,但在mob观察日之前还上了。此时如果是当前逾期的口径,那么在mob月底观察是未发生逾期的,那么逾期天数为0;如果是曾经逾期口径,那么就发生过逾期了,逾期天数=实还日-应还日=5
第四种是,实际还款日在mob观察日之后,虽然也还了,但晚于mob观察点,因此当前逾期与曾经逾期口径是一样的,逾期天数都=MOB观察日-应还款日=21
第五种是,从应还日一直到当前观察日,客户一直没有还款动作,也就是一直未结清。因此当前逾期与曾经逾期口径也是一样的,逾期天数都=MOB观察日-应还款日=21
前面我们根据4个统计时点,计算出每个客户在各个mob下的逾期状态和逾期天数。
对于核心部分逾期天数和金额计算的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
以上是全部。
完整代码如下(真实业务数据+代码实操):