code wiki / (root) / nx_survey_quality.nx

nx_survey_quality.nx source

↩ module page · 114 lines · 5366 B

1// nx_survey_quality.nx -- careless / insufficient-effort responding (C/IER) detection, the DATA-QUALITY guard 2// (operator 2026-07-10: evidence-driven, "the feedback and questions identify quant+qual areas of improvement" 3// -- and careless responses corrupt the very metrics nx_survey_stats computes, so trust must be measured). 4// Evidence base (banked 2024-26, knowledge/library/swc_svy_quality.txt): "Response Satisficing Across Online 5// Data Sources", "reducing careless responding in crowdsourced online surveys". Methods, integer, gate-KAT'd: 6// - ATTENTION CHECK (instructed-response item, the C/IER gold standard): the survey's @check meta (qid:val,) 7// names a question with a KNOWN correct answer; a respondent whose answer != expected (or absent) is flagged. 8// - LONGSTRING / straight-lining: the longest run of identical consecutive answers across a block of 9// same-format items; a maxed run = suspected careless. HONEST: a weak signal on SHORT blocks -> data-driven 10// threshold + caveat; the attention check is the strong signal. 11// - CONSISTENCY: two items that should agree diverging beyond a data-driven max (available; spec-wire later). 12// FLAG, NEVER silently drop -- the admin sees the count + can recompute on the trusted subset. Composes 13// nx_survey_stats' st_answer_int (so ballots decode identically). license_tier: ORIGINAL 14import "nx_survey_stats.nx" 15const K_MAGIC_999999: i64 = 999999 16 17// parse the survey's @check meta ("qid:expected,qid:expected,") and score one ballot against it. 18// out[0] = failed checks, out[1] = total checks present. returns failed. 19func sq_attention(vp: *u8, vl: i64, checks: *u8, out: *i64) -> i64 { 20 var total: i64 = 0 21 var failed: i64 = 0 22 var i: i64 = 0 23 while checks[i] != (0 as u8) { 24 let qid: *u8 = sys_mmap(40) 25 var q: i64 = 0 26 var g1: i64 = 1 27 while g1 == 1 { 28 if checks[i] == (0 as u8) { g1 = 0 } else { 29 if checks[i] == (58 as u8) { g1 = 0 } else { if q < 39 { qid[q] = checks[i]; q = q + 1 } i = i + 1 } 30 } 31 } 32 if checks[i] == (58 as u8) { i = i + 1 } 33 qid[q] = 0 as u8 34 let vb: *u8 = sys_mmap(16) 35 var v: i64 = 0 36 var g2: i64 = 1 37 while g2 == 1 { 38 if checks[i] == (0 as u8) { g2 = 0 } else { 39 if checks[i] == (44 as u8) { g2 = 0 } else { if v < 15 { vb[v] = checks[i]; v = v + 1 } i = i + 1 } 40 } 41 } 42 if checks[i] == (44 as u8) { i = i + 1 } 43 vb[v] = 0 as u8 44 if q > 0 { if v > 0 { 45 total = total + 1 46 let expected: i64 = sv_atoi(vb) 47 let got: i64 = st_answer_int(vp, vl, qid) 48 // absent (-1) OR wrong answer = a failed attention check (careless / not reading) 49 if got != expected { failed = failed + 1 } 50 } } 51 } 52 out[0] = failed 53 out[1] = total 54 return failed 55} 56// longest run of identical consecutive numeric answers across qids[] (a same-format item block). 57// An absent answer breaks the run. Returns the max run length (>=1 when any answered; 0 when none). 58func sq_longstring(vp: *u8, vl: i64, qids: *i64, nq: i64) -> i64 { 59 var best: i64 = 0 60 var run: i64 = 0 61 var prev: i64 = 0 - K_MAGIC_999999 62 var i: i64 = 0 63 while i < nq { 64 let a: i64 = st_answer_int(vp, vl, qids[i] as *u8) 65 if a >= 0 { 66 if a == prev { run = run + 1 } else { run = 1; prev = a } 67 if run > best { best = run } 68 } else { run = 0; prev = 0 - K_MAGIC_999999 } 69 i = i + 1 70 } 71 return best 72} 73// consistency: 1 if two answers that should agree diverge beyond maxdiff; 0 if consistent or either absent. 74func sq_consistency(vp: *u8, vl: i64, qa: *u8, qb: *u8, maxdiff: i64) -> i64 { 75 let a: i64 = st_answer_int(vp, vl, qa) 76 let b: i64 = st_answer_int(vp, vl, qb) 77 if a < 0 { return 0 } 78 if b < 0 { return 0 } 79 var d: i64 = a - b 80 if d < 0 { d = 0 - d } 81 if d > maxdiff { return 1 } 82 return 0 83} 84// composite careless flag for one ballot. reason[0]=attn_fails reason[1]=longstring reason[2]=flag(1/0). 85// flagged iff any attention check failed OR (ls_thresh>0 AND longstring >= ls_thresh). 86func sq_ballot_careless(vp: *u8, vl: i64, checks: *u8, sqids: *i64, nsq: i64, ls_thresh: i64, reason: *i64) -> i64 { 87 let ao: *i64 = sys_mmap(16) as *i64 88 sq_attention(vp, vl, checks, ao) 89 let ls: i64 = sq_longstring(vp, vl, sqids, nsq) 90 reason[0] = ao[0] 91 reason[1] = ls 92 var flag: i64 = 0 93 if ao[0] > 0 { flag = 1 } 94 if ls_thresh > 0 { if ls >= ls_thresh { flag = 1 } } 95 reason[2] = flag 96 return flag 97} 98// rollup across all effective ballots. out[0]=clean out[1]=flagged out[2]=attention-failers out[3]=straightliners. 99func sq_rollup(vout: *i64, lout: *i64, nb: i64, checks: *u8, sqids: *i64, nsq: i64, ls_thresh: i64, out: *i64) -> i64 { 100 out[0] = 0 101 out[1] = 0 102 out[2] = 0 103 out[3] = 0 104 let reason: *i64 = sys_mmap(32) as *i64 105 var i: i64 = 0 106 while i < nb { 107 let f: i64 = sq_ballot_careless(vout[i] as *u8, lout[i], checks, sqids, nsq, ls_thresh, reason) 108 if f == 1 { out[1] = out[1] + 1 } else { out[0] = out[0] + 1 } 109 if reason[0] > 0 { out[2] = out[2] + 1 } 110 if ls_thresh > 0 { if reason[1] >= ls_thresh { out[3] = out[3] + 1 } } 111 i = i + 1 112 } 113 return out[1] 114}