code wiki / _hdl_build / nx_analyst_report.nx

nx_analyst_report.nx source

↩ module page · 178 lines · 9428 B

1// nx_analyst_report.nx -- LIB: the "ANALYZE THIS DATASET" front door (capstone). Hand it N named i64 columns 2// + a target column; it composes the whole sovereign analytics stack into ONE human-readable report: 3// - per-column PROFILE + plain-English read (nx_analyst_data) 4// - each column's CORRELATION to the target + strength class (nx_analyst_multi) 5// - the strongest ASSOCIATION with the target (F1005: not called a DRIVER -- that is a causal word 6// and this is a correlation; the report says so in-band and points at nx_analyst_causal) 7// - a one-line SUMMARY 8// This is what a user actually wants: point at a dataset, get an analysis. license_tier: ORIGINAL 9import "nx_syscalls.nx" 10import "_hdl_build/nx_analyst_data.nx" 11import "_hdl_build/nx_analyst_multi.nx" 12import "nx_analyst_infer.nx" 13 14func arx_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i } 15func arx_catn(d: *u8, o: i64, v: i64) -> i64 { 16 var oo: i64 = o 17 var m: i64 = v 18 if m < 0 { d[oo] = 45 as u8; oo = oo + 1; m = 0 - m } 19 let t: *u8 = sys_mmap(24) 20 var k: i64 = 0 21 if m == 0 { t[0] = 48 as u8; k = 1 } 22 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 23 var i: i64 = k - 1 24 while i >= 0 { d[oo] = t[i]; oo = oo + 1; i = i - 1 } 25 return oo 26} 27 28// cols = array of column pointers (i64 each); names = array of name pointers (i64 each); target_idx names the 29// target column. Emits the full report into out. Returns bytes written. 30func ar_analyze(cols: *i64, names: *i64, ncol: i64, n: i64, target_idx: i64, out: *u8, cap: i64) -> i64 { 31 if cap < 8192 { return 0 } 32 if ncol <= 0 { return 0 } 33 if target_idx < 0 { return 0 } 34 if target_idx >= ncol { return 0 } 35 var o: i64 = 0 36 o = arx_cat(out, o, "=== DATASET ANALYSIS ===\nrows=" as *u8) 37 o = arx_catn(out, o, n) 38 o = arx_cat(out, o, " columns=" as *u8) 39 o = arx_catn(out, o, ncol) 40 o = arx_cat(out, o, " target=" as *u8) 41 let tname: *u8 = names[target_idx] as *u8 42 o = arx_cat(out, o, tname) 43 o = arx_cat(out, o, "\n\n-- PER-COLUMN PROFILES --\n" as *u8) 44 45 let prof: *i64 = sys_mmap(16 * 8) as *i64 46 let sub: *u8 = sys_mmap(4096) 47 var c: i64 = 0 48 while c < ncol { 49 if o + 4200 >= cap { return o } 50 let colc: *i64 = cols[c] as *i64 51 let namec: *u8 = names[c] as *u8 52 ad_profile(colc, n, 10, prof) 53 let sn: i64 = ad_report(namec, prof, sub, 4096) 54 var si: i64 = 0 55 while si < sn { out[o] = sub[si]; o = o + 1; si = si + 1 } 56 c = c + 1 57 } 58 59 // relationships to the target 60 o = arx_cat(out, o, "\n-- RELATIONSHIPS TO " as *u8) 61 o = arx_cat(out, o, tname) 62 o = arx_cat(out, o, " --\n" as *u8) 63 let tcol: *i64 = cols[target_idx] as *i64 64 let cls: *u8 = sys_mmap(128) 65 let sigb: *u8 = sys_mmap(512) // significance verdict text (nx_analyst_infer) 66 c = 0 67 while c < ncol { 68 if c != target_idx { 69 if o + 400 >= cap { return o } 70 let colc2: *i64 = cols[c] as *i64 71 let namec2: *u8 = names[c] as *u8 72 let r: i64 = am_pearson_milli(tcol, colc2, n) 73 am_class(0 as *u8, r, cls) 74 o = arx_cat(out, o, " " as *u8) 75 o = arx_cat(out, o, namec2) 76 o = arx_cat(out, o, ": r=" as *u8) 77 o = arx_catn(out, o, r) 78 o = arx_cat(out, o, "/1000 (" as *u8) 79 o = arx_cat(out, o, cls) 80 // SIGNIFICANCE (2026-07-23): a strength class on its own says nothing about sample size, so 81 // the same "negative weak" sentence is printed for a real effect at n=300 and for pure noise at 82 // n=8. Every r now also states whether it is distinguishable from no correlation, and if not, 83 // what n it would take to settle it. 84 o = arx_cat(out, o, ") " as *u8) 85 ai_r_verdict(r, n, sigb) 86 o = arx_cat(out, o, sigb) 87 o = arx_cat(out, o, "\n" as *u8) 88 } 89 c = c + 1 90 } 91 92 // key driver (exclude the target itself by building a candidate list of the OTHER columns) 93 let cand: *i64 = sys_mmap(64 * 8) as *i64 94 let cand_names: *i64 = sys_mmap(64 * 8) as *i64 95 var nc: i64 = 0 96 c = 0 97 while c < ncol { 98 if c != target_idx { if nc < 64 { cand[nc] = cols[c]; cand_names[nc] = names[c]; nc = nc + 1 } } 99 c = c + 1 100 } 101 let oi: *i64 = sys_mmap(8) as *i64 102 let orr: *i64 = sys_mmap(8) as *i64 103 o = arx_cat(out, o, "\n-- STRONGEST ASSOCIATION with " as *u8) 104 o = arx_cat(out, o, tname) 105 o = arx_cat(out, o, " --\n " as *u8) 106 if am_key_driver(tcol, cand, nc, n, oi, orr) == 1 { 107 let dname: *u8 = cand_names[oi[0]] as *u8 108 am_class(0 as *u8, orr[0], cls) 109 // GUARD (2026-07-23): the strongest |r| is a FINDING only if it clears significance at this n. 110 // am_key_driver is a bare argmax -- it ALWAYS names something, however little data backs it. 111 // Below the bar the honest report is that the data cannot support naming a driver at all, 112 // plus the n that would settle it. 113 // Bonferroni over the nc candidates: naming the LARGEST of nc correlations IS a selection, so the 114 // driver must clear the family-wise level, not the per-test one. Uncorrected, ~nc*5% of datasets 115 // hand you a "driver" that is luck. 116 if ai_r_sig_bonferroni(orr[0], n, nc) == 1 { 117 o = arx_cat(out, o, dname) 118 o = arx_cat(out, o, " has the strongest association (r=" as *u8) 119 o = arx_catn(out, o, orr[0]) 120 o = arx_cat(out, o, "/1000, " as *u8) 121 o = arx_cat(out, o, cls) 122 o = arx_cat(out, o, ") " as *u8) 123 ai_r_verdict(orr[0], n, sigb) 124 o = arx_cat(out, o, sigb) 125 o = arx_cat(out, o, "\n" as *u8) 126 // F1013: the plausible RANGE, not just the point and a yes/no. At small n this is often 127 // embarrassingly wide, which is the point -- a reader deserves to see how little the data pins down. 128 let cl: *i64 = sys_mmap(8) as *i64 129 let ch: *i64 = sys_mmap(8) as *i64 130 if ai_r_ci(orr[0], n, 50, cl, ch) == 1 { 131 o = arx_cat(out, o, " 95pct CONFIDENCE INTERVAL for r: " as *u8) 132 o = arx_catn(out, o, cl[0]) 133 o = arx_cat(out, o, " to " as *u8) 134 o = arx_catn(out, o, ch[0]) 135 o = arx_cat(out, o, "/1000 (Fisher z)\n" as *u8) 136 } 137 // CONFOUND CHECK (F1004): a pairwise correlation cannot tell a cause from a shared cause. If 138 // controlling for another candidate collapses the relationship, the "driver" was that other 139 // column all along -- so say so instead of leaving a confound with a number attached. 140 let zi: *i64 = sys_mmap(8) as *i64 141 let pr: *i64 = sys_mmap(8) as *i64 142 if ai_confound_scan(tcol, cand, nc, n, oi[0], zi, pr) == 1 { 143 // one control consumes one dof, so the partial is tested at n-1 (df = n-3) 144 let still: i64 = ai_r_significant(pr[0], n - 1) 145 o = arx_cat(out, o, " CONTROLLING FOR " as *u8) 146 o = arx_cat(out, o, cand_names[zi[0]] as *u8) 147 o = arx_cat(out, o, " (the control that weakens it most): partial r=" as *u8) 148 o = arx_catn(out, o, pr[0]) 149 if still == 1 { 150 o = arx_cat(out, o, "/1000 -- SURVIVES, the relationship is not explained away by it\n" as *u8) 151 } else { 152 o = arx_cat(out, o, "/1000 -- CONFOUNDED: it does NOT survive that control, so this is a shared cause, not an effect\n" as *u8) 153 } 154 // HONEST NAMING (F1005): the control here was picked because it moves the number most. 155 // That is a heuristic, and the SAME adjustment is required for a confounder, forbidden for 156 // a mediator, and actively harmful for a collider -- so say what this is and is not. 157 o = arx_cat(out, o, " NOTE: that control was picked because it weakens the association most -- a HEURISTIC, not a causal identification. Adjusting is REQUIRED for a confounder, FORBIDDEN for a mediator, and actively harmful for a collider (it manufactures associations from independent columns), and correlation cannot tell those apart. Declare roles via nx_analyst_causal to license a causal reading.\n" as *u8) 158 } 159 } else { 160 o = arx_cat(out, o, "NO ASSOCIATION SUPPORTED BY THIS DATA (family-wise .05 over " as *u8) 161 o = arx_catn(out, o, nc) 162 o = arx_cat(out, o, " candidates) -- the strongest relationship (" as *u8) 163 o = arx_cat(out, o, dname) 164 o = arx_cat(out, o, " r=" as *u8) 165 o = arx_catn(out, o, orr[0]) 166 o = arx_cat(out, o, "/1000) is not distinguishable from noise at n=" as *u8) 167 o = arx_catn(out, o, n) 168 let needn: i64 = ai_min_n(orr[0]) 169 if needn > 0 { o = arx_cat(out, o, "; an r that size needs n>=" as *u8); o = arx_catn(out, o, needn) } 170 o = arx_cat(out, o, "\n" as *u8) 171 } 172 } else { 173 o = arx_cat(out, o, "no measurable driver (all candidates constant)\n" as *u8) 174 } 175 if o + 2 >= cap { return o } 176 out[o] = 0 as u8 177 return o 178}