"""Build every LaTeX table and figure of the paper from data/*.csv (no hand-typed numbers).""" import csv, json from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt ROOT = Path(__file__).resolve().parent.parent D, T, F = ROOT / "data", ROOT / "paper" / "tables", ROOT / "paper" / "figures" T.mkdir(exist_ok=True); F.mkdir(exist_ok=True) S = json.load(open(D / "stats.json")) def rows(name): with open(D / f"{name}.csv") as f: return list(csv.DictReader(f)) def n(x, d=0): x = float(x) return f"{x:,.{d}f}".replace(",", "{,}") def w(name, body): (T / name).write_text(body); print("table", name) LAB = {"ma_integration": "M\\&A / integration", "capex_program": "Capital expenditure program", "partnership": "Strategic partnership", "geographic_expansion": "Geographic expansion", "plant_construction": "Plant / site construction", "cost_reduction": "Cost reduction / restructuring", "drug_pipeline": "Drug / clinical pipeline", "product_launch": "New product / launch", "rd_program": "R\\&D program", "infrastructure": "Infrastructure investment", "manufacturing_expansion": "Manufacturing / capacity expansion", "technology_deployment": "Technology deployment", "digital_transformation": "Digital transformation", "erp_implementation": "ERP / systems implementation", "sustainability": "Sustainability / ESG", "supply_chain": "Supply-chain initiative", "data_center": "Data center", "energy_transition": "Energy transition / renewables", "ai_initiative": "AI / ML initiative", "automation": "Automation / robotics", "cloud_migration": "Cloud migration"} # ---- Table: universe reconciliation ---- U = S["universe"] w("tab_reconciliation.tex", f"""\\begin{{tabular}}{{lr}} \\toprule Step & Count \\\\ \\midrule iShares IWV holdings file, equity lines with a ticker (as of {U['russell_holdings_as_of']}) & 2{{,}}587 \\\\ \\quad of which mapped to an SEC CIK (CVRs, escrow lines and FDIC-only banks excluded) & {n(U['russell_equity_lines_mapped'])} \\\\ \\quad distinct CIKs (share classes collapsed) & {n(U['russell_distinct_ciks'])} \\\\ S\\&P~500 constituents, cached snapshot of {U['sp500_fetched_at'][:10]} (500 CIKs) & {n(U['sp500_rows'])} \\\\ \\midrule Merged issuer universe (distinct CIKs) & {n(U['n'])} \\\\ \\quad in both lists & {n(U['both'])} \\\\ \\quad S\\&P~500 only & {n(U['sp_only'])} \\\\ \\quad Russell proxy only & {n(U['ru_only'])} \\\\ \\midrule Issuers with at least one 10-K, 10-Q or 8-K since 2010 & {n(S['issuers_with_filings'])} \\\\ Issuers without any such filing (foreign private issuers filing 20-F/40-F, FDIC-only banks) & {n(S['issuers_without_filings'])} \\\\ Issuers with at least one resolved project & {n(S['issuers_with_projects'])} \\\\ \\bottomrule \\end{{tabular}}""") # ---- Table: filings by form ---- fb = rows("filings_by_form"); ps = rows("parse_status") err = {} for r in ps: err.setdefault(r["form_type"], {})[r["parse_status"]] = int(r["n"]) body = "\\begin{tabular}{lrrrrr}\n\\toprule\nForm & Filings & First filing & Last filing & Raw HTML (GB) & Not parsed \\\\\n\\midrule\n" for r in fb: e = err.get(r["form_type"], {}); notp = e.get("error", 0) + e.get("missing", 0) body += f"{r['form_type']} & {n(r['n'])} & {r['first']} & {r['last']} & {float(r['gb']):.1f} & {notp} \\\\\n" body += f"\\midrule\nTotal & {n(S['filings_total'])} & 2010-01-04 & 2026-10-27 & {S['raw_gb']:.1f} & {sum(e.get('error',0)+e.get('missing',0) for e in err.values())} \\\\\n\\bottomrule\n\\end{{tabular}}" w("tab_filings.tex", body) # ---- Table: coverage by year ---- iy = rows("issuers_by_year"); fy = rows("filings_by_year_form"); py = {r["yr"]: r for r in rows("projects_by_first_seen_year")}; my = {r["yr"]: r for r in rows("mentions_by_year")} byf = {} for r in fy: byf.setdefault(r["yr"], {})[r["form_type"]] = r["n"] body = "\\begin{tabular}{lrrrrrrr}\n\\toprule\nYear & Issuers & 10-K & 10-Q & 8-K & Mentions & Issuers w/ mentions & Projects first seen \\\\\n\\midrule\n" for r in iy: y = r["yr"]; b = byf[y] body += f"{y} & {n(r['issuers'])} & {n(b['10-K'])} & {n(b['10-Q'])} & {n(b['8-K'])} & {n(my[y]['mentions'])} & {n(my[y]['issuers'])} & {n(py[y]['n'])} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_coverage_year.tex", body) # ---- Table: sections and extraction ledger ---- sb = {r["form_type"]: r for r in rows("sections_by_form")}; lb = rows("ledger_by_form") body = "\\begin{tabular}{lrrrrrr}\n\\toprule\nForm & Parsed sections & Median words & Sections sent to the extractor & Sections with $\\geq 1$ mention & Mentions & Issuers \\\\\n\\midrule\n" tot = [0, 0, 0, 0] for r in lb: s = sb[r["form_type"]] body += f"{r['form_type']} & {n(s['sections'])} & {n(s['median_words'])} & {n(r['sections'])} & {n(r['with_mentions'])} & {n(r['mentions'])} & {n(r['issuers'])} \\\\\n" tot[0] += int(s["sections"]); tot[1] += int(r["sections"]); tot[2] += int(r["with_mentions"]); tot[3] += int(r["mentions"]) body += f"\\midrule\nTotal & {n(tot[0])} & & {n(tot[1])} & {n(tot[2])} & {n(tot[3])} & {n(S['issuers_with_filings'])} \\\\\n\\bottomrule\n\\end{{tabular}}" w("tab_sections.tex", body) # ---- Table: 10-K / 10-Q items in scope ---- body = "\\begin{tabular}{llrr}\n\\toprule\nForm & Section (item) & Sections extracted & Mentions \\\\\n\\midrule\n" for r in rows("ledger_10k_items"): body += f"10-K & \\texttt{{{r['section_name'].replace('_','\\_')}}} & {n(r['n'])} & {n(r['mentions'])} \\\\\n" body += "\\midrule\n" for r in rows("ledger_10q_items"): body += f"10-Q & \\texttt{{{r['section_name'].replace('_','\\_')}}} & {n(r['n'])} & {n(r['mentions'])} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_items.tex", body) # ---- Table: projects by type ---- pt = rows("projects_by_type") body = "\\begin{tabular}{llrrrrr}\n\\toprule\nCode & Category & Projects & Share (\\%) & With amount & Median amount (M USD) & Mentions / project \\\\\n\\midrule\n" for r in pt: body += f"\\texttt{{{r['project_type'].replace('_','\\_')}}} & {LAB[r['project_type']]} & {n(r['n'])} & {r['pct']} & {n(r['with_amount'])} & {n(r['median_amount_musd'],1)} & {float(r['mean_mentions']):.2f} \\\\\n" body += f"\\midrule\n & Total & {n(S['projects'])} & 100.0 & {n(S['projects_fields'][0])} & 128.5 & {S['mentions']/S['projects']:.2f} \\\\\n\\bottomrule\n\\end{{tabular}}" w("tab_types.tex", body) # ---- Table: by sector ---- cs = {r["sector"]: r for r in rows("companies_by_sector")} body = "\\begin{tabular}{lrrrrr}\n\\toprule\nGICS sector (iShares) & Issuers in universe & of which S\\&P~500 & Issuers with projects & Projects & Projects per issuer \\\\\n\\midrule\n" for r in rows("projects_by_sector"): c = cs.get(r["sector"], {"n": "", "sp500": ""}) body += f"{r['sector']} & {n(c['n']) if c['n'] else ''} & {n(c['sp500']) if c['sp500'] else ''} & {n(r['issuers'])} & {n(r['n'])} & {float(r['per_issuer']):.1f} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_sectors.tex", body) # ---- Table: status, mention buckets, duration, confidence ---- st = rows("projects_by_status"); mb = rows("mentions_per_project"); du = rows("duration"); cf = rows("confidence_dist") body = "\\begin{tabular}{lr@{\\hspace{2em}}lr@{\\hspace{2em}}lr}\n\\toprule\nLatest status & Projects & Mentions per project & Projects & Span of cited years & Projects \\\\\n\\midrule\n" du_order = ["0", "1-2", "3-5", ">5", "unknown"]; dus = {r["span_years"]: r["n"] for r in du} rowsn = max(len(st), len(mb), len(du_order)) for i in range(rowsn): a = f"\\texttt{{{st[i]['status'].replace('_','\\_')}}} & {n(st[i]['n'])}" if i < len(st) else "&" b = f"{mb[i]['bucket']} & {n(mb[i]['n'])}" if i < len(mb) else "&" c = f"{du_order[i].replace('>','$>$')} years & {n(dus[du_order[i]])}" if i < len(du_order) else "&" body += f"{a} & {b} & {c} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_status.tex", body) body = "\\begin{tabular}{lr}\n\\toprule\nSelf-reported confidence (rounded) & Mentions \\\\\n\\midrule\n" for r in cf: body += f"{r['conf']} & {n(r['n'])} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_confidence.tex", body) # ---- Table: amounts ---- ad = rows("amount_dist")[0]; q = json.loads(ad["q"]); ppi = json.loads(rows("projects_per_issuer")[0]["q"]) body = f"""\\begin{{tabular}}{{lrrrrrr}} \\toprule & P10 & P25 & Median & P75 & P90 & P99 \\\\ \\midrule Largest amount cited per project, M USD ($N$ = {n(ad['n'])}) & {q[0]/1e6:,.1f} & {q[1]/1e6:,.0f} & {q[2]/1e6:,.1f} & {q[3]/1e6:,.0f} & {q[4]/1e6:,.0f} & {q[5]/1e6:,.0f} \\\\ Projects per issuer ($N$ = {n(S['issuers_with_projects'])}) & {ppi[0]:.0f} & {ppi[1]:.0f} & {ppi[2]:.0f} & {ppi[3]:.0f} & {ppi[4]:.0f} & {ppi[5]:.0f} \\\\ \\bottomrule \\end{{tabular}}""".replace(",", "{,}") w("tab_amounts.tex", body) # ---- Table: knowledge graph ---- kg = rows("kg") body = "\\begin{tabular}{llr}\n\\toprule\n & Type & Count \\\\\n\\midrule\n" for r in kg: body += f"{r['k'].capitalize()} & \\texttt{{{r['node_type'].replace('_','\\_')}}} & {n(r['n'])} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_graph.tex", body) # ---- Table: top issuers ---- body = "\\begin{tabular}{llr}\n\\toprule\nTicker & Issuer & Resolved projects \\\\\n\\midrule\n" for r in rows("top_issuers")[:12]: body += f"{r['ticker']} & {r['company_name'].title().replace('&','\\&')} & {n(r['projects'])} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_top_issuers.tex", body) # ---- Table: provenance example (Cheniere Corpus Christi) ---- ex = [r for r in rows("example_provenance") if r["ticker"] == "LNG"] body = "\\begin{tabular}{llllp{6.2cm}}\n\\toprule\nAccession & Filed & Form & Status / amount & Evidence snippet (model-generated description grounded in the section) \\\\\n\\midrule\n" for r in ex[:8]: amt = f"{float(r['amount_usd'])/1e9:.1f} B USD" if r["amount_usd"] else "--" snip = r["snippet"].replace("&", "\\&").replace("%", "\\%").replace("$", "\\$")[:170] body += f"\\texttt{{{r['accession_number']}}} & {r['filing_date']} & {r['form_type']} & {r['m_status'].replace('_','\\_')} / {amt} & {snip} \\\\\n" body += "\\bottomrule\n\\end{tabular}" w("tab_provenance.tex", body) S["example_project"] = {k: ex[0][k] for k in ("project_id", "ticker", "project_name", "project_type", "status", "total_amount_usd", "n_mentions", "first_seen", "last_seen")} if ex else {} # ---- Figures ---- BLUE, GOLD, GREY = "#16365c", "#c9a227", "#8a8a8a" yrs = [int(r["yr"]) for r in iy] fig, ax1 = plt.subplots(figsize=(7.2, 3.4)) ax1.bar(yrs, [int(r["filings"]) for r in iy], color=BLUE, alpha=.85, label="Filings (10-K, 10-Q, 8-K)") ax1.set_ylabel("Filings per year"); ax1.set_xlabel("Filing year") ax2 = ax1.twinx(); ax2.plot(yrs, [int(r["issuers"]) for r in iy], color=GOLD, marker="o", lw=2, label="Issuers with $\\geq$1 filing") ax2.set_ylabel("Issuers"); ax2.set_ylim(0, 2700) h1, l1 = ax1.get_legend_handles_labels(); h2, l2 = ax2.get_legend_handles_labels(); ax1.legend(h1 + h2, l1 + l2, loc="upper left", frameon=False, fontsize=8) for a in (ax1, ax2): a.spines[["top"]].set_visible(False) fig.tight_layout(); fig.savefig(F / "fig_coverage.pdf"); plt.close(fig) fig, ax = plt.subplots(figsize=(7.2, 5.2)) labs = [LAB[r["project_type"]].replace("\\&", "&") for r in pt][::-1]; vals = [int(r["n"]) for r in pt][::-1] ax.barh(labs, vals, color=BLUE); ax.set_xlabel("Resolved projects"); ax.spines[["top", "right"]].set_visible(False) for i, v in enumerate(vals): ax.text(v + 300, i, f"{v:,}", va="center", fontsize=7) fig.tight_layout(); fig.savefig(F / "fig_types.pdf"); plt.close(fig) fig, ax = plt.subplots(figsize=(7.2, 3.2)) ax.plot(yrs, [int(my[str(y)]["mentions"]) for y in yrs], color=BLUE, marker="o", lw=2, label="Project mentions") ax.plot(yrs, [int(py[str(y)]["n"]) for y in yrs], color=GOLD, marker="s", lw=2, label="Projects first observed") ax.set_xlabel("Filing year"); ax.set_ylabel("Count"); ax.legend(frameon=False, fontsize=8); ax.spines[["top", "right"]].set_visible(False) fig.tight_layout(); fig.savefig(F / "fig_mentions_year.pdf"); plt.close(fig) fig, ax = plt.subplots(figsize=(7.2, 3.0)) ax.bar([r["conf"] for r in cf], [int(r["n"]) for r in cf], width=0.08, color=BLUE) ax.set_xlabel("Self-reported extraction confidence"); ax.set_ylabel("Mentions"); ax.spines[["top", "right"]].set_visible(False) fig.tight_layout(); fig.savefig(F / "fig_confidence.pdf"); plt.close(fig) print("figures ok") json.dump(S, open(D / "stats.json", "w"), indent=2, default=str)