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}