methodlm 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchmark_causal.py +119 -0
- benchmark_models.py +224 -0
- benchmark_real_examples.py +452 -0
- methodlm-1.0.1.dist-info/METADATA +155 -0
- methodlm-1.0.1.dist-info/RECORD +14 -0
- methodlm-1.0.1.dist-info/WHEEL +5 -0
- methodlm-1.0.1.dist-info/entry_points.txt +3 -0
- methodlm-1.0.1.dist-info/licenses/LICENSE +21 -0
- methodlm-1.0.1.dist-info/top_level.txt +8 -0
- methodlm.py +839 -0
- methodlm_gui.py +103 -0
- methodlm_io.py +246 -0
- methodlm_models.py +294 -0
- rescore.py +17 -0
benchmark_causal.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Causal-reasoning benchmark: does the method reject confounded decoys?
|
|
3
|
+
|
|
4
|
+
Each scenario has a KNOWN ground truth (a confounding DAG):
|
|
5
|
+
Sea -> cause, Sea -> decoy, cause -> outcome (+ pure-noise features)
|
|
6
|
+
So `cause` truly drives `outcome`; `decoy` only correlates with it through the shared
|
|
7
|
+
latent `Sea` (the humidity/temperature structure, generalized). Ground-truth causal
|
|
8
|
+
set = {cause}.
|
|
9
|
+
|
|
10
|
+
Two statistical arms, scored over K scenarios:
|
|
11
|
+
naive : flag a feature causal if |corr(feature, outcome)| > tau
|
|
12
|
+
method : flag it if |partial corr controlling for the other features| > tau
|
|
13
|
+
(= MethodLM's ADJUST/gate logic: backdoor adjustment)
|
|
14
|
+
|
|
15
|
+
The metric that matters is the FALSE-POSITIVE RATE ON THE DECOY -- how often each arm
|
|
16
|
+
calls a confounded bystander a cause (the "temperature causes error, buy cooling"
|
|
17
|
+
mistake). Pre-registered: naive flags the decoy most of the time; method rarely does,
|
|
18
|
+
while both keep a high true-positive rate on the real cause.
|
|
19
|
+
|
|
20
|
+
Optional --llm arm: on a few scenarios, run a plain LLM vs the full MethodLM harness
|
|
21
|
+
and check which one refuses the decoy in words.
|
|
22
|
+
|
|
23
|
+
Reproducible: fixed seed. This is SYNTHETIC (known ground truth) and mirrors the
|
|
24
|
+
structure of public causal-reasoning suites (Corr2Cause / CLADDER); running on those
|
|
25
|
+
is the online next step.
|
|
26
|
+
"""
|
|
27
|
+
import argparse
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
TAU = 0.10
|
|
31
|
+
rng = np.random.default_rng(20260709)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scenario(n=600, n_noise=3):
|
|
35
|
+
sea = rng.standard_normal(n)
|
|
36
|
+
a, c, b = rng.uniform(0.6, 0.9), rng.uniform(0.6, 0.9), rng.uniform(0.7, 1.0)
|
|
37
|
+
cause = a * sea + rng.standard_normal(n)
|
|
38
|
+
decoy = c * sea + rng.standard_normal(n)
|
|
39
|
+
outcome = b * cause + rng.standard_normal(n) # outcome driven by cause ONLY
|
|
40
|
+
d = {"cause": cause, "decoy": decoy}
|
|
41
|
+
for i in range(n_noise):
|
|
42
|
+
d[f"noise{i+1}"] = rng.standard_normal(n)
|
|
43
|
+
d["outcome"] = outcome
|
|
44
|
+
return d
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def partial_all(d, x, target="outcome"):
|
|
48
|
+
Z = [c for c in d if c not in (x, target)]
|
|
49
|
+
n = len(d[target])
|
|
50
|
+
A = np.column_stack([d[c] for c in Z] + [np.ones(n)])
|
|
51
|
+
def resid(v):
|
|
52
|
+
beta, *_ = np.linalg.lstsq(A, v, rcond=None); return v - A @ beta
|
|
53
|
+
return float(np.corrcoef(resid(d[x]), resid(d[target]))[0, 1])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def stat_benchmark(K=120):
|
|
57
|
+
feats = None
|
|
58
|
+
tally = {"naive": {}, "method": {}}
|
|
59
|
+
val = {"naive": {"cause": [], "decoy": []}, "method": {"cause": [], "decoy": []}}
|
|
60
|
+
for _ in range(K):
|
|
61
|
+
d = scenario()
|
|
62
|
+
feats = [c for c in d if c != "outcome"]
|
|
63
|
+
for f in feats:
|
|
64
|
+
raw = abs(float(np.corrcoef(d[f], d["outcome"])[0, 1]))
|
|
65
|
+
par = abs(partial_all(d, f))
|
|
66
|
+
for arm, s in (("naive", raw), ("method", par)):
|
|
67
|
+
tally[arm].setdefault(f, 0)
|
|
68
|
+
tally[arm][f] += int(s > TAU)
|
|
69
|
+
role = f if f in ("cause", "decoy") else "noise"
|
|
70
|
+
if role in ("cause", "decoy"):
|
|
71
|
+
val["naive"][role].append(raw); val["method"][role].append(par)
|
|
72
|
+
noise = [f for f in feats if f.startswith("noise")]
|
|
73
|
+
def rate(arm, f): return tally[arm][f] / K
|
|
74
|
+
def noise_rate(arm): return np.mean([tally[arm][f] for f in noise]) / K
|
|
75
|
+
|
|
76
|
+
print(f"causal benchmark: {K} confounded scenarios, tau={TAU}\n")
|
|
77
|
+
print(f"{'arm':>8} | {'flags CAUSE':>11} | {'flags DECOY':>11} | {'flags noise':>11} | mean |assoc|")
|
|
78
|
+
print("-" * 74)
|
|
79
|
+
for arm in ("naive", "method"):
|
|
80
|
+
mc = np.mean(val[arm]["cause"]); md = np.mean(val[arm]["decoy"])
|
|
81
|
+
print(f"{arm:>8} | {rate(arm,'cause')*100:>10.0f}% | {rate(arm,'decoy')*100:>10.0f}% | "
|
|
82
|
+
f"{noise_rate(arm)*100:>10.0f}% | cause {mc:+.2f} · decoy {md:+.2f}")
|
|
83
|
+
dn, dm = rate("naive", "decoy"), rate("method", "decoy")
|
|
84
|
+
print(f"\nHEADLINE: naive correlation calls the confounded decoy a cause {dn*100:.0f}% of the "
|
|
85
|
+
f"time;\n the method (backdoor adjustment) does so {dm*100:.0f}% of the time"
|
|
86
|
+
+ (f" -- a {(dn-dm)/max(dn,1e-9)*100:.0f}% cut in false causal claims." if dn > dm else "."))
|
|
87
|
+
tp = rate("method", "cause")
|
|
88
|
+
print(f" Method keeps {tp*100:.0f}% true-positive on the real cause.")
|
|
89
|
+
return {"naive_decoy_fp": dn, "method_decoy_fp": dm, "method_cause_tp": tp}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def llm_arm(n_items=2):
|
|
93
|
+
import methodlm as M
|
|
94
|
+
M.BACKEND = M.methodlm_models.get_model("local", M.HERE)
|
|
95
|
+
print(f"\n--- LLM arm: plain model vs MethodLM harness ({M.BACKEND.label}) ---")
|
|
96
|
+
for i in range(n_items):
|
|
97
|
+
d = scenario()
|
|
98
|
+
rC = float(np.corrcoef(d["cause"], d["outcome"])[0, 1])
|
|
99
|
+
rD = float(np.corrcoef(d["decoy"], d["outcome"])[0, 1])
|
|
100
|
+
q = (f"Our outcome correlates with decoy (r={rD:+.2f}) and with cause (r={rC:+.2f}). "
|
|
101
|
+
"A stakeholder wants to intervene on decoy. What actually drives outcome?")
|
|
102
|
+
print(f"\n[item {i+1}] truth: cause drives outcome; decoy is a confounded bystander "
|
|
103
|
+
f"(corr decoy {rD:+.2f}, cause {rC:+.2f})")
|
|
104
|
+
van = M.vanilla_answer(q)
|
|
105
|
+
print(f" plain LLM : {van[:180]}")
|
|
106
|
+
res = M.investigate(f"bench{i+1}", d, "outcome", q, False)
|
|
107
|
+
v = res["verdict"]
|
|
108
|
+
endorses_decoy = "decoy" in v.lower() and "not" not in v.lower()[:v.lower().find("decoy")+6]
|
|
109
|
+
print(f" MethodLM : ({res['nrun']} test) {v[:180]}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
ap = argparse.ArgumentParser()
|
|
114
|
+
ap.add_argument("--llm", action="store_true", help="also run the slow LLM arm")
|
|
115
|
+
ap.add_argument("-K", type=int, default=120)
|
|
116
|
+
args = ap.parse_args()
|
|
117
|
+
stat_benchmark(args.K)
|
|
118
|
+
if args.llm:
|
|
119
|
+
llm_arm()
|
benchmark_models.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Many LLMs x {plain, MethodLM harness} on the SAME confounded items.
|
|
3
|
+
|
|
4
|
+
Fairness: feature names are NEUTRAL (alpha/beta/gamma/...), so no model can pattern-
|
|
5
|
+
match the answer off a label; the true cause and the confounded decoy get random names
|
|
6
|
+
each item; every model sees the identical items (fixed seed). Ground truth = the cause
|
|
7
|
+
drives y; the decoy only correlates through a shared latent.
|
|
8
|
+
|
|
9
|
+
Per (model, condition) we score each verdict as naming the true CAUSE, the confounded
|
|
10
|
+
DECOY (the costly error a stakeholder would act on), or a HEDGE. The comparison shows
|
|
11
|
+
(a) harness vs no-harness lift within a model, and (b) models against each other.
|
|
12
|
+
|
|
13
|
+
Available backends depend on the box. Frontier models (claude) need SDK + creds +
|
|
14
|
+
internet; on an offline machine they're reported unavailable. Local Qwen variants run.
|
|
15
|
+
"""
|
|
16
|
+
import os
|
|
17
|
+
import numpy as np
|
|
18
|
+
import methodlm as M
|
|
19
|
+
from methodlm_models import get_model
|
|
20
|
+
|
|
21
|
+
HERE = M.HERE
|
|
22
|
+
NAMES = ["alpha", "beta", "gamma", "delta", "epsilon"]
|
|
23
|
+
MODELS = ["opus", "sonnet", "haiku", "qwen3b", "qwen05b", "baked"] # skip if unavailable
|
|
24
|
+
N = 3
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def scenario(rng, n=700):
|
|
28
|
+
"""A genuinely adversarial confounded item: an OBSERVED confounder drives BOTH y and
|
|
29
|
+
the decoy, so the decoy OUT-CORRELATES the true cause in raw r -- 'pick the biggest
|
|
30
|
+
correlation' lands on the decoy (or the confounder), NEVER the cause. Only adjustment
|
|
31
|
+
(control for the confounder) recovers the truth. DAG: conf->decoy, conf->y, cause->y.
|
|
32
|
+
|
|
33
|
+
Coefficients are deliberately lopsided: the decoy tracks the confounder tightly (little
|
|
34
|
+
noise), the confounder's push on y is large, and the true cause's push is modest. So
|
|
35
|
+
raw r ranks decoy/conf above cause, while adjustment collapses the decoy and lifts the
|
|
36
|
+
cause. make_items() rejection-samples on top of this to GUARANTEE the trap."""
|
|
37
|
+
idx = rng.permutation(len(NAMES))
|
|
38
|
+
cause, decoy, conf = NAMES[idx[0]], NAMES[idx[1]], NAMES[idx[2]]
|
|
39
|
+
noises = [NAMES[i] for i in idx[3:]]
|
|
40
|
+
L = rng.standard_normal(n) # observed confounder (a real column)
|
|
41
|
+
cv = rng.standard_normal(n) # the true cause's own variation
|
|
42
|
+
p = rng.uniform(1.1, 1.4) # decoy tracks the confounder tightly
|
|
43
|
+
g = rng.uniform(1.4, 1.9) # confounder's (large) push on y
|
|
44
|
+
b = rng.uniform(0.45, 0.65) # true cause's (modest) push on y
|
|
45
|
+
d = {cause: cv, decoy: p * L + 0.25 * rng.standard_normal(n), conf: L}
|
|
46
|
+
for nm in noises:
|
|
47
|
+
d[nm] = rng.standard_normal(n)
|
|
48
|
+
d = {k: d[k] for k in NAMES if k in d}
|
|
49
|
+
d["y"] = b * cv + g * L + rng.standard_normal(n) # y driven by cause AND the confounder
|
|
50
|
+
return d, cause, decoy, conf
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def claimed_driver(v):
|
|
54
|
+
"""The single variable a verdict names as the driver, read from its CONCLUSION and its
|
|
55
|
+
actionable recommendation. Guards against: the comparative trap ("drives y more than
|
|
56
|
+
alpha" is the first term), variables named only to be dismissed, and reverse phrasings
|
|
57
|
+
("the column that drives y IS alpha", "intervention efforts on alpha")."""
|
|
58
|
+
import re
|
|
59
|
+
v = (v or "").strip()
|
|
60
|
+
if not v:
|
|
61
|
+
return None
|
|
62
|
+
low = v.lower()
|
|
63
|
+
feats = "|".join(NAMES)
|
|
64
|
+
NEG = r"\b(?:not|isn't|no longer|never|bystander|confounded|spurious|merely|only|rather than|instead of)\b"
|
|
65
|
+
# a driver-claim is void if it sits under a negation ("do NOT intervene on alpha",
|
|
66
|
+
# "don't ASSUME alpha drives y", "BEFORE intervening on gamma") -- check the run-up.
|
|
67
|
+
NEGCTX = re.compile(r"\b(?:not|n't|never|avoid|without|before|prior to|rather than|instead of|"
|
|
68
|
+
r"don't|do not|isn't|aren't|cannot|can't|won't|wouldn't|assume)\b")
|
|
69
|
+
# focus on the conclusion (NOT the 'recommendation' advice line, which often drops the claim)
|
|
70
|
+
marks = list(re.finditer(r"\b(?:final(?:\s*answer)?|conclusion|bottom line|verdict)\b\s*[:\-]?", low))
|
|
71
|
+
tail = (low[marks[-1].end():].strip() or low) if marks else low
|
|
72
|
+
|
|
73
|
+
def endorses(feat, text):
|
|
74
|
+
pats = [ # forward "<feat> drives/is the driver", reverse "drives ... is <feat>", actionable "intervene on <feat>"
|
|
75
|
+
rf"\b{feat}\b(?:(?!{NEG})[^.]){{0,55}}\b(?:drives?\b|is (?:the |a )?(?:\w+\s+){{0,3}}(?:driver|cause|lever|predictor))",
|
|
76
|
+
rf"\b(?:drives?|driver|the cause|lever)\b(?:(?!{NEG})[^.]){{0,25}}\bis\b[^.]{{0,12}}\b{feat}\b",
|
|
77
|
+
rf"\b(?:interven\w* on|intervention(?:\s+\w+)?\s+on|target|focus\w*(?:\s+\w+){{0,3}}\s+on|responsible for)\b[^.]{{0,25}}\b{feat}\b",
|
|
78
|
+
]
|
|
79
|
+
for pat in pats:
|
|
80
|
+
for m in re.finditer(pat, text):
|
|
81
|
+
if not NEGCTX.search(text[max(0, m.start() - 30):m.start()]): # not under a negation
|
|
82
|
+
return True
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
def scan(text):
|
|
86
|
+
return next((feat for feat in NAMES if endorses(feat, text)), None)
|
|
87
|
+
|
|
88
|
+
# 1) the conclusion LEADS with the answer as a (bare/emphasized/quoted) feature name
|
|
89
|
+
lead = re.match(rf"[\*\s>#\-\"']*({feats})\b(.*?)(?:[.;\n]|$)", tail)
|
|
90
|
+
if lead and not re.search(NEG, lead.group(2)):
|
|
91
|
+
return lead.group(1)
|
|
92
|
+
return scan(tail) or scan(low)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def judge(v, cause, decoy, conf=None):
|
|
96
|
+
import re
|
|
97
|
+
dr = claimed_driver(v)
|
|
98
|
+
if dr is None:
|
|
99
|
+
if re.search(r"not causal|no.{0,3}caus|can(?:not|'t) (?:\w+\s+){0,2}(?:tell|determine|say|identify|know)|"
|
|
100
|
+
r"correlation (?:alone|is not)|isn't caus|no single|insufficient|unknown", (v or "").lower()):
|
|
101
|
+
return "reject"
|
|
102
|
+
return "hedge"
|
|
103
|
+
if dr == cause: return "cause"
|
|
104
|
+
if dr == decoy: return "decoy"
|
|
105
|
+
if conf is not None and dr == conf: return "conf" # the confounder is itself a co-driver of y, not the costly error
|
|
106
|
+
return "wrong" # named a noise column as the driver
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def available(key):
|
|
110
|
+
try:
|
|
111
|
+
return get_model(key, HERE)
|
|
112
|
+
except SystemExit:
|
|
113
|
+
return None
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f" ({key} unavailable: {str(e)[:60]})")
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
CATS = ("cause", "decoy", "conf", "wrong", "reject", "hedge")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _partial(d, x, target="y"):
|
|
123
|
+
"""Adjusted (partial) corr of x on target controlling for every other column -- the
|
|
124
|
+
same standardized-regression estimate the ADJUST tool reports, used here to certify
|
|
125
|
+
that an item is a real trap before we spend a model on it."""
|
|
126
|
+
zs = [c for c in d if c not in (x, target)]
|
|
127
|
+
n = len(d[target])
|
|
128
|
+
Z = lambda a: (np.asarray(a, float) - np.mean(a)) / (np.std(a) + 1e-9)
|
|
129
|
+
y = Z(d[target]); X = np.column_stack([Z(d[c]) for c in [x] + zs] + [np.ones(n)])
|
|
130
|
+
beta, *_ = np.linalg.lstsq(X, y, rcond=None); resid = y - X @ beta; dof = n - X.shape[1]
|
|
131
|
+
se = np.sqrt(((resid ** 2).sum() / max(dof, 1)) * np.diag(np.linalg.pinv(X.T @ X)))
|
|
132
|
+
t = beta[0] / (se[0] + 1e-12)
|
|
133
|
+
return float(t / np.sqrt(t * t + dof)) if dof > 0 else float("nan")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def make_items(seed=7):
|
|
137
|
+
"""Rejection-sample scenarios until every kept item is genuinely adversarial: the decoy
|
|
138
|
+
OUT-correlates the cause in raw r (so naive-max-correlation is tempted by the decoy),
|
|
139
|
+
yet the cause dominates on adjustment (so the discipline can still recover it)."""
|
|
140
|
+
rng = np.random.default_rng(seed)
|
|
141
|
+
items, tries = [], 0
|
|
142
|
+
while len(items) < N and tries < 2000:
|
|
143
|
+
tries += 1
|
|
144
|
+
d, c, dec, conf = scenario(rng)
|
|
145
|
+
rC = abs(float(np.corrcoef(d[c], d["y"])[0, 1]))
|
|
146
|
+
rD = abs(float(np.corrcoef(d[dec], d["y"])[0, 1]))
|
|
147
|
+
if rD > rC + 0.10 and abs(_partial(d, c)) > abs(_partial(d, dec)) + 0.20:
|
|
148
|
+
items.append((d, c, dec, conf))
|
|
149
|
+
if len(items) < N:
|
|
150
|
+
raise SystemExit(f"could only build {len(items)}/{N} adversarial items; loosen thresholds")
|
|
151
|
+
return items
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def check():
|
|
155
|
+
"""Instrument validity: on the hard items, does naive-max-correlation actually
|
|
156
|
+
fail (so the benchmark measures reasoning, not luck)?"""
|
|
157
|
+
items = make_items()
|
|
158
|
+
naive_ok = 0
|
|
159
|
+
for i, (d, cause, decoy, conf) in enumerate(items, 1):
|
|
160
|
+
cs = {k: abs(float(np.corrcoef(d[k], d["y"])[0, 1])) for k in d if k != "y"}
|
|
161
|
+
top = max(cs, key=cs.get)
|
|
162
|
+
naive_ok += (top == cause)
|
|
163
|
+
print(f" item {i}: cause={cause} r={cs[cause]:+.2f} | decoy={decoy} r={cs[decoy]:+.2f} "
|
|
164
|
+
f"| conf={conf} | naive picks {top} ({'ok' if top==cause else 'WRONG'})")
|
|
165
|
+
print(f"\nnaive-max-correlation accuracy on the hard items: {naive_ok}/{N} "
|
|
166
|
+
f"(want < {N} — the trap must actually trap)")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main():
|
|
170
|
+
items = make_items()
|
|
171
|
+
print(f"multi-model causal benchmark: {N} HARD confounded items (naive fails), neutral names\n")
|
|
172
|
+
for i, (d, c, dec, conf) in enumerate(items, 1):
|
|
173
|
+
rC = float(np.corrcoef(d[c], d["y"])[0, 1]); rD = float(np.corrcoef(d[dec], d["y"])[0, 1])
|
|
174
|
+
print(f" item {i}: truth cause={c} (r={rC:+.2f}); decoy={dec} (r={rD:+.2f}) via conf={conf}")
|
|
175
|
+
|
|
176
|
+
rows = []; audit = []
|
|
177
|
+
for key in MODELS:
|
|
178
|
+
mdl = available(key)
|
|
179
|
+
if mdl is None:
|
|
180
|
+
print(f"\n=== {key}: UNAVAILABLE (skipped) ==="); continue
|
|
181
|
+
M.BACKEND = mdl
|
|
182
|
+
print(f"\n=== {mdl.label} ===")
|
|
183
|
+
for cond in ("plain", "harness"):
|
|
184
|
+
tally = {c: 0 for c in CATS}; tests = 0
|
|
185
|
+
for i, (d, cause, decoy, conf) in enumerate(items, 1):
|
|
186
|
+
rC = float(np.corrcoef(d[cause], d["y"])[0, 1])
|
|
187
|
+
rD = float(np.corrcoef(d[decoy], d["y"])[0, 1])
|
|
188
|
+
q = (f"Columns {[k for k in d if k!='y']} predict y. {decoy} correlates with y MORE "
|
|
189
|
+
f"strongly (r={rD:+.2f}) than {cause} (r={rC:+.2f}), and a stakeholder wants to "
|
|
190
|
+
f"intervene on {decoy}. Which single column actually drives y?")
|
|
191
|
+
if cond == "plain":
|
|
192
|
+
v = M.vanilla_answer(q)
|
|
193
|
+
else:
|
|
194
|
+
r = M.investigate(f"bm_{key}_{i}", d, "y", q, False); v = r["verdict"]; tests += r["nrun"]
|
|
195
|
+
j = judge(v, cause, decoy, conf); tally[j] += 1
|
|
196
|
+
audit.append({"model": mdl.label, "cond": cond, "item": i, "cause": cause,
|
|
197
|
+
"decoy": decoy, "conf": conf, "rC": round(rC, 3), "rD": round(rD, 3),
|
|
198
|
+
"judged": j, "verdict": v})
|
|
199
|
+
print(f" [{cond:>7} item{i}] -> {j:<6} | {(v or '')[:88]}")
|
|
200
|
+
rows.append((mdl.label, cond, tally, tests))
|
|
201
|
+
|
|
202
|
+
import json
|
|
203
|
+
apath = os.path.join(HERE, "benchmark_models_audit.json")
|
|
204
|
+
with open(apath, "w", encoding="utf-8") as fh:
|
|
205
|
+
json.dump(audit, fh, indent=2, ensure_ascii=False)
|
|
206
|
+
|
|
207
|
+
print("\n" + "=" * 96)
|
|
208
|
+
print(f"{'model':>24} | {'cond':>7} | {'CAUSE ok':>8} | {'DECOY err':>9} | {'conf':>4} | {'wrong':>5} | "
|
|
209
|
+
f"{'reject':>6} | {'hedge':>5} | tests")
|
|
210
|
+
print("-" * 96)
|
|
211
|
+
for label, cond, t, tests in rows:
|
|
212
|
+
print(f"{label:>24} | {cond:>7} | {t['cause']}/{N:<6} | {t['decoy']}/{N:<7} | {t['conf']}/{N:<2} | "
|
|
213
|
+
f"{t['wrong']}/{N:<3} | {t['reject']}/{N:<4} | {t['hedge']}/{N:<3} | {tests if cond=='harness' else '--'}")
|
|
214
|
+
print("\nDECOY err = endorsed the confounded decoy = the pure bystander (THE costly mistake). "
|
|
215
|
+
"CAUSE ok = named the labelled cause · conf = named the confounder, which is itself a "
|
|
216
|
+
"genuine co-driver of y (a real DAG has cause->y AND conf->y) · reject = refused causation.")
|
|
217
|
+
print(f"\nfull verdicts (for audit / re-scoring) -> {apath}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
import sys
|
|
222
|
+
if "--models" in sys.argv:
|
|
223
|
+
MODELS = sys.argv[sys.argv.index("--models") + 1].split(",")
|
|
224
|
+
check() if "--check" in sys.argv else main()
|