滚动率计算逻辑

滚动率(Roll Rate)是贷后风险监控的核心指标,它衡量资产从当前逾期状态向更严重逾期状态迁移的比例。准确计算滚动率是设定漂移告警阈值的前提。本节将详细讲解两种主流的滚动率计算方法:基于账龄(Vintage)和基于观测点(Observation Point),并给出可直接复用的 Python 实现。

核心概念:滚动率 = 从状态 A 迁移到状态 B 的资产数量(或金额) / 期初处于状态 A 的资产总数。通常以月为观测周期。

一、基于账龄(Vintage)的滚动率计算

Vintage 分析以“放款月份”为分组维度,追踪同一批客户在后续各月中的逾期表现。其滚动率计算关注的是:同一 Vintage 的资产,在特定账龄(如第 6 个月)时,从 M0 滚动到 M1 的比例。

计算步骤:

  1. 分组:按放款月份(Vintage)对资产进行分组。
  2. 确定观测账龄:选定一个固定的账龄月数(例如,观察第 6 个月的表现)。
  3. 统计状态:统计该 Vintage 在账龄第 N 个月时,处于 M0(正常)和 M1(逾期 1-30 天)的资产数量。
  4. 计算迁移:统计该 Vintage 在账龄第 N+1 个月时,从 M0 变为 M1 的资产数量。
  5. 计算比率:滚动率 = 从 M0 迁移至 M1 的资产数 / 账龄第 N 个月时 M0 的资产总数。
适用场景:Vintage 滚动率主要用于评估不同时期放款资产的“先天质量”,常用于贷前策略回溯和资产组合的长期趋势分析。它剔除了账龄效应,能更纯粹地反映客群质量变化。

二、基于观测点(Observation Point)的滚动率计算

观测点法以“固定日历时间”为锚点,计算在特定时间点处于某一逾期状态的资产,在下一个月末的迁移情况。这是贷后监控中最常用的实时滚动率计算方法。

计算步骤:

  1. 确定观测点:选定一个具体的月末日期(例如 2024-01-31)。
  2. 统计期初状态:统计观测点当天,所有资产所处的逾期状态(M0, M1, M2...)。
  3. 统计期末状态:统计观测点下一个月末(例如 2024-02-29),同一批资产所处的逾期状态。
  4. 构建迁移矩阵:交叉统计期初状态与期末状态,形成 N x N 的迁移矩阵。
  5. 计算比率:例如,M0→M1 滚动率 = 期初为 M0 且期末变为 M1 的资产数 / 期初 M0 的总资产数。
两种滚动率计算方法对比
对比维度 Vintage 滚动率 观测点滚动率
分组维度 放款月份 固定日历时间
核心目的 评估不同批次资产的长期质量 监控近期资产状态的实时迁移
对账龄的处理 固定账龄,消除账龄影响 不固定账龄,反映当前状态
应用场景 贷前策略、资产组合分析 贷后监控、风险预警、阈值设定
数据要求 需要完整的放款与还款历史 需要最新的账户状态快照
滚动率计算逻辑对比图 基于 Vintage 的滚动率 Step 1: 按放款月份分组 Step 2: 固定账龄(如第6个月) Step 3: 统计 M0 → M1 迁移 输出:批次质量趋势 基于观测点的滚动率 Step 1: 确定观测点(月末) Step 2: 统计期初各状态 Step 3: 构建迁移矩阵 输出:实时迁移率

三、Python 实现滚动率计算函数

下面提供一个通用的 Python 函数,支持上述两种计算模式。该函数基于 Pandas 实现,适用于生产环境中的批量计算。

import pandas as pd
import numpy as np

