fin-paper-figure
Generate academic-quality figures (>=300 DPI) for economics and finance papers. Reads FIGURE_PLAN.md and actual data, then produces publication-ready figures using FinancialChartFactory.
Step 0: Environment Check
Before generating any figures, verify the environment:
bash# Check required packages python -c "import matplotlib; import seaborn; import pandas; print('OK')" # Check data availability ls -la data/processed/ ls -la output/fin-experiments/
python# Verify output directories exist import os output_base = "output/fin-manuscript/draft_v1" figure_dir = f"{output_base}/figures" os.makedirs(figure_dir, exist_ok=True) print(f"Figure output directory: {figure_dir}")
Step 1: Read Input Files
Read the figure plan and actual data:
pythonimport pandas as pd from pathlib import Path # Read FIGURE_PLAN.md outline_path = Path("output/fin-manuscript/draft_v1/FIGURE_PLAN.md") if outline_path.exists(): figure_plan = outline_path.read_text(encoding="utf-8") print("Read FIGURE_PLAN.md") # Read TABLE_PLAN.md for reference table_plan_path = Path("output/fin-manuscript/draft_v1/TABLE_PLAN.md") if table_plan_path.exists(): table_plan = table_plan_path.read_text(encoding="utf-8") # Read PAPER_OUTLINE.md to determine journal style outline_path = Path("output/fin-manuscript/draft_v1/PAPER_OUTLINE.md") if outline_path.exists(): paper_outline = outline_path.read_text(encoding="utf-8") # Extract target journal # target_journal = extract_journal(paper_outline)
Step 2: Configure Chart Settings
Set up the ChartConfig based on the target journal:
pythonfrom dataclasses import dataclass from typing import List, Optional from enum import Enum class JournalStyle(Enum): CHINESE_TOP = "chinese_top" # 经济研究/金融研究/管理世界 AEA = "aea" # AER/JF/JFE/RFS CHICAGO = "chicago" # JPE IEEE = "ieee" # 通用英文 @dataclass class ChartConfig: """Configuration for academic figure generation.""" # Canvas figsize: tuple = (8, 5.5) # width, height in inches dpi: int = 300 # dots per inch (publication standard) tight_layout: bool = True # Font (critical for Chinese journals) font_family: str = "Times New Roman" # English journals font_size: int = 10 title_fontsize: int = 12 label_fontsize: int = 10 legend_fontsize: int = 9 tick_fontsize: int = 9 # Colors color_palette: str = "colorblind" # "Set2" for Chinese printing primary_color: str = "#2E86AB" # Blue secondary_color: str = "#F6AE2D" # Orange accent_color: str = "#E94F37" # Red for policy year ci_color: str = "#2E86AB" # Confidence interval fill # Output output_formats: List[str] = None # ["pdf", "png", "svg"] style: str = "seaborn-v0_8-paper" # Grid grid_alpha: float = 0.3 grid_linestyle: str = "--" # Line styles line_width: float = 1.5 marker_size: float = 5 ci_alpha: float = 0.2 def __post_init__(self): if self.output_formats is None: self.output_formats = ["pdf", "png"] # Chinese journal configuration (经济研究/金融研究/管理世界) CHINESE_CONFIG = ChartConfig( figsize=(8, 5.5), dpi=300, font_family="SimHei", font_size=10, title_fontsize=12, label_fontsize=10, legend_fontsize=9, tick_fontsize=9, color_palette="Set2", # Better for Chinese printing primary_color="#2E86AB", secondary_color="#F6AE2D", accent_color="#E94F37", ci_color="#2E86AB", output_formats=["pdf", "png"], style="seaborn-v0_8-paper", grid_alpha=0.3, grid_linestyle="--", line_width=1.5, marker_size=5, ci_alpha=0.2, ) # English top journal configuration (JF/JFE/RFS/AER) ENGLISH_CONFIG = ChartConfig( figsize=(7, 5), dpi=300, font_family="Times New Roman", font_size=10, title_fontsize=12, label_fontsize=10, legend_fontsize=9, tick_fontsize=9, color_palette="colorblind", primary_color="#4472C4", secondary_color="#ED7D31", accent_color="#C00000", ci_color="#4472C4", output_formats=["pdf", "png"], style="seaborn-v0_8-paper", grid_alpha=0.3, grid_linestyle="--", line_width=1.5, marker_size=5, ci_alpha=0.2, )
Step 3: Set Up FinancialChartFactory
pythonimport matplotlib.pyplot as plt import matplotlib.patches as mpatches import seaborn as sns import pandas as pd import numpy as np from pathlib import Path from typing import Optional, Dict, List, Tuple, Any from datetime import datetime class FinancialChartFactory: """ Factory for generating publication-quality academic figures in economics and finance. """ def __init__(self, output_dir: str, config: ChartConfig): self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True) self.config = config self._apply_style() self._setup_fonts() def _apply_style(self): """Apply matplotlib style settings.""" plt.style.use(self.config.style) sns.set_palette(self.config.color_palette) def _setup_fonts(self): """Configure fonts for the target journal.""" if self.config.font_family == "SimHei": # Chinese font setup plt.rcParams['font.sans-serif'] = ['SimHei', 'Heiti TC', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False else: # English font setup plt.rcParams['font.family'] = ['Times New Roman'] plt.rcParams['font.size'] = self.config.font_size plt.rcParams['axes.titlesize'] = self.config.title_fontsize plt.rcParams['axes.labelsize'] = self.config.label_fontsize plt.rcParams['xtick.labelsize'] = self.config.tick_fontsize plt.rcParams['ytick.labelsize'] = self.config.tick_fontsize def _save(self, fig, filename: str): """Save figure in multiple formats.""" base_path = self.output_dir / filename.replace(".pdf", "").replace(".png", "") for fmt in self.config.output_formats: path = base_path.with_suffix(f".{fmt}") fig.savefig(path, dpi=self.config.dpi, bbox_inches='tight', format=fmt) print(f" Saved: {path}") def _add_publication_style(self, ax, xlabel: str = "", ylabel: str = "", title: str = "", grid: bool = True): """Apply consistent publication styling to a plot.""" if xlabel: ax.set_xlabel(xlabel, fontsize=self.config.label_fontsize) if ylabel: ax.set_ylabel(ylabel, fontsize=self.config.label_fontsize) if title: ax.set_title(title, fontsize=self.config.title_fontsize, fontweight='bold') if grid: ax.grid(True, alpha=self.config.grid_alpha, linestyle=self.config.grid_linestyle) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) def plot_parallel_trends(self, df: pd.DataFrame, time_var: str = "relative_time", treat_var: str = "did", y_var: str = "y", ci_level: float = 0.95, save_path: str = "parallel_trends.pdf", title: str = "Parallel Trends Test") -> plt.Figure: """ Plot parallel trends / event study figure for DID designs. Parameters ---------- df : pd.DataFrame Data with columns: relative_time, y (point estimate), y_se (std err) time_var : str Column name for relative time (e.g., -5 to +5) treat_var : str Column name for treatment indicator (unused, kept for API compatibility) y_var : str Column name for the outcome variable ci_level : float Confidence interval level (default 0.95) save_path : str Output filename title : str Figure title Returns ------- plt.Figure """ fig, ax = plt.subplots(figsize=self.config.figsize) # Compute confidence interval z = 1.96 if ci_level == 0.95 else 1.645 df = df.copy() if 'y_se' in df.columns: df['ci_upper'] = df[y_var] + z * df['y_se'] df['ci_lower'] = df[y_var] - z * df['y_se'] # Determine pre/post split if time_var in df.columns: time_col = time_var else: time_col = df.columns[0] # Plot point estimates ax.plot(df[time_col], df[y_var], color=self.config.primary_color, linewidth=self.config.line_width, marker='o', markersize=self.config.marker_size, label='DID Estimate') # Plot confidence interval if 'ci_upper' in df.columns: ax.fill_between(df[time_col], df['ci_lower'], df['ci_upper'], color=self.config.primary_color, alpha=self.config.ci_alpha, label='95% CI') # Add vertical line at policy year (time=0) if 0 in df[time_col].values: ax.axvline(x=0, color=self.config.accent_color, linestyle='--', linewidth=1.5, label='Policy Year') # Add horizontal line at y=0 ax.axhline(y=0, color='gray', linestyle='-', linewidth=0.8, alpha=0.5) # Reference line at 0 for pre-period (should be near 0 if parallel holds) ax.set_xlabel('Relative Time (Years)', fontsize=self.config.label_fontsize) ax.set_ylabel('DID Coefficient', fontsize=self.config.label_fontsize) ax.set_title(title, fontsize=self.config.title_fontsize, fontweight='bold') self._add_publication_style(ax) ax.legend(loc='best', fontsize=self.config.legend_fontsize) self._save(fig, save_path) plt.close(fig) return fig
Step 4: Figure Generation Functions
4.1 DID专用图 (DID Event Study)
pythondef plot_cumulative_effect(self, df: pd.DataFrame, event_window: str = "relative_week", car: str = "car", se: Optional[str] = None, save_path: str = "cumulative_effect.pdf", title: str = "Cumulative Abnormal Returns") -> plt.Figure: """ Plot cumulative abnormal returns (CAR) event study. Used for short-window event studies around policy announcements. """ fig, ax = plt.subplots(figsize=self.config.figsize) z = 1.96 df = df.copy() # Compute CAR if not provided if car not in df.columns and 'car' in df.columns: df[car] = df['car'] if event_window not in df.columns: ax.plot(df.index, df[car], color=self.config.primary_color, linewidth=self.config.line_width, marker='o', markersize=self.config.marker_size) else: ax.plot(df[event_window], df[car], color=self.config.primary_color, linewidth=self.config.line_width, marker='o', markersize=self.config.marker_size) # Add CI if SE provided if se and se in df.columns: upper = df[car] + z * df[se] lower = df[car] - z * df[se] if event_window in df.columns: ax.fill_between(df[event_window], lower, upper, color=self.config.primary_color, alpha=self.config.ci_alpha) else: ax.fill_between(df.index, lower, upper, color=self.config.primary_color, alpha=self.config.ci_alpha) # Event window boundaries ax.axvline(x=0, color=self.config.accent_color, linestyle='--', linewidth=1.5) ax.axhline(y=0, color='gray', linestyle='-', linewidth=0.8, alpha=0.5) self._add_publication_style(ax, ylabel="Cumulative Return (%)", title=title) self._save(fig, save_path) plt.close(fig) return fig def plot_placebo_distribution(self, coefs: np.ndarray, true_coef: float, save_path: str = "placebo.pdf", title: str = "Placebo Test", xlabel: str = "Estimated Coefficient", n_simulations: int = 500) -> plt.Figure: """ Plot histogram of placebo (randomized) coefficients with true coefficient marked. Parameters ---------- coefs : np.ndarray Array of estimated coefficients from placebo tests true_coef : float The actual estimated coefficient from the main regression save_path : str Output filename title : str Figure title xlabel : str X-axis label n_simulations : int Number of placebo simulations (for caption) """ fig, ax = plt.subplots(figsize=self.config.figsize) # Histogram of placebo coefficients n, bins, patches = ax.hist(coefs, bins=30, color=self.config.primary_color, alpha=0.7, edgecolor='white', linewidth=0.5) # Mark the true coefficient ymax = ax.get_ylim()[1] ax.axvline(x=true_coef, color=self.config.accent_color, linestyle='--', linewidth=2.0, label=f'True Effect = {true_coef:.3f}') # Mark 95th percentile of placebo distribution p95 = np.percentile(coefs, 95) ax.axvline(x=p95, color='gray', linestyle=':', linewidth=1.5, label=f'95th Pct = {p95:.3f}') ax.set_xlabel(xlabel, fontsize=self.config.label_fontsize) ax.set_ylabel('Frequency', fontsize=self.config.label_fontsize) ax.set_title(title, fontsize=self.config.title_fontsize, fontweight='bold') self._add_publication_style(ax) # Legend ax.legend(loc='upper right', fontsize=self.config.legend_fontsize) self._save(fig, save_path) plt.close(fig) return fig def plot_robustness_summary(self, results_dict: Dict[str, Tuple[float, float]], save_path: str = "robustness_forest.pdf", title: str = "Robustness Check Results", true_coef: Optional[float] = None) -> plt.Figure: """ Forest plot summarizing robustness check coefficients. Parameters ---------- results_dict : dict Dictionary mapping test names to (coef, se) tuples save_path : str Output filename title : str Figure title true_coef : float, optional The baseline coefficient to compare against """ names = list(results_dict.keys()) coefs = [v[0] for v in results_dict.values()] ses = [v[1] for v in results_dict.values()] # Sort by coefficient value sorted_pairs = sorted(zip(names, coefs, ses), key=lambda x: x[1]) names, coefs, ses = zip(*sorted_pairs) y_pos = np.arange(len(names)) z = 1.96 fig, ax = plt.subplots(figsize=(7, max(4, len(names) * 0.4))) # Horizontal dot plot ax.scatter(coefs, y_pos, color=self.config.primary_color, s=50, zorder=3) # CI whiskers for i, (c, s) in enumerate(zip(coefs, ses)): ax.plot([c - z*s, c + z*s], [i, i], color=self.config.primary_color, linewidth=1.5) ax.plot([c - z*s, c - z*s], [i - 0.1, i + 0.1], color=self.config.primary_color, linewidth=1.5) ax.plot([c + z*s, c + z*s], [i - 0.1, i + 0.1], color=self.config.primary_color, linewidth=1.5) # Reference line if true_coef is not None: ax.axvline(x=true_coef, color=self.config.accent_color, linestyle='--', linewidth=1.5, label=f'Baseline = {true_coef:.3f}') ax.axvline(x=0, color='gray', linestyle='-', linewidth=0.8, alpha=0.5) ax.set_yticks(y_pos) ax.set_yticklabels(names, fontsize=self.config.tick_fontsize) ax.set_xlabel('Coefficient Estimate', fontsize=self.config.label_fontsize) ax.set_title(title, fontsize=self.config.title_fontsize, fontweight='bold') self._add_publication_style(ax) if true_coef is not None: ax.legend(loc='upper right', fontsize=self.config.legend_fontsize) self._save(fig, save_path) plt.close(fig) return fig
4.2 异质性分析图
pythondef plot_heterogeneity(self, df: pd.DataFrame, group_col: str, y_var: str, se_var: Optional[str] = None, save_path: str = "heterogeneity.pdf", title: str = "Heterogeneity Analysis", ylabel: str = "DID Coefficient", xlabel: str = "Group") -> plt.Figure: """ Grouped bar chart for heterogeneity analysis. Parameters ---------- df : pd.DataFrame Data with group_col, y_var, and optionally se_var group_col : str Column for grouping (e.g., 'size_group', 'soe', 'industry') y_var : str Column for the coefficient/estimate se_var : str, optional Column for standard error (for CI whiskers) save_path : str Output filename title : str Figure title ylabel : str Y-axis label xlabel : str X-axis label """ fig, ax = plt.subplots(figsize=self.config.figsize) groups = df[group_col].unique() x_pos = np.arange(len(groups)) # Colors colors = sns.color_palette(self.config.color_palette, n_colors=len(groups)) bars = ax.bar(x_pos, df[y_var], color=colors, width=0.6, edgecolor='white', linewidth=0.5) # Add CI whiskers if SE provided if se_var: z = 1.96 for i, (bar, se) in enumerate(zip(bars, df[se_var])): ax.plot([bar.get_x() + bar.get_width()/2, bar.get_x() + bar.get_width()/2], [bar.get_height() - z*se, bar.get_height() + z*se], color='black', linewidth=1.0) ax.plot([bar.get_x() + bar.get_width()/2 - 0.05, bar.get_x() + bar.get_width()/2 + 0.05], [bar.get_height() + z*se, bar.get_height() + z*se], color='black', linewidth=1.0) ax.plot([bar.get_x() + bar.get_width()/2 - 0.05, bar.get_x() + bar.get_width()/2 + 0.05], [bar.get_height() - z*se, bar.get_height() - z*se], color='black', linewidth=1.0) # Add value labels on bars for bar in bars: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height, f'{height:.3f}', ha='center', va='bottom', fontsize=self.config.tick_fontsize) ax.set_xticks(x_pos) ax.set_xticklabels(groups, fontsize=self.config.tick_fontsize, rotation=15 if len(groups) > 4 else 0) ax.set_ylabel(ylabel, fontsize=self.config.label_fontsize) ax.set_xlabel(xlabel, fontsize=self.config.label_fontsize) ax.set_title(title, fontsize=self.config.title_fontsize, fontweight='bold') self._add_publication_style(ax, grid=False) self._save(fig, save_path) plt.close(fig) return fig def plot_marginal_effects(self, df: pd.DataFrame, x_var: str, marginal_effect: str, se: Optional[str] = None, save_path: str = "marginal_effects.pdf", title: str = "Marginal Effects", xlabel: Optional[str] = None, ylabel: str = "Marginal Effect") -> plt.Figure: """ Plot marginal effects with confidence intervals. """ fig, ax = plt.subplots(figsize=self.config.figsize) z = 1.96 ax.plot(df[x_var], df[marginal_effect], color=self.config.primary_color, linewidth=self.config.line_width, marker='o', markersize=self.config.marker_size) if se and se in df.columns: upper = df[marginal_effect] + z * df[se] lower = df[marginal_effect] - z * df[se] ax.fill_between(df[x_var], lower, upper, color=self.config.primary_color, alpha=self.config.ci_alpha) ax.axhline(y=0, color='gray', linestyle='-', linewidth=0.8, alpha=0.5) self._add_publication_style(ax, xlabel=xlabel or x_var, ylabel=ylabel, title=title) self._save(fig, save_path) plt.close(fig) return fig
4.3 资产定价图
pythondef plot_factor_returns(self, df: pd.DataFrame, date_col: str = "date", factor_cols: List[str] = None, save_path: str = "factor_returns.pdf", title: str = "Factor Cumulative Returns", factor_names: Optional[Dict[str, str]] = None) -> plt.Figure: """ Plot cumulative factor returns over time. Parameters ---------- df : pd.DataFrame Data with date column and factor return columns date_col : str Column name for dates factor_cols : list List of factor column names (e.g., ['mkt_rf', 'smb', 'hml']) save_path : str Output filename title : str Figure title factor_names : dict, optional Mapping from factor_cols to display names """ if factor_cols is None: factor_cols = ['mkt_rf', 'smb', 'hml'] if factor_names is None: factor_names = {} fig, ax = plt.subplots(figsize=self.config.figsize) colors = sns.color_palette(self.config.color_palette, n_colors=len(factor_cols)) for i, col in enumerate(factor_cols): cumulative = (1 + df[col]/100).cumprod() label = factor_names.get(col, col) ax.plot(df[date_col], cumulative, color=colors[i], linewidth=self.config.line_width, label=label) ax.axhline(y=1, color='gray', linestyle='-', linewidth=0.8, alpha=0.5) ax.set_xlabel('Date', fontsize=self.config.label_fontsize) ax.set_ylabel('Cumulative Return (Starting at 1)', fontsize=self.config.label_fontsize) ax.set_title(title, fontsize=self.config.title_fontsize, fontweight='bold') self._add_publication_style(ax) ax.legend(loc='best', fontsize=self.config.legend_fontsize, framealpha=0.9) self._save(fig, save_path) plt.close(fig) return fig def plot_correlation_matrix(self, df: pd.DataFrame, var_cols: List[str], save_path: str = "corr_heatmap.pdf", title: str = "Correlation Matrix", method: str = 'pearson', annot: bool = True, fmt: str = '.2f') -> plt.Figure: """ Plot correlation matrix as a heatmap. """ corr = df[var_cols].corr(method=method) fig, ax = plt.subplots(figsize=(max(6, len(var_cols) * 0.8), max(5, len(var_cols) * 0.8))) mask = np.triu(np.ones_like(corr, dtype=bool), k=1) cmap = sns.diverging_palette(220, 10, as_cmap=True) sns.heatmap(corr, mask=mask, cmap=cmap, center=0, annot=annot, fmt=fmt, square=True, linewidths=0.5, ax=ax, cbar_kws={"shrink": 0.8}, annot_kws={"size": self.config.tick_fontsize - 1}) ax.set_title(title, fontsize=self.config.title_fontsize, fontweight='bold', pad=10) plt.xticks(rotation=45, ha='right', fontsize=self.config.tick_fontsize) plt.yticks(rotation=0, fontsize=self.config.tick_fontsize) self._save(fig, save_path) plt.close(fig) return fig def plot_time_series(self, df: pd.DataFrame, date_col: str, value_col: str, save_path: str = "time_series.pdf", title: str = "Time Series", ylabel: Optional[str] = None, xlabel: str = "Date", hline_value: Optional[float] = None, hline_label: Optional[str] = None) -> plt.Figure: """ Plot generic time series with optional reference line. """ fig, ax = plt.subplots(figsize=self.config.figsize) ax.plot(df[date_col], df[value_col], color=self.config.primary_color, linewidth=self.config.line_width) if hline_value is not None: ax.axhline(y=hline_value, color=self.config.accent_color, linestyle='--', linewidth=1.5, label=hline_label or f'{hline_value}') ax.legend(loc='best', fontsize=self.config.legend_fontsize) self._add_publication_style(ax, xlabel=xlabel, ylabel=ylabel or value_col, title=title) self._save(fig, save_path) plt.close(fig) return fig
Step 5: Provenance Tracking
Record data lineage for every figure:
pythonfrom dataclasses import dataclass, field, asdict from datetime import datetime from typing import Optional, Dict, Any, List import json @dataclass class ProvenanceRecord: """Record of data provenance for a generated figure.""" entity: str # e.g., "figure:parallel_trends" source: str # e.g., "empirical_results.dta" transformation: str # e.g., "event_study_regression" timestamp: str # ISO format datetime parameters: Dict[str, Any] = field(default_factory=dict) output_files: List[str] = field(default_factory=list) data_hash: Optional[str] = None # hash of source data figure_code: Optional[str] = None # snippet of plotting code notes: Optional[str] = None class ProvenanceTracker: """ Track provenance for all generated figures. Ensures reproducibility and data lineage. """ def __init__(self, output_path: str = "output/fin-manuscript/draft_v1/provenance.json"): self.output_path = Path(output_path) self.output_path.parent.mkdir(parents=True, exist_ok=True) self.records: List[ProvenanceRecord] = [] self._load_existing() def _load_existing(self): """Load existing provenance records.""" if self.output_path.exists(): with open(self.output_path, "r", encoding="utf-8") as f: data = json.load(f) self.records = [ProvenanceRecord(**r) for r in data.get("records", [])] def _save(self): """Save provenance records to JSON.""" data = { "version": "1.0", "generated_at": datetime.now().isoformat(), "records": [asdict(r) for r in self.records] } with open(self.output_path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def record(self, entity: str, source: str, transformation: str, parameters: Optional[Dict[str, Any]] = None, output_files: Optional[List[str]] = None, data_hash: Optional[str] = None, figure_code: Optional[str] = None, notes: Optional[str] = None): """ Record a provenance entry for a generated figure. """ record = ProvenanceRecord( entity=entity, source=source, transformation=transformation, timestamp=datetime.now().isoformat(), parameters=parameters or {}, output_files=output_files or [], data_hash=data_hash, figure_code=figure_code, notes=notes, ) self.records.append(record) self._save() print(f" Provenance recorded: {entity}")
Step 5b: 图表注释规范(强制)
所有生成的图表 caption 必须包含以下要素,否则不得输出:
不合格(禁止):
pythonfig.suptitle("TR 和 ESP 的趋势变化") # ❌ 仅描述"展示什么" ax.set_title("图1 平行趋势检验") # ❌ 无解读
合格(必须):
python# 平行趋势图的 caption caption = ( "图1 平行趋势检验。纵轴为事件研究法估计的ATT系数,横轴为相对政策实施年份(0为政策实施时点)。" "政策前(t-5至t-1)处理组与对照组系数均不显著异于零且置信区间包含0,支持平行趋势假设;" "政策后(t+1至t+3)处理组系数持续为正且逐步上升(ATT从0.58上升至1.25)," "表明LCCP政策显著促进了试点城市TR提升。" )
Caption 必须包含(三者缺一不可):
- 图表读法:坐标轴含义、参考线意义
- 核心发现:政策前平行/政策后上升等关键结论
- 具体数字:ATT 值、置信区间、倍数等
代码示例:
pythondef _save_with_provenance_caption(fig, ax, filename, title, key_finding, numbers): """保存图表时自动生成合格 caption。""" # 自动组合 caption caption = f"{title}。{key_finding}。{numbers}" ax.set_title(caption, fontsize=config.title_fontsize, fontweight='bold', pad=10) fig.savefig(...)
自动化检测:
生成图表后,用 scripts/paper_language_checker.py 检测 caption 中的数字和解读关键词。
Step 6: Generate All Figures from FIGURE_PLAN.md
Main execution script:
python#!/usr/bin/env python3 """Generate all figures from FIGURE_PLAN.md.""" import pandas as pd import numpy as np from pathlib import Path from financial_chart_factory import FinancialChartFactory, ChartConfig, ProvenanceTracker def main(): # === Configuration === output_dir = "output/fin-manuscript/draft_v1" figure_dir = f"{output_dir}/figures" data_dir = "output/fin-experiments" # Detect journal style from PAPER_OUTLINE.md outline_file = Path(f"{output_dir}/PAPER_OUTLINE.md") if outline_file.exists(): content = outline_file.read_text(encoding="utf-8") if "经济研究" in content or "金融研究" in content or "管理世界" in content: config = ChartConfig( figsize=(8, 5.5), dpi=300, font_family="SimHei", color_palette="Set2", ) else: config = ChartConfig( figsize=(7, 5), dpi=300, font_family="Times New Roman", color_palette="colorblind", ) else: config = ChartConfig() # Initialize factory and tracker factory = FinancialChartFactory(figure_dir, config) tracker = ProvenanceTracker(output_path=f"{output_dir}/provenance.json") print("=" * 60) print("Generating Academic Figures") print(f"Output directory: {figure_dir}") print(f"Journal config: {config.font_family}, {config.dpi} DPI") print("=" * 60) # === 图4-1: 平行趋势检验 === print("\n[1/4] Generating parallel trends figure...") try: pt_data = pd.read_stata(f"{data_dir}/parallel_trends.dta") factory.plot_parallel_trends( df=pt_data, time_var="relative_time", y_var="did_coef", save_path="fig_parallel_trends.pdf", title="Parallel Trends Test" ) tracker.record( entity="figure:parallel_trends", source=f"{data_dir}/parallel_trends.dta", transformation="event_study_regression", parameters={"ci_level": 0.95}, output_files=[f"{figure_dir}/fig_parallel_trends.pdf"], ) except Exception as e: print(f" ⚠ Could not generate: {e}") # === 图4-2: 动态效应 === print("\n[2/4] Generating dynamic effects figure...") try: dyn_data = pd.read_stata(f"{data_dir}/dynamic_effects.dta") factory.plot_cumulative_effect( df=dyn_data, event_window="relative_year", car="car", se="car_se", save_path="fig_dynamic_effects.pdf", title="Dynamic Treatment Effects" ) except Exception as e: print(f" ⚠ Could not generate: {e}") # === 图5-1: 安慰剂检验 === print("\n[3/4] Generating placebo test figure...") try: placebo_coefs = np.load(f"{data_dir}/placebo_coefs.npy") baseline_coef = 0.052 # from main regression factory.plot_placebo_distribution( coefs=placebo_coefs, true_coef=baseline_coef, save_path="fig_placebo.pdf", title="Placebo Test (500 Simulations)", xlabel="Estimated DID Coefficient", n_simulations=500 ) except Exception as e: print(f" ⚠ Could not generate: {e}") # === 图4-3: 异质性分析 === print("\n[4/4] Generating heterogeneity figure...") try: het_data = pd.read_stata(f"{data_dir}/heterogeneity_results.dta") factory.plot_heterogeneity( df=het_data, group_col="size_group", y_var="did_coef", se_var="did_se", save_path="fig_het_size.pdf", title="Heterogeneity by Firm Size", ylabel="DID Coefficient", xlabel="Size Quintile" ) except Exception as e: print(f" ⚠ Could not generate: {e}") print("\n" + "=" * 60) print("✅ Figure generation complete!") print(f"Output: {figure_dir}/") print("=" * 60) if __name__ == "__main__": main()
Step 7: Quick Reference — Chart Types
| Figure Type | Method | Use Case |
|---|---|---|
| Event Study / Parallel Trends | plot_parallel_trends() | DID identification check |
| Cumulative Effect | plot_cumulative_effect() | CAR / cumulative shock response |
| Forest Plot | plot_robustness_summary() | Multiple robustness coefficients |
| Placebo Distribution | plot_placebo_distribution() | Randomization inference |
| Grouped Bar | plot_heterogeneity() | Heterogeneous treatment effects |
| Marginal Effects | plot_marginal_effects() | Continuous moderator |
| Factor Returns | plot_factor_returns() | Asset pricing factor performance |
| Correlation Heatmap | plot_correlation_matrix() | Variable correlation overview |
| Time Series | plot_time_series() | Macro indicators, portfolio returns |
Output Summary
After generation, print a summary:
✅ FIGURES GENERATED Figures: 4 Output: output/fin-manuscript/draft_v1/figures/ ├── fig_parallel_trends.pdf (8.0 x 5.5 in, 300 DPI) ├── fig_dynamic_effects.pdf (8.0 x 5.5 in, 300 DPI) ├── fig_placebo.pdf (8.0 x 5.5 in, 300 DPI) └── fig_het_size.pdf (8.0 x 5.5 in, 300 DPI) Provenance: output/fin-manuscript/draft_v1/provenance.json
ChartConfig Quick Reference
python# For Chinese journals (经济研究/金融研究/管理世界) config = ChartConfig( font_family="SimHei", # Critical: Chinese characters font_size=10, title_fontsize=12, label_fontsize=10, color_palette="Set2", # Colorblind-safe for Chinese printing dpi=300, # Publication standard figsize=(8, 5.5), # Standard academic ratio ) # For English journals (JF/JFE/RFS/AER) config = ChartConfig( font_family="Times New Roman", font_size=10, title_fontsize=12, label_fontsize=10, color_palette="colorblind", dpi=300, figsize=(7, 5), )


