由累计违约概率反推期边际违约概率:逆向 bootstrap
Marginal Default Probability from a Cumulative-PD Term Structure: Inverse Bootstrap
开始编码实现 solution(cumulative_pd: list[float]) -> list[float]。某信贷风险团队拿到一只债务人的累计违约概率期限结构(由 CDS 利差或评级迁移在不同视野下校准):cumulative_pd[t] 表示在第 t+1 期末(自然语义按 1 计数)的累计违约概率,元素 ∈ [0.0, 1.0],且非递减。团队需要由此反推边际(每期、条件于存活)违约概率,作为期分解期望损失瀑布的基本输入(EL_t = marginal_pd_t * LGD_t * EAD_t * survival_t)。请返回长度为 T 的 list[float],其中 marginal_pd[t] 表示在 0..t-1 都存活的条件下,第 t 期发生违约的条件概率。
定义
marginal_pd[0] = cumulative_pd[0],无需除法(t = 0 之前存活概率为 1,条件化平凡)。对 t >= 1,
marginal_pd[t] = (cumulative_pd[t] - cumulative_pd[t-1]) / (1.0 - cumulative_pd[t-1])这是前向合成 cumulative_pd[t] = 1 - prod_{k=0..t}(1 - marginal_pd[k]) 的唯一逆运算。重排存活递推 (1 - cumulative_pd[t]) = (1 - cumulative_pd[t-1]) * (1 - marginal_pd[t]) 解出 marginal_pd[t] 即得上式:marginal_pd[t] = 1 - (1 - cumulative_pd[t]) / (1 - cumulative_pd[t-1]) = (cumulative_pd[t] - cumulative_pd[t-1]) / (1 - cumulative_pd[t-1])。
示例
solution([0.1, 0.2, 0.4]) 返回 [0.1, 0.1111..., 0.25]。逐步:
marginal_pd[0] = cumulative_pd[0] = 0.1。marginal_pd[1] = (0.2 - 0.1) / (1 - 0.1) = 0.1 / 0.9 ≈ 0.1111。注意不是0.1(朴素增量)——见下方陷阱。marginal_pd[2] = (0.4 - 0.2) / (1 - 0.2) = 0.2 / 0.8 = 0.25。
第二例:solution([0.05, 0.145, 0.27325]) 返回 [0.05, 0.10, 0.15]。这是前向 sister 题的逆运算——marginals [0.05, 0.10, 0.15] 经 cumulative[t] = 1 - prod(1 - marginal[k]) 合成累计 [0.05, 0.145, 0.27325],把累计回喂给 solution 还原原始 marginal。
条件 vs. 增量陷阱
最常见的作者错误是写 marginal_pd[t] = cumulative_pd[t] - cumulative_pd[t-1](朴素增量)。增量是"第 t 期违约 AND 在 t-1 之前存活"的联合概率;marginal 是"第 t 期违约 GIVEN 在 t-1 之前存活"的条件概率。两者只在 cumulative_pd[t-1] = 0 时相等。算例:
cumulative_pd = [0.1, 0.2]:增量给marginal_pd[1] = 0.1;正确条件给0.1 / 0.9 ≈ 0.1111。cumulative_pd = [0.5, 0.7, 0.9]:增量给[0.5, 0.2, 0.2];正确条件给[0.5, 0.4, 0.6667]。cumulative_pd = [0.9, 0.95, 0.99]:增量给[0.9, 0.05, 0.04];正确条件给[0.9, 0.5, 0.8]。
差距随前一位累计增大;高 PD 曲线下增量解法可能漏掉一个量级的实际经济风险。修法很简单:每个增量再除以存活概率 1 - cumulative_pd[t-1]。
t = 0 的特殊情形
t = 0 时条件化平凡(P(在 0 之前存活) = 1),所以 marginal_pd[0] = cumulative_pd[0],无需除法。作者若写一个总是除以 1 - cumulative_pd[t-1] 的单分支实现,需要虚拟 cumulative_pd[-1] = 0 让公式退化:(cum[0] - 0) / (1 - 0) = cum[0]。数值上可行,但参考实现显式分开 t = 0 更整洁,也使 t >= 1 的吸收态逻辑与基线分支干净分离。
除零哨兵
当 cumulative_pd[t-1] == 1.0 时债务人在 t-1 期末已确定违约。"在存活条件下第 t 期违约的条件概率"无意义(无法对零测度的存活事件做条件),合约规定 marginal_pd[t] 返回 float('nan')。关键:一旦存活掉到 0,便永远是 0,所以触发后所有后续期 marginal 都得 NaN。参考实现检测触发条件(任意 t >= 1 处 cumulative_pd[t-1] == 1.0)后用 NaN 吞掉整条尾巴。
三种常见失败:
- 让 Python 自动抛
ZeroDivisionError算(cum[t] - 1.0) / 0.0。合约规定值(NaN)而非异常。务必先检测cumulative_pd[t-1] == 1.0,再做除法。 - 在触发位置返回 NaN,但后续位置仍照常算
(cum[t+1] - 1.0) / (1 - 1.0)。一旦触发,往后每一项都得 NaN。 - 触发条件写成
cumulative_pd[t] == 1.0(当前位置)而不是cumulative_pd[t-1] == 1.0(前一位置)。哨兵看前一位——cumulative_pd = [0.5, 1.0]返回[0.5, 1.0](无 NaN),因为t = 1时前一位是0.5、不是1.0。对照cumulative_pd = [1.0, 1.0]返回[1.0, NaN]——t = 1时前一位才是1.0。
其他边界
T == 0:返回[]。T == 1:返回[cumulative_pd[0]](t >= 1分支不执行;不会触发 NaN)。cumulative_pd = [0.0, 0.0, ..., 0.0]:全 0(公式自然给(0 - 0) / 1 = 0)。- 连续相等的累计(
cumulative_pd[t] == cumulative_pd[t-1],且都 < 1.0):本期无新增违约,分子为 0,marginal_pd[t] = 0。公式自然处理,无需特判。 cumulative_pd = [0.5, 1.0]:返回[0.5, 1.0](最后一位的1.0不触发 NaN——哨兵看前一位,不是当前位)。
合约假设前置条件 cumulative_pd[t] >= cumulative_pd[t-1](累计违约概率不能下降)对所有 t >= 1 成立。参考实现不做校验或裁剪。
预期算法 O(T):单次前向扫描,跟踪前一位累计、计算每期 marginal、触发后切到"吸收态"。
stubs/stub.py 提供函数骨架。
实践背景
信贷风险团队经常拿到累计违约概率期限结构(由 CDS 利差或评级迁移在 1y、3y、5y、10y 多视野估计),需要反推暗含的边际每期违约率。Marginal 是期分解期望损失瀑布的天然输入(EL_t = marginal_pd_t * LGD_t * EAD_t * survival_t),用于全寿命预期信用损失(CECL 或 IFRS 9)报表、按期释放准备、减值计划等。solution(cumulative_pd) 给出由累计反向 bootstrap 出 marginal 的唯一逆映射——正向合成(cumulative_pd[t] = 1 - prod(1 - marginal_pd[k]))的天然姊妹/逆运算。Desk 两个方向都常用:正向把 hazard 模型投射成全寿命 PD,反向从挂出或建模的累计曲线提取每期违约率。
约束条件
- 0 <= len(cumulative_pd) <= 60
- 0.0 <= cumulative_pd[t] <= 1.0 对所有 t
- 非递减:cumulative_pd[t] >= cumulative_pd[t-1] 对所有 t >= 1(前置条件;参考实现不做校验)
- 输出:list[float] 长度 T;元素 ∈ [0.0, 1.0] 或 float('nan')(当先前累计已达 1.0);rel_tol=1e-9、abs_tol=1e-9;NaN 与 NaN 视为相等
样例
Case 1 · statement-example: [0.1, 0.2, 0.4] -> [0.1, 0.1111..., 0.25]
输入: [[0.1,0.2,0.4]]
期望: [0.1,0.11111111111111112,0.25]
marginal[0]=cum[0]=0.1;marginal[1]=(0.2-0.1)/(1-0.1)=0.1/0.9≈0.1111;marginal[2]=(0.4-0.2)/(1-0.2)=0.2/0.8=0.25。注意 t>=1 是条件概率,不能简单写 cum[t]-cum[t-1]。
Case 2 · inverse-of-sibling: [0.05, 0.145, 0.27325] recovers [0.05, 0.10, 0.15]
输入: [[0.05,0.145,0.27325]]
期望: [0.05,0.09999999999999999,0.15]
前向 sibling 用 marginal=[0.05,0.10,0.15] 合成 cumulative=[0.05,0.145,0.27325];本题做逆向,应严格回到原始 marginal 序列(浮点容差内)。
Case 3 · adversarial: increment-only-trap [0.1, 0.2] (correct = 0.1/0.9, NOT 0.1)
输入: [[0.1,0.2]]
期望: [0.1,0.11111111111111112]
条件概率陷阱:作者若错写 marginal[1]=cum[1]-cum[0]=0.1 会答错。正确:(0.2-0.1)/(1-0.1)=0.1/0.9≈0.1111。
Case 4 · boundary: T=0 empty input returns []
输入: [[]]
期望: []
空输入直接返回空列表,不应崩溃。
Case 5 · boundary: [1.0, 1.0] absorbing from start (NaN tail)
输入: [[1,1]]
期望: [1,"NaN"]
marginal[0]=1.0;t>=1 时 cum[0]=1.0 → 分母为 0 → NaN。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: [0.1, 0.2, 0.4] -> [0.1, 0.1111..., 0.25]
输入: [[0.1,0.2,0.4]]
期望: [0.1,0.11111111111111112,0.25]
marginal[0]=cum[0]=0.1;marginal[1]=(0.2-0.1)/(1-0.1)=0.1/0.9≈0.1111;marginal[2]=(0.4-0.2)/(1-0.2)=0.2/0.8=0.25。注意 t>=1 是条件概率,不能简单写 cum[t]-cum[t-1]。
Case 2 · inverse-of-sibling: [0.05, 0.145, 0.27325] recovers [0.05, 0.10, 0.15]
输入: [[0.05,0.145,0.27325]]
期望: [0.05,0.09999999999999999,0.15]
前向 sibling 用 marginal=[0.05,0.10,0.15] 合成 cumulative=[0.05,0.145,0.27325];本题做逆向,应严格回到原始 marginal 序列(浮点容差内)。
Case 3 · adversarial: increment-only-trap [0.1, 0.2] (correct = 0.1/0.9, NOT 0.1)
输入: [[0.1,0.2]]
期望: [0.1,0.11111111111111112]
条件概率陷阱:作者若错写 marginal[1]=cum[1]-cum[0]=0.1 会答错。正确:(0.2-0.1)/(1-0.1)=0.1/0.9≈0.1111。
Case 4 · boundary: T=0 empty input returns []
输入: [[]]
期望: []
空输入直接返回空列表,不应崩溃。
Case 5 · boundary: [1.0, 1.0] absorbing from start (NaN tail)
输入: [[1,1]]
期望: [1,"NaN"]
marginal[0]=1.0;t>=1 时 cum[0]=1.0 → 分母为 0 → NaN。