/python-panel-data¶
Panel and causal-inference code in Python via pyfixest, linearmodels.PanelOLS, and statsmodels, with correct fixed effects, two-way clustering, IV2SLS, event studies through sunab, weak-IV diagnostics, and the wildboottest package below 30 clusters. Unusually candid about the ecosystem gap: for Callaway-Sant'Anna, Borusyak-Jaravel-Spiess, or de Chaisemartin-D'Haultfoeuille it routes the user to the R or Stata skill (or rpy2) rather than improvising, and documents the cross-language jump in the decisions log.
analysisCC0-1.0 per the repo LICENSE file and README ("CC0 1.0 — public domain"); GitHub's API reports the licence as NOASSERTION / "Other" — record bothdata-analysis · code-generationPython Panel Data¶
Generate rigorous panel and causal-inference code in Python. The modern Python ecosystem is pyfixest for fixest-style speed and syntax, linearmodels.PanelOLS for textbook panel models, and statsmodels for general purpose. Where Python's modern-DiD support is incomplete, point the user to R.
Operating Principles¶
- Identification before code. State the estimand, the source of identifying variation, and the threats to identification before writing any model.
- Use
pyfixestfirst when speed and modern syntax matter. Fall back tolinearmodels.PanelOLSfor textbook panel models,statsmodelsfor general regression and IV with simple specs. - Be honest about ecosystem gaps. Python's heterogeneity-robust DiD support is weaker than R or Stata. For Callaway-Sant'Anna, BJS, or de Chaisemartin-D'Haultfoeuille, use the corresponding R or Stata skill, or call R via
rpy2. - Reproducibility discipline. Pin versions in
pyproject.tomlorrequirements.txt. Usepathlibfor paths,pd.read_*withdtype=arguments, and seed every random call. - Tables are produced, never copy-pasted. Use
pyfixest.etable,linearmodelsLaTeX export, orstargazerforstatsmodels.
Decision Policy¶
This skill follows the repo-wide Agent Policy.
ASK before proceeding (blocking):
- Estimand and identification strategy.
- Library choice when both apply:
pyfixestfor speed and modern syntax vslinearmodels.PanelOLSfor textbook panel API. - Cluster level for inference.
- Whether to switch language (R / Stata) for parts where Python's ecosystem is weaker (Callaway-Sant'Anna, BJS, AR weak-IV CIs).
DEFAULT + flag (use this default; tell the user how to override):
pyfixest.feolsfor panel TWFE, event studies (sunab), and IV with FE.linearmodels.PanelOLSwhen the user wants explicitEntityEffects/TimeEffects/ RandomEffects / FirstDifferenceOLS.statsmodelsonly for cross-section logit/probit/GLM/simple OLS.wildboottestpackage when G < 30; otherwisevcov = {"CRV1": "unit"}.- Outputs as parquet in
data/processed/; tables topaper/tabs/viapyfixest.etable.
DOCUMENT and proceed (write into the decisions log):
- Library choice and the reason if non-default.
- Panel structure (balanced / unbalanced / rotating) inferred from the data.
- Cross-language jumps (e.g. "Used R
didpackage via subprocess for Callaway-Sant'Anna because no mature Python equivalent").
PROCEED items: pyproject.toml + lockfile, Path(__file__).resolve().parents[N], numpy.random.default_rng(20240101), assert df.duplicated([keys]).sum() == 0 after every load.
Pre-flight Checklist¶
Before writing code, confirm with the user — and write the answers as a header docstring:
- Estimand. ATT, LATE, ITT, descriptive?
- Unit of observation and panel keys. Assert with
df.duplicated(["unit", "time"]).sum() == 0. - Treatment timing. Single shock, simultaneous, staggered, or reversible?
- Identifying variation. Which comparison drives the coefficient?
- Clustering level. Where is treatment assigned, or where does within-group correlation live?
- Sample. Document each filter with sample sizes.
- Output target. Working paper, slides, journal submission, or exploratory.
Library Choice¶
| Need | Library | Notes |
|---|---|---|
| Panel TWFE, event studies, IV with FE | pyfixest |
Fastest; mirrors R fixest; feols, fepois, iplot |
| Textbook panel: PanelOLS, RandomEffects, FirstDifferenceOLS, BetweenOLS | linearmodels |
Rich panel API; explicit EntityEffects, TimeEffects |
| Cross-section IV2SLS, GMM | linearmodels |
IV2SLS, IVGMM |
| Logit/probit, GLM, simple OLS | statsmodels |
Standard, well-documented |
| Tables (multiple models) | pyfixest.etable, stargazer, linearmodels compare() |
Pick one and stick with it |
Install:
Estimator Decision Tree¶
Difference-in-Differences¶
Single shock, two groups, uniform timing
└── pyfixest.feols("y ~ treat_post | unit + time", data=df,
vcov={"CRV1": "unit"})
Staggered adoption (the realistic case)
├── Sun-Abraham via pyfixest:
│ pyfixest.feols("y ~ sunab(cohort, time) | unit + time",
│ data=df, vcov={"CRV1": "unit"})
├── BJS imputation: not yet a polished package; implement manually
│ or use the `differences` package (limited).
└── Callaway-Sant'Anna: use R `did` via rpy2, or switch to the
R or Stata skill for that estimator.
Continuous or non-binary treatment
└── No mature Python implementation. Use Stata
`did_multiplegt_dyn` or R `DIDmultiplegt`.
Instrumental Variables¶
Cross-section 2SLS:
from linearmodels import IV2SLS
IV2SLS.from_formula("y ~ 1 + x_exog + [endog ~ z]", data=df).fit(
cov_type="clustered", clusters=df.cluster
)
Panel IV with high-dim FE:
pyfixest.feols("y ~ x_exog | unit + time | endog ~ z",
data=df, vcov={"CRV1": "unit"})
Weak-IV diagnostics:
- linearmodels reports Kleibergen-Paap rk Wald F.
- For Olea-Pflueger effective F or AR sets, use R `ivDiag`,
Stata `weakivtest`, or implement AR by hand.
Effective F:
- F >= 100 → conventional 2SLS is fine.
- 10 <= F < 100 → use AR or tF; do not rely on conventional t.
- F < 10 → instrument is weak; do not interpret 2SLS causally.
Inference¶
Number of clusters G:
G >= 50 → cluster-robust SE is fine
30 <= G < 50 → consider HC3-style small-sample correction
G < 30 → use wild cluster bootstrap (`pyfixest` supports
`wildboottest` via the `wildboottest` package)
or call Stata `boottest` / R `fwildclusterboot`.
Multi-way clustering:
pyfixest: vcov={"CRV1": ["unit", "year"]}
linearmodels: cov_type="clustered", clusters=df[["unit", "year"]]
Output Skeleton¶
Every analysis script should follow:
"""
analysis_main.py
Project: ProjectABC
Purpose: Main DiD specification on outcome Y
Inputs : data/processed/analysis.parquet
Outputs: results/tables/table_did_main.tex
results/figures/event_study.pdf
Estimand: ATT of program on Y
Cluster : at the unit level (treatment assignment)
"""
from pathlib import Path
import pandas as pd
import pyfixest as pf
PROJECT = Path(__file__).resolve().parents[2]
DATA_FINAL = PROJECT / "data" / "processed"
RESULTS = PROJECT / "results"
RESULTS.joinpath("tables").mkdir(parents=True, exist_ok=True)
RESULTS.joinpath("figures").mkdir(parents=True, exist_ok=True)
## 1. Load and assert structure
df = pd.read_parquet(DATA_FINAL / "analysis.parquet")
assert df.duplicated(["unit_id", "year"]).sum() == 0
## 2. Sample construction
df = df.dropna(subset=["outcome"]) # log this filter
## 3. Main and robustness specs
m_main = pf.feols("outcome ~ treat_post | unit_id + year",
data=df, vcov={"CRV1": "unit_id"})
m_two = pf.feols("outcome ~ treat_post | unit_id + year",
data=df, vcov={"CRV1": ["unit_id", "year"]})
## 4. Export
pf.etable([m_main, m_two],
type="tex",
file_name=str(RESULTS / "tables" / "table_did_main.tex"))
Common Pitfalls¶
- Plain TWFE on staggered timing in
pyfixestis just as biased as infixest. Usesunabor switch language. - Forgetting to set the panel index in
linearmodels. Alwaysdf.set_index(["unit", "time"])beforePanelOLS.from_formula. - Cluster-robust SE with very few clusters — undercoverage; use wild bootstrap.
- Mixing
pyfixestandlinearmodelsresults in the same table — they sometimes treat absorbed FE differently. Stay within one framework per table. - Hardcoded paths — use
pathlib.Path(__file__).resolve().parents[N]. df["x"] = ...after a chained operation losing.copy()— usedf = df.assign(...)or explicit.copy().
Additional Resources¶
reference.md— extended code patterns for each estimator family.examples/— runnable scripts:examples/twfe_pyfixest.py— TWFE with pyfixestexamples/twfe_linearmodels.py— TWFE with linearmodels.PanelOLSexamples/event_study_sun_abraham.py— Sun-Abraham via pyfixestexamples/iv_panel_pyfixest.py— panel IV with diagnosticsexamples/clustered_inference.py— multi-way clustering and CR2examples/balance_table.py— pandas + scipy balance tables
Requirements¶
- Python >= 3.10
- Core:
pandas,pyarrow,numpy,scipy - Econometrics:
pyfixest,linearmodels,statsmodels - Tables and plots:
stargazer,matplotlib
References¶
- Berge (2018),
fixest: Fast Fixed Effects Estimation. Original reference for thefixest/pyfixestfamily. - Sun & Abraham (2021). Estimating Dynamic Treatment Effects in Event Studies. JoE.
- Borusyak, Jaravel & Spiess (2024). Revisiting Event Study Designs. ReStud.
- Olea & Pflueger (2013). A Robust Test for Weak Instruments. JBES.
- Wooldridge (2010). Econometric Analysis of Cross Section and Panel Data.
- DIME Analytics, Reproducible Research.
- DIME Analytics, Data Analysis.
- pyfixest documentation: https://py-econometrics.github.io/pyfixest/
- linearmodels documentation: https://bashtage.github.io/linearmodels/