1"""F4-conf-03: harmonise five effect sizes from printed quantities only.2Run from workspace root: ~/wp29/venv/bin/python out/analysis.py3"""4import json, math, re, os56ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))7D = os.path.join(ROOT, "data")8S = os.path.join(D, "sources")910def lines(fn):11 with open(fn, encoding="utf-8") as f:12 return f.read().split("\n")1314def find(fname, pattern):15 """Return (line_no, line) of the unique line starting with pattern; print it."""16 L = lines(os.path.join(S, fname))17 hits = [(i + 1, l) for i, l in enumerate(L) if l.startswith(pattern)]18 assert len(hits) == 1, (fname, pattern, hits)19 print(f"[{fname} L{hits[0][0]}] {hits[0][1]}")20 return hits[0]2122def cells(line):23 return [c.strip() for c in line.split("|")]2425def num(s):26 m = re.search(r"-?\d+(?:,\d{3})*(?:\.\d+)?", s.replace("−", "-"))27 return float(m.group(0).replace(",", ""))2829effects = json.load(open(os.path.join(D, "effects.json")))30H = {}3132# ---- E1: log-point ATT -> percent change33DCR = "arxiv-2609.27732v1.txt"34ln_hdr, hdr = find(DCR, "| 1 km | 3 km | 5 km")35ln_att, att = find(DCR, "ATT | -0.399")36ln_pct, pct = find(DCR, "% change | -32.9%")37c = cells(att); b = num(c[3])38e1 = 100 * (math.exp(b) - 1)39printed_pct = num(cells(pct)[3])40# consistency check of printed % change row for all columns41for k in (1, 2, 3):42 bb = num(cells(att)[k]); pp = num(cells(pct)[k])43 print(f" check col {k}: 100(exp({bb})-1) = {100*(math.exp(bb)-1):.2f}% vs printed {pp}%")44H["E1"] = dict(value=round(e1, 2), unit="percent change in the drug-related hospitalisation rate relative to the counterfactual",45 transformation=f"100*(exp(b)-1) with b = {c[3]} (log points) -> 100*(exp(-0.431)-1) = {e1:.4f}%; agrees with printed '% change' {cells(pct)[3]}",46 harmonisable=True, missing_quantity=None,47 locator=f"arxiv-2609.27732v1 | Table 6 | row 'ATT' (line {ln_att}) | column '5 km' (value -0.431**); cross-check row '% change' (line {ln_pct}) column '5 km' (-35.0%)")4849# ---- E2: pp effect -> relative percent of control mean50RE = "arxiv-2606.14621v1.txt"51find(RE, "Outcome | Treatment | Control |")52ln_r, r = find(RE, "Reemployment | 75.2%")53c = cells(r)54p_ctrl = num(c[2]); delta = num(c[4])55e2 = 100 * delta / p_ctrl56H["E2"] = dict(value=round(e2, 2), unit="percent of the control-group mean reemployment rate",57 transformation=f"100*Delta/p with Delta = {delta} pp (ATE) and p = {p_ctrl}% (control mean) -> 100*1.47/75.0 = {e2:.4f}%",58 harmonisable=True, missing_quantity=None,59 locator=f"arxiv-2606.14621v1 | Table 2 | row 'Reemployment' (line {ln_r}) | column 'Average treatment effect estimate' (1.47 pp) and column 'Control' (75.0%)")6061# ---- E3: elasticity x printed % MW change62MW = "arxiv-2505.04555v3.txt"63ln_el, el = find(MW, "Employment elasticity w.r.t MW")64ln_g, gl = find(MW, "% MW changes")65find(MW, "Note: The table reports the effects of a minimum wage increase")66eta = num(cells(el)[1]); g = num(cells(gl)[1])67e3 = 100 * eta * g68H["E3"] = dict(value=round(e3, 2), unit="percent change in employment",69 transformation=f"100*eta*g with eta = {eta} and g = {g} (printed '% MW changes', a proportion) -> 100*(-0.388)*(0.047) = {e3:.4f}% (first-order/linear approximation)",70 harmonisable=True, missing_quantity=None,71 locator=f"arxiv-2505.04555v3 | Table 2 | row 'Employment elasticity w.r.t MW' (line {ln_el}) | column 'Estimate (SE)' (-0.388); row '% MW changes' (line {ln_g}) | column 'Estimate (SE)' (0.047)")7273# ---- E4: log-point ATT -> absolute change per 100,000: needs baseline level74ln_f, fl = find(DCR, "Drug-related (fatal) |")75bf = num(cells(fl)[3])76rel4 = 100 * (math.exp(bf) - 1)77print(f" E4 relative change (not the target): 100(exp({bf})-1) = {rel4:.2f}%")78H["E4"] = dict(value=None, unit="hospitalisations per 100,000 inhabitants per year",79 transformation=f"Absolute change = Y0*(exp(b)-1) with b = {cells(fl)[3]}; the relative part (exp(-0.354)-1 = {rel4/100:.4f}) is computable, but the counterfactual/baseline level Y0 of the fatal drug-related hospitalisation rate is not printed in the extract, so the absolute change cannot be computed",80 harmonisable=False,81 missing_quantity="baseline (pre-treatment or counterfactual) mean fatal drug-related hospitalisation rate per 100,000 inhabitants per year for the treated regions within 5 km (Y0), which the extract does not print",82 locator=f"arxiv-2609.27732v1 | Table 7 | row 'Drug-related (fatal)' (line {ln_f}) | column '5 km' (-0.354**)")8384# ---- E5: LPM coefficient (0-1 scale) -> pp85BE = "arxiv-2510.03658v1.txt"86ln_t, tl = find(BE, "Treatment | -0.0911")87ln_m, ml = find(BE, "Mean Dep. Var.")88beta = num(cells(tl)[4])89e5 = 100 * beta90H["E5"] = dict(value=round(e5, 2), unit="percentage points",91 transformation=f"100*beta with beta = {cells(tl)[4]} (probability on 0-1 scale) -> {e5:.2f} pp (exact rescaling)",92 harmonisable=True, missing_quantity=None,93 locator=f"arxiv-2510.03658v1 | Table 3 | row 'Treatment' (line {ln_t}) | column '(4)' (-0.0962)")94print(f" E5 cross-check: -0.0962/0.261 = {beta/num(cells(ml)[4]):.4f} vs printed % Change -0.3685")9596data_issues = [97 {"issue": "arxiv-2505.04555v3 Table 2 note describes the 2023 minimum-wage changes as 'about 4% minimum wage increase', whereas the table row '% MW changes' prints 0.047 (4.7%).",98 "handling": f"Used the table value 0.047 as instructed by the target definition ('size of that increase as printed in the same table'); with 4% the implied change would be {100*eta*0.04:.2f}% instead of {e3:.2f}%."},99 {"issue": "arxiv-2505.04555v3 abstract reports 'a 2% employment decline in affected bins', which is a different quantity (affected-bin employment) and not identical to eta*g = -1.82%.",100 "handling": "Not used as an input; noted only for context."},101 {"issue": f"arxiv-2609.27732v1: the abstract says fatal outcomes 'fall by 35%', Section 6.1 says 'approximately 30% at 5 km', while the Table 7 coefficient -0.354 implies 100(exp(-0.354)-1) = {rel4:.1f}%. The abstract's 35% matches the overall ATT (Table 6), not the fatal sub-outcome.",102 "handling": "Relied only on the Table 7 coefficient; the text percentages were not used. E4 is not harmonisable regardless (absolute target)."},103 {"issue": "arxiv-2609.27732v1 Table A19 prints SE (0.218) for the CS-DR ATT -0.431 whereas Table 6 prints (0.210) for the same estimate; the A19 note mentions 640 never-treated comparison regions while the A19 table and Tables 6-7 state 608 control regions.",104 "handling": "Point estimates agree (-0.431, -35.0%); E1 uses Table 6 as specified. SEs are not needed (no uncertainty propagation)."},105 {"issue": "arxiv-2609.27732v1 Table 7 note lists only four sub-outcome definitions; the 'Drug-related (fatal)' category is not defined in the note.",106 "handling": "Recorded; does not affect the computation."},107 {"issue": "arxiv-2606.14621v1 Table 2: the 75.0% control mean is the raw aggregate control-group mean, whereas the 1.47 pp ATE adjusts for randomisation week (blocks); the raw treatment-control difference is only 0.26 pp. The control mean is therefore only an approximate counterfactual level for the block-adjusted ATE.",108 "handling": "Used the printed control-group mean as the target definition requires; flagged as approximation."},109 {"issue": "arxiv-2510.03658v1 text refers to 'table 2' (estimate 0.0855, mean 0.455), which is omitted from the extract; footnote 11 is garbled in rendering (three percentages listed against five wealth categories); Table 3 prints mean dep. var. 0.261 and % change -0.3685 consistent with -0.0962/0.261.",110 "handling": "E5 uses only Table 3 column (4); the Table-2-based numbers in the abstract were not used."},111 {"issue": "arxiv-2606.14621v1 text contains rendering artefact 'p ¡ 0.001'.",112 "handling": "Cosmetic; no effect."},113]114115def nlines(p):116 with open(p, "rb") as f:117 return f.read().count(b"\n")118119n_input = {"effects.json": len(effects), "README.md": nlines(os.path.join(D, "README.md"))}120for fn in sorted(os.listdir(S)):121 n_input[fn] = nlines(os.path.join(S, fn))122123n_h = sum(1 for v in H.values() if v["value"] is not None)124n_nh = sum(1 for v in H.values() if not v["harmonisable"])125res = dict(task_id="F4-conf-03", harmonised=H, n_harmonised=n_h, n_not_harmonisable=n_nh,126 feasibility="complete",127 feasibility_reason=None,128 data_issues=data_issues, n_input_rows=n_input, n_analysed=len(effects), random_seed=None)129os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)130with open(os.path.join(ROOT, "out", "results.json"), "w") as f:131 json.dump(res, f, indent=2, ensure_ascii=False)132for k, v in H.items():133 print(k, v["value"], v["unit"])134