SPB Git

spb/wp44_uqo

Public

WP44: phase 2-6 pipeline script, category figures (batch 4), Part VIII brief

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

3 changed files +168 −1

added agents/briefs/WRITER_PART08.md +26 −0
@@ -0,0 +1,26 @@
1 +# Brief — Part VIII "Scientific Knowledge Networks" (file manuscript/sections/part08_networks.tex; bib manuscript/references_part08.bib)
2 +
3 +Target 25–35 pages. Follow WRITER_COMMON.md. Analyse authors, institutions, countries, citations, interdisciplinarity and
4 +knowledge diffusion (CLAUDE.md §9) using ONLY the corpus v2 result files below (copies in the repository; every number via the
5 +macros of manuscript/tables/numbers.tex — regenerate with `python3 tables/make_numbers.py .` — or, when a value has no macro,
6 +cite the result file and row in a footnote and add the value to a LaTeX table generated by tables/make_tables.py — you may
7 +extend make_tables.py with new tables built from these CSVs, re-run it, and \input the generated tables/*.tex):
8 +- results/bibliometrics/team_size_yearly.csv, international_collab_yearly.csv, industry_share_yearly.csv, institution_types_yearly.csv,
9 + countries_yearly.csv (fractional and full counting), country_pairs_by_period.csv (edges ≥ 50 joint works), top_institutions_by_period.csv
10 + (OpenAlex institution ids; names are not in the file — refer to ids or omit names), top_sources_by_period.csv, venue_hhi_yearly.csv,
11 + citation_windows_yearly.csv (c12/c36 windows with completeness flags; Gini), top_cited_by_year.csv (titles of the 50 most cited per year by c36 — may be quoted),
12 + indexing_lag.csv (explain why created_date cannot measure lag), dblp_only_yearly.csv.
13 +- results/graph/field_flows_yearly.csv (references from the corpus to works inside it vs outside by OpenAlex field), cited_age_yearly.csv,
14 + reference_coverage_yearly.csv. NOTE: results/graph/ is being regenerated for corpus v2; if the files are missing or dated before
15 + 2026-11-03 18:00, write the citation-flow subsections against the figure files figures/fig_field_inflows.pdf / fig_cited_age.pdf
16 + (generated from v1) and mark the numbers \pending{v2 graph}. Category-level flows and modularity (results/graph/category_flows_yearly.csv,
17 + cross_area_share_yearly.csv, modularity_yearly.csv) do NOT exist yet: write subsection "Knowledge flows between categories" as a
18 + short placeholder paragraph marked \pending{category flows after classification} that the director will fill.
19 +- Figures available in manuscript/figures/: fig_collaboration.pdf, fig_country_shares.pdf, fig_venue_concentration.pdf,
20 + fig_citation_inequality.pdf, fig_field_inflows.pdf, fig_cited_age.pdf (reference them with \includegraphics and \label).
21 +Hypotheses to confront (protocol P1): H4 (cross-area citation share rises; modularity falls — only the field-level part is testable now),
22 +H5 (citation inequality higher post for AI — only the aggregate Gini is available now; say so), H6 (industry affiliation share and China
23 +share rise; international co-authorship share does not rise). Report what the tables show, including null or opposite results,
24 +with the right-truncation caveats (2025–2026 windows incomplete; 2026 partial). No causal language. Methods are in Part IV
25 +(cite its labels by name). Cite the networks/bibliometrics literature from manuscript/references.bib (keys exist for Newman, Wagner,
26 +Waltman, Nielsen & Andersen, Porter & Rafols, Stirling, Leydesdorff, Priem…); verify any new key.
modified figures/make_figures.py +122 −1
@@ -89,7 +89,7 @@ def fig_collab(R, out):
89 89
90 90 def main():
91 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) + _BATCH2 + _BATCH3:
92 + for f in (fig_prisma, fig_yearly_volume, fig_monthly, fig_types_sources, fig_collab) + _BATCH2 + _BATCH3 + _BATCH4:
93 93 try:
94 94 f(a.root, out); print("ok", f.__name__)
95 95 except Exception as e:
@@ -212,5 +212,126 @@ def fig_placebo_total(R, out):
212 212 _BATCH3 = (fig_its_total, fig_placebo_total)
213 213
214 214
215 +
216 +
217 +# ---------------- batch 4: categories (after classification) ----------------
218 +def _cat_names(R):
219 + import yaml
220 + o = yaml.safe_load(open(f"{R}/taxonomy/ontology.yaml")); return {c["id"]: c["name"] for c in o["categories"]}, {c["id"]: c["area"] for c in o["categories"]}
221 +
222 +
223 +def fig_category_growth(R, out):
224 + p = f"{R}/results/graph/category_counts_yearly.csv"
225 + if not os.path.exists(p):
226 + return
227 + names, areas = _cat_names(R); d = pd.read_csv(p); d = d[(d.year >= 2018) & (d.year <= 2025)]
228 + piv = d.pivot_table(index="year", columns="category_id", values="n_primary", fill_value=0); base = piv.loc[2018]
229 + idx = piv / base.replace(0, np.nan) * 100; cats = sorted(idx.columns, key=lambda c: -idx.loc[2025, c] if 2025 in idx.index else 0)
230 + fig, axes = plt.subplots(1, 2, figsize=(7.4, 3.6), sharey=False)
231 + for ax, sel, title in [(axes[0], cats[:8], "Eight fastest-growing categories"), (axes[1], cats[-8:], "Eight slowest-growing categories")]:
232 + for i, c in enumerate(sel):
233 + ax.plot(idx.index, idx[c], marker="o", ms=3, color=style.CATEGORICAL[i % 8], label=names.get(c, c), lw=1.1)
234 + ax.axhline(100, color=style.AXIS, lw=0.6); ax.set_title(title, loc="left"); ax.set_ylabel("Primary works, index 2018 = 100"); ax.legend(fontsize=6)
235 + style.save(fig, f"{out}/fig_category_growth.pdf"); reg("fig_category_growth", "Yearly primary-category output indexed to 2018, fastest- and slowest-growing categories.", [p])
236 +
237 +
238 +def fig_category_shares(R, out):
239 + p = f"{R}/results/graph/category_counts_yearly.csv"
240 + if not os.path.exists(p):
241 + return
242 + names, areas = _cat_names(R); d = pd.read_csv(p); d = d[(d.year >= 2018) & (d.year <= 2025)]; d["area"] = d.category_id.map(areas)
243 + piv = d.pivot_table(index="year", columns="area", values="n_primary", aggfunc="sum", fill_value=0); sh = piv.div(piv.sum(1), axis=0) * 100
244 + fig, ax = plt.subplots(figsize=(6.8, 3.4))
245 + for i, a in enumerate(sh.columns):
246 + ax.plot(sh.index, sh[a], marker="o", ms=3, color=style.CATEGORICAL[i % 8], label=a, lw=1.2)
247 + ax.annotate(a, (sh.index[-1], sh[a].iloc[-1]), xytext=(3, 0), textcoords="offset points", fontsize=6.5, color=style.INK2, va="center")
248 + ax.set_ylabel("Share of primary assignments (%)"); ax.set_title("Composition of CS output by area (primary category)", loc="left"); ax.legend(ncol=2, fontsize=6.5)
249 + style.save(fig, f"{out}/fig_area_shares.pdf"); reg("fig_area_shares", "Share of eligible works by top-level area (primary category) per year.", [p])
250 +
251 +
252 +def fig_breaks_heatmap(R, out):
253 + p = f"{R}/results/econometrics/breaks.csv"
254 + if not os.path.exists(p):
255 + return
256 + names, areas = _cat_names(R); d = pd.read_csv(p); d = d[d.series.str.startswith("cat_")].copy(); d["cat"] = d.series.str[4:]
257 + if d.empty:
258 + return
259 + d = d.sort_values("chatgpt_slope", ascending=False)
260 + fig, ax = plt.subplots(figsize=(6.8, 7.2))
261 + y = np.arange(len(d)); cols = [style.CATEGORICAL[0] if p_ < 0.05 else style.GRID for p_ in d.placebo_slope_p.fillna(1)]
262 + ax.barh(y, 100 * d.chatgpt_slope, color=cols, height=0.7); ax.set_yticks(y); ax.set_yticklabels([names.get(c, c) for c in d.cat], fontsize=6.5); ax.invert_yaxis()
263 + ax.axvline(0, color=style.AXIS, lw=0.6); ax.set_xlabel("Change in monthly growth rate after 2022-12 (percentage points per month)")
264 + ax.set_title("ITS slope change by category (blue: placebo p < 0.05)", loc="left")
265 + for yi, (s, lp) in enumerate(zip(d.chatgpt_slope, d.chatgpt_level)):
266 + ax.text(100 * s + (0.03 if s >= 0 else -0.03), yi, f"level {100*lp:+.0f}%", fontsize=5.5, va="center", ha="left" if s >= 0 else "right", color=style.INK2)
267 + style.save(fig, f"{out}/fig_breaks_by_category.pdf"); reg("fig_breaks_by_category", "ITS slope change at 2022-12 by category with level change annotated; bars in colour pass the placebo test.", [p])
268 +
269 +
270 +def fig_flow_matrix(R, out):
271 + p = f"{R}/results/graph/area_flows_yearly.csv"
272 + if not os.path.exists(p):
273 + return
274 + d = pd.read_csv(p); fig, axes = plt.subplots(1, 2, figsize=(7.4, 3.6))
275 + for ax, (lo, hi), title in [(axes[0], (2018, 2022), "Baseline 2018–2022"), (axes[1], (2023, 2026), "Post 2023–2026")]:
276 + g = d[(d.citing_year >= lo) & (d.citing_year <= hi)].groupby(["citing_area", "cited_area"]).n.sum().unstack(fill_value=0)
277 + g = g.reindex(index=sorted(set(g.index) | set(g.columns)), columns=sorted(set(g.index) | set(g.columns)), fill_value=0); rs = g.div(g.sum(1), axis=0) * 100
278 + im = ax.imshow(rs.values, cmap=style.seq_cmap(), vmin=0, vmax=max(rs.values.max(), 1)); ax.set_xticks(range(len(rs.columns))); ax.set_xticklabels(rs.columns, rotation=60, ha="right", fontsize=6); ax.set_yticks(range(len(rs.index))); ax.set_yticklabels(rs.index, fontsize=6)
279 + ax.set_title(title, loc="left"); ax.grid(False)
280 + for i in range(len(rs.index)):
281 + for j in range(len(rs.columns)):
282 + v = rs.values[i, j]
283 + if v >= 5: ax.text(j, i, f"{v:.0f}", ha="center", va="center", fontsize=5.5, color="white" if v > 0.6 * rs.values.max() else style.INK)
284 + fig.text(0.5, -0.02, "Row shares (%): references from the citing area (rows) to the cited area (columns), internal citations only", ha="center", fontsize=7, color=style.INK2)
285 + style.save(fig, f"{out}/fig_area_flow_matrix.pdf"); reg("fig_area_flow_matrix", "Citation-flow matrices between areas (row shares) for the baseline and post periods.", [p])
286 +
287 +
288 +def fig_cross_area(R, out):
289 + p = f"{R}/results/graph/cross_area_share_yearly.csv"; q = f"{R}/results/graph/modularity_yearly.csv"
290 + if not os.path.exists(p):
291 + return
292 + d = pd.read_csv(p); fig, axes = plt.subplots(1, 2, figsize=(7.4, 3.2))
293 + for i, a in enumerate(sorted(d.citing_area.unique())):
294 + g = d[d.citing_area == a]; axes[0].plot(g.citing_year, 100 * g.share_cross, marker="o", ms=3, color=style.CATEGORICAL[i % 8], label=a, lw=1.1)
295 + axes[0].set_title("Share of references leaving the citing area (%)", loc="left"); axes[0].legend(fontsize=6, ncol=2)
296 + if os.path.exists(q):
297 + m = pd.read_csv(q); axes[1].plot(m.year, m.Q_areas, marker="o", color=style.CATEGORICAL[0], label="Modularity Q (areas)"); axes[1].plot(m.year, m.Q_categories, marker="o", color=style.CATEGORICAL[1], label="Modularity Q (categories)"); axes[1].plot(m.year, m.self_share, marker="o", color=style.CATEGORICAL[2], label="Within-category share")
298 + axes[1].set_title("Modularity of the yearly category citation network", loc="left"); axes[1].legend(fontsize=6)
299 + style.save(fig, f"{out}/fig_cross_area.pdf"); reg("fig_cross_area", "Cross-area reference shares by citing area and modularity of the category citation network by year.", [p, q])
300 +
301 +
302 +def fig_emerging(R, out):
303 + import glob as _g
304 + files = sorted(_g.glob(f"{R}/results/emerging/metrics_h*.json"))
305 + if not files:
306 + return
307 + fig, axes = plt.subplots(1, len(files), figsize=(3.7 * len(files), 3.0), squeeze=False)
308 + for ax, f in zip(axes[0], files):
309 + M = json.load(open(f)); models = [m for m in ("zscore_baseline", "logistic", "gbm") if m in M]
310 + x = np.arange(len(models)); vals = [[M[m]["auc"], M[m]["precision_at_20"], M[m]["precision_at_50"]] for m in models]
311 + for k, (lab, col) in enumerate([("AUC", style.CATEGORICAL[0]), ("P@20", style.CATEGORICAL[1]), ("P@50", style.CATEGORICAL[2])]):
312 + ax.bar(x + (k - 1) * 0.26, [v[k] for v in vals], width=0.24, color=col, label=lab)
313 + for xi, v in zip(x, vals): ax.text(xi + (k - 1) * 0.26, v[k] + 0.01, f"{v[k]:.2f}", ha="center", fontsize=5.5, color=style.INK2)
314 + ax.set_xticks(x); ax.set_xticklabels(["z-score", "logistic", "GBM"]); ax.set_ylim(0, 1.05); ax.set_title(f"Horizon {M['horizon']} months (test cutoffs {M['test_cutoffs'][0]}–{M['test_cutoffs'][-1]})", loc="left", fontsize=8); ax.legend(fontsize=6)
315 + style.save(fig, f"{out}/fig_emerging_metrics.pdf"); reg("fig_emerging_metrics", "Out-of-time detection performance of the emerging-topic models by horizon.", files)
316 +
317 +
318 +def fig_known_set(R, out):
319 + p = f"{R}/results/emerging/known_set_monthly.csv"
320 + if not os.path.exists(p):
321 + return
322 + d = pd.read_csv(p, index_col=0); x = pd.to_datetime(d.index + "-01"); fig, axes = plt.subplots(3, 4, figsize=(7.6, 5.4), sharex=True)
323 + lt = pd.read_csv(f"{R}/results/emerging/known_set_lead_times.csv").set_index("concept")
324 + for ax, c in zip(axes.ravel(), d.columns):
325 + ax.plot(x, d[c], color=style.CATEGORICAL[0], lw=1.0); ax.set_title(c, fontsize=7, loc="left"); ax.grid(True)
326 + det = lt.loc[c, "detection_month"] if c in lt.index else None
327 + if isinstance(det, str): ax.axvline(pd.Timestamp(det + "-01"), color=style.CATEGORICAL[1], lw=0.9, ls="--")
328 + ax.tick_params(labelsize=6)
329 + fig.suptitle("Preregistered known-emergence concepts: monthly works (keyword series) and detection month (dashed)", fontsize=8, x=0.02, ha="left")
330 + style.save(fig, f"{out}/fig_known_set.pdf"); reg("fig_known_set", "Monthly keyword series for the twelve preregistered concepts with the detector's first alarm.", [p])
331 +
332 +
333 +_BATCH4 = (fig_category_growth, fig_category_shares, fig_breaks_heatmap, fig_flow_matrix, fig_cross_area, fig_emerging, fig_known_set)
334 +
335 +
215 336 if __name__ == "__main__":
216 337 main()
added scripts/run_phase2_6.sh +20 −0
@@ -0,0 +1,20 @@
1 +#!/bin/sh
2 +# Post-classification pipeline on M3U96b (Phases 2–6): waits for CLASSIFY_DONE and GRAPH_V2_DONE, then runs in order.
3 +cd ~/wp44_uqo; . .venv/bin/activate; export WP44_DUCKDB_MEM=${WP44_DUCKDB_MEM:-40GB}; ulimit -n 65536; ONT=taxonomy/ontology.yaml
4 +while ! grep -q CLASSIFY_DONE logs/classify/prepare.log 2>/dev/null; do sleep 60; done
5 +while ! grep -q GRAPH_V2_DONE logs/citation_graph_v2.log 2>/dev/null; do sleep 60; done
6 +run() { echo "[$(date +%H:%M:%S)] $1"; shift; "$@" > logs/p26_$(echo $1 | tr '/' '_' | sed 's/\.py//').log 2>&1 || echo "FAILED: $*"; }
7 +run "category flows" python knowledge_graph/02_category_flows.py --root ~/wp44_uqo --ontology $ONT
8 +run "diversity/CAGR" python bibliometrics/04_diversity_cagr.py --root ~/wp44_uqo
9 +run "structural change" python econometrics/run_structural_change.py --root ~/wp44_uqo
10 +run "validation draw" python taxonomy/validate_sample.py --root ~/wp44_uqo --draw
11 +run "rater haiku" python taxonomy/validate_sample.py --root ~/wp44_uqo --rate --model claude-haiku-5-5
12 +run "rater sonnet" python taxonomy/validate_sample.py --root ~/wp44_uqo --rate --model claude-sonnet-5-5
13 +run "validation metrics" python taxonomy/validate_sample.py --root ~/wp44_uqo --metrics
14 +run "citation impact" python bibliometrics/03_citation_impact.py --root ~/wp44_uqo
15 +run "landmarks" python literature_synthesis/select_landmarks.py --root ~/wp44_uqo --ontology $ONT
16 +run "emerging h12" python experiments/emerging_detection.py --root ~/wp44_uqo --horizon 12
17 +run "emerging h24" python experiments/emerging_detection.py --root ~/wp44_uqo --horizon 24
18 +run "known set" python experiments/known_set.py --root ~/wp44_uqo
19 +run "figures" python figures/make_figures.py --root ~/wp44_uqo
20 +echo PHASE26_DONE >> logs/p26_pipeline.log
21
Menu

Theme