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 天,以捕捉坏账率的连续变化曲线。
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 流处理中,形成完整的坏账率计算管道。下图展示了从原始还款事件到最终坏账率输出的核心流程:
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。回测时需明确“以窗口结束时间点的逾期状态为准”。