1"""F4-conf-03: harmonise five printed effect sizes. Reads data/ only; writes out/results.json.2Deterministic; no randomness. Every printed input is parsed from the extract and the source line printed."""3import json, math, os, re45ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))6D = os.path.join(ROOT, "data")7SRC = os.path.join(D, "sources")8910def lines_of(sid):11 with open(os.path.join(SRC, sid + ".txt"), encoding="utf-8") as f:12 return f.read().split("\n")131415def find_line(sid, prefix, contains=None):16 """Return (line_no, text) of the first line starting with prefix (and containing `contains`)."""17 for i, l in enumerate(lines_of(sid), 1):18 if l.startswith(prefix) and (contains is None or contains in l):19 print(f"[{sid} L{i}] {l.strip()[:220]}")20 return i, l21 raise ValueError(f"line not found: {sid} {prefix}")222324def cells(line):25 return [c.strip() for c in line.split("|")]262728def num(s):29 m = re.search(r"-?\d+(?:\.\d+)?", s.replace(",", ""))30 return float(m.group(0)), m.group(0)313233H = {}34issues = []3536# ---- E1: DCR Table 6, ATT 5 km (log points) -> relative % change37sid = "arxiv-2609.27732v1"38i, l = find_line(sid, "ATT |") # first 'ATT |' line is Table 639b, b_s = num(cells(l)[3])40i2, l2 = find_line(sid, "% change |")41printed_pct = cells(l2)[3]42v1 = round(100 * (math.exp(b) - 1), 2)43H["E1"] = {44 "value": v1, "unit": "percent change relative to the counterfactual hospitalisation rate",45 "transformation": f"100*(exp(b)-1) with b = {b_s} log points -> 100*(exp({b_s})-1) = {100*(math.exp(b)-1):.4f}%, reported to 2 dp (consistent with printed '% change' {printed_pct})",46 "harmonisable": True, "missing_quantity": None,47 "locator": f"{sid} | Table 6 | row 'ATT' (line {i}) | column '5 km'; cross-check row '% change' (line {i2}) | column '5 km'",48}4950# ---- E2: RESEA Table 2, Reemployment ATE pp / control mean51sid = "arxiv-2606.14621v1"52i, l = find_line(sid, "Reemployment |")53c = cells(l)54p, p_s = num(c[2]); d, d_s = num(c[4].split(";")[0])55v2 = round(100 * d / p, 2)56H["E2"] = {57 "value": v2, "unit": "percent change relative to the control-group mean reemployment rate",58 "transformation": f"100*Delta/p with Delta = {d_s} pp (ATE) and p = {p_s}% (control mean) -> 100*{d_s}/{p_s} = {100*d/p:.4f}%, reported to 2 dp",59 "harmonisable": True, "missing_quantity": None,60 "locator": f"{sid} | Table 2 | row 'Reemployment' (line {i}) | columns 'Average treatment effect estimate' (Delta) and 'Control' (p)",61}6263# ---- E3: MW Table 2, elasticity x % MW change64sid = "arxiv-2505.04555v3"65i, l = find_line(sid, "Employment elasticity w.r.t MW")66eta, eta_s = num(cells(l)[1])67i2, l2 = find_line(sid, "% MW changes")68g, g_s = num(cells(l2)[1])69find_line(sid, "Note: The table reports the effects of a minimum wage increase")70v3 = round(100 * eta * g, 4)71H["E3"] = {72 "value": v3, "unit": "percent change in employment",73 "transformation": f"100*eta*g with eta = {eta_s} and g = {g_s} (proportional MW change) -> 100*({eta_s})*{g_s} = {100*eta*g:.4f}% (first-order/linear approximation)",74 "harmonisable": True, "missing_quantity": None,75 "locator": f"{sid} | Table 2 | row 'Employment elasticity w.r.t MW' (line {i}) | column 'Estimate (SE)'; row '% MW changes' (line {i2}) | column 'Estimate (SE)'",76}7778# ---- E4: DCR Table 7 fatal 5 km -> absolute change per 100,000: needs baseline level79sid = "arxiv-2609.27732v1"80i, l = find_line(sid, "Drug-related (fatal) |")81bf, bf_s = num(cells(l)[3])82# search the extract for any printed level of the fatal hospitalisation rate (mean, baseline, pre-period)83level_hits = [k for k, t in enumerate(lines_of(sid), 1)84 if re.search(r"\b(mean|baseline|average rate|pre-treatment level|per 100,000 inhabitants was)\b", t, re.I)85 and re.search(r"fatal", t, re.I) and re.search(r"\d", t)]86print(f"[E4] lines with a printed level of the fatal rate: {level_hits}")87H["E4"] = {88 "value": None, "unit": "hospitalisations per 100,000 inhabitants per year",89 "transformation": f"absolute change = R0*(exp(b)-1) with b = {bf_s}; exp({bf_s})-1 = {math.exp(bf)-1:.4f} (relative), but R0 (counterfactual level of the fatal rate) is not printed, so the absolute change cannot be computed",90 "harmonisable": False,91 "missing_quantity": "counterfactual (or pre-treatment / control-group mean) level of the fatal drug-related hospitalisation rate per 100,000 inhabitants per year for the treated 5 km regions",92 "locator": f"{sid} | Table 7 | row 'Drug-related (fatal)' (line {i}) | column '5 km'",93}9495# ---- E5: Benin Table 3, Treatment col (4), 0-1 scale -> pp96sid = "arxiv-2510.03658v1"97i, l = find_line(sid, "Treatment |")98bt, bt_s = num(cells(l)[4])99v5 = round(100 * bt, 2)100H["E5"] = {101 "value": v5, "unit": "percentage points",102 "transformation": f"100*beta with beta = {bt_s} (probability, 0-1 scale) -> 100*({bt_s}) = {100*bt:.2f} pp (exact rescaling)",103 "harmonisable": True, "missing_quantity": None,104 "locator": f"{sid} | Table 3 | row 'Treatment' (line {i}) | column '(4)'",105}106107# ---- data issues (each backed by printed lines)108print("--- lines supporting data issues ---")109find_line("arxiv-2609.27732v1", "| (0.412)")110find_line("arxiv-2609.27732v1", "| (0.218)")111find_line("arxiv-2609.27732v1", "never-treated comparison regions (640")112issues.append({"issue": "arxiv-2609.27732v1: CS-DR 5 km ATT -0.431 printed with SE 0.210 in Table 6 but 0.218 in Table A19; Table A19 note says 640 never-treated regions while tables say 608.",113 "handling": "Internal inconsistency of the source; irrelevant to point estimates (no SE propagated); recorded only."})114fatal_rel = 100 * (math.exp(bf) - 1)115issues.append({"issue": f"arxiv-2609.27732v1: abstract says fatal hospitalisations 'fall by 35%', Section 6.1 says 'approximately 30%'; Table 7 5 km coefficient {bf_s} implies 100*(exp({bf_s})-1) = {fatal_rel:.1f}%.",116 "handling": "Abstract is inconsistent with Table 7; Table 7 coefficient used as the printed input for E4 (E4 not harmonisable anyway)."})117issues.append({"issue": "arxiv-2609.27732v1: no level (mean, baseline or control) of the fatal drug-related hospitalisation rate per 100,000 is printed in the extract.",118 "handling": "E4 declared not harmonisable; value null; missing quantity named. No external value substituted."})119issues.append({"issue": f"arxiv-2505.04555v3: Table 2 note describes 'about 4% minimum wage increase' while row '% MW changes' prints {g_s}.",120 "handling": f"Used the table row value g = {g_s} as instructed by the target definition; the note's '4%' is a rounded/loose description; with g = 0.04 the result would be {100*eta*0.04:.2f}%."})121issues.append({"issue": "arxiv-2606.14621v1: control mean 75.0% in Table 2 is the raw aggregate control-group mean (not adjusted for randomisation week), while the 1.47 pp ATE is block-adjusted; the paper states raw aggregate comparisons are biased.",122 "handling": "Used 75.0% as p because the target definition specifies 'percent of the control-group mean'; flagged as an approximation of the counterfactual level."})123issues.append({"issue": f"arxiv-2510.03658v1: text cites a 'table 2' estimate -0.0855 with mean 0.455 (table not in extract); printed % Change -0.3685 vs -0.0962/0.261 = {-0.0962/0.261:.4f} (rounding of inputs).",124 "handling": "Not relevant to E5 (pure x100 rescaling of Table 3 col (4)); recorded only."})125126# ---- n_input_rows127n_rows = {}128for fn in sorted(os.listdir(SRC)):129 with open(os.path.join(SRC, fn), "rb") as f:130 n_rows[fn] = f.read().count(b"\n")131with open(os.path.join(D, "README.md"), "rb") as f:132 n_rows["README.md"] = f.read().count(b"\n")133with open(os.path.join(D, "effects.json"), encoding="utf-8") as f:134 effects = json.load(f)135n_rows["effects.json"] = len(effects)136assert [e["id"] for e in effects] == ["E1", "E2", "E3", "E4", "E5"]137138n_h = sum(1 for v in H.values() if v["value"] is not None)139n_nh = sum(1 for v in H.values() if v["harmonisable"] is False)140res = {141 "task_id": "F4-conf-03",142 "harmonised": H,143 "n_harmonised": n_h,144 "n_not_harmonisable": n_nh,145 "feasibility": "complete" if n_h + n_nh == len(effects) else "partial",146 "feasibility_reason": None,147 "data_issues": issues,148 "n_input_rows": n_rows,149 "n_analysed": len(effects),150 "random_seed": None,151}152os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)153with open(os.path.join(ROOT, "out", "results.json"), "w", encoding="utf-8") as f:154 json.dump(res, f, indent=2, ensure_ascii=False)155print("--- harmonised ---")156for k, v in H.items():157 print(k, v["value"], v["unit"], "| harmonisable =", v["harmonisable"])158print("n_harmonised", n_h, "n_not_harmonisable", n_nh, "feasibility", res["feasibility"])159