成交 PnL 的线性插值百分位
Linear-Interpolation Percentile of Trade PnL
开始编码TCA(成交质量分析)报表常常要回答这样一个问题:在最近一批成交里,第 p 百分位的单笔 PnL 是多少?例如「95 百分位的尾部亏损」用来度量极端情形下的滑点暴露。请实现 solution(pnls: list[float], p: float) -> float:给定一组单笔成交 PnL(货币单位与输入一致)和一个 [0, 100] 区间内的百分位 p,返回线性插值的第 p 百分位。
算法分三步。(1) 把 pnls 升序排序,得到顺序统计量序列 sorted[0..n-1]。(2) 计算分数排名 r = (n - 1) * (p / 100.0),注意分母用 n - 1 而不是 n —— 这样 p=0 落在下标 0、p=100 落在下标 n-1,正好覆盖整段排序数组。(3) 设 lo = floor(r)、hi = ceil(r)、frac = r - lo,结果 = sorted[lo] * (1 - frac) + sorted[hi] * frac。当 r 是整数(例如 p=0、p=100,或 n=11、p=70 时 r=7)时,lo == hi,公式自动退化为直接读取该下标,不需要特判。
举一个具体例子:solution([3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0, 5.0, 3.0, 5.0], 70.0) 升序后为 [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9],n=11,r=(11-1)*0.7=7.0 是整数;lo = hi = 7,frac=0;结果直接读 sorted[7] = 5.0。再换 p=33.3:r=(11-1)*0.333=3.33,lo=3, hi=4, frac=0.33,结果 = 3 * 0.67 + 3 * 0.33 = 3.0。两次中间样本恰好相等,所以插值结果也是 3。
这道题有两个常见陷阱。第一,分母:用 n * p / 100 而不是 (n - 1) * p / 100 会让 p=100 时的 r 等于 n,下标越界;这是 numpy 把这一约定单独命名为 "linear" 的根源——区别于 "lower"、"higher"、"nearest" 等其他方法。第二,端点处理:p=0 与 p=100 都是合法输入(不是异常路径),分别返回 sorted[0] 与 sorted[n-1];nearest-rank 实现常常把 p=100 翻译成下标 n(越界)或 p=0 翻译成下标 -1,而线性插值实现只要按公式走就自然落在两端。再加一条:千万不要在排序之前用 nearest-rank 取整再插值——必须先排序再按分数排名取邻居。
约束条件
- 1 ≤ n ≤ 200000,n = len(pnls)。n ≥ 1 是调用方先验保证的前提条件,不要求实现处理空输入。
- 0 ≤ p ≤ 100,作为调用方先验保证的前提条件。端点 p=0 与 p=100 必须按定义返回最小值与最大值。
- PnL 取值在 ±1e9 内的有限浮点数,可以含负值与重复值,不保证已排序。
- 百分位约定:线性插值,公式为 `r = (n - 1) * p / 100`,匹配 `numpy.percentile(..., method="linear")` 的语义。
- 浮点比较容差:rel_tol=1e-9,abs_tol=1e-12(输出是单个 float,所以容差非常紧)。
样例
Case 1 · typical 10-trade pnl set, p=95 tail
输入: [[-120.5,-50,-10,5.5,12,25,60,80,100,250],95]
期望: 182.49999999999986
10 笔成交 PnL 升序为 [-120.5, -50, -10, 5.5, 12, 25, 60, 80, 100, 250]。p=95 时 r=(10-1)*0.95=8.55,落在 sorted[8]=100 与 sorted[9]=250 之间,权重 frac=0.55;结果 = 100*(1-0.55) + 250*0.55 = 45 + 137.5 = 182.5。常见 bug:用 n=10 而不是 n-1=9 算位置,会得到 r=9.5,索引出界。
Case 2 · median (p=50) of 4 pnls — averages middle two
输入: [[-3,-1,2,4],50]
期望: 0.5
n=4 的 p=50 中位数:r=(4-1)*0.5=1.5,落在 sorted[1]=-1 与 sorted[2]=2 之间;frac=0.5,结果 = -1*0.5 + 2*0.5 = 0.5。即偶数样本的中位数等于中间两个的平均。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · typical 10-trade pnl set, p=95 tail
输入: [[-120.5,-50,-10,5.5,12,25,60,80,100,250],95]
期望: 182.49999999999986
10 笔成交 PnL 升序为 [-120.5, -50, -10, 5.5, 12, 25, 60, 80, 100, 250]。p=95 时 r=(10-1)*0.95=8.55,落在 sorted[8]=100 与 sorted[9]=250 之间,权重 frac=0.55;结果 = 100*(1-0.55) + 250*0.55 = 45 + 137.5 = 182.5。常见 bug:用 n=10 而不是 n-1=9 算位置,会得到 r=9.5,索引出界。
Case 2 · median (p=50) of 4 pnls — averages middle two
输入: [[-3,-1,2,4],50]
期望: 0.5
n=4 的 p=50 中位数:r=(4-1)*0.5=1.5,落在 sorted[1]=-1 与 sorted[2]=2 之间;frac=0.5,结果 = -1*0.5 + 2*0.5 = 0.5。即偶数样本的中位数等于中间两个的平均。