Skip to content

/publication-figures

Figure conventions for empirical finance and economics: matplotlib styling, palettes, sizes, export settings, journal overrides and templates for time series, decile bars, coefficient plots and event studies.

Category: figures
Field: finance
License: MIT (repo LICENSE, "Copyright (c) 2025 Alex Dickerson")
Updated: 2026-04-19
Stages: data-analysis · dissemination

Publication-Ready Figures

For this repo, the production plotting toolkit is fintools.figures. Read docs/ai/figures.md first, then choose the plotting path deliberately:

  • Use native fintools.figures helpers for repo work, Word proof packs, validation checks, and dataframe-to-figure suites.
  • Use style="fins" for the house publication style and style="ft" for FT-style output.
  • Use the legacy skill-local finance.mplstyle / figutils.py assets only when the user explicitly wants the older standalone helper style or needs a portable snippet outside the package workflow.

To recreate the FT validation gallery:

Bash
python tools/figure_examples.py --style ft --docx --output results/figures

To recreate the house-style gallery:

Bash
python tools/figure_examples.py --style fins --docx --output results/figures

Generated PNG/PDF/DOCX/caption files belong under ignored results/figures/ paths. Do not commit maintainer proof packs or local gallery outputs.

Apply these conventions whenever creating figures. The goal: every figure Claude produces is publication-ready by default — no manual cleanup needed.

Legacy Helper Quick Start

Prefer fintools.figures for this repo. The helper assets below remain available for standalone or explicitly requested legacy publication-style plots.

Copy finance.mplstyle from this skill directory into the project, then:

Python
import matplotlib.pyplot as plt
plt.style.use('path/to/finance.mplstyle')

Or apply inline (no file needed):

Python
import matplotlib.pyplot as plt
plt.rcParams.update({
    'font.family': 'serif',
    'font.serif': ['Times New Roman', 'STIXGeneral', 'DejaVu Serif'],
    'mathtext.fontset': 'stix',
    'font.size': 9,
    'axes.labelsize': 9,
    'xtick.labelsize': 8,
    'ytick.labelsize': 8,
    'legend.fontsize': 8,
    'axes.linewidth': 0.6,
    'axes.spines.top': False,
    'axes.spines.right': False,
    'lines.linewidth': 1.2,
    'xtick.direction': 'out',
    'ytick.direction': 'out',
    'legend.frameon': False,
    'figure.dpi': 150,
    'savefig.dpi': 600,
    'savefig.format': 'pdf',
    'pdf.fonttype': 42,
})

Default Aesthetic

  • Fonts: Times New Roman / STIX (serif, no LaTeX dependency)
  • Spines: Bottom + left only (no top/right)
  • Grid: Off by default
  • Ticks: Outward, 8pt labels
  • Colors: Okabe-Ito colorblind-safe palette (blue first)
  • Export: PDF vector, 600 DPI, fonts embedded (type 42)

Color Palettes

Default cycle (Okabe-Ito, colorblind-safe):

Python
PALETTE = ['#377EB8', '#E41A1C', '#4DAF4A', '#984EA3',
           '#FF7F00', '#A65628', '#F781BF', '#999999']

Two-series (long vs short, treatment vs control):

Python
BLUE_RED = ['#377EB8', '#E41A1C']

Grayscale-safe (for guaranteed print clarity):

Python
GRAYSCALE = ['#000000', '#555555', '#999999', '#CCCCCC']
## Combine with linestyles: '-', '--', ':', '-.'

Sequential/diverging colormaps: Use viridis or cividis (colorblind-safe) for heatmaps. Use RdBu_r for diverging (correlation matrices).

Figure Sizing

Context Width (inches) Use
Single column 3.5 Most journal figures
1.5 column 5.25 Medium panels
Double column / full width 7.0 Wide multi-panel figures
Slide / presentation 10.0 Beamer, PowerPoint

Aspect ratio: Default to golden ratio (width / 1.618). Use square for heatmaps, wide (width / 2.0) for time series.

Python
def set_size(width='single', ratio='golden'):
    widths = {'single': 3.5, 'onehalf': 5.25, 'double': 7.0, 'slide': 10.0}
    ratios = {'golden': 1.618, 'square': 1.0, 'wide': 2.0}
    w = widths.get(width, width)
    r = ratios.get(ratio, ratio)
    return (w, w / r)

Common Figure Types in Empirical Finance

Time Series

Python
fig, ax = plt.subplots(figsize=set_size('double', 'wide'))
ax.plot(dates, values)
ax.set_xlabel(''); ax.set_ylabel('Return (%)')
- Use ax.axhline(0, color='grey', linewidth=0.5, zorder=0) for zero reference - Add NBER recession bands with ax.axvspan(start, end, alpha=0.1, color='grey')

