1"""F4-conf-01: extract printed values from two arXiv extracts; derive two quantities.2Reads only data/; writes out/results.json deterministically. No randomness."""3import json, re, os, hashlib45ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))6D = os.path.join(ROOT, "data")7DCR_ID, RI_ID = "arxiv-2609.27732v1", "arxiv-2606.14621v1"8Z = 1.96 # normal critical value for two-sided 95% intervals91011def read(rel):12 with open(os.path.join(D, rel), encoding="utf-8") as f:13 return f.read()141516def wc_l(text):17 return text.count("\n")181920readme = read("README.md")21dcr = read(f"sources/{DCR_ID}.txt")22ri = read(f"sources/{RI_ID}.txt")23n_input_rows = {"README.md": wc_l(readme), f"sources/{DCR_ID}.txt": wc_l(dcr),24 f"sources/{RI_ID}.txt": wc_l(ri)}2526# verify SHA-256 against README provenance table27for name, txt in [(DCR_ID, dcr), (RI_ID, ri)]:28 h = hashlib.sha256(txt.encode("utf-8")).hexdigest()29 assert h in readme, f"SHA mismatch for {name}"30 print(f"[sha256 ok] {name} {h}")3132dcr_lines = [l for l in dcr.split("\n") if l.strip()]333435def table_block(lines, start_pat, end_pat):36 i = next(k for k, l in enumerate(lines) if l.startswith(start_pat))37 j = next(k for k, l in enumerate(lines) if k > i and l.startswith(end_pat))38 return lines[i:j]394041def cells(line):42 return [c.strip() for c in line.split("|")]434445def num(s):46 """Parse a printed number: strip stars, parentheses, $, commas, %; keep sign."""47 t = s.replace("$", "").replace(",", "").replace("*", "").strip("() %")48 return float(t) if "." in t else int(t) # integers printed without decimals stay integers495051def coef_and_se(block, row_label, col_label, header_idx=0):52 header = cells(block[header_idx])53 col = header.index(col_label)54 k = next(k for k, l in enumerate(block) if cells(l)[0] == row_label)55 row, se_row = cells(block[k]), cells(block[k + 1])56 assert se_row[0] == "" and se_row[col].startswith("("), se_row57 print(f"[match] row: {block[k]!r}\n se : {block[k+1]!r} -> col {col_label!r}: {row[col]} / {se_row[col]}")58 return row[col], se_row[col]596061# ---- DCR Table 662t6 = table_block(dcr_lines, "Table 6:", "Note:")[1:]63att5_s, se5_s = coef_and_se(t6, "ATT", "5 km")64att3_s, se3_s = coef_and_se(t6, "ATT", "3 km")65hdr6 = cells(t6[0])66obs_line = next(l for l in t6 if cells(l)[0] == "Observations")67print(f"[match] {obs_line!r}")68obs5 = int(cells(obs_line)[hdr6.index("5 km")].replace(",", ""))6970# ---- DCR Table 771t7 = table_block(dcr_lines, "Table 7:", "Note:")[1:]72narc_s, narc_se_s = coef_and_se(t7, "Narcotics", "3 km")73fatal_s, fatal_se_s = coef_and_se(t7, "Drug-related (fatal)", "1 km")74hiv_s, hiv_se_s = coef_and_se(t7, "HIV", "5 km")75hiv_sig10 = "*" in hiv_s # any star = p<0.10 per table note76# exact p-value: does the extract print one for Table 7 cells?77t7_has_p = any(re.search(r"p\s*=\s*0?\.\d", l) for l in t7)78hiv_p = None79print(f"[check] Table 7 prints exact p-values: {t7_has_p}")80assert not t7_has_p8182# ---- Table A19 (only used for consistency check)83ta19 = table_block(dcr_lines, "Table A19:", "Note:")[1:]84a19_att, a19_se = coef_and_se(ta19, "ATT", "CS-DR")8586# ---- RI Table 287ri_lines = ri.split("\n")88hdr_idx = next(k for k, l in enumerate(ri_lines) if l.startswith("Outcome | Treatment | Control"))89hdr2 = cells(ri_lines[hdr_idx])90t2 = {}91for l in ri_lines[hdr_idx + 1:]:92 if not l.strip():93 break94 c = cells(l)95 t2[c[0]] = dict(zip(hdr2, c))96ATE = "Average treatment effect estimate"979899def parse_effect(cell):100 m = re.match(r"^(-?\$?-?[\d,]*\d(?:\.\d+)?)\s*(?:pp|weeks|wks)?;\s*p\s*([=<])\s*([\d.]+);\s*95% CI:\s*\[(-?[\d.]+(?:,\d{3})*),\s*(-?[\d.]+(?:,\d{3})*)\]$", cell)101 assert m, cell102 return m.group(1), m.group(2) + m.group(3), m.group(4), m.group(5)103104105wage_cell = t2["Wage"][ATE]106reemp_cell = t2["Reemployment"][ATE]107weeks_cell = t2["Weeks on UI"][ATE]108ctrl_cell = t2["Wage"]["Control"]109for lab in ["Wage", "Reemployment", "Weeks on UI"]:110 print(f"[match] Table 2 {lab!r} ATE cell: {t2[lab][ATE]!r}")111print(f"[match] Table 2 'Wage' Control cell: {ctrl_cell!r}")112w_pt, w_p, w_lo, w_hi = parse_effect(wage_cell)113r_pt, r_p, r_lo, r_hi = parse_effect(reemp_cell)114u_pt, u_p, u_lo, u_hi = parse_effect(weeks_cell)115ctrl_mean = num(re.match(r"^(\$[\d,]+)", ctrl_cell).group(1))116wage_se_reported = bool(re.search(r"\bSE\b|standard error|\(\s*[\d,.]+\s*\)", wage_cell))117118# ---- derived119att5, se5 = num(att5_s), num(se5_s)120ci_lo = round(att5 - Z * se5, 4)121ci_hi = round(att5 + Z * se5, 4)122wlo, whi = num(w_lo), num(w_hi)123wage_se = round((whi - wlo) / (2 * Z), 2)124125res = {126 "task_id": "F4-conf-01",127 "dcr_att_5km": att5, "dcr_att_5km_se": se5,128 "dcr_att_3km": num(att3_s), "dcr_att_3km_se": num(se3_s),129 "dcr_obs_5km": obs5,130 "dcr_narcotics_att_3km": num(narc_s), "dcr_narcotics_att_3km_se": num(narc_se_s),131 "dcr_fatal_att_1km": num(fatal_s), "dcr_fatal_att_1km_se": num(fatal_se_s),132 "dcr_hiv_att_5km": num(hiv_s), "dcr_hiv_att_5km_se": num(hiv_se_s),133 "dcr_hiv_att_5km_p_value": hiv_p,134 "dcr_hiv_att_5km_significant_10pct": hiv_sig10,135 "dcr_att_5km_ci_low": ci_lo, "dcr_att_5km_ci_high": ci_hi,136 "ri_wage_ate": num(w_pt), "ri_wage_ate_ci_low": wlo, "ri_wage_ate_ci_high": whi,137 "ri_wage_ate_se_reported": wage_se_reported,138 "ri_wage_ate_se_derived": wage_se,139 "ri_control_mean_wage": ctrl_mean,140 "ri_reemployment_ate_pp": num(r_pt),141 "ri_weeks_ui_ate": num(u_pt), "ri_weeks_ui_ate_ci_low": num(u_lo), "ri_weeks_ui_ate_ci_high": num(u_hi),142}143144L6 = f"{DCR_ID} | Table 6"145L7 = f"{DCR_ID} | Table 7"146L2 = f"{RI_ID} | Table 2"147res["locators"] = {148 "dcr_att_5km": f'{L6} | row "ATT" | column "5 km"',149 "dcr_att_5km_se": f'{L6} | row "ATT" (standard-error line in parentheses) | column "5 km"',150 "dcr_att_3km": f'{L6} | row "ATT" | column "3 km"',151 "dcr_att_3km_se": f'{L6} | row "ATT" (standard-error line in parentheses) | column "3 km"',152 "dcr_obs_5km": f'{L6} | row "Observations" | column "5 km"',153 "dcr_narcotics_att_3km": f'{L7} | row "Narcotics" | column "3 km"',154 "dcr_narcotics_att_3km_se": f'{L7} | row "Narcotics" (standard-error line in parentheses) | column "3 km"',155 "dcr_fatal_att_1km": f'{L7} | row "Drug-related (fatal)" | column "1 km"',156 "dcr_fatal_att_1km_se": f'{L7} | row "Drug-related (fatal)" (standard-error line in parentheses) | column "1 km"',157 "dcr_hiv_att_5km": f'{L7} | row "HIV" | column "5 km"',158 "dcr_hiv_att_5km_se": f'{L7} | row "HIV" (standard-error line in parentheses) | column "5 km"',159 "dcr_hiv_att_5km_p_value": f"not reported in {DCR_ID} (Table 7 prints only significance stars, no exact p-values; an exact p-value could only be computed under an additional distributional assumption)",160 "dcr_att_5km_ci_low": f'{L6} | row "ATT" | column "5 km" | derived from printed ATT and SE (ATT - 1.96*SE)',161 "dcr_att_5km_ci_high": f'{L6} | row "ATT" | column "5 km" | derived from printed ATT and SE (ATT + 1.96*SE)',162 "ri_wage_ate": f'{L2} | row "Wage" | column "Average treatment effect estimate" (point estimate)',163 "ri_wage_ate_ci_low": f'{L2} | row "Wage" | column "Average treatment effect estimate" (95% CI lower bound)',164 "ri_wage_ate_ci_high": f'{L2} | row "Wage" | column "Average treatment effect estimate" (95% CI upper bound)',165 "ri_wage_ate_se_derived": f'{L2} | row "Wage" | column "Average treatment effect estimate" | derived from printed 95% CI ((high - low)/(2*1.96)); SE not reported in {RI_ID}',166 "ri_control_mean_wage": f'{L2} | row "Wage" | column "Control" (mean)',167 "ri_reemployment_ate_pp": f'{L2} | row "Reemployment" | column "Average treatment effect estimate" (point estimate)',168 "ri_weeks_ui_ate": f'{L2} | row "Weeks on UI" | column "Average treatment effect estimate" (point estimate)',169 "ri_weeks_ui_ate_ci_low": f'{L2} | row "Weeks on UI" | column "Average treatment effect estimate" (95% CI lower bound)',170 "ri_weeks_ui_ate_ci_high": f'{L2} | row "Weeks on UI" | column "Average treatment effect estimate" (95% CI upper bound)',171}172res["derivation_notes"] = {173 "dcr_att_5km_ci": "Normal (Wald) approximation with critical value 1.96: ATT +/- 1.96*SE using the Table 6 printed ATT -0.431 and clustered SE 0.210; bounds rounded to 4 decimals. Assumes approximate normality of the Callaway-Sant'Anna estimator; the source prints no CI for the overall ATT.",174 "ri_wage_ate_se": "SE = (upper - lower)/(2*1.96) from the Table 2 printed 95% CI [166, 2,141]; rounded to 2 decimals. Assumes the CI is a symmetric normal-approximation (Wald) interval; the printed CI is nearly symmetric about 1,153 (987 below, 988 above). With N = 23,549 a t critical value is practically identical to 1.96.",175}176# consistency checks -> data issues177issues = []178if (a19_att.replace("*", ""), a19_se) != (att5_s.replace("*", ""), se5_s):179 issues.append({"issue": f"Internal inconsistency in {DCR_ID}: the 5 km CS-DR ATT {att5_s} has SE {se5_s} in Table 6 but SE {a19_se} in Appendix Table A19 (same estimator, buffer and N=17,250).",180 "handling": "Used Table 6 as required by the key definitions; the derived CI uses the Table 6 SE 0.210. Flagged; not resolvable from the extract."})181if "640 regions" in dcr:182 issues.append({"issue": f"Internal inconsistency in {DCR_ID}: Tables 6/7 and the Figure 6 note report 608 never-treated control regions, but the Table A19 note says 640 never-treated comparison regions.",183 "handling": "No requested key depends on it; flagged only."})184if "fall by 35%" in dcr and "approximately 30% at 5 km" in dcr:185 issues.append({"issue": f"Text inconsistency in {DCR_ID}: the abstract says fatal hospitalisations fall by 35%, Section 6.1 says ~30% at 5 km (Table 7 fatal 5 km ATT -0.354 implies exp(-0.354)-1 = -29.8%). Section 6 text also calls HIV estimates 'close to zero' although the 5 km point estimate is -0.340.",186 "handling": "Narrative only; all keys taken from the tables. Flagged."})187if "[1.75 weeks, 2.24 weeks]" in ri:188 issues.append({"issue": f"Sign convention differs in {RI_ID}: the text reports the weeks-on-UI CI as magnitudes of a reduction [1.75, 2.24], while Table 2 prints the ATE -1.99 with CI [-2.24, -1.75].",189 "handling": "Used the Table 2 signed values as printed (lower bound first)."})190if "p ¡ 0.001" in ri:191 issues.append({"issue": f"Rendering defect in {RI_ID} text: 'p ¡ 0.001' (should read p < 0.001).",192 "handling": "Does not affect any key; flagged."})193t3w = next(l for l in ri_lines if l.startswith("Wage | $1,153"))194assert cells(t3w)[1] == wage_cell, "Table 3 / Table 2 wage mismatch"195issues.append({"issue": f"{RI_ID} Table 2 was re-set by the extract editor (one row per outcome; p-values reattached to columns), per the extract header.",196 "handling": "Accepted the editor's layout; the Table 3 'Difference in means' wage cell duplicates the Table 2 wage ATE cell exactly (checked in code), consistent with correct reattachment."})197res["data_issues"] = issues198res["feasibility"] = "complete"199res["feasibility_reason"] = None200res["n_input_rows"] = n_input_rows201value_keys = [k for k in res if k.startswith(("dcr_", "ri_"))]202res["n_analysed"] = len(value_keys)203res["random_seed"] = None204205os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)206with open(os.path.join(ROOT, "out", "results.json"), "w") as f:207 json.dump(res, f, indent=2, ensure_ascii=False)208print(json.dumps({k: res[k] for k in value_keys}, indent=1))209