def calculate_roll_rate(df, method='observation_point', 
                        vintage_col='vintage_month',
                        obs_date_col='obs_date',
                        current_state_col='current_state',
                        next_state_col='next_state',
                        target_transition=('M0', 'M1')):
    """
    计算滚动率(支持 Vintage 和观测点两种模式)
    
    Parameters
    ----------
    df : pd.DataFrame
        输入数据,必须包含期初状态和期末状态
    method : str
        'vintage' 或 'observation_point'
    vintage_col : str
        放款月份列名(仅 vintage 模式需要)
    obs_date_col : str
        观测日期列名(仅 observation_point 模式需要)
    current_state_col : str
        期初逾期状态列名
    next_state_col : str
        期末逾期状态列名
    target_transition : tuple
        目标迁移路径,如 ('M0', 'M1')
    
    Returns
    -------
    pd.DataFrame
        包含滚动率计算结果的数据框
    """
    from_state, to_state = target_transition
    
    if method == 'observation_point':
        # 按观测点分组计算
        result = []
        for obs_date, group in df.groupby(obs_date_col):
            # 期初处于 from_state 的资产
            initial_count = len(group[group[current_state_col] == from_state])
            if initial_count == 0:
                continue
            # 期末迁移到 to_state 的资产
            transition_count = len(group[
                (group[current_state_col] == from_state) & 
                (group[next_state_col] == to_state)
            ])
            roll_rate = transition_count / initial_count
            result.append({
                'obs_date': obs_date,
                'from_state': from_state,
                'to_state': to_state,
                'initial_count': initial_count,
                'transition_count': transition_count,
                'roll_rate': round(roll_rate, 4)
            })
        return pd.DataFrame(result)
    
    elif method == 'vintage':
        # 按 Vintage 分组计算
        result = []
        for vintage, group in df.groupby(vintage_col):
            initial_count = len(group[group[current_state_col] == from_state])
            if initial_count == 0:
                continue
            transition_count = len(group[
                (group[current_state_col] == from_state) & 
                (group[next_state_col] == to_state)
            ])
            roll_rate = transition_count / initial_count
            result.append({
                'vintage_month': vintage,
                'from_state': from_state,
                'to_state': to_state,
                'initial_count': initial_count,
                'transition_count': transition_count,
                'roll_rate': round(roll_rate, 4)
            })
        return pd.DataFrame(result)
    
    else:
        raise ValueError("method 参数必须为 'vintage' 或 'observation_point'")

# 示例用法
if __name__ == "__main__":
    # 构造示例数据
    sample_data = pd.DataFrame({
        'obs_date': ['2024-01-31'] * 1000 + ['2024-02-29'] * 1000,
        'current_state': ['M0'] * 800 + ['M1'] * 200 + ['M0'] * 750 + ['M1'] * 250,
        'next_state': ['M0'] * 700 + ['M1'] * 100 + ['M0'] * 50 + ['M1'] * 150 + 
                      ['M0'] * 650 + ['M1'] * 100 + ['M0'] * 60 + ['M1'] * 190
    })
    
    # 计算观测点滚动率
    obs_result = calculate_roll_rate(
        sample_data, 
        method='observation_point',
        target_transition=('M0', 'M1')
    )
    print("观测点滚动率结果:")
    print(obs_result)
注意事项:
  • 确保输入数据已按时间顺序排序,且期初与期末状态一一对应。
  • 当 initial_count 过小时(如 < 30),计算出的滚动率统计意义较弱,建议在告警阈值设定时进行样本量过滤。
  • 生产环境中建议使用 Decimal 类型或乘以 10000 取整,避免浮点数精度问题。

四、滚动率计算的关键参数与输出

无论采用哪种方法,滚动率计算的核心输出都是一个 迁移矩阵。下表展示了 M0→M1 滚动率的典型输出结构:

M0→M1 滚动率输出示例(观测点法)
观测点 期初 M0 数量 迁移至 M1 数量 滚动率 样本占比
2024-01-31 12,450 498 4.00% 100%
2024-02-29 12,100 605 5.00% 100%
2024-03-31 11,800 354 3.00% 100%
2024-04-30 11,500 690 6.00% 100%

在实际的贷后监控系统中,滚动率计算函数通常作为数据管道的一部分,每日或每月自动执行,输出结果直接用于后续的漂移检测和告警阈值判定。