Cumulative Return / Wealth Paths

Python
cumret = (1 + returns).cumprod()
ax.plot(cumret.index, cumret.values)
ax.set_ylabel('Growth of $1')
- Start at 1.0 (or 100 for percentage scale) - Log scale optional for long horizons: ax.set_yscale('log')

Decile Portfolio Bar Chart with Newey-West CIs

Python
## returns_df: DataFrame with columns 0..9 (portfolio return time series)
plot_portfolio_bars(ax, returns_df, show_ls=True, ls_label='10-1')
## Computes means, Newey-West SEs (lag = floor(T^0.25)), 95% CI error bars
## Includes long-short bar with t-stat annotation
- Use plot_portfolio_bars for any portfolio sort figure — it handles NW SEs automatically - Short leg (decile 1) colored red, long leg (decile 10) green, L-S bar purple - t-stat annotated on the L-S bar

For simple bars without CIs (pre-computed means):

Python
plot_decile_bars(ax, means, highlight_extremes=True, spread_label=True)

Coefficient Plot (Forest Plot)

Python
ax.errorbar(coefs, range(len(coefs)), xerr=[coefs-ci_lo, ci_hi-coefs],
            fmt='o', color='#377EB8', capsize=3, markersize=4)
ax.axvline(0, color='grey', linewidth=0.5, linestyle='--')
ax.set_yticks(range(len(names))); ax.set_yticklabels(names)

Event Study (CAR Plot)

Python
days = range(event_window[0], event_window[1] + 1)
ax.plot(days, car, color='#377EB8')
ax.fill_between(days, ci_lo, ci_hi, alpha=0.2, color='#377EB8')
ax.axvline(0, color='grey', linewidth=0.5, linestyle='--')
ax.axhline(0, color='grey', linewidth=0.5)
ax.set_xlabel('Days Relative to Event'); ax.set_ylabel('CAR (%)')

Correlation Heatmap

Python
import seaborn as sns
mask = np.triu(np.ones_like(corr, dtype=bool), k=1)
sns.heatmap(corr, mask=mask, cmap='RdBu_r', center=0, vmin=-1, vmax=1,
            annot=True, fmt='.2f', linewidths=0.5, ax=ax,
            cbar_kws={'shrink': 0.8})

Multi-Panel Figures

Python
fig, axes = plt.subplots(1, 3, figsize=set_size('double', 'wide'))
## Label panels
for i, ax in enumerate(axes):
    ax.text(-0.1, 1.05, f'({chr(97+i)})', transform=ax.transAxes,
            fontsize=10, fontweight='bold', va='top')

Export Checklist

Before saving any figure: 1. Format: PDF (vector) for papers; PNG (300+ DPI) for slides/web 2. Font embedding: pdf.fonttype = 42 (already in style) 3. Bbox: bbox_inches='tight' to avoid clipped labels 4. DPI: 600 for publication, 150 for screen preview 5. Size: Match target journal column width — don't rescale in LaTeX/Word

Python
fig.savefig('figure.pdf', bbox_inches='tight', dpi=600)
## Also save PNG for quick preview:
fig.savefig('figure.png', bbox_inches='tight', dpi=150)

Journal-Specific Overrides

Journal Override
RFS Export as TIF at 300 DPI (photos) or 600 DPI (line art). Fonts: Arial, Courier, Times, Helvetica, Symbol only.
AER No shading, no gridlines, no background color. Vector PDF/EPS preferred. Max 9 columns wide including row headings for tables.
JF Color figures OK online (free). Color in print costs $500/page. Design for grayscale print compatibility.
Nature Sans-serif fonts required (Helvetica/Arial). Override: plt.rcParams['font.family'] = 'sans-serif'

Do NOT

  • Use rainbow colormaps (jet, hsv) — not perceptually uniform, not colorblind-safe
  • Use 3D plots unless the data genuinely requires a third dimension
  • Add chartjunk: unnecessary gridlines, borders, background colors
  • Scale figures in LaTeX/Word — set correct size in matplotlib, include at 100%
  • Use different fonts/sizes across figures in the same paper
  • Use LaTeX escapes (\&, \%, \_) in matplotlib text (titles, labels, annotations) — matplotlib's default text engine renders backslashes literally. Write plain S&P 500, not S\&P 500. LaTeX escapes only work when plt.rcParams['text.usetex'] = True, which requires a full LaTeX installation and is NOT enabled by default in our style.