← 返回编程题库
coding-marginal-pd-from-cumulative-term-structure中等免费版2000ms未尝试

由累计违约概率反推期边际违约概率:逆向 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)。请返回长度为 Tlist[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 >= 1cumulative_pd[t-1] == 1.0)后用 NaN 吞掉整条尾巴。

三种常见失败:

  1. 让 Python 自动抛 ZeroDivisionError(cum[t] - 1.0) / 0.0。合约规定(NaN)而非异常。务必先检测 cumulative_pd[t-1] == 1.0,再做除法。
  2. 在触发位置返回 NaN,但后续位置仍照常算 (cum[t+1] - 1.0) / (1 - 1.0)。一旦触发,往后每一项都得 NaN。
  3. 触发条件写成 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 可见样例;服务端提交会运行可见样例和隐藏测试。

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

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

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。