WP44: structural-change figures, figure fixes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
18 changed files +65 −8
modified
figures/FIGURES.json
+15 −0
@@ -69,5 +69,20 @@ | |||||
| 69 | "sources": [ | 69 | "sources": [ | ||
| 70 | "results/bibliometrics/countries_yearly.csv" | 70 | "results/bibliometrics/countries_yearly.csv" | ||
| 71 | ] | 71 | ] | ||
| 72 | + }, | ||||
| 73 | + { | ||||
| 74 | + "file": "fig_its_total", | ||||
| 75 | + "caption": "Monthly output with the interrupted-time-series fit; level and slope changes with HAC and placebo p-values.", | ||||
| 76 | + "sources": [ | ||||
| 77 | + "results/econometrics/fit_total.csv", | ||||
| 78 | + "results/econometrics/its_results.json" | ||||
| 79 | + ] | ||||
| 80 | + }, | ||||
| 81 | + { | ||||
| 82 | + "file": "fig_placebo_total", | ||||
| 83 | + "caption": "Placebo distribution of ITS t-statistics over every candidate event month; the true event is highlighted.", | ||||
| 84 | + "sources": [ | ||||
| 85 | + "results/econometrics/placebo_total.csv" | ||||
| 86 | + ] | ||||
| 72 | } | 87 | } | ||
| 73 | ] | 88 | ] | ||
| \ No newline at end of file | |||||
modified
figures/fig_citation_inequality.pdf
+0 −0
Binary file not shown.
modified
figures/fig_cited_age.pdf
+0 −0
Binary file not shown.
modified
figures/fig_collaboration.pdf
+0 −0
Binary file not shown.
modified
figures/fig_country_shares.pdf
+0 −0
Binary file not shown.
modified
figures/fig_country_shares.png
+0 −0
Binary file not shown.
modified
figures/fig_field_inflows.pdf
+0 −0
Binary file not shown.
modified
figures/fig_field_inflows.png
+0 −0
Binary file not shown.
added
figures/fig_its_total.pdf
+0 −0
Binary file not shown.
added
figures/fig_its_total.png
+0 −0
Binary file not shown.
added
figures/fig_placebo_total.pdf
+0 −0
Binary file not shown.
added
figures/fig_placebo_total.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.
modified
figures/fig_venue_concentration.pdf
+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/make_figures.py
+50 −8
@@ -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) + _BATCH2: | 92 | + for f in (fig_prisma, fig_yearly_volume, fig_monthly, fig_types_sources, fig_collab) + _BATCH2 + _BATCH3: | ||
| 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: | ||
@@ -118,15 +118,18 @@ def fig_field_inflows(R, out): | |||||
| 118 | if not os.path.exists(p): | 118 | if not os.path.exists(p): | ||
| 119 | return | 119 | return | ||
| 120 | d = pd.read_csv(p); d = d[(d.citing_year >= 2018) & (d.citing_year <= 2026)] | 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"} | 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"] | 122 | + tot = d.groupby("citing_year").n.sum(); ext = d[d.kind == "external"]; internal = d[d.kind == "internal"].groupby("citing_year").n.sum() / tot * 100 | ||
| 123 | − top = ext.groupby("cited_field_id").n.sum().nlargest(8).index | 123 | + top = ext.groupby("cited_field_id").n.sum().nlargest(7).index | ||
| 124 | − fig, ax = plt.subplots(figsize=(6.8, 3.4)) | 124 | + 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") | ||||
| 125 | for i, f in enumerate(top): | 127 | for i, f in enumerate(top): | ||
| 126 | s = ext[ext.cited_field_id == f].groupby("citing_year").n.sum() / tot * 100 | 128 | 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) | 129 | 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") | 130 | + if s.values[-1] > 5: | ||
| 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) | 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) | ||||
| 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"]) | 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"]) | ||
| 131 | 134 | ||||
| 132 | 135 | ||||
@@ -162,7 +165,9 @@ def fig_countries(R, out): | |||||
| 162 | fig, ax = plt.subplots(figsize=(6.8, 3.4)) | 165 | fig, ax = plt.subplots(figsize=(6.8, 3.4)) | ||
| 163 | for i, c in enumerate(top): | 166 | for i, c in enumerate(top): | ||
| 164 | s = d[d.country == c].set_index("year").n_frac / tot * 100 | 167 | 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") | 168 | + 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") | ||||
| 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) | 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) | ||
| 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"]) | 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"]) | ||
| 168 | 173 | ||||
@@ -170,5 +175,42 @@ def fig_countries(R, out): | |||||
| 170 | _BATCH2 = (fig_citation_inequality, fig_field_inflows, fig_cited_age, fig_venue_hhi, fig_countries) | 175 | _BATCH2 = (fig_citation_inequality, fig_field_inflows, fig_cited_age, fig_venue_hhi, fig_countries) | ||
| 171 | 176 | ||||
| 172 | 177 | ||||
| 178 | + | ||||
| 179 | + | ||||
| 180 | +# ---------------- batch 3: structural change (results/econometrics) ---------------- | ||||
| 181 | +def fig_its_total(R, out): | ||||
| 182 | + p = f"{R}/results/econometrics/fit_total.csv" | ||||
| 183 | + if not os.path.exists(p): | ||||
| 184 | + return | ||||
| 185 | + 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="lower right") | ||||
| 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"]) | ||||
| 195 | + | ||||
| 196 | + | ||||
| 197 | +def fig_placebo_total(R, out): | ||||
| 198 | + p = f"{R}/results/econometrics/placebo_total.csv" | ||||
| 199 | + if not os.path.exists(p): | ||||
| 200 | + return | ||||
| 201 | + 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"]) | ||||
| 210 | + | ||||
| 211 | + | ||||
| 212 | +_BATCH3 = (fig_its_total, fig_placebo_total) | ||||
| 213 | + | ||||
| 214 | + | ||||
| 173 | if __name__ == "__main__": | 215 | if __name__ == "__main__": | ||
| 174 | main() | 216 | main() | ||
| 175 | 217 | ||||