1"""F4-conf-03: harmonise five printed effect sizes. Reads only data/, writes out/results.json.2Run from workspace root: ~/wp29/venv/bin/python out/analysis.py3No randomness is involved (random_seed = null)."""4import json, math, os, re56ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))7D = os.path.join(ROOT, "data")8SRC = os.path.join(D, "sources")91011def lines(fname):12 with open(os.path.join(SRC, fname), encoding="utf-8") as f:13 return f.read().split("\n")141516def find_line(fname, prefix):17 """Return (1-based line number, text) of the unique line starting with prefix; prints it."""18 L = lines(fname)19 hits = [(i + 1, l) for i, l in enumerate(L) if l.startswith(prefix)]20 assert len(hits) == 1, (fname, prefix, hits)21 n, t = hits[0]22 print(f"[{fname}:{n}] {t}")23 return n, t242526def cells(text):27 return [c.strip() for c in text.split("|")]282930def num(s):31 m = re.search(r"-?\d[\d,]*\.?\d*", s.replace("−", "-"))32 return float(m.group(0).replace(",", ""))333435def wc_l(path):36 with open(path, "rb") as f:37 return f.read().count(b"\n")383940effects = json.load(open(os.path.join(D, "effects.json"), encoding="utf-8"))41assert [e["id"] for e in effects] == ["E1", "E2", "E3", "E4", "E5"]4243DCR = "arxiv-2609.27732v1.txt"44RES = "arxiv-2606.14621v1.txt"45MW = "arxiv-2505.04555v3.txt"46BEN = "arxiv-2510.03658v1.txt"4748H = {}49checks = {}5051# ---------------- E1: log points -> relative percent change ----------------52n_hdr, hdr = find_line(DCR, "| 1 km | 3 km | 5 km")53n_att, att = find_line(DCR, "ATT | -0.399")54n_pc, pc = find_line(DCR, "% change | -32.9%")55c = cells(att)56assert cells(hdr)[3] == "5 km" and c[3] == "-0.431**"57b1 = num(c[3])58e1 = 100 * (math.exp(b1) - 1)59printed_pc_5km = num(cells(pc)[3])60checks["E1_vs_printed_pct_change"] = {"computed": round(e1, 4), "printed": printed_pc_5km,61 "agree_to_printed_precision": round(e1, 1) == printed_pc_5km}62H["E1"] = {63 "value": round(e1, 2),64 "unit": "percent change relative to the counterfactual hospitalisation rate",65 "transformation": f"100*(exp(b)-1) with b = {b1:.3f} log points: 100*(exp(-0.431)-1) = {e1:.2f}% (printed '% change' row: -35.0%)",66 "harmonisable": True,67 "missing_quantity": None,68 "locator": f"arxiv-2609.27732v1 | Table 6 | row 'ATT' | column '5 km' (value -0.431**, extract line {n_att}); cross-check row '% change', column '5 km' (-35.0%, line {n_pc})",69}7071# ---------------- E2: pp -> percent of control mean ----------------72n_r, r = find_line(RES, "Reemployment | 75.2%")73c = cells(r)74assert c[2] == "75.0%" and c[4].startswith("1.47 pp")75p_ctrl = num(c[2]) # 75.0 (%)76ate_pp = num(c[4]) # 1.47 pp77e2 = 100 * ate_pp / p_ctrl78H["E2"] = {79 "value": round(e2, 2),80 "unit": "percent change relative to the control-group mean reemployment rate",81 "transformation": f"100*Delta/p with Delta = {ate_pp:.2f} pp and p = {p_ctrl:.1f}% (control mean): 100*1.47/75.0 = {e2:.4f}%",82 "harmonisable": True,83 "missing_quantity": None,84 "locator": f"arxiv-2606.14621v1 | Table 2 | row 'Reemployment' | column 'Average treatment effect estimate' (1.47 pp) and column 'Control' (75.0%) (extract line {n_r})",85}8687# ---------------- E3: elasticity * observed MW change ----------------88n_el, el = find_line(MW, "Employment elasticity w.r.t MW")89n_g, g = find_line(MW, "% MW changes")90n_note, note = find_line(MW, "Note: The table reports the effects of a minimum wage increase")91assert cells(el)[1] == "-0.388" and cells(g)[1] == "0.047"92eta = num(cells(el)[1]) # -0.38893gg = num(cells(g)[1]) # 0.047 (proportion)94e3 = 100 * eta * gg95H["E3"] = {96 "value": round(e3, 2),97 "unit": "percent change in employment",98 "transformation": f"100*eta*g with eta = {eta:.3f} and g = {gg:.3f} (proportional MW change, Table 2 row '% MW changes'): 100*(-0.388)*0.047 = {e3:.4f}%",99 "harmonisable": True,100 "missing_quantity": None,101 "locator": f"arxiv-2505.04555v3 | Table 2 | row 'Employment elasticity w.r.t MW' | column 'Estimate (SE)' (-0.388, line {n_el}); row '% MW changes' | column 'Estimate (SE)' (0.047, line {n_g})",102}103checks["E3_alt_with_note_4pct"] = round(100 * eta * 0.04, 4) # sensitivity only, not the reported value104105# ---------------- E4: log points -> absolute change per 100,000 ----------------106n_f, f = find_line(DCR, "Drug-related (fatal) |")107assert cells(f)[3] == "-0.354**"108b4 = num(cells(f)[3]) # -0.354109rel4 = 100 * (math.exp(b4) - 1)110checks["E4_relative_pct_only"] = round(rel4, 4)111H["E4"] = {112 "value": None,113 "unit": "hospitalisations per 100,000 inhabitants per year",114 "transformation": (f"absolute change = R0*(exp(b)-1) with b = {b4:.3f}; exp(-0.354)-1 = {math.exp(b4)-1:.4f} "115 f"(relative change {rel4:.2f}%), but R0, the counterfactual (or pre-treatment) level of the fatal "116 "drug-related hospitalisation rate per 100,000 in treated 5 km regions, is not printed in the extract"),117 "harmonisable": False,118 "missing_quantity": ("baseline/counterfactual level R0 of the fatal drug-related hospitalisation rate per 100,000 inhabitants "119 "per year for the treated 5 km-buffer regions (e.g. pre-opening or never-treated mean rate); "120 "not printed in Section 6, Table 7, Section 6.1 or Table A19 of the extract"),121 "locator": f"arxiv-2609.27732v1 | Table 7 | row 'Drug-related (fatal)' | column '5 km' (-0.354**, extract line {n_f})",122}123124# ---------------- E5: 0-1 probability change -> percentage points ----------------125n_hd, hd = find_line(BEN, "| (1) | (2) | (3) | (4)")126n_t, t = find_line(BEN, "Treatment | -0.0911")127n_m, m = find_line(BEN, "Mean Dep. Var.")128n_c, cc = find_line(BEN, "% Change |")129assert cells(hd)[4] == "(4)" and cells(t)[4] == "-0.0962"130b5 = num(cells(t)[4]) # -0.0962131e5 = 100 * b5132mean5 = num(cells(m)[4]); pct5 = num(cells(cc)[4])133checks["E5_pct_change_consistency"] = {"b_over_mean": round(b5 / mean5, 4), "printed": pct5}134H["E5"] = {135 "value": round(e5, 2),136 "unit": "percentage points",137 "transformation": f"100*b with b = {b5:.4f} (0-1 scale): 100*(-0.0962) = {e5:.2f} pp",138 "harmonisable": True,139 "missing_quantity": None,140 "locator": f"arxiv-2510.03658v1 | Table 3 | row 'Treatment' | column '(4)' (-0.0962, extract line {n_t})",141}142143# ---------------- internal-consistency diagnostics of sources ----------------144n_a19, a19 = find_line(DCR, "ATT | -0.431** | -0.438**")145n_a19p, a19p = find_line(DCR, "% change | -35.0% | -35.5%")146find_line(DCR, "| (0.218) | (0.208)")147find_line(DCR, "| (0.412) | (0.279) | (0.210)")148find_line(DCR, "never-treated comparison regions (640 regions)")149pc_checks = []150for bt, pt in list(zip(cells(att)[1:], cells(pc)[1:])) + list(zip(cells(a19)[1:], cells(a19p)[1:])):151 bv, pv = num(bt), num(pt)152 cv = 100 * (math.exp(bv) - 1)153 pc_checks.append({"b": bv, "printed_pct": pv, "computed_pct": round(cv, 2), "match_1dp": round(cv, 1) == pv})154checks["DCR_pct_change_rows"] = pc_checks155narc_b = num(cells(find_line(DCR, "Narcotics |")[1])[3])156mental_b = num(cells(find_line(DCR, "Mental/Behavioral |")[1])[3])157checks["DCR_text_claims_5km_pct"] = {"fatal": round(rel4, 2), "narcotics": round(100 * (math.exp(narc_b) - 1), 2),158 "mental": round(100 * (math.exp(mental_b) - 1), 2)}159find_line(DCR, "This paper estimates the causal effect")160find_line(DCR, "The sub-outcome results point to two channels")161162mism = [x for x in pc_checks if not x["match_1dp"]]163data_issues = [164 {"issue": "arxiv-2609.27732v1: the abstract states fatal drug-related hospitalisations 'fall by 35%%', but Section 6.1 says 'approximately 30%% at 5 km' and the Table 7 5 km coefficient -0.354 implies 100*(exp(-0.354)-1) = %.1f%%." % rel4,165 "handling": "Internal inconsistency of the source. E4 uses only the Table 7 coefficient as specified; the 35% figure in the abstract is not used."},166 {"issue": "arxiv-2609.27732v1: the standard error of the same 5 km CS-DR ATT (-0.431) is printed as (0.210) in Table 6 but (0.218) in Table A19.",167 "handling": "Does not affect point estimates (no uncertainty propagation required); recorded only."},168 {"issue": "arxiv-2609.27732v1: the Table A19 note says 640 never-treated comparison regions while the tables (and the Table 6 note) report 608 control regions.",169 "handling": "Recorded; does not affect the harmonised values."},170]171for x in mism:172 data_issues.append({"issue": "arxiv-2609.27732v1: printed '%% change' %.1f%% for b = %.3f does not equal 100*(exp(b)-1) = %.2f%% at one decimal." % (x["printed_pct"], x["b"], x["computed_pct"]),173 "handling": "Minor discrepancy (at most 0.1 percentage point; plausibly from the printed coefficient being rounded to 3 decimals) in a cell not used for any harmonised item (the E1 cell matches); recorded only."})174data_issues += [175 {"issue": "arxiv-2609.27732v1: no level (mean) of any hospitalisation rate is printed in the extract, so log-point effects cannot be converted to absolute changes per 100,000.",176 "handling": "E4 declared not harmonisable (value null) with the missing quantity named; no external value substituted."},177 {"issue": "arxiv-2505.04555v3: Table 2 prints '% MW changes' = 0.047 while the table note says 'about 4% minimum wage increase'. Despite the '%' label, 0.047 is read as a proportion (4.7%), consistent with the note's order of magnitude.",178 "handling": "Used the table value g = 0.047 as the target definition requires ('as printed in the same table'); with g = 0.04 the implied change would be %.2f%% (sensitivity only, not reported as the value)." % checks["E3_alt_with_note_4pct"]},179 {"issue": "arxiv-2510.03658v1: the abstract and the first paragraph of Section 4.1 report an effect of 0.0855 (18.79%%, mean 0.455) attributed to 'table 2', which is not in the extract; Table 3 column (4) prints -0.0962 with Mean Dep. Var. 0.261 and %% Change -0.3685 (computed b/mean = %.4f). Footnote markers are rendered as duplicated text (e.g. 'percent^9^9 9')." % (b5 / mean5),180 "handling": "E5 uses Table 3 column (4) as specified by effects.json; the 0.0855 figure is not used. The rendering defect does not affect any number used."},181 {"issue": "arxiv-2510.03658v1: the source calls b/mean a 'percentage change' (a relative change), which differs from the percentage-point target definition.",182 "handling": "E5 reports the absolute change in percentage points (100*b); the relative figure is not used as the harmonised value."},183 {"issue": "arxiv-2606.14621v1: the Table 2 control mean (75.0%) is a raw aggregate group mean, whereas the ATE (1.47 pp) adjusts for randomisation week (the paper notes that raw group comparisons are biased under blocking). Also a rendering defect 'p ¡ 0.001' in the text; the abstract rounds the effect to 1.5 pp.",184 "handling": "Used the printed control mean as p, as the target definition specifies (percent of the control-group mean); flagged as approximate. Used 1.47 pp from Table 2, not the rounded 1.5."},185]186187n_input_rows = {"effects.json": len(effects), "README.md": wc_l(os.path.join(D, "README.md"))}188for fn in sorted(os.listdir(SRC)):189 n_input_rows["sources/" + fn] = wc_l(os.path.join(SRC, fn))190191n_h = sum(1 for v in H.values() if v["value"] is not None)192n_nh = sum(1 for v in H.values() if v["harmonisable"] is False)193assert n_h + n_nh == 5194195results = {196 "task_id": "F4-conf-03",197 "harmonised": H,198 "n_harmonised": n_h,199 "n_not_harmonisable": n_nh,200 "feasibility": "complete",201 "feasibility_reason": None,202 "data_issues": data_issues,203 "n_input_rows": n_input_rows,204 "n_analysed": len(effects),205 "random_seed": None,206 "baseline_printed_estimates": {"E1": b1, "E2": ate_pp, "E3": eta, "E4": b4, "E5": b5},207 "consistency_checks": checks,208}209with open(os.path.join(ROOT, "out", "results.json"), "w", encoding="utf-8") as fo:210 json.dump(results, fo, indent=2, ensure_ascii=False)211print("\nHarmonised values:")212for k, v in H.items():213 print(f" {k}: value={v['value']} unit={v['unit']} harmonisable={v['harmonisable']}")214print(f"n_harmonised={n_h} n_not_harmonisable={n_nh} feasibility=complete")215