1"""F4-conf-01: extract printed treatment effects / uncertainty from two source extracts.2Parses data/sources/*.txt, prints every matched source line, derives two quantities,3runs internal consistency checks, writes out/results.json. Deterministic; no randomness."""4import json, math, re, os5from scipy.stats import norm67ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))8SRC = os.path.join(ROOT, "data", "sources")9DCR_ID, RI_ID = "arxiv-2609.27732v1", "arxiv-2606.14621v1"10Z = 1.96 # normal critical value for 95% two-sided intervals1112def read(name):13 with open(os.path.join(ROOT, "data", name), encoding="utf-8") as f:14 return f.read()1516def nlines(txt): # equals `wc -l` (newline count)17 return txt.count("\n")1819dcr_txt = read(f"sources/{DCR_ID}.txt"); ri_txt = read(f"sources/{RI_ID}.txt"); readme = read("README.md")20dcr = dcr_txt.split("\n"); ri = ri_txt.split("\n")2122def block(lines, start_pat, end_pat):23 s = next(i for i, l in enumerate(lines) if l.startswith(start_pat))24 e = next(i for i in range(s + 1, len(lines)) if lines[i].startswith(end_pat))25 return s, e2627def cells(line):28 return [c.strip() for c in line.split("|")]2930def num(cell):31 """Return (printed string without stars/parentheses, float, stars)."""32 m = re.match(r"^\(?(-?[\d,]*\.?\d+)\)?(\**)$", cell.strip())33 s = m.group(1).replace(",", "")34 return s, float(s), m.group(2)3536def reg_table(start_pat, end_pat):37 s, e = block(dcr, start_pat, end_pat)38 rows = [(s + i, l) for i, l in enumerate(dcr[s:e]) if l.strip()]39 header = None; out = {}; last = None40 for ln, l in rows[1:]:41 c = cells(l)42 if header is None:43 header = c; header_ln = ln; continue44 label = c[0]45 if label == "" and last is not None: # SE line belongs to previous coef row46 out[last]["se"] = (ln + 1, l, c[1:])47 else:48 out[label] = {"coef": (ln + 1, l, c[1:])}; last = label49 return header_ln + 1, header, out5051def get(tab, row, col, kind="coef"):52 hln, header, rows = tab53 j = header.index(col) - 154 ln, line, c = rows[row][kind]55 print(f" [line {ln}] {line.strip()} -> row '{row}', col '{col}' ({kind}): {c[j]}")56 return num(c[j])5758print(f"== {DCR_ID} Table 6")59t6 = reg_table("Table 6:", "Note:")60att5 = get(t6, "ATT", "5 km"); att5se = get(t6, "ATT", "5 km", "se")61att3 = get(t6, "ATT", "3 km"); att3se = get(t6, "ATT", "3 km", "se")62obs5 = get(t6, "Observations", "5 km")63print(f"== {DCR_ID} Table 7")64t7 = reg_table("Table 7:", "Note:")65nar3 = get(t7, "Narcotics", "3 km"); nar3se = get(t7, "Narcotics", "3 km", "se")66fat1 = get(t7, "Drug-related (fatal)", "1 km"); fat1se = get(t7, "Drug-related (fatal)", "1 km", "se")67hiv5 = get(t7, "HIV", "5 km"); hiv5se = get(t7, "HIV", "5 km", "se")68print(f"== {DCR_ID} Table A19")69ta19 = reg_table("Table A19:", "Note:")70a19 = get(ta19, "ATT", "CS-DR"); a19se = get(ta19, "ATT", "CS-DR", "se")7172# exact p-value for HIV 5 km: does the extract print one? search for p= near HIV73hiv_p_printed = re.search(r"HIV[^\n]*p\s*=\s*0\.\d+", dcr_txt) is not None74print(f" exact p-value printed for HIV ATT anywhere in {DCR_ID}: {hiv_p_printed}")7576# ---- RESEA Table 277print(f"== {RI_ID} Table 2")78s = next(i for i, l in enumerate(ri) if l.startswith("Outcome | Treatment | Control"))79hdr = cells(ri[s]); t2 = {}80for i in range(s + 1, len(ri)):81 if not ri[i].strip(): break82 c = cells(ri[i]); t2[c[0]] = (i + 1, ri[i], c)8384def ri_cell(row, col):85 ln, line, c = t2[row]; v = c[hdr.index(col)]86 print(f" [line {ln}] {line.strip()}\n -> row '{row}', col '{col}': {v}")87 return v8889def parse_effect(v):90 # simpler robust split91 pt, p, ci = [x.strip() for x in v.split(";")]92 est = re.sub(r"[^\d.\-]", "", pt.replace("-$", "-"))93 lo, hi = re.match(r"95% CI:\s*\[(-?[\d,.]+),\s*(-?[\d,.]+)\]", ci).groups()94 return est, p, lo.replace(",", ""), hi.replace(",", "")9596ATE = "Average treatment effect estimate"97w_est, w_p, w_lo, w_hi = parse_effect(ri_cell("Wage", ATE))98r_est, r_p, r_lo, r_hi = parse_effect(ri_cell("Reemployment", ATE))99u_est, u_p, u_lo, u_hi = parse_effect(ri_cell("Weeks on UI", ATE))100cm = ri_cell("Wage", "Control")101ctrl_wage = re.match(r"\$([\d,]+)", cm).group(1).replace(",", "")102w_se_printed = bool(re.search(r"\b(SE|s\.e\.|standard error)\b", t2["Wage"][1][len("Wage"):], re.I))103print(f" standard error printed for wage ATE: {w_se_printed}")104105def asnum(s):106 return int(s) if re.fullmatch(r"-?\d+", s) else float(s)107108# ---- derived quantities109ci_lo = round(att5[1] - Z * att5se[1], 3); ci_hi = round(att5[1] + Z * att5se[1], 3)110w_se = round((float(w_hi) - float(w_lo)) / (2 * Z), 1)111112# ---- consistency checks (reported as data issues where they fail)113checks = []114def star_ok(coef, se, stars):115 p = 2 * norm.sf(abs(coef / se))116 exp = "***" if p < .01 else "**" if p < .05 else "*" if p < .10 else ""117 return p, exp == stars118for name, (c, s_) in {"T6 5km": (att5, att5se), "T6 3km": (att3, att3se), "T7 Narc 3km": (nar3, nar3se),119 "T7 fatal 1km": (fat1, fat1se), "T7 HIV 5km": (hiv5, hiv5se), "A19 CS-DR": (a19, a19se)}.items():120 p, ok = star_ok(c[1], s_[1], c[2]); checks.append((name, round(p, 4), ok))121 print(f" star check {name}: z-based p={p:.4f}, stars '{c[2]}' consistent={ok}")122w_p_implied = 2 * norm.sf(float(w_est) / w_se)123w_mid = (float(w_lo) + float(w_hi)) / 2124print(f" wage ATE: CI midpoint={w_mid}, implied p (normal)={w_p_implied:.4f} vs printed {w_p}")125hiv_p_implied = 2 * norm.sf(abs(hiv5[1] / hiv5se[1]))126print(f" (not reported) normal-approx p for HIV 5km would be {hiv_p_implied:.3f}; NOT used as a result")127128data_issues = [129 {"issue": f"Same estimate (CS-DR, 5 km ATT -0.431) is printed with SE (0.210) in Table 6 but SE ({a19se[0]}) in Appendix Table A19 of {DCR_ID}.",130 "handling": "Keys name Table 6, so dcr_att_5km_se = 0.210 and the derived CI uses 0.210; A19 value not used."},131 {"issue": f"Control-group size inconsistent in {DCR_ID}: Tables 6/7 and Figure 6 note say 608 never-treated regions; Table A19 note says 640.",132 "handling": "Recorded only; no requested key depends on it."},133 {"issue": f"{DCR_ID} abstract says fatal hospitalisations 'fall by 35%' and narcotics 'rise by a comparable amount'; Table 7 5 km fatal ATT -0.354 log points corresponds to exp(-0.354)-1 = -29.8% (Section 6.1 says ~30%), and narcotics +0.390 to +47.7%. The abstract appears to read log points as percent.",134 "handling": "Recorded; values extracted as printed in Table 7 (log points), no conversion applied."},135 {"issue": f"{DCR_ID} text says 'HIV estimates are insignificant and close to zero', while the 5 km HIV ATT is -0.340 (SE 0.294), not small in magnitude though insignificant.",136 "handling": "Recorded; significance flag follows the table's stars (none => not significant at 10%)."},137 {"issue": f"{DCR_ID} prints no exact p-values, only significance stars.",138 "handling": "dcr_hiv_att_5km_p_value = null; no p-value was back-computed into the results."},139 {"issue": f"{RI_ID} running text reports the weeks-on-UI CI as magnitudes '[1.75 weeks, 2.24 weeks]' for a 'reduction', whereas Table 2 prints the signed interval [-2.24, -1.75].",140 "handling": "Used Table 2 signed values (lower bound -2.24 first)."},141 {"issue": f"Rendering defect in {RI_ID} text: 'p ¡ 0.001' (should read p < 0.001); Tables 2-3 were re-set by the extract editor.",142 "handling": "No effect on extracted values, which come from the re-set Table 2 whose values are stated to be verbatim."},143 {"issue": f"{RI_ID} prints no standard errors for ATEs, only p-values and 95% CIs; Table 2 caption says CIs follow 'the methodology described in the main text', which is omitted from the extract.",144 "handling": "SE derived as CI width / (2*1.96) assuming a symmetric normal-approximation interval; check: implied p = %.3f vs printed %s." % (w_p_implied, w_p)},145]146bad = [c for c in checks if not c[2]]147if bad:148 data_issues.append({"issue": "Star/SE mismatch: " + str(bad), "handling": "Recorded; printed values kept."})149150L = lambda src, t, r, c: f'{src} | Table {t} | row "{r}" | column "{c}"'151res = {152 "task_id": "F4-conf-01",153 "dcr_att_5km": asnum(att5[0]), "dcr_att_5km_se": asnum(att5se[0]),154 "dcr_att_3km": asnum(att3[0]), "dcr_att_3km_se": asnum(att3se[0]),155 "dcr_obs_5km": int(obs5[0]),156 "dcr_narcotics_att_3km": asnum(nar3[0]), "dcr_narcotics_att_3km_se": asnum(nar3se[0]),157 "dcr_fatal_att_1km": asnum(fat1[0]), "dcr_fatal_att_1km_se": asnum(fat1se[0]),158 "dcr_hiv_att_5km": asnum(hiv5[0]), "dcr_hiv_att_5km_se": asnum(hiv5se[0]),159 "dcr_hiv_att_5km_p_value": None,160 "dcr_hiv_att_5km_significant_10pct": "*" in hiv5[2],161 "dcr_att_5km_ci_low": ci_lo, "dcr_att_5km_ci_high": ci_hi,162 "ri_wage_ate": asnum(w_est), "ri_wage_ate_ci_low": asnum(w_lo), "ri_wage_ate_ci_high": asnum(w_hi),163 "ri_wage_ate_se_reported": w_se_printed, "ri_wage_ate_se_derived": w_se,164 "ri_control_mean_wage": asnum(ctrl_wage),165 "ri_reemployment_ate_pp": asnum(r_est),166 "ri_weeks_ui_ate": asnum(u_est), "ri_weeks_ui_ate_ci_low": asnum(u_lo), "ri_weeks_ui_ate_ci_high": asnum(u_hi),167 "locators": {168 "dcr_att_5km": L(DCR_ID, 6, "ATT", "5 km"),169 "dcr_att_5km_se": L(DCR_ID, 6, "ATT", "5 km") + " | standard error (parenthesised line below)",170 "dcr_att_3km": L(DCR_ID, 6, "ATT", "3 km"),171 "dcr_att_3km_se": L(DCR_ID, 6, "ATT", "3 km") + " | standard error (parenthesised line below)",172 "dcr_obs_5km": L(DCR_ID, 6, "Observations", "5 km"),173 "dcr_narcotics_att_3km": L(DCR_ID, 7, "Narcotics", "3 km"),174 "dcr_narcotics_att_3km_se": L(DCR_ID, 7, "Narcotics", "3 km") + " | standard error (parenthesised line below)",175 "dcr_fatal_att_1km": L(DCR_ID, 7, "Drug-related (fatal)", "1 km"),176 "dcr_fatal_att_1km_se": L(DCR_ID, 7, "Drug-related (fatal)", "1 km") + " | standard error (parenthesised line below)",177 "dcr_hiv_att_5km": L(DCR_ID, 7, "HIV", "5 km"),178 "dcr_hiv_att_5km_se": L(DCR_ID, 7, "HIV", "5 km") + " | standard error (parenthesised line below)",179 "dcr_hiv_att_5km_p_value": f"not reported in {DCR_ID} (Table 7 prints only significance stars; no exact p-values anywhere in the extract)",180 "dcr_att_5km_ci_low": L(DCR_ID, 6, "ATT", "5 km") + " | derived from printed coefficient and SE (lower bound)",181 "dcr_att_5km_ci_high": L(DCR_ID, 6, "ATT", "5 km") + " | derived from printed coefficient and SE (upper bound)",182 "ri_wage_ate": L(RI_ID, 2, "Wage", ATE) + " | point estimate",183 "ri_wage_ate_ci_low": L(RI_ID, 2, "Wage", ATE) + " | 95% CI lower bound",184 "ri_wage_ate_ci_high": L(RI_ID, 2, "Wage", ATE) + " | 95% CI upper bound",185 "ri_wage_ate_se_derived": L(RI_ID, 2, "Wage", ATE) + " | derived from printed 95% CI bounds (SE not reported in source)",186 "ri_control_mean_wage": L(RI_ID, 2, "Wage", "Control") + " | mean",187 "ri_reemployment_ate_pp": L(RI_ID, 2, "Reemployment", ATE) + " | point estimate",188 "ri_weeks_ui_ate": L(RI_ID, 2, "Weeks on UI", ATE) + " | point estimate",189 "ri_weeks_ui_ate_ci_low": L(RI_ID, 2, "Weeks on UI", ATE) + " | 95% CI lower bound",190 "ri_weeks_ui_ate_ci_high": L(RI_ID, 2, "Weeks on UI", ATE) + " | 95% CI upper bound",191 },192 "derivation_notes": {193 "dcr_att_5km_ci": "Normal approximation, critical value 1.96: ATT +/- 1.96*SE = -0.431 +/- 1.96*0.210 using Table 6 printed values; rounded to 3 decimals. Assumes asymptotic normality of the clustered-SE ATT estimator (no t / small-cluster correction).",194 "ri_wage_ate_se": "SE = (upper - lower)/(2*1.96) = (2141 - 166)/3.92 from Table 2 printed 95%% CI, assuming a symmetric normal-approximation (Wald) interval; rounded to 0.1 USD. With N = 23,549 a t critical value is indistinguishable from 1.96. Consistency check: implied two-sided p = %.3f vs printed p=0.022." % w_p_implied,195 },196 "feasibility": "complete",197 "feasibility_reason": None,198 "data_issues": data_issues,199 "n_input_rows": {"README.md": nlines(readme), f"{DCR_ID}.txt": nlines(dcr_txt), f"{RI_ID}.txt": nlines(ri_txt)},200 "n_analysed": 26,201 "random_seed": None,202}203os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)204with open(os.path.join(ROOT, "out", "results.json"), "w") as f:205 json.dump(res, f, indent=2, ensure_ascii=False)206print(json.dumps({k: v for k, v in res.items() if k not in ("locators", "data_issues", "derivation_notes")}, indent=1))207# sensitivity figures quoted in report.md (not stored as results)208print("sensitivity: CI with z=2.0:", round(att5[1]-2*att5se[1],3), round(att5[1]+2*att5se[1],3),209 "| CI with A19 SE:", round(att5[1]-Z*a19se[1],3), round(att5[1]+Z*a19se[1],3),210 "| fatal 5km % =", round(100*(math.exp(-0.354)-1),1), "| narcotics 5km % =", round(100*(math.exp(0.390)-1),1))211