code wiki / (root) / nx_analyst_infer.nx

nx_analyst_infer.nx source

↩ module page · 486 lines · 23668 B

1// nx_analyst_infer.nx -- STATISTICAL INFERENCE for the general Nishi analyst: the "is it actually real?" 2// layer that every SOTA analyst tool (pandas/scipy, R, DuckDB+stats) reports and ours did not. 3// 4// THE GAP THIS CLOSES (measured 2026-07-23, ws=analyst-sota): nx_analyst_multi computes Pearson r and 5// nx_analyst_report NAMES A KEY DRIVER from it -- with NO significance guard whatsoever. The driver was a 6// bare argmax over |r|, so the report named one at ANY sample size. Its capstone gate happens to run at 7// n=300 where its r=-172 IS real, so no live finding was false -- but nothing in the code said so, and the 8// same call at n=8 returns the same confident sentence about pure noise (this organ's T2/T4 teeth show the 9// flip). Naming a driver that cannot be distinguished from noise is the liar-kill law applied to statistics. 10// 11// The ecosystem ALREADY had sovereign inference (nx_survey_stats: chi-square vs a data-driven critical 12// table, fail-closed past the table, 95% proportion margin) but it was locked inside the SURVEY vertical 13// and consumed se_ballots. This organ is the general, dataset-agnostic extraction (rule 15 DRY): it takes 14// a bare r and n, so ANY caller -- dataframe, store, timeseries, survey -- can ask "is it real?". 15// 16// METHOD: Student-t test on a correlation coefficient. t = r*sqrt(n-2)/sqrt(1-r^2), df = n-2, compared to 17// a two-tailed alpha=.05 critical value. Both sides are SQUARED so the whole test is INTEGER-EXACT -- no 18// sqrt, no float, bit-reproducible (the ecosystem's integer-exact-analytics EXCEED holds through inference): 19// t^2 > tc^2 <=> rp^2 * (n-2) > ceil( tcp^2 * (SCALE_SQ - rp^2) / SCALE_SQ ) 20// with rp = r*1000 and tcp = t_crit*1000. The RHS division rounds UP so truncation can only make the test 21// STRICTER -- an integer-rounding artifact can never manufacture a false "significant". 22// 23// FAIL-CLOSED BY CONSTRUCTION (mirrors nx_survey_stats' own adversary guard): 24// - n < 3 -> df < 1 -> CANNOT TEST (-1), never silently "not significant" 25// - df not tabulated -> use the largest tabulated df <= query. t_crit DECREASES in df, so a lower-df row 26// is a LARGER threshold = conservative. No usable row at all -> tcrit 0 -> NOT significant. 27// license_tier: ORIGINAL No hardware writes (Rule 26). 28import "nx_syscalls.nx" 29import "_hdl_build/nx_analyst_multi.nx" 30 31const AI_SCALE: i64 = 1000 // permille fixed point: r and t are both carried x1000 32const AI_SCALE_SQ: i64 = 1000000 // AI_SCALE * AI_SCALE 33const AI_TBL_MAX: i64 = 64 // max critical-table rows held 34const AI_CONF_CAP: i64 = 8192 // critical-table conf read window 35const AI_MINN_CAP: i64 = 1000000 // give up searching for a min-n beyond this many pairs 36const AI_MIN_PAIRS: i64 = 3 // df = n-2 >= 1 requires n >= 3 37const AI_TAB: i64 = 9 38const AI_HASH: i64 = 35 39const AI_NL: i64 = 10 40const AI_CR: i64 = 13 41const AI_ZERO: i64 = 48 42const AI_TBL_ALL: i64 = 512 // rows held when loading EVERY alpha (audit / Bonferroni lookup) 43const AI_ALPHA_05: i64 = 50 // permille; the classic .05 two-tailed level, and a legacy line's default 44 45// STOPS at the first non-digit rather than skipping it. That distinction is not cosmetic: the skipping 46// version read a 3-field line "50<TAB>1<TAB>12706" through a 2-field parser as 1 and 12706 CONCATENATED 47// into 112706 -- a plausible-looking threshold out of a format mismatch. A parser that silently welds 48// fields together turns a contract change into wrong numbers instead of a visible failure. 49// (Found 2026-07-23: a not-yet-rebuilt gate reading the new 3-column conf; debt filed.) 50func ai_atoi_rng(buf: *u8, a: i64, b: i64) -> i64 { 51 var v: i64 = 0 52 var i: i64 = a 53 var go: i64 = 1 54 while i < b { 55 if go == 1 { 56 let c: i64 = buf[i] as i64 57 if c < AI_ZERO { go = 0 } else { if c > (AI_ZERO + 9) { go = 0 } else { v = v * 10 + (c - AI_ZERO) } } 58 } 59 i = i + 1 60 } 61 return v 62} 63 64// Load the alpha=.05 two-tailed Student-t critical table (df <TAB> t*1000) from 65// knowledge/registry/analyst_tcrit.conf into dfs[]/tcs[]. Returns row count; 0 = table unavailable, 66// which the callers treat as "cannot certify significance" (fail-closed, never a false positive). 67// rule 11/17: the thresholds are DATA, not literals buried in this code. 68// General conf parser. want_alpha > 0 keeps ONLY rows at that alpha; want_alpha == 0 keeps every row and 69// also fills alphas (pass 0 as *i64 when you do not need them). A legacy 2-field line (df <TAB> tcrit) is 70// read as alpha = AI_ALPHA_05, so an older copy of the conf still loads (rule 19). 71func ai_tcrit_parse(alphas: *i64, dfs: *i64, tcs: *i64, maxn: i64, want_alpha: i64) -> i64 { 72 var cnt: i64 = 0 73 let fd: i64 = sys_openat_rd("knowledge/registry/analyst_tcrit.conf" as *u8) 74 if fd < 0 { return 0 } 75 let cb: *u8 = sys_mmap(AI_CONF_CAP) 76 var n: i64 = 0 77 var r: i64 = 1 78 while r > 0 { if n >= AI_CONF_CAP { r = 0 } else { r = sys_read(fd, (((cb as i64) + n) as *u8), AI_CONF_CAP - n); if r > 0 { n = n + r } } } 79 sys_close(fd) 80 var ls: i64 = 0 81 while ls < n { 82 var le: i64 = ls 83 var e: i64 = 0 84 while e == 0 { if le >= n { e = 1 } else { if cb[le] == (AI_NL as u8) { e = 1 } else { le = le + 1 } } } 85 var te: i64 = le 86 if te > ls { if cb[te-1] == (AI_CR as u8) { te = te - 1 } } 87 if te > ls { if cb[ls] != (AI_HASH as u8) { 88 var t1: i64 = ls 89 var f1: i64 = 0 90 while f1 == 0 { if t1 >= te { f1 = 1 } else { if cb[t1] == (AI_TAB as u8) { f1 = 1 } else { t1 = t1 + 1 } } } 91 var t2: i64 = t1 + 1 92 var f2: i64 = 0 93 while f2 == 0 { if t2 >= te { f2 = 1 } else { if cb[t2] == (AI_TAB as u8) { f2 = 1 } else { t2 = t2 + 1 } } } 94 if t1 < te { 95 var al: i64 = AI_ALPHA_05 96 var dv: i64 = 0 97 var tv: i64 = 0 98 if t2 < te { 99 al = ai_atoi_rng(cb, ls, t1) 100 dv = ai_atoi_rng(cb, t1 + 1, t2) 101 tv = ai_atoi_rng(cb, t2 + 1, te) 102 } else { 103 dv = ai_atoi_rng(cb, ls, t1) 104 tv = ai_atoi_rng(cb, t1 + 1, te) 105 } 106 var keep: i64 = 0 107 if al > 0 { if dv > 0 { if tv > 0 { keep = 1 } } } 108 if want_alpha > 0 { if al != want_alpha { keep = 0 } } 109 if keep == 1 { if cnt < maxn { 110 if (alphas as i64) != 0 { alphas[cnt] = al } 111 dfs[cnt] = dv 112 tcs[cnt] = tv 113 cnt = cnt + 1 114 } } 115 } 116 } } 117 ls = le + 1 118 } 119 return cnt 120} 121func ai_tcrit_load(dfs: *i64, tcs: *i64, maxn: i64) -> i64 { return ai_tcrit_parse(0 as *i64, dfs, tcs, maxn, AI_ALPHA_05) } 122 123// STRUCTURAL AUDIT of the whole critical table. Any correct Student-t table satisfies two invariants: 124// t falls as df rises (at fixed alpha), and t rises as alpha shrinks (at fixed df). Returns the violation 125// count -- 0 = structurally sound. ~240 thresholds are hand-entered here; without this a single transposed 126// digit is not a visible error, it is a silently wrong verdict for one (alpha, df) cell forever. 127func ai_tcrit_audit() -> i64 { 128 let al: *i64 = sys_mmap(8 * AI_TBL_ALL) as *i64 129 let df: *i64 = sys_mmap(8 * AI_TBL_ALL) as *i64 130 let tc: *i64 = sys_mmap(8 * AI_TBL_ALL) as *i64 131 let n: i64 = ai_tcrit_parse(al, df, tc, AI_TBL_ALL, 0) 132 var bad: i64 = 0 133 var i: i64 = 0 134 while i < n { 135 var j: i64 = 0 136 while j < n { 137 if al[i] == al[j] { if df[i] < df[j] { if tc[i] < tc[j] { bad = bad + 1 } } } 138 if df[i] == df[j] { if al[i] > al[j] { if tc[i] > tc[j] { bad = bad + 1 } } } 139 j = j + 1 140 } 141 i = i + 1 142 } 143 return bad 144} 145 146// Bonferroni: for m comparisons the per-test level is alpha/m. Returns the LARGEST tabulated alpha that is 147// <= AI_ALPHA_05/m, or 0 when the required level is stricter than anything tabulated -- callers must then 148// REFUSE rather than quietly testing at a laxer level than the correction demands. Integer division 149// truncates downward, which only ever makes the requirement stricter. 150func ai_alpha_for_m(m: i64) -> i64 { 151 if m < 1 { return 0 } 152 let need: i64 = AI_ALPHA_05 / m 153 if need < 1 { return 0 } 154 let al: *i64 = sys_mmap(8 * AI_TBL_ALL) as *i64 155 let df: *i64 = sys_mmap(8 * AI_TBL_ALL) as *i64 156 let tc: *i64 = sys_mmap(8 * AI_TBL_ALL) as *i64 157 let n: i64 = ai_tcrit_parse(al, df, tc, AI_TBL_ALL, 0) 158 var best: i64 = 0 159 var i: i64 = 0 160 while i < n { if al[i] <= need { if al[i] > best { best = al[i] } } i = i + 1 } 161 return best 162} 163 164// Pure lookup: critical t (permille) for df, choosing the row with the LARGEST tabulated df <= query. 165// t_crit is monotonically DECREASING in df, so falling back to a lower-df row yields a LARGER threshold 166// = strictly conservative. Returns 0 when no row qualifies (df below the whole table / empty table). 167func ai_tcrit_from(dfs: *i64, tcs: *i64, cnt: i64, df: i64) -> i64 { 168 var best: i64 = 0 169 var bestdf: i64 = 0 170 var i: i64 = 0 171 while i < cnt { 172 if dfs[i] <= df { if dfs[i] >= bestdf { bestdf = dfs[i]; best = tcs[i] } } 173 i = i + 1 174 } 175 return best 176} 177 178// Pure test. 1 = significant at alpha .05 two-tailed, 0 = not, -1 = cannot test (n < 3). 179func ai_r_sig_from(dfs: *i64, tcs: *i64, cnt: i64, rp: i64, n: i64) -> i64 { 180 if n < AI_MIN_PAIRS { return 0 - 1 } 181 var a: i64 = rp 182 if a < 0 { a = 0 - a } 183 // An |r| above 1000 is NOT a correlation -- it is a sentinel (am_pearson_milli returns 184 // AM_R_DEGENERATE = -2000 for a constant/degenerate column). Clamping it to 1000 turned that 185 // sentinel into PERFECT correlation and therefore always-significant, which is precisely backwards. 186 // Out of range => CANNOT TEST. (Found 2026-07-23 by reading the insight miner's own output: five 187 // "undefined (constant column)" pairs were ranking above every real finding.) 188 if a > AI_SCALE { return 0 - 1 } 189 let df: i64 = n - 2 190 let tcp: i64 = ai_tcrit_from(dfs, tcs, cnt, df) 191 if tcp <= 0 { return 0 } 192 let lhs: i64 = a * a * df 193 // ceiling division keeps truncation on the STRICT side: rounding can never invent significance 194 let rhs: i64 = (tcp * tcp * (AI_SCALE_SQ - a * a) + AI_SCALE_SQ - 1) / AI_SCALE_SQ 195 if lhs > rhs { return 1 } 196 return 0 197} 198 199// Convenience wrappers that own the table load (one conf read per call; the _from variants exist so a 200// scan like ai_min_n does not re-read the conf per candidate n). 201func ai_r_significant(rp: i64, n: i64) -> i64 { 202 let dfs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 203 let tcs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 204 let cnt: i64 = ai_tcrit_load(dfs, tcs, AI_TBL_MAX) 205 return ai_r_sig_from(dfs, tcs, cnt, rp, n) 206} 207func ai_tcrit(df: i64) -> i64 { 208 let dfs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 209 let tcs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 210 let cnt: i64 = ai_tcrit_load(dfs, tcs, AI_TBL_MAX) 211 return ai_tcrit_from(dfs, tcs, cnt, df) 212} 213func ai_tcrit_alpha(alpha_permil: i64, df: i64) -> i64 { 214 let dfs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 215 let tcs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 216 let cnt: i64 = ai_tcrit_parse(0 as *i64, dfs, tcs, AI_TBL_MAX, alpha_permil) 217 return ai_tcrit_from(dfs, tcs, cnt, df) 218} 219// significance at an arbitrary tabulated alpha (permille). Untabulated alpha -> 0 rows -> NOT significant. 220func ai_r_sig_alpha(rp: i64, n: i64, alpha_permil: i64) -> i64 { 221 if alpha_permil <= 0 { return 0 } 222 let dfs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 223 let tcs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 224 let cnt: i64 = ai_tcrit_parse(0 as *i64, dfs, tcs, AI_TBL_MAX, alpha_permil) 225 return ai_r_sig_from(dfs, tcs, cnt, rp, n) 226} 227// Significance of ONE correlation among m tested against the same target, at family-wise .05. 228// WHY THIS MATTERS: testing m columns at .05 each gives roughly an m*5% chance that at least one looks 229// real by luck -- with 20 candidate columns about one WILL. Picking the LARGEST of m correlations and 230// calling it "the driver" is exactly that selection, so the driver decision must carry the correction. 231// -1 cannot test; 0 not significant (or the required level is off the table); 1 significant. 232func ai_r_sig_bonferroni(rp: i64, n: i64, m: i64) -> i64 { 233 if n < AI_MIN_PAIRS { return 0 - 1 } 234 let a: i64 = ai_alpha_for_m(m) 235 if a <= 0 { return 0 } 236 return ai_r_sig_alpha(rp, n, a) 237} 238 239// DOUBLED average ranks (x2 keeps tie mid-ranks integral: a tie group of size k starting at position p has 240// average rank p+(k-1)/2, doubled = 2p+k-1). Pearson is scale-invariant, so doubling does not move r. 241// O(n^2) on purpose: analyst-scale n, and it needs no sort buffer and no stable-sort guarantee. 242func ai_rank2_into(x: *i64, n: i64, out: *i64) -> i64 { 243 var i: i64 = 0 244 while i < n { 245 var less: i64 = 0 246 var eq: i64 = 0 247 var j: i64 = 0 248 while j < n { 249 if x[j] < x[i] { less = less + 1 } 250 if x[j] == x[i] { eq = eq + 1 } 251 j = j + 1 252 } 253 out[i] = 2 + 2 * less + (eq - 1) 254 i = i + 1 255 } 256 return n 257} 258// ---- CONFIDENCE INTERVAL on r (F1013) --------------------------------------------------------------- 259// A point estimate plus a yes/no test is still not an interval. "r=994, significant" and "r=994, plausible 260// range 991..996" say very different things, and at small n the honest interval is often embarrassingly 261// wide -- which is exactly the information a reader needs. 262// 263// Fisher z: z = atanh(r), SE = 1/sqrt(n-3), CI in z = z +- q*SE, then back through tanh. That looks like it 264// needs atanh AND tanh in fixed point. It does not. Because e^(2*atanh(r)) = (1+r)/(1-r), the whole thing 265// collapses to ONE exponential and then pure rational arithmetic: 266// e^(2z - 2d) = (1+r) / ((1-r) * E) and e^(2z + 2d) = (1+r) * E / (1-r), where E = e^(2d) 267// bound = (num - den) / (num + den) 268// So only e^(2d) is ever computed, on a small positive argument, and every remaining step is integer 269// division. No atanh series, no tanh, no float -- same input, same bits, always. 270// 271// The quantile is the NORMAL one (Fisher z uses z, not t), which is exactly the df=10000 asymptote row 272// already carried explicitly in analyst_tcrit.conf -- so it stays data-driven rather than a baked 1.96. 273const AI_MICRO: i64 = 1000000 274const AI_LN2_MICRO: i64 = 693147 // ln 2 in micro 275const AI_EXP_TERMS: i64 = 16 // Taylor terms after range reduction (reduced arg < ln2, so ample) 276const AI_EXP_KMAX: i64 = 40 // refuse absurd exponents rather than overflow the doubling loop 277const AI_Z_ASYMPTOTE_DF: i64 = 10000 // the conf row carrying the normal quantile 278 279// e^(w/1e6) scaled by 1e6, for w >= 0. Range-reduce by ln2 then Taylor on the remainder. 280func ai_exp_micro(w: i64) -> i64 { 281 if w < 0 { return 0 } 282 let k: i64 = w / AI_LN2_MICRO 283 if k > AI_EXP_KMAX { return 0 } 284 let r: i64 = w - k * AI_LN2_MICRO 285 var sum: i64 = AI_MICRO 286 var term: i64 = AI_MICRO 287 var i: i64 = 1 288 while i <= AI_EXP_TERMS { 289 term = term * r / AI_MICRO / i 290 sum = sum + term 291 i = i + 1 292 } 293 var out: i64 = sum 294 var j: i64 = 0 295 while j < k { out = out * 2; j = j + 1 } 296 return out 297} 298 299// 95pct (or alpha_permil) confidence interval for r, written into lo/hi in permille. 300// Returns 1 on success, 0 when the interval cannot be formed (n < 4, or no quantile in the table). 301// |r| == 1000 degenerates to the point itself rather than dividing by zero. 302func ai_r_ci(rp: i64, n: i64, alpha_permil: i64, lo: *i64, hi: *i64) -> i64 { 303 if n < 4 { return 0 } 304 if n > 9000000 { return 0 } 305 var a: i64 = rp 306 if a < 0 { a = 0 - a } 307 if a > AI_SCALE { return 0 } 308 if a == AI_SCALE { lo[0] = rp; hi[0] = rp; return 1 } 309 let zq: i64 = ai_tcrit_alpha(alpha_permil, AI_Z_ASYMPTOTE_DF) 310 if zq <= 0 { return 0 } 311 let s: i64 = df_isqrt((n - 3) * AI_MICRO * AI_MICRO) 312 if s <= 0 { return 0 } 313 let two_d: i64 = 2 * (zq * 1000) * AI_MICRO / s 314 let e: i64 = ai_exp_micro(two_d) 315 if e <= 0 { return 0 } 316 let onep: i64 = AI_SCALE + rp 317 let onem: i64 = AI_SCALE - rp 318 let nl: i64 = onep * AI_MICRO 319 let dl: i64 = onem * e 320 lo[0] = (nl - dl) * AI_SCALE / (nl + dl) 321 let nh: i64 = onep * e 322 let dh: i64 = onem * AI_MICRO 323 hi[0] = (nh - dh) * AI_SCALE / (nh + dh) 324 return 1 325} 326 327// ---- PARTIAL CORRELATION (F1004): does a relationship SURVIVE controlling for a third column? -------- 328// The analyst names a "key driver" from pairwise correlation, and pairwise correlation cannot tell a cause 329// from a shared cause. If z drives both x and y, then x and y correlate strongly and neither explains the 330// other -- report that as a driver and you have handed someone a confound with a number attached. 331// 332// r(xy.z) = ( r_xy - r_xz*r_yz ) / sqrt( (1 - r_xz^2)(1 - r_yz^2) ) 333// 334// carried in permille throughout, with the ecosystem's integer isqrt -- no float, still bit-reproducible. 335// Returns AI_R_UNDEF (magnitude > 1000) when the control leaves no variance to work with, which the 336// significance layer already treats as CANNOT TEST rather than as a strong correlation. 337func ai_r_undef() -> i64 { return 0 - 2000 } 338func ai_partial_milli(r_xy: i64, r_xz: i64, r_yz: i64) -> i64 { 339 if r_xy > AI_SCALE { return ai_r_undef() } 340 if r_xy < (0 - AI_SCALE) { return ai_r_undef() } 341 if r_xz > AI_SCALE { return ai_r_undef() } 342 if r_xz < (0 - AI_SCALE) { return ai_r_undef() } 343 if r_yz > AI_SCALE { return ai_r_undef() } 344 if r_yz < (0 - AI_SCALE) { return ai_r_undef() } 345 let qx: i64 = AI_SCALE_SQ - r_xz * r_xz 346 let qy: i64 = AI_SCALE_SQ - r_yz * r_yz 347 if qx <= 0 { return ai_r_undef() } 348 if qy <= 0 { return ai_r_undef() } 349 let den: i64 = df_isqrt(qx * qy) 350 if den <= 0 { return ai_r_undef() } 351 let num: i64 = r_xy * AI_SCALE - r_xz * r_yz 352 var p: i64 = num * AI_SCALE / den 353 // integer rounding can push a near-unity partial a hair past the legal range; clamp only THERE, never 354 // in the significance path (clamping a sentinel was the round-4 bug) 355 if p > AI_SCALE { p = AI_SCALE } 356 if p < (0 - AI_SCALE) { p = 0 - AI_SCALE } 357 return p 358} 359 360// Scan every OTHER candidate column as a potential confounder of (target, cand). Writes the control that 361// shrinks the relationship MOST into out_z and the surviving partial correlation into out_partial. 362// Returns 1 when a usable control existed, else 0. cols/names are the CANDIDATE arrays (target excluded). 363func ai_confound_scan(target: *i64, cands: *i64, ncand: i64, n: i64, ci: i64, out_z: *i64, out_partial: *i64) -> i64 { 364 if ncand < 2 { return 0 } 365 let cx: *i64 = cands[ci] as *i64 366 let r_xy: i64 = am_pearson_milli(target, cx, n) 367 var bestz: i64 = 0 - 1 368 var bestp: i64 = 0 369 var z: i64 = 0 370 while z < ncand { 371 if z != ci { 372 let cz: *i64 = cands[z] as *i64 373 let r_xz: i64 = am_pearson_milli(target, cz, n) 374 let r_yz: i64 = am_pearson_milli(cx, cz, n) 375 let p: i64 = ai_partial_milli(r_xy, r_xz, r_yz) 376 var ap: i64 = p 377 if ap < 0 { ap = 0 - ap } 378 if ap <= AI_SCALE { 379 var ab: i64 = bestp 380 if ab < 0 { ab = 0 - ab } 381 if bestz < 0 { bestz = z; bestp = p } else { if ap < ab { bestz = z; bestp = p } } 382 } 383 } 384 z = z + 1 385 } 386 if bestz < 0 { return 0 } 387 out_z[0] = bestz 388 out_partial[0] = bestp 389 return 1 390} 391 392// SPEARMAN rank correlation in permille = Pearson on the ranks. Catches MONOTONE relationships Pearson 393// understates or misses (cubic, exponential, any saturating curve) and is robust to outliers, because only 394// the ORDER survives ranking. df = n-2, so the same significance machinery applies unchanged. 395func ai_spearman_milli(x: *i64, y: *i64, n: i64) -> i64 { 396 if n < 2 { return 0 } 397 let rx: *i64 = sys_mmap(8 * n) as *i64 398 let ry: *i64 = sys_mmap(8 * n) as *i64 399 ai_rank2_into(x, n, rx) 400 ai_rank2_into(y, n, ry) 401 return am_pearson_milli(rx, ry, n) 402} 403 404// The ACTIONABLE half: smallest n at which this |r| would clear significance. 0 = not within AI_MINN_CAP 405// (e.g. r = 0). Significance is monotone in n for fixed r (lhs grows linearly in df while the threshold 406// only falls), so a binary search is exact -- no linear scan, no re-reading the conf. 407func ai_min_n(rp: i64) -> i64 { 408 let dfs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 409 let tcs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 410 let cnt: i64 = ai_tcrit_load(dfs, tcs, AI_TBL_MAX) 411 if ai_r_sig_from(dfs, tcs, cnt, rp, AI_MINN_CAP) != 1 { return 0 } 412 var lo: i64 = AI_MIN_PAIRS 413 var hi: i64 = AI_MINN_CAP 414 while lo < hi { 415 let mid: i64 = lo + (hi - lo) / 2 416 if ai_r_sig_from(dfs, tcs, cnt, rp, mid) == 1 { hi = mid } else { lo = mid + 1 } 417 } 418 return lo 419} 420 421// Plain-English verdict into out (NUL-terminated). This is the string the analyst prints next to every r 422// so a reader can never mistake noise for a finding. 423func ai_itoa(v: i64, out: *u8, at: i64) -> i64 { 424 var q: i64 = at 425 var m: i64 = v 426 if m < 0 { out[q] = 45 as u8; q = q + 1; m = 0 - m } 427 let t: *u8 = sys_mmap(28) 428 var k: i64 = 0 429 if m == 0 { t[0] = AI_ZERO as u8; k = 1 } 430 while m > 0 { t[k] = (AI_ZERO + (m % 10)) as u8; m = m / 10; k = k + 1 } 431 while k > 0 { k = k - 1; out[q] = t[k]; q = q + 1 } 432 return q 433} 434func ai_cat(out: *u8, at: i64, s: *u8) -> i64 { 435 var q: i64 = at 436 var j: i64 = 0 437 while s[j] != (0 as u8) { out[q] = s[j]; q = q + 1; j = j + 1 } 438 return q 439} 440func ai_r_verdict(rp: i64, n: i64, out: *u8) -> i64 { 441 let sig: i64 = ai_r_significant(rp, n) 442 var q: i64 = 0 443 if sig < 0 { 444 q = ai_cat(out, q, "CANNOT TEST -- need at least 3 paired observations, have n=" as *u8) 445 q = ai_itoa(n, out, q) 446 out[q] = 0 as u8 447 return sig 448 } 449 if sig == 1 { 450 q = ai_cat(out, q, "SIGNIFICANT at alpha .05 (n=" as *u8) 451 q = ai_itoa(n, out, q) 452 q = ai_cat(out, q, ") -- distinguishable from no correlation" as *u8) 453 out[q] = 0 as u8 454 return sig 455 } 456 q = ai_cat(out, q, "NOT SIGNIFICANT at alpha .05 (n=" as *u8) 457 q = ai_itoa(n, out, q) 458 q = ai_cat(out, q, ") -- INDISTINGUISHABLE FROM NOISE; do not report as a finding" as *u8) 459 let need: i64 = ai_min_n(rp) 460 if need > 0 { 461 q = ai_cat(out, q, ". An r this size would need n>=" as *u8) 462 q = ai_itoa(need, out, q) 463 } 464 out[q] = 0 as u8 465 return sig 466} 467 468// GUARDED KEY DRIVER: the significance-aware replacement for a bare argmax over |r|. Returns the index of 469// the strongest driver that is ALSO significant, or -1 when the strongest relationship in the data cannot 470// be distinguished from noise -- i.e. the honest answer "this dataset does not support naming a driver". 471// rs[] = per-column r in permille (as produced by am_pearson_milli), ncol = length, n = paired rows. 472func ai_key_driver_guarded(rs: *i64, ncol: i64, n: i64) -> i64 { 473 let dfs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 474 let tcs: *i64 = sys_mmap(8 * AI_TBL_MAX) as *i64 475 let cnt: i64 = ai_tcrit_load(dfs, tcs, AI_TBL_MAX) 476 var best: i64 = 0 - 1 477 var bestabs: i64 = 0 - 1 478 var i: i64 = 0 479 while i < ncol { 480 var a: i64 = rs[i] 481 if a < 0 { a = 0 - a } 482 if a > bestabs { if ai_r_sig_from(dfs, tcs, cnt, rs[i], n) == 1 { bestabs = a; best = i } } 483 i = i + 1 484 } 485 return best 486}