1"""F4-conf-03: harmonise five printed effect sizes onto target definitions.2Reads only data/; writes out/results.json deterministically. No randomness.3Run from workspace root: ~/wp29/venv/bin/python out/analysis.py4"""5import json, math, os, re67DATA = "data"8SRC = os.path.join(DATA, "sources")91011def lines_of(fname):12 with open(os.path.join(SRC, fname), encoding="utf-8") as f:13 return f.read().split("\n")141516def find_line(fname, startswith, after=None):17 """Return (1-based line number, text) of first line starting with `startswith`18 (optionally after a line containing `after`)."""19 L = lines_of(fname)20 start = 021 if after is not None:22 start = next(i for i, l in enumerate(L) if after in l)23 for i in range(start, len(L)):24 if L[i].startswith(startswith):25 print(f"[{fname}:{i+1}] {L[i]}")26 return i + 1, L[i]27 raise ValueError(f"{startswith!r} not found in {fname}")282930def cells(line):31 return [c.strip() for c in line.split("|")]323334def num(s):35 """Parse a printed number, dropping stars, %, 'pp' and thousands separators."""36 m = re.search(r"-?\d[\d,]*\.?\d*", s.replace("−", "-"))37 return float(m.group(0).replace(",", ""))383940def wc_l(path):41 with open(path, "rb") as f:42 return f.read().count(b"\n")434445effects = json.load(open(os.path.join(DATA, "effects.json"), encoding="utf-8"))46assert [e["id"] for e in effects] == ["E1", "E2", "E3", "E4", "E5"]47H, baseline = {}, {}4849# ---------- E1: Armendariz Table 6, ATT, 5 km (log points -> % change) ----------50f1 = "arxiv-2609.27732v1.txt"51_, hdr = find_line(f1, "| 1 km | 3 km | 5 km", after="Table 6:")52ln, row = find_line(f1, "ATT |", after="Table 6:")53c = cells(row); col = cells(hdr).index("5 km")54b1_txt = c[col]; b1 = num(b1_txt)55lnp, rowp = find_line(f1, "% change |", after="Table 6:")56pc_printed = cells(rowp)[col]57v1 = 100.0 * (math.exp(b1) - 1.0)58H["E1"] = dict(value=round(v1, 4), unit="percent (relative change vs counterfactual rate)",59 transformation=f"100*(exp(b)-1) with b = {b1_txt} log points -> 100*(exp({b1})-1) = {v1:.4f}%",60 harmonisable=True, missing_quantity=None,61 locator=f"arxiv-2609.27732v1 | Table 6 | row 'ATT' (line {ln}) | column '5 km': {b1_txt}; "62 f"cross-check row '% change' (line {lnp}) column '5 km': {pc_printed}")63baseline["E1"] = {"printed_value": b1_txt, "printed_unit": "log points", "author_printed_conversion": pc_printed}6465# ---------- E2: Li et al. Table 2, Reemployment ATE (pp -> % of control mean) ----------66f2 = "arxiv-2606.14621v1.txt"67_, hdr2 = find_line(f2, "Outcome | Treatment | Control")68ln2, row2 = find_line(f2, "Reemployment |", after="Outcome | Treatment | Control")69h2, c2 = cells(hdr2), cells(row2)70ctrl_txt = c2[h2.index("Control")]71ate_cell = c2[h2.index("Average treatment effect estimate")]72ate_txt = ate_cell.split(";")[0]73p_ctrl, d2 = num(ctrl_txt), num(ate_txt)74v2 = 100.0 * d2 / p_ctrl75H["E2"] = dict(value=round(v2, 4), unit="percent (relative change vs control-group mean)",76 transformation=f"100*Delta/p with Delta = {ate_txt}, p = {ctrl_txt} -> 100*{d2}/{p_ctrl} = {v2:.4f}%",77 harmonisable=True, missing_quantity=None,78 locator=f"arxiv-2606.14621v1 | Table 2 | row 'Reemployment' (line {ln2}) | column 'Average treatment effect estimate': {ate_txt}; "79 f"column 'Control': {ctrl_txt}")80baseline["E2"] = {"printed_value": ate_txt, "printed_unit": "percentage points", "author_printed_conversion": None}8182# ---------- E3: Kanayama et al. Table 2, elasticity x printed MW change ----------83f3 = "arxiv-2505.04555v3.txt"84ln3, row3 = find_line(f3, "Employment elasticity w.r.t MW |")85ln3g, row3g = find_line(f3, "% MW changes |")86find_line(f3, "Note: The table reports the effects of a minimum wage increase")87eta_txt, g_txt = cells(row3)[1], cells(row3g)[1]88eta, g = num(eta_txt), num(g_txt)89v3 = 100.0 * eta * g90H["E3"] = dict(value=round(v3, 4), unit="percent (relative change in employment)",91 transformation=f"100*eta*g with eta = {eta_txt}, g = {g_txt} (proportional MW change) -> 100*({eta})*({g}) = {v3:.4f}%",92 harmonisable=True, missing_quantity=None,93 locator=f"arxiv-2505.04555v3 | Table 2 | row 'Employment elasticity w.r.t MW' (line {ln3}) | column 'Estimate (SE)': {eta_txt}; "94 f"row '% MW changes' (line {ln3g}) | column 'Estimate (SE)': {g_txt}")95baseline["E3"] = {"printed_value": eta_txt, "printed_unit": "elasticity", "author_printed_conversion": None}9697# ---------- E4: Armendariz Table 7, fatal, 5 km (log points -> absolute per 100,000) ----------98_, hdr7 = find_line(f1, "Outcome | 1 km | 3 km | 5 km", after="Table 7:")99ln4, row4 = find_line(f1, "Drug-related (fatal) |", after="Table 7:")100b4_txt = cells(row4)[cells(hdr7).index("5 km")]; b4 = num(b4_txt)101rel4 = 100.0 * (math.exp(b4) - 1.0)102# Search the extract for any printed level of the fatal hospitalisation rate (mean / baseline).103txt1 = "\n".join(lines_of(f1))104level_hits = re.findall(r"(?i)(mean|baseline level|pre-treatment (mean|level)|average rate)[^\n]{0,80}per 100,000", txt1)105print(f"[E4] search for a printed level of the fatal rate per 100,000: {len(level_hits)} hit(s)")106H["E4"] = dict(value=None, unit="hospitalisations per 100,000 inhabitants per year",107 transformation=(f"Would require: Delta = Y0*(exp(b)-1) with b = {b4_txt} log points and Y0 the counterfactual "108 f"fatal rate per 100,000 per year. Only the relative change 100*(exp({b4})-1) = {rel4:.4f}% "109 f"is computable from the extract; Y0 is not printed."),110 harmonisable=False,111 missing_quantity=("the counterfactual (or pre-treatment / control-group mean) level of the fatal drug-related "112 "hospitalisation rate per 100,000 inhabitants per year for the 5 km treated regions "113 "(not printed in the extract; Tables 6, 7, A19 print only log-point ATTs, SEs and counts)"),114 locator=f"arxiv-2609.27732v1 | Table 7 | row 'Drug-related (fatal)' (line {ln4}) | column '5 km': {b4_txt}")115baseline["E4"] = {"printed_value": b4_txt, "printed_unit": "log points", "author_printed_conversion": None,116 "computable_relative_change_percent": round(rel4, 4)}117118# ---------- E5: Tossou Table 3, Treatment, column (4) (0-1 -> pp) ----------119f5 = "arxiv-2510.03658v1.txt"120_, hdr5 = find_line(f5, "| (1) | (2) | (3) | (4)")121ln5, row5 = find_line(f5, "Treatment |", after="Table 3:")122b5_txt = cells(row5)[cells(hdr5).index("(4)")]; b5 = num(b5_txt)123v5 = 100.0 * b5124H["E5"] = dict(value=round(v5, 4), unit="percentage points",125 transformation=f"100*beta with beta = {b5_txt} (0-1 probability scale) -> 100*({b5}) = {v5:.4f} pp",126 harmonisable=True, missing_quantity=None,127 locator=f"arxiv-2510.03658v1 | Table 3 | row 'Treatment' (line {ln5}) | column '(4)': {b5_txt}")128baseline["E5"] = {"printed_value": b5_txt, "printed_unit": "probability (0-1)", "author_printed_conversion": None}129130# ---------- consistency evidence for data_issues ----------131find_line(f1, "This paper estimates the causal effect") # abstract (fatal 'fall by 35%')132find_line(f1, "The sub-outcome results point to two channels") # Sec 6.1 (~30%)133_, a19 = find_line(f1, "| (0.218)", after="Table A19: Robustness")134find_line(f1, "never-treated comparison regions (640 regions)")135_, a19pc = find_line(f1, "% change |", after="Table A19: Robustness")136_, a19b = find_line(f1, "ATT |", after="Table A19: Robustness")137a19_check = {h: (round(100 * (math.exp(num(b)) - 1), 1), p) for h, b, p in138 zip(["CS-DR", "Sun & Abraham", "Synthetic DiD", "Stacked DiD"], cells(a19b)[1:], cells(a19pc)[1:])}139print("[A19 check] computed vs printed % change:", a19_check)140_, t5pc = find_line(f5, "% Change |"); _, t5m = find_line(f5, "Mean Dep. Var. |")141t5_check = round(b5 / num(cells(t5m)[4]), 4)142print(f"[Tossou check] -0.0962/0.261 = {t5_check} vs printed {cells(t5pc)[4]}")143find_line(f2, "The results for the short-term outcomes") # 'p ¡ 0.001' rendering144145data_issues = [146 {"issue": f"arxiv-2609.27732v1: abstract says fatal drug-related hospitalisations 'fall by 35%', but Table 7 (5 km) prints {b4_txt} "147 f"log points, i.e. {rel4:.1f}%; Section 6.1 says 'approximately 30%'.",148 "handling": "Table 7 value used for E4; abstract figure not used."},149 {"issue": "arxiv-2609.27732v1: Table A19 prints SE (0.218) for the CS-DR 5 km ATT -0.431, whereas Table 6 prints (0.210) for the same estimate; "150 "A19 note says 640 never-treated regions while the table body and Tables 6-7 say 608.",151 "handling": "Not used: no standard errors are propagated and region counts do not enter any transformation. Reported only."},152 {"issue": f"arxiv-2609.27732v1: Table A19 '% change' for Synthetic DiD and Stacked DiD differ by 0.1 from 100*(exp(b)-1) on the printed coefficients "153 f"(computed vs printed: {a19_check}).",154 "handling": "Not used (E1 uses Table 6, where printed -35.0% matches the computed value). Attributed to rounding of coefficients."},155 {"issue": "arxiv-2609.27732v1: no level (mean, baseline or counterfactual) of the fatal drug-related hospitalisation rate per 100,000 is printed, "156 "and the time unit of the panel ('per year') is not stated explicitly.",157 "handling": "E4 declared not harmonisable (value null); missing quantity named; no external value substituted."},158 {"issue": f"arxiv-2505.04555v3: Table 2 note describes 'about 4% minimum wage increase' while the row '% MW changes' prints {g_txt} (4.7%).",159 "handling": f"Task instructs use of the size printed in the table: g = {g_txt}. With 0.04 the result would differ; noted in the report."},160 {"issue": f"arxiv-2510.03658v1: abstract/text quote an effect of 0.0855 relative to a mean of 0.455 from 'table 2' (not in the extract); "161 f"Table 3 col (4) '% Change' prints {cells(t5pc)[4]} vs {t5_check} recomputed from printed coefficient and mean (rounding); footnote 11 text is garbled.",162 "handling": "Only Table 3, row Treatment, column (4) is used for E5, as specified in effects.json."},163 {"issue": "arxiv-2606.14621v1: rendering defect 'p ¡ 0.001' in the short-term results paragraph; the control mean (75.0%) is the raw aggregate "164 "control-group mean, whereas the ATE (1.47 pp) is block (randomisation-week) adjusted.",165 "handling": "Rendering defect irrelevant to E2. Control mean used as the counterfactual level p as the target definition requires; caveat stated."},166]167168n_input_rows = {"effects.json": len(effects), "README.md": wc_l(os.path.join(DATA, "README.md"))}169for fn in sorted(os.listdir(SRC)):170 n_input_rows[fn] = wc_l(os.path.join(SRC, fn))171172n_h = sum(1 for v in H.values() if v["value"] is not None)173n_nh = sum(1 for v in H.values() if v["harmonisable"] is False)174results = {175 "task_id": "F4-conf-03",176 "harmonised": H,177 "n_harmonised": n_h,178 "n_not_harmonisable": n_nh,179 "feasibility": "complete",180 "feasibility_reason": None,181 "data_issues": data_issues,182 "n_input_rows": n_input_rows,183 "n_analysed": len(effects),184 "random_seed": None,185 "baseline_reported": baseline,186}187os.makedirs("out", exist_ok=True)188with open(os.path.join("out", "results.json"), "w", encoding="utf-8") as f:189 json.dump(results, f, indent=2, ensure_ascii=False)190print("\nHarmonised values:")191for k, v in H.items():192 print(f" {k}: value={v['value']} [{v['unit']}] harmonisable={v['harmonisable']}")193print(f"n_harmonised={n_h} n_not_harmonisable={n_nh} feasibility=complete")194