1"""F4-conf-01: extract printed values from two source extracts, derive two quantities.2Run from workspace root: ~/wp29/venv/bin/python out/analysis.py3Values are parsed from the text; every matched source line is printed."""4import json, re, os56D = "data/sources"7DCR_ID, RI_ID = "arxiv-2609.27732v1", "arxiv-2606.14621v1"8dcr = open(f"{D}/{DCR_ID}.txt", encoding="utf-8").read().splitlines()9ri = open(f"{D}/{RI_ID}.txt", encoding="utf-8").read().splitlines()1011def section(lines, start, end):12 i = next(k for k, l in enumerate(lines) if l.startswith(start))13 j = next(k for k in range(i + 1, len(lines)) if lines[k].startswith(end))14 return lines[i:j]1516def cells(line):17 return [c.strip() for c in line.split("|")]1819def row_with_se(tab, label, table_name):20 """Return (coef cells, se cells) for a coefficient row and the following SE line."""21 idx = [k for k, l in enumerate(tab) if cells(l)[0] == label]22 assert len(idx) == 1, (label, idx)23 k = idx[0]24 se_k = next(m for m in range(k + 1, len(tab)) if tab[m].strip())25 print(f"[{table_name}] matched: {tab[k]!r}\n[{table_name}] SE line : {tab[se_k]!r}")26 return cells(tab[k])[1:], cells(tab[se_k])[1:]2728def num(s):29 m = re.match(r"^\(?(-?\d+(?:\.\d+)?)\)?(\**)$", s.strip())30 assert m, s31 return float(m.group(1)), len(m.group(2))3233# ---------- DCR Table 6 ----------34t6 = section(dcr, "Table 6:", "Note:")35hdr6 = cells(next(l for l in t6 if l.startswith("| 1 km")))[1:]36col6 = {h: i for i, h in enumerate(hdr6)}37att, se = row_with_se(t6, "ATT", "Table 6")38obs_line = next(l for l in t6 if cells(l)[0] == "Observations")39print(f"[Table 6] matched: {obs_line!r}")40obs = cells(obs_line)[1:]4142dcr_att_5km, stars_5 = num(att[col6["5 km"]])43dcr_att_5km_se, _ = num(se[col6["5 km"]])44dcr_att_3km, _ = num(att[col6["3 km"]])45dcr_att_3km_se, _ = num(se[col6["3 km"]])46dcr_obs_5km = int(obs[col6["5 km"]].replace(",", ""))4748# ---------- DCR Table 7 ----------49t7 = section(dcr, "Table 7:", "Note:")50hdr7 = cells(next(l for l in t7 if l.startswith("Outcome |")))[1:]51col7 = {h: i for i, h in enumerate(hdr7)}52c, s = row_with_se(t7, "Narcotics", "Table 7")53narc3, _ = num(c[col7["3 km"]]); narc3_se, _ = num(s[col7["3 km"]])54c, s = row_with_se(t7, "Drug-related (fatal)", "Table 7")55fat1, _ = num(c[col7["1 km"]]); fat1_se, _ = num(s[col7["1 km"]])56c, s = row_with_se(t7, "HIV", "Table 7")57hiv5, hiv5_stars = num(c[col7["5 km"]]); hiv5_se, _ = num(s[col7["5 km"]])58hiv5_sig10 = hiv5_stars >= 1 # any star means p<0.10 per table note59# exact p-value: search the DCR extract for any printed p-value (p=...)60pvals = [l for l in dcr if re.search(r"\bp\s*=\s*0?\.\d", l)]61print(f"[DCR] lines with exact p-values printed: {len(pvals)}")62hiv_p = None6364# ---------- derived 95% CI for Table 6 5 km ATT ----------65Z = 1.9666ci_lo = round(dcr_att_5km - Z * dcr_att_5km_se, 4)67ci_hi = round(dcr_att_5km + Z * dcr_att_5km_se, 4)6869# ---------- RI Table 2 ----------70t2 = section(ri, "Outcome | Treatment | Control", "Table 2:")71hdr2 = cells(t2[0])72col2 = {h: i for i, h in enumerate(hdr2)}73rows2 = {cells(l)[0]: cells(l) for l in t2[1:] if l.strip()}7475def ate_parse(cell):76 m = re.match(r"^(-?)\$?(-?[\d,]+(?:\.\d+)?)\s*(pp|weeks|wks)?;\s*p\s*([=<])\s*([\d.]+);\s*95% CI:\s*\[(-?[\d,.]+),\s*(-?[\d,.]+)\]$", cell)77 assert m, cell78 sign = -1 if m.group(1) == "-" else 179 f = lambda x: float(x.replace(",", ""))80 v = sign * f(m.group(2))81 return v, f(m.group(6)), f(m.group(7))8283ATE = "Average treatment effect estimate"84for r in ["Wage", "Reemployment", "Weeks on UI"]:85 print(f"[Table 2] matched: {' | '.join(rows2[r])!r}")86w, wlo, whi = ate_parse(rows2["Wage"][col2[ATE]])87re_ate, _, _ = ate_parse(rows2["Reemployment"][col2[ATE]])88wk, wklo, wkhi = ate_parse(rows2["Weeks on UI"][col2[ATE]])89cm = re.match(r"^\$([\d,]+)", rows2["Wage"][col2["Control"]])90ri_control_mean_wage = int(cm.group(1).replace(",", ""))91toint = lambda x: int(x) if float(x).is_integer() else x92se_reported = bool(re.search(r"\b(SE|s\.e\.|standard error)\b", rows2["Wage"][col2[ATE]], re.I))93se_derived = round((whi - wlo) / (2 * Z), 2)9495loc = lambda src, t, r, c: f'{src} | Table {t} | row "{r}" | column "{c}"'96res = {97 "task_id": "F4-conf-01",98 "dcr_att_5km": dcr_att_5km, "dcr_att_5km_se": dcr_att_5km_se,99 "dcr_att_3km": dcr_att_3km, "dcr_att_3km_se": dcr_att_3km_se,100 "dcr_obs_5km": dcr_obs_5km,101 "dcr_narcotics_att_3km": narc3, "dcr_narcotics_att_3km_se": narc3_se,102 "dcr_fatal_att_1km": fat1, "dcr_fatal_att_1km_se": fat1_se,103 "dcr_hiv_att_5km": hiv5, "dcr_hiv_att_5km_se": hiv5_se,104 "dcr_hiv_att_5km_p_value": hiv_p,105 "dcr_hiv_att_5km_significant_10pct": hiv5_sig10,106 "dcr_att_5km_ci_low": ci_lo, "dcr_att_5km_ci_high": ci_hi,107 "ri_wage_ate": toint(w), "ri_wage_ate_ci_low": toint(wlo), "ri_wage_ate_ci_high": toint(whi),108 "ri_wage_ate_se_reported": se_reported,109 "ri_wage_ate_se_derived": se_derived,110 "ri_control_mean_wage": ri_control_mean_wage,111 "ri_reemployment_ate_pp": re_ate,112 "ri_weeks_ui_ate": wk, "ri_weeks_ui_ate_ci_low": wklo, "ri_weeks_ui_ate_ci_high": wkhi,113 "locators": {114 "dcr_att_5km": loc(DCR_ID, 6, "ATT", "5 km"),115 "dcr_att_5km_se": loc(DCR_ID, 6, "ATT (standard error line, in parentheses)", "5 km"),116 "dcr_att_3km": loc(DCR_ID, 6, "ATT", "3 km"),117 "dcr_att_3km_se": loc(DCR_ID, 6, "ATT (standard error line, in parentheses)", "3 km"),118 "dcr_obs_5km": loc(DCR_ID, 6, "Observations", "5 km"),119 "dcr_narcotics_att_3km": loc(DCR_ID, 7, "Narcotics", "3 km"),120 "dcr_narcotics_att_3km_se": loc(DCR_ID, 7, "Narcotics (standard error line, in parentheses)", "3 km"),121 "dcr_fatal_att_1km": loc(DCR_ID, 7, "Drug-related (fatal)", "1 km"),122 "dcr_fatal_att_1km_se": loc(DCR_ID, 7, "Drug-related (fatal) (standard error line, in parentheses)", "1 km"),123 "dcr_hiv_att_5km": loc(DCR_ID, 7, "HIV", "5 km"),124 "dcr_hiv_att_5km_se": loc(DCR_ID, 7, "HIV (standard error line, in parentheses)", "5 km"),125 "dcr_hiv_att_5km_p_value": f"not reported in {DCR_ID} (Table 7 prints only significance stars; no exact p-value; deriving one from estimate/SE would require a distributional assumption)",126 "dcr_att_5km_ci_low": loc(DCR_ID, 6, "ATT", "5 km") + " | derived (estimate - 1.96*SE)",127 "dcr_att_5km_ci_high": loc(DCR_ID, 6, "ATT", "5 km") + " | derived (estimate + 1.96*SE)",128 "ri_wage_ate": loc(RI_ID, 2, "Wage", ATE),129 "ri_wage_ate_ci_low": loc(RI_ID, 2, "Wage", ATE) + " (95% CI lower bound)",130 "ri_wage_ate_ci_high": loc(RI_ID, 2, "Wage", ATE) + " (95% CI upper bound)",131 "ri_wage_ate_se_derived": loc(RI_ID, 2, "Wage", ATE) + " | derived ((upper - lower)/(2*1.96)); SE not reported in " + RI_ID,132 "ri_control_mean_wage": loc(RI_ID, 2, "Wage", "Control"),133 "ri_reemployment_ate_pp": loc(RI_ID, 2, "Reemployment", ATE),134 "ri_weeks_ui_ate": loc(RI_ID, 2, "Weeks on UI", ATE),135 "ri_weeks_ui_ate_ci_low": loc(RI_ID, 2, "Weeks on UI", ATE) + " (95% CI lower bound)",136 "ri_weeks_ui_ate_ci_high": loc(RI_ID, 2, "Weeks on UI", ATE) + " (95% CI upper bound)",137 },138 "derivation_notes": {139 "dcr_att_5km_ci": "Normal approximation with critical value 1.96: CI = ATT +/- 1.96*SE using the Table 6 5 km ATT (-0.431) and its printed clustered SE (0.210); rounded to 4 decimals. Assumes asymptotic normality of the estimator; the paper's own CIs (figures) may use a different (e.g. multiplier-bootstrap) critical value.",140 "ri_wage_ate_se": "SE = (upper - lower)/(2*1.96) from the printed Table 2 95% CI [166, 2,141], assuming a symmetric normal-based (Wald) interval with critical value 1.96; rounded to 2 decimals. The printed CI is nearly symmetric around 1,153 (987 below, 988 above), consistent with this assumption; with N=23,549 a t critical value is indistinguishable from 1.96.",141 },142 "feasibility": "complete",143 "feasibility_reason": None,144 "data_issues": [145 {"issue": "DCR: the 5 km CS-DR ATT -0.431 has SE (0.210) in Table 6 but SE (0.218) in Appendix Table A19 (same estimator, buffer and N=17,250).",146 "handling": "Used Table 6 as the key definitions require; derived CI uses 0.210. Flagged as an internal inconsistency."},147 {"issue": "DCR: control group is 608 never-treated regions in Tables 6/7 and A19 rows, but the A19 note says 640 never-treated comparison regions.",148 "handling": "Not used for any key; flagged."},149 {"issue": "DCR: abstract says fatal outcomes 'fall by 35%', but Table 7 5 km fatal ATT is -0.354 log points (~-29.8%); Section 6.1 itself says ~30% at 5 km. The abstract appears to read log points as percent.",150 "handling": "Not used for any key; values taken from Table 7; flagged."},151 {"issue": "DCR: text describes HIV estimates as 'close to zero', while the Table 7 HIV 5 km ATT is -0.340 (SE 0.294), similar in size to other effects; it is insignificant (no star) but not close to zero.",152 "handling": "Reported printed values; significance taken from stars (none)."},153 {"issue": "RESEA: the text reports the weeks-on-UI CI as magnitudes '[1.75 weeks, 2.24 weeks]' whereas Table 2 prints [-2.24, -1.75].",154 "handling": "Used Table 2 values with printed signs, lower bound first."},155 {"issue": "RESEA: rendering defect 'p ¡ 0.001' in the short-term weeks-on-UI paragraph (should be 'p < 0.001').",156 "handling": "Not used for any key; noted."},157 {"issue": "RESEA: Tables 2 and 3 were re-set by the extract editor (one row per outcome).",158 "handling": "Accepted as verbatim per extract header; Table 2 values cross-checked equal to Table 3 'Difference in means' column."},159 ],160 "n_input_rows": {},161 "n_analysed": None,162 "random_seed": None,163}164# cross-check Table 3 first column equals Table 2 ATE column165t3 = section(ri, "Outcome | Difference in means", "Table 3:")166r3 = {cells(l)[0]: cells(l) for l in t3[1:] if l.strip()}167for r in ["Wage", "Reemployment", "Weeks on UI"]:168 a = ate_parse(rows2[r][col2[ATE]]); b = ate_parse(r3[r][1].replace("wks", "weeks"))169 print(f"[check] Table2 vs Table3 {r}: {a} vs {b} -> {'equal' if a == b else 'DIFFERENT'}")170# Table 6 vs A19 SE check171a19 = section(dcr, "Table A19:", "Note:")172a_att, a_se = row_with_se(a19, "ATT", "Table A19")173print(f"[check] Table 6 5 km SE {dcr_att_5km_se} vs A19 CS-DR SE {num(a_se[0])[0]}")174175for f in ["README.md", "sources/arxiv-2609.27732v1.txt", "sources/arxiv-2606.14621v1.txt"]:176 with open(os.path.join("data", f), "rb") as fh:177 res["n_input_rows"][f] = fh.read().count(b"\n")178scalar_keys = [k for k in res if k.startswith(("dcr_", "ri_"))]179res["n_analysed"] = len(scalar_keys)180res["printed_strings"] = {181 "dcr_att_5km": att[col6["5 km"]], "dcr_att_5km_se": se[col6["5 km"]],182 "dcr_att_3km": att[col6["3 km"]], "dcr_att_3km_se": se[col6["3 km"]],183 "dcr_obs_5km": obs[col6["5 km"]],184 "table7_narcotics": " | ".join(cells([l for l in t7 if l.startswith("Narcotics")][0])),185 "table7_fatal": " | ".join(cells([l for l in t7 if l.startswith("Drug-related (fatal)")][0])),186 "table7_hiv": " | ".join(cells([l for l in t7 if l.startswith("HIV")][0])),187 "ri_wage_ate_cell": rows2["Wage"][col2[ATE]], "ri_wage_control_cell": rows2["Wage"][col2["Control"]],188 "ri_reemployment_ate_cell": rows2["Reemployment"][col2[ATE]],189 "ri_weeks_ui_ate_cell": rows2["Weeks on UI"][col2[ATE]],190 "note": "JSON numbers cannot carry trailing zeros (e.g. printed 0.210 is stored as 0.21); the printed strings are kept here verbatim.",191}192os.makedirs("out", exist_ok=True)193json.dump(res, open("out/results.json", "w"), indent=2, ensure_ascii=False)194print(json.dumps({k: res[k] for k in scalar_keys}, indent=1))195