← 返回编程题库
coding-vintage-cohort-period-default-rate中等免费版2000ms未尝试

期龄队列每期违约率:贷款 vintage 的条件风险率

Vintage Cohort Per-Period Default Rate: Conditional Hazard from a Loan Vintage

开始编码

实现 solution(cohort_originated_count: int, defaults_by_age: list[int], prepays_by_age: list[int]) -> list[float]。某零售信贷风险口在 0 时刻发放了一支由 cohort_originated_count 笔贷款构成的队列(vintage)。在随后的 T 个期龄上,交易台记录了 defaults_by_age[t](第 t 期违约的债务人数)与 prepays_by_age[t](第 t提前还款——没有违约就离开队列——的债务人数)。对每个 t = 0..T-1,返回条件违约率:在已存活到第 t 期开始的债务人中、第 t 期违约的比例。具体地:

active[0] = cohort_originated_count
对 t in 0..T-1:
    若 active[t] == 0:
        out[t] = NaN
    否则:
        out[t] = defaults_by_age[t] / active[t]
    active[t+1] = active[t] - defaults_by_age[t] - prepays_by_age[t]

**第 t 期的分母是 active[t](期初存活者),不是 cohort_originated_count 每期都除以原始队列大小得到的是无条件违约率(每个原始债务人的违约数),这会随队列缩小而机械下降,哪怕真实风险率恒定;交易台需要的是经验风险率**——"在已存活到该期"的条件下、当期的违约概率——这样才能在不同 vintage、不同期龄之间做同口径的对比。active 是滚动的存活计数:在前期把违约与提前还款都扣掉之后剩下的人数。

Example

solution(1000, [10, 8, 6, 4], [50, 40, 30, 20]) 返回 [0.01, 0.00851..., 0.00673..., 0.00467...]。逐期算:active[0] = 1000,第 0 期率 10/1000 = 0.01,然后 active[1] = 1000 - 10 - 50 = 940。第 1 期率 8/940 ≈ 0.00851active[2] = 940 - 8 - 40 = 892,第 2 期率 6/892 ≈ 0.00673active[3] = 892 - 6 - 30 = 856,第 3 期率 4/856 ≈ 0.00467。注意各期率始终在 0.5%-1% 附近——这是一条近似恒定的风险率。无条件版本(每期都除以 1000)会因为分子下降、分母不变而机械下降。

求和-与-原 cohort 的对照算例:cohort 100、defaults_by_age = [10, 9]prepays_by_age = [20, 30]。active 序列是 [100, 70]。第 1 期率是 9/70 ≈ 0.1286不是 9/100 = 0.09。无条件答法会告诉交易台"第 1 期风险率降到 9%",而真实条件风险率上升到约 13%——这一差错把趋势的方向都翻反了。

易错点

PREPAYS 减少下一期分母,不进入当期分子。prepay 是没有违约就离开队列的债务人——贷款被提前还清了。cohort 100、defaults_by_age = [5]prepays_by_age = [50] 时第 0 期率为 5/100 = 0.05不是 (5+50)/100 = 0.55。50 个 prepay 把 active[1] 砍到 45,但绝不进入第 0 期的分子。

ORDER 很关键:在第 tactive[t] 算 rate,defaults[t] + prepays[t] 得到 active[t+1]。先扣后除(用 defaults[t] / active[t+1] 当 rate)会让分母变小、违约率虚高。

active[t] == 0 时返回 NaN不是 0.0。NaN 信号"此期已无债务人,率值未定义"——dashboard 凭此区分这种状态与"在存活者中违约为零"(一个真实、独立、健康的状态)。测试集包含三种"队列耗尽"场景:仅由违约耗尽、仅由 prepay 耗尽、混合耗尽——耗尽之后的所有期都是 NaN。

T = 0(空的 defaults_by_ageprepays_by_age)返回 [],不要崩。

期望算法 O(T):单次正向扫描,维护滚动的 active 计数。对每个 t,先把 rate(或 active=0 时的 NaN)追加到输出,再扣 defaults[t] + prepays[t] 推进到下一期。无须前缀和或重启;递推一次完成。

实现细节由 stubs/stub.py 提供。

实践背景

