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}