1#!/usr/bin/env python32"""WP44 — H-1: draw the record-linkage validation sample (500 DBLP↔OpenAlex pairs stratified by linkage method) with3automatic agreement checks (normalised title similarity, year difference, first-author surname match). The PI reviews a4200-pair subsample (column pi_review = TRUE). Output: data/metadata/linkage_sample.csv and linkage_sample_summary.json."""5import argparse, os, json, duckdb, difflib678def main():9 ap = argparse.ArgumentParser(); ap.add_argument("--root", required=True); ap.add_argument("--seed", type=int, default=44); a = ap.parse_args(); R = a.root10 con = duckdb.connect(os.path.join(R, "data/processed/wp44.duckdb"), read_only=True)11 rows = con.execute(f"""12 SELECT l.method, l.dblp_key, l.oa_id, d.title AS dblp_title, d.year AS dblp_year, d.authors[1] AS dblp_first_author, d.doi AS dblp_doi,13 o.title AS oa_title, o.publication_year AS oa_year, o.doi AS oa_doi,14 (SELECT author_name FROM oa_authorships au WHERE au.work_id = o.id ORDER BY position LIMIT 1) AS oa_first_author15 FROM (SELECT *, row_number() OVER (PARTITION BY method ORDER BY hash(dblp_key || '{a.seed}')) rn FROM link_dblp_oa) l16 JOIN dblp_raw d ON d.key = l.dblp_key JOIN oa_works o ON o.id = l.oa_id17 WHERE (l.method = 'doi' AND rn <= 250) OR (l.method = 'title_year' AND rn <= 200) OR (l.method = 'arxiv' AND rn <= 50)""").fetchall()18 out = []; agree = {}19 for i, r in enumerate(rows):20 method, key, oa, dt, dy, da, ddoi, ot, oy, odoi, oa_fa = r21 sim = difflib.SequenceMatcher(None, (dt or "").lower(), (ot or "").lower()).ratio()22 fa = (da or "").split()[-1].lower() if da else ""; ofa = (oa_fa or "").split()[-1].lower() if oa_fa else ""23 auto_ok = sim >= 0.9 and (dy is None or oy is None or abs(dy - oy) <= 1) and (not fa or not ofa or fa == ofa)24 out.append(dict(method=method, dblp_key=key, oa_id=oa, dblp_title=dt, oa_title=ot, dblp_year=dy, oa_year=oy, dblp_doi=ddoi, oa_doi=odoi,25 dblp_first_author=da, oa_first_author=oa_fa, title_similarity=round(sim, 3), auto_ok=auto_ok, pi_review=(i % 5 < 2), pi_verdict=""))26 agree.setdefault(method, [0, 0]); agree[method][0] += 1; agree[method][1] += int(auto_ok)27 import csv28 with open(os.path.join(R, "data/metadata/linkage_sample.csv"), "w", newline="") as fh:29 w = csv.DictWriter(fh, fieldnames=list(out[0].keys())); w.writeheader(); w.writerows(out)30 summ = {m: {"n": n, "auto_ok": k, "auto_precision": round(k / n, 4)} for m, (n, k) in agree.items()}31 json.dump(summ, open(os.path.join(R, "data/metadata/linkage_sample_summary.json"), "w"), indent=1); print(summ)323334if __name__ == "__main__":35 main()36