#!/usr/bin/env python3 """Freeze document-level splits (heldout_v1): 20% held out, stratified by year × article_type, seed 45. Held-out documents never feed profile construction or prompt tuning. Author-surname overlap between splits is measured and reported.""" import json, random, re, sys import pandas as pd sys.path.insert(0, "src") from jof250 import config from jof250.provenance import stamp random.seed(45) man = pd.read_parquet(config.manifests_dir() / "corpus_manifest.parquet"); inc = man[man.decision == "include"].copy() held = [] for (y, t), g in inc.groupby(["bib_year", "article_type"]): ids = sorted(g.doc_id.tolist()); k = round(0.2 * len(ids)) held += random.sample(ids, k) train = sorted(set(inc.doc_id) - set(held)); held = sorted(held) def surnames(a): return {x.strip().split()[-1].lower() for x in str(a).split(";") if x.strip()} tr_auth = set().union(*[surnames(a) for a in inc[inc.doc_id.isin(train)].bib_authors]) if train else set() overlap = [d for d in held if surnames(inc[inc.doc_id == d].bib_authors.iloc[0]) & tr_auth] out = {"protocol": "heldout_v1", "seed": 45, "rule": "20% per (year, article_type) stratum, document level", "n_train": len(train), "n_heldout": len(held), "heldout_with_author_surname_overlap_in_train": len(overlap), "note": "Author overlap is reported, not eliminated: eliminating it would remove most held-out documents in a single-journal corpus. Profiles are built on train only.", "train": train, "heldout": held, **stamp()} (config.ROOT / "data" / "splits").mkdir(exist_ok=True) (config.ROOT / "data" / "splits" / "heldout_v1.json").write_text(json.dumps(out, indent=1)) print({k: v for k, v in out.items() if k not in ("train", "heldout", "host")})