Reproducible Research and Publication-Ready Outputs
Learning Objectives
- Structure a research project for full reproducibility — from raw data to final tables
- Create publication-quality tables using statsmodels summaries and formatted output
- Produce journal-ready figures with matplotlib: custom styles, multi-panel layouts, proper DPI
- Build dynamic documents that update automatically when data or models change
- Apply version control with Git to track every stage of empirical work
- Package a complete, reproducible research project ready for journal submission
Setup & Prerequisites
pip install pandas numpy statsmodels linearmodels matplotlib seaborn jupyterlab
This is the capstone module. Everything from Modules 1–9 comes together here — not new techniques, but the practices that make your research credible, reusable, and publishable.
In This Module
1. The Reproducible Research Project — Structure Before Analysis
Research Question
"Can another researcher — or you, six months from now — reproduce every number in your paper from your project files?"
Intuition
The replication crisis in social science is real. A 2016 Nature survey found that 70% of researchers had tried and failed to reproduce another scientist's results. In economics, replicators often find that published results shrink by 30-50% or disappear entirely when re-analysed. The problem is rarely fraud — it is disorganisation. Files scattered across folders, manual steps not documented, "final" versions that aren't final.
The solution is not more sophisticated software. It is a disciplined project structure that separates raw data from processed data, code from outputs, and ensures that every result in your paper can be traced back to a specific script.
The Standard Project Template
research-project/
├── README.md # Project description, data sources, instructions
├── requirements.txt # Python dependencies with versions
├── data/
│ ├── raw/ # Original data — NEVER modify
│ └── processed/ # Cleaned data, derived variables
├── code/
│ ├── 01_clean_data.py # Data cleaning script
│ ├── 02_explore.py # Descriptive statistics, EDA
│ ├── 03_models.py # Main estimation
│ ├── 04_tables.py # Generate tables
│ └── 05_figures.py # Generate figures
├── outputs/
│ ├── tables/ # .tex, .csv, .html tables
│ └── figures/ # .png, .pdf, .svg figures
├── notebooks/
│ └── exploratory.ipynb # Exploratory analysis (optional)
└── paper/
├── manuscript.tex # LaTeX manuscript
└── references.bib # Bibliography
01_, 02_, 03_). This communicates the workflow to anyone opening the project. A single master script (00_run_all.py) should execute the entire pipeline from raw data to final outputs. If you can type python 00_run_all.py and walk away, your project is reproducible.Python Implementation — A Master Runner
# 00_run_all.py — Execute entire project with one command
import subprocess
import sys
scripts = [
'01_clean_data.py',
'02_explore.py',
'03_models.py',
'04_tables.py',
'05_figures.py'
]
for script in scripts:
print(f"\n{'='*60}")
print(f"Running {script}...")
print('='*60)
result = subprocess.run([sys.executable, f'code/{script}'],
capture_output=True, text=True)
if result.returncode != 0:
print(f"ERROR in {script}:")
print(result.stderr)
sys.exit(1)
print(result.stdout)
print("\n✓ All scripts completed successfully. Outputs in /outputs/")
2. Publication-Quality Tables — Beyond Copy-Paste
Research Question
"How do I create a regression table suitable for journal submission — with proper formatting, stars, and multiple model columns — directly from my Python output?"
Intuition
Manual tables are the enemy of reproducibility. The moment you copy a coefficient from a Python console into Excel, you have broken the link between your code and your paper. If you re-estimate the model (new data, different controls, corrected specification), you must manually update the table — and you will make errors.
The solution: generate tables programmatically. Python can produce LaTeX (for academic papers), HTML (for websites), and formatted text output directly from model results. The summary_col function in statsmodels creates multi-model comparison tables; for more control, extract coefficients and standard errors yourself and format them.
Python Implementation
import pandas as pd; import numpy as np
import statsmodels.api as sm; import statsmodels.formula.api as smf
from statsmodels.iolib.summary2 import summary_col
import warnings; warnings.filterwarnings('ignore')
df = sm.datasets.get_rdataset("mroz", "wooldridge").data.dropna(subset=['lwage'])
# ── Estimate four model variants ──────────────────────────────────
m1 = smf.ols("lwage ~ educ", data=df).fit()
m2 = smf.ols("lwage ~ educ + exper + expersq", data=df).fit()
m3 = smf.ols("lwage ~ educ + exper + expersq + age + kidslt6 + kidsge6", data=df).fit()
m4 = smf.ols("lwage ~ educ + exper + expersq + age + kidslt6 + kidsge6 + nwifeinc", data=df).fit()
# ── Method 1: summary_col (quick, good for exploration) ───────────
table = summary_col(
[m1, m2, m3, m4],
stars=True,
model_names=['(1) Basic', '(2) +Experience', '(3) +Demographics', '(4) +Husband Inc'],
info_dict={'N': lambda x: f"{int(x.nobs)}", 'R²': lambda x: f"{x.rsquared:.3f}"}
)
print(table)
# ── Method 2: Manual table (full control for journal submission) ──
def build_reg_table(models, var_list, model_names):
"""Build a formatted regression table string."""
header = f"{'':<18}" + ''.join(f"{name:>14}" for name in model_names)
lines = [header, '-' * len(header)]
for var in var_list:
row = f"{var:<18}"
for m in models:
if var in m.params.index:
coef = m.params[var]; se = m.bse[var]; p = m.pvalues[var]
stars = '***' if p < 0.01 else '**' if p < 0.05 else '*' if p < 0.10 else ''
row += f" {coef:>8.4f}{stars}"
row += f"\n{'':<18} ({se:>8.4f})"
else:
row += f" {'':>14}"
lines.append(row)
# Model stats
n_obs = f"{'Observations':<18}" + ''.join(f" {int(m.nobs):>14}" for m in models)
r_sq = f"{'R-squared':<18}" + ''.join(f" {m.rsquared:>14.3f}" for m in models)
lines.extend([n_obs, r_sq, '', 'Robust standard errors in parentheses', '*** p<0.01, ** p<0.05, * p<0.10'])
return '\n'.join(lines)
var_list = ['educ', 'exper', 'expersq', 'age', 'kidslt6', 'kidsge6', 'nwifeinc']
print("\n─── Publication-Ready Regression Table ───")
print(build_reg_table([m1, m2, m3, m4], var_list,
['(1)', '(2)', '(3)', '(4)']))
─── Publication-Ready Regression Table ───
(1) (2) (3) (4)
----------------------------------------------------------------------
educ 0.1086*** 0.1079*** 0.0988*** 0.0952***
(0.0144) (0.0143) (0.0146) (0.0148)
exper 0.0406*** 0.0384*** 0.0404***
(0.0133) (0.0134) (0.0133)
expersq -0.0007* -0.0006 -0.0007*
(0.0004) (0.0004) (0.0004)
age -0.0034 -0.0038
(0.0061) (0.0062)
kidslt6 -0.0966 -0.0407
(0.0884) (0.0952)
kidsge6 -0.0092 -0.0095
(0.0329) (0.0328)
nwifeinc 0.0026
(0.0033)
Observations 428 428 428 428
R-squared 0.117 0.145 0.157 0.167
Interpretation
The education coefficient is stable across specifications (0.095–0.109) — a strong signal of robustness. Progressive addition of controls increases R² from 0.117 to 0.167. The table is ready for a journal: coefficients with significance stars, standard errors in parentheses, observation counts, and fit statistics. No Excel, no manual entry, no copy-paste errors.
3. Publication-Quality Figures — Every Journal's Requirements
Research Question
"How do I create a figure that meets journal standards: proper resolution, readable fonts, consistent styling, and multi-panel layout?"
Intuition
Journals have specific figure requirements: 300+ DPI for raster images, vector formats preferred (PDF/SVG), readable font sizes (≥8pt at final size), consistent colour palettes that work in greyscale, and proper labelling. Most default matplotlib output fails these standards — default fonts are too small, default DPI is 100, and colours may be indistinguishable when printed.
Journal Figure Checklist
| Requirement | Matplotlib Solution |
|---|---|
| Minimum 300 DPI | plt.savefig('fig.png', dpi=300) |
| Vector format preferred | plt.savefig('fig.pdf') — infinitely scalable |
| Readable fonts (≥8pt) | plt.rcParams['font.size'] = 10 |
| Consistent styling | plt.style.use('seaborn-v0_8-darkgrid') |
| Greyscale-compatible | Use line styles (—, - -, ··) in addition to colour |
| Multi-panel layout | fig, axes = plt.subplots(2, 2, figsize=(12, 10)) |
| No chartjunk | Remove unnecessary borders, grids, and decorations |
Python Implementation — Complete Publication Figure
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams.update({
'font.size': 10, 'axes.titlesize': 12, 'axes.labelsize': 11,
'legend.fontsize': 9, 'figure.dpi': 150, 'savefig.dpi': 300,
'savefig.bbox': 'tight', 'font.family': 'serif'
})
# ── Create a four-panel diagnostic figure ─────────────────────────
df = sm.datasets.get_rdataset("mroz", "wooldridge").data.dropna(subset=['lwage'])
m = smf.ols("lwage ~ educ + exper + expersq", data=df).fit()
resid = m.resid; fitted = m.fittedvalues
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Panel (a): Q-Q Plot
from scipy import stats
sm.qqplot(resid, stats.norm, fit=True, line='45', ax=axes[0,0],
markerfacecolor='steelblue', markeredgecolor='none', alpha=0.5)
axes[0,0].set_title('(a) Q-Q Plot of Residuals', fontweight='bold')
# Panel (b): Residuals vs Fitted
axes[0,1].scatter(fitted, resid, alpha=0.4, edgecolors='none', color='#2c7fb8')
axes[0,1].axhline(y=0, color='#d7191c', linestyle='--', linewidth=1)
axes[0,1].set_xlabel('Fitted Values'); axes[0,1].set_ylabel('Residuals')
axes[0,1].set_title('(b) Residuals vs Fitted', fontweight='bold')
# Panel (c): Coefficient Plot with CIs
coefs = m.params[['educ', 'exper', 'expersq']]; ses = m.bse[['educ', 'exper', 'expersq']]
y_pos = range(len(coefs))
axes[1,0].errorbar(coefs.values, y_pos, xerr=1.96*ses.values, fmt='o',
capsize=5, markersize=8, color='#2c7fb8', linewidth=2)
axes[1,0].axvline(x=0, color='gray', linestyle=':', alpha=0.5)
axes[1,0].set_yticks(y_pos); axes[1,0].set_yticklabels(coefs.index)
axes[1,0].set_xlabel('Coefficient Estimate'); axes[1,0].set_title('(c) Coefficient Plot (95% CI)', fontweight='bold')
# Panel (d): Density of Residuals vs Normal
from scipy.stats import gaussian_kde
x_range = np.linspace(resid.min(), resid.max(), 200)
axes[1,1].hist(resid, bins=30, density=True, alpha=0.5, color='steelblue', edgecolor='white')
kde = gaussian_kde(resid)
axes[1,1].plot(x_range, kde(x_range), 'b-', linewidth=2, label='Residuals')
axes[1,1].plot(x_range, stats.norm.pdf(x_range, resid.mean(), resid.std()),
'r--', linewidth=2, label='Normal')
axes[1,1].set_xlabel('Residuals'); axes[1,1].set_title('(d) Residual Density vs Normal', fontweight='bold')
axes[1,1].legend(fontsize=8)
plt.suptitle('Figure 1: Diagnostic Plots for Mincer Wage Equation', fontweight='bold', fontsize=14, y=1.01)
plt.tight_layout()
plt.savefig('../../assets/images/m10-diagnostic-figure.png', dpi=300)
plt.savefig('../../assets/images/m10-diagnostic-figure.pdf') # Vector format for journal
plt.show()
print("Figure saved at 300 DPI (PNG) and as vector PDF for journal submission.")
Figure saved at 300 DPI (PNG) and as vector PDF for journal submission. Journal submission tip: Submit PDF for the manuscript (scalable, small file). Provide high-res PNG/TIFF only if the journal specifically requires raster format.
4. Dynamic Documents — When Your Paper Updates Itself
Research Question
"If I update my dataset or change a specification, can my tables, figures, and manuscript update automatically?"
Intuition
A dynamic document embeds code directly in the manuscript. When you recompile the document, the code runs, producing updated tables and figures that are inserted automatically. No more "Did I remember to update Table 3 after I added that control variable?" The document and the analysis are one.
Three approaches, in order of complexity:
- Jupyter Notebook → LaTeX: Write your analysis in a notebook, export to LaTeX via nbconvert. Good for papers with a single main analysis.
- Quarto: The modern successor to R Markdown. Write in .qmd files mixing markdown and Python; render to HTML, PDF, or Word. Best choice for new projects.
- Papermill: Parameterise notebooks — run the same analysis with different parameters (regions, time periods, specifications) without duplicating code.
Python Implementation — Quarto Example
# Install Quarto (one-time)
# Download from https://quarto.org/docs/get-started/
# Create a new Quarto document
# quarto create project my-paper
# Render to PDF
quarto render my-paper.qmd --to pdf
# Render to HTML
quarto render my-paper.qmd --to html
# Example: my-paper.qmd contents
# ---
# title: "Returns to Education — Replication Package"
# author: "Your Name"
# format: pdf
# jupyter: python3
# ---
#
# ## Results
#
# ```{python}
# import statsmodels.formula.api as smf
# import statsmodels.api as sm
# df = sm.datasets.get_rdataset("mroz", "wooldridge").data.dropna(subset=['lwage'])
# model = smf.ols("lwage ~ educ + exper + expersq", data=df).fit()
# print(model.summary())
# ```
#
# The return to education is `{python} round(model.params['educ'], 3)` (p < 0.001).
`{python} round(model.params['educ'], 3)` is Quarto's killer feature — it inserts the actual value of the coefficient into the prose. When you update the model, the number in the sentence updates automatically. Never manually type a coefficient into a manuscript again.5. Version Control for Research — Git for Economists
Research Question
"How do I track changes to my analysis, collaborate with co-authors, and ensure I can always recover the exact version that produced submitted results?"
Intuition
Empirical research involves iteration: you try a specification, it doesn't work, you try another. Three weeks later, a reviewer asks about Specification A that you briefly tried and discarded. Without version control, that specification is lost — or worse, you can't remember which version of the code produced the results in the submitted manuscript.
Git solves all of this. Every change is recorded with a timestamp, a description, and the ability to revert. Tags mark the exact code version used for submission. Branches allow you to explore alternative specifications without breaking the main analysis.
Essential Git Workflow
# Initialise a Git repository in your project folder
git init
# Create a .gitignore — files Git should NOT track
echo "__pycache__/" >> .gitignore
echo "*.pyc" >> .gitignore
echo "data/raw/" >> .gitignore # Raw data may be too large or proprietary
echo "outputs/" >> .gitignore # Generated outputs — can be recreated
# First commit: project structure
git add README.md requirements.txt .gitignore code/
git commit -m "Initial project structure"
# After completing analysis: commit
git add code/03_models.py outputs/tables/
git commit -m "Baseline wage regressions with robust SEs"
# Tag the version used for submission
git tag -a "v1.0-submission" -m "Version submitted to journal"
# Explore an alternative specification on a branch
git checkout -b alt-iv-spec
# ... make changes ...
git commit -m "IV specification using only father's education"
# Return to main analysis
git checkout main
# View history
git log --oneline --graph --all
.gitignore to exclude raw data and outputs. Store large datasets separately (university drive, Dropbox, data repository) and document their location in README.md. Git is for code — keep it fast.6. The Journal Submission Checklist — What Reviewers Check
Intuition
Reviewers evaluate your empirical work on credibility, not just significance. A coefficient with p=0.001 from a model that fails diagnostics is less convincing than p=0.04 from a well-diagnosed model. Here is what reviewers look for — and what your paper should address:
| # | Reviewer Question | Your Response | Module |
|---|---|---|---|
| 1 | Are the data described clearly? | Source, coverage, summary statistics table, missing data handling | M9 |
| 2 | Are diagnostic tests reported? | Normality, heteroscedasticity, linearity, multicollinearity, autocorrelation as applicable | M1 |
| 3 | Is endogeneity addressed? | IV strategy with first-stage results, weak instrument tests, overidentification if applicable | M2 |
| 4 | Are standard errors appropriate? | Robust/clustered SEs when diagnostics indicate, justification stated | M1 |
| 5 | Is the functional form justified? | RESET test, CPR plots, theoretical basis for logs/polynomials/interactions | M1, M3 |
| 6 | Are results robust? | Multiple specifications, alternative estimators, sensitivity to controls | M3, M8 |
| 7 | Is the model appropriate for the data structure? | Panel methods for panel data, time series methods for time series, etc. | M5–M7 |
| 8 | Are coefficients economically meaningful? | Interpretation in substantive terms, not just "statistically significant" | All |
| 9 | Can results be reproduced? | Code and data availability, README with instructions, project structure | M10 |
| 10 | Are limitations acknowledged? | Instrument validity caveats, external validity, measurement concerns | M2, M7 |
Final Capstone: Build Your Reproducible Research Package
Research Question
Take the wage analysis from Module 9 and transform it into a complete, reproducible research package ready for journal submission. Create: (1) a structured project directory, (2) a publication-ready regression table with 4 model specifications, (3) a four-panel diagnostic figure at 300 DPI, (4) a README with reproduction instructions, and (5) a Git repository with meaningful commit history.
Steps
- Create the project structure from Section 1 — folders for data, code, outputs, paper
- Split your Module 9 analysis into numbered scripts (01_ to 05_)
- Generate a formatted regression table with 4 progressive model specifications
- Create a four-panel diagnostic figure at 300 DPI and PDF
- Write a requirements.txt with pinned versions (
pip freeze > requirements.txt) - Write a README.md explaining the project structure and how to reproduce results
- Initialise Git, make meaningful commits, tag v1.0
- Test: clone your repo to a new folder and run
python 00_run_all.py
View Solution / Walkthrough
Complete README.md Template
# Returns to Education — Replication Package
This repository contains the complete replication package for "Returns
to Education: Evidence from the Mroz Married Women's Labour Supply Data."
## Data
- Source: Mroz (1987), available via statsmodels
- 428 working married women from the 1975 PSID
- Key variables: lwage, educ, exper, age, kidslt6, kidsge6, nwifeinc
## Requirements
Python 3.10+ with dependencies in requirements.txt:
```
pip install -r requirements.txt
```
## Reproduction
Run the master script to execute the entire pipeline:
```
python code/00_run_all.py
```
Or run individual scripts in order:
1. `01_clean_data.py` — Load and clean the Mroz dataset
2. `02_explore.py` — Descriptive statistics and exploratory plots
3. `03_models.py` — Estimate OLS specifications (1)—(4)
4. `04_tables.py` — Generate publication-ready regression table
5. `05_figures.py` — Generate four-panel diagnostic figure
Outputs appear in `outputs/tables/` and `outputs/figures/`.
## Repository Structure
- `data/raw/` — Original data (not tracked in Git)
- `data/processed/` — Cleaned data with derived variables
- `code/` — All analysis scripts
- `outputs/` — Generated tables and figures
- `paper/` — Manuscript files
## Contact
[Your Name], [Your Institution], [Your Email]
Key Takeaways
Reproducibility starts before analysis. A clean project structure with numbered scripts, separated data/code/outputs, and a master runner is worth more than any single statistical technique — it is the foundation of credible research.
Never type a coefficient into a manuscript manually. Generate tables programmatically from model output. The moment you copy-paste a number, the link between your analysis and your paper is broken — and errors are inevitable.
Publication figures need 300+ DPI, readable fonts, greyscale compatibility, and vector format options. Default matplotlib output will be rejected by most journals. Invest one hour in a custom style — you'll use it for every paper.
Git is not just for software developers. It answers the question every researcher faces: "Which version of my code produced those results?" Tags link submitted manuscripts to exact code versions — the gold standard for reproducibility.
You have now completed the full FDP — from diagnostic testing (M1) to reproducible research (M10). The techniques you have learned constitute a complete toolkit for empirical social science. The rest is practice: apply them to your data, your questions, your papers.
Test Your Understanding
20 questions — covers reproducible workflows, publication tables/figures, dynamic documents, Git, and journal submission.
What is the purpose of a 00_run_all.py script in a reproducible project?
Why should raw data NEVER be modified in a reproducible workflow?
The minimum DPI for a journal-quality figure is typically:
What is the main advantage of saving figures as PDF (vector format) instead of PNG?
The summary_col function from statsmodels produces:
What should be included in .gitignore for a research project?
What is the purpose of a Git tag in a research project?
Quarto allows inline code like `{python} round(model.params['educ'], 3)`. This means:
The requirements.txt file in a research project should contain:
What is the main risk of manually copy-pasting coefficients from Python output into a Word table?
Why should you use both colour AND line styles (—, - -, ··) in publication figures?
The four-panel diagnostic figure in this module included which plots?
What does a Git branch allow you to do in a research context?
In the regression table, the education coefficient remained stable (0.095–0.109) across 4 specifications. This demonstrates:
The AEA's Data Editor checks that submitted code:
What is the correct order of scripts in a numbered analysis workflow?
The journal submission checklist in this module asks reviewers to check:
What command creates a requirements.txt with exact version numbers of installed packages?
The 2016 Nature survey found that approximately what percentage of researchers had tried and failed to reproduce another scientist's results?
You have completed all 10 modules of the FDP. Which of the following best describes the complete empirical workflow you have learned?