4. 坏账率指标定义:公式、时间窗口与逾期口径

坏账率是量化回测中最核心的风险指标之一,直接反映信贷资产组合的信用损失水平。在 Flink 流处理环境中,坏账率的计算需要同时解决三个关键问题:计算公式的选取、时间窗口的划分、逾期口径的标准化。本节将逐一拆解,并给出流式场景下的最佳实践。

4.1 坏账率计算公式

坏账率的本质是“逾期损失金额”与“应回收本金总额”的比值。根据业务场景不同,主流公式分为以下三种:

公式类型 计算公式 适用场景 流式适配难度
期末余额法 坏账率 = 期末逾期余额 / 期末在贷余额 静态报表、监管报送 低(快照计算)
发生额法 坏账率 = 当期新增坏账金额 / 当期应还本金总额 流量监控、风险预警 中(需对齐还款周期)
Vintage 法 坏账率 = 各账龄段累计坏账 / 对应放款金额 资产质量分析、回测归因 高(需按放款批次分组)
流处理推荐:在 Flink 实时回测场景中,优先使用 发生额法 作为主指标,因为它天然适配事件驱动模型——每笔还款事件到达时即可更新分子分母,无需等待期末快照。Vintage 法则作为辅助分析指标,用于评估不同放款批次的长周期表现。

4.2 时间窗口:滚动 / 滑动 / 会话

时间窗口决定了坏账率计算的“观察范围”。Flink 提供了三种原生窗口类型,在坏账率计算中有不同的应用模式:

  • 滚动窗口(Tumbling Window):固定长度、不重叠。例如“每日坏账率”,每天 00:00:00 至 23:59:59 为一个窗口,窗口结束时输出当日坏账率。适合日报、周报等固定周期报表。
  • 滑动窗口(Sliding Window):固定长度、可重叠。例如“近 30 天坏账率,每 1 天滑动一次”,窗口长度为 30 天,滑动步长为 1 天。适合实时监控面板,能平滑反映风险趋势变化。
  • 会话窗口(Session Window):按事件活跃间隔划分。例如“用户连续借贷行为窗口”,当用户超过 7 天无还款事件时关闭当前会话。适合分析用户维度的借贷周期坏账率。
回测场景建议:量化回测通常使用 滑动窗口 作为主窗口类型,因为回测需要模拟“实时滚动观察”的效果。窗口长度建议设置为 30 天或 90 天,滑动步长设置为 1 天,以捕捉坏账率的连续变化曲线。
三种时间窗口在坏账率计算中的应用对比 滚动窗口 长度固定,不重叠 例:每日坏账率 滑动窗口 长度固定,可重叠 例:近30天坏账率(日滑动) 会话窗口 按事件间隔划分 例:用户借贷周期坏账率 窗口选择决策指南 • 固定报表 → 滚动窗口(计算简单,结果稳定) • 实时监控 → 滑动窗口(趋势平滑,灵敏度高) • 用户行为分析 → 会话窗口(贴合业务周期) • 量化回测 → 滑动窗口(模拟连续观察,推荐30天/90天)

4.3 逾期口径:M1 / M2 / M3+

逾期口径是坏账率计算的“分子定义”。不同逾期阶段对应不同的风险等级和损失预期。在量化回测中,必须明确指定使用哪个逾期口径,否则回测结果无法横向对比。

逾期口径 逾期天数范围 风险等级 回测常用场景
M1 1 ~ 30 天 早期预警 流动性风险监控、催收效率评估
M2 31 ~ 60 天 中度风险 信用评分卡验证、策略调优
M3+ 61 天及以上 高损失风险 坏账拨备、资产证券化定价
口径一致性警告:在同一个回测项目中,所有策略对比必须使用 相同的逾期口径。例如,策略 A 使用 M2 口径计算坏账率 3.5%,策略 B 使用 M1 口径计算坏账率 8.2%,两者不能直接比较。建议在回测配置文件中显式声明逾期口径参数,并在输出指标名称中标注(如 bad_rate_m2)。

4.4 流式坏账率计算架构

