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}