nx_survey_stats.nx source
↩ module page · 431 lines · 16805 B
1// nx_survey_stats.nx -- the INSIGHTS engine over survey responses (operator 2026-07-10: "we have the survey
2// front end but we dont have any of the statistics or all the capabilities that power meaningful insights").
3// CONSUMES nx_survey_engine's effective ballots (se_ballots -> vout[i]/lout[i] = ballot record bytes) and
4// question records; turns raw responses into the analytics a real survey product delivers:
5// - descriptive: n/min/max/mean/median/mode/stddev (fixed-point permille, integer isqrt)
6// - CX metrics: NPS (promoters-detractors), top/bottom-box, 95% confidence margin on a proportion
7// - cross-tabulation: r x c contingency of one question by another
8// - significance: chi-square test of independence vs a DATA-DRIVEN critical table (alpha .05), FAIL-CLOSED
9// past df 10 (never a false "significant") -- the adversary's own guard
10// - key drivers: Pearson correlation of every scale question against a chosen outcome, ranked by |r|
11// - segment analysis: mean of a target question within each level of a segment question
12// - trend: before/after split on real ballot timestamps
13// - welfare triage rollup: composes nx_survey's sv_triage/sv_band to count crisis/strained/stable
14// - open text: sovereign tokenized theme frequency (honest: frequency-based, NOT neural topic/sentiment)
15// 100% integer / fixed-point, no floats, no hardware writes. Every stat is hand-KAT'd in the gate and carries
16// an adversarial neg-control (a fabricated significance / correlation must be rejected). license_tier: ORIGINAL
17import "nx_survey_engine.nx"
18const ST_MAGIC_3841: i64 = 3841
19const ST_MAGIC_5991: i64 = 5991
20const ST_MAGIC_7815: i64 = 7815
21const ST_MAGIC_9488: i64 = 9488
22const ST_MAGIC_11070: i64 = 11070
23const ST_MAGIC_12592: i64 = 12592
24const ST_MAGIC_14067: i64 = 14067
25const ST_MAGIC_15507: i64 = 15507
26const ST_MAGIC_16919: i64 = 16919
27const ST_MAGIC_18307: i64 = 18307
28const ST_MAGIC_9223372036854775807: i64 = 9223372036854775807
29const ST_MAGIC_4096: i64 = 4096
30
31const ST_SCALE: i64 = 1000 // permille fixed-point
32const ST_Z95SQ: i64 = 3841600 // (1.96 * 1000)^2 = 3841600, the 95% two-tailed z^2 in permille^2
33
34// floor(sqrt(n)) via Newton's method on i64 (the one primitive every stat leans on)
35func st_isqrt(n: i64) -> i64 {
36 if n <= 0 { return 0 }
37 if n < 4 { return 1 }
38 var x: i64 = n
39 var y: i64 = (x + 1) / 2
40 while y < x { x = y; y = (x + n / x) / 2 }
41 return x
42}
43
44// integer answer to qid in one ballot, or -1 when absent / non-numeric
45func st_answer_int(vp: *u8, vl: i64, qid: *u8) -> i64 {
46 let ab: *u8 = sys_mmap(64)
47 let n: i64 = se_answer_of(vp, vl, qid, ab, 64)
48 if n <= 0 { return 0 - 1 }
49 if se_digits(ab) == 0 { return 0 - 1 }
50 return sv_atoi(ab)
51}
52// the ballot's commit timestamp (usec field), or -1
53func st_ballot_usec(vp: *u8, vl: i64) -> i64 {
54 let ks: *i64 = sys_mmap(8 * 40) as *i64
55 let vs: *i64 = sys_mmap(8 * 40) as *i64
56 let nf: i64 = canon_decode(vp, vl, ks, vs, 40)
57 if nf < 0 { return 0 - 1 }
58 let v: *u8 = sv_get(ks, vs, nf, "usec" as *u8)
59 if (v as i64) == 0 { return 0 - 1 }
60 return sv_atoi(v)
61}
62// collect the numeric answers to qid across ballots into xs[]; returns count
63func st_collect(vout: *i64, lout: *i64, nb: i64, qid: *u8, xs: *i64) -> i64 {
64 var m: i64 = 0
65 var i: i64 = 0
66 while i < nb {
67 let v: i64 = st_answer_int(vout[i] as *u8, lout[i], qid)
68 if v >= 0 { xs[m] = v; m = m + 1 }
69 i = i + 1
70 }
71 return m
72}
73// collect ballots answering BOTH qidX and qidY numerically -> paired xs[]/ys[]; returns pair count
74func st_collect_pairs(vout: *i64, lout: *i64, nb: i64, qx: *u8, qy: *u8, xs: *i64, ys: *i64) -> i64 {
75 var m: i64 = 0
76 var i: i64 = 0
77 while i < nb {
78 let a: i64 = st_answer_int(vout[i] as *u8, lout[i], qx)
79 let b: i64 = st_answer_int(vout[i] as *u8, lout[i], qy)
80 if a >= 0 { if b >= 0 { xs[m] = a; ys[m] = b; m = m + 1 } }
81 i = i + 1
82 }
83 return m
84}
85// ascending insertion sort of a fresh copy (median/mode); writes into dst[]
86func st_sortcopy(xs: *i64, m: i64, dst: *i64) -> i64 {
87 var i: i64 = 0
88 while i < m { dst[i] = xs[i]; i = i + 1 }
89 i = 1
90 while i < m {
91 let cur: i64 = dst[i]
92 var j: i64 = i - 1
93 var go: i64 = 1
94 while go == 1 {
95 if j < 0 { go = 0 } else {
96 if dst[j] > cur { dst[j + 1] = dst[j]; j = j - 1 } else { go = 0 }
97 }
98 }
99 dst[j + 1] = cur
100 i = i + 1
101 }
102 return 0
103}
104// descriptive stats into out[7]: [0]=n [1]=min [2]=max [3]=mean_pm [4]=median_pm [5]=stddev_pm [6]=mode
105func st_desc(xs: *i64, m: i64, out: *i64) -> i64 {
106 if m <= 0 { var z: i64 = 0; while z < 7 { out[z] = 0; z = z + 1 } return 0 }
107 var sum: i64 = 0
108 var sumsq: i64 = 0
109 var mn: i64 = xs[0]
110 var mx: i64 = xs[0]
111 var i: i64 = 0
112 while i < m {
113 let v: i64 = xs[i]
114 sum = sum + v
115 sumsq = sumsq + v * v
116 if v < mn { mn = v }
117 if v > mx { mx = v }
118 i = i + 1
119 }
120 out[0] = m
121 out[1] = mn
122 out[2] = mx
123 out[3] = sum * ST_SCALE / m
124 // median from sorted copy
125 let srt: *i64 = sys_mmap(8 * (m + 2)) as *i64
126 st_sortcopy(xs, m, srt)
127 if m % 2 == 1 { out[4] = srt[m / 2] * ST_SCALE } else { out[4] = (srt[m / 2 - 1] + srt[m / 2]) * ST_SCALE / 2 }
128 // stddev permille = isqrt( (n*sumsq - sum^2) * SCALE^2 ) / n (population)
129 let numer: i64 = m * sumsq - sum * sum
130 if numer <= 0 { out[5] = 0 } else { out[5] = st_isqrt(numer * ST_SCALE * ST_SCALE) / m }
131 // mode = value with the longest run in the sorted copy
132 var best: i64 = srt[0]
133 var bestc: i64 = 1
134 var curc: i64 = 1
135 i = 1
136 while i < m {
137 if srt[i] == srt[i - 1] { curc = curc + 1 } else { curc = 1 }
138 if curc > bestc { bestc = curc; best = srt[i] }
139 i = i + 1
140 }
141 out[6] = best
142 return 0
143}
144// NPS into out[4]: [0]=nps(-100..100) [1]=promoters [2]=passives [3]=detractors. prom_min/det_max = cutoffs (DATA).
145func st_nps(xs: *i64, m: i64, prom_min: i64, det_max: i64, out: *i64) -> i64 {
146 var pr: i64 = 0
147 var de: i64 = 0
148 var i: i64 = 0
149 while i < m {
150 if xs[i] >= prom_min { pr = pr + 1 } else { if xs[i] <= det_max { de = de + 1 } }
151 i = i + 1
152 }
153 out[1] = pr
154 out[2] = m - pr - de
155 out[3] = de
156 if m == 0 { out[0] = 0 } else { out[0] = (pr - de) * 100 / m }
157 return 0
158}
159// top-box permille: share of answers with value >= maxval - boxsize + 1
160func st_topbox(xs: *i64, m: i64, maxval: i64, boxsize: i64) -> i64 {
161 if m == 0 { return 0 }
162 let thr: i64 = maxval - boxsize + 1
163 var c: i64 = 0
164 var i: i64 = 0
165 while i < m { if xs[i] >= thr { c = c + 1 } i = i + 1 }
166 return c * ST_SCALE / m
167}
168// 95% two-tailed confidence margin (permille) on proportion k/n: 1.96*sqrt(p(1-p)/n)
169func st_ci_margin(k: i64, n: i64) -> i64 {
170 if n <= 0 { return 0 }
171 if k < 0 { return 0 }
172 if k > n { return 0 }
173 // margin_pm = isqrt( Z95SQ * k * (n-k) / n^3 )
174 return st_isqrt(ST_Z95SQ * k * (n - k) / (n * n * n))
175}
176
177// ---- cross-tabulation + chi-square ----
178// fill an ra x cb contingency table (row-major, table[i*cb+j]) of ballots answering BOTH qa and qb;
179// returns the grand total counted. Out-of-range choice indices are skipped (defensive).
180func st_xtab(vout: *i64, lout: *i64, nb: i64, qa: *u8, qb: *u8, ra: i64, cb: i64, table: *i64) -> i64 {
181 var z: i64 = 0
182 while z < ra * cb { table[z] = 0; z = z + 1 }
183 var grand: i64 = 0
184 var i: i64 = 0
185 while i < nb {
186 let a: i64 = st_answer_int(vout[i] as *u8, lout[i], qa)
187 let b: i64 = st_answer_int(vout[i] as *u8, lout[i], qb)
188 if a >= 0 { if a < ra { if b >= 0 { if b < cb {
189 table[a * cb + b] = table[a * cb + b] + 1
190 grand = grand + 1
191 } } } }
192 i = i + 1
193 }
194 return grand
195}
196// chi-square critical value at alpha=0.05 (permille), df 1..10; FAIL-CLOSED (huge) past the table so we can
197// never falsely declare significance for a df we do not have a bound for (the adversary's guard).
198func st_crit05_pm(df: i64) -> i64 {
199 if df == 1 { return ST_MAGIC_3841 }
200 if df == 2 { return ST_MAGIC_5991 }
201 if df == 3 { return ST_MAGIC_7815 }
202 if df == 4 { return ST_MAGIC_9488 }
203 if df == 5 { return ST_MAGIC_11070 }
204 if df == 6 { return ST_MAGIC_12592 }
205 if df == 7 { return ST_MAGIC_14067 }
206 if df == 8 { return ST_MAGIC_15507 }
207 if df == 9 { return ST_MAGIC_16919 }
208 if df == 10 { return ST_MAGIC_18307 }
209 return ST_MAGIC_9223372036854775807
210}
211// chi-square of independence on table[r*c]; out[0]=chisq_pm out[1]=df. Returns 1 significant (>=crit .05), else 0.
212func st_chisq(table: *i64, r: i64, c: i64, out: *i64) -> i64 {
213 let rowt: *i64 = sys_mmap(8 * (r + 1)) as *i64
214 let colt: *i64 = sys_mmap(8 * (c + 1)) as *i64
215 var i: i64 = 0
216 while i < r { rowt[i] = 0; i = i + 1 }
217 var j: i64 = 0
218 while j < c { colt[j] = 0; j = j + 1 }
219 var nn: i64 = 0
220 i = 0
221 while i < r {
222 j = 0
223 while j < c {
224 let o: i64 = table[i * c + j]
225 rowt[i] = rowt[i] + o
226 colt[j] = colt[j] + o
227 nn = nn + o
228 j = j + 1
229 }
230 i = i + 1
231 }
232 out[1] = (r - 1) * (c - 1)
233 if nn == 0 { out[0] = 0; return 0 }
234 var chi: i64 = 0
235 i = 0
236 while i < r {
237 j = 0
238 while j < c {
239 if rowt[i] > 0 { if colt[j] > 0 {
240 let o: i64 = table[i * c + j]
241 let d: i64 = o * nn - rowt[i] * colt[j]
242 chi = chi + (d * d) * ST_SCALE / (nn * rowt[i] * colt[j])
243 } }
244 j = j + 1
245 }
246 i = i + 1
247 }
248 out[0] = chi
249 let crit: i64 = st_crit05_pm(out[1])
250 if chi >= crit { return 1 }
251 return 0
252}
253
254// Pearson correlation (permille, -1000..1000) of m paired values; 0 when undefined (flat variable)
255func st_corr(xs: *i64, ys: *i64, m: i64) -> i64 {
256 if m < 2 { return 0 }
257 var sx: i64 = 0
258 var sy: i64 = 0
259 var sxy: i64 = 0
260 var sxx: i64 = 0
261 var syy: i64 = 0
262 var i: i64 = 0
263 while i < m {
264 let x: i64 = xs[i]
265 let y: i64 = ys[i]
266 sx = sx + x
267 sy = sy + y
268 sxy = sxy + x * y
269 sxx = sxx + x * x
270 syy = syy + y * y
271 i = i + 1
272 }
273 let num: i64 = m * sxy - sx * sy
274 let dxx: i64 = m * sxx - sx * sx
275 let dyy: i64 = m * syy - sy * sy
276 if dxx <= 0 { return 0 }
277 if dyy <= 0 { return 0 }
278 let den: i64 = st_isqrt(dxx * dyy)
279 if den == 0 { return 0 }
280 return num * ST_SCALE / den
281}
282
283// segment means: for each level 0..seg_n-1 of seg_qid, count + mean_pm of tgt_qid; into out_n[]/out_meanpm[]
284func st_segment_means(vout: *i64, lout: *i64, nb: i64, seg_qid: *u8, tgt_qid: *u8, seg_n: i64, out_n: *i64, out_meanpm: *i64) -> i64 {
285 let sum: *i64 = sys_mmap(8 * (seg_n + 1)) as *i64
286 var s: i64 = 0
287 while s < seg_n { sum[s] = 0; out_n[s] = 0; out_meanpm[s] = 0; s = s + 1 }
288 var i: i64 = 0
289 while i < nb {
290 let g: i64 = st_answer_int(vout[i] as *u8, lout[i], seg_qid)
291 let t: i64 = st_answer_int(vout[i] as *u8, lout[i], tgt_qid)
292 if g >= 0 { if g < seg_n { if t >= 0 {
293 sum[g] = sum[g] + t
294 out_n[g] = out_n[g] + 1
295 } } }
296 i = i + 1
297 }
298 s = 0
299 while s < seg_n {
300 if out_n[s] > 0 { out_meanpm[s] = sum[s] * ST_SCALE / out_n[s] }
301 s = s + 1
302 }
303 return 0
304}
305// before/after trend on real ballot timestamps: out[5] = [n_before, mean_before_pm, n_after, mean_after_pm, delta_pm]
306func st_trend_split(vout: *i64, lout: *i64, nb: i64, qid: *u8, split_usec: i64, out: *i64) -> i64 {
307 var nb0: i64 = 0
308 var sb0: i64 = 0
309 var na0: i64 = 0
310 var sa0: i64 = 0
311 var i: i64 = 0
312 while i < nb {
313 let v: i64 = st_answer_int(vout[i] as *u8, lout[i], qid)
314 if v >= 0 {
315 let ts: i64 = st_ballot_usec(vout[i] as *u8, lout[i])
316 if ts >= 0 {
317 if ts < split_usec { nb0 = nb0 + 1; sb0 = sb0 + v } else { na0 = na0 + 1; sa0 = sa0 + v }
318 }
319 }
320 i = i + 1
321 }
322 out[0] = nb0
323 out[2] = na0
324 var mb: i64 = 0
325 var ma: i64 = 0
326 if nb0 > 0 { mb = sb0 * ST_SCALE / nb0 }
327 if na0 > 0 { ma = sa0 * ST_SCALE / na0 }
328 out[1] = mb
329 out[3] = ma
330 out[4] = ma - mb
331 return 0
332}
333// welfare triage rollup: compose sv_triage/sv_band per ballot; out[3]=[stable,strained,crisis]. wqids are the
334// ballot field keys ("a_<qid>"), weights + band thresholds are DATA (rule 11).
335func st_triage_rollup(vout: *i64, lout: *i64, nb: i64, wqids: *i64, weights: *i64, nw: i64, t_str: i64, t_cri: i64, out: *i64) -> i64 {
336 out[0] = 0
337 out[1] = 0
338 out[2] = 0
339 var i: i64 = 0
340 while i < nb {
341 let ks: *i64 = sys_mmap(8 * 48) as *i64
342 let vs: *i64 = sys_mmap(8 * 48) as *i64
343 let nf: i64 = canon_decode(vout[i] as *u8, lout[i], ks, vs, 48)
344 if nf > 0 {
345 let sc: i64 = sv_triage(ks, vs, nf, wqids, weights, nw)
346 let band: i64 = sv_band(sc, t_str, t_cri)
347 out[band] = out[band] + 1
348 }
349 i = i + 1
350 }
351 return 0
352}
353
354// ---- open-text theme frequency (sovereign; frequency-based, honest NOT neural) ----
355// common English stopwords so themes surface content words, not glue
356func st_stopword(t: *u8) -> i64 {
357 if se_seq(t, "the" as *u8) == 1 { return 1 }
358 if se_seq(t, "and" as *u8) == 1 { return 1 }
359 if se_seq(t, "for" as *u8) == 1 { return 1 }
360 if se_seq(t, "are" as *u8) == 1 { return 1 }
361 if se_seq(t, "our" as *u8) == 1 { return 1 }
362 if se_seq(t, "was" as *u8) == 1 { return 1 }
363 if se_seq(t, "with" as *u8) == 1 { return 1 }
364 if se_seq(t, "that" as *u8) == 1 { return 1 }
365 if se_seq(t, "this" as *u8) == 1 { return 1 }
366 if se_seq(t, "have" as *u8) == 1 { return 1 }
367 if se_seq(t, "would" as *u8) == 1 { return 1 }
368 if se_seq(t, "could" as *u8) == 1 { return 1 }
369 if se_seq(t, "you" as *u8) == 1 { return 1 }
370 if se_seq(t, "your" as *u8) == 1 { return 1 }
371 if se_seq(t, "not" as *u8) == 1 { return 1 }
372 if se_seq(t, "but" as *u8) == 1 { return 1 }
373 if se_seq(t, "get" as *u8) == 1 { return 1 }
374 if se_seq(t, "from" as *u8) == 1 { return 1 }
375 if se_seq(t, "they" as *u8) == 1 { return 1 }
376 if se_seq(t, "can" as *u8) == 1 { return 1 }
377 return 0
378}
379// tally content-word frequency across all text answers to qid. Parallel arrays terms_out[]/freq_out[] hold up
380// to cap distinct terms (pool-backed term strings). Returns distinct-term count (bounded by cap).
381func st_text_themes(vout: *i64, lout: *i64, nb: i64, qid: *u8, terms_out: *i64, freq_out: *i64, cap: i64) -> i64 {
382 let pool: *u8 = sys_mmap(cap * 40 + ST_MAGIC_4096)
383 var pooloff: i64 = 0
384 var nt: i64 = 0
385 let tbl: *u8 = sys_mmap(272)
386 ss_tok_table(tbl)
387 let ab: *u8 = sys_mmap(ST_MAGIC_4096)
388 let tok: *u8 = sys_mmap(40)
389 let pos: *i64 = sys_mmap(16) as *i64
390 var i: i64 = 0
391 while i < nb {
392 let al: i64 = se_answer_of(vout[i] as *u8, lout[i], qid, ab, ST_MAGIC_4096)
393 if al > 0 {
394 pos[0] = 0
395 var go: i64 = 1
396 while go == 1 {
397 let tl: i64 = ss_tok_next2(ab, al, pos, tok, tbl)
398 if tl < 0 { go = 0 } else {
399 if st_stopword(tok) == 0 {
400 // linear find/insert (term counts are small; cap bounds it)
401 var f: i64 = 0 - 1
402 var k: i64 = 0
403 while k < nt { if se_seq(terms_out[k] as *u8, tok) == 1 { f = k; k = nt } else { k = k + 1 } }
404 if f >= 0 { freq_out[f] = freq_out[f] + 1 } else {
405 if nt < cap {
406 let dst: *u8 = (pool as i64 + pooloff) as *u8
407 var x: i64 = 0
408 while x <= tl { dst[x] = tok[x]; x = x + 1 }
409 pooloff = pooloff + tl + 1
410 terms_out[nt] = dst as i64
411 freq_out[nt] = 1
412 nt = nt + 1
413 }
414 }
415 }
416 }
417 }
418 }
419 i = i + 1
420 }
421 return nt
422}
423// frequency of one exact term across text answers (the gate's deterministic oracle)
424func st_term_freq(vout: *i64, lout: *i64, nb: i64, qid: *u8, term: *u8) -> i64 {
425 let terms: *i64 = sys_mmap(8 * 256) as *i64
426 let freqs: *i64 = sys_mmap(8 * 256) as *i64
427 let nt: i64 = st_text_themes(vout, lout, nb, qid, terms, freqs, 256)
428 var i: i64 = 0
429 while i < nt { if se_seq(terms[i] as *u8, term) == 1 { return freqs[i] } i = i + 1 }
430 return 0
431}