将上述三个要素整合到 Flink 流处理中,形成完整的坏账率计算管道。下图展示了从原始还款事件到最终坏账率输出的核心流程:

Flink 流式坏账率计算架构 还款事件流 Kafka / Pulsar 逾期口径映射 M1 / M2 / M3+ 时间窗口聚合 滑动窗口 30d/1d 坏账率计算 发生额法公式 结果输出 ClickHouse / Redis 配置参数示例 逾期口径: M2 窗口类型: 滑动 窗口长度: 30 天 滑动步长: 1 天 计算公式: 发生额法

4.5 代码示例:Flink SQL 实现坏账率计算

以下 Flink SQL 示例展示了如何组合逾期口径过滤、滑动窗口聚合和发生额法公式,实现实时坏账率计算:

-- 定义还款事件源表
CREATE TABLE repayment_events (
    loan_id      STRING,
    user_id      STRING,
    due_date     TIMESTAMP(3),
    repay_date   TIMESTAMP(3),
    due_amount   DECIMAL(12,2),
    repay_amount DECIMAL(12,2),
    event_time   TIMESTAMP(3) METADATA FROM 'timestamp',
    WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'repayment_events',
    'properties.bootstrap.servers' = 'localhost:9092',
    'format' = 'json'
);

-- 计算 M2 口径坏账率(逾期 31~60 天)
-- 使用滑动窗口:窗口长度 30 天,滑动步长 1 天
CREATE VIEW bad_rate_m2 AS
SELECT
    window_start,
    window_end,
    -- 分子:逾期 31~60 天的未还本金
    SUM(CASE 
        WHEN DATEDIFF(DAY, due_date, repay_date) BETWEEN 31 AND 60 
        THEN due_amount - repay_amount 
        ELSE 0 
    END) AS overdue_amount_m2,
    -- 分母:窗口内应还本金总额
    SUM(due_amount) AS total_due_amount,
    -- 坏账率
    SUM(CASE 
        WHEN DATEDIFF(DAY, due_date, repay_date) BETWEEN 31 AND 60 
        THEN due_amount - repay_amount 
        ELSE 0 
    END) / NULLIF(SUM(due_amount), 0) AS bad_rate_m2
FROM TABLE(
    HOP(TABLE repayment_events, DESCRIPTOR(event_time), INTERVAL '1' DAY, INTERVAL '30' DAY)
)
GROUP BY window_start, window_end;

-- 输出到结果表
CREATE TABLE bad_rate_sink (
    window_start      TIMESTAMP(3),
    window_end        TIMESTAMP(3),
    overdue_amount_m2 DECIMAL(12,2),
    total_due_amount  DECIMAL(12,2),
    bad_rate_m2       DECIMAL(6,4)
) WITH (
    'connector' = 'jdbc',
    'url' = 'jdbc:clickhouse://localhost:8123/default',
    'table-name' = 'bad_rate_m2_daily'
);

INSERT INTO bad_rate_sink
SELECT * FROM bad_rate_m2;
口径切换技巧:将逾期天数范围提取为配置参数,通过 Flink 动态表属性或广播变量传入,即可在不重启作业的情况下切换 M1 / M2 / M3+ 口径。例如,将 DATEDIFF(DAY, due_date, repay_date) BETWEEN 31 AND 60 替换为参数化表达式,实现口径热切换。

4.6 常见陷阱与避坑指南

  • 分母为零问题:当窗口内无应还本金时,坏账率计算会出现除零异常。务必使用 NULLIF(SUM(due_amount), 0) 或 CASE WHEN 保护。
  • 逾期天数计算基准:逾期天数 = 实际还款日 - 应还款日。如果用户提前还款,逾期天数为负数,应过滤或归零处理。
  • 窗口边界对齐:滑动窗口的起始时间应与业务日历对齐(如自然日 00:00:00),避免因时区偏移导致数据错位。
  • 口径漂移:同一笔贷款在不同时间窗口可能落入不同逾期口径。例如,一笔贷款在第 29 天属于 M1,在第 31 天变为 M2。回测时需明确“以窗口结束时间点的逾期状态为准”。