1"""F4-conf-03: harmonise five printed effect sizes to target definitions.2Run from workspace root: ~/wp29/venv/bin/python out/analysis.py3All inputs are parsed from data/ extracts; the source lines used are printed."""4import json, math, os, re56D = "data"7S = os.path.join(D, "sources")89def lines(fn):10 with open(fn, encoding="utf-8") as f:11 return f.read().split("\n")1213def find(fn, pattern):14 """Return (line_no, line) of the unique-first line matching regex pattern."""15 for i, l in enumerate(lines(fn), 1):16 if re.search(pattern, l):17 print(f"[{os.path.basename(fn)} L{i}] {l.strip()}")18 return i, l19 raise ValueError(f"pattern {pattern!r} not found in {fn}")2021def cells(line):22 return [c.strip() for c in line.split("|")]2324def num(s):25 return float(re.sub(r"[^0-9.\-]", "", s))2627effects = json.load(open(os.path.join(D, "effects.json"), encoding="utf-8"))28ids = [e["id"] for e in effects]29assert ids == ["E1", "E2", "E3", "E4", "E5"]3031H = {}32issues = []3334# ---------- E1: DCR overall ATT, 5 km, log points -> percent ----------35f1 = os.path.join(S, "arxiv-2609.27732v1.txt")36_, hdr = find(f1, r"^\| 1 km \| 3 km \| 5 km$")37ln, row = find(f1, r"^ATT \| -0\.399")38c = cells(row); att5_str = c[3]; att5 = num(att5_str) # "-0.431**"39_, pct_row = find(f1, r"^% change \| -32\.9%")40printed_pct = num(cells(pct_row)[3])41v1 = 100 * (math.exp(att5) - 1)42H["E1"] = dict(43 value=round(v1, 2), unit="percent change in the drug-related hospitalisation rate (relative to counterfactual)",44 transformation=f"100*(exp(b)-1) with b = {att5:.3f} (printed '{att5_str}') -> 100*(exp(-0.431)-1) = {v1:.4f}%; "45 f"matches the printed '% change' row ({printed_pct}%). Exact under the log-point convention.",46 harmonisable=True, missing_quantity=None,47 locator=f"arxiv-2609.27732v1 | Table 6 | row 'ATT' (line {ln}) | column '5 km' ; cross-check: Table 6 | row '% change' | column '5 km'")4849# ---------- E2: RESEA reemployment ATE pp -> percent of control mean ----------50f2 = os.path.join(S, "arxiv-2606.14621v1.txt")51ln2, row2 = find(f2, r"^Reemployment \| 75")52c = cells(row2)53ctrl = num(c[2]) # "75.0%"54ate = num(c[4].split(";")[0]) # "1.47 pp"55v2 = 100 * ate / ctrl56H["E2"] = dict(57 value=round(v2, 2), unit="percent change in the reemployment probability (relative to control-group mean)",58 transformation=f"100*Delta/p with Delta = {ate:.2f} pp (ATE) and p = {ctrl:.1f}% (control mean) -> 100*1.47/75.0 = {v2:.4f}%",59 harmonisable=True, missing_quantity=None,60 locator=f"arxiv-2606.14621v1 | Table 2 | row 'Reemployment' (line {ln2}) | column 'Average treatment effect estimate' (1.47 pp) and column 'Control' (75.0%)")6162# ---------- E3: MW elasticity x printed % MW change ----------63f3 = os.path.join(S, "arxiv-2505.04555v3.txt")64ln3a, r3a = find(f3, r"^Employment elasticity w\.r\.t MW")65eta = num(cells(r3a)[1])66ln3b, r3b = find(f3, r"^% MW changes")67g = num(cells(r3b)[1])68_, note3 = find(f3, r"about 4% minimum wage increase")69_, r_miss = find(f3, r"^Missing jobs below new MW")70_, r_exc = find(f3, r"^Excess jobs above new MW")71miss, exc = num(cells(r_miss)[1]), num(cells(r_exc)[1])72v3 = 100 * eta * g73H["E3"] = dict(74 value=round(v3, 2), unit="percent change in employment (for the observed minimum-wage increase)",75 transformation=f"100*eta*g with eta = {eta:.3f} and g = {g:.3f} (proportional MW change printed in the same table) -> 100*(-0.388)*0.047 = {v3:.4f}%. "76 "Linear (first-order) approximation; exact only if the elasticity is interpreted as a ratio of proportional changes.",77 harmonisable=True, missing_quantity=None,78 locator=f"arxiv-2505.04555v3 | Table 2 | row 'Employment elasticity w.r.t MW' (line {ln3a}) | column 'Estimate (SE)' ; Table 2 | row '% MW changes' (line {ln3b}) | column 'Estimate (SE)'")79cs_check = (miss + exc) / g80print(f"Check E3: (missing+excess)/%MW = ({miss}+{exc})/{g} = {cs_check:.4f} vs printed elasticity {eta}")8182# ---------- E4: fatal hospitalisations, log points -> absolute per 100,000 ----------83ln4, r4 = find(f1, r"^Drug-related \(fatal\)")84b4_str = cells(r4)[3]; b4 = num(b4_str)85pct4 = 100 * (math.exp(b4) - 1)86_, abs4 = find(f1, r"fatal outcomes, which fall by 35%")87_, mech4 = find(f1, r"by approximately 30% at 5 km")88print(f"E4 relative-change equivalent (not the target): 100*(exp({b4})-1) = {pct4:.4f}%")89H["E4"] = dict(90 value=None, unit="hospitalisations per 100,000 inhabitants per year (absolute change)",91 transformation=f"Would require Delta_abs = Y0*(exp(b)-1) with b = {b4:.3f} (printed '{b4_str}'); only the relative change "92 f"100*(exp(-0.354)-1) = {pct4:.2f}% can be computed from the extract. Y0 is not printed, so the target cannot be reached.",93 harmonisable=False,94 missing_quantity="baseline (counterfactual / pre-treatment mean) level of the fatal drug-related hospitalisation rate per 100,000 inhabitants per year for the treated 5 km regions (not printed anywhere in the extract; also the time unit of the rate is not stated)",95 locator=f"arxiv-2609.27732v1 | Table 7 | row 'Drug-related (fatal)' (line {ln4}) | column '5 km'")9697# ---------- E5: LPM coefficient (0-1) -> percentage points ----------98f5 = os.path.join(S, "arxiv-2510.03658v1.txt")99_, hdr5 = find(f5, r"^\| \(1\) \| \(2\) \| \(3\) \| \(4\)")100ln5, r5 = find(f5, r"^Treatment \| -0\.0911")101b5 = num(cells(r5)[4])102_, mean5 = find(f5, r"^Mean Dep\. Var\.")103_, pc5 = find(f5, r"^% Change \|")104m5 = num(cells(mean5)[4]); pcp5 = num(cells(pc5)[4])105v5 = 100 * b5106print(f"Check E5: b/mean = {b5}/{m5} = {b5/m5:.4f} vs printed % Change {pcp5}")107H["E5"] = dict(108 value=round(v5, 2), unit="percentage points (change in probability of infant death or stillbirth)",109 transformation=f"100*b with b = {b5:.4f} (0-1 probability scale) -> {v5:.2f} pp. Exact rescaling.",110 harmonisable=True, missing_quantity=None,111 locator=f"arxiv-2510.03658v1 | Table 3 | row 'Treatment' (line {ln5}) | column '(4)'")112113# ---------- data issues detected ----------114_, se_a19 = find(f1, r"^\| \(0\.218\)")115_, n640 = find(f1, r"never-treated comparison regions \(640 regions\)")116_, t2 = find(f5, r"result in table 2 suggests")117issues = [118 {"issue": "arxiv-2609.27732v1: the abstract states fatal drug-related hospitalisations 'fall by 35%%', but Table 7 (5 km) prints -0.354 log points, i.e. 100*(exp(-0.354)-1) = %.1f%%; Section 6.1 says 'approximately 30%%'. The 35%% figure matches the overall outcome (Table 6), not the fatal sub-outcome." % pct4,119 "handling": "Used the Table 7 coefficient as specified in effects.json; flagged the abstract figure as internally inconsistent. Does not affect E4 (not harmonisable anyway)."},120 {"issue": "arxiv-2609.27732v1: the same CS-DR 5 km ATT (-0.431) has SE (0.210) in Table 6 but (0.218) in Table A19; the A19 note states 640 never-treated comparison regions while its table body and Table 6 state 608.",121 "handling": "Point estimates agree, so E1 is unaffected; no uncertainty propagation is required. Recorded only."},122 {"issue": "arxiv-2505.04555v3: Table 2 note describes the 2023 increase as 'about 4%' while the table row '% MW changes' prints 0.047 (4.7%).",123 "handling": "Used the printed table value 0.047, as instructed by the target definition ('as printed in the same table'). With the note's 'about 4%%' (g = 0.04) the result would be %.2f percent; reported as sensitivity only." % (100*eta*0.04)},124 {"issue": "arxiv-2505.04555v3: Table 2 rows are on different scales (e.g. 'Missing jobs below new MW' -0.03 printed with two decimals); the employment elasticity is approximately (missing+excess)/(MW change) = %.3f, consistent with -0.388 up to rounding." % cs_check,125 "handling": "Consistency check only; no change."},126 {"issue": "arxiv-2510.03658v1: the abstract/Section 4.1 headline (-0.0855, mean 0.455, '18.79 percentage change') refers to a Table 2 not included in the extract; it is also labelled inconsistently ('percentage change' vs a relative change). Table 3 col (4) relative change printed as -0.3685 vs -0.0962/0.261 = %.4f (rounding of the printed mean)." % (b5/m5),127 "handling": "Used Table 3 col (4) as specified; the Table 2 numbers were not used."},128 {"issue": "data/README.md mentions a re-set 'London Table 3 header', but no extract in this task concerns a London study (apparent copy-over from another task's README).",129 "handling": "Ignored; no numbers depend on it."},130 {"issue": "arxiv-2606.14621v1: the Table 2 'Control' mean (75.0%) is the raw aggregate control-group mean, not a block(week)-adjusted counterfactual level; the authors stress that raw group comparisons are biased by week of randomisation.",131 "handling": "Used 75.0% because the target definition explicitly asks for percent of the control-group mean; noted that a block-adjusted counterfactual mean is not printed."},132]133134# ---------- input row counts ----------135def wc_l(fn):136 with open(fn, "rb") as f:137 return f.read().count(b"\n")138n_rows = {"effects.json": len(effects), "README.md": wc_l(os.path.join(D, "README.md"))}139for fn in sorted(os.listdir(S)):140 n_rows[fn] = wc_l(os.path.join(S, fn))141142n_h = sum(1 for k in H if H[k]["value"] is not None)143n_nh = sum(1 for k in H if not H[k]["harmonisable"])144res = {145 "harmonised": H,146 "n_harmonised": n_h,147 "n_not_harmonisable": n_nh,148 "task_id": "F4-conf-03",149 "feasibility": "complete",150 "feasibility_reason": None,151 "data_issues": issues,152 "n_input_rows": n_rows,153 "n_analysed": len(effects),154 "random_seed": None,155}156os.makedirs("out", exist_ok=True)157with open(os.path.join("out", "results.json"), "w", encoding="utf-8") as f:158 json.dump(res, f, indent=2, ensure_ascii=False)159print(json.dumps({k: H[k]["value"] for k in H}, indent=1))160