SPB Git

spb/wp44_uqo

Public
25.2 KB · 338 lines python
1#!/usr/bin/env python32"""WP44 — publication figures, batch 1 (corpus and volume). Every figure is generated from result files; none is drawn by hand.3Run on the compute node: python figures/make_figures.py --root ~/wp44_uqo → figures/*.pdf + .png, and figures/FIGURES.json (inventory)."""4import argparse, json, os, sys5import numpy as np, pandas as pd, matplotlib6matplotlib.use("Agg")7import matplotlib.pyplot as plt8sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))9import style10style.apply()11INV = []121314def reg(name, caption, sources):15    INV.append({"file": name, "caption": caption, "sources": sources})161718def fig_prisma(R, out):19    P = json.load(open(f"{R}/data/metadata/prisma_counts.json")); fig, ax = plt.subplots(figsize=(7.2, 6.2)); ax.axis("off"); ax.grid(False)20    boxes = [("Identified (OpenAlex works with a CS topic, 2018–2026, snapshot 2026-10-14)", P["identified"]["openalex_total_records_loaded"]),21             ("Outside window (E1)", -P["excluded_sequential"]["e1_outside_window"]), ("Non-research types (E2)", -P["excluded_sequential"]["e2_type"]),22             ("XPAC expansion records, not DBLP-linked (E3)", -P["excluded_sequential"]["e3_xpac"]), ("No title (E4)", -P["excluded_sequential"]["e4_no_title"]),23             ("Preprint duplicates merged into the published version (E5)", -P["excluded_sequential"]["e5_duplicate"]),24             ("Generic deposit platforms without DBLP/arXiv support (E7)", -P["excluded_sequential"].get("e7_generic_deposit", 0)),25             ("After exclusions", P["after_exclusions"]), ("CS core — mapping corpus (DBLP ∨ primary CS ∨ CS topic ∧ arXiv cs.*)", P["cs_core_eligible"]),26             ("CS penumbra — secondary CS topic only (sensitivity set)", P["cs_penumbra_eligible_otherwise"])]27    y = 0.9528    for label, n in boxes:29        excl = n < 0; txt = f"{abs(n):,}"30        ax.text(0.02 if not excl else 0.40, y, (label if not excl else "− " + label) + f"\n{txt}", fontsize=8.2, va="top", ha="left",31                bbox=dict(boxstyle="round,pad=0.4", fc="white", ec=style.AXIS if not excl else style.GRID, lw=0.8), transform=ax.transAxes, color=style.INK)32        y -= 0.09533    ax.set_title("PRISMA-style flow of the WP44 mapping corpus", loc="left")34    style.save(fig, f"{out}/fig_prisma_flow.pdf"); reg("fig_prisma_flow", "PRISMA-style flow of record identification, exclusion and the final mapping corpus.", ["data/metadata/prisma_counts.json"])353637def fig_yearly_volume(R, out):38    d = pd.read_csv(f"{R}/results/bibliometrics/volume_yearly.csv"); d = d[d.year <= 2026]39    fig, ax = plt.subplots(figsize=(6.8, 3.4))40    cols = [("n", "All eligible works"), ("n_dblp", "DBLP-indexed"), ("n_cs_primary", "Primary field = CS"), ("n_arxiv", "arXiv version linked")]41    for i, (c, lab) in enumerate(cols):42        ax.plot(d.year, d[c] / 1e3, marker="o", label=lab, color=style.CATEGORICAL[i])43        ax.annotate(lab, (d.year.iloc[-1], d[c].iloc[-1] / 1e3), xytext=(4, 0), textcoords="offset points", fontsize=7, color=style.INK2, va="center")44    ax.axvspan(2026, 2026.6, color=style.GRID, alpha=0.5, lw=0); ax.text(2026.3, ax.get_ylim()[0] + 0.02 * ax.get_ylim()[1], "2026\n(partial)", fontsize=7, color=style.INK2, va="bottom", ha="center")45    ax.set_ylabel("Works (thousands)"); ax.set_xlabel(""); ax.set_xlim(2017.7, 2027.4); ax.legend(loc="upper left")46    ax.set_title("Eligible computer-science works per publication year, by membership", loc="left")47    style.save(fig, f"{out}/fig_volume_yearly.pdf"); reg("fig_volume_yearly", "Eligible works per publication year by source membership (2026 incomplete).", ["results/bibliometrics/volume_yearly.csv"])484950def fig_monthly(R, out):51    d = pd.read_csv(f"{R}/results/bibliometrics/volume_monthly.csv")52    m = d.groupby("first_month")["n"].sum().reset_index(); m = m[(m.first_month >= "2018-01") & (m.first_month <= "2026-10")]53    # month-precision only: records dated YYYY-01-01 are year-precision and are excluded from monthly series (A-002); here approximated by dropping January spikes is NOT done:54    # volume_monthly is built from first_month of all eligible works; the precise month-only series comes from category_counts_monthly (n_month_precision). We plot both when available.55    x = pd.to_datetime(m.first_month + "-01"); fig, ax = plt.subplots(figsize=(6.8, 3.2))56    ax.plot(x, m.n / 1e3, color=style.CATEGORICAL[0], lw=1.2, label="All eligible works (first appearance month)")57    style.event_line(ax, pd.Timestamp("2022-11-30"))58    ax.set_ylabel("Works per month (thousands)"); ax.set_title("Monthly first appearances of eligible works (January spikes = year-precision dates)", loc="left")59    ax.legend(loc="upper left"); style.save(fig, f"{out}/fig_volume_monthly_raw.pdf")60    reg("fig_volume_monthly_raw", "Monthly first appearances; January spikes reflect year-only publication dates and motivate the month-precision filter.", ["results/bibliometrics/volume_monthly.csv"])616263def fig_types_sources(R, out):64    d = pd.read_csv(f"{R}/results/bibliometrics/volume_yearly.csv"); d = d[d.year <= 2026]65    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))66    ax = axes[0]67    for i, (c, lab) in enumerate([("n_article", "Journal article"), ("n_conf", "Conference paper"), ("n_preprint", "Preprint")]):68        ax.plot(d.year, 100 * d[c] / d.n, marker="o", label=lab, color=style.CATEGORICAL[i])69    ax.set_ylabel("Share of eligible works (%)"); ax.set_title("Document types", loc="left"); ax.legend()70    ax = axes[1]71    for i, (c, lab) in enumerate([("n_abstract", "Abstract available"), ("n_oa", "Open access"), ("n_arxiv", "arXiv linked")]):72        ax.plot(d.year, 100 * d[c] / d.n, marker="o", label=lab, color=style.CATEGORICAL[i + 3])73    ax.set_title("Metadata completeness and openness", loc="left"); ax.legend(); ax.set_ylabel("Share of eligible works (%)")74    style.save(fig, f"{out}/fig_types_openness.pdf"); reg("fig_types_openness", "Document-type composition and metadata completeness by year.", ["results/bibliometrics/volume_yearly.csv"])757677def fig_collab(R, out):78    p = f"{R}/results/bibliometrics/team_size_yearly.csv"79    if not os.path.exists(p):80        return81    d = pd.read_csv(p); d = d[d.year <= 2026]; ind = pd.read_csv(f"{R}/results/bibliometrics/industry_share_yearly.csv")82    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))83    ax = axes[0]; ax.plot(d.year, d.mean_authors, marker="o", color=style.CATEGORICAL[0], label="Mean authors per work"); ax.set_ylabel("Authors"); ax.set_title("Team size", loc="left"); ax.legend()84    ax = axes[1]; ax.plot(d.year, 100 * d.intl / d.n, marker="o", color=style.CATEGORICAL[1], label="≥ 2 countries (all works)")85    ax.plot(ind.year, 100 * ind.share_company, marker="o", color=style.CATEGORICAL[2], label="Company affiliation (works with affiliations)")86    ax.set_ylabel("Share (%)"); ax.set_title("International and industry participation", loc="left"); ax.legend()87    style.save(fig, f"{out}/fig_collaboration.pdf"); reg("fig_collaboration", "Team size, international co-authorship and industry participation by year.", ["results/bibliometrics/team_size_yearly.csv", "results/bibliometrics/industry_share_yearly.csv"])888990def 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)92    for f in (fig_prisma, fig_yearly_volume, fig_monthly, fig_types_sources, fig_collab) + _BATCH2 + _BATCH3 + _BATCH4:93        try:94            f(a.root, out); print("ok", f.__name__)95        except Exception as e:96            print("FAIL", f.__name__, e)97    json.dump(INV, open(f"{out}/FIGURES.json", "w"), indent=1)9899100101102# ---------------- batch 2: citations and knowledge flows (generated from results/bibliometrics + results/graph) ----------------103def fig_citation_inequality(R, out):104    p = f"{R}/results/bibliometrics/citation_windows_yearly.csv"105    if not os.path.exists(p):106        return107    d = pd.read_csv(p); d = d[d.year <= 2023]  # c36 requires three full years of citation data108    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"])114115116def fig_field_inflows(R, out):117    p = f"{R}/results/graph/field_flows_yearly.csv"118    if not os.path.exists(p):119        return120    d = pd.read_csv(p); d = d[(d.citing_year >= 2018) & (d.citing_year <= 2026)]121    names = {17: "CS outside the corpus (mostly pre-2018)", 26: "Mathematics", 22: "Engineering", 31: "Physics & astronomy", 27: "Medicine", 33: "Social sciences", 13: "Biochem./genetics", 18: "Decision sciences", 28: "Neuroscience", 20: "Economics & finance", 32: "Psychology", 12: "Arts & humanities", 36: "Health professions", 11: "Agricultural & biological", 23: "Environmental science", 16: "Chemistry", 25: "Materials science", 14: "Business & management"}122    tot = d.groupby("citing_year").n.sum(); ext = d[d.kind == "external"]; internal = d[d.kind == "internal"].groupby("citing_year").n.sum() / tot * 100123    top = ext.groupby("cited_field_id").n.sum().nlargest(7).index124    fig, ax = plt.subplots(figsize=(6.8, 3.6))125    ax.plot(internal.index, internal.values, marker="o", color=style.INK2, label="Inside the corpus (CS, 2018–2026)", lw=1.4)126    ax.annotate("inside corpus", (internal.index[-1], internal.values[-1]), xytext=(3, 0), textcoords="offset points", fontsize=6.5, color=style.INK2, va="center")127    for i, f in enumerate(top):128        s = ext[ext.cited_field_id == f].groupby("citing_year").n.sum() / tot * 100129        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)130        if s.values[-1] > 5:131            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")132    ax.set_ylabel("Share of all resolved references (%)"); ax.set_title("Where the references of the CS corpus point, by citing year", loc="left"); ax.legend(ncol=2, fontsize=6.5)133    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"])134135136def fig_cited_age(R, out):137    p = f"{R}/results/graph/cited_age_yearly.csv"138    if not os.path.exists(p):139        return140    d = pd.read_csv(p); d = d[(d.age >= 0) & (d.age <= 30)]141    fig, ax = plt.subplots(figsize=(6.8, 3.2))142    for i, y in enumerate([2018, 2020, 2022, 2024, 2026]):143        g = d[d.citing_year == y]; s = g.n / g.n.sum()144        ax.plot(g.age, s * 100, color=style.SEQUENTIAL[2 + 2 * i], label=str(y), lw=1.3)145    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")146    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"])147148149def fig_venue_hhi(R, out):150    p = f"{R}/results/bibliometrics/venue_hhi_yearly.csv"151    if not os.path.exists(p):152        return153    d = pd.read_csv(p); d = d[d.year <= 2026]154    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))155    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")156    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()157    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"])158159160def fig_countries(R, out):161    p = f"{R}/results/bibliometrics/countries_yearly.csv"162    if not os.path.exists(p):163        return164    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).index165    fig, ax = plt.subplots(figsize=(6.8, 3.4))166    for i, c in enumerate(top):167        s = d[d.country == c].set_index("year").n_frac / tot * 100168        ax.plot(s.index, s.values, marker="o", color=style.CATEGORICAL[i], label=c, lw=1.2)169        if i < 4:170            ax.annotate(c, (s.index[-1], s.values[-1]), xytext=(3, 0), textcoords="offset points", fontsize=7, color=style.INK2, va="center")171    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)172    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"])173174175_BATCH2 = (fig_citation_inequality, fig_field_inflows, fig_cited_age, fig_venue_hhi, fig_countries)176177178179180# ---------------- batch 3: structural change (results/econometrics) ----------------181def fig_its_total(R, out):182    p = f"{R}/results/econometrics/fit_total.csv"183    if not os.path.exists(p):184        return185    d = pd.read_csv(p); x = pd.to_datetime(d.month + "-01"); res = json.load(open(f"{R}/results/econometrics/its_results.json"))["series"]["total"]186    fig, ax = plt.subplots(figsize=(6.8, 3.3))187    ax.plot(x, d.y / 1e3, color=style.CATEGORICAL[0], lw=1.0, label="Observed (month-precision records)")188    ax.plot(x, d.fitted / 1e3, color=style.CATEGORICAL[1], lw=1.6, label="ITS fit (log-linear, seasonal, HAC)")189    style.event_line(ax, pd.Timestamp("2022-11-30"))190    its = res["its_chatgpt"]191    ax.text(0.02, 0.97, f"level change {its['level_change']:+.3f} (p = {its['level_p']:.2f}; placebo p = {res['placebo_chatgpt']['level_t_placebo_p']:.2f})\nslope change {its['slope_change']:+.4f}/month (p = {its['slope_p']:.3f}; placebo p = {res['placebo_chatgpt']['slope_t_placebo_p']:.2f})",192            transform=ax.transAxes, fontsize=7, va="top", color=style.INK2)193    ax.set_ylabel("Works per month (thousands)"); ax.set_title("Interrupted time series of monthly CS output at the 2022-11 reference date", loc="left"); ax.legend(loc="center left", bbox_to_anchor=(0.0, 0.62))194    style.save(fig, f"{out}/fig_its_total.pdf"); reg("fig_its_total", "Monthly output with the interrupted-time-series fit; level and slope changes with HAC and placebo p-values.", ["results/econometrics/fit_total.csv", "results/econometrics/its_results.json"])195196197def fig_placebo_total(R, out):198    p = f"{R}/results/econometrics/placebo_total.csv"199    if not os.path.exists(p):200        return201    d = pd.read_csv(p); d["event"] = pd.to_datetime(d.event)202    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))203    for ax, col, lab in [(axes[0], "level_t", "Level-change t-statistic"), (axes[1], "slope_t", "Slope-change t-statistic")]:204        ax.plot(d.event, d[col], color=style.CATEGORICAL[0], lw=1.2); ax.axhline(0, color=style.AXIS, lw=0.6)205        tv = d[d.event == pd.Timestamp("2022-12-01")]206        if len(tv):207            ax.plot(tv.event, tv[col], "o", color=style.CATEGORICAL[1], ms=6); ax.annotate("2022-12", (tv.event.iloc[0], tv[col].iloc[0]), xytext=(4, 4), textcoords="offset points", fontsize=7, color=style.INK2)208        ax.set_title(lab, loc="left"); ax.set_xlabel("Placebo event month")209    style.save(fig, f"{out}/fig_placebo_total.pdf"); reg("fig_placebo_total", "Placebo distribution of ITS t-statistics over every candidate event month; the true event is highlighted.", ["results/econometrics/placebo_total.csv"])210211212_BATCH3 = (fig_its_total, fig_placebo_total)213214215216217# ---------------- batch 4: categories (after classification) ----------------218def _cat_names(R):219    import yaml220    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"]}221222223def fig_category_growth(R, out):224    p = f"{R}/results/graph/category_counts_yearly.csv"225    if not os.path.exists(p):226        return227    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])236237238def fig_category_shares(R, out):239    p = f"{R}/results/graph/category_counts_yearly.csv"240    if not os.path.exists(p):241        return242    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) * 100244    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])250251252def fig_breaks_heatmap(R, out):253    p = f"{R}/results/econometrics/breaks.csv"254    if not os.path.exists(p):255        return256    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        return259    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])268269270def fig_flow_matrix(R, out):271    p = f"{R}/results/graph/area_flows_yearly.csv"272    if not os.path.exists(p):273        return274    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) * 100278        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])286287288def 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        return292    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])300301302def fig_emerging(R, out):303    import glob as _g304    files = sorted(_g.glob(f"{R}/results/emerging/metrics_h*.json"))305    if not files:306        return307    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)316317318def fig_known_set(R, out):319    p = f"{R}/results/emerging/known_set_monthly.csv"320    if not os.path.exists(p):321        return322    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 None327        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])331332333_BATCH4 = (fig_category_growth, fig_category_shares, fig_breaks_heatmap, fig_flow_matrix, fig_cross_area, fig_emerging, fig_known_set)334335336if __name__ == "__main__":337    main()338
Files · 4e6cbe5ed1dd2a34a387c52896b7a93fdcdf1f94
Menu

Theme