分段扁平 hazard 期限结构下多视野存活概率
Survival Probability at Multiple Horizons under a Piecewise-Flat Hazard Term Structure
开始编码实现 solution(hazards: list[float], period_lengths: list[float], query_horizons: list[float]) -> list[float]。某信用风险口对一个债务人持有一组分段扁平的 hazard 期限结构——hazards[t] 是第 t 个 tenor 桶上的恒定 hazard 速率(单位 1/年),period_lengths[t] 是该桶的长度(单位年)。各桶从左到右无缝拼接:第 t 个桶覆盖 [endpoint[t-1], endpoint[t]),其中 endpoint[t] = sum_{k<=t} period_lengths[k]。对每个查询视野 h(来自 query_horizons),返回存活概率 Q(h) = exp(-integral_0^h lambda(s) ds)——债务人在 h 年前未违约的概率。
在分段扁平假设下,积分可拆为若干完整桶之和,加上 h 所在桶的部分期贡献。若 endpoint[i-1] < h <= endpoint[i],则
cumulative_hazard_to_h = sum_{k<i} (hazards[k] * period_lengths[k])
+ hazards[i] * (h - endpoint[i-1])
Q(h) = exp(-cumulative_hazard_to_h)第一个求和是上一个桶端点处的累计 hazard;第二项是 h 所在桶的部分期贡献。
例
solution([0.01, 0.02, 0.04], [1.0, 1.0, 1.0], [0.5, 1.0, 2.0, 3.0]) 返回 [0.9950124791926823, 0.9900498337491681, 0.9704455335485082, 0.9323938199059483]。三个桶分别为 0..1(hazard 0.01)、1..2(hazard 0.02)、2..3(hazard 0.04)。h = 0.5 时累计 hazard 为 0.01 * 0.5 = 0.005(桶 0 的部分期),Q = exp(-0.005) ≈ 0.99501。h = 1 时刚走完桶 0,cum_haz = 0.01,Q = exp(-0.01) ≈ 0.99005。h = 2 时 cum_haz = 0.01 + 0.02 = 0.03。h = 3 时 cum_haz = 0.01 + 0.02 + 0.04 = 0.07,Q = exp(-0.07) ≈ 0.93239。
易错点
第一处是部分期贡献。查询 h 落在桶 i 内部(严格在 endpoint[i-1] 与 endpoint[i] 之间)时,桶 i 的贡献为 hazards[i] * (h - endpoint[i-1])——只有 h 左侧那部分。作者若误用整桶 period_lengths[i],对任何不在桶端点的 h 都会高估 hazard。具体校验:hazards = [0.01, 0.50]、period_lengths = [1.0, 1.0]、h = 1.7,正确 cum_haz = 0.01 + 0.50 * 0.7 = 0.36(Q ≈ 0.6977);错误的整桶 cum_haz = 0.01 + 0.50 * 1.0 = 0.51(Q ≈ 0.6005)。单测大约差 0.1,桶 hazard 越大、部分期占比越小,错得越厉害。
第二处是保序。输出必须与 query_horizons 顺序完全一致——重复也按输入顺序输出。作者若内部对 query 排序(为单调指针扫描的便利)后忘了把排列还原回去,会得到值正确但槽位错误的输出。参考解直接按 query_horizons 顺序遍历、对每个 h 独立计算 Q(h);T <= 30、K <= 30 之下无需任何排序优化。query_horizons 的重复按输入顺序逐一输出;不要去重。
第三处是 h = 0 边界。Q(0) = 1.0 恒成立——零时间流逝即必然存活,与 hazard 期限结构无关。在做任何部分期算术 之前 就对 h <= 0 短路。作者若依赖线性扫描跑到 i = 0、再算 hazards[0] * (0 - 0) = 0 会侥幸算对,但 T = 0 时(无桶可索引)hazards[0] 立即抛 IndexError。
第四处是恰好落在桶端点。h = endpoint[i] 时,部分期项 hazards[i] * (h - endpoint[i-1]) 正好等于整桶 hazards[i] * period_lengths[i]——累计 hazard 恰是 cum_haz[i](桶 i 端点处的累计 hazard)。作者若特判"仅当 h 严格小于端点时才加部分期"、在端点处跳过这一项,会把最后一个桶整体漏掉。干净的规则:只要 endpoints[i-1] < h <= endpoints[i],就有 cum_haz_to_h = cum_haz[i-1] + hazards[i] * (h - endpoints[i-1]),等号情况落在右边。
第五处是存活概率形式。Q(h) = exp(-cum_haz_to_h),不是 1 - cum_haz_to_h、也 不是 prod_t (1 - hazards[t] * period_lengths[t])。当 lambda * dt 较大时离散线性近似会发散:单桶 hazards = 3.0、period_lengths = 1.0、h = 1.0,线性 1 - 3.0 = -2.0(非法——负的存活概率),正确 exp(-3.0) ≈ 0.0498。一律用 math.exp 与指数形式。
第六处是 T = 0 哨兵。无桶定义时,从 0 到任意正 h 的积分未定义(无 hazard 可积)。T = 0 时:h = 0 返回 1.0(空积分为零),h > 0 返回 NaN。用 float('nan')。作者若抛异常或返回 0.0 都违反契约;评测器开启 nan_equals_nan = true、NaN 与期望输出里的 NaN 精确匹配。
哨兵与边界
len(query_horizons) == 0 时立即返回 []。len(hazards) == 0 时:任何 query_horizons[k] > 0 的项为 NaN;任何 query_horizons[k] == 0 的项为 1.0。否则算法为:先 O(T) 一遍预计算累计端点与累计 hazard 前缀;再对每个 query 用线性扫描或二分定位 h 所在桶,套累计-hazard-加部分期公式即可。T <= 30、K <= 30 之下,每查询线性扫描即可。
实现细节由 stubs/stub.py 提供。
实践背景
信用风险建模中违约强度的期限结构常被参数化为分段扁平——每个 tenor 桶(典型的 CDS 标准网格 1y、3y、5y、7y、10y)一个常数 hazard lambda_t。任意视野 h 处的累计存活概率 Q(h) 由 lambda(s) 的分段积分给出:完全落在 h 左侧的桶贡献整段,h 所在桶贡献部分期。该函数回答"债务人活到 h 年的概率是多少?"——是终生预期损失合成、CDS 定价、压力情景下资本预测的输入列。把部分期项写错(h 落在桶中间却用整桶长度)会高估 hazard、低估存活;同一个错误公式落到每个 obligor 上时,整个组合的偏差会同向叠加。
姊妹题
coding-cumulative-default-prob-from-marginal-hazard-rates——离散时间 边际 PD 期限结构;本题用 连续时间 分段扁平 hazard。coding-cds-implied-flat-hazard-cumulative-pd——单 tenor par-CDS 利差到累计 PD。coding-cds-curve-flat-hazard-survival-curve——CDS 曲线下逐 tenor 独立 扁平-hazard 存活(不做链式合成)。coding-marginal-pd-from-cumulative-term-structure——反向(累计 → 边际)。
本题是分段连续 hazard 累计存活的姊妹题:给定分段扁平 hazard 期限结构与一组任意查询视野,返回每个视野上与期限结构一致的存活概率。
约束条件
- 0 <= len(hazards) == len(period_lengths) <= 30
- 0 <= len(query_horizons) <= 30
- 0.0 <= hazards[t] <= 5.0(年化 hazard;5.0 ≈ 1y 内 99% 违约)
- 0.001 <= period_lengths[t] <= 30.0(正数)
- 0.0 <= query_horizons[k] <= sum(period_lengths)(在期限结构覆盖范围内)
- 输出为长度 K 的 list[float];每项落在 [0, 1] 或为 NaN(仅当 T = 0 且视野为正时);按 rel_tol=1e-9、abs_tol=1e-9 比对;nan_equals_nan = true
样例
Case 1 · statement-example: 3-bucket flat hazard at front-end, mid, late horizons
输入: [[0.01,0.02,0.04],[1,1,1],[0.5,1,2,3]]
期望: [0.9950124791926823,0.9900498337491681,0.9704455335485082,0.9323938199059483]
cum_haz: 0.5y -> 0.005 (partial); 1y -> 0.01; 2y -> 0.03; 3y -> 0.07. Q = exp(-cum_haz).
Case 2 · visible: partial-period contribution mid-bucket (h=1.5 sits inside bucket 1)
输入: [[0.05,0.1],[1,1],[0.5,1,1.5,2]]
期望: [0.9753099120283326,0.951229424500714,0.9048374180359595,0.8607079764250578]
0.5y: 0.025 (partial of bucket 0); 1y: 0.05; 1.5y: 0.05+0.10*0.5=0.10; 2y: 0.15.
Case 3 · visible: empty query_horizons returns empty list
输入: [[0.05,0.1,0.15],[1,1,1],[]]
期望: []
K=0 -> [] (no horizons asked).
Case 4 · visible: h=0 returns 1.0 (no time elapsed = certain survival)
输入: [[0.5,1,2],[1,1,1],[0,0,0]]
期望: [1,1,1]
h=0 short-circuits to Q=1; duplicates preserve order.
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: 3-bucket flat hazard at front-end, mid, late horizons
输入: [[0.01,0.02,0.04],[1,1,1],[0.5,1,2,3]]
期望: [0.9950124791926823,0.9900498337491681,0.9704455335485082,0.9323938199059483]
cum_haz: 0.5y -> 0.005 (partial); 1y -> 0.01; 2y -> 0.03; 3y -> 0.07. Q = exp(-cum_haz).
Case 2 · visible: partial-period contribution mid-bucket (h=1.5 sits inside bucket 1)
输入: [[0.05,0.1],[1,1],[0.5,1,1.5,2]]
期望: [0.9753099120283326,0.951229424500714,0.9048374180359595,0.8607079764250578]
0.5y: 0.025 (partial of bucket 0); 1y: 0.05; 1.5y: 0.05+0.10*0.5=0.10; 2y: 0.15.
Case 3 · visible: empty query_horizons returns empty list
输入: [[0.05,0.1,0.15],[1,1,1],[]]
期望: []
K=0 -> [] (no horizons asked).
Case 4 · visible: h=0 returns 1.0 (no time elapsed = certain survival)
输入: [[0.5,1,2],[1,1,1],[0,0,0]]
期望: [1,1,1]
h=0 short-circuits to Q=1; duplicates preserve order.