#!/usr/bin/env python3 """WP44 — citation graph of the eligible corpus (Phase 3, structural layer). Builds, in DuckDB and Parquet (data/processed/graph/): cit_edges_internal.parquet (citing_id, cited_id, citing_year, cited_year) both ends eligible cit_edges_external.parquet (citing_id, cited_id, cited_field_id, cited_year, cited_type) cited work outside the corpus, resolved through the slim table of all 465 M OpenAlex works (field of the cited work) field_flows_yearly.csv citing year × cited primary field (OpenAlex field) counts — interdisciplinary inflows (RQ4) cited_age_yearly.csv distribution of citation age (citing year − cited year) by citing year reference_coverage_yearly.csv share of references resolvable (in OpenAlex), internal vs external Category-level flows are produced by knowledge_graph/02_category_flows.py after classification.""" import argparse, os, duckdb def main(): ap = argparse.ArgumentParser(); ap.add_argument("--root", required=True); a = ap.parse_args(); R = a.root G = os.path.join(R, "data/processed/graph"); out = os.path.join(R, "results/graph"); os.makedirs(G, exist_ok=True); os.makedirs(out, exist_ok=True) con = duckdb.connect(os.path.join(R, "data/processed/wp44.duckdb")); con.execute("SET threads TO 24; SET memory_limit='" + os.environ.get("WP44_DUCKDB_MEM", "60GB") + "';") con.execute("CREATE OR REPLACE TEMP TABLE E AS SELECT id, publication_year AS y, type, primary_field_id FROM corpus_works WHERE eligible") con.execute("""CREATE OR REPLACE TABLE cit_edges_internal AS SELECT r.work_id AS citing_id, r.ref_id AS cited_id, a.y AS citing_year, b.y AS cited_year FROM oa_references r JOIN E a ON a.id = r.work_id JOIN E b ON b.id = r.ref_id""") con.execute("""CREATE OR REPLACE TABLE cit_edges_external AS SELECT r.work_id AS citing_id, r.ref_id AS cited_id, a.y AS citing_year, s.publication_year AS cited_year, s.primary_field_id AS cited_field_id, s.type AS cited_type, s.cs_any_topic AS cited_cs_any FROM oa_references r JOIN E a ON a.id = r.work_id LEFT JOIN E b ON b.id = r.ref_id JOIN oa_slim s ON s.id = r.ref_id WHERE b.id IS NULL""") for t in ("cit_edges_internal", "cit_edges_external"): con.execute(f"COPY {t} TO '{G}/{t}.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)") con.execute(f"""COPY (SELECT citing_year, 'internal' AS kind, NULL AS cited_field_id, count(*) n FROM cit_edges_internal GROUP BY 1 UNION ALL SELECT citing_year, 'external', cited_field_id, count(*) FROM cit_edges_external GROUP BY 1,3 ORDER BY 1,2,3) TO '{out}/field_flows_yearly.csv' (HEADER)""") con.execute(f"""COPY (SELECT citing_year, citing_year - cited_year AS age, count(*) n FROM (SELECT citing_year, cited_year FROM cit_edges_internal UNION ALL SELECT citing_year, cited_year FROM cit_edges_external) WHERE cited_year IS NOT NULL GROUP BY 1,2 ORDER BY 1,2) TO '{out}/cited_age_yearly.csv' (HEADER)""") con.execute(f"""COPY (SELECT e.y AS year, count(*) n_works, sum(n_refs) n_refs_declared, (SELECT count(*) FROM cit_edges_internal i WHERE i.citing_year = e.y) n_internal, (SELECT count(*) FROM cit_edges_external x WHERE x.citing_year = e.y) n_external FROM E e JOIN corpus_works c ON c.id = e.id GROUP BY 1 ORDER BY 1) TO '{out}/reference_coverage_yearly.csv' (HEADER)""") print(open(f"{out}/reference_coverage_yearly.csv").read()) if __name__ == "__main__": main()