code wiki / (root) / nx_survey_stats.nx

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}