code wiki / (root) / sketch_zscore.nx

sketch_zscore.nx source

↩ module page · 128 lines · 4232 B

1// sketch_zscore.nx -- point z-score anomaly detector. 2// 3// Composes against sketch_stream_stats (running mean + stddev) to flag 4// individual samples deviating from the streaming baseline: 5// 6// z = (x - mean) / stddev 7// alarm if |z| > threshold (typical 2.0 ~ 95% conf, 3.0 ~ 99.7%) 8// 9// CAPABILITY: 10// - O(1) state (mean + variance from StreamStats) 11// - online: baseline updates with every sample 12// - Returns Z in PPM ([-1e10, +1e10] practical range) 13// - Sealed verdict: NORMAL / HIGH / LOW 14// 15// COMPLEMENTS THE ANOMALY FAMILY: 16// - sketch_cusum cumulative shift over a window 17// - sketch_mann_kendall monotonic trend test 18// - sketch_zscore (this) point outlier vs running baseline 19// 20// CONFIGURATION: 21// "warm-up" period: do not raise alarms until count >= min_count. 22// Default: min_count = 30 (statistician's rule-of-thumb for CLT). 23// 24// LOSSLESS-LANGUAGE DISCIPLINE: returns z in PPM with NX_ENV_ABS, 25// param_a = 1 (PPM quantization). Production tier; exact integer 26// arithmetic over the running statistics. 27 28import "syscalls.nx" 29import "sketch_stream_stats.nx" 30import "sketch_types.nx" 31 32const NX_ZS_VERDICT_NORMAL: i64 = 0 33const NX_ZS_VERDICT_HIGH: i64 = 1 34const NX_ZS_VERDICT_LOW: i64 = 2 35 36const NX_ZS_MIN_COUNT_DEFAULT: i64 = 30 37 38struct ZScore { 39 stats: *StreamStats, 40 threshold_ppm: i64, // |z| > threshold_ppm/1e6 triggers alarm 41 min_count: i64, 42 n_alarms: i64, 43} 44 45// === construction ================================================= 46// 47// threshold_ppm: e.g. 2_000_000 = z >= 2.0 48// min_count: warm-up samples before alarms fire (default 30 if 0). 49 50func nx_zs_alloc(threshold_ppm: i64, min_count: i64) -> *ZScore { 51 if threshold_ppm <= 0 { return 0 as *ZScore } 52 let stats: *StreamStats = nx_stats_alloc() 53 if stats == (0 as *StreamStats) { return 0 as *ZScore } 54 let raw: *u8 = sys_mmap(40) 55 let z: *ZScore = raw as *ZScore 56 z.stats = stats 57 z.threshold_ppm = threshold_ppm 58 if min_count <= 0 { 59 z.min_count = NX_ZS_MIN_COUNT_DEFAULT 60 } 61 if min_count > 0 { 62 z.min_count = min_count 63 } 64 z.n_alarms = 0 65 return z 66} 67 68// === observe ===================================================== 69// 70// Compute z BEFORE updating stats (so the score reflects deviation 71// from the prior baseline rather than including the sample itself). 72// Caller can choose to call nx_zs_score_then_update vs separate 73// observe/score patterns; this primitive does observe-after-score. 74 75func nx_zs_score_ppm(z: *ZScore, x: i64) -> i64 { 76 if nx_stats_count(z.stats) < z.min_count { return 0 } 77 let mean: i64 = nx_stats_mean(z.stats) 78 let var_val: i64 = nx_stats_variance(z.stats) 79 if var_val <= 0 { return 0 } 80 let stddev: i64 = nx_stats_isqrt(var_val) 81 if stddev == 0 { return 0 } 82 return ((x - mean) * 1000000) / stddev 83} 84 85func nx_zs_verdict(z: *ZScore, x: i64) -> i64 { 86 let zppm: i64 = nx_zs_score_ppm(z, x) 87 if zppm >= z.threshold_ppm { return NX_ZS_VERDICT_HIGH } 88 if zppm <= -z.threshold_ppm { return NX_ZS_VERDICT_LOW } 89 return NX_ZS_VERDICT_NORMAL 90} 91 92// Full pipeline: score, classify, then incorporate sample into baseline. 93// Returns the verdict. 94func nx_zs_step(z: *ZScore, x: i64) -> i64 { 95 let v: i64 = nx_zs_verdict(z, x) 96 if v != NX_ZS_VERDICT_NORMAL { z.n_alarms = z.n_alarms + 1 } 97 nx_stats_add(z.stats, x) 98 return v 99} 100 101// === introspection ================================================ 102 103func nx_zs_count(z: *ZScore) -> i64 { 104 return nx_stats_count(z.stats) 105} 106 107func nx_zs_mean(z: *ZScore) -> i64 { 108 return nx_stats_mean(z.stats) 109} 110 111func nx_zs_stddev(z: *ZScore) -> i64 { 112 return nx_stats_stddev(z.stats) 113} 114 115func nx_zs_n_alarms(z: *ZScore) -> i64 { 116 return z.n_alarms 117} 118 119func nx_zs_query(z: *ZScore, x: i64) -> *ApproxI64 { 120 let v: i64 = nx_zs_verdict(z, x) 121 return nx_approx_new(v, NX_ENV_ABS, 0, 1000000000, 122 NX_MATURITY_PRODUCTION, 123 NX_ADV_HONEST) 124} 125 126func nx_zs_memory_bytes(z: *ZScore) -> i64 { 127 return 40 + nx_stats_memory_bytes(z.stats) 128}