SPB Git

spb/wp44_uqo

Public

WP44: figure batch 2, embeddings multi-node drivers, semantic atlas, writer briefs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 3, 2026) parent a5b49ef Browse files

28 changed files +333 −6

added agents/briefs/AGENT_K_CRITIC.md +12 −0
@@ -0,0 +1,12 @@
1+# Brief — Agent K (Scientific Critic)
2+
3+You receive a structured artefact (a specialist review directory, an econometric result set, or a manuscript part) and
4+return `reports/critic/<artefact>_critique.md` + `<artefact>_issues.csv` (issue_id, location, severity ∈ {blocking, major,
5+minor}, type ∈ {unsupported_claim, causal_language, missing_uncertainty, metadata_as_fulltext, preprint_as_peer_reviewed,
6+number_without_artifact, citation_unverified, overclaim, inconsistency, method_weakness}, quote, required_fix).
7+Checks (CLAUDE.md §1, §10): every number traces to a file; every claim has evidence_type and supporting keys; no causal
8+language about ChatGPT ("after" not "because"); preprints never called peer-reviewed; abstracts never treated as full text;
9+"largest ever" and similar superlatives absent unless independently established; uncertainty reported; competing
10+explanations considered (indexing lag, OpenAlex classifier drift, venue growth, XPAC, duplicates). You may run the scripts to
11+re-derive a number. Verdict: ACCEPT / ACCEPT WITH FIXES / REJECT, with the blocking issues listed first. You never edit the
12+artefact yourself.
added agents/briefs/WRITER_COMMON.md +26 −0
@@ -0,0 +1,26 @@
1+# Common rules for manuscript writers (Agent L instances), WP44
2+
3+Repository: /Users/simon-pierreboucher/Desktop/UQO/Recherche/Recherche-UQO/informatique/wp44 . Read CLAUDE.md §1, §9, §10 and
4+RESEARCH_PLAN.md first. The manuscript is `manuscript/main.tex` (UQO template, natbib author-year, `\bibliographystyle{aer}`,
5+`\part{}` already declared in main.tex — you write `\section`/`\subsection` content only, in the file assigned to you).
6+
7+Style (CLAUDE.md §10): formal academic English, critical, precise, no promotional language, no repetitive AI prose (vary
8+sentence structure, no "In this section we…" openers in every subsection, no bullet lists in body prose except where a list is
9+the natural structure), real mathematical notation where it carries meaning, assumptions and limitations explicit. Length:
10+the target page count for your part is given in your brief; do not pad.
11+
12+Citations: `\citep{key}` / `\citet{key}`. You may cite only keys that exist in `manuscript/references.bib` (being built by
13+another agent — check it) OR keys you add to YOUR OWN bib file `manuscript/references_<part>.bib`, every entry verified
14+by a Crossref/OpenAlex/arXiv single lookup (`curl -s "https://api.crossref.org/works/<DOI>?mailto=spbou4@icloud.com"`,
15+`https://api.openalex.org/works/https://doi.org/<DOI>`, `https://export.arxiv.org/api/query?id_list=<id>`), recorded in
16+`manuscript/references_verification_<part>.csv` (key, doi_or_arxiv, verified_via, verified_title, match). Never invent a
17+reference. If you cannot verify, do not cite. Keys: `surnameYYYYkeyword`. Main-bib keys you reuse: do not redefine them.
18+
19+Numbers: never type a corpus number. Use the macros of `manuscript/tables/numbers.tex` (generated; list them with
20+`grep newcommand manuscript/tables/numbers.tex`). If a number you need has no macro, write `\pending{macro name}` and list
21+it in your report. Results sections are written by others; do not describe empirical findings of WP44 beyond the macros.
22+
23+Integrity: no causal claim about ChatGPT; preprints are "preprints"; what you know only from an abstract is said so; no
24+"largest review ever". Deliverable = your .tex file(s), your bib + verification CSV, and a report ≤ 20 lines (what you wrote,
25+word count, citations added, pending macros, anything uncertain). Compile check: `cd manuscript && latexmk -pdf -interaction=nonstopmode main.tex`
26+may fail on other parts — check only that YOUR file has no LaTeX syntax errors (e.g. with `lacheck` or by including it alone in a scratch document).
added agents/briefs/WRITER_PART02.md +18 −0
@@ -0,0 +1,18 @@
1+# Brief — Part II "The Evolution of Computer Science" (file manuscript/sections/part02_evolution.tex; bib manuscript/references_part02.bib)
2+
3+Target 25–35 pages (≈ 12,000–16,000 words). Purpose (CLAUDE.md §9): the conceptual and historical background needed to
4+interpret post-2022 developments — not a history of computing in general. Suggested sections (adapt):
5+1. The disciplinary structure of computer science (ACM CCS, the 40-category ontology of this paper, how fields relate; how
6+ bibliometric field classifications — OpenAlex topics, DBLP venues, arXiv categories — encode that structure and where they disagree).
7+2. Three prior transformations and how they propagated across subfields: the internet/web era, the deep-learning wave
8+ (2012–2017: AlexNet, ResNet, seq2seq, attention), the transformer/pretraining era (2017–2022: Transformer, BERT, GPT-2/3,
9+ scaling laws, CLIP, diffusion, AlphaFold). For each: what changed in publication practice (preprints, benchmarks, industry labs).
10+3. The state of each non-AI area at the baseline (2018–2022): systems (cloud, serverless, disaggregation), hardware
11+ (end of Dennard scaling, accelerators, "golden age"), theory (fine-grained complexity, learning theory), PL/verification
12+ (Rust, proof assistants), security/crypto (post-quantum standardisation, ML security), HCI, graphics (NeRF), quantum (NISQ),
13+ bioinformatics (AlphaFold 2) — concise, citation-backed.
14+4. November 2022 as a reference date: what ChatGPT was and was not (an interface + RLHF over GPT-3.5), the preceding
15+ releases (InstructGPT, Codex, Stable Diffusion), why the date is used descriptively and not causally (CLAUDE.md §6.5).
16+5. What the literature already claims about the post-2022 transformation (growth of AI papers, LLM-assisted writing, arXiv
17+ submission surges, benchmark saturation, industry concentration) and why those claims need the corpus-level test of this paper.
18+Landmark references: verify each (group G/H of the reference brief). Coordinate with `manuscript/references.bib` if present.
added agents/briefs/WRITER_PART03.md +15 −0
@@ -0,0 +1,15 @@
1+# Brief — Part III "Data and Systematic Review Protocol" (file manuscript/sections/part03_data.tex; bib manuscript/references_part03.bib)
2+
3+Target 25–35 pages. Explain corpus construction, source coverage, deduplication, eligibility and research integrity
4+(CLAUDE.md §9, PRISMA 2020 items where applicable). Sources of truth to read: RESEARCH_PLAN.md §2–§3, protocol/PROTOCOL.md,
5+protocol/amendments.csv, reports/PHASE0_FEASIBILITY.md, data/raw/README.md, ingestion/*.py (what each does, measured sizes),
6+systematic_review/define_corpus.py (exclusion codes E1–E7, the CS-core rule, date precision), data/metadata/linkage_report.json,
7+data/metadata/prisma_counts.json, figures/FIGURES.json (fig_prisma_flow, fig_volume_yearly, fig_types_openness exist in
8+manuscript/figures/ — reference them with \includegraphics{fig_prisma_flow.pdf} etc.; copy from figures/ to manuscript/figures/ if missing).
9+Sections: 1 sources and access (licences, snapshot release, API budget facts, Semantic Scholar unavailable); 2 identification
10+(what each source contributes, sizes via macros); 3 linkage (methods, counts via macros, the second snapshot pass and why);
11+4 deduplication of preprint/published pairs; 5 eligibility (codes, counts, decisions A-001/A-002 with their evidence);
12+6 the CS-core vs penumbra design and the strict core; 7 date precision, indexing lag, right truncation (created_date is
13+unusable for lag — explain); 8 quality indicators and known biases (DBLP coverage, OpenAlex topic noise, XPAC, Zenodo flood,
14+missing abstracts, forward-dated records); 9 research-integrity procedures (preregistration, amendments log, human checkpoints,
15+agent register, no fabrication rules). Every number through macros (`\PrismaCsCoreEligible`, `\LinkDblpLinkedDistinct`, …).
added agents/briefs/WRITER_PART04.md +27 −0
@@ -0,0 +1,27 @@
1+# Brief — Part IV "Computational Methodology" (file manuscript/sections/part04_methodology.tex; bib manuscript/references_part04.bib)
2+
3+Target 35–50 pages (≈ 15,000–22,000 words) with real mathematics. Document EXACTLY what the code does — read these files
4+before writing: RESEARCH_PLAN.md §3–§7, protocol/PROTOCOL.md, ingestion/link_sources.py, systematic_review/define_corpus.py,
5+taxonomy/classify.py + taxonomy/ontology.yaml (assignment_rule) + taxonomy/VALIDATION.md, bibliometrics/01–03, knowledge_graph/01–03,
6+econometrics/structural_change.py + run_structural_change.py, embeddings/embed_corpus.py + semantic_atlas.py,
7+experiments/emerging_detection.py. Where the code and the plan differ, describe the code and flag the difference in your report.
8+Sections:
9+1. Notation and the corpus as a measurable object (works w, dates, categories C(w) ⊆ 𝒦, periods B/P, month-precision filter).
10+2. Record linkage and deduplication (deterministic DOI/arXiv keys, normalised-title blocking, precision estimation design).
11+3. Multi-label classification: score s_k(w) = Σ_signal min(1, Σ weights), threshold, primary rule, validation metrics
12+ (precision/recall/F1, Cohen's κ, Jaccard), and the known failure modes.
13+4. Bibliometric indicators with definitions and estimators: counts, shares, CAGR, fixed-window citations c12/c36,
14+ field-normalised NCS, FWCI (OpenAlex definition, cite the documentation), Gini, HHI, fractional counting, international
15+ collaboration share, Rao–Stirling and Leinster–Cobbold diversity (define with formulas), citation-age bias and why fixed windows.
16+5. Networks: citation graph internal/external, category flow matrices F_t, modularity Q (Newman–Girvan formula as implemented
17+ in knowledge_graph/02), cross-area share; co-authorship/country networks.
18+6. Semantic mapping: SPECTER2/SciNCL/MiniLM embeddings, cosine geometry, UMAP + HDBSCAN (state assumptions), stability by
19+ split-half ARI, category centroid drift, novelty definition.
20+7. Structural change: ITS model with seasonality and HAC errors (write the regression), placebo distribution and its p-value,
21+ Bai–Perron DP with BIC/LWZ (state the SSR criterion and minimum segment h), PELT penalty, BOCPD with Normal–Inverse-Gamma
22+ conjugacy (give the predictive and the run-length recursion), alternative event dates, right-truncation handling.
23+8. Emerging-topic detection: features, label definition at horizon h, train/test cutoffs, metrics, look-ahead safeguards,
24+ the preregistered known-emergence set.
25+9. Knowledge-graph extraction: schema, provenance span, machine verification, confidence; what is and is not evidence.
26+10. Uncertainty and reproducibility: bootstrap/HAC intervals, seeds, ledgers, hashes.
27+Cite method sources (Waltman 2016, Bai–Perron, Killick, Adams–MacKay, McInnes UMAP/HDBSCAN, Cohan SPECTER, Stirling, Newman…) — verify each.
added embeddings/collect_multinode.sh +4 −0
@@ -0,0 +1,4 @@
1+#!/bin/sh
2+cd ~/wp44_uqo; MODEL=${1:-allenai/specter2_base}; TAG=$(echo $MODEL | sed 's#/#__#g')
3+for node in M3U96a M4M64a M4M64c; do rsync -aq $node:~/wp44_uqo/data/processed/embeddings/$TAG/ data/processed/embeddings/$TAG/ && echo "$node collected"; done
4+ls data/processed/embeddings/$TAG/shard_*.npy | wc -l
added embeddings/run_multinode.sh +17 −0
@@ -0,0 +1,17 @@
1+#!/bin/sh
2+# Distribute embedding shards over the nodes (run from M3U96b after `embed_corpus.py --prepare`).
3+# Each node gets a contiguous shard range proportional to its measured throughput; results are rsynced back to M3U96b.
4+# Usage: sh embeddings/run_multinode.sh allenai/specter2_base
5+cd ~/wp44_uqo; MODEL=${1:-allenai/specter2_base}; TAG=$(echo $MODEL | sed 's#/#__#g')
6+NS=$(ls -d data/processed/embeddings/input/shard=* | wc -l | tr -d ' ')
7+# weights: M3U96b 3, M3U96a 3, M4M64a 2, M4M64c 2 (M3 Ultra ≈ 300 texts/s, M4 Max ≈ 200 texts/s measured/estimated)
8+A=$((NS*3/10)); B=$((NS*3/10)); C=$((NS*2/10)); D=$((NS - A - B - C))
9+echo "shards: $NS → M3U96b 0-$A, M3U96a $A-$((A+B)), M4M64a $((A+B))-$((A+B+C)), M4M64c $((A+B+C))-$NS"
10+for spec in "M3U96a:$A:$((A+B))" "M4M64a:$((A+B)):$((A+B+C))" "M4M64c:$((A+B+C)):$NS"; do
11+ node=${spec%%:*}; rest=${spec#*:}; from=${rest%%:*}; to=${rest#*:}
12+ ssh $node "mkdir -p ~/wp44_uqo/data/processed/embeddings/input"
13+ k=$from; while [ $k -lt $to ]; do rsync -aq data/processed/embeddings/input/shard=$k $node:~/wp44_uqo/data/processed/embeddings/input/ ; k=$((k+1)); done
14+ ssh $node "cd ~/wp44_uqo; . .venv/bin/activate; nohup python embeddings/embed_corpus.py --root ~/wp44_uqo --run --model $MODEL --shard-from $from --shard-to $to > logs/embed_$TAG.log 2>&1 &" && echo "$node started $from-$to"
15+done
16+. .venv/bin/activate; nohup python embeddings/embed_corpus.py --root ~/wp44_uqo --run --model $MODEL --shard-from 0 --shard-to $A > logs/embed_$TAG.log 2>&1 &
17+echo "M3U96b started 0-$A; collect with: sh embeddings/collect_multinode.sh $MODEL"
added embeddings/semantic_atlas.py +93 −0
@@ -0,0 +1,93 @@
1+#!/usr/bin/env python3
2+"""WP44 — semantic research mapping (RQ2/RQ5/RQ10 inputs): UMAP projection, HDBSCAN clusters, stability, temporal drift.
3+Inputs: data/processed/embeddings/<model>/shard_*.npy (+ ids), corpus_works, classification (primary categories).
4+Steps: (1) stratified sample of --n works (seed 44) for the 2-D atlas; (2) HDBSCAN on a 50-D UMAP of the sample, cluster labels
5+and top TF-IDF terms per cluster; (3) stability: ARI between HDBSCAN partitions of two bootstrap halves projected into the same
6+UMAP (repeated 5×); (4) category centroids per year in the native 768-D space, cosine drift year-over-year, inter-category
7+cosine similarity matrix per period (baseline vs post) → tests H4/H5 at the semantic level; (5) per-work novelty = 1 − max cosine
8+similarity to the previous 24 months' centroid of the same category (feeds experiments/emerging_detection.py).
9+Outputs: results/semantic/{atlas_sample.parquet, clusters.csv, cluster_terms.csv, stability.json, category_drift.csv,
10+category_similarity_<period>.csv, novelty.parquet}."""
11+import argparse, glob, json, os, numpy as np, pandas as pd, duckdb
12+
13+
14+def load_embeddings(root, tag):
15+ E, I = [], []
16+ for f in sorted(glob.glob(f"{root}/data/processed/embeddings/{tag}/shard_*.npy")):
17+ if f.endswith(".ids.npy"):
18+ continue
19+ E.append(np.load(f)); I.append(np.load(f.replace(".npy", ".ids.npy")))
20+ return np.concatenate(E).astype(np.float32), np.concatenate(I)
21+
22+
23+def main():
24+ ap = argparse.ArgumentParser(); ap.add_argument("--root", required=True); ap.add_argument("--model", default="allenai/specter2_base"); ap.add_argument("--n", type=int, default=200000)
25+ ap.add_argument("--seed", type=int, default=44); a = ap.parse_args(); R = a.root; tag = a.model.replace("/", "__"); out = f"{R}/results/semantic"; os.makedirs(out, exist_ok=True)
26+ E, ids = load_embeddings(R, tag); print("embeddings", E.shape)
27+ con = duckdb.connect(f"{R}/data/processed/wp44.duckdb", read_only=True)
28+ con.execute(f"CREATE OR REPLACE VIEW work_primary AS SELECT * FROM read_parquet('{R}/data/processed/classification/primary/*.parquet')")
29+ meta = con.execute("SELECT c.id AS work_id, c.publication_year AS year, c.period, c.first_month, p.primary_category FROM corpus_works c LEFT JOIN work_primary p ON p.work_id = c.id WHERE c.eligible").df().set_index("work_id")
30+ pos = {w: i for i, w in enumerate(ids)}; meta = meta[meta.index.isin(pos)]; meta["row"] = [pos[w] for w in meta.index]
31+ rng = np.random.default_rng(a.seed)
32+ # (4) category centroids per year + drift + similarity
33+ rows = []; cents = {}
34+ for (cat, y), g in meta.dropna(subset=["primary_category"]).groupby(["primary_category", "year"]):
35+ if len(g) < 100:
36+ continue
37+ v = E[g.row.values].mean(0); v /= np.linalg.norm(v) + 1e-9; cents[(cat, y)] = v
38+ for (cat, y), v in cents.items():
39+ prev = cents.get((cat, y - 1)); rows.append({"category": cat, "year": y, "n": int(((meta.primary_category == cat) & (meta.year == y)).sum()), "drift_cos_dist": None if prev is None else float(1 - v @ prev)})
40+ pd.DataFrame(rows).to_csv(f"{out}/category_drift.csv", index=False)
41+ for per, yrs in [("baseline", range(2018, 2023)), ("post", range(2023, 2027))]:
42+ cats = sorted({c for (c, y) in cents if y in yrs}); M = np.zeros((len(cats), len(cats)))
43+ for i, c1 in enumerate(cats):
44+ v1 = np.mean([cents[(c1, y)] for y in yrs if (c1, y) in cents], 0)
45+ for j, c2 in enumerate(cats):
46+ v2 = np.mean([cents[(c2, y)] for y in yrs if (c2, y) in cents], 0); M[i, j] = float(v1 @ v2 / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-9))
47+ pd.DataFrame(M, index=cats, columns=cats).to_csv(f"{out}/category_similarity_{per}.csv")
48+ # (5) novelty vs trailing 24-month centroid of the same category
49+ meta_m = meta.dropna(subset=["primary_category"]).copy(); months = sorted(meta_m.first_month.unique()); nov = np.full(len(meta_m), np.nan, dtype=np.float32)
50+ mcent = {}
51+ for (cat, m), g in meta_m.groupby(["primary_category", "first_month"]):
52+ mcent[(cat, m)] = (E[g.row.values].sum(0), len(g))
53+ midx = {m: i for i, m in enumerate(months)}; arr_pos = {w: i for i, w in enumerate(meta_m.index)}
54+ for (cat, m), g in meta_m.groupby(["primary_category", "first_month"]):
55+ i0 = midx[m]; prev = [mcent[(cat, mm)] for mm in months[max(0, i0 - 24):i0] if (cat, mm) in mcent]
56+ if not prev:
57+ continue
58+ v = np.sum([p[0] for p in prev], 0) / max(sum(p[1] for p in prev), 1); v /= np.linalg.norm(v) + 1e-9
59+ sims = E[g.row.values] @ v
60+ for w, s in zip(g.index, sims):
61+ nov[arr_pos[w]] = 1 - s
62+ meta_m["novelty"] = nov; meta_m[["novelty", "primary_category", "first_month"]].reset_index().to_parquet(f"{out}/novelty.parquet")
63+ # (1–3) atlas on a sample
64+ import umap, hdbscan
65+ from sklearn.metrics import adjusted_rand_score
66+ samp = meta.sample(min(a.n, len(meta)), random_state=a.seed); X = E[samp.row.values]
67+ red = umap.UMAP(n_components=50, n_neighbors=30, min_dist=0.0, metric="cosine", random_state=a.seed).fit_transform(X)
68+ lab = hdbscan.HDBSCAN(min_cluster_size=200, min_samples=20).fit_predict(red)
69+ xy = umap.UMAP(n_components=2, n_neighbors=30, min_dist=0.1, metric="cosine", random_state=a.seed).fit_transform(X)
70+ samp = samp.assign(cluster=lab, x=xy[:, 0], y=xy[:, 1]); samp.reset_index().to_parquet(f"{out}/atlas_sample.parquet")
71+ titles = con.execute(f"SELECT id AS work_id, title FROM corpus_works WHERE id IN (SELECT work_id FROM read_parquet('{out}/atlas_sample.parquet'))").df().set_index("work_id").title
72+ from sklearn.feature_extraction.text import TfidfVectorizer
73+ docs = samp.index.map(lambda w: titles.get(w, "") or ""); vec = TfidfVectorizer(max_features=50000, stop_words="english", ngram_range=(1, 2), min_df=5); T = vec.fit_transform(docs); terms = np.array(vec.get_feature_names_out())
74+ crow = []
75+ for c in sorted(set(lab)):
76+ m = lab == c; v = np.asarray(T[m].mean(0)).ravel(); top = terms[np.argsort(-v)[:12]]
77+ pc = samp[m].primary_category.value_counts(normalize=True)
78+ crow.append({"cluster": int(c), "n": int(m.sum()), "top_terms": "; ".join(top), "top_category": pc.index[0] if len(pc) else None, "top_category_share": float(pc.iloc[0]) if len(pc) else None,
79+ "share_post": float((samp[m].period == "post").mean())})
80+ pd.DataFrame(crow).to_csv(f"{out}/clusters.csv", index=False)
81+ aris = []
82+ for r in range(5):
83+ idx = rng.permutation(len(X)); h1, h2 = idx[: len(X) // 2], idx[len(X) // 2:]
84+ l1 = hdbscan.HDBSCAN(min_cluster_size=100, min_samples=20).fit_predict(red[h1]); l2 = hdbscan.HDBSCAN(min_cluster_size=100, min_samples=20).fit_predict(red[h2])
85+ # compare through nearest-neighbour label transfer from half 1 to half 2
86+ from sklearn.neighbors import KNeighborsClassifier
87+ knn = KNeighborsClassifier(n_neighbors=5).fit(red[h1], l1); aris.append(float(adjusted_rand_score(l2, knn.predict(red[h2]))))
88+ json.dump({"n_sample": int(len(X)), "n_clusters": int(len(set(lab)) - (1 if -1 in lab else 0)), "noise_share": float((lab == -1).mean()), "ari_bootstrap": aris, "ari_mean": float(np.mean(aris))}, open(f"{out}/stability.json", "w"), indent=1)
89+ print("atlas done", len(set(lab)), "clusters; ARI", np.mean(aris))
90+
91+
92+if __name__ == "__main__":
93+ main()
modified figures/FIGURES.json +35 −0
@@ -34,5 +34,40 @@
34 "results/bibliometrics/team_size_yearly.csv", 34 "results/bibliometrics/team_size_yearly.csv",
35 "results/bibliometrics/industry_share_yearly.csv" 35 "results/bibliometrics/industry_share_yearly.csv"
36 ] 36 ]
37+ },
38+ {
39+ "file": "fig_citation_inequality",
40+ "caption": "Fixed-window citation levels and inequality by publication year (years with complete 36-month windows).",
41+ "sources": [
42+ "results/bibliometrics/citation_windows_yearly.csv"
43+ ]
44+ },
45+ {
46+ "file": "fig_field_inflows",
47+ "caption": "Share of references pointing outside the corpus, by cited OpenAlex field and citing year.",
48+ "sources": [
49+ "results/graph/field_flows_yearly.csv"
50+ ]
51+ },
52+ {
53+ "file": "fig_cited_age",
54+ "caption": "Distribution of the age of cited works for selected citing years.",
55+ "sources": [
56+ "results/graph/cited_age_yearly.csv"
57+ ]
58+ },
59+ {
60+ "file": "fig_venue_concentration",
61+ "caption": "Number of distinct venues and venue concentration (HHI, top-source share) by year.",
62+ "sources": [
63+ "results/bibliometrics/venue_hhi_yearly.csv"
64+ ]
65+ },
66+ {
67+ "file": "fig_country_shares",
68+ "caption": "Country shares of output with fractional counting, top eight countries.",
69+ "sources": [
70+ "results/bibliometrics/countries_yearly.csv"
71+ ]
37 } 72 }
38 ] 73 ]
\ No newline at end of file
added figures/fig_citation_inequality.pdf +0 −0

Binary file not shown.

added figures/fig_citation_inequality.png +0 −0

Binary file not shown.

added figures/fig_cited_age.pdf +0 −0

Binary file not shown.

added figures/fig_cited_age.png +0 −0

Binary file not shown.

modified figures/fig_collaboration.pdf +0 −0

Binary file not shown.

added figures/fig_country_shares.pdf +0 −0

Binary file not shown.

added figures/fig_country_shares.png +0 −0

Binary file not shown.

added figures/fig_field_inflows.pdf +0 −0

Binary file not shown.

added figures/fig_field_inflows.png +0 −0

Binary file not shown.

modified figures/fig_prisma_flow.pdf +0 −0

Binary file not shown.

modified figures/fig_types_openness.pdf +0 −0

Binary file not shown.

added figures/fig_venue_concentration.pdf +0 −0

Binary file not shown.

added figures/fig_venue_concentration.png +0 −0

Binary file not shown.

modified figures/fig_volume_monthly_raw.pdf +0 −0

Binary file not shown.

modified figures/fig_volume_yearly.pdf +0 −0

Binary file not shown.

modified figures/fig_volume_yearly.png +0 −0

Binary file not shown.

modified figures/make_figures.py +74 −1
@@ -89,7 +89,7 @@ def fig_collab(R, out):
89 89
90 def main(): 90 def main():
91 ap = argparse.ArgumentParser(); ap.add_argument("--root", required=True); a = ap.parse_args(); out = f"{a.root}/figures"; os.makedirs(out, exist_ok=True) 91 ap = argparse.ArgumentParser(); ap.add_argument("--root", required=True); a = ap.parse_args(); out = f"{a.root}/figures"; os.makedirs(out, exist_ok=True)
92− for f in (fig_prisma, fig_yearly_volume, fig_monthly, fig_types_sources, fig_collab): 92+ for f in (fig_prisma, fig_yearly_volume, fig_monthly, fig_types_sources, fig_collab) + _BATCH2:
93 try: 93 try:
94 f(a.root, out); print("ok", f.__name__) 94 f(a.root, out); print("ok", f.__name__)
95 except Exception as e: 95 except Exception as e:
@@ -97,5 +97,78 @@ def main():
97 json.dump(INV, open(f"{out}/FIGURES.json", "w"), indent=1) 97 json.dump(INV, open(f"{out}/FIGURES.json", "w"), indent=1)
98 98
99 99
100+
101+
102+# ---------------- batch 2: citations and knowledge flows (generated from results/bibliometrics + results/graph) ----------------
103+def fig_citation_inequality(R, out):
104+ p = f"{R}/results/bibliometrics/citation_windows_yearly.csv"
105+ if not os.path.exists(p):
106+ return
107+ d = pd.read_csv(p); d = d[d.year <= 2023] # c36 requires three full years of citation data
108+ fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))
109+ ax = axes[0]; ax.plot(d.year, d.mean_c36, marker="o", color=style.CATEGORICAL[0], label="Mean c36"); ax.plot(d.year, d.p99_c36 / 10, marker="o", color=style.CATEGORICAL[1], label="p99 c36 ÷ 10")
110+ ax.set_title("Fixed-window citations (36 months)", loc="left"); ax.set_ylabel("Citations"); ax.legend()
111+ ax = axes[1]; ax.plot(d.year, d.gini_c36, marker="o", color=style.CATEGORICAL[2], label="Gini of c36"); ax.plot(d.year, d.share_uncited_36, marker="o", color=style.CATEGORICAL[3], label="Share uncited at 36 months")
112+ ax.set_title("Citation inequality", loc="left"); ax.set_ylim(0, 1); ax.legend()
113+ style.save(fig, f"{out}/fig_citation_inequality.pdf"); reg("fig_citation_inequality", "Fixed-window citation levels and inequality by publication year (years with complete 36-month windows).", ["results/bibliometrics/citation_windows_yearly.csv"])
114+
115+
116+def fig_field_inflows(R, out):
117+ p = f"{R}/results/graph/field_flows_yearly.csv"
118+ if not os.path.exists(p):
119+ return
120+ d = pd.read_csv(p); d = d[(d.citing_year >= 2018) & (d.citing_year <= 2026)]
121+ names = {17: "Computer science (outside corpus)", 26: "Mathematics", 22: "Engineering", 31: "Physics & astronomy", 27: "Medicine", 33: "Social sciences", 13: "Biochem./genetics", 18: "Decision sciences", 28: "Neuroscience", 20: "Economics & finance"}
122+ tot = d.groupby("citing_year").n.sum(); ext = d[d.kind == "external"]
123+ top = ext.groupby("cited_field_id").n.sum().nlargest(8).index
124+ fig, ax = plt.subplots(figsize=(6.8, 3.4))
125+ for i, f in enumerate(top):
126+ s = ext[ext.cited_field_id == f].groupby("citing_year").n.sum() / tot * 100
127+ ax.plot(s.index, s.values, marker="o", color=style.CATEGORICAL[i % 8], label=names.get(int(f), f"field {int(f)}"), lw=1.2)
128+ ax.annotate(names.get(int(f), str(int(f))), (s.index[-1], s.values[-1]), xytext=(3, 0), textcoords="offset points", fontsize=6.5, color=style.INK2, va="center")
129+ ax.set_ylabel("Share of all references (%)"); ax.set_title("References from the CS corpus to works outside it, by cited field", loc="left"); ax.legend(ncol=2, fontsize=6.5)
130+ style.save(fig, f"{out}/fig_field_inflows.pdf"); reg("fig_field_inflows", "Share of references pointing outside the corpus, by cited OpenAlex field and citing year.", ["results/graph/field_flows_yearly.csv"])
131+
132+
133+def fig_cited_age(R, out):
134+ p = f"{R}/results/graph/cited_age_yearly.csv"
135+ if not os.path.exists(p):
136+ return
137+ d = pd.read_csv(p); d = d[(d.age >= 0) & (d.age <= 30)]
138+ fig, ax = plt.subplots(figsize=(6.8, 3.2))
139+ for i, y in enumerate([2018, 2020, 2022, 2024, 2026]):
140+ g = d[d.citing_year == y]; s = g.n / g.n.sum()
141+ ax.plot(g.age, s * 100, color=style.SEQUENTIAL[2 + 2 * i], label=str(y), lw=1.3)
142+ ax.set_xlabel("Age of cited work (years)"); ax.set_ylabel("Share of references (%)"); ax.set_title("Reference age distribution by citing year", loc="left"); ax.legend(title="Citing year")
143+ style.save(fig, f"{out}/fig_cited_age.pdf"); reg("fig_cited_age", "Distribution of the age of cited works for selected citing years.", ["results/graph/cited_age_yearly.csv"])
144+
145+
146+def fig_venue_hhi(R, out):
147+ p = f"{R}/results/bibliometrics/venue_hhi_yearly.csv"
148+ if not os.path.exists(p):
149+ return
150+ d = pd.read_csv(p); d = d[d.year <= 2026]
151+ fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))
152+ axes[0].plot(d.year, d.n_sources, marker="o", color=style.CATEGORICAL[0]); axes[0].set_title("Distinct sources (venues) per year", loc="left")
153+ axes[1].plot(d.year, d.hhi * 1e4, marker="o", color=style.CATEGORICAL[1], label="HHI × 10⁴"); axes[1].plot(d.year, d.top1_share * 100, marker="o", color=style.CATEGORICAL[2], label="Top source share (%)"); axes[1].set_title("Venue concentration", loc="left"); axes[1].legend()
154+ style.save(fig, f"{out}/fig_venue_concentration.pdf"); reg("fig_venue_concentration", "Number of distinct venues and venue concentration (HHI, top-source share) by year.", ["results/bibliometrics/venue_hhi_yearly.csv"])
155+
156+
157+def fig_countries(R, out):
158+ p = f"{R}/results/bibliometrics/countries_yearly.csv"
159+ if not os.path.exists(p):
160+ return
161+ d = pd.read_csv(p); d = d[d.year <= 2026]; tot = d.groupby("year").n_frac.sum(); top = d.groupby("country").n_frac.sum().nlargest(8).index
162+ fig, ax = plt.subplots(figsize=(6.8, 3.4))
163+ for i, c in enumerate(top):
164+ s = d[d.country == c].set_index("year").n_frac / tot * 100
165+ ax.plot(s.index, s.values, marker="o", color=style.CATEGORICAL[i], label=c, lw=1.2); ax.annotate(c, (s.index[-1], s.values[-1]), xytext=(3, 0), textcoords="offset points", fontsize=7, color=style.INK2, va="center")
166+ ax.set_ylabel("Share of fractional output (%)"); ax.set_title("Country shares of eligible CS output (fractional counting, top 8)", loc="left"); ax.legend(ncol=4, fontsize=7)
167+ style.save(fig, f"{out}/fig_country_shares.pdf"); reg("fig_country_shares", "Country shares of output with fractional counting, top eight countries.", ["results/bibliometrics/countries_yearly.csv"])
168+
169+
170+_BATCH2 = (fig_citation_inequality, fig_field_inflows, fig_cited_age, fig_venue_hhi, fig_countries)
171+
172+
100 if __name__ == "__main__": 173 if __name__ == "__main__":
101 main() 174 main()
modified taxonomy/classify.py +1 −1
@@ -103,7 +103,7 @@ def prepare(root, db, out_dir, chunk_rows):
103 (SELECT list(subfield_id ORDER BY rank) FROM oa_work_topics t WHERE t.work_id = w.id) AS subfield_ids, 103 (SELECT list(subfield_id ORDER BY rank) FROM oa_work_topics t WHERE t.work_id = w.id) AS subfield_ids,
104 (SELECT flatten(list(x.categories)) FROM link_arxiv_oa l JOIN arxiv_keys x USING(arxiv_id) WHERE l.oa_id = w.id) AS arxiv_cats, 104 (SELECT flatten(list(x.categories)) FROM link_arxiv_oa l JOIN arxiv_keys x USING(arxiv_id) WHERE l.oa_id = w.id) AS arxiv_cats,
105 (SELECT list(DISTINCT d.stream) FROM link_dblp_oa l JOIN dblp_keys d USING(dblp_key) WHERE l.oa_id = w.id) AS dblp_streams 105 (SELECT list(DISTINCT d.stream) FROM link_dblp_oa l JOIN dblp_keys d USING(dblp_key) WHERE l.oa_id = w.id) AS dblp_streams
106− FROM corpus_works w LEFT JOIN oa_abstracts a ON a.work_id = w.id""") 106+ FROM corpus_works w LEFT JOIN oa_abstracts a ON a.work_id = w.id WHERE w.eligible OR w.cs_penumbra""")
107 n = con.execute("SELECT count(*) FROM cls_input").fetchone()[0] 107 n = con.execute("SELECT count(*) FROM cls_input").fetchone()[0]
108 con.execute(f"""COPY (SELECT *, (row_number() OVER ()) // {chunk_rows} AS chunk FROM cls_input) TO '{out_dir}/input' (FORMAT PARQUET, PARTITION_BY (chunk), OVERWRITE_OR_IGNORE, COMPRESSION ZSTD)""") 108 con.execute(f"""COPY (SELECT *, (row_number() OVER ()) // {chunk_rows} AS chunk FROM cls_input) TO '{out_dir}/input' (FORMAT PARQUET, PARTITION_BY (chunk), OVERWRITE_OR_IGNORE, COMPRESSION ZSTD)""")
109 print("prepared", n, "works") 109 print("prepared", n, "works")
modified taxonomy/run_classify_shards.sh +11 −4
@@ -1,14 +1,21 @@
1 #!/bin/sh 1 #!/bin/sh
2−# Classify the corpus with N independent processes (macOS multiprocessing pool hangs with pyarrow in spawned workers). 2+# Classify the corpus with independent processes on two nodes (M3U96b local + M3U96a remote); macOS pools hang with pyarrow.
3−# Usage: sh taxonomy/run_classify_shards.sh [N] — waits for the pass-2 chain (CHAIN_DONE) before preparing the input. 3+# Usage: sh taxonomy/run_classify_shards.sh [N_total] — waits for the pass-2 chain (CHAIN_DONE) before preparing the input.
4−cd ~/wp44_uqo; . .venv/bin/activate; ulimit -n 65536; N=${1:-20} 4+cd ~/wp44_uqo; . .venv/bin/activate; ulimit -n 65536; N=${1:-56}; HALF=$((N/2)); R=M3U96a
5 while ! grep -q CHAIN_DONE logs/define_corpus_v2.log 2>/dev/null; do sleep 60; done 5 while ! grep -q CHAIN_DONE logs/define_corpus_v2.log 2>/dev/null; do sleep 60; done
6 rm -rf data/processed/classification; mkdir -p logs/classify 6 rm -rf data/processed/classification; mkdir -p logs/classify
7 python taxonomy/classify.py --root ~/wp44_uqo --ontology taxonomy/ontology.yaml --prepare > logs/classify/prepare.log 2>&1 7 python taxonomy/classify.py --root ~/wp44_uqo --ontology taxonomy/ontology.yaml --prepare > logs/classify/prepare.log 2>&1
8−i=0; while [ $i -lt $N ]; do 8+ssh $R "mkdir -p ~/wp44_uqo/data/processed/classification ~/wp44_uqo/taxonomy ~/wp44_uqo/logs/classify; rm -rf ~/wp44_uqo/data/processed/classification/*"
9+rsync -aq data/processed/classification/input $R:~/wp44_uqo/data/processed/classification/
10+rsync -aq taxonomy/classify.py taxonomy/ontology.yaml taxonomy/__init__.py $R:~/wp44_uqo/taxonomy/
11+i=0; while [ $i -lt $HALF ]; do
9 nohup python taxonomy/classify.py --root ~/wp44_uqo --ontology taxonomy/ontology.yaml --run --shard $i/$N > logs/classify/shard_$i.log 2>&1 & 12 nohup python taxonomy/classify.py --root ~/wp44_uqo --ontology taxonomy/ontology.yaml --run --shard $i/$N > logs/classify/shard_$i.log 2>&1 &
10 i=$((i+1)); done 13 i=$((i+1)); done
14+ssh $R "cd ~/wp44_uqo; . .venv/bin/activate; i=$HALF; while [ \$i -lt $N ]; do nohup python taxonomy/classify.py --root ~/wp44_uqo --ontology taxonomy/ontology.yaml --run --shard \$i/$N > logs/classify/shard_\$i.log 2>&1 & i=\$((i+1)); done; echo remote started"
11 wait 15 wait
16+while ssh $R "pgrep -f 'classify.py --root' >/dev/null"; do sleep 60; done
17+rsync -aq $R:~/wp44_uqo/data/processed/classification/cats/ data/processed/classification/cats/
18+rsync -aq $R:~/wp44_uqo/data/processed/classification/primary/ data/processed/classification/primary/
12 python - <<EOF2 19 python - <<EOF2
13 import duckdb, json, datetime as dt, yaml 20 import duckdb, json, datetime as dt, yaml
14 con = duckdb.connect(); o = yaml.safe_load(open("taxonomy/ontology.yaml")) 21 con = duckdb.connect(); o = yaml.safe_load(open("taxonomy/ontology.yaml"))
15 22
Menu

Theme