code wiki / (root) / nx_analyst_causal_gate.nx

nx_analyst_causal_gate.nx source

↩ module page · 149 lines · 7827 B

1// nx_analyst_causal_gate.nx -- F1005. The role logic is trivial to assert; what this gate really has to 2// prove is WHY it must exist. So the centre of it is two EMPIRICAL demonstrations on real generated data, 3// where the identical operation -- "control for a third column" -- is right once and catastrophic twice: 4// T6 COLLIDER: two INDEPENDENT columns acquire a strong NEGATIVE partial correlation purely by 5// conditioning on their sum. The association is manufactured out of nothing. 6// T7 MEDIATOR: a real, strong x->m->y effect is driven toward ZERO by conditioning on m. 7// T8 CONFOUNDER: the same operation is CORRECT, collapsing a spurious link. 8// F1004's ai_confound_scan picks whichever control weakens the relationship most -- which in T6 and T7 is 9// exactly the wrong column. That is the defect this organ exists to gate. 10// D001-compliant verdict via nx_gate_verdict. license_tier: ORIGINAL expect_exit: 0 11import "nx_gate_verdict.nx" 12import "nx_analyst_causal.nx" 13 14const CG_N: i64 = 60 15 16func cg_eq(a: i64, b: i64) -> i64 { if a == b { return 1 } return 0 } 17func cg_has(hay: *u8, needle: *u8) -> i64 { 18 var hn: i64 = 0 19 while hay[hn] != (0 as u8) { hn = hn + 1 } 20 var nl: i64 = 0 21 while needle[nl] != (0 as u8) { nl = nl + 1 } 22 var i: i64 = 0 23 while i + nl <= hn { 24 var j: i64 = 0 25 var eq: i64 = 1 26 while j < nl { if hay[i+j] != needle[j] { eq = 0 } j = j + 1 } 27 if eq == 1 { return 1 } 28 i = i + 1 29 } 30 return 0 31} 32 33func main() -> i64 { 34 let ctr: *i64 = gv_ctr() 35 gv_head("nx_analyst_causal_gate -- when is controlling for a third column right, and when does it lie?" as *u8) 36 37 // ---- role logic -------------------------------------------------------------------------------- 38 let roles: *i64 = sys_mmap(8 * 4) as *i64 39 roles[0] = AC_ROLE_UNKNOWN 40 roles[1] = AC_ROLE_UNKNOWN 41 roles[2] = AC_ROLE_UNKNOWN 42 roles[3] = AC_ROLE_UNKNOWN 43 gv_check("T1 nothing declared -> ASSOCIATION ONLY (the default is never a causal claim)" as *u8, cg_eq(ac_verdict(roles, 4, 0, 1), AC_ASSOCIATION_ONLY), ctr) 44 roles[1] = AC_ROLE_CONFOUNDER 45 gv_check("T2 adjusting for a declared CONFOUNDER is licensed" as *u8, cg_eq(ac_verdict(roles, 4, 0, 1), AC_ADJUSTED), ctr) 46 roles[2] = AC_ROLE_COLLIDER 47 gv_check("T3 adjusting for a declared COLLIDER is REFUSED (it would induce the association)" as *u8, cg_eq(ac_verdict(roles, 4, 0, 2), AC_REFUSED_COLLIDER), ctr) 48 roles[3] = AC_ROLE_MEDIATOR 49 gv_check("T4 adjusting for a declared MEDIATOR is REFUSED (it would erase the real effect)" as *u8, cg_eq(ac_verdict(roles, 4, 0, 3), AC_REFUSED_MEDIATOR), ctr) 50 let aset: *i64 = sys_mmap(8 * 8) as *i64 51 var t5: i64 = 1 52 if ac_adjustment_set(roles, 4, 0, aset) != 1 { t5 = 0 } 53 if aset[0] != 1 { t5 = 0 } 54 gv_check("T5 the adjustment set holds ONLY the confounder -- collider and mediator excluded" as *u8, t5, ctr) 55 let ex: *u8 = sys_mmap(1024) 56 ac_explain(AC_ASSOCIATION_ONLY, ex) 57 gv_check("T5b the undeclared wording refuses a causal reading in words, not just in a code" as *u8, cg_has(ex, "ASSOCIATION ONLY" as *u8), ctr) 58 59 // ---- T6 COLLIDER: an association MANUFACTURED from two independent columns -------------------- 60 // x and y are independent draws; c = x + y is a common EFFECT of both. Conditioning on c must create 61 // a strong NEGATIVE partial correlation between causes that have no relationship whatsoever. 62 let x: *i64 = sys_mmap(8 * CG_N) as *i64 63 let y: *i64 = sys_mmap(8 * CG_N) as *i64 64 let c: *i64 = sys_mmap(8 * CG_N) as *i64 65 var s: i64 = 12345 66 var i: i64 = 0 67 while i < CG_N { 68 s = (s * 1103515245 + 12345) & 0x7fffffff 69 x[i] = s % 1000 70 s = (s * 1103515245 + 12345) & 0x7fffffff 71 y[i] = s % 1000 72 c[i] = x[i] + y[i] 73 i = i + 1 74 } 75 let r_xy: i64 = am_pearson_milli(x, y, CG_N) 76 let r_xc: i64 = am_pearson_milli(x, c, CG_N) 77 let r_yc: i64 = am_pearson_milli(y, c, CG_N) 78 let p_xy_c: i64 = ai_partial_milli(r_xy, r_xc, r_yc) 79 var arxy: i64 = r_xy 80 if arxy < 0 { arxy = 0 - arxy } 81 var t6: i64 = 1 82 if arxy > 300 { t6 = 0 } // x and y really are ~independent 83 if p_xy_c > (0 - 400) { t6 = 0 } // conditioning on their sum creates a strong NEGATIVE link 84 gv_check("T6 COLLIDER: independent x,y (|r|<300) acquire partial r < -400 by conditioning on x+y -- the association is MANUFACTURED" as *u8, t6, ctr) 85 86 // ---- T7 MEDIATOR: a real effect ERASED -------------------------------------------------------- 87 // x -> m -> y. The x->y effect is real and travels entirely through m, so conditioning on m must 88 // drive it to ~0. An analyst that "controls for whatever weakens it most" would report no effect. 89 let mx: *i64 = sys_mmap(8 * CG_N) as *i64 90 let mm: *i64 = sys_mmap(8 * CG_N) as *i64 91 let my: *i64 = sys_mmap(8 * CG_N) as *i64 92 i = 0 93 while i < CG_N { 94 let v: i64 = i + 1 95 // noise must be a real fraction of the signal. The first version used (i%7) and (i%5), which made 96 // the chain almost deterministic: r(y,m) rounded to exactly 1000, the denominator went to zero and 97 // ai_partial_milli correctly answered UNDEFINED. The organ was right and the FIXTURE was degenerate 98 // -- a reminder that a "too clean" test dataset can fail a gate for reasons that are not the code. 99 mx[i] = v 100 mm[i] = 4 * v + (i % 13) * 5 101 my[i] = 3 * mm[i] + (i % 17) * 12 102 i = i + 1 103 } 104 let r_xy2: i64 = am_pearson_milli(mx, my, CG_N) 105 let r_xm: i64 = am_pearson_milli(mx, mm, CG_N) 106 let r_ym: i64 = am_pearson_milli(my, mm, CG_N) 107 let p_xy_m: i64 = ai_partial_milli(r_xy2, r_xm, r_ym) 108 var ap: i64 = p_xy_m 109 if ap < 0 { ap = 0 - ap } 110 var t7: i64 = 1 111 if r_xy2 < 900 { t7 = 0 } // the total effect is genuinely strong 112 if ap > 400 { t7 = 0 } // conditioning on the mediator collapses it 113 gv_check("T7 MEDIATOR: a real r>900 effect collapses to |partial|<400 through its own mediator -- controlling ERASES it" as *u8, t7, ctr) 114 115 // ---- T8 CONFOUNDER: the SAME operation, and here it is correct -------------------------------- 116 let zz: *i64 = sys_mmap(8 * CG_N) as *i64 117 let zx: *i64 = sys_mmap(8 * CG_N) as *i64 118 let zy: *i64 = sys_mmap(8 * CG_N) as *i64 119 i = 0 120 while i < CG_N { 121 let v: i64 = i + 1 122 zz[i] = v 123 zx[i] = 3 * v + (i % 11) * 2 124 zy[i] = 5 * v + (i % 13) * 3 125 i = i + 1 126 } 127 let r_ab: i64 = am_pearson_milli(zx, zy, CG_N) 128 let r_az: i64 = am_pearson_milli(zx, zz, CG_N) 129 let r_bz: i64 = am_pearson_milli(zy, zz, CG_N) 130 let p_ab_z: i64 = ai_partial_milli(r_ab, r_az, r_bz) 131 var ap2: i64 = p_ab_z 132 if ap2 < 0 { ap2 = 0 - ap2 } 133 var t8: i64 = 1 134 if r_ab < 900 { t8 = 0 } 135 if ap2 > 300 { t8 = 0 } 136 gv_check("T8 CONFOUNDER: the identical operation correctly collapses a SPURIOUS r>900 to |partial|<300" as *u8, t8, ctr) 137 138 // ---- the point of the whole organ ------------------------------------------------------------- 139 // T6, T7 and T8 all ran "control for a third column" and got a big drop. Only in T8 was that right. 140 // Nothing in the numbers distinguishes them -- which is exactly why an undeclared graph caps the 141 // claim at ASSOCIATION and why picking the most-shrinking control is unsound. 142 var t9: i64 = 0 143 if p_xy_c < 0 { if ap < 400 { if ap2 < 300 { t9 = 1 } } } 144 gv_check("T9 all three cases show a large drop under control, and ONLY the confounder case earned it" as *u8, t9, ctr) 145 146 let rc: i64 = gv_verdict("ANALYST-CAUSAL-GATE" as *u8, ctr, "roles are declared, never inferred from correlation; colliders and mediators are refused" as *u8) 147 sys_exit(rc) 148 return rc 149}