
Stata Python Translation
- 1 installs
- 3.2k repo stars
- Updated August 4, 2026
- brycewang-stanford/awesome-agent-skills-for-empirical-research
stata-python-translation is a Claude skill that maps Stata commands and packages to Python equivalents (polars, pyfixest, statsmodels) for cross-language data analysis.
About
A translation reference that maps Stata commands, idioms, and workflows to their Python equivalents across data management, regression, causal inference, surveys, and visualization. Stata-background users use it to audit or learn Python analysis code, and agents use it to annotate Python output with Stata-equivalent comments. It also documents paradigm gaps where the two ecosystems diverge.
- Maps Stata commands (reghdfe, xtreg, ivregress, margins, svy:) to Python equivalents
- Covers polars, pyfixest, statsmodels, linearmodels, marginaleffects, and plotnine
- Routing hub with per-topic reference files and Stata-user gotchas in Python
Stata Python Translation by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,803 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
stata-python-translation capabilities & compatibility
- Capabilities
- regression modeling · causal inference · data cleaning
- Use cases
- data analysis · translation
What stata-python-translation says it does
Stata-to-Python translation for data analysis. Maps Stata commands (reghdfe, xtreg, ivregress, margins, esttab, svy:) to Python (polars, pyfixest, statsmodels, svy).
Cross-language translation reference for researchers moving between the Stata and Python data analysis ecosystems.
This skill is a **routing hub** -- it provides overview tables, decision trees, and directs readers to the detailed reference files listed below.
npx skills add https://github.com/brycewang-stanford/awesome-agent-skills-for-empirical-research --skill stata-python-translationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 3.2k |
| Last updated | August 4, 2026 |
| Repository | brycewang-stanford/awesome-agent-skills-for-empirical-research ↗ |
What it does
Map Stata commands and packages to their Python equivalents so Stata-background users can read or write Python analysis code.
Who is it for?
Stata users auditing or learning Python data-analysis code.
Skip if: Writing Stata code from scratch or general Python development unrelated to analysis.
When should I use this skill?
The user has a Stata background or requests Stata-equivalent comments for Python analysis code.
What you get
Python code understood or annotated with its Stata-equivalent operations.
- Stata-to-Python command mappings
- Stata-equivalent code annotations
By the numbers
- 10 topic-focused reference files
Files
Stata-to-Python Translation Skill
Stata-to-Python translation reference for quantitative social science data analysis. Maps Stata commands and packages (reghdfe, xtreg, ivregress, margins, esttab, svy:, graph twoway) to DAAF Python equivalents (polars, pyfixest, statsmodels, linearmodels, marginaleffects, svy, plotnine). Use when user mentions Stata background, requests Stata-equivalent code comments, needs to understand Python analysis code from a Stata perspective, or wants to translate Stata data analysis concepts to Python. Covers paradigm differences, command-by-command operation translations, regression modeling, causal inference, visualization, and workflow adaptation.
Cross-language translation reference for researchers moving between the Stata and Python data analysis ecosystems. This skill maps Stata commands, idioms, and workflows to their DAAF Python equivalents so that Stata-background users can audit, understand, and learn from DAAF-produced code, and so that code-producing agents can annotate their output with Stata equivalents when directed.
This skill is a routing hub -- it provides overview tables, decision trees, and directs readers to the detailed reference files listed below. The reference files contain the exhaustive command-by-command mappings, code examples, and edge-case documentation.
What This Skill Does
- Maps the Stata command universe to DAAF's Python stack across data management, regression modeling, causal inference, surveys, visualization, and workflow tooling
- Provides a structured annotation protocol for agents to add inline Stata-equivalent comments to Python code
- Identifies paradigm gaps where Stata and Python diverge fundamentally, so users know where to expect friction
Use cases:
1. Stata user auditing DAAF Python code and needing to understand what operations are being performed 2. Agent annotating code with Stata-equivalent comments for a Stata-background researcher 3. Stata user learning Python for data analysis and needing a conceptual bridge 4. Translating a specific Stata command or do-file idiom to its Python equivalent 5. Understanding where Stata commands have no direct Python equivalent (and what the workaround is)
How to Use This Skill
Reference File Structure
Each topic in ./references/ contains focused documentation:
| File | Purpose | When to Read |
|---|---|---|
paradigm-differences.md | Core language and paradigm differences (single-dataset model, missing values, value labels, macros, by:/_n/_N) | Encountering fundamental Stata-vs-Python confusion |
data-management.md | gen/replace/keep/drop/sort/merge/append/reshape/collapse/egen to polars | Reading or writing data manipulation code |
strings-dates-labels.md | String functions, date epoch, value labels, encode/decode | Working with string, date, or categorical columns |
regression-modeling.md | regress/areg/reghdfe/xtreg/ivregress/logit/probit/margins/test/esttab to pyfixest/statsmodels/linearmodels | Reading or writing regression code |
causal-inference.md | DiD/RDD/IV/event studies/synthetic control/matching | Working with causal inference methods |
visualization.md | graph twoway/bar/box/histogram to plotnine/plotly | Reading or writing visualization code |
survey-spatial-ml.md | svy: commands, spatial data, machine learning | Working with surveys, spatial data, or ML |
workflow-environment.md | Do-files/log/macros/ado/ssc to Python/DAAF execution model | Adapting to DAAF's execution model |
external-resources.md | Curated guides and tutorials with provenance | Seeking additional learning materials |
gotchas.md | Common Stata-user mistakes in Python | Debugging or reviewing code from Stata perspective |
Reading Order
1. Stata user auditing DAAF code: paradigm-differences.md then the relevant domain file (e.g., data-management.md for wrangling, regression-modeling.md for models) then gotchas.md 2. Agent annotating code with Stata equivalents: Agent Code Annotation Protocol section below, then the relevant domain file for the code being annotated 3. Learning Python from Stata background: paradigm-differences.md then data-management.md then workflow-environment.md then external-resources.md 4. Looking up a specific Stata command translation: Quick Decision Trees below, then the relevant reference file
Quick Decision Trees
"How do I do X from Stata in Python?"
What kind of Stata command?
+- Data management (gen, replace, keep, drop, merge, reshape, collapse)
| +-- ./references/data-management.md
+- Group operations (by:, bysort, egen)
| +-- ./references/data-management.md
+- Regression / estimation (regress, areg, reghdfe, xtreg, logit, probit)
| +-- ./references/regression-modeling.md
+- Post-estimation (margins, test, lincom, nlcom, predict, esttab)
| +-- ./references/regression-modeling.md
+- Causal inference (diff, did_multiplegt, rdrobust, teffects, synth)
| +-- ./references/causal-inference.md
+- Surveys (svyset, svy:)
| +-- ./references/survey-spatial-ml.md
+- Plotting (graph twoway, histogram, graph bar)
| +-- ./references/visualization.md
+- String/date manipulation (substr, strpos, date, mdy)
| +-- ./references/strings-dates-labels.md
+- Value labels (label define, encode, decode)
| +-- ./references/strings-dates-labels.md
+-- Programming (local, global, foreach, forvalues, tempvar, preserve)
+-- ./references/workflow-environment.md"Why does this Python code look different from Stata?"
What looks unfamiliar?
+- Expression syntax (pl.col().method().alias())
| +-- ./references/paradigm-differences.md
+- Missing values (None vs NaN vs null vs .)
| +-- ./references/paradigm-differences.md
+- No single "dataset" -- multiple DataFrames everywhere
| +-- ./references/paradigm-differences.md
+- Value labels missing from output
| +-- ./references/paradigm-differences.md
+- Regression output structure (model objects vs e()/r())
| +-- ./references/regression-modeling.md
+- No `by:` prefix -- .over() and .group_by() instead
| +-- ./references/paradigm-differences.md
+-- Import statements and namespacing
+-- ./references/gotchas.md"I want to translate a Stata do-file to Python"
What does the do-file do?
+- Loads and wrangles data (use, gen, replace, keep, merge, collapse)
| +-- ./references/data-management.md
+- Runs regressions (regress, xtreg, reghdfe, ivregress)
| +-- ./references/regression-modeling.md
+- Creates tables (esttab, outreg2, margins)
| +-- ./references/regression-modeling.md
+- Creates plots (graph twoway, histogram)
| +-- ./references/visualization.md
+- Uses survey weights (svyset, svy:)
| +-- ./references/survey-spatial-ml.md
+- Multiple of the above
| +-- Start with ./references/paradigm-differences.md, then each relevant file
+-- Uses macros, loops, or programs
+-- ./references/workflow-environment.md"Something isn't working and I think it's a Stata habit"
What went wrong?
+- Missing values behaving differently than expected
| +-- ./references/paradigm-differences.md
+- gen/replace pattern not translating
| +-- ./references/gotchas.md
+- Merge producing wrong results (no _merge diagnostic)
| +-- ./references/gotchas.md
+- Model output looks different from Stata
| +-- ./references/regression-modeling.md
+- Off-by-one error (0-indexed vs 1-indexed)
| +-- ./references/gotchas.md
+- `by:` / `_n` / `_N` not available
| +-- ./references/paradigm-differences.md
+-- Macro syntax not working
+-- ./references/gotchas.md"Which Python package replaces my Stata command?"
Which Stata command?
+- regress / areg / reghdfe -> pyfixest
| +-- ./references/regression-modeling.md
+- xtreg (fe/re) -> pyfixest (FE) / linearmodels (RE)
| +-- ./references/regression-modeling.md
+- ivregress / ivreg2 / ivreghdfe -> pyfixest / linearmodels
| +-- ./references/regression-modeling.md
+- logit / probit / ologit / mlogit -> statsmodels
| +-- ./references/regression-modeling.md
+- poisson / nbreg / ppmlhdfe -> statsmodels / pyfixest fepois
| +-- ./references/regression-modeling.md
+- margins / marginsplot -> marginaleffects
| +-- ./references/regression-modeling.md
+- esttab / outreg2 -> pf.etable()
| +-- ./references/regression-modeling.md
+- test / lincom / nlcom -> pyfixest .wald_test() / marginaleffects hypotheses()
| +-- ./references/regression-modeling.md
+- gen / replace / drop / keep / sort -> polars
| +-- ./references/data-management.md
+- merge / append -> polars .join() / pl.concat()
| +-- ./references/data-management.md
+- collapse / egen -> polars .group_by().agg() / .over()
| +-- ./references/data-management.md
+- reshape long/wide -> polars .unpivot() / .pivot()
| +-- ./references/data-management.md
+- graph twoway / histogram / graph bar -> plotnine / plotly
| +-- ./references/visualization.md
+- svyset / svy: -> svy package
| +-- ./references/survey-spatial-ml.md
+- rdrobust -> rdrobust (Python, same authors)
| +-- ./references/causal-inference.md
+- binscatter -> binsreg (Python, same authors)
| +-- ./references/causal-inference.md
+- synth -> scpi (Python, same authors)
| +-- ./references/causal-inference.md
+-- ado-file / ssc install -> pip install
+-- ./references/workflow-environment.mdCommand Mapping Overview
| Stata Command(s) | Python Package | Fidelity | Key Difference |
|---|---|---|---|
regress, areg, reghdfe | pyfixest | High | Near-identical formula syntax; `\ |
xtreg, fe | pyfixest | High | No xtset needed; FE specified in formula |
xtreg, re | linearmodels | Medium | Requires pandas MultiIndex for panel structure |
ivregress, ivreg2, ivreghdfe | pyfixest / linearmodels | High | Three-part formula for IV in pyfixest |
logit, probit, ologit, mlogit | statsmodels | Medium | Requires .fit(); C() for categoricals in formulas |
poisson, ppmlhdfe | statsmodels / pyfixest fepois | High | fepois for Poisson with multi-way FE |
margins, marginsplot | marginaleffects | Medium | Separate package; different function names |
esttab, outreg2 | pyfixest etable() | High | pf.etable([m1, m2]) produces publication tables |
gen, replace, drop, keep, sort | polars | Low | Expression system vs imperative commands; immutable DataFrames |
merge, append | polars .join(), pl.concat() | Medium | No automatic _merge diagnostic |
collapse, egen | polars .group_by().agg(), .over() | Medium | Must choose aggregation vs window explicitly |
reshape long/wide | polars .unpivot(), .pivot() | Medium | No in-place reshape; column naming differs |
graph twoway, histogram, graph bar | plotnine / plotly | Low | Declarative grammar of graphics vs imperative graph syntax |
svyset, svy: | svy | Medium | Explicit Design/Sample objects instead of persistent svyset |
rdrobust, rdplot | rdrobust (Python) | Very High | Same authors; nearly identical API |
binscatter, binsreg | binsreg (Python) | Very High | Same authors; nearly identical API |
synth | scpi | High | Same authors; includes prediction intervals |
local, global, foreach, forvalues | Python variables, f-strings, for loops | Low | Fundamentally different paradigm (text substitution vs value binding) |
Fidelity key: Very High = same authors, near-identical API. High = same capability, similar syntax. Medium = same capability, different API patterns. Low = fundamentally different paradigm requiring conceptual remapping.
Library Versions
Translations in this skill reference specific library versions. Python versions are pinned in DAAF's Docker environment (Python 3.12). Stata versions reference the current release as of March 2026. When syntax or behavior has changed between versions, the reference files note the change.
| Python Package | DAAF Version | Stata Equivalent | Stata Version |
|---|---|---|---|
| polars | 1.38.1 | Data management commands (gen, replace, merge, etc.) | Stata 18 |
| pyfixest | 0.40.0 | regress, areg, reghdfe, ivreghdfe, ppmlhdfe, esttab | Stata 18 + reghdfe 6.x |
| statsmodels | 0.14.6 | regress, logit, probit, poisson, nbreg, glm | Stata 18 |
| linearmodels | unpinned | xtreg, sureg, ivregress | Stata 18 |
| plotnine | 0.15.3 | graph twoway, graph bar, graph box, histogram | Stata 18 |
| plotly | 6.5.2 | (no direct Stata equivalent; interactive charts) | N/A |
| svy | 0.13.0 | svyset, svy: prefix commands | Stata 18 |
| marginaleffects | unpinned | margins, marginsplot, lincom, nlcom | Stata 18 |
| rdrobust | unpinned | rdrobust, rdplot, rdbwselect | rdrobust (SSC) |
| binsreg | unpinned | binsreg, binscatter | binsreg (SSC) |
| scpi | unpinned | synth, synth_runner | synth (SSC) |
| scikit-learn | 1.8.0 | (limited; teffects, psmatch2 partially) | Stata 18 |
| marimo | 0.19.11 | (no equivalent; replaces do-file + log workflow) | N/A |
Unpinned packages: linearmodels, marginaleffects, rdrobust, binsreg, and scpi install the latest version at Docker build time. Translations reference their documented API as of March 2026.
Stata version note: Stata 18 is the current release as of March 2026. Most command mappings apply to Stata 15+; version-specific features (frames, hdidregress) are noted in the reference files.
Top 10 Paradigm Differences
These are the friction points Stata users encounter most frequently when reading or writing DAAF Python code. Each is covered in depth in the referenced file.
| # | Friction Point | Stata Way | Python Way | Reference |
|---|---|---|---|---|
| 1 | Single-dataset model | One dataset in memory; commands implicit | Multiple DataFrames as variables; must specify which | paradigm-differences.md |
| 2 | Missing values | . = +infinity; 27 types (.a-.z) | null excluded from comparisons; one null type; NaN distinct | paradigm-differences.md |
| 3 | Value labels | Three-layer system (data, variable, value labels) | No built-in equivalent; dictionaries or Enum types | paradigm-differences.md |
| 4 | by:/_n/_N system | bysort group: gen x = _N | .over("group") (window) vs .group_by().agg() (collapse) | paradigm-differences.md |
| 5 | In-place modification | replace var = expr modifies data directly | df = df.with_columns(...) returns new DataFrame | paradigm-differences.md |
| 6 | Macro system | ` local' ` and $global` text substitution | Python variables + f-strings | paradigm-differences.md |
| 7 | .fit() required | regress y x auto-fits and prints | smf.ols("y ~ x", data=df).fit() -- two steps | regression-modeling.md |
| 8 | Expression system | gen z = x * 2 (bare column names) | df.with_columns((pl.col("x") * 2).alias("z")) | data-management.md |
| 9 | Merge diagnostics | _merge variable (1/2/3) automatic | No automatic merge indicator; must add explicitly | gotchas.md |
| 10 | 1-based vs 0-based indexing | _n starts at 1; var[1] = first obs | Python indices start at 0 | gotchas.md |
Agent Code Annotation Protocol
This section defines when and how code-producing agents add inline Stata-equivalent comments to DAAF Python scripts.
When to Annotate
Annotations are added only when the orchestrator explicitly passes a Stata-background directive to the agent. This is not a default behavior.
Trigger conditions (orchestrator activates this when any apply):
- User states they have a Stata background
- User requests Stata-equivalent comments in code
- User asks to understand Python code from a Stata perspective
How the orchestrator passes the directive: The orchestrator adds the following to the agent prompt:
"User has Stata background. Load stata-python-translation skill. Add inline Stata-equivalent comments for non-trivial data operations."
Comment Format
# Stata: gen log_income = log(income)
df = df.with_columns(pl.col("income").log().alias("log_income"))
# Stata: bysort state: egen mean_score = mean(test_score)
df = df.with_columns(
pl.col("test_score").mean().over("state").alias("mean_score")
)
# Stata: reghdfe wage education experience, absorb(industry year) cluster(state)
fit = pf.feols("wage ~ education + experience | industry + year",
data=pdf, vcov={"CRV1": "state"})
# Stata: drop if missing(income)
df = df.filter(pl.col("income").is_not_null())
# Stata: merge 1:1 school_id using "districts.dta"
df = df.join(districts, on="school_id", how="inner")What to Annotate
- Annotate: Data wrangling (polars operations), modeling calls (pyfixest, statsmodels, linearmodels), visualization layer construction (plotnine, plotly), causal inference method calls, survey estimation calls
- Do NOT annotate: Import statements,
print()/assertvalidation lines, file I/O boilerplate (pl.read_parquet,df.write_parquet), config sections, section separator comments
Rules
- One
# Stata:comment per logical operation, placed on the line immediately above the Python code - Keep annotations to a single line; abbreviate complex Stata command sequences if needed
- Stata annotations are in addition to standard IAT comments (
# INTENT:,# REASONING:,# ASSUMES:), not a replacement - Consumer agents: research-executor, code-reviewer, debugger, data-ingest
Related Skills
| Skill | Relationship |
|---|---|
polars | Python-side data wrangling -- detailed API reference for the gen/replace/merge/collapse equivalent |
pyfixest | Python-side fixed effects regression -- detailed API for the regress/reghdfe/ivregress equivalent |
plotnine | Python-side static visualization -- detailed API for the graph twoway equivalent |
plotly | Python-side interactive visualization -- no direct Stata equivalent |
statsmodels | Python-side general modeling -- covers logit, probit, poisson, nbreg, glm equivalents |
linearmodels | Python-side panel/IV models -- covers xtreg, sureg, ivregress equivalents |
scikit-learn | Python-side ML -- covers limited teffects/matching equivalents |
svy | Python-side survey analysis -- covers svyset and svy: prefix command equivalents |
geopandas | Python-side spatial data -- covers Stata spmap/spregress equivalents |
marimo | Python-side notebooks -- replaces do-file + log workflow |
r-python-translation | Parallel skill for R-background users -- shares the same Python target stack |
Note: Individual tool skills contain library-specific usage guidance (syntax, gotchas, performance). This skill provides the Stata-to-Python conceptual bridge -- use both together when a Stata-background user is working with a specific library.
Topic Index
| Topic | Reference File |
|---|---|
| Single-dataset model (one dataset in memory) | ./references/paradigm-differences.md |
| Missing values (. vs None/NaN/null) | ./references/paradigm-differences.md |
| Extended missing values (.a-.z) | ./references/paradigm-differences.md |
| Value labels (label define, label values) | ./references/paradigm-differences.md |
| Variable labels (label variable) | ./references/paradigm-differences.md |
| by: prefix and _n/_N system variables | ./references/paradigm-differences.md |
| In-place modification vs immutable DataFrames | ./references/paradigm-differences.md |
| Macro system (local, global) | ./references/paradigm-differences.md |
| Estimation and e()/r() stored results | ./references/paradigm-differences.md |
| Type system (destring/tostring vs .cast()) | ./references/paradigm-differences.md |
| Panel data operators (L./F./D. vs .shift()/.over()) | ./references/paradigm-differences.md |
| Package model (ssc install vs pip install) | ./references/paradigm-differences.md |
| gen / replace / rename | ./references/data-management.md |
| drop / keep (variables and observations) | ./references/data-management.md |
| sort / gsort | ./references/data-management.md |
| merge 1:1 / m:1 / 1:m | ./references/data-management.md |
| append | ./references/data-management.md |
| reshape long / reshape wide | ./references/data-management.md |
| collapse (aggregation) | ./references/data-management.md |
| egen functions (mean, sum, count, rowtotal, group, tag) | ./references/data-management.md |
| encode / decode | ./references/strings-dates-labels.md |
| String functions (substr, strpos, subinstr, regexm) | ./references/strings-dates-labels.md |
| Date system (epoch, %td, mdy(), date()) | ./references/strings-dates-labels.md |
| destring / tostring | ./references/strings-dates-labels.md |
| regress (OLS) | ./references/regression-modeling.md |
| areg / reghdfe (fixed effects) | ./references/regression-modeling.md |
| xtreg fe / xtreg re (panel models) | ./references/regression-modeling.md |
| ivregress / ivreg2 / ivreghdfe (IV) | ./references/regression-modeling.md |
| logit / probit / ologit / mlogit | ./references/regression-modeling.md |
| poisson / nbreg / ppmlhdfe | ./references/regression-modeling.md |
| sureg (seemingly unrelated regression) | ./references/regression-modeling.md |
| margins / marginsplot (marginal effects) | ./references/regression-modeling.md |
| test / lincom / nlcom (hypothesis testing) | ./references/regression-modeling.md |
| esttab / outreg2 (regression tables) | ./references/regression-modeling.md |
| predict (fitted values, residuals) | ./references/regression-modeling.md |
| Robust and clustered standard errors | ./references/regression-modeling.md |
| Factor variable notation (i., c., #, ##) | ./references/regression-modeling.md |
| diff / did_multiplegt / csdid (DiD) | ./references/causal-inference.md |
| eventstudyinteract / event studies | ./references/causal-inference.md |
| rdrobust / rdplot (regression discontinuity) | ./references/causal-inference.md |
| teffects / psmatch2 / cem (matching) | ./references/causal-inference.md |
| synth / synth_runner (synthetic control) | ./references/causal-inference.md |
| binscatter / binsreg | ./references/causal-inference.md |
| graph twoway scatter / line / area / connected | ./references/visualization.md |
| graph bar / graph box / histogram / kdensity | ./references/visualization.md |
| graph export | ./references/visualization.md |
| coefplot / iplot | ./references/visualization.md |
| svyset / svy: prefix | ./references/survey-spatial-ml.md |
| svy: mean / total / proportion / ratio | ./references/survey-spatial-ml.md |
| svy: regress / logit | ./references/survey-spatial-ml.md |
| Spatial data analysis | ./references/survey-spatial-ml.md |
| Machine learning (teffects, matching workarounds) | ./references/survey-spatial-ml.md |
| Do-file execution model | ./references/workflow-environment.md |
| Log files (log using) | ./references/workflow-environment.md |
| Ado-files and ssc install | ./references/workflow-environment.md |
| Macros in loops (foreach, forvalues) | ./references/workflow-environment.md |
| tempvar / tempfile / preserve / restore | ./references/workflow-environment.md |
| quietly / capture / noisily | ./references/workflow-environment.md |
| Curated Stata-to-Python migration guides | ./references/external-resources.md |
| Textbooks with trilingual code (Stata/R/Python) | ./references/external-resources.md |
| Package documentation links | ./references/external-resources.md |
| Tutorial recommendations with provenance | ./references/external-resources.md |
| gen/replace pattern not translating | ./references/gotchas.md |
| Missing value comparison traps | ./references/gotchas.md |
| drop if -> filter(NOT) negation trap | ./references/gotchas.md |
| Merge without _merge diagnostics | ./references/gotchas.md |
| egen rowtotal missing-value behavior | ./references/gotchas.md |
| .fit() forgotten in statsmodels | ./references/gotchas.md |
| Robust SE syntax differences | ./references/gotchas.md |
| 0-based vs 1-based indexing | ./references/gotchas.md |
| Macro syntax in Python context | ./references/gotchas.md |
| Error message translation table | ./references/gotchas.md |
Causal Inference: Stata to Python Translation
Stata is the dominant platform for applied causal inference in economics and quantitative social science. Most influential methods papers ship Stata packages first — reghdfe, did_multiplegt, rdrobust, synth — and Python ports follow months to years later. Some never arrive. This creates a real translation challenge: Stata users working with DAAF's Python stack will encounter methods where the Python coverage ranges from "nearly identical API by the same authors" to "no equivalent exists."
DAAF's Python stack covers the most common causal designs:
- Difference-in-differences: pyfixest (TWFE, did2s, lpdid, Sun-Abraham event_study)
- Event studies: pyfixest (i() operator, iplot, panelview)
- Regression discontinuity: rdrobust (same authors as Stata version)
- Instrumental variables: pyfixest (with FE) and linearmodels (LIML, GMM)
- Marginal effects / interpretation: marginaleffects (same author as R version)
- Synthetic control: scpi (same authors as Stata version), CausalPy (Bayesian)
- Binscatter: binsreg (same authors as Stata version)
This reference documents both what translates cleanly and where real gaps remain.
Versions referenced:
Python: pyfixest 0.40.0, rdrobust (unpinned), marginaleffects (unpinned),
scpi (unpinned), binsreg (unpinned)
Stata: Stata 18 (SE/MP)
See SKILL.md for the complete version table.
Sources: Cunningham, Causal Inference: The Mixtape (Yale, 2021);
Huntington-Klein, The Effect (CRC Press, 2021);
Fischer et al., pyfixest (pyfixest.org, v0.40.0, accessed 2026-03-28);
Cattaneo, Idrobo, & Titiunik, *A Practical Introduction to Regression
Discontinuity Designs* (Cambridge, 2020);
Arel-Bundock, Greifer, & Heiss, "marginaleffects for R and Python" (JSS, 2024);
Goodman-Bacon, "Difference-in-Differences with Variation in Treatment Timing"
(J. Econometrics, 2021);
de Chaisemartin & D'Haultfoeuille, "Two-Way Fixed Effects Estimators with
Heterogeneous Treatment Effects" (AER, 2020)
---
1. Difference-in-Differences
Traditional TWFE DiD
The simplest DiD design: all treated units adopt simultaneously, effects are homogeneous. When these assumptions hold, TWFE and modern estimators produce identical results.
Stata:
* Method 1: Interaction operator
reg y treated##post, cluster(unit)
* Method 2: Explicit indicator + FE
reghdfe y treated_post, absorb(unit year) cluster(unit)
* Method 3: Community diff command
diff y, t(treated) p(post)Python (pyfixest):
import pyfixest as pf
# Method 1: Interaction
fit = pf.feols("y ~ treat:post | unit + time", data=df, vcov={"CRV1": "unit"})
# Method 2: Explicit indicator + FE (equivalent)
fit = pf.feols("y ~ treated_post | unit + year", data=df, vcov={"CRV1": "unit"})
fit.summary()The formula syntax is nearly identical. The only difference is the clustering syntax: Stata's cluster(unit) becomes pyfixest's vcov={"CRV1": "unit"}.
When TWFE fails: With staggered treatment timing and heterogeneous effects, TWFE can produce severely biased estimates including sign reversals (Goodman-Bacon, 2021; de Chaisemartin & D'Haultfoeuille, 2020). Use one of the modern estimators below for staggered designs.
Two-Stage DiD (Gardner, 2022)
did2s imputes the counterfactual using only untreated observations, then estimates treatment effects in a second stage. This avoids the negative weighting problem of TWFE under treatment effect heterogeneity.
Stata (`did2s`):
did2s y, first_stage(i.unit i.time) second_stage(i.rel_time) treatment(treated) cluster(unit)Python (pyfixest):
fit = pf.did2s(
data=df,
yname="y",
first_stage="~ 0 | unit + time", # FE from untreated obs
second_stage="~ i(rel_time, ref=-1)", # Treatment effect spec
treatment="treated",
cluster="unit",
)
fit.summary()
fit.iplot() # Event study plotPooled ATT (single treatment effect instead of event study):
Stata:
did2s y, first_stage(i.unit i.time) second_stage(treated) treatment(treated) cluster(unit)Python:
fit = pf.did2s(df, "y", "~ 0 | unit + time", "~ treated", "treated", "unit")Key differences:
- Stata uses
first_stage()andsecond_stage()options; Python uses string formula arguments - Stata uses
cluster()option; Python usescluster=keyword - Both return objects compatible with etable/iplot
Callaway-Sant'Anna (2021)
Group-time ATTs that properly handle staggered adoption with doubly-robust estimation.
Stata (`csdid`):
csdid y, ivar(unit_id) time(year) gvar(first_treat) method(dripw)
estat simple * Overall ATT
estat event * Dynamic event study
estat group * Group-specific ATT
csdid_plot * Event study visualizationPython (`csdid` package):
# pip install csdid
from csdid import att_gt
out = att_gt(
yname="y",
tname="year",
idname="unit_id",
gname="first_treat",
data=df,
control_group="nevertreated",
est_method="dr", # Doubly robust
)
# API mirrors Stata csdid package; check csdid docs
# for current aggregation and plotting methodsCoverage note: The Python csdid package is a community port (d2cml-ai), not an official release by Callaway & Sant'Anna. It aims to replicate the Stata API but may lag behind on features and bug fixes. Verify results against Stata when using for published research.
Sun-Abraham Saturated Estimator
Fully saturates the model with cohort-by-period indicators, then aggregates. Avoids contamination from comparing already-treated to newly-treated units.
Stata:
* Using fixest-style sunab() in Stata:
* eventstudyinteract y lead_lag_vars, absorb(unit year) cohort(first_treat) control_cohort(never_treat)
* Or manual saturation:
reghdfe y ib(-1).rel_time, absorb(unit year) cluster(unit)Python (pyfixest):
fit = pf.event_study(
data=df,
yname="y",
idname="unit",
tname="period",
gname="cohort", # Year of treatment adoption
estimator="saturated", # Sun-Abraham
att=False, # False = dynamic event study
cluster="unit",
)
fit.summary()
fit.iplot()
# Aggregate to overall ATT
agg = fit.aggregate(weighting="shares")Key difference: Stata uses sunab() as a formula function or a separate command. Python uses pf.event_study() with estimator="saturated". The underlying estimator is identical.
Local Projections DiD (Dube et al., 2023)
Flexible dynamics without assuming a specific functional form for treatment effects over time.
Stata: No widely adopted standalone Stata package; typically implemented manually via local projections (Jorda, 2005).
Python (pyfixest):
result = pf.lpdid(
data=df,
yname="y",
idname="unit",
tname="year",
gname="treatment_year",
att=True, # True = pooled ATT, False = period-specific
pre_window=5,
post_window=10,
never_treated=0, # Value of gname for never-treated units
)
# Important: lpdid() returns a DataFrame, NOT a Feols object
# result.summary() and result.iplot() will NOT workThis is a rare case where Python (pyfixest) has a more convenient implementation than Stata.
Important: lpdid() returns a pandas DataFrame with columns for period, estimate, std_error, ci_lower, ci_upper, etc. It cannot be passed to pf.etable() or use .iplot(). Plot results manually with plotnine or matplotlib.
DiD Estimator Summary
| Stata | pyfixest | When to Use |
|---|---|---|
reghdfe y treated, absorb(unit year) | `pf.feols("y ~ treated \ | unit + year")` |
did2s | pf.did2s(...) | Staggered; imputation-based |
csdid | csdid.att_gt(...) | Staggered; group-time ATTs |
eventstudyinteract / sunab | pf.event_study(estimator="saturated") | Staggered; fully saturated |
| Manual LP-DiD | pf.lpdid(...) | Flexible dynamics |
did_multiplegt | No Python equivalent | de Chaisemartin-D'Haultfoeuille |
---
2. Event Studies
Manual Event Study with i()
Stata:
* Create relative-time variable
gen rel_year = year - treatment_year
* Event study regression — omit t=-1 as reference
reghdfe y ib(-1).rel_year, absorb(unit year) cluster(unit)
* Plot (community-contributed)
coefplot, keep(*.rel_year) vertical
event_plotPython (pyfixest):
# Create relative-time variable
df["rel_year"] = df["year"] - df["treatment_year"]
# Event study with i() — omit t=-1 as reference
fit = pf.feols("y ~ i(rel_year, ref=-1) | unit + year", data=df,
vcov={"CRV1": "unit"})
# Plot
fit.iplot()
# With joint confidence bands (Bonferroni + Scheffe)
fit.iplot(joint="both")Event Study Syntax Comparison
| Feature | Stata | pyfixest |
|---|---|---|
| Reference level | ib(-1).rel_year | i(rel_year, ref=-1) |
| Plot | coefplot / event_plot | fit.iplot() |
| Joint bands | Varies by package | fit.iplot(joint="both") |
| Band types | Bonferroni | "bonferroni", "scheffe", or "both" |
Unified Event Study Interface
pyfixest provides a single function that can run TWFE, did2s, or Sun-Abraham event studies:
# TWFE event study
fit_twfe = pf.event_study(
data=df, yname="y", idname="unit", tname="year",
gname="treatment_year", estimator="twfe", att=False, cluster="unit"
)
# did2s event study
fit_d2s = pf.event_study(
data=df, yname="y", idname="unit", tname="year",
gname="treatment_year", estimator="did2s", att=False, cluster="unit"
)
# Sun-Abraham event study
fit_sa = pf.event_study(
data=df, yname="y", idname="unit", tname="year",
gname="treatment_year", estimator="saturated", att=False, cluster="unit"
)
# Compare visually
pf.coefplot([fit_twfe, fit_d2s, fit_sa])Stata does not have a single unified function for this — you must choose between different commands (reghdfe + i(), did2s, eventstudyinteract).
Treatment Pattern Visualization
Stata:
* panelview (if installed)
panelview y, i(unit) t(year) d(treated) type(treat)Python (pyfixest):
pf.panelview(data=df, unit="unit", time="year", treat="treated")panelview() produces a heatmap showing treatment assignment across units and time periods. This is built into pyfixest with no additional package needed.
---
3. Regression Discontinuity
All three implementations (Stata, R, Python) are maintained by the same authors (Cattaneo, Idrobo, Titiunik), ensuring very high cross-platform fidelity. The translation is nearly mechanical.
Sharp RD
Stata:
rdrobust Y X, c(cutoff)
rdplot Y X, c(cutoff)
rdbwselect Y X, c(cutoff)Python:
# pip install rdrobust
from rdrobust import rdrobust, rdplot, rdbwselect
# Point estimate with robust bias-corrected CI
rd = rdrobust(Y, X, c=cutoff)
print(rd)
# Data-driven RD plot
rdplot(Y, X, c=cutoff)
# Bandwidth selection
bw = rdbwselect(Y, X, c=cutoff)
print(bw)Fuzzy RD
Stata:
rdrobust Y X, c(cutoff) fuzzy(T)Python:
rd = rdrobust(Y, X, c=cutoff, fuzzy=T)Key Parameter Mapping
| Parameter | Stata | Python | Notes |
|---|---|---|---|
| Outcome | Y (1st positional) | Y (1st positional) | Numpy array or Series |
| Running variable | X (2nd positional) | X (2nd positional) | Numpy array or Series |
| Cutoff | c(0) | c=0 | Default is 0 |
| Kernel | kernel(tri) | kernel="tri" | "tri", "uni", "epa" |
| Bandwidth | h(h_left h_right) | h=[h_left, h_right] | Stata space-separated; Python list |
| Polynomial order | p(1) | p=1 | Local linear is default |
| Fuzzy | fuzzy(T) | fuzzy=T | Treatment indicator |
| Covariates | covs(c1 c2) | covs=covs_array | Stata varlist; Python numpy array |
| Clustering | cluster(cl) | cluster=cl | Cluster variable |
The API is virtually identical. The main syntactic differences are Stata's space-separated paired values vs Python's lists, and Stata's varlist vs numpy arrays for covariates.
Additional RD Packages (Same Authors, Same API Across Languages)
| Tool | Stata Command | Python Package | Install |
|---|---|---|---|
| Local polynomial RD | rdrobust | rdrobust | pip install rdrobust |
| RD plots | rdplot | rdrobust.rdplot() | Included |
| Bandwidth selection | rdbwselect | rdrobust.rdbwselect() | Included |
| Manipulation testing | rddensity | rddensity | pip install rddensity |
| Multi-cutoff/score | rdmulti | rdmulti | pip install rdmulti |
| Power calculations | rdpower | rdpower | pip install rdpower |
All packages in the rdpackages suite are maintained by the same team across Stata, R, and Python. Translation is mechanical.
Data Preparation Difference
The one meaningful difference is data handling. Stata reads from the in-memory dataset; Python passes arrays explicitly:
Stata:
use "election_data.dta", clear
rdrobust vote_share margin, c(0)Python:
import polars as pl
df = pl.read_parquet("election_data.parquet")
Y = df["vote_share"].to_numpy()
X = df["margin"].to_numpy()
rd = rdrobust(Y, X, c=0)---
4. Instrumental Variables (Causal Inference Context)
See also Section 4 of the companion regression-modeling.md for formula syntax details. This section focuses on the causal inference aspects of IV.
IV with Fixed Effects
Stata:
* Classic IV: education instrumented by college proximity
ivreghdfe log_wage experience (education = college_prox), absorb(state year) cluster(state)Python (pyfixest):
fit = pf.feols(
"log_wage ~ experience | state + year | education ~ college_prox",
data=df, vcov={"CRV1": "state"}
)
fit.IV_Diag() # Effective F, Cragg-Donald, etc.
fit._model_1st_stage.summary() # First-stage resultsLIML and GMM
Stata:
ivregress liml y x_exog (x_endog = z1 z2)
ivregress gmm y x_exog (x_endog = z1 z2)Python (linearmodels):
from linearmodels.iv import IVLIML, IVGMM
fit_liml = IVLIML.from_formula("y ~ 1 + x_exog + [x_endog ~ z1 + z2]", data=df).fit()
fit_gmm = IVGMM.from_formula("y ~ 1 + x_exog + [x_endog ~ z1 + z2]", data=df).fit()linearmodels has broader IV estimator coverage than the standard Stata ivregress suite (LIML, GMM, CUE all in one package).
Weak Instrument Testing
Stata:
ivregress 2sls y (x_endog = z1 z2), first
estat firststagePython (pyfixest):
fit = pf.feols("y ~ 1 | 0 | x_endog ~ z1 + z2", data=df)
fit.IV_Diag() # Reports effective F (Olea-Pflueger), Cragg-Donald, Kleibergen-Paappyfixest reports the effective F-statistic (Olea & Pflueger, 2013), which is preferred over the Cragg-Donald F-statistic for models with clustered SEs or multiple endogenous regressors.
---
5. Matching and Propensity Scores
This is a significant gap in the Python ecosystem relative to Stata's built-in teffects suite.
Stata's teffects Suite
* Propensity score matching
teffects psmatch (y) (treat x1 x2 x3)
* Inverse probability weighting
teffects ipw (y) (treat x1 x2 x3)
* Regression adjustment
teffects ra (y x1 x2 x3) (treat)
* Doubly-robust AIPW
teffects aipw (y x1 x2 x3) (treat x1 x2 x3)
* Community-contributed matching
psmatch2 treat x1 x2, outcome(y) neighbor(3)
* Coarsened exact matching
cem x1 x2 x3, treatment(treat)Python Alternatives (Fragmented)
| Stata | Python Equivalent | Fidelity | Notes |
|---|---|---|---|
teffects psmatch | pymatchit-causal or manual sklearn | Low-Medium | SEs do not account for estimated PS |
teffects ipw | Manual: sklearn LogisticRegression + weighting | Low | Must implement manually |
teffects ra | Manual: separate regressions + averaging | Low | |
teffects aipw / teffects ipwra | econml.dr.DRLearner | Medium | Different implementation |
psmatch2 | pymatchit-causal or psmpy | Low-Medium | Community packages |
cem | pymatchit-causal (CEM method) | Low-Medium |
Manual PSM in Python (Sketch)
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors
import numpy as np
# Estimate propensity scores
X = df[["x1", "x2", "x3"]].to_numpy()
treatment = df["treat"].to_numpy()
ps_model = LogisticRegression().fit(X, treatment)
ps = ps_model.predict_proba(X)[:, 1]
# Nearest-neighbor matching on propensity score
treated_ps = ps[treatment == 1].reshape(-1, 1)
control_ps = ps[treatment == 0].reshape(-1, 1)
nn = NearestNeighbors(n_neighbors=1)
nn.fit(control_ps)
distances, indices = nn.kneighbors(treated_ps)This sketch lacks MatchIt/teffects' balance diagnostics, caliper options, exact matching constraints, variance ratio checks, and (critically) correct standard errors.
The Standard Error Problem
Stata's teffects psmatch computes standard errors that account for the fact that propensity scores are estimated (Abadie & Imbens, 2016). Most Python matching packages do NOT make this correction, which means their reported SEs and p-values are wrong (too small). For published research using propensity score matching, consider running the matching step in Stata or R and importing the matched dataset.
Available Python Matching Packages
| Package | Install | Methods | Quality |
|---|---|---|---|
pymatchit-causal | pip install pymatchit-causal | Nearest neighbor, optimal, exact, CEM | Medium (port of R MatchIt) |
psmpy | pip install psmpy | PS matching via k-NN | Medium (academic) |
pysmatch | pip install pysmatch | Gradient-based propensity matching | Fair |
| Manual sklearn | Pre-installed | LogisticRegression + NearestNeighbors | Requires manual implementation |
---
6. Synthetic Control
Classic Synthetic Control (Abadie-Diamond-Hainmueller)
Stata:
synth depvar predictor1 predictor2, trunit(treated_id) trperiod(treatment_year)Python (scpi — recommended):
# pip install scpi
import scpi
# scpi (by Cattaneo, Feng, Palomba, Titiunik) provides prediction intervals
# and uncertainty quantification; same authors as Stata versionPython (SyntheticControlMethods — community):
# pip install SyntheticControlMethods
from SyntheticControlMethods import Synth
sc = Synth(df, "outcome", "unit_id", "time", treatment_id, treatment_time)
sc.fit()
sc.path_plot()Python (CausalPy — Bayesian alternative):
# pip install CausalPy
import causalpy as cp
result = cp.SyntheticControl(
df,
treatment_time=treatment_time,
formula="y ~ 0 + x1 + x2",
model=cp.pymc_models.WeightedSumFitter(),
)
result.plot()Synthetic Control Package Comparison
| Stata | Python Equivalent | Install | Fidelity | Notes |
|---|---|---|---|---|
synth | scpi | pip install scpi | High | Same authors; includes prediction intervals |
synth | SyntheticControlMethods | pip install SyntheticControlMethods | Medium | Community; classic ADH estimator |
synth_runner | No direct equivalent | N/A | Gap | Automated inference for synth |
| N/A | CausalPy | pip install CausalPy | Different methodology | Bayesian SC (PyMC) |
| N/A | synthdid | pip install synthdid | Medium | Synthetic DiD (Arkhangelsky et al.) |
The scpi package by Cattaneo et al. is the most rigorous Python option for synthetic control, providing prediction intervals and uncertainty quantification.
---
7. Binscatter
binsreg (Same Authors Across Languages)
Stata:
* Community-contributed
binsreg y x, nbins(20)
binsreg y x, nbins(20) controls(x2 x3)
binscatter y x, controls(x2 x3)Python:
# pip install binsreg
import binsreg
# Basic binscatter
binsreg.binsreg(y=df["y"].to_numpy(), x=df["x"].to_numpy(), nbins=20)
# With controls
binsreg.binsreg(
y=df["y"].to_numpy(),
x=df["x"].to_numpy(),
w=df[["x2", "x3"]].to_numpy(),
nbins=20,
)The binsreg package is maintained by Cattaneo, Crump, Farrell, and Feng across Stata, R, and Python. The Python API closely mirrors the Stata syntax. The main difference is that Stata reads from the in-memory dataset while Python requires explicit numpy arrays.
---
8. Other Causal Tools
Survival Analysis / Duration Models
Stata:
stset time, failure(event)
stcox x1 x2, strata(group)
sts graphPython:
# pip install lifelines
from lifelines import CoxPHFitter
cph = CoxPHFitter()
cph.fit(df_pd, duration_col="time", event_col="event",
formula="x1 + x2 + strata(group)")
cph.print_summary()
cph.plot()lifelines is mature and well-maintained. It covers Cox PH, Kaplan-Meier, Nelson-Aalen, and parametric survival models. This is an area of good Python coverage.
Bayesian Causal Impact (Time Series Intervention)
Stata: No direct equivalent. Some users implement manually or use R's CausalImpact via rcall.
Python:
# pip install CausalPy
import causalpy as cp
result = cp.InterruptedTimeSeries(
df,
treatment_time=treatment_time,
formula="y ~ 1 + t",
model=cp.pymc_models.LinearRegression(),
)
result.plot()Dynamic Panel GMM
Stata:
xtabond y L.y x1 x2, lags(2) vce(robust)
xtdpd y L(1/2).y x1 x2, dgmmiv(y) lgmmiv(y) vce(robust)
xtabond2 y L.y x1 x2, gmm(L.y) iv(x1 x2)Python: Limited support. linearmodels does not implement Arellano-Bond or Blundell-Bond GMM estimators. This is a significant gap for researchers working with dynamic panel models.
Partial workaround: implement the first-differenced IV approach manually using pyfixest or linearmodels.IV2SLS with lagged instruments, but this does not replicate xtabond2's full functionality (sequential moment conditions, Windmeijer correction, Sargan/Hansen tests for over-identification).
---
9. Coverage Gaps: Honest Assessment
What Stata Has That Python Lacks
| Stata Command | Gap Description | Severity | Best Workaround |
|---|---|---|---|
did_multiplegt | de Chaisemartin & D'Haultfoeuille estimators | High | No Python port; use Stata or R |
teffects suite | Unified treatment effects framework with correct SEs | High | Manual implementation; verify SEs carefully |
teffects psmatch SE correction | SEs accounting for estimated propensity scores | High | Run matching in Stata/R; import matched data |
teffects ipwra / teffects aipw | Doubly-robust AIPW with correct SEs | High | econml.dr.DRLearner (different implementation) |
xtabond / xtabond2 / xtdpd | Dynamic panel GMM (Arellano-Bond, Blundell-Bond) | High | No adequate Python equivalent |
feglm / GLM + high-dim FE | pyfixest feglm() does not support FE absorption | High | LPM, manual dummies, or fepois |
synth_runner | Automated SC inference and placebo tests | Medium | Manual iteration; scpi for prediction intervals |
psmatch2 balance diagnostics | Balance tables, variance ratios | Medium | Manual computation |
cem (coarsened exact matching) | Limited Python coverage | Medium | pymatchit-causal partial support |
What Python Does Better
| Capability | Python Advantage | Notes |
|---|---|---|
pf.lpdid() | Convenient LP-DiD wrapper | No Stata equivalent package |
pf.event_study() | Unified interface for TWFE/did2s/Sun-Abraham | Stata requires different commands for each |
pf.panelview() | Built-in treatment pattern visualization | Stata requires separate install |
| LIML/GMM estimators | linearmodels has LIML, GMM, CUE in one package | Stata ivregress has 2SLS/LIML/GMM but less unified |
| Multiple model objects | Models coexist as variables | Stata has one active e() result |
| Reproducible pipelines | DAAF's file-first execution model | Stata do-files have no immutable audit trail |
---
10. Ecosystem Mapping Table
| Method | Stata | Python | Fidelity | Notes |
|---|---|---|---|---|
| TWFE DiD | reghdfe | pf.feols() | Very High | Cluster SE syntax only |
| did2s | did2s | pf.did2s() | Very High | Arg names differ |
| Sun-Abraham | eventstudyinteract | pf.event_study(est="saturated") | High | Different API, same estimator |
| Callaway-Sant'Anna | csdid | csdid.att_gt() | Medium | Community port |
| LP-DiD | Manual | pf.lpdid() | N/A | Python has better wrapper |
| de Chaisemartin-D'Haultfoeuille | did_multiplegt | No equivalent | N/A | Gap |
| Event study (iplot) | coefplot / event_plot | fit.iplot() | Very High | Method vs command |
| RD (sharp/fuzzy) | rdrobust | rdrobust | Very High | Same authors |
| RD plots | rdplot | rdrobust.rdplot() | Very High | Same authors |
| RD density test | rddensity | rddensity | Very High | Same authors |
| IV + FE | ivreghdfe | pf.feols() | Very High | Three-part formula |
| IV (2SLS, no FE) | ivregress 2sls | linearmodels.IV2SLS() | High | Formula syntax differs |
| LIML | ivregress liml | linearmodels.IVLIML() | High | |
| GMM-IV | ivregress gmm | linearmodels.IVGMM() | High | |
| PS matching | teffects psmatch | pymatchit-causal / manual | Low | SE gap |
| IPW | teffects ipw | Manual sklearn + weighting | Low | SE gap |
| AIPW | teffects aipw | econml.dr.DRLearner | Medium | Different implementation |
| Synthetic control | synth | scpi | High | Same authors |
| Binscatter | binsreg / binscatter | binsreg | Very High | Same authors |
| Survival/Cox | stcox | lifelines.CoxPHFitter() | High | Good coverage |
| Dynamic panel GMM | xtabond2 | No equivalent | N/A | Gap |
| Marginal effects | margins | marginaleffects | High | Same author (R version); Python is alpha |
Fidelity Legend
- Very High: Same or near-identical API, same authors, or results match to high precision
- High: Reliable implementation with minor syntax differences; results match
- Medium: Community port or different implementation; verify results for published work
- Low: Partial coverage; significant feature gaps or SE issues
- N/A: No equivalent available
Sources: Cunningham, Causal Inference: The Mixtape (Yale, 2021), ch. 5-9;
Huntington-Klein, The Effect (CRC Press, 2021), ch. 16-21;
Gardner, "Two-Stage Differences in Differences" (arXiv:2207.05943, 2022);
Callaway & Sant'Anna, "Difference-in-Differences with Multiple Time Periods"
(J. Econometrics, 2021);
Sun & Abraham, "Estimating Dynamic Treatment Effects in Event Studies with
Heterogeneous Treatment Effects" (J. Econometrics, 2021);
Dube, Girardi, Jorda, & Taylor, "A Local Projections Approach to
Difference-in-Differences" (NBER WP 31184, 2023);
Cattaneo, Idrobo, & Titiunik, *A Practical Introduction to Regression
Discontinuity Designs* (Cambridge, 2020);
Calonico, Cattaneo, Farrell, & Titiunik, "rdrobust: Software for
Regression-discontinuity Designs" (Stata Journal, 2017);
Cattaneo, Feng, Palomba, & Titiunik, "scpi" (nppackages.github.io/scpi);
Abadie, Diamond, & Hainmueller, "Synthetic Control Methods" (JASA, 2010);
Abadie & Imbens, "Matching on the Estimated Propensity Score" (Econometrica,
2016);
de Chaisemartin & D'Haultfoeuille, "Two-Way Fixed Effects Estimators with
Heterogeneous Treatment Effects" (AER, 2020);
Goodman-Bacon, "Difference-in-Differences with Variation in Treatment Timing"
(J. Econometrics, 2021);
Arel-Bundock, "marginaleffects" (marginaleffects.com, accessed 2026-03-28);
Fischer et al., pyfixest (pyfixest.org, v0.40.0, accessed 2026-03-28);
Naqvi, "DiD estimator comparison" (asjadnaqvi.github.io/DiD);
Stata teffects manual (stata.com/manuals/causal.pdf)Data Management: Stata to Python (polars)
Companion file: For string operations, date/time operations, and value
labels / categorical types, see strings-dates-labels.md.This document provides an exhaustive command-by-command translation between Stata's core data management commands and Python's polars library. The fundamental mental model shift: Stata operates on a single implicit dataset and modifies it in place; polars operates on explicitly named DataFrames and returns new objects from every operation. Stata users will find that most commands have a direct polars equivalent, but the grammar requires (a) naming the DataFrame, (b) wrapping column references in pl.col(), and (c) reassigning the result (df = df.operation(...)).
Versions referenced:
Python: polars 1.38.1, pyreadstat 1.2.x
See SKILL.md for the complete version table.
Sources: Sullivan, "Stata to Python Equivalents" (danielmsullivan.com, accessed
2026-03-28); Turrell, "Coming from Stata" in Coding for Economists
(aeturrell.github.io, accessed 2026-03-28); pandas documentation, "Comparison with
Stata" (pandas.pydata.org, accessed 2026-03-28); polars 1.x User Guide and API
Reference (docs.pola.rs, accessed 2026-03-28); pyreadstat (github.com/Roche/pyreadstat,
accessed 2026-03-28); Stata manuals (stata.com/manuals, accessed 2026-03-28).
---
Section 1: Data I/O
use / save / import / export
| Stata | polars | Notes |
|---|---|---|
use myfile.dta | df = pl.read_parquet("myfile.parquet") | DAAF uses parquet exclusively |
use var1 var2 using myfile.dta | df = pl.read_parquet("myfile.parquet", columns=["var1", "var2"]) | Column-selective read |
save myfile.dta, replace | df.write_parquet("myfile.parquet") | DAAF mandate: parquet only |
import delimited myfile.csv | df = pl.read_csv("myfile.csv") | polars infers types aggressively |
import delimited, delimiters("\t") | df = pl.read_csv("myfile.csv", separator="\t") | Tab-delimited |
export delimited myfile.csv | df.write_csv("myfile.csv") | |
import excel myfile.xlsx | df = pl.read_excel("myfile.xlsx") | Requires xlsx2csv or openpyxl backend |
Reading .dta files (bridge pattern)
Polars cannot read .dta files natively. Use pyreadstat to preserve Stata metadata including value labels, variable labels, and extended missing values:
# pyreadstat bridge -- preserves all Stata metadata
import pyreadstat
df_pd, meta = pyreadstat.read_dta("myfile.dta")
df = pl.from_pandas(df_pd)
# Metadata available from pyreadstat:
# meta.variable_value_labels -- dict: {varname: {int: label_string, ...}}
# meta.column_names_to_labels -- dict: {varname: "variable label text"}
# meta.original_variable_types -- dict: {varname: "stata_type"}# Simpler bridge via pandas (loses some metadata)
df = pl.from_pandas(pd.read_stata("myfile.dta"))Lazy scanning (preferred DAAF pattern)
# Lazy scan -- reads only needed columns and rows from Parquet
df = (
pl.scan_parquet("myfile.parquet")
.filter(pl.col("year") >= 2015)
.select("id", "year", "enrollment")
.collect()
)Key behavioral differences:
- Stata loads one dataset into memory at a time (the "one dataset" model). Python
holds arbitrarily many DataFrames simultaneously as separate variables.
- Stata's .dta format embeds variable labels, value labels, and format specs.Parquet does not -- store metadata in documentation or a companion schema dict.
- The single-dataset model eliminatespreserve/restorein Python: creating a
subset never destroys the original.
Sources: polars User Guide (docs.pola.rs, accessed 2026-03-28); pyreadstat (github.com/Roche/pyreadstat, accessed 2026-03-28); pandas "Comparison with Stata" (pandas.pydata.org, accessed 2026-03-28).
---
Section 2: Variable Creation and Modification
generate -- Create a New Column
* Stata
generate newvar = oldvar + 7# polars
df = df.with_columns((pl.col("oldvar") + 7).alias("newvar"))The .alias() call names the output column. This is how polars maps Stata's generate newvar = expr syntax.
generate ... if -- Conditional Creation
* Stata
generate category = "low" if income < 30000
replace category = "mid" if income >= 30000 & income < 80000
replace category = "high" if income >= 80000# polars
df = df.with_columns(
pl.when(pl.col("income") < 30000).then(pl.lit("low"))
.when(pl.col("income") < 80000).then(pl.lit("mid"))
.otherwise(pl.lit("high"))
.alias("category")
)Key behavioral difference: Stata's generate var = expr if condition setsunmet rows to . (missing). In polars, usepl.when(cond).then(expr).otherwise(None) to replicate this. Without the.otherwise() clause, polars sets unmet rows to null, which happens to matchStata's behavior -- but being explicit is safer.
replace -- Overwrite a Column
Unconditional replace:
* Stata
replace var = var + 1# polars
df = df.with_columns(pl.col("var") + 1)When the expression is derived from pl.col("var"), polars infers the output name automatically -- no .alias() needed.
Conditional replace:
* Stata
replace income = 0 if missing(income)# polars
df = df.with_columns(
pl.when(pl.col("income").is_null())
.then(pl.lit(0))
.otherwise(pl.col("income"))
.alias("income")
)Or more concisely using fill_null:
# polars -- simpler for the common "replace missing with value" case
df = df.with_columns(pl.col("income").fill_null(0))Key behavioral difference: Stata modifies the dataset in place. Polars returns
a new DataFrame (immutable). Always reassign: df = df.with_columns(...).Multiple columns in one call
* Stata
generate rate = count / total
generate pct = rate * 100# polars -- NOTE: columns created in the same with_columns cannot reference each other
df = df.with_columns(
(pl.col("count") / pl.col("total")).alias("rate"),
(pl.col("count") / pl.col("total") * 100).alias("pct"),
)Unlike Stata, polars with_columns does not see columns created earlier in the same call. Each expression sees the original DataFrame state. To use a derived column, chain a second with_columns:
# polars -- sequential dependency
df = df.with_columns(
(pl.col("count") / pl.col("total")).alias("rate"),
)
df = df.with_columns(
(pl.col("rate") * 100).alias("pct"),
)rename
* Stata
rename old new
rename (old1 old2) (new1 new2)# polars -- dictionary-based; can rename multiple at once
df = df.rename({"old": "new"})
df = df.rename({"old1": "new1", "old2": "new2"})Note the reversed key-value order compared to Stata: in polars, the dict key is the old name and the value is the new name.
Type casting
| Stata | polars | Notes |
|---|---|---|
destring var, replace | df = df.with_columns(pl.col("var").cast(pl.Float64)) | String to numeric |
destring var, replace force | df = df.with_columns(pl.col("var").cast(pl.Float64, strict=False)) | strict=False coerces invalid to null |
destring var, gen(numvar) | df = df.with_columns(pl.col("var").cast(pl.Float64).alias("numvar")) | New numeric column |
tostring var, replace | df = df.with_columns(pl.col("var").cast(pl.Utf8)) | Numeric to string |
tostring var, format(%9.2f) | df = df.with_columns(pl.col("var").round(2).cast(pl.Utf8)) | Formatted string |
Key behavioral difference: Stata's destring fails if non-numeric charactersare present unlessforceis specified (which sets those to.). Polars'
cast(pl.Float64, strict=False)sets unparseable values tonull.
Sources: Sullivan, "Stata to Python Equivalents" (danielmsullivan.com, accessed 2026-03-28); Turrell, "Coming from Stata" (aeturrell.github.io, accessed 2026-03-28); polars User Guide (docs.pola.rs, accessed 2026-03-28).
---
Section 3: Sample Selection
keep variables / drop variables -- Column Selection
* Stata
keep var1 var2 var3
keep varstem*
drop var1 var2
drop varstem*# polars
df = df.select("var1", "var2", "var3")
df = df.select(cs.starts_with("varstem")) # requires: import polars.selectors as cs
df = df.drop("var1", "var2")
df = df.select(~cs.starts_with("varstem")) # negate selector with ~Column selection by type
* Stata -- no built-in type selection; must use ds command
ds, has(type numeric)# polars -- column selectors by type
import polars.selectors as cs
df = df.select(cs.numeric()) # all numeric columns
df = df.select(cs.string()) # all string columns
df = df.select(cs.temporal()) # all date/time columnskeep if / drop if -- Row Selection
* Stata
keep if year == 2020
keep if enrollment > 500 & !missing(enrollment)
drop if missing(income)# polars
df = df.filter(pl.col("year") == 2020)
df = df.filter(pl.col("enrollment") > 500) # nulls excluded automatically
df = df.filter(pl.col("income").is_not_null())NEGATION TRAP: Stata'sdrop if conditionbecomesfilter(~condition)in
polars -- note the tilde (~) for negation:* Stata
drop if state == "PR"# polars
df = df.filter(pl.col("state") != "PR")
# or equivalently:
df = df.filter(~(pl.col("state") == "PR"))Membership filtering
* Stata
keep if inlist(state, "CA", "NY", "TX")
drop if inlist(race, 8, 9)# polars
df = df.filter(pl.col("state").is_in(["CA", "NY", "TX"]))
df = df.filter(~pl.col("race").is_in([8, 9]))Positional selection (in)
* Stata
keep in 1/10
keep in -5/l# polars
df = df.head(10) # first 10 rows
df = df.tail(5) # last 5 rows
df = df.slice(0, 10) # offset 0, length 10Note: Stata's in is 1-indexed; polars' slice is 0-indexed.
Compound conditions
* Stata
keep if age >= 18 & age <= 65 & !missing(income)# polars -- parentheses around each condition are mandatory (Python operator precedence)
df = df.filter(
(pl.col("age") >= 18) & (pl.col("age") <= 65) & pl.col("income").is_not_null()
)
# or using is_between:
df = df.filter(
pl.col("age").is_between(18, 65) & pl.col("income").is_not_null()
)Key behavioral difference -- the missing value trap in reverse:
In Stata, keep if income > 50000 INCLUDES rows where income is missing (because.sorts as +infinity, so. > 50000is TRUE). In polars,
df.filter(pl.col("income") > 50000) EXCLUDES null rows automatically. This isthe single most dangerous difference for Stata-to-Python transitions: the same
logical intent produces different row sets.
Sources: Sullivan, "Stata to Python Equivalents" (danielmsullivan.com, accessed 2026-03-28); pandas "Comparison with Stata" (pandas.pydata.org, accessed 2026-03-28); polars User Guide (docs.pola.rs, accessed 2026-03-28).
---
Section 4: Sorting
sort -- Ascending Sort
* Stata
sort var1 var2# polars
df = df.sort("var1", "var2")gsort -- Mixed Ascending/Descending
* Stata
gsort -var1 var2
gsort -var1 -var2# polars
df = df.sort("var1", "var2", descending=[True, False])
df = df.sort("var1", "var2", descending=True) # scalar True applies to allNull placement
* Stata -- missing values sort LAST (greater than all numeric values)
sort var1# polars -- nulls sort last by default (same as Stata)
df = df.sort("var1", nulls_last=True) # explicit (default behavior)
df = df.sort("var1", nulls_last=False) # nulls firstKey behavioral difference: Stata sorts in place. Polars returns a new
DataFrame; must reassign.
Sources: polars User Guide (docs.pola.rs, accessed 2026-03-28).
---
Section 5: Group Operations (by: prefix)
This is the most conceptually challenging section for Stata users. Stata's by: prefix maps to two completely different polars patterns, and choosing the wrong one produces silently incorrect results.
The Two Patterns
| Stata Pattern | Polars Pattern | Rows Preserved? | Use When |
|---|---|---|---|
bysort group: gen/egen | .over("group") inside with_columns | Yes (window function) | Adding a group stat to each row |
collapse ..., by(group) | .group_by("group").agg(...) | No (aggregation) | Reducing to one row per group |
Pattern 1: Window functions (preserves rows)
Stata's bysort group: gen newvar = expr and bysort group: egen newvar = func() create a new column where each row gets its group's value. In polars, this is .over("group"):
* Stata
bysort state: gen state_count = _N
bysort state: gen state_obs = _n
bysort state: egen mean_score = mean(test_score)
bysort state: egen total_enroll = total(enrollment)
bysort state: egen sd_score = sd(test_score)# polars -- window functions via .over()
df = df.with_columns(
pl.len().over("state").alias("state_count"), # _N
pl.col("test_score").cum_count().over("state").alias("state_obs"), # _n (0-based)
pl.col("test_score").mean().over("state").alias("mean_score"),
pl.col("enrollment").sum().over("state").alias("total_enroll"),
pl.col("test_score").std().over("state").alias("sd_score"),
)Pattern 2: Aggregation (reduces rows)
Stata's collapse reduces the dataset to one row per group. In polars, this is group_by().agg():
* Stata
collapse (mean) test_score (sum) enrollment (count) n=school_id, by(state)# polars
state_stats = df.group_by("state").agg(
pl.col("test_score").mean(),
pl.col("enrollment").sum(),
pl.col("school_id").count().alias("n"),
)Critical difference: Stata's collapse destroys the original data(replaces it with the collapsed version). Polars group_by().agg() returns anew DataFrame; the original is preserved.
The _n and _N system variables
Stata's _n (row number within group, 1-based) and _N (group size) are system variables available inside by: blocks. Polars equivalents:
| Stata | polars | Notes |
|---|---|---|
_N (group size) | pl.len().over("group") | |
_n (row number in group) | pl.col("x").cum_count().over("group") | 0-based in polars vs 1-based in Stata |
_n == 1 (first in group) | pl.int_range(pl.len()).over("group") == 0 | Or use group_by().first() |
_n == _N (last in group) | Manual: compare row index to group size | Or use group_by().last() |
* Stata -- keep first observation per group
bysort state (year): keep if _n == 1# polars
df = df.sort("year").group_by("state").first()* Stata -- keep last observation per group
bysort state (year): keep if _n == _N# polars
df = df.sort("year").group_by("state").last()Lag and lead within groups
Stata's subscript notation var[_n-1] accesses adjacent observations. Polars uses .shift():
* Stata
bysort state (year): gen lag_score = test_score[_n-1]
bysort state (year): gen lead_score = test_score[_n+1]
bysort state (year): gen change = test_score - test_score[_n-1]# polars -- MUST sort first; .shift() operates on current row order
df = df.sort("state", "year").with_columns(
pl.col("test_score").shift(1).over("state").alias("lag_score"),
pl.col("test_score").shift(-1).over("state").alias("lead_score"),
(pl.col("test_score") - pl.col("test_score").shift(1).over("state")).alias("change"),
)Key behavioral difference: Stata's bysort state (year): sorts within groupsautomatically before applying the subscript. Polars .shift() operates on whateverorder the data is in -- you must sort explicitly first. Forgetting to sort produces
silently wrong lag/lead values.
Multiple grouping variables
* Stata
bysort state district: egen district_mean = mean(test_score)# polars -- pass a list to .over()
df = df.with_columns(
pl.col("test_score").mean().over(["state", "district"]).alias("district_mean")
)Panel time-series operators (xtset context)
Stata's xtset declaration enables L., F., D. operators:
* Stata
xtset state_id year
gen lag_score = L.test_score
gen lead_score = F.test_score
gen diff_score = D.test_score# polars -- no declaration needed; explicit sort + shift
df = df.sort("state_id", "year").with_columns(
pl.col("test_score").shift(1).over("state_id").alias("lag_score"),
pl.col("test_score").shift(-1).over("state_id").alias("lead_score"),
(pl.col("test_score") - pl.col("test_score").shift(1).over("state_id"))
.alias("diff_score"),
)Sources: Sullivan, "Stata to Python Equivalents" (danielmsullivan.com, accessed 2026-03-28); Turrell, "Coming from Stata" (aeturrell.github.io, accessed 2026-03-28); polars User Guide, "Window functions" (docs.pola.rs, accessed 2026-03-28); Notre Dame Library, "by, _n, _N" (libguides.library.nd.edu, accessed 2026-03-28); Poverty Action, "Sort, by, bysort, egen" (povertyaction.github.io, accessed 2026-03-28).
---
Section 6: egen Functions
egen ("extensions to generate") provides group-aware and row-wise functions.
Group-level functions (with by:)
These attach a group statistic to every row in the group, preserving the original row count:
Stata egen | polars | Notes |
|---|---|---|
egen mean_x = mean(x), by(g) | pl.col("x").mean().over("g") | |
egen sum_x = sum(x), by(g) | pl.col("x").sum().over("g") | |
egen count_x = count(x), by(g) | pl.col("x").count().over("g") | Excludes nulls |
egen sd_x = sd(x), by(g) | pl.col("x").std().over("g") | polars uses N-1 denominator by default |
egen min_x = min(x), by(g) | pl.col("x").min().over("g") | |
egen max_x = max(x), by(g) | pl.col("x").max().over("g") | |
egen med_x = median(x), by(g) | pl.col("x").median().over("g") | |
egen total_x = total(x) | pl.col("x").sum() | No by() = full column |
egen pct_x = pctile(x), p(75) by(g) | pl.col("x").quantile(0.75).over("g") |
* Stata
bysort state: egen mean_score = mean(test_score)
bysort state: egen n_schools = count(school_id)# polars
df = df.with_columns(
pl.col("test_score").mean().over("state").alias("mean_score"),
pl.col("school_id").count().over("state").alias("n_schools"),
)egen group -- Numeric Group IDs
* Stata
egen state_id = group(state region)# polars -- approximate equivalent using rank on struct
df = df.with_columns(
pl.struct("state", "region").rank("dense").alias("state_id")
)
# More exact: create lookup table, then join
group_ids = (
df.select("state", "region").unique()
.sort("state", "region")
.with_row_index("state_id", offset=1)
)
df = df.join(group_ids, on=["state", "region"], how="left")egen tag -- First-Occurrence Indicator
* Stata
egen first_in_group = tag(state year)# polars -- 1 for first occurrence of each group, 0 otherwise
df = df.with_columns(
(pl.int_range(pl.len()).over("state", "year") == 0)
.cast(pl.Int32)
.alias("first_in_group")
)Row-wise functions (rowtotal, rowmean, etc.)
These operate across columns within a single row:
Stata egen | polars | Notes |
|---|---|---|
egen total = rowtotal(x1 x2 x3) | pl.sum_horizontal("x1", "x2", "x3") | TRAP: see below |
egen avg = rowmean(x1 x2 x3) | pl.mean_horizontal("x1", "x2", "x3") | |
egen lo = rowmin(x1 x2 x3) | pl.min_horizontal("x1", "x2", "x3") | |
egen hi = rowmax(x1 x2 x3) | pl.max_horizontal("x1", "x2", "x3") |
* Stata
egen total_income = rowtotal(wage salary bonus)# polars
df = df.with_columns(
pl.sum_horizontal("wage", "salary", "bonus").alias("total_income")
)THE rowtotal MISSING VALUE TRAP:
Stata'srowtotal()treats missing values as zero by default. Ifwageis
missing,rowtotal(wage salary)returns justsalary. Polars'sum_horizontal()
propagates null -- if any input is null, the result is null.
>
To replicate Stata behavior, fill nulls with 0 first:
>
```python
# polars -- replicate Stata rowtotal behavior (missing = 0)
df = df.with_columns(
pl.sum_horizontal(
pl.col("wage").fill_null(0),
pl.col("salary").fill_null(0),
pl.col("bonus").fill_null(0),
).alias("total_income")
)
```
>
Stata also has rowtotal(...), missing which returns missing if ALL values aremissing. The fill_null(0) approach always returns a number, so add a null checkif you need the "all-missing" behavior.
egen cut -- Quantile Binning
* Stata
egen quintile = cut(income), group(5)# polars
df = df.with_columns(
pl.col("income").qcut(5).alias("quintile")
)egen rank -- Within-Group Ranking
* Stata
bysort state: egen rank_score = rank(test_score)# polars
df = df.with_columns(
pl.col("test_score").rank(method="average").over("state").alias("rank_score")
)Polars rank methods: "average" (default), "min", "max", "dense", "ordinal".
Sources: Sullivan, "Stata to Python Equivalents" (danielmsullivan.com, accessed 2026-03-28); Stata egen manual (stata.com/manuals/degen.pdf); polars User Guide (docs.pola.rs, accessed 2026-03-28).
---
Section 7: Merging
merge -- Join Two Datasets
| Stata | polars | Notes |
|---|---|---|
merge 1:1 key using file2 | df1.join(df2, on="key", how="inner") | |
merge m:1 key using file2 | df1.join(df2, on="key", how="left") | Many-to-one: left join |
merge 1:m key using file2 | df1.join(df2, on="key", how="left") | One-to-many: left join |
merge m:m key using file2 | Avoid | Stata's m:m is rarely correct |
merge ..., keep(3) | df1.join(df2, on="key", how="inner") | _merge==3 = matched |
merge ..., keep(1 3) | df1.join(df2, on="key", how="left") | Master + matched |
* Stata
merge m:1 state_fips using "state_names.dta"
tab _merge
keep if _merge == 3
drop _merge# polars
df = df.join(state_names, on="state_fips", how="left")Multiple merge keys
* Stata
merge 1:1 state_id year using "panel_data.dta"# polars
df = df.join(panel_data, on=["state_id", "year"], how="inner")Different key names
* Stata -- not directly supported; rename first
rename stfips state_fips
merge m:1 state_fips using "states.dta"# polars -- left_on / right_on for different key names
df = df.join(states, left_on="stfips", right_on="state_fips", how="left")Replicating Stata's _merge diagnostic
Stata's merge creates a _merge variable (1=master only, 2=using only, 3=matched in both). Polars has no automatic equivalent. Replicate it manually:
# polars -- replicate Stata _merge indicator
df1 = df1.with_columns(pl.lit(True).alias("_in_master"))
df2 = df2.with_columns(pl.lit(True).alias("_in_using"))
merged = df1.join(df2, on="key", how="full", coalesce=True)
merged = merged.with_columns(
pl.when(pl.col("_in_master").is_not_null() & pl.col("_in_using").is_not_null())
.then(pl.lit(3))
.when(pl.col("_in_master").is_not_null())
.then(pl.lit(1))
.otherwise(pl.lit(2))
.alias("_merge")
)
# Tabulate the merge result
print(merged.group_by("_merge").len().sort("_merge"))Assert on merge results
* Stata
merge m:1 state_fips using "states.dta", assert(3) nogenerate# polars -- validate all rows matched
pre_count = df.height
df = df.join(states, on="state_fips", how="left")
unmatched = df.filter(pl.col("state_name").is_null()).height
assert unmatched == 0, f"{unmatched} rows failed to match on state_fips"
assert df.height == pre_count, f"Row count changed: {pre_count} -> {df.height}"Key behavioral differences:
- Stata requires the dataset to be sorted on merge keys. Polars does not.
- Stata merges one in-memory dataset with a file on disk. Polars merges two
in-memory DataFrames.
- Stata's _merge variable is automatic. Polars requires explicit diagnostics.- Suffix handling: Stata appends nothing (requires unique names). Polars adds a
configurable suffix (suffix="_right" by default) to duplicate non-key columns.Join types (complete mapping)
| Stata equivalent | polars how= | Description |
|---|---|---|
merge ..., keep(3) | "inner" | Only matched rows |
merge ..., keep(1 3) | "left" | All from left + matched from right |
merge ..., keep(2 3) | "right" | All from right + matched from left |
merge ..., keep(1 2 3) | "full" | All rows from both sides |
| (no Stata equivalent) | "anti" | Rows in left NOT in right |
| (no Stata equivalent) | "semi" | Rows in left that HAVE a match in right |
| (no Stata equivalent) | "cross" | Cartesian product (every row x every row) |
Sources: Sullivan, "Stata to Python Equivalents" (danielmsullivan.com, accessed 2026-03-28); pandas "Comparison with Stata" (pandas.pydata.org, accessed 2026-03-28); polars User Guide, "Joins" (docs.pola.rs, accessed 2026-03-28).
---
Section 8: Appending
append -- Stack Datasets Vertically
* Stata
use "file1.dta", clear
append using "file2.dta"
append using "file3.dta"# polars
df = pl.concat([df1, df2])
df = pl.concat([df1, df2, df3])Handling different column sets
* Stata -- append fills missing columns with .
append using "file2.dta"# polars -- use how="diagonal" for union-style concatenation (fills missing with null)
df = pl.concat([df1, df2], how="diagonal")The default how="vertical" in polars requires identical schemas (same columns in same order). Use how="diagonal" when the DataFrames have different column sets -- it creates the union of all columns and fills missing values with null.
# polars -- strict vertical (schemas must match)
df = pl.concat([df1, df2], how="vertical")
# polars -- relaxed (different column sets, same as Stata append)
df = pl.concat([df1, df2], how="diagonal")Horizontal concatenation (no Stata equivalent)
# polars -- side-by-side concatenation (column-bind)
df = pl.concat([df1, df2], how="horizontal")Sources: polars User Guide (docs.pola.rs, accessed 2026-03-28).
---
Section 9: Reshaping
reshape long -- Wide to Long
* Stata
* Data has: id income2018 income2019 income2020
reshape long income, i(id) j(year)
* Now has: id year income# polars -- unpivot (formerly melt)
income_cols = [c for c in df.columns if c.startswith("income")]
df_long = df.unpivot(
on=income_cols,
index="id",
variable_name="year",
value_name="income",
)
# Clean the year column: "income2018" -> 2018
df_long = df_long.with_columns(
pl.col("year").str.replace("income", "").cast(pl.Int32)
)Using column selectors for wide columns:
# polars -- selector-based
import polars.selectors as cs
df_long = df.unpivot(
on=cs.starts_with("income"),
index="id",
variable_name="year",
value_name="income",
)reshape wide -- Long to Wide
* Stata
* Data has: id year income
reshape wide income, i(id) j(year)
* Now has: id income2018 income2019 income2020# polars -- pivot
df_wide = df.pivot(on="year", index="id", values="income")
# Column names will be the year values (2018, 2019, 2020)
# Optionally prefix them:
df_wide = df_wide.rename(
{c: f"income{c}" for c in df_wide.columns if c != "id"}
)Pivot with aggregation
When duplicate index-on combinations exist, polars requires an aggregation function:
# polars -- pivot with aggregation
df_wide = df.pivot(
on="product", index="date", values="sales",
aggregate_function="sum",
)Key behavioral differences:
- Stata's reshape modifies data in place and remembers the reshape spec forreversal (reshape long<->reshape wide). Polarsunpivot/pivotreturn
new DataFrames with no memory of the previous shape.
- Polars pivot() is available only in eager mode.- Stata auto-names stub columns (e.g., income2018). Polars names pivot columns from the on column values; stub prefixes must be added manually.Sources: polars User Guide, "Pivots" (docs.pola.rs, accessed 2026-03-28); pandas "Comparison with Stata" (pandas.pydata.org, accessed 2026-03-28); QuantEcon Statistics Cheatsheet (cheatsheets.quantecon.org, accessed 2026-03-28).
---
Section 10: Aggregation (collapse)
Basic collapse
* Stata
collapse (mean) test_score (sum) enrollment, by(state)# polars
state_stats = df.group_by("state").agg(
pl.col("test_score").mean(),
pl.col("enrollment").sum(),
)Multiple statistics
* Stata
collapse (mean) avg_score=test_score (sd) sd_score=test_score ///
(min) min_score=test_score (max) max_score=test_score ///
(count) n=test_score, by(state)# polars
state_stats = df.group_by("state").agg(
pl.col("test_score").mean().alias("avg_score"),
pl.col("test_score").std().alias("sd_score"),
pl.col("test_score").min().alias("min_score"),
pl.col("test_score").max().alias("max_score"),
pl.col("test_score").count().alias("n"),
)Percentile aggregation
* Stata
collapse (p25) p25_income=income (p50) p50_income=income (p75) p75_income=income, by(state)# polars
state_income = df.group_by("state").agg(
pl.col("income").quantile(0.25).alias("p25_income"),
pl.col("income").quantile(0.50).alias("p50_income"),
pl.col("income").quantile(0.75).alias("p75_income"),
)Common aggregation function mapping
| Stata collapse stat | polars expression | Notes |
|---|---|---|
(mean) x | pl.col("x").mean() | |
(sum) x | pl.col("x").sum() | |
(count) x | pl.col("x").count() | Excludes nulls |
(sd) x | pl.col("x").std() | N-1 denominator (ddof=1) by default |
(min) x | pl.col("x").min() | |
(max) x | pl.col("x").max() | |
(median) x | pl.col("x").median() | |
(first) x | pl.col("x").first() | |
(last) x | pl.col("x").last() | |
(p25) x | pl.col("x").quantile(0.25) | |
(p75) x | pl.col("x").quantile(0.75) |
collapse without by() -- Full-column aggregation
* Stata
collapse (mean) test_score enrollment
* Result: one row with the overall means# polars -- two approaches
# Approach 1: select with aggregations
result = df.select(
pl.col("test_score").mean(),
pl.col("enrollment").mean(),
)
# Approach 2: using describe for a quick summary
df.select("test_score", "enrollment").describe()Counting observations
* Stata
collapse (count) n=school_id, by(state)
* Stata -- tabulate (interactive)
tab state# polars -- group count
df.group_by("state").len()
df.group_by("state").len().sort("len", descending=True)Key behavioral differences:
- Stata'scollapsedestroys the original data. Polarsgroup_by().agg()
returns a new DataFrame; the original is preserved.
- Stata'scollapsewithoutby()produces a single row. Polars:
df.select(pl.col("x").mean())ordf.agg(pl.col("x").mean()).
- Naming: Stata keeps the original variable name by default. Polars also keeps the
original column name but can be renamed with .alias().Sources: Sullivan, "Stata to Python Equivalents" (danielmsullivan.com, accessed 2026-03-28); Turrell, "Coming from Stata" (aeturrell.github.io, accessed 2026-03-28).
---
Section 11: Duplicate Management
duplicates report
* Stata
duplicates report
duplicates report state year# polars -- overall duplicate check
print(f"Total rows: {df.height}")
print(f"Unique rows: {df.unique().height}")
print(f"Duplicate rows: {df.height - df.unique().height}")
# polars -- check uniqueness of specific columns
key_dupes = df.group_by("state", "year").len().filter(pl.col("len") > 1)
print(f"Duplicate key combinations: {key_dupes.height}")
print(key_dupes.sort("len", descending=True).head())duplicates tag
* Stata
duplicates tag state year, gen(dup_count)# polars -- count occurrences of each key combination, broadcast to rows
df = df.with_columns(
(pl.len().over("state", "year") - 1).alias("dup_count")
)
# dup_count = 0 means unique; > 0 means duplicated (matches Stata convention)duplicates drop
* Stata
duplicates drop
duplicates drop state year, force# polars
df = df.unique() # drop fully identical rows
df = df.unique(subset=["state", "year"]) # drop by key columns (keeps first)
df = df.unique(subset=["state", "year"], keep="first") # explicit
df = df.unique(subset=["state", "year"], keep="last") # keep last insteadis_duplicated / is_unique
# polars -- flag rows (useful for inspection before dropping)
df = df.with_columns(
pl.struct("state", "year").is_duplicated().alias("is_dup"),
pl.struct("state", "year").is_unique().alias("is_unique"),
)
# Inspect the duplicates
print(df.filter(pl.col("is_dup")).sort("state", "year"))Sources: polars User Guide (docs.pola.rs, accessed 2026-03-28).
---
Section 12: Missing Value Operations
The Missing Value Model
| Concept | Stata | polars |
|---|---|---|
| Standard missing | . (system missing) | null |
| Extended missing | .a through .z (26 types) | No equivalent (use separate column) |
| Not-a-number | Not a separate concept | NaN (distinct from null in float columns) |
| Test for missing | missing(var) | pl.col("var").is_null() |
| Missing in comparisons | . sorts as +infinity (> 100 is TRUE) | null excluded from comparisons |
| Missing in aggregations | Excluded by default | null excluded; NaN propagates |
mvdecode -- Convert Sentinel Values to Missing
* Stata
mvdecode income, mv(-9 -99 = .)
mvdecode income, mv(-9 = .r \ -99 = .d)# polars -- convert sentinel values to null
df = df.with_columns(
pl.when(pl.col("income").is_in([-9, -99]))
.then(None)
.otherwise(pl.col("income"))
.alias("income")
)For multiple sentinel patterns across many columns:
# polars -- sentinel replacement across multiple columns
sentinel_vals = [-9, -99, -999]
numeric_cols = df.select(cs.numeric()).columns
df = df.with_columns(
pl.when(pl.col(col).is_in(sentinel_vals))
.then(None)
.otherwise(pl.col(col))
.alias(col)
for col in numeric_cols
)mvencode -- Fill Missing Values
* Stata
mvencode income, mv(0)
replace income = 0 if missing(income)# polars
df = df.with_columns(pl.col("income").fill_null(0))misstable summarize -- Missing Value Report
* Stata
misstable summarize
misstable patterns# polars -- missing value summary
null_counts = df.null_count()
print(null_counts)
# Detailed missing report
for col in df.columns:
n_null = df.select(pl.col(col).is_null().sum()).item()
pct = n_null / df.height * 100
if n_null > 0:
print(f"{col}: {n_null} missing ({pct:.1f}%)")drop if missing / keep if not missing
* Stata
drop if missing(income)
drop if missing(income) | missing(age)# polars
df = df.filter(pl.col("income").is_not_null())
df = df.drop_nulls(subset=["income"]) # equivalent
df = df.drop_nulls(subset=["income", "age"]) # drop if EITHER is null
df = df.drop_nulls() # drop if ANY column is nullForward-fill and backward-fill
* Stata -- requires carryforward (community package)
bysort id (time): carryforward var, replace# polars
df = df.with_columns(pl.col("var").forward_fill())
df = df.with_columns(pl.col("var").backward_fill())
# Within groups:
df = df.sort("id", "time").with_columns(
pl.col("var").forward_fill().over("id")
)The NaN vs null trap
When reading Stata .dta files via pandas, Stata's . (missing) arrives as NaN in float columns. In polars, NaN and null are different:
# NaN is NOT null -- this is a common trap
df = pl.DataFrame({"x": [1.0, float("nan"), None]})
df.select(pl.col("x").is_null()) # [False, False, True]
df.select(pl.col("x").is_nan()) # [False, True, False]
df.select(pl.col("x").mean()) # NaN (NaN propagates!)
# Safe pattern: convert NaN to null first
df = df.with_columns(pl.col("x").fill_nan(None))
df.select(pl.col("x").mean()) # 1.0 (correct)THE MISSING-AS-INFINITY TRAP:
>
This is the single most dangerous Stata-to-Python difference. In Stata:
```stata
* Stata: THIS INCLUDES MISSING OBSERVATIONS
count if income > 50000
keep if var < threshold / SAFE: missing excluded because . > threshold /
```
>
In polars:
```python
# polars: nulls are EXCLUDED from comparisons (safe by default)
df.filter(pl.col("income") > 50000) # null rows excluded
```
>
The risk reverses: Stata users learn to add & !missing(x) defensively. Inpolars, this is unnecessary -- but if you're writing Stata code after working in
Python, you must remember that Stata's behavior goes the other way.
Extended missing values (.a through .z)
Stata's 27 missing types have no Python equivalent. When reading .dta files:
# pyreadstat preserves extended missing types
import pyreadstat
df_pd, meta = pyreadstat.read_dta("data.dta", user_missing=True)
# Extended missing values are preserved as special NaN values
# Access via meta.missing_ranges
# Strategy: create a separate "reason for missing" column
# before converting to polars nullSources: pandas "Working with missing data" (pandas.pydata.org, accessed 2026-03-28); polars documentation, "Handling Missing Values" (docs.pola.rs, accessed 2026-03-28); StataCorp, "Missing values" (stata.com/manuals/dmissingvalues.pdf); pyreadstat (github.com/Roche/pyreadstat).
---
Quick Reference Table
A condensed lookup for the most common data management translations:
| Stata | polars | Category |
|---|---|---|
use myfile.dta | pl.read_parquet("myfile.parquet") | I/O |
save myfile.dta, replace | df.write_parquet("myfile.parquet") | I/O |
generate newvar = expr | .with_columns(expr.alias("newvar")) | Create |
replace var = expr if cond | .with_columns(pl.when(cond).then(expr).otherwise(pl.col("var"))) | Modify |
rename old new | .rename({"old": "new"}) | Rename |
keep var1 var2 | .select("var1", "var2") | Columns |
drop var1 var2 | .drop("var1", "var2") | Columns |
keep if condition | .filter(condition) | Rows |
drop if condition | .filter(~condition) | Rows |
sort var1 var2 | .sort("var1", "var2") | Sort |
gsort -var1 var2 | .sort("var1", "var2", descending=[True, False]) | Sort |
bysort g: egen m = mean(x) | .with_columns(pl.col("x").mean().over("g")) | Window |
collapse (mean) x, by(g) | .group_by("g").agg(pl.col("x").mean()) | Aggregate |
merge m:1 key using file | .join(df2, on="key", how="left") | Join |
append using file2 | pl.concat([df1, df2]) | Stack |
reshape long stub, i(id) j(t) | .unpivot(on=cols, index="id") | Reshape |
reshape wide stub, i(id) j(t) | .pivot(on="t", index="id", values="stub") | Reshape |
duplicates drop key, force | .unique(subset=["key"]) | Dedup |
drop if missing(var) | .filter(pl.col("var").is_not_null()) | Missing |
mvdecode var, mv(-9) | pl.when(pl.col("var") == -9).then(None)... | Missing |
destring var, replace | .with_columns(pl.col("var").cast(pl.Float64)) | Type |
tostring var, replace | .with_columns(pl.col("var").cast(pl.Utf8)) | Type |
External Resources for Stata-to-Python Translation
Curated catalog of resources for Stata users transitioning to Python in quantitative social science contexts. Each entry is self-contained with provenance tracking, quality assessment, and key takeaways so that the resource's value can be judged without visiting the link.
Resources are assessed for currency, accuracy, and relevance to the DAAF stack (polars, pyfixest, statsmodels, linearmodels, plotnine, svy, geopandas). Entries marked with currency concerns should be cross-checked against current documentation.
Contents
- Package-Specific Documentation
- Textbooks with Dual-Language Code
- General Stata-to-Python Guides
- Social Science Methodology Resources
- Community Resources
- Stata Official Documentation (for Reference)
---
Package-Specific Documentation
pyfixest Documentation
- Author(s): Alexander Fischer, Styfen Schaer, and contributors
- URL: https://pyfixest.org/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- actively maintained, aligned with fixest/reghdfe
- Key content: Python implementation of R's fixest, which itself was inspired
by Stata's reghdfe. Supports OLS, WLS, IV, Poisson, and GLMs with multi-way high-dimensional fixed effects. Formula syntax: Y ~ X1 + X2 | fe1 + fe2 for FE absorption, Y ~ X1 | fe1 | endo ~ instrument for IV. Inference: iid, HC1-3, CRV1/CRV3 clustering, wild bootstrap, randomization inference. Includes DiD estimators: TWFE, did2s, lpdid, Sun-Abraham. The quickstart page explicitly references both Stata reghdfe syntax and R fixest syntax for comparison.
- Relevance to DAAF: High -- primary regression package in the DAAF stack.
The formula syntax deliberately mirrors reghdfe conventions, making it the most natural bridge for Stata users. etable() replaces esttab/outreg2.
- Strengths: Near-identical formula syntax to Stata's reghdfe; includes
multiple estimation shortcuts (sw(), csw(), split); publication-quality tables via etable() with LaTeX, markdown, and HTML output.
- Limitations:
feglm()does not yet support fixed effects absorption (the
single largest feature gap vs Stata). Some fixest R features not yet ported. See the DAAF pyfixest skill's gotchas.md for the full gap list.
statsmodels Documentation
- Author(s): Josef Perktold, Skipper Seabold, Jonathan Taylor, and contributors
- URL: https://www.statsmodels.org/stable/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: None -- v0.14.6 stable
- Key content: Comprehensive statistical modeling library covering OLS, WLS,
GLS, GLM (logit, probit, Poisson, negative binomial), discrete choice models (ordered logit/probit, multinomial logit), time series, mixed effects, and hypothesis testing. For Stata users: the formula API (smf.ols("y ~ x1 + x2", data=df)) is intentionally modeled on R/Stata formula syntax. Important gotcha: statsmodels does NOT include an intercept by default (must use sm.add_constant() or the formula API with smf).
- Relevance to DAAF: High -- core dependency for models beyond pyfixest's
scope, especially GLMs, discrete choice, time series, and mixed effects.
- Strengths: Formula API makes translation from Stata straightforward.
Extensive diagnostic methods (influence plots, residual tests, specification tests) mirror what Stata users expect from post-estimation.
- Limitations: Documentation can be dense. The two-step pattern (specify
model, then .fit()) differs from Stata's single-call approach. No built-in high-dimensional FE support -- use pyfixest for that.
marginaleffects: Model to Meaning
- Author(s): Vincent Arel-Bundock, Noah Greifer, Andrew Heiss
- URL: https://marginaleffects.com/
- Type: Documentation / Book
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- actively maintained with bilingual R/Python support
- Key content: Unified interface for post-estimation analysis, directly
corresponding to Stata's margins command. Core functions: predictions(), comparisons() (contrasts, risk ratios, odds), slopes() (marginal effects), hypotheses() (test, lincom, nlcom equivalents). Every documentation page has an R/Python toggle showing equivalent code. Published in the Journal of Statistical Software (v111, i09). Explicitly validated against Stata output.
- Relevance to DAAF: High -- Stata's
marginscommand is one of the most
heavily used post-estimation tools. This package is the direct Python equivalent, covering margins, marginsplot, lincom, and nlcom.
- Strengths: The bilingual R/Python toggle is the gold standard for
cross-language translation. Covers causal inference, experiments, categorical outcomes, and ML interpretation. Author royalties support charity.
- Limitations: Described by the author as alpha for the Python version.
Python API coverage lags slightly behind R. Verify critical results against Stata for published research.
plotnine Documentation
- Author(s): Hassan Kibirige
- URL: https://plotnine.org/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: None -- v0.15.3 as of verification date
- Key content: Python implementation of the grammar of graphics with syntax
mirroring R's ggplot2. For Stata users, this represents a paradigm shift from Stata's imperative graph twoway syntax to a declarative, layered approach. Covers geoms, aesthetics, scales, facets, coordinates, and themes.
- Relevance to DAAF: High -- DAAF's primary static visualization library.
Stata users should read this alongside the skill's visualization.md reference, which provides direct Stata-to-plotnine command mappings.
- Strengths: Comprehensive geom coverage; grammar-of-graphics composability
exceeds Stata's graph system in flexibility. Plot composition operators (|, /) available in v0.15+.
- Limitations: Requires pandas DataFrames (convert from polars). Some R
ggplot2 extensions (e.g., ggrepel) have no plotnine equivalent. Stata users unfamiliar with the grammar of graphics face a learning curve.
svy: Complex Survey Analysis in Python
- Author(s): svy development team
- URL: https://svylab.com/docs/svy/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- actively maintained; supersedes samplics
- Key content: Python equivalent of Stata's
svy:command prefix. Design
specification via svy.Design(stratum=..., psu=..., wgt=...), directly paralleling Stata's svyset. Supports Taylor linearization, BRR, jackknife, and bootstrap variance estimation. Estimation: means, totals, proportions, ratios, medians, and regression models. Validated to be numerically equivalent to R's survey package.
- Relevance to DAAF: High -- primary complex survey analysis library. Direct
mapping to Stata's svyset and svy: prefix commands makes it the natural translation path for Stata users working with NHANES, DHS, BRFSS, ACS, and similar survey data.
- Strengths: Design-based inference with proper variance estimation. API
conceptually parallels Stata's svy workflow.
- Limitations: Narrower model family coverage than Stata's
svy:(no ordinal
logistic, Cox survival, negative binomial). For unsupported models, use the rpy2 bridge to R's survey package.
Polars: Coming from Pandas
- Author(s): Polars contributors
- URL: https://docs.pola.rs/user-guide/migration/pandas/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- maintained as part of official polars docs
- Key content: Official migration guide covering the seven fundamental
conceptual differences between pandas and polars: no index, Arrow memory format, parallelism, multiple engines, lazy evaluation, strict typing, and expression-based API. Useful for Stata users because most external Stata-to-Python resources target pandas, not polars -- this guide helps translate from the pandas code you find online to the polars code DAAF expects.
- Relevance to DAAF: Medium -- pandas-to-polars, not Stata-to-polars
directly. Valuable as a secondary translation step when Stata users find pandas examples online and need to convert to polars.
- Strengths: Authoritative, emphasizes avoiding pandas-style patterns.
- Limitations: Assumes pandas familiarity, not Stata. Stata users should use
the DAAF skill's data-management.md reference first.
rdrobust: RD Packages
- Author(s): Sebastian Calonico, Matias Cattaneo, Rocio Titiunik, and others
- URL: https://rdpackages.github.io/rdrobust/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- maintained across R, Python, and Stata
- Key content: Unified documentation hub for the rdrobust family providing
local polynomial RD estimation, bandwidth selection (rdbwselect), and RD plots (rdplot). Available on SSC (Stata), CRAN (R), and PyPI (Python) with identical APIs and function names. Academic references (Calonico, Cattaneo, Titiunik 2014, 2015) provide rigorous methodological grounding.
- Relevance to DAAF: High -- same function names, same arguments, same output
across all three languages. The Stata-to-Python translation is mechanical.
- Strengths: Truly parallel implementation across Stata, R, and Python.
Actively maintained by the original methodological authors.
- Limitations: Python documentation is sparser than Stata's manual entries.
Consult the Stata help file for detailed parameter explanations, then apply directly to the Python version.
binsreg: Binscatter Regressions
- Author(s): Matias D. Cattaneo, Richard K. Crump, Max H. Farrell, Yingjie Feng
- URL: https://nppackages.github.io/binsreg/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- maintained across Stata, R, and Python
- Key content: Identical methodology in all three languages. Includes
binsreg (least squares binscatter), binslogit/binsprobit (nonlinear), binstest (hypothesis testing), binspwc (pairwise comparison), and binsregselect (bin selection). Directly replaces Stata's older binscatter command with improved methodology and inference.
- Relevance to DAAF: High -- binscatter is a standard visualization in
applied economics. Identical API across Stata and Python means near-zero translation friction.
- Strengths: Same authors, same methodology, same API across all languages.
Proper inference (confidence intervals, hypothesis testing) unlike the original binscatter Stata command.
- Limitations: Does not directly support
absorb()for fixed effects in the
Python version. Residualize manually using pyfixest before passing to binsreg.
---
Textbooks with Dual-Language Code
The Effect: An Introduction to Research Design and Causality
- Author(s): Nick Huntington-Klein
- URL: https://theeffectbook.net/
- Type: Book (free online)
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- 2nd edition available; code in R, Stata, and Python
- Key content: Causal inference textbook covering research design, DAGs,
matching, regression, instrumental variables, regression discontinuity, difference-in-differences, and event studies. All methods chapters include code in R, Stata, and Python using the causaldata package (available via pip install causaldata). 60+ video lectures accompany the text.
- Relevance to DAAF: High -- the trilingual code examples are the gold
standard for seeing how the same causal inference method is implemented across ecosystems. Stata users can look up any method and see the Python equivalent side-by-side.
- Strengths: Free online. Triple-language code. Exceptionally clear
conceptual explanations. The causaldata package enables hands-on practice.
- Limitations: Python examples use pandas rather than polars. Translation to
the DAAF polars stack requires an additional step, but the methodology and logic transfer directly.
Causal Inference: The Mixtape
- Author(s): Scott Cunningham
- URL: https://mixtape.scunning.com/
- Type: Book (free online)
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: Minor -- official code is R and Stata only; Python
translations are community-maintained
- Key content: Causal inference textbook covering potential outcomes, matching,
instrumental variables, regression discontinuity, difference-in-differences, and synthetic control. The official book uses R and Stata code. Community-contributed Python Jupyter notebooks replicate all examples (https://github.com/alexanderthclark/Causal-Inference-Mixtape).
- Relevance to DAAF: High -- alongside The Effect, one of the two most popular
causal inference textbooks in economics. The Stata code in the book maps directly to Python equivalents documented in the DAAF skill.
- Strengths: Accessible writing with real-world examples. The
causaldata
package provides datasets in Python. Companion Mixtape Sessions workshops (mixtapesessions.io) offer Python implementations.
- Limitations: Official book does not include Python code. Community
translations may not be fully maintained. Prefer "The Effect" for native three-language support.
Data Analysis for Business, Economics, and Policy
- Author(s): Gabor Bekes and Gabor Kezdi
- URL: https://gabors-data-analysis.com/
- Type: Textbook (code freely available)
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: Low -- published 2021 by Cambridge University Press; code
repository actively maintained
- Key content: Comprehensive data analysis textbook with all code freely
available in R, Stata, and Python via GitHub (github.com/gabors-data-analysis/da_case_studies). Covers data exploration, regression analysis, predictive analytics (cross-validation, tree-based ML), and causal analysis. Each case study has parallel implementations in all three languages.
- Relevance to DAAF: High -- the three-language parallel code is ideal for
building Stata-to-Python translation intuition. Case studies drawn from real-world economics and policy contexts.
- Strengths: 47 case studies with parallel R/Stata/Python code. Teaching
guide available. Published by a major academic press.
- Limitations: Python examples use pandas and statsmodels, not polars or
pyfixest. Print edition required for text; code is free on GitHub.
Using R, Python, and Julia for Introductory Econometrics
- Author(s): Florian Heiss, Daniel Brunner
- URL: https://www.urfie.net/
- Type: Book (free PDF available)
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: Minor -- examples use standard econometrics libraries
- Key content: Companion to Wooldridge's Introductory Econometrics (the most
widely assigned Stata-based econometrics textbook). Chapters mirror Wooldridge's structure covering regression, inference, heteroskedasticity, time series, panel data, IV/2SLS, and limited dependent variables. Available in R, Python, and Julia versions.
- Relevance to DAAF: High -- since Wooldridge is the canonical Stata-based
econometrics textbook, this companion directly bridges every Wooldridge example to Python. Invaluable for graduate students making the transition.
- Strengths: Every example exists in multiple languages with identical data and
expected results. Perfect for verifying that a Python translation produces the same numbers as the Stata original. Free PDF version available.
- Limitations: Python examples use pandas and statsmodels, not polars or
pyfixest. May lag behind latest library versions.
Causal Inference for the Brave and True
- Author(s): Matheus Facure
- URL: https://matheusfacure.github.io/python-causality-handbook/
- Type: Online handbook / textbook (Python-only)
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: Low -- actively maintained
- Key content: Python-focused causal inference resource covering RCTs, linear
regression, propensity score matching, DiD (including modern heterogeneous treatment effects methods), synthetic control, and synthetic DiD. Particularly strong chapter on "The Difference-in-Differences Saga" covering Callaway-Sant'Anna, Sun-Abraham, and imputation approaches.
- Relevance to DAAF: Medium -- Python-only (no Stata code), but excellent for
Stata users who need to understand how familiar causal inference methods are implemented in Python. Free online and also published by O'Reilly.
- Strengths: Modern methods coverage (heterogeneous DiD) surpasses most
textbooks. Clear Python implementations. Free online version.
- Limitations: No Stata code for direct comparison. Uses pandas.
Tidy Finance with Python
- Author(s): Christoph Scheuch, Stefan Voigt, Patrick Weiss
- URL: https://www.tidy-finance.org/python/
- Type: Online textbook
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: Low -- uses pyfixest, actively maintained
- Key content: Finance-focused Python textbook with chapters on fixed effects,
clustered standard errors, difference-in-differences, and asset pricing. Demonstrates pyfixest alongside pandas. The fixed effects chapter explicitly shows the equivalence between Stata's areg/reghdfe and pyfixest's feols().
- Relevance to DAAF: Medium -- finance focus limits direct applicability to
social science, but econometric techniques (FE, clustering, DiD) are identical.
- Strengths: Uses pyfixest (same as DAAF). Clear practical examples.
- Limitations: Finance-specific examples and datasets.
---
General Stata-to-Python Guides
Daniel M. Sullivan: Stata to Python Equivalents
- Author(s): Daniel M. Sullivan
- URL: https://www.danielmsullivan.com/pages/tutorial_stata_to_python.html
- Type: Guide / Reference
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: Moderate -- references
econtoolsfor regression (now
dated; pyfixest is the modern equivalent); pandas API stable but some methods deprecated. Core data manipulation mappings remain accurate.
- Key content: The single most comprehensive Stata-to-Python command mapping
online. Covers 13 sections: Input/Output, Sample Selection, Data Info, Variable Manipulation, Bysort, Panel Data, Merging and Joining, Reshape, Econometrics, Plotting, and Other Differences. Maps 80+ Stata commands to pandas equivalents. Includes critical notes on missing value behavior and composability differences.
- Relevance to DAAF: High -- directly maps the Stata commands social scientists
use daily. While it targets pandas (not polars), the conceptual mappings are foundational and the Stata command inventory ensures complete coverage.
- Strengths: Most comprehensive single-page Stata-to-Python reference. Covers
edge cases and behavioral differences that other guides miss. Free.
- Limitations: Uses pandas exclusively (DAAF uses polars). The econtools
regression package is outdated -- use pyfixest instead.
Coding for Economists: Coming from Stata
- Author(s): Arthur Turrell and contributors
- URL: https://aeturrell.github.io/coding-for-economists/coming-from-stata.html
- Type: Guide (chapter in free online book)
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- actively maintained
- Key content: Dedicated chapter for Stata users transitioning to Python for
economics work. Detailed package equivalency table mapping Stata packages to Python counterparts. Side-by-side code comparisons for common operations. Supplementary regression reference table covering fixed effects, categorical variables, interaction terms, robust/clustered standard errors, and IV. Recommends polars as the dplyr-like option.
- Relevance to DAAF: High -- uses the same Python stack as DAAF (pyfixest,
statsmodels, binsreg, plotnine). The broader book covers the full Python data analysis workflow.
- Strengths: Written for economists, not generic data scientists. Covers the
exact package stack relevant to DAAF. Actively maintained.
- Limitations: The broader book leans toward pandas in some chapters, though
the "Coming from Stata" section correctly identifies polars as dplyr-like.
pandas Official Documentation: Comparison with Stata
- Author(s): pandas development team
- URL: https://pandas.pydata.org/docs/getting_started/comparison/comparison_with_stata.html
- Type: Official documentation
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- updated with each pandas release
- Key content: Official pandas page mapping Stata concepts across 8 sections:
Data Structures, I/O, Data Operations, String Processing, Merging, Missing Data, GroupBy, and Other Considerations. Detailed examples for collapse/groupby, egen/transform, merge/pd.merge(). Documents zero-based indexing, copies vs. in-place operations, and NaN vs. dot missing values.
- Relevance to DAAF: High -- authoritative source for core data manipulation
mappings. While DAAF uses polars, the conceptual mappings are foundational.
- Strengths: Authoritative, comprehensive, well-maintained. The standard
reference that most Stata-to-Python guides build upon.
- Limitations: pandas-only (DAAF uses polars). Stata users should use this
for conceptual understanding, then translate to polars using the DAAF skill.
UC Berkeley Econ 148: Python for Economists
- Author(s): Rohan Jha
- URL: https://www.econ148.org/textbook/content/01-python_v_stata/index.html
- Type: University course textbook
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: Low -- copyright 2024
- Key content: UC Berkeley course textbook with a dedicated "Python vs Stata"
section covering pedagogy, language history, summary of differences, and syntax translation. Emphasizes that economists spend ~1/3 of their time getting and cleaning data, motivating Python's data engineering capabilities.
- Relevance to DAAF: Medium -- useful for high-level conceptual framing and
motivational context, though less detailed than Sullivan or Turrell for specific command mappings.
- Strengths: Academic context. Modern (2024). Good motivational framing.
- Limitations: Less detailed command mappings than Sullivan or Turrell.
QuantEcon Statistics Cheatsheet
- Author(s): Thomas J. Sargent, John Stachurski, and contributors
- URL: https://cheatsheets.quantecon.org/stats-cheatsheet.html
- Type: Cheat sheet / Quick reference
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: Low -- core operations are stable
- Key content: Three-column crosswalk (Stata, Pandas, Base R) covering ~29
statistical operations organized into Basics, Filtering, Summarizing, Reshaping, Merging, and Plotting.
- Relevance to DAAF: Medium -- useful as a quick-reference companion but too
abbreviated for in-depth translation. Good for quick lookups.
- Strengths: Clean three-language side-by-side format. Quick to scan.
- Limitations: Too abbreviated for learning. Covers only common operations.
Adam Ross Nelson: StataQuickReference Crosswalk
- Author(s): Adam Ross Nelson
- URL: https://github.com/adamrossnelson/StataQuickReference/blob/master/spcrosswlk.md
- Type: GitHub repository / Reference document
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: Moderate -- repository activity unclear; core API stable
- Key content: Comprehensive crosswalk covering 7 sections: Starting Out,
Categorical/Factor Variables, Merge, Append, Reshape, Loops, and Exporting. Includes notes on unicode handling and data type conversion for .dta files. The loop translation section (foreach/forvalues to Python for) is useful.
- Relevance to DAAF: Medium -- pandas-focused, but the Stata command inventory
helps ensure complete coverage.
- Strengths: Covers loop patterns and .dta export details often missing from
other guides. Community-contributed.
- Limitations: pandas-only. Repository maintenance status unclear.
---
Social Science Methodology Resources
Nick Huntington-Klein's Econometrics Resources
- Author(s): Nick Huntington-Klein
- URL: https://nickchk.com/econometrics.html
- Type: Resource hub
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- regularly updated
- Key content: Curated collection of econometrics learning materials including
data manipulation tutorials, data access packages, animated causal inference visualizations, video lecture series, and links to major econometrics textbooks. Includes the "Econometrics Navigator" and R-for-Economists video series.
- Relevance to DAAF: High -- maintained by the author of "The Effect." Covers
both R and Python. The animated causal inference plots are uniquely valuable for building intuition about identification strategies.
- Strengths: Comprehensive link collection. Animated visualizations.
Regularly updated by a leading econometrics educator.
- Limitations: Python coverage less extensive than R/Stata. Best used alongside
"The Effect" for methodology with DAAF skill files for implementation.
LOST: Library of Statistical Techniques
- Author(s): Nick Huntington-Klein and community contributors
- URL: https://lost-stats.github.io/
- Type: Community wiki / Rosetta Stone
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: Moderate -- community-maintained; some pages may be dated
- Key content: Rosetta Stone for statistical software. Each page covers one
statistical technique with implementations in multiple languages (Python, R, Stata, SAS, and more). Seven categories: Data Manipulation, Geo-Spatial, Machine Learning, Model Estimation, Presentation, Time Series, and Other.
- Relevance to DAAF: High -- the multi-language format makes this the best
"look up any technique in Stata, see the Python equivalent" resource. Covers OLS, matching, logit/probit, multilevel models, DiD, RDD, event studies.
- Strengths: Multi-language implementations for each technique. Community-
editable. Covers modern causal inference methods.
- Limitations: Community-maintained -- quality varies by page. Some pages may
use outdated package versions.
Mixtape Sessions
- Author(s): Scott Cunningham and guest instructors
- URL: https://www.mixtapesessions.io/sessions/
- Type: Course / Workshop
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- workshops run regularly with updated materials
- Key content: Multi-day workshops teaching causal inference methods with
hands-on coding in Python and Stata. Flagship courses cover potential outcomes, matching, IV, RD, DiD, and synthetic control. Materials publicly available on GitHub (Mixtape-Sessions organization).
- Relevance to DAAF: Medium -- taught by leading applied econometricians.
Python implementations use pyfixest and modern packages. Materials are open-access after workshops.
- Strengths: Expert instruction. Practical exercises. Python materials
actively maintained. Open-access after the workshop.
- Limitations: Paid workshops (materials free afterward). Python code uses
pandas, not polars.
Coding for Economists (Full Book)
- Author(s): Arthur Turrell and contributors
- URL: https://aeturrell.github.io/coding-for-economists/intro.html
- Type: Book (free online)
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: None -- actively maintained
- Key content: Comprehensive Python guide for economists covering programming
basics, data handling, visualization (plotnine chapter), econometrics (OLS, IV, causal inference), time series, machine learning, text analysis, geospatial analysis, and reproducible research. Designed to take economists from zero Python to productive users.
- Relevance to DAAF: High -- the most complete single resource for economists
learning Python. Includes chapters on reproducibility and software engineering that align with DAAF's philosophy. The "Coming from Stata" and "Coming from R" chapters serve as entry points for transitioning users.
- Strengths: Comprehensive. Free. Economics-focused. Includes chapters on
every domain relevant to DAAF (data, viz, econometrics, ML, spatial, text).
- Limitations: Some chapters lean toward pandas. Econometrics coverage is
introductory -- use DAAF skill files for advanced methods.
---
Community Resources
Chuck Huber's Stata/Python Integration Blog Series
- Author(s): Chuck Huber (Director of Statistical Outreach, StataCorp)
- URL: https://blog.stata.com/author/chuber/
- Type: Blog series (8 parts)
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: Moderate -- written 2020; core concepts valid but Stata
17-19 may have evolved some APIs
- Key content: Official 8-part series on using Python within Stata (introduced
in Stata 16). Covers: installation/setup, three ways to call Python from Stata, installing packages, using packages, 3D surface plots, API/JSON data access, and the Stata Function Interface (SFI) for data exchange.
- Relevance to DAAF: Medium -- focused on Python-WITHIN-Stata rather than
Python-INSTEAD-OF-Stata. Useful for understanding how Stata officially positions Python integration, and for users maintaining a hybrid workflow during transition.
- Strengths: Written by StataCorp staff. Authoritative on Stata's Python
integration capabilities.
- Limitations: Covers Stata 16's Python integration, not standalone Python.
Most DAAF users will work in pure Python, not Stata+Python.
UCLA OARC: Stata Resources
- Author(s): UCLA Office of Advanced Research Computing
- URL: https://stats.oarc.ucla.edu/stata/
- Type: University resource hub
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: Low -- regularly maintained
- Key content: Comprehensive Stata learning resource featuring FAQs, learning
modules, annotated output for many statistical procedures, textbook examples, and web books. Extensive coverage of logistic regression, multilevel models, ordinal outcomes, and more with step-by-step Stata implementations.
- Relevance to DAAF: Medium -- understanding how UCLA OARC teaches Stata helps
identify common workflows that need Python translation. The annotated output sections are useful for understanding what Stata users expect in output.
- Strengths: Comprehensive. Well-organized by statistical method. Annotated
output shows exactly what Stata produces.
- Limitations: Stata-only. No Python equivalents provided.
German Rodriguez: Princeton Stata Tutorial
- Author(s): German Rodriguez (Princeton University)
- URL: https://grodri.github.io/stata/
- Type: Tutorial
- Last verified: 2026-03-28
- Quality: Excellent
- Currency concern: Low -- updated for Stata 18
- Key content: Widely cited introductory Stata tutorial covering data
management, graphics, tables, and programming. Recommended by stata.com as a primary learning resource.
- Relevance to DAAF: Medium -- not a translation resource per se, but
understanding the standard Stata tutorial path helps frame the skill to meet users where they are. Every concept covered here has a Python equivalent in the DAAF skill.
- Strengths: Clear, concise, authoritative. Recommended by StataCorp.
- Limitations: Stata-only. Tutorial format, not a translation reference.
Oscar Torres-Reyna: Princeton Panel Data Tutorial
- Author(s): Oscar Torres-Reyna (Princeton University)
- URL: https://www.princeton.edu/~otorres/Panel101.pdf
- Type: Tutorial / PDF
- Last verified: 2026-03-28
- Quality: Good
- Currency concern: Moderate -- older tutorial, but panel data fundamentals
unchanged
- Key content: Classic tutorial on panel data analysis with Stata. Covers
xtreg with fixed and random effects, Hausman test, and practical implementation. Widely used in economics graduate programs.
- Relevance to DAAF: Medium -- the skill should map every command in this
tutorial to its Python equivalent (pyfixest for FE, linearmodels for RE). Understanding this tutorial helps anticipate what Stata users know and expect.
- Strengths: The standard panel data reference for Stata users. Concise,
practical.
- Limitations: Stata-only. Panel data fundamentals are stable but the
tutorial predates modern DiD methods.
---
Stata Official Documentation (for Reference)
These are Stata's own documentation resources. They serve as the "source" for the patterns that DAAF's Python stack translates from.
Stata Documentation Overview
- URL: https://www.stata.com/features/documentation/
- Type: Official documentation hub
- Last verified: 2026-03-28
- Key content: Stata 18 documentation consists of over 19,000 pages organized
by command. Core manuals cover data management, graphics, functions, and reporting. 18+ subject-specific statistics manuals cover every domain from Bayesian analysis to survival models. Each entry follows a consistent format: syntax, options, examples, stored results, and methods/formulas.
- Why this matters for translation: Stata's documentation is organized around
COMMANDS (one entry per command), not tasks or workflows. Stata users think in terms of specific commands (reghdfe, xtreg, margins) and need Python equivalents for those exact commands. The DAAF skill is organized to support this lookup pattern.
Stata Graph Overview (Visual Reference)
- URL: https://www.stata.com/support/faqs/graphics/gph/stata-graphs/
- Type: Visual reference
- Last verified: 2026-03-28
- Key content: 100+ categorized graph examples showing Stata's graph system
capabilities. Organized by plot type (scatter, line, bar, box, etc.) with the Stata code that produced each graph.
- Why this matters for translation: When a Stata user shows you a graph and
asks "how do I make this in Python," this visual reference identifies the Stata command that produced it. Then the DAAF skill's visualization.md provides the plotnine equivalent.
Resources for Learning Stata
- URL: https://www.stata.com/links/resources-for-learning-stata/
- Type: Curated link collection
- Last verified: 2026-03-28
- Key content: StataCorp's curated collection of learning resources: 350+
video tutorials, NetCourses, Stata Blog, Statalist forum (60,000+ users), Stata Journal, and links to university tutorials.
- Why this matters for translation: Understanding which resources Stata users
learned from helps predict which commands and workflows they consider standard. The DAAF skill prioritizes translating the commands taught in these resources.
---
Sources: All resources listed above were accessed and verified on
2026-03-28. Quality and currency assessments are based on examination of the
resource content, publication date, maintenance activity, and relevance to the
DAAF Python stack. This catalog was compiled from systematic research documented
in /daaf/research/2026-03-28_Stata_Python_Translation_Research.md.Related skills
FAQ
Which Python packages does it map Stata to?
polars, pyfixest, statsmodels, linearmodels, marginaleffects, svy, and plotnine.
Who should use it?
Stata-background users auditing or learning Python code, and agents annotating Python output with Stata-equivalent comments.