从边际违约概率构造累计违约概率:期限结构合成
Cumulative Default Probability from Marginal Hazard Rates: Term-Structure Composition
开始编码实现 solution(marginal_pd: list[float], horizons: list[int]) -> list[float]。某信用风险口对一笔长久期债务人完成了边际违约概率期限结构的标定:marginal_pd[t] 表示在已存活过 0..t-1 期的条件下、第 t 期违约的概率。该口需要在多个汇报视野(1 年、3 年、5 年、到期)上得到累计违约概率,以便代入终生预期信用损失计算。对每个 1-indexed 视野 q(已经经过 q 期),返回截至第 q-1 期末的累计违约概率,即 1 - prod_{t=0..q-1}(1 - marginal_pd[t])。
**累计违约概率是 1 减去 (1 - marginal_pd[t]) 的乘积,不是 marginal_pd[t] 的求和**。第 t 期的边际违约概率是在已存活过 t-1 期的条件下定义的,因此累计存活概率是各期存活概率的乘积,而累计违约概率为 1 - 累计存活。把边际相加会重复计数:两个 0.5 的边际,求和说视野 2 处的累计违约概率为 1.0(认定一定违约),实际值为 1 - 0.5*0.5 = 0.75,仍保留两期都存活的 25% 概率。
例
solution([0.05, 0.10, 0.15], [1, 2, 3]) 返回 [0.05, 0.145, 0.27325]。逐期算存活前缀:surv[0] = 1.0,surv[1] = 1.0 * (1 - 0.05) = 0.95,surv[2] = 0.95 * (1 - 0.10) = 0.855,surv[3] = 0.855 * (1 - 0.15) = 0.72675。视野 q 处的累计违约概率是 1 - surv[q]:视野 1 是 1 - 0.95 = 0.05,视野 2 是 1 - 0.855 = 0.145,视野 3 是 1 - 0.72675 = 0.27325。注意视野 q=2 消耗了 marginal_pd[0] 与 marginal_pd[1](前两期);1-indexed 视野 q 始终查 surv[q]、消耗前 q 期。
求和与乘积的对照算例:marginal_pd = [0.5, 0.5]、horizons = [1, 2],答案为 [0.5, 0.75]。错用求和:0.5 + 0.5 = 1.0(认定必违约)。正确乘法:1 - (1 - 0.5)*(1 - 0.5) = 1 - 0.25 = 0.75,保留了两次伯努利试验都存活的 25% 概率。
需要明确的细节:horizons = [] 返回 []——函数不抛异常。horizons 可有重复,输出按输入顺序保留每个重复项:[1, 1, 2, 1] 返回四个 float、顺序与输入一致;不要去重。任意 marginal_pd[t] == 1.0 时,从第 t+1 期起存活变为精确 0.0、对所有 q >= t+1 累计违约概率精确为 1.0——乘法形式天然处理(若改写为 log(1 - mp) 走加法,会撞上 log(0) = -inf 而崩)。所有 marginal_pd = 0.0 累计违约概率处处为 0.0;所有 marginal_pd = 1.0 处处为 1.0。
期望算法 O(T + H):先 O(T) 一遍扫,预计算 surv[k] = prod_{t=0..k-1}(1 - marginal_pd[t]),k 从 0 到 T、surv[0] = 1.0;再 O(H) 一遍扫,对每个询问 q 把 1 - surv[q] 追加到输出。1-indexed 的视野 q 查 surv[q]、不是 surv[q-1]——这一处 off-by-one 是仅次于"求和与乘积混淆"的第二常见错。
实现细节由 stubs/stub.py 提供。
实践背景
某信贷台持有长久期贷款或债券,其边际违约率期限结构通常用信用违约互换(CDS)利差或评级迁移历史标定:marginal_pd[t] 是在已存活过第 t-1 期的条件下、第 t 期违约的概率。为了产出送入 CECL 或 IFRS 9 准备金的终生预期信用损失数字,交易台需要在多个汇报视野(1 年、3 年、5 年、到期)上的累计违约概率,再分别乘以对应的违约损失率与违约时敞口,得到终生预期损失向量。累计违约概率是"1 减去各期存活的乘积"——这是尊重边际违约率"条件存活"语义的唯一合法合成方式。把边际相加会对后期的概率质量重复计数(后期的边际是基于早期存活条件的、不是无条件概率),在长久期债务人上甚至会输出大于 1 的"概率",这种数字一旦落到监管报表里立刻是审计红线。solution(...) 的输出就是预期损失瀑布里要乘 LGD 与 EAD 的"终生 PD"那一列。
约束条件
- 1 <= T == len(marginal_pd) <= 60
- 0 <= H == len(horizons) <= 60
- 0.0 <= marginal_pd[t] <= 1.0(是概率而不是百分数)
- 1 <= horizons[h] <= T(1-indexed;有效区间为 [1, T])
- 输出为长度 H 的 list[float];每项落在 [0.0, 1.0];按 rel_tol=1e-9、abs_tol=1e-9 比对
样例
Case 1 · statement-example: 3-period term-structure cumulative-PD at horizons 1, 2, 3
输入: [[0.05,0.1,0.15],[1,2,3]]
期望: [0.050000000000000044,0.14500000000000002,0.27325]
surv = [1, 0.95, 0.95*0.9=0.855, 0.855*0.85=0.72675];累计违约概率 = 1 - surv[q],因此 [0.05, 0.145, 0.27325]。
Case 2 · visible: two 0.5 marginals - product not sum (cum-PD at horizon 2 is 0.75, not 1.0)
输入: [[0.5,0.5],[1,2]]
期望: [0.5,0.75]
若错用 sum: 0.5+0.5=1.0(认定一定违约)。正确做法:1 - 0.5*0.5 = 0.75(25% 概率两期都活着)。返回 [0.5, 0.75]。
Case 3 · visible: empty horizons returns empty list (not crash)
输入: [[0.05,0.1,0.15],[]]
期望: []
horizons 为空时返回 [],不要崩溃。
Case 4 · visible: duplicate horizons preserve input order (don't deduplicate)
输入: [[0.1,0.2],[1,1,2,1,2]]
期望: [0.09999999999999998,0.09999999999999998,0.2799999999999999,0.09999999999999998,0.2799999999999999]
surv = [1, 0.9, 0.72];horizons 重复但每次都要按输入顺序输出对应的 1-surv[q]。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: 3-period term-structure cumulative-PD at horizons 1, 2, 3
输入: [[0.05,0.1,0.15],[1,2,3]]
期望: [0.050000000000000044,0.14500000000000002,0.27325]
surv = [1, 0.95, 0.95*0.9=0.855, 0.855*0.85=0.72675];累计违约概率 = 1 - surv[q],因此 [0.05, 0.145, 0.27325]。
Case 2 · visible: two 0.5 marginals - product not sum (cum-PD at horizon 2 is 0.75, not 1.0)
输入: [[0.5,0.5],[1,2]]
期望: [0.5,0.75]
若错用 sum: 0.5+0.5=1.0(认定一定违约)。正确做法:1 - 0.5*0.5 = 0.75(25% 概率两期都活着)。返回 [0.5, 0.75]。
Case 3 · visible: empty horizons returns empty list (not crash)
输入: [[0.05,0.1,0.15],[]]
期望: []
horizons 为空时返回 [],不要崩溃。
Case 4 · visible: duplicate horizons preserve input order (don't deduplicate)
输入: [[0.1,0.2],[1,1,2,1,2]]
期望: [0.09999999999999998,0.09999999999999998,0.2799999999999999,0.09999999999999998,0.2799999999999999]
surv = [1, 0.9, 0.72];horizons 重复但每次都要按输入顺序输出对应的 1-surv[q]。