1"""F4-conf-04: verify five references against the provided source extracts.2Run from the workspace root: ~/wp29/venv/bin/python out/analysis.py3Uses data/ only; deterministic; prints every header line and table cell relied upon."""4import json, math, re, unicodedata5from pathlib import Path67ROOT = Path(__file__).resolve().parent.parent8DATA = ROOT / "data"9refs = json.loads((DATA / "reference_list.json").read_text(encoding="utf-8"))101112def lines_of(rel):13 return (DATA / rel).read_text(encoding="utf-8").split("\n")141516def find(lines, pattern, show=True):17 """Return first line matching regex pattern (and print it)."""18 for i, l in enumerate(lines):19 if re.search(pattern, l):20 if show:21 print(f" L{i+1}: {l.strip()[:300]}")22 return i, l23 raise RuntimeError(f"pattern not found: {pattern}")242526def next_nonblank(lines, i):27 j = i + 128 while not lines[j].strip():29 j += 130 print(f" L{j+1}: {lines[j].strip()}")31 return lines[j]323334def norm_title(t):35 t = unicodedata.normalize("NFKD", t)36 t = "".join(c for c in t if not unicodedata.combining(c))37 t = t.lower().replace("&", "and")38 t = re.sub(r"[^a-z0-9 ]", " ", t)39 return " ".join(t.split())404142def entry_title_year(entry):43 m = re.match(r"^(.*?)\((\d{4})\)\.\s*(.*?)\.\s*arXiv:", entry)44 return int(m.group(2)), m.group(3)454647def header_info(lines):48 title = lines[1].split("Title:", 1)[1].strip()49 bib = [l for l in lines[:10] if l.startswith("Bibliographic record:")][0]50 years = sorted({int(y) for y in re.findall(r"\b\d{1,2} [A-Z][a-z]{2} (\d{4})", bib)})51 body_title = [l for l in lines if l.startswith("# ")][0][2:].strip()52 return title, body_title, bib, years535455def cell(row, k):56 return [c.strip() for c in row.split("|")][k]575859checks, data_issues = {}, []60n_rows = {"reference_list.json": len(refs)}6162for r in refs:63 rid, src = r["id"], r["source_file"]64 L = lines_of(src)65 n_rows[src.split("/")[-1]] = (DATA / src).read_text(encoding="utf-8").count("\n")66 sid = src.split("/")[-1].replace(".txt", "")67 title, body_title, bib, years = header_info(L)68 ey, et = entry_title_year(r["entry"])69 print(f"\n=== {rid} vs {sid}")70 print(f" header Title: {title}")71 print(f" body title : {body_title}")72 print(f" {bib}")73 print(f" entry year {ey}, entry title: {et}")74 tm = norm_title(et) == norm_title(title) == norm_title(body_title)75 first_year, version_year = years[0], years[-1]76 ym = ey in (first_year, version_year)77 print(f" title_matches={tm}; years in record={years}; year_matches={ym}")7879 print(" finding evidence:")80 if rid == "R1":81 i, row = find(L, r"^ATT \| -0\.399")82 se = next_nonblank(L, i)83 _, pc = find(L, r"^% change \| -32\.9%")84 find(L, r"statistically significant at the 5% level")85 find(L, r"roughly 35% within 5 km")86 find(L, r"staggered introduction of DCRs")87 att, se5, pct = cell(row, 3), cell(se, 3), cell(pc, 3)88 calc = 100 * (math.exp(-0.431) - 1)89 print(f" 5 km: ATT {att}, SE {se5}, % change {pct}; exp(-0.431)-1 = {calc:.1f}%")90 ok = att == "-0.431**" and se5 == "(0.210)" and pct == "-35.0%"91 loc = f"{sid} | Table 6, column '5 km', rows ATT / SE / % change (+ abstract)"92 note = ("Table 6 5 km column prints ATT -0.431** (SE 0.210), -35.0%, significant at 5%; abstract gives "93 "roughly 35% within 5 km from staggered DCR openings using Swiss hospital records.")94 j, a19 = find(L, r"^ATT \| -0\.431\*\* \| -0\.438")95 se19 = next_nonblank(L, j)96 find(L, r"never-treated comparison regions \(640 regions\)")97 data_issues.append({"issue": f"{sid}: the same 5 km CS-DR ATT (-0.431**) has SE (0.210) in Table 6 but {cell(se19,1)} in Table A19; "98 "Table A19 note says 640 never-treated regions while Tables 6/A19 rows say 608.",99 "handling": "Cited SE 0.210 matches Table 6 (main results table) exactly, so it is counted as stated; "100 "the 5% significance conclusion holds under either SE (|t| = 2.05 or 1.98). Inconsistency reported, not resolved."})101 find(L, r"which fall by 35%")102 find(L, r"by approximately 30% at 5 km")103 data_issues.append({"issue": f"{sid}: abstract says fatal hospitalisations fall by 35%, Section 6.1 says ~30% (Table 7 5 km fatal ATT -0.354, exp-1 = {100*(math.exp(-0.354)-1):.1f}%).",104 "handling": "Does not concern the cited finding (overall ATT); reported only."})105 elif rid == "R2":106 find(L, r"N = 23,549\) preregistered randomized controlled trial")107 rows = {}108 for key, pat in [("Wage", r"^Wage \|"), ("Reemployment", r"^Reemployment \|"), ("Weeks on UI", r"^Weeks on UI \|")]:109 _, row = find(L, pat)110 rows[key] = cell(row, 4)111 print(f" ATT column: {rows}")112 ok = (rows["Wage"].startswith("$1,153;") and "95% CI: [166, 2,141]" in rows["Wage"]113 and rows["Reemployment"].startswith("1.47 pp;") and rows["Weeks on UI"].startswith("-1.99 weeks;"))114 loc = f"{sid} | Table 2, column 'Average treatment effect estimate', rows Wage / Reemployment / Weeks on UI (+ abstract for N, preregistered RCT)"115 note = ("Table 2 ATT column prints $1,153 (95% CI [166, 2,141]), 1.47 pp and -1.99 weeks; abstract states N = 23,549 "116 "preregistered RCT and that the wage effect is annualised.")117 find(L, r"p ¡ 0\.001")118 data_issues.append({"issue": f"{sid}: rendering defect 'p ¡ 0.001' (should read p < 0.001) in Section 3 text; Tables 2-3 were re-set by the extract editor.",119 "handling": "Does not affect cited values, which were read from Table 2 and cross-checked against Table 3 column 1 and the text."})120 elif rid == "R3":121 find(L, r"stacked coefficient is 0\.005 at")122 find(L, r"prices do not jump immediately")123 find(L, r"^Clean-onset threshold-2 \|")124 ok = False125 loc = f"{sid} | Section 3.2 'Headline dynamic pattern' (stacked coefficients by tau) and Table 3 row 'Clean-onset threshold-2', tau=4/tau=5 columns"126 note = ("Title and year (2024) do not match the extract (title 'Does Entry of Food-and-Drink Establishments Raise Local House Prices?...', "127 "submitted 2026), and the source reports a gradual rise from 0.005 at tau=0 to 0.037/0.034 at tau=4/5 with 'prices do not jump immediately', "128 "contradicting a 3.4-3.7% jump in the entry year with no later appreciation.")129 data_issues.append({"issue": f"{sid}: Table 3 header row garbled in the HTML rendering (column names restored by the extract editor's note).",130 "handling": "Used the editor-supplied column order; tau=4/5 values 0.037/0.034 agree with the Section 3.2 text."})131 elif rid == "R4":132 find(L, r"Japan’s spot labor market")133 i, row = find(L, r"^Employment elasticity w\.r\.t MW \|")134 se1 = next_nonblank(L, i)135 j, row2 = find(L, r"^Emp\. elasticity w\.r\.t affected wage \|")136 se2 = next_nonblank(L, j)137 k, ae = find(L, r"^Affected employment \|")138 se_ae = next_nonblank(L, k)139 _, aw = find(L, r"^Affected wages \|")140 ok = cell(row, 1) == "-0.388" and cell(se1, 1) == "(0.132)" and cell(row2, 1) == "-0.732" and cell(se2, 1) == "(0.091)"141 ratio = float(cell(ae, 1)) / float(cell(aw, 1))142 print(f" arithmetic: affected employment / affected wages = {ratio:.3f} (printed own-wage elasticity -0.732)")143 loc = f"{sid} | Appendix A.1, Table 2, rows 'Employment elasticity w.r.t MW' and 'Emp. elasticity w.r.t affected wage' (+ abstract for Japan's spot labour market)"144 note = ("Table 2 prints -0.388 (0.132) and -0.732 (0.091), and the abstract places the study in Japan's spot labor market; "145 "year 2025 equals the v1 submission year.")146 data_issues.append({"issue": f"{sid}: SE printed for the own-wage elasticity (0.091) is identical to the SE of 'Affected employment' (0.091), although the elasticity is the ratio "147 f"affected employment / affected wages ({ratio:.3f}) and affected wages has SE 0.171; possible duplication in the source table.",148 "handling": "Cannot be resolved from the extract; the cited SE equals the printed value so the finding is counted as stated, with this caveat."})149 elif rid == "R5":150 i, row = find(L, r"^Treatment \| -0\.0911")151 se = next_nonblank(L, i)152 _, pc = find(L, r"^% Change \|")153 _, mean = find(L, r"^Mean Dep\. Var\. \|")154 find(L, r"most preferred specification \(Column \(4\)\)")155 b, s, p, m = cell(row, 4), cell(se, 4), cell(pc, 4), cell(mean, 4)156 print(f" col (4): beta {b}, SE {s}, % change {p}, mean {m}; beta/mean = {float(b)/float(m):.4f}")157 ok = b == "-0.0962" and s == "(0.0129)" and p == "-0.3685"158 loc = f"{sid} | Table 3, column (4) (preferred), rows Treatment / SE / % Change (+ Section 4.1 text)"159 note = ("Table 3 column (4), identified in Section 4.1 as the preferred specification, prints -0.0962 (0.0129) with % change -0.3685 "160 "(=-0.0962/0.261), i.e. 36.85%.")161 find(L, r"result in table 2 suggests")162 data_issues.append({"issue": f"{sid}: Section 4.1 attributes the 0.0855 / 18.79% estimate (also the abstract headline) to Table 2, which is omitted from the extract; "163 "abstract and Table 3 thus report different headline magnitudes for different outcome definitions.",164 "handling": "Cited finding refers explicitly to the Table 3 binary outcome in the preferred column, which is in the extract; the abstract figure was not used."})165 checks[rid] = {"title_matches": bool(tm), "year_matches": bool(ym), "finding_supported": bool(ok),166 "locator": loc, "note": note}167 print(f" -> {rid}: {checks[rid]['title_matches']}, {checks[rid]['year_matches']}, {checks[rid]['finding_supported']}")168169n_rows["README.md"] = (DATA / "README.md").read_text(encoding="utf-8").count("\n")170unver = [k for k, v in checks.items() if not (v["title_matches"] and v["year_matches"] and v["finding_supported"])]171results = {172 "task_id": "F4-conf-04",173 "reference_checks": checks,174 "unverifiable_references": unver,175 "n_verified": len(checks) - len(unver),176 "feasibility": "complete",177 "feasibility_reason": None,178 "data_issues": data_issues,179 "n_input_rows": dict(sorted(n_rows.items())),180 "n_analysed": len(checks),181 "random_seed": None,182}183(ROOT / "out" / "results.json").write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8")184print("\nunverifiable:", unver, "n_verified:", results["n_verified"])185