某零售或中小企业信贷风险口监控的贷款组合按 vintage(同一发放季度发放的贷款队列)、按期龄(自发放起的月数/季数)追踪——而不是按日历日期。"vintage 曲线"绘制每个期龄上的条件违约率(per-period default rate),分析师就能把 2024-Q1 vintage 的第 6 期风险率与 2023-Q1 vintage 的第 6 期风险率做同口径对比。条件存活是关键:无条件违约率(按原始债务人数当分母)会随队列缩小、随期龄推移而机械下降,所有 vintage 看起来都在"持续好转",哪怕真实风险率恒定。条件版本(本题)就是经验风险率,可以与终生 EL 聚合器的 marginal_pd 输入(姊妹题 coding-cumulative-default-prob-from-marginal-hazard-rates)做拼接。Prepay 必须从分母中扣除(prepay 的债务人已离开 at-risk 池,不应再占着分母),但绝不进入分子(prepay 不是违约)。solution(...) 的输出就是组合期龄诊断的日常报表:vintage 跟踪 dashboard 上"按期龄绘制的风险率"那一列——每条 vintage 一条曲线,用来在累计损失数字暴露之前提前发现信用质量恶化。

约束条件

  • 1 <= cohort_originated_count <= 10000
  • 0 <= len(defaults_by_age) == len(prepays_by_age) <= 60
  • 0 <= defaults_by_age[t], prepays_by_age[t](对所有 t;均为非负整数)
  • schedule 可行:以 active[0] = cohort_originated_count、active[t+1] = active[t] - defaults_by_age[t] - prepays_by_age[t] 演化,则每个 active[t] >= 0 且 defaults_by_age[t] + prepays_by_age[t] <= active[t]
  • 输出为长度 T 的 list[float];每项落在 [0.0, 1.0] 或为 NaN;按 rel_tol=1e-9、abs_tol=1e-9 比对;NaN 与 NaN 相等

样例

Case 1 · statement-example: cohort 1000, mild defaults and prepays over 4 ages

输入: [1000,[10,8,6,4],[50,40,30,20]]

期望: [0.01,0.00851063829787234,0.006726457399103139,0.004672897196261682]

active = [1000, 940, 892, 856];逐期 = defaults / active 得 [0.01, 8/940, 6/892, 4/856]。

Case 2 · visible: denominator is active not cohort - late-period rate uses survivors

输入: [100,[10,9],[20,30]]

期望: [0.1,0.12857142857142856]

active = [100, 70];正确 9/70。若错用 9/100=0.09 是把分母锚回原始 cohort、漏掉了存活条件。

Case 3 · visible: T=0 empty defaults and prepays returns []

输入: [100,[],[]]

期望: []

T=0 直接返回 [],不抛异常。

Case 4 · visible: cohort exhausted -> NaN for subsequent ages

输入: [10,[10,0,0],[0,0,0]]

期望: [1,"NaN","NaN"]

active = [10, 0, 0, 0];t=0 是 10/10=1.0,之后 active=0,按约定返回 NaN(不是 0.0)。

Case 5 · visible: prepays reduce next-age denominator but never enter numerator

输入: [100,[5],[50]]

期望: [0.05]

5/100 = 0.05;预付 50 减少下一期 active、但不计入当期违约的分子。

最近提交

还没有提交记录。

编码区

实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。

加载编辑器...
计时0:00

默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。

Case 1 · statement-example: cohort 1000, mild defaults and prepays over 4 ages

输入: [1000,[10,8,6,4],[50,40,30,20]]

期望: [0.01,0.00851063829787234,0.006726457399103139,0.004672897196261682]

active = [1000, 940, 892, 856];逐期 = defaults / active 得 [0.01, 8/940, 6/892, 4/856]。

Case 2 · visible: denominator is active not cohort - late-period rate uses survivors

输入: [100,[10,9],[20,30]]

期望: [0.1,0.12857142857142856]

active = [100, 70];正确 9/70。若错用 9/100=0.09 是把分母锚回原始 cohort、漏掉了存活条件。

Case 3 · visible: T=0 empty defaults and prepays returns []

输入: [100,[],[]]

期望: []

T=0 直接返回 [],不抛异常。

Case 4 · visible: cohort exhausted -> NaN for subsequent ages

输入: [10,[10,0,0],[0,0,0]]

期望: [1,"NaN","NaN"]

active = [10, 0, 0, 0];t=0 是 10/10=1.0,之后 active=0,按约定返回 NaN(不是 0.0)。

Case 5 · visible: prepays reduce next-age denominator but never enter numerator

输入: [100,[5],[50]]

期望: [0.05]

5/100 = 0.05;预付 50 减少下一期 active、但不计入当期违约的分子。