Open GitHub ↗Fork on GitHub ↗
git clone https://github.com/natalienkhalil/paper-stats.git

8a8f954 / src/plot.py

Raw
"""Generate publication-quality charts from OpenAlex data."""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns

from config import DATA_DIR, FIG_DIR, LAST_FULL_YEAR, COUNTRY_NAMES

# Seaborn styling
sns.set_theme(style="white", font_scale=1.1)
PALETTE = sns.color_palette("tab10")


def setup_fig(figsize=(12, 6)):
    fig, ax = plt.subplots(figsize=figsize)
    return fig, ax


def save(fig, name):
    path = FIG_DIR / name
    fig.savefig(path, dpi=200, bbox_inches="tight", facecolor="white")
    plt.close(fig)
    print(f"  → {path}")


def plot_total_articles():
    """Bar chart of total articles per year with exponential trend."""
    df = pd.read_csv(DATA_DIR / "yearly_totals.csv")
    full = df[df["year"] <= LAST_FULL_YEAR].copy()

    fig, ax = setup_fig()

    ax.bar(full["year"], full["count"], color=PALETTE[0], width=0.8, edgecolor="none")

    # Exponential trend line
    x = np.array(full["year"] - full["year"].min(), dtype=float)
    y = np.array(full["count"], dtype=float)
    coeffs = np.polyfit(x, np.log(y), 1)
    trend_x = np.linspace(0, x.max(), 100)
    trend_y = np.exp(coeffs[1]) * np.exp(coeffs[0] * trend_x)
    trend_years = trend_x + full["year"].min()
    ax.plot(trend_years, trend_y, color="red", linewidth=2, linestyle="--",
            label=f"Exponential fit (r={coeffs[0]*100:.1f}%/yr)", zorder=5)

    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v/1e6:.1f}M"))
    ax.set_xlabel("Year")
    ax.set_ylabel("Number of Articles")
    ax.set_title("Global Scientific Article Production (2000–2025)", fontsize=14, fontweight="bold")
    ax.legend(loc="upper left")
    save(fig, "total_articles_per_year.png")


def plot_yoy_growth():
    """Line chart of year-over-year growth rate."""
    df = pd.read_csv(DATA_DIR / "yearly_totals.csv")
    full = df[df["year"] <= LAST_FULL_YEAR].sort_values("year").copy()
    full["yoy"] = full["count"].pct_change() * 100
    full = full.dropna()

    fig, ax = setup_fig()
    ax.plot(full["year"], full["yoy"], marker="o", markersize=5, color=PALETTE[0], linewidth=2)
    ax.axhline(y=5.6, color="red", linestyle="--", alpha=0.7, label="Hanson et al. reported rate (5.6%)")
    ax.axhline(y=0, color="gray", linestyle="-", alpha=0.3)

    ax.set_xlabel("Year")
    ax.set_ylabel("Year-over-Year Growth (%)")
    ax.set_title("Annual Growth Rate of Scientific Articles", fontsize=14, fontweight="bold")
    ax.legend(loc="upper right")
    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%"))
    save(fig, "yoy_growth_rate.png")


def plot_field_growth():
    """Stacked area chart of articles by domain."""
    df = pd.read_csv(DATA_DIR / "yearly_by_domain.csv")
    full = df[df["year"] <= LAST_FULL_YEAR].copy()

    pivot = full.pivot_table(index="year", columns="domain_name", values="count", aggfunc="sum").fillna(0)
    pivot = pivot.sort_index()

    fig, ax = setup_fig()
    ax.stackplot(pivot.index, [pivot[col] for col in pivot.columns],
                 labels=pivot.columns, alpha=0.85)
    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v/1e6:.1f}M"))
    ax.set_xlabel("Year")
    ax.set_ylabel("Number of Articles")
    ax.set_title("Scientific Articles by Domain (2000–2025)", fontsize=14, fontweight="bold")
    ax.legend(loc="upper left", fontsize=9)
    save(fig, "field_growth_absolute.png")


