在线 Z 分数离群标记:用 Welford 累计历史检测 tick 流异常
Online Z-Score Outlier Flagger: Welford-Cumulative Anomaly Detection on a Tick Stream
开始编码实现 solution(x: list[float], warmup: int, threshold: float) -> int。给定一段长度为 N 的 tick 流 x、一个非负整数 warmup 与一个有限阈值 threshold,对每个 i ∈ [0, N) 用 prior 观测 x[0..i-1] 计算运行均值与样本标准差(分母 count - 1),再算 z 分数 z = (x[i] - mean_prior) / std_prior,并返回 |z| > threshold 的 tick 总数。仅当 i >= max(warmup, 2) 时才计算并测试该 tick;之前的 tick 仅作为 prior 历史进入 Welford 状态,不参与标记。若 std_prior == 0(prior 样本全相等),按 sentinel 规则把 z 视为 0(不标记),而不是除零。
关键工程要点是先打分、后吸收:到 i 为止只允许使用 x[0..i-1] 的统计量,把 x[i] 折入 mean 与 M2 必须放在打分之后,否则 x[i] 会自污染分母。Welford 递推是单趟 O(N)、额外空间 O(1),且对大均值-小离散度体制(如挂在 1e6 量级的 mid_px)保持数值稳定,朴素的 sum(x^2)/n − (sum(x)/n)^2 累加器在该体制下会输出零或负方差。
例:solution([1.0, 1.1, 0.9, 1.05, 0.95, 1.02, 5.0, 0.98, 1.0], 3, 2.5) 返回 1。前 3 个 tick 是暖启动;从索引 3 起开始打分。索引 3..5 相对各自的 prior 历史 |z| 都远低于 2.5;索引 6 的值 5.0 相对前 6 个样本(均值约 1.0、std 极小)的 |z| 远超 2.5,被标记一次。索引 7、8 在 5.0 已经被吸收进 mean 与 M2 之后 std 显著放大,不再触发阈值。
需明确的若干细节:solution([], 0, 3.0) 返回 0;warmup 等于或大于 N 时没有任何 tick 被检查,返回 0;threshold 取严格不等号,|z| == threshold 不算一次标记;prior 样本数不足 2(i < 2)时也不打分;threshold 也允许是 0 或负数。
实现细节由 stubs/stub.py 提供。
实践背景
实时风控与 microstructure 监控里经常需要在一条只读 tick 流(mid 的 size delta、spread update、trade size、连续报价 quote-life)上挂一个最轻量的离群检测器:不开历史窗口缓冲、不重扫旧 tick、每 tick 状态量恒定。Welford 累计历史就是那种最便宜的实现——O(1) 状态、O(1) 单 tick 摊销、随时可被 cumulative-history Sharpe / VWAP 等其它在线统计量复用。把"prior-only"语义写死在合约里是有意的:对当前 tick 用包含自己的统计量打分会让 z 系统性地偏小,明显的 spike 会因为自污染而漏报,这是新人写在线 z 分数最容易踩的坑。把暖启动单独做出来则解决另一个问题——开市前几秒、新合约挂出的最初几 tick、流过 reconnect 之后的 stale 缓冲尾段都不应该被纳入 prior 估计;warmup 把那段路径上的 tick 仅当作"history 累积"而不去打分,也不在尚未稳定的方差估计上误报。把 std=0 的 sentinel 写明白则避免一种最坑的故障模式:在一段恒定区间结束时来一个微小的真扰动,朴素实现会除零得到 inf 然后报警,把所有 spread-quote 一致的盘前路径全部刷成红屏。
约束条件
- 0 <= len(x) <= 2000,其中 x 为 solution(x, warmup, threshold) 的输入流
- warmup 为非负整数,允许等于或超过 len(x);不会出现负数
- threshold 为有限 float(可正、可零、可负)
- x[i] 为有限 float,绝对值不超过 1e9(adversarial 用例会触及大均值-小方差体制)
- 返回值是非负整数——|z| 严格大于 threshold 的 tick 计数;空输入返回 0
样例
Case 1 · statement-example: one large outlier at index 6
输入: [[1,1.1,0.9,1.05,0.95,1.02,5,0.98,1],3,2.5]
期望: 1
前 3 个 tick 是暖启动,从 i=3 起开始检查。索引 3..5 对历史均值相对接近;索引 6 的值 5.0 相对前 6 个样本(均值约 1.0、标准差较小)|z| 远超 2.5,被标记。索引 7,8 在已经吸收了那个 5.0 的均值/方差下不会触发阈值。
Case 2 · visible: warmup >= len returns 0
输入: [[0.5,-0.3,0.7,-0.6,0.2],10,2]
期望: 0
warmup=10 大于序列长度,无任何 tick 进入检查区间,返回 0。
Case 3 · visible: zero prior std uses sentinel z=0
输入: [[2,2,2,2,7],2,1]
期望: 0
前 4 个 prior 样本全为 2.0,样本标准差为 0;按 sentinel 规则索引 4 的 z 视为 0,不被标记。
Case 4 · visible: empty stream returns 0
输入: [[],0,3]
期望: 0
空输入直接返回 0。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: one large outlier at index 6
输入: [[1,1.1,0.9,1.05,0.95,1.02,5,0.98,1],3,2.5]
期望: 1
前 3 个 tick 是暖启动,从 i=3 起开始检查。索引 3..5 对历史均值相对接近;索引 6 的值 5.0 相对前 6 个样本(均值约 1.0、标准差较小)|z| 远超 2.5,被标记。索引 7,8 在已经吸收了那个 5.0 的均值/方差下不会触发阈值。
Case 2 · visible: warmup >= len returns 0
输入: [[0.5,-0.3,0.7,-0.6,0.2],10,2]
期望: 0
warmup=10 大于序列长度,无任何 tick 进入检查区间,返回 0。
Case 3 · visible: zero prior std uses sentinel z=0
输入: [[2,2,2,2,7],2,1]
期望: 0
前 4 个 prior 样本全为 2.0,样本标准差为 0;按 sentinel 规则索引 4 的 z 视为 0,不被标记。
Case 4 · visible: empty stream returns 0
输入: [[],0,3]
期望: 0
空输入直接返回 0。