1"""F4-conf-01: extract printed treatment effects/uncertainty from two source extracts.2Reads only data/; writes out/results.json deterministically. No randomness (random_seed = null).3Every extracted value is parsed from the text and the matched source line is printed."""4import json, re, os, hashlib5from pathlib import Path67ROOT = Path(__file__).resolve().parent.parent8DATA = ROOT / "data"9DCR = DATA / "sources" / "arxiv-2609.27732v1.txt"10RI = DATA / "sources" / "arxiv-2606.14621v1.txt"11DCR_ID, RI_ID = "arxiv-2609.27732v1", "arxiv-2606.14621v1"12Z = 1.96 # normal critical value for two-sided 95%1314def lines(p):15 return p.read_text(encoding="utf-8").split("\n")1617def num(s):18 """Parse a printed number keeping its printed decimals (e.g. '-0.431**' -> -0.431, '$1,153' -> 1153)."""19 s = s.strip().replace("$", "").replace(",", "").replace("%", "")20 s = re.sub(r"\*+$", "", s).strip()21 s = s.strip("()")22 return int(s) if re.fullmatch(r"-?\d+", s) else float(s)2324def stars(s):25 m = re.search(r"(\*+)\s*$", s.strip()); return len(m.group(1)) if m else 02627def block(ls, start_pat, end_pat):28 i0 = next(i for i, l in enumerate(ls) if re.match(start_pat, l))29 i1 = next(i for i in range(i0 + 1, len(ls)) if re.match(end_pat, ls[i]))30 return i0, ls[i0:i1]3132def parse_reg_table(ls, title_pat, end_pat):33 """Return header columns, {row: {col: (raw_coef, raw_se, lineno)}}, {row: {col: raw}} for non-coef rows."""34 i0, blk = block(ls, title_pat, end_pat)35 rows = [(i0 + k + 1, l) for k, l in enumerate(blk) if l.strip() and " | " in l or l.startswith("|")]36 rows = [(n, l) for n, l in rows if l.strip()]37 header = [c.strip() for c in rows[0][1].split("|")][1:]38 coef, other = {}, {}39 k = 140 while k < len(rows):41 n, l = rows[k]42 cells = [c.strip() for c in l.split("|")]43 label, vals = cells[0], cells[1:]44 nxt = rows[k + 1][1] if k + 1 < len(rows) else ""45 if label and nxt.strip().startswith("|") and "(" in nxt:46 ses = [c.strip() for c in nxt.split("|")][1:]47 coef[label] = {h: (v, s, n, rows[k + 1][0]) for h, v, s in zip(header, vals, ses)}48 k += 249 else:50 other[label] = {h: (v, n) for h, v in zip(header, vals)}51 k += 152 return header, coef, other5354dl = lines(DCR); rl = lines(RI)55h6, t6, o6 = parse_reg_table(dl, r"^Table 6:", r"^Note:")56h7, t7, o7 = parse_reg_table(dl, r"^Table 7:", r"^Note:")57hA, tA, oA = parse_reg_table(dl, r"^Table A19:", r"^Note:")58if h7 and h7[0] == "Outcome": # Table 7 header line starts with 'Outcome |'59 pass60# Table 7 header has 'Outcome' as first cell rather than empty: re-parse header61_, blk7 = block(dl, r"^Table 7:", r"^Note:")62hdr7 = next(l for l in blk7 if l.startswith("Outcome |"))63h7 = [c.strip() for c in hdr7.split("|")][1:]64_, t7, o7 = (None, *parse_reg_table(dl, r"^Table 7:", r"^Note:")[1:])65# re-key Table 7 entries with the correct header66def rekey(tab, hdr):67 out = {}68 for r, d in tab.items():69 vals = list(d.values())70 out[r] = dict(zip(hdr, vals))71 return out72t7 = rekey(t7, h7); o7 = rekey(o7, h7)7374def show(src, n):75 print(f" [{src} line {n}] {(dl if src == DCR_ID else rl)[n - 1]}")7677res, loc = {}, {}78def put_coef(key, tab, tabname, row, col):79 v, s, n1, n2 = tab[row][col]80 res[key] = num(v); res[key + "_se"] = num(s)81 loc[key] = f'{DCR_ID} | Table {tabname} | row "{row}" | column "{col}"'82 loc[key + "_se"] = f'{DCR_ID} | Table {tabname} | row "{row}" (standard-error line below, in parentheses) | column "{col}"'83 print(f"{key} = {res[key]} (stars={stars(v)}), {key}_se = {res[key + '_se']}"); show(DCR_ID, n1); show(DCR_ID, n2)84 return stars(v)8586put_coef("dcr_att_5km", t6, "6", "ATT", "5 km")87put_coef("dcr_att_3km", t6, "6", "ATT", "3 km")88v, n = o6["Observations"]["5 km"]89res["dcr_obs_5km"] = num(v)90loc["dcr_obs_5km"] = f'{DCR_ID} | Table 6 | row "Observations" | column "5 km"'91print(f"dcr_obs_5km = {res['dcr_obs_5km']}"); show(DCR_ID, n)92put_coef("dcr_narcotics_att_3km", t7, "7", "Narcotics", "3 km")93put_coef("dcr_fatal_att_1km", t7, "7", "Drug-related (fatal)", "1 km")94hiv_stars = put_coef("dcr_hiv_att_5km", t7, "7", "HIV", "5 km")9596# exact p-value: search the whole DCR extract for any printed p-value97pv_lines = [i + 1 for i, l in enumerate(dl) if re.search(r"\bp\s*=\s*0?\.\d", l)]98print(f"lines in {DCR_ID} printing an exact p-value (p = ...): {pv_lines}")99res["dcr_hiv_att_5km_p_value"] = None100loc["dcr_hiv_att_5km_p_value"] = (f"not reported in {DCR_ID} (Table 7 prints only the coefficient, the clustered SE "101 f"and significance stars; no exact p-value is printed anywhere in the extract)")102res["dcr_hiv_att_5km_significant_10pct"] = hiv_stars >= 1103print(f"dcr_hiv_att_5km_significant_10pct = {res['dcr_hiv_att_5km_significant_10pct']} (stars on Table 7 HIV 5 km cell: {hiv_stars})")104105# derived 95% CI from Table 6 printed SE, normal approximation106b, se = res["dcr_att_5km"], res["dcr_att_5km_se"]107res["dcr_att_5km_ci_low"] = round(b - Z * se, 4)108res["dcr_att_5km_ci_high"] = round(b + Z * se, 4)109loc["dcr_att_5km_ci_low"] = f'{DCR_ID} | Table 6 | row "ATT" | column "5 km" | derived (estimate - 1.96 x printed SE)'110loc["dcr_att_5km_ci_high"] = f'{DCR_ID} | Table 6 | row "ATT" | column "5 km" | derived (estimate + 1.96 x printed SE)'111112# ---- RESEA Table 2 ----113_, blk2 = block(rl, r"^\[Table 2", r"^Table 2:")114i2 = next(i for i, l in enumerate(rl) if l.startswith("[Table 2"))115hdr = next(l for l in blk2 if l.startswith("Outcome |"))116H2 = [c.strip() for c in hdr.split("|")]117T2 = {}118for k, l in enumerate(blk2):119 if " | " in l and not l.startswith("Outcome |") and not l.startswith("["):120 cells = [c.strip() for c in l.split("|")]121 T2[cells[0]] = (dict(zip(H2, cells)), i2 + k + 1)122123def parse_effect(cell):124 m = re.match(r"^(-?)\$?(-?[\d,\.]+)\s*(pp|weeks|wks)?;\s*p\s*([=<])\s*([\d\.]+);\s*95% CI:\s*\[([^,\]]+(?:,\d{3})*),\s*([^\]]+)\]$",125 cell.replace("-$", "-"))126 if not m:127 raise ValueError(cell)128 est = num(m.group(1) + m.group(2))129 # split CI bounds robustly (thousands separators: "166, 2,141")130 ci = cell.split("95% CI:")[1].strip().strip("[]")131 parts = re.findall(r"-?\d{1,3}(?:,\d{3})+|-?\d+(?:\.\d+)?", ci)132 return est, num(parts[0]), num(parts[1]), m.group(4) + m.group(5)133134ATE = "Average treatment effect estimate"135for key, row in [("ri_wage_ate", "Wage"), ("ri_reemployment_ate_pp", "Reemployment"), ("ri_weeks_ui_ate", "Weeks on UI")]:136 d, n = T2[row]137 est, lo, hi, p = parse_effect(d[ATE])138 res[key] = est139 loc[key] = f'{RI_ID} | Table 2 | row "{row}" | column "{ATE}" (point estimate)'140 print(f"{key} = {est}; printed 95% CI [{lo}, {hi}]; p{p}"); print(f" [{RI_ID} line {n}] {rl[n - 1]}")141 if key in ("ri_wage_ate", "ri_weeks_ui_ate"):142 res[key + "_ci_low"], res[key + "_ci_high"] = lo, hi143 loc[key + "_ci_low"] = f'{RI_ID} | Table 2 | row "{row}" | column "{ATE}" (95% CI lower bound)'144 loc[key + "_ci_high"] = f'{RI_ID} | Table 2 | row "{row}" | column "{ATE}" (95% CI upper bound)'145d, n = T2["Wage"]146cm = re.match(r"^\$([\d,]+)", d["Control"]).group(1)147res["ri_control_mean_wage"] = num(cm)148loc["ri_control_mean_wage"] = f'{RI_ID} | Table 2 | row "Wage" | column "Control" (mean)'149print(f"ri_control_mean_wage = {res['ri_control_mean_wage']}"); print(f" [{RI_ID} line {n}] {rl[n - 1]}")150151wage_cell = T2["Wage"][0][ATE]152res["ri_wage_ate_se_reported"] = bool(re.search(r"\(\s*[\d,\.]+\s*\)|\bSE\b|s\.e\.", wage_cell, re.I))153print(f"ri_wage_ate_se_reported = {res['ri_wage_ate_se_reported']} (wage ATE cell: '{wage_cell}')")154lo, hi = res["ri_wage_ate_ci_low"], res["ri_wage_ate_ci_high"]155res["ri_wage_ate_se_derived"] = round((hi - lo) / (2 * Z), 2)156loc["ri_wage_ate_se_derived"] = (f'{RI_ID} | Table 2 | row "Wage" | column "{ATE}" | derived '157 f'((CI upper - CI lower) / (2 x 1.96))')158159# ---- consistency checks that feed data_issues ----160issues = []161seA = num(tA["ATT"]["CS-DR"][1])162if seA != res["dcr_att_5km_se"]:163 issues.append({"issue": (f"{DCR_ID}: the same 5 km CS-DR overall ATT ({res['dcr_att_5km']}) is printed with SE "164 f"{res['dcr_att_5km_se']} in Table 6 but SE {seA} in Appendix Table A19 (column 'CS-DR')."),165 "handling": "Used Table 6 as the key definitions require (dcr_att_5km_se, derived CI); A19 value not used."})166ctrl6 = num(o6["Control regions"]["5 km"][0])167mA = re.search(r"never-treated comparison regions \((\d+) regions\)", "\n".join(dl))168if mA and int(mA.group(1)) != ctrl6:169 issues.append({"issue": (f"{DCR_ID}: Tables 6/7 and A19 rows report {ctrl6} never-treated control regions, but the "170 f"Table A19 note says {mA.group(1)} never-treated comparison regions."),171 "handling": "Recorded only; no requested key depends on the control-region count."})172fatal5 = num(t7["Drug-related (fatal)"]["5 km"][0])173issues.append({"issue": (f"{DCR_ID}: the abstract says fatal hospitalisations 'fall by 35%', whereas Table 7 gives a 5 km "174 f"fatal ATT of {fatal5} log points (exp({fatal5})-1 = {100 * (2.718281828459045 ** fatal5 - 1):.1f}%), "175 f"and Section 6.1 says 'approximately 30% at 5 km'; the abstract figure appears to equate log points with percent."),176 "handling": "Recorded only; values were taken from the tables named in the key definitions."})177hiv5 = res["dcr_hiv_att_5km"]178issues.append({"issue": (f"{DCR_ID}: the text describes HIV estimates as 'insignificant and close to zero', but the printed "179 f"5 km HIV ATT is {hiv5} log points (SE {res['dcr_hiv_att_5km_se']}); it is insignificant (no star) but not numerically close to zero."),180 "handling": "Recorded only; significance flag set from the absence of stars in Table 7."})181issues.append({"issue": (f"{RI_ID}: the Results text states the weeks-on-UI CI as magnitudes '[1.75 weeks, 2.24 weeks]' for a "182 f"'1.99 week reduction', while Table 2 prints the ATE as -1.99 with CI [-2.24, -1.75]."),183 "handling": "Used the signed Table 2 values as required (lower bound first)."})184if any("¡" in l for l in rl):185 issues.append({"issue": f"{RI_ID}: rendering defect in the Section 3 text: 'p ¡ 0.001' (should read p < 0.001).",186 "handling": "Not used for any key (Table 2 cell prints 'p<0.001')."})187issues.append({"issue": (f"{RI_ID}: Tables 2 and 3 were re-set by the extract editor into a one-row-per-outcome layout "188 f"(HTML rendering split rows and detached p-values)."),189 "handling": "Relied on the editor's statement that values are verbatim; Table 3 'Difference in means' column matches Table 2 ATE column exactly (checked)."})190# check Table 3 duplicates Table 2 ATEs191_, blk3 = block(rl, r"^\[Table 3", r"^Table 3:")192t3w = next(l for l in blk3 if l.startswith("Wage |")).split("|")[1].strip()193assert parse_effect(t3w)[:3] == (res["ri_wage_ate"], lo, hi), "Table 3 vs Table 2 wage mismatch"194195def wc_l(p):196 return p.read_bytes().count(b"\n")197198out = {199 "task_id": "F4-conf-01",200 **{k: res[k] for k in ["dcr_att_5km", "dcr_att_5km_se", "dcr_att_3km", "dcr_att_3km_se", "dcr_obs_5km",201 "dcr_narcotics_att_3km", "dcr_narcotics_att_3km_se", "dcr_fatal_att_1km", "dcr_fatal_att_1km_se",202 "dcr_hiv_att_5km", "dcr_hiv_att_5km_se", "dcr_hiv_att_5km_p_value",203 "dcr_hiv_att_5km_significant_10pct", "dcr_att_5km_ci_low", "dcr_att_5km_ci_high",204 "ri_wage_ate", "ri_wage_ate_ci_low", "ri_wage_ate_ci_high", "ri_wage_ate_se_reported",205 "ri_wage_ate_se_derived", "ri_control_mean_wage", "ri_reemployment_ate_pp",206 "ri_weeks_ui_ate", "ri_weeks_ui_ate_ci_low", "ri_weeks_ui_ate_ci_high"]},207 "locators": loc,208 "derivation_notes": {209 "dcr_att_5km_ci": ("95% CI = ATT +/- 1.96 x SE using the Table 6 printed ATT (-0.431) and clustered SE (0.210); "210 "normal (large-sample) critical value 1.96; rounded to 4 decimals. Assumes asymptotic normality of the "211 "CS-DR estimator and that the printed SE (itself rounded to 3 decimals) is the relevant SE."),212 "ri_wage_ate_se": ("SE = (upper - lower) / (2 x 1.96) using the Table 2 printed 95% CI [166, 2,141]; normal critical value "213 "1.96 (with N = 23,549 a t critical value is indistinguishable); rounded to 2 decimals. Assumes the "214 "printed CI is a symmetric Wald interval (midpoint 1153.5 vs estimate 1,153, consistent up to rounding)."),215 },216 "feasibility": "complete",217 "feasibility_reason": None,218 "data_issues": issues,219 "n_input_rows": {"README.md": wc_l(DATA / "README.md"), DCR.name: wc_l(DCR), RI.name: wc_l(RI)},220 "n_analysed": 25,221 "random_seed": None,222}223os.makedirs(ROOT / "out", exist_ok=True)224(ROOT / "out" / "results.json").write_text(json.dumps(out, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")225print("wrote out/results.json")226