"""F4-conf-04: verify a reference list against provided arXiv extracts. Reads only data/, writes out/results.json deterministically. No randomness. Run from workspace root: ~/wp29/venv/bin/python out/analysis.py """ import json, math, os, re, unicodedata DATA = "data" OUT = os.path.join("out", "results.json") def read_lines(path): with open(path, encoding="utf-8") as f: return f.read().split("\n") def wc_l(path): with open(path, "rb") as f: return f.read().count(b"\n") def norm_title(s): s = unicodedata.normalize("NFKD", s) s = "".join(c for c in s if not unicodedata.combining(c)) s = s.lower().replace("&", " and ") s = re.sub(r"[^a-z0-9 ]", " ", s) return " ".join(s.split()) def find_line(lines, needle): """Return (1-based line number, line) of first line containing needle; raise if absent.""" for i, l in enumerate(lines, 1): if needle in l: return i, l raise AssertionError(f"evidence not found: {needle!r}") def show(src, lines, needle): i, l = find_line(lines, needle) txt = l.strip() if len(txt) > 220: # long prose line: print the matched passage with context k = txt.index(needle) txt = "..." + txt[max(0, k - 40):k + len(needle) + 40] + "..." print(f" [{src} L{i}] {txt}") return l refs = json.load(open(os.path.join(DATA, "reference_list.json"), encoding="utf-8")) n_input_rows = {"README.md": wc_l(os.path.join(DATA, "README.md")), "reference_list.json": len(refs)} # ---- per-reference evidence specification ------------------------------------------- # For each reference: the evidence strings (must exist verbatim in the extract) that decide # the finding check, arithmetic checks, and the decision with its locator. spec = { "R1": dict( evidence=["ATT | -0.399 | -0.283 | -0.431**", "| (0.412) | (0.279) | (0.210)", "% change | -32.9% | -24.7% | -35.0%", "This estimate is statistically significant at the 5% level", "using individual-level hospital records and a difference-in-differences estimator", "staggered introduction of DCRs across Swiss cities", "roughly 35% within 5 km of a facility"], arithmetic=[("exp(-0.431)-1 (5 km % change)", math.exp(-0.431) - 1, -0.350, 0.0005), ("|ATT/SE| = 0.431/0.210 > 1.96", 0.431 / 0.210, 2.052, 0.001)], supported=True, locator="arxiv-2609.27732v1 | Table 6, row ATT, column 5 km (with % change row; Section 6 text; abstract)", note="ATT -0.431** (SE 0.210) and -35.0% at 5 km are printed in Table 6, 5% significance is stated in the text, and the staggered DiD on Swiss hospital records is described in the abstract."), "R2": dict( evidence=["large (N = 23,549) preregistered randomized controlled trial", "$1,153; p=0.022; 95% CI: [166, 2,141]", "1.47 pp; p=0.013; 95% CI: [0.32, 2.62]", "-1.99 weeks; p<0.001; 95% CI: [-2.24, -1.75]", "selection into the program increased annualized wages by $1,153"], arithmetic=[], supported=True, locator="arxiv-2606.14621v1 | Table 2, column 'Average treatment effect estimate', rows Wage, Reemployment, Weeks on UI (N from abstract)", note="Table 2's ATE column prints $1,153 (95% CI [166, 2,141]), 1.47 pp and -1.99 weeks, and the abstract gives the preregistered RCT with N = 23,549."), "R3": dict( evidence=["the stacked coefficient is 0.005 at \\tau=0 , 0.012 at \\tau=1 , 0.018 at \\tau=2 , 0.037 at \\tau=4 , and 0.034 by \\tau=5", "roughly 0.5% in the event year and about 3.4–3.7% after four to five years", "prices do not jump immediately, but instead drift upward", "Clean-onset threshold-2 | 774 | 0.241 | 0.472 | 0.037 | 0.034"], arithmetic=[], supported=False, locator="arxiv-2603.03260v2 | Section 3.2 'Headline dynamic pattern' (stacked coefficients tau=0..5) and Table 3, row Clean-onset threshold-2, columns tau=4/tau=5", note="The extract reports a gradual rise from 0.005 (about 0.5%) at the event year to 0.037/0.034 (3.4-3.7%) only by years four and five and states prices do not jump immediately, contradicting a 3.4-3.7% jump in the entry year with no later appreciation; title and year (2024) also disagree with the record."), "R4": dict( evidence=["Employment elasticity w.r.t MW | -0.388", "Emp. elasticity w.r.t affected wage | -0.732", "our own-wage employment elasticity is - 0.732", "short-run minimum-wage effects in Japan’s spot labor market"], se_after={"Employment elasticity w.r.t MW | -0.388": "| (0.132)", "Emp. elasticity w.r.t affected wage | -0.732": "| (0.091)"}, arithmetic=[("affected employment / affected wages = -0.268/0.366", -0.268 / 0.366, -0.732, 0.0005)], supported=True, locator="arxiv-2505.04555v3 | Appendix A.1, Table 2, rows 'Employment elasticity w.r.t MW' and 'Emp. elasticity w.r.t affected wage' (Japan spot market from abstract)", note="Table 2 prints -0.388 (0.132) and -0.732 (0.091), the text calls the latter the own-wage employment elasticity, and the abstract places the study in Japan's spot labor market."), "R5": dict( evidence=["Treatment | -0.0911 | -0.0908 | -0.0961 | -0.0962", "| (0.0113) | (0.0113) | (0.0126) | (0.0129)", "% Change | -0.3490 | -0.3478 | -0.3681 | -0.3685", "Mean Dep. Var. | 0.261 | 0.261 | 0.261 | 0.261", "Using my most preferred specification (Column (4))", "this effect represents a percentage change of 36.85 percent"], arithmetic=[("0.0962 / 0.261 (relative to mean)", 0.0962 / 0.261, 0.3685, 0.0005)], supported=True, locator="arxiv-2510.03658v1 | Table 3, row Treatment, column (4) (with % Change and Mean Dep. Var. rows; Section 4.1 text)", note="Table 3 column (4), identified as the preferred specification in Section 4.1, prints -0.0962 (0.0129) and a -0.3685 change relative to the mean 0.261, matching the 36.85% stated in the text."), } data_issues = [ {"issue": "arxiv-2609.27732v1: the 5 km CS-DR ATT -0.431 has SE 0.210 in Table 6 but SE 0.218 in Table A19; Table 6 reports 608 never-treated control regions while the Table A19 note says 640.", "handling": "R1 cites Table 6 (SE 0.210), which is printed verbatim; finding judged supported, and with either SE |t| > 1.96 so the 5% significance claim is unaffected. Inconsistency reported, not resolved."}, {"issue": "arxiv-2609.27732v1: abstract says fatal outcomes fall by 35%, Section 6.1 says approximately 30%; exp(-0.354)-1 = -29.8% from Table 7.", "handling": "Not part of any cited finding; recorded only."}, {"issue": "arxiv-2505.04555v3: the SE of the own-wage elasticity (0.091) equals the SE of 'Affected employment' (0.091); the point estimate -0.732 = -0.268/0.366 is reproduced but the SE cannot be checked from the extract.", "handling": "R4 quotes the printed value (0.091); judged supported as stated in the source, possible source typo flagged. The table note's 'about 4%' MW increase vs the '% MW changes' row 0.047 is also noted."}, {"issue": "arxiv-2603.03260v2: Table 3 header row is garbled in the HTML rendering (column order reconstructed by the extract editor).", "handling": "Used the editor's column order; the tau=4/tau=5 values 0.037/0.034 match the Section 3.2 text, so the reconstruction is corroborated."}, {"issue": "arxiv-2510.03658v1: abstract headline (0.0855, 18.79%, Table 2 not in extract, mean 0.455) differs from Table 3 (binary outcome, 0.0962, 36.85%, mean 0.261).", "handling": "Different outcome definitions per Section 4.1; R5 cites the Table 3 result, which is checked directly. Recorded only."}, {"issue": "arxiv-2606.14621v1: Tables 2 and 3 were re-set by the extract editor (values stated verbatim).", "handling": "Values taken as printed; Table 3 column 1 duplicates Table 2's ATE column, providing a cross-check (identical values)."}, ] reference_checks, baseline = {}, {} n_items = 0 for r in refs: rid = r["id"] src_rel = r["source_file"] src_id = os.path.basename(src_rel).replace(".txt", "") path = os.path.join(DATA, src_rel) lines = read_lines(path) n_input_rows[src_rel] = wc_l(path) print(f"\n=== {rid}: {r['entry']}") print(f" cited_for: {r['cited_for']}") # --- header --- _, tline = find_line(lines, "Title: ") show(src_id, lines, "Title: ") bib = show(src_id, lines, "Bibliographic record:") doc_title = tline.split("Title: ", 1)[1].strip() body_title = next(l[2:].strip() for l in lines if l.startswith("# ")) assert norm_title(doc_title) == norm_title(body_title), "header/body title mismatch" # --- entry parsing --- m = re.match(r"^(.*?)\((\d{4})\)\.\s*(.*?)\.\s*arXiv:(\d{4}\.\d{5})\.?$", r["entry"]) entry_year, entry_title, entry_arxiv = int(m.group(2)), m.group(3), m.group(4) assert entry_arxiv in src_id, "arXiv number mismatch" title_ok = norm_title(entry_title) == norm_title(doc_title) # --- year rule: first submission year or year of reproduced version --- dates = re.findall(r"submitted (\d{1,2} \w{3} (\d{4}))", bib) first_year = int(dates[0][1]) ver = re.search(r"arXiv:\d{4}\.\d{5}v(\d+)", bib).group(1) if ver == "1": version_year = first_year else: version_year = int(re.search(r"version %s \(this extract\) submitted \d{1,2} \w{3} (\d{4})" % ver, bib).group(1)) year_ok = entry_year in (first_year, version_year) print(f" title check: entry='{entry_title}' | doc='{doc_title}' -> {title_ok}") print(f" year check: entry={entry_year}, first submission={first_year}, reproduced v{ver}={version_year} -> {year_ok}") # --- baseline: every number in cited_for appears somewhere in the extract --- txt = "\n".join(lines).replace("−", "-") nums = re.findall(r"\d[\d,]*\.?\d*", r["cited_for"].replace("−", "-")) nums = [n.rstrip(".,") for n in nums] missing = [n for n in nums if n not in txt] baseline[rid] = {"title_matches": title_ok, "year_matches": year_ok, "all_cited_numbers_found": len(missing) == 0, "numbers_not_found": missing} # --- finding check --- s = spec[rid] print(" finding evidence:") for e in s["evidence"]: show(src_id, lines, e) n_items += 1 for coef, se in s.get("se_after", {}).items(): i, _ = find_line(lines, coef) nxt = next(l for l in lines[i:] if l.strip()) assert nxt.strip() == se, (coef, nxt) print(f" [{src_id} SE row after '{coef}'] {nxt.strip()}") for label, val, target, tol in s["arithmetic"]: ok = abs(val - target) <= tol assert ok, label print(f" arithmetic: {label} = {val:.4f} (printed {target}) ok") reference_checks[rid] = {"title_matches": bool(title_ok), "year_matches": bool(year_ok), "finding_supported": bool(s["supported"]), "locator": s["locator"], "note": s["note"]} print(f" -> title {title_ok}, year {year_ok}, finding {s['supported']}") unverifiable = [k for k, v in reference_checks.items() if not (v["title_matches"] and v["year_matches"] and v["finding_supported"])] results = { "task_id": "F4-conf-04", "reference_checks": reference_checks, "unverifiable_references": unverifiable, "n_verified": len(reference_checks) - len(unverifiable), "feasibility": "complete", "feasibility_reason": None, "data_issues": data_issues, "n_input_rows": n_input_rows, "n_analysed": len(reference_checks), "random_seed": None, "baseline_naive_number_match": baseline, "n_evidence_strings_verified": n_items, } os.makedirs("out", exist_ok=True) with open(OUT, "w", encoding="utf-8") as f: json.dump(results, f, indent=2, ensure_ascii=False) print(f"\nUnverifiable references: {unverifiable}; n_verified = {results['n_verified']}")