def plot_field_growth_rates():
    """Horizontal bar chart of fastest-growing fields (2015→2025)."""
    df = pd.read_csv(DATA_DIR / "yearly_by_field.csv")
    full = df[df["year"] <= LAST_FULL_YEAR].copy()

    y1, y2 = 2015, LAST_FULL_YEAR
    f1 = full[full["year"] == y1].set_index("field_name")["count"]
    f2 = full[full["year"] == y2].set_index("field_name")["count"]
    growth = pd.DataFrame({"c_start": f1, "c_end": f2}).dropna()
    growth["growth_pct"] = (growth["c_end"] - growth["c_start"]) / growth["c_start"] * 100
    growth = growth.sort_values("growth_pct", ascending=True)  # ascending for horizontal bar

    fig, ax = setup_fig(figsize=(12, 8))
    ax.barh(growth.index, growth["growth_pct"], color="steelblue", edgecolor="none")

    ax.set_xlabel("Growth (%)")
    ax.set_title(f"Field Growth Rates ({y1}–{y2})", fontsize=14, fontweight="bold")
    ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%"))
    save(fig, "field_growth_rates.png")


def plot_field_share():
    """100% stacked area chart showing domain composition."""
    df = pd.read_csv(DATA_DIR / "yearly_by_domain.csv")
    full = df[df["year"] <= LAST_FULL_YEAR].copy()

    pivot = full.pivot_table(index="year", columns="domain_name", values="count", aggfunc="sum").fillna(0)
    pivot = pivot.sort_index()
    shares = pivot.div(pivot.sum(axis=1), axis=0) * 100

    fig, ax = setup_fig()
    ax.stackplot(shares.index, [shares[col] for col in shares.columns],
                 labels=shares.columns, alpha=0.85)

    ax.set_ylim(0, 100)
    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%"))
    ax.set_xlabel("Year")
    ax.set_ylabel("Share of Articles")
    ax.set_title("Domain Composition of Scientific Output", fontsize=14, fontweight="bold")
    ax.legend(loc="center right", fontsize=9)
    save(fig, "field_share_over_time.png")


def plot_top_countries():
    """Line chart for top 10 countries."""
    df = pd.read_csv(DATA_DIR / "yearly_by_country.csv")
    full = df[df["year"] <= LAST_FULL_YEAR].copy()

    # Find top 10 by latest year
    latest = full[full["year"] == LAST_FULL_YEAR]
    top10 = latest.nlargest(10, "count")["country_code"].tolist()

    fig, ax = setup_fig()
    for i, cc in enumerate(top10):
        sub = full[full["country_code"] == cc].sort_values("year")
        label = COUNTRY_NAMES.get(cc, cc)
        ax.plot(sub["year"], sub["count"], marker=".", linewidth=2, label=label, color=PALETTE[i % len(PALETTE)])

    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v/1e6:.1f}M" if v >= 1e6 else f"{v/1e3:.0f}K"))
    ax.set_xlabel("Year")
    ax.set_ylabel("Number of Articles")
    ax.set_title("Top 10 Countries by Scientific Output (2000–2025)", fontsize=14, fontweight="bold")
    ax.legend(loc="upper left", fontsize=8, ncol=2)
    save(fig, "top_countries_over_time.png")


def plot_country_share():
    """Share of global output for top 5 countries over time."""
    df = pd.read_csv(DATA_DIR / "yearly_by_country.csv")
    full = df[df["year"] <= LAST_FULL_YEAR].copy()

    totals = pd.read_csv(DATA_DIR / "yearly_totals.csv")
    yearly_total = totals[totals["year"] <= LAST_FULL_YEAR].set_index("year")["count"]

    # Top 5 by latest year
    latest = full[full["year"] == LAST_FULL_YEAR]
    top5 = latest.nlargest(5, "count")["country_code"].tolist()

    fig, ax = setup_fig()
    for i, cc in enumerate(top5):
        country = full[full["country_code"] == cc].groupby("year")["count"].sum()
        share = (country / yearly_total * 100).dropna()
        label = COUNTRY_NAMES.get(cc, cc)
        ax.plot(share.index, share.values, linewidth=2.5, label=label,
                color=PALETTE[i], marker=".")

    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%"))
    ax.set_xlabel("Year")
    ax.set_ylabel("Share of Global Output (%)")
    ax.set_title("Global Share: Top 5 Countries (2000–2025)", fontsize=14, fontweight="bold")
    ax.legend(loc="upper right", fontsize=9)
    save(fig, "country_share_shift.png")


def main():
    FIG_DIR.mkdir(exist_ok=True)
    print("Generating charts...")
    plot_total_articles()
    plot_yoy_growth()
    plot_field_growth()
    plot_field_growth_rates()
    plot_field_share()
    plot_top_countries()
    plot_country_share()
    print("Done.")


if __name__ == "__main__":
    main()