
R Python Translation
- 1 installs
- 3.2k repo stars
- Updated August 4, 2026
- brycewang-stanford/awesome-agent-skills-for-empirical-research
r-python-translation is a Claude skill that maps R data-analysis packages and idioms to their Python equivalents for quantitative social science.
About
A skill that maps R data-analysis packages and idioms to their Python equivalents for quantitative social science. It translates tidyverse, ggplot2, fixest, survey, sf, and plm to Python tools like polars, plotnine, pyfixest, statsmodels, linearmodels, and geopandas, covering data wrangling, regression, visualization, and causal inference. A developer with an R background uses it to audit or translate Python analysis code, or to annotate output with R-equivalent comments.
- Maps R data-analysis packages to Python equivalents
- Covers tidyverse/ggplot2/fixest to polars/plotnine/pyfixest
- Provides an annotation protocol for R-equivalent code comments
R 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)
r-python-translation capabilities & compatibility
- Capabilities
- r to python translation · code annotation
- Use cases
- translation · data analysis
What r-python-translation says it does
R-to-Python translation for data analysis. Maps R packages (tidyverse, ggplot2, fixest, survey, sf, plm) to Python equivalents (polars, plotnine, pyfixest, svy, geopandas).
Use when user mentions R/RStudio background, requests R-equivalent code comments
This skill is a **routing hub** — it provides overview tables, decision trees
npx skills add https://github.com/brycewang-stanford/awesome-agent-skills-for-empirical-research --skill r-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
Translate R data-analysis code and idioms to their Python equivalents for social science research.
Who is it for?
R-background developers auditing or translating Python analysis code, or annotating Python code with R-equivalent comments.
Skip if: Learning either language from scratch outside a data-analysis context.
When should I use this skill?
A user mentions an R or RStudio background, requests R-equivalent code comments, or needs to translate R data analysis to Python.
What you get
Correct R-to-Python translations and optional inline R-equivalent annotations for analysis code.
- R-to-Python code translations
- Inline R-equivalent code annotations
Files
R-to-Python Translation Skill
R-to-Python translation reference for quantitative social science data analysis. Maps R ecosystem packages (tidyverse/dplyr, ggplot2, fixest, survey, sf, plm, lme4, marginaleffects, rdrobust) to DAAF Python equivalents (polars, plotnine, pyfixest, statsmodels, linearmodels, svy, geopandas). Use when user mentions R/RStudio background, requests R-equivalent code comments, needs to understand Python analysis code from an R perspective, or wants to translate R data analysis concepts to Python. Covers paradigm differences, verb-by-verb operation translations, regression modeling, causal inference, visualization, and workflow adaptation.
Cross-language translation reference for researchers moving between the R and Python data analysis ecosystems. This skill maps R packages, idioms, and workflows to their DAAF Python equivalents so that R-background users can audit, understand, and learn from DAAF-produced code, and so that code-producing agents can annotate their output with R 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 verb-by-verb mappings, code examples, and edge-case documentation.
What This Skill Does
- Maps the R data analysis ecosystem to DAAF's Python stack across data wrangling, modeling, visualization, causal inference, surveys, spatial analysis, and workflow tooling
- Provides a structured annotation protocol for agents to add inline R-equivalent comments to Python code
- Identifies paradigm gaps where R and Python diverge fundamentally, so users know where to expect friction
Use cases:
1. R user auditing DAAF Python code and needing to understand what operations are being performed 2. Agent annotating code with R-equivalent comments for an R-background researcher 3. R user learning Python for data analysis and needing a conceptual bridge 4. Translating a specific R operation or idiom to its Python equivalent 5. Understanding where R tools 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 | Encountering fundamental R-vs-Python confusion |
polars-dplyr.md | Core dplyr/tidyr to polars verb mapping (select, filter, mutate, joins, reshaping, window functions, lazy eval) | Reading or writing data manipulation code |
polars-strings-dates-factors.md | String, date/time, and factor operations (stringr, lubridate, forcats to polars) | Working with string/date/categorical columns |
regression-modeling.md | fixest/stats/plm to pyfixest/statsmodels/linearmodels | Reading or writing regression code |
visualization.md | ggplot2/plotly R to plotnine/plotly Python | Reading or writing visualization code |
causal-inference.md | R causal inference ecosystem to Python equivalents | Working with DiD, RDD, IV, event studies |
survey-spatial-ml.md | survey/sf/tidymodels to svy/geopandas/scikit-learn | Working with surveys, spatial data, or ML |
workflow-environment.md | RStudio/Quarto workflow to DAAF/marimo workflow | Adapting to DAAF's execution model |
external-resources.md | Curated guides and tutorials with provenance | Seeking additional learning materials |
gotchas.md | Common R-user mistakes in Python | Debugging or reviewing code from R perspective |
Reading Order
1. R user auditing DAAF code: paradigm-differences.md then the relevant domain file (e.g., polars-dplyr.md for data wrangling, regression-modeling.md for models) then gotchas.md 2. Agent annotating code with R equivalents: Agent Code Annotation Protocol section below, then the relevant domain file for the code being annotated 3. Learning Python from R background: paradigm-differences.md then polars-dplyr.md then workflow-environment.md then external-resources.md 4. Looking up a specific translation: Quick Decision Trees below, then the relevant reference file
Quick Decision Trees
"How do I do X from R in Python?"
What kind of R operation?
├─ Data wrangling (filter, mutate, join, pivot, summarise)
│ └─ ./references/polars-dplyr.md
├─ Regression / statistical modeling
│ └─ ./references/regression-modeling.md
├─ Plotting / visualization
│ └─ ./references/visualization.md
├─ Causal inference (DiD, RDD, IV, event studies)
│ └─ ./references/causal-inference.md
├─ Surveys / spatial / machine learning
│ └─ ./references/survey-spatial-ml.md
└─ Fundamental language differences (types, syntax, environment)
└─ ./references/paradigm-differences.md"Why does this Python code look different from R?"
What looks unfamiliar?
├─ Expression syntax (pl.col().method().alias())
│ └─ ./references/paradigm-differences.md
├─ Missing values (None vs NaN vs null vs NA)
│ └─ ./references/paradigm-differences.md
├─ Formula interface (~) behaves differently
│ └─ ./references/regression-modeling.md
├─ Import patterns and namespacing
│ └─ ./references/gotchas.md
└─ No interactive REPL / console workflow
└─ ./references/workflow-environment.md"I want to translate an R script to Python"
What does the R script do?
├─ Loads and wrangles data (read_csv, dplyr verbs)
│ └─ ./references/polars-dplyr.md
├─ Runs regressions (lm, feols, plm)
│ └─ ./references/regression-modeling.md
├─ Creates plots (ggplot, plotly)
│ └─ ./references/visualization.md
├─ Uses survey weights (svydesign, svymean)
│ └─ ./references/survey-spatial-ml.md
├─ Spatial operations (sf, st_join)
│ └─ ./references/survey-spatial-ml.md
├─ Multiple of the above
│ └─ Start with ./references/paradigm-differences.md, then each relevant file
└─ Uses a package not listed above
└─ ./references/external-resources.md for broader ecosystem guidance"Something isn't working and I think it's an R habit"
What went wrong?
├─ 1-indexed access gave wrong element
│ └─ ./references/gotchas.md
├─ Factor/categorical behaves differently
│ └─ ./references/gotchas.md
├─ NA handling surprised me
│ └─ ./references/paradigm-differences.md
├─ Pipe operator (|> or %>%) not available
│ └─ ./references/paradigm-differences.md
├─ library() vs import confusion
│ └─ ./references/gotchas.md
└─ Model output structure is different
└─ ./references/regression-modeling.md"Which Python package replaces my R package?"
Which R package?
├─ dplyr / tidyr / readr / tibble → polars
│ └─ ./references/polars-dplyr.md
├─ ggplot2 → plotnine
│ └─ ./references/visualization.md
├─ plotly (R) → plotly (Python)
│ └─ ./references/visualization.md
├─ fixest → pyfixest
│ └─ ./references/regression-modeling.md
├─ stats (lm, glm) → statsmodels
│ └─ ./references/regression-modeling.md
├─ plm / lme4 / estimatr → linearmodels
│ └─ ./references/regression-modeling.md
├─ survey → svy
│ └─ ./references/survey-spatial-ml.md
├─ sf / terra → geopandas
│ └─ ./references/survey-spatial-ml.md
├─ tidymodels / caret → scikit-learn
│ └─ ./references/survey-spatial-ml.md
├─ marginaleffects → marginaleffects (Python)
│ └─ ./references/regression-modeling.md
├─ rdrobust / did / synthdid → rdrobust / pyfixest DiD
│ └─ ./references/causal-inference.md
└─ Quarto / RMarkdown → marimo
└─ ./references/workflow-environment.mdPackage Mapping Overview
| Python Package | R Equivalent | Fidelity | Key Difference |
|---|---|---|---|
| polars | dplyr + tidyr + data.table | Low | Expression system vs verb grammar; method chaining vs pipe |
| pyfixest | fixest | High | Near-identical formula syntax; minor SE default differences |
| plotnine | ggplot2 | High | Same grammar of graphics; Python string quoting for aes |
| plotly | plotly (R) | High | px.scatter() vs plot_ly(); similar output |
| statsmodels | base R stats + lmtest + sandwich | Medium | Three formula dialects; manual vcov specification |
| linearmodels | plm + lme4 + estimatr | Medium | Requires pandas MultiIndex for panel structure |
| scikit-learn | tidymodels / caret | Medium | Imperative fit/predict vs declarative recipe pipeline |
| geopandas | sf + terra | Medium | shapely geometries vs sfc; different CRS handling |
| svy | survey (Lumley) | Medium | Limited GLM family coverage (gaussian/binomial/Poisson only) |
| marimo | Quarto / RMarkdown | Medium | Reactive cells vs knit-based linear execution |
Fidelity key: High = near-direct translation, same mental model. 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). R versions reference CRAN releases as of March 2026. When syntax or behavior has changed between versions, the reference files note the change.
| Python Package | DAAF Version | R Equivalent | R Version (CRAN) |
|---|---|---|---|
| polars | 1.38.1 | dplyr + tidyr + data.table | dplyr 1.2.0, tidyr 1.3.2, data.table 1.18.2 |
| pyfixest | 0.40.0 | fixest | 0.14.0 |
| plotnine | 0.15.3 | ggplot2 | 4.0.2 |
| plotly | 6.5.2 | plotly (R) | 4.12.0 |
| statsmodels | 0.14.6 | base R stats + lmtest + sandwich | lmtest 0.9-40, sandwich 3.1-1 |
| linearmodels | unpinned | plm + lme4 + estimatr | plm 2.6-7, lme4 2.0-1 |
| scikit-learn | 1.8.0 | tidymodels / caret | tidymodels 1.4.1, caret 7.0-1 |
| geopandas | 1.1.3 | sf + terra | sf 1.1-0, terra 1.9-11 |
| svy | 0.13.0 | survey | survey 4.5 |
| marginaleffects | unpinned | marginaleffects (R) | 0.32.0 |
| rdrobust | unpinned | rdrobust (R) | 3.0.0 |
| marimo | 0.19.11 | Quarto / RMarkdown | Quarto 1.6.x |
Unpinned packages: linearmodels, marginaleffects, and rdrobust install the latest version at Docker build time. Translations for these packages reference their documented API as of March 2026.
R version note: R package versions are from CRAN as of March 2026 (R 4.5.3). Check packageVersion("pkg") in your R installation to verify your local version matches.
Top 10 Paradigm Differences
These are the friction points R users encounter most frequently when reading or writing DAAF Python code. Each is covered in depth in the referenced file.
| # | Friction Point | R Way | Python Way | Reference |
|---|---|---|---|---|
| 1 | Expression system | df %>% mutate(x = a + b) | df.with_columns((pl.col("a") + pl.col("b")).alias("x")) | paradigm-differences.md |
| 2 | Formula fragmentation | One universal ~ syntax | Three dialects (pyfixest, statsmodels, linearmodels) | regression-modeling.md |
| 3 | Missing values | Single NA type | None, NaN, and null (context-dependent) | paradigm-differences.md |
| 4 | mutate equivalent | mutate(new = expr) | with_columns(expr.alias("new")) | polars-dplyr.md |
| 5 | No row index | Tibbles have row numbers | Polars has no row index; use with_row_index() | paradigm-differences.md |
| 6 | Polars-to-pandas bridge | Data frames go directly into models | Must call .to_pandas() before statsmodels/pyfixest | paradigm-differences.md |
| 7 | Factor vs Categorical | factor() with ordered levels | pl.Categorical / pd.Categorical (different semantics) | gotchas.md |
| 8 | Package fragmentation | One package per domain (fixest does it all) | Multiple packages per domain (statsmodels + linearmodels + pyfixest) | paradigm-differences.md |
| 9 | 1-indexed vs 0-indexed | x[1] is first element | x[0] is first element | gotchas.md |
| 10 | Namespace model | library() exports all names | import requires explicit namespacing | gotchas.md |
Agent Code Annotation Protocol
This section defines when and how code-producing agents add inline R-equivalent comments to DAAF Python scripts.
When to Annotate
Annotations are added only when the orchestrator explicitly passes an R-background directive to the agent. This is not a default behavior.
Trigger conditions (orchestrator activates this when any apply):
- User states they have an R / RStudio background
- User requests R-equivalent comments in code
- User asks to understand Python code from an R perspective
How the orchestrator passes the directive: The orchestrator adds the following to the agent prompt:
"User has R background. Load r-python-translation skill. Add inline R-equivalent comments for non-trivial data operations."
Comment Format
# R: df %>% filter(year == 2020)
filtered = df.filter(pl.col("year") == 2020)
# R: df %>% mutate(pct = count / sum(count))
result = df.with_columns(
(pl.col("count") / pl.col("count").sum()).alias("pct")
)
# R: feols(y ~ x1 + x2 | state + year, data = df, cluster = ~state)
fit = pf.feols("y ~ x1 + x2 | state + year", data=pdf, vcov={"CRV1": "state"})What to Annotate
- Annotate: Data wrangling (polars operations), modeling calls (pyfixest, statsmodels, linearmodels), visualization layer construction (plotnine, plotly), causal inference method 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
# R:comment per logical operation, placed on the line immediately above the Python code - Keep annotations to a single line; abbreviate complex R pipelines if needed
- R 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 dplyr/tidyr equivalent |
pyfixest | Python-side fixed effects regression — detailed API for the fixest equivalent |
plotnine | Python-side static visualization — detailed API for the ggplot2 equivalent |
plotly | Python-side interactive visualization — detailed API for plotly R equivalent |
statsmodels | Python-side general modeling — covers base R stats, lmtest, sandwich equivalents |
linearmodels | Python-side panel/IV models — covers plm, lme4, estimatr equivalents |
scikit-learn | Python-side ML — covers tidymodels/caret equivalents |
geopandas | Python-side spatial data — covers sf/terra equivalents |
svy | Python-side survey analysis — covers survey (Lumley) equivalents |
marimo | Python-side notebooks — covers Quarto/RMarkdown workflow equivalents |
stata-python-translation | Parallel skill for Stata-background users — shares the same Python target stack |
Note: Individual tool skills contain library-specific usage guidance (syntax, gotchas, performance). This skill provides the R-to-Python conceptual bridge — use both together when an R-background user is working with a specific library.
Topic Index
| Topic | Reference File |
|---|---|
Pipe operator (%>% / ` | >`) equivalents |
| Expression system (pl.col, .alias) | ./references/paradigm-differences.md |
| Missing value semantics (NA vs None/NaN/null) | ./references/paradigm-differences.md |
| Type system differences | ./references/paradigm-differences.md |
| Package/namespace model | ./references/paradigm-differences.md |
| 0-indexing vs 1-indexing | ./references/paradigm-differences.md |
| Polars-to-pandas conversion for modeling | ./references/paradigm-differences.md |
| Row index differences | ./references/paradigm-differences.md |
| dplyr verb mapping (filter, select, mutate, arrange) | ./references/polars-dplyr.md |
| summarise / group_by equivalents | ./references/polars-dplyr.md |
| tidyr verbs (pivot_longer, pivot_wider, separate, unite) | ./references/polars-dplyr.md |
| Join operations (left_join, inner_join, anti_join) | ./references/polars-dplyr.md |
| String operations (stringr vs polars .str) | ./references/polars-strings-dates-factors.md |
| Date operations (lubridate vs polars .dt) | ./references/polars-strings-dates-factors.md |
| across() / where() equivalents | ./references/polars-dplyr.md |
| case_when equivalent | ./references/polars-dplyr.md |
| readr I/O equivalents | ./references/polars-dplyr.md |
| fixest formula syntax in pyfixest | ./references/regression-modeling.md |
| lm() / glm() in statsmodels | ./references/regression-modeling.md |
| Formula interface comparison (three Python dialects) | ./references/regression-modeling.md |
| Standard error specification differences | ./references/regression-modeling.md |
| plm panel models in linearmodels | ./references/regression-modeling.md |
| lme4 mixed effects equivalents | ./references/regression-modeling.md |
| marginaleffects (R to Python) | ./references/regression-modeling.md |
| Model summary / tidy output | ./references/regression-modeling.md |
| Sandwich / robust SE equivalents | ./references/regression-modeling.md |
| ggplot2 layer mapping to plotnine | ./references/visualization.md |
| aes() string quoting in plotnine | ./references/visualization.md |
| Theme customization | ./references/visualization.md |
| Scale functions | ./references/visualization.md |
| Faceting (facet_wrap, facet_grid) | ./references/visualization.md |
| plotly R vs plotly Python | ./references/visualization.md |
| ggsave equivalent | ./references/visualization.md |
| Difference-in-differences (did, did2s) | ./references/causal-inference.md |
| Regression discontinuity (rdrobust) | ./references/causal-inference.md |
| Instrumental variables (ivreg vs pyfixest IV) | ./references/causal-inference.md |
| Event study designs | ./references/causal-inference.md |
| Synthetic control | ./references/causal-inference.md |
| Matching / propensity scores | ./references/causal-inference.md |
| survey package to svy | ./references/survey-spatial-ml.md |
| svydesign / svymean / svyglm equivalents | ./references/survey-spatial-ml.md |
| sf spatial operations to geopandas | ./references/survey-spatial-ml.md |
| CRS / projection handling | ./references/survey-spatial-ml.md |
| Spatial joins (st_join vs sjoin) | ./references/survey-spatial-ml.md |
| tidymodels pipeline to scikit-learn | ./references/survey-spatial-ml.md |
| RStudio vs DAAF workflow | ./references/workflow-environment.md |
| Quarto / RMarkdown vs marimo | ./references/workflow-environment.md |
| Interactive console vs file-first execution | ./references/workflow-environment.md |
| Package management (renv vs pip/uv) | ./references/workflow-environment.md |
| Project structure conventions | ./references/workflow-environment.md |
| Curated R-to-Python migration guides | ./references/external-resources.md |
| Package documentation links | ./references/external-resources.md |
| Tutorial recommendations with provenance | ./references/external-resources.md |
| 1-indexed list/vector access | ./references/gotchas.md |
| Factor vs Categorical pitfalls | ./references/gotchas.md |
| library() vs import habits | ./references/gotchas.md |
| T/F vs True/False | ./references/gotchas.md |
| Assignment operator (<- vs =) | ./references/gotchas.md |
| Vectorized operations expectations | ./references/gotchas.md |
| NULL vs None differences | ./references/gotchas.md |
| apply family vs map/list comprehension | ./references/gotchas.md |
| Copying semantics (R copy-on-modify vs Python references) | ./references/gotchas.md |
| Logical operators (& / | vs and / or) |
| String interpolation (glue vs f-strings) | ./references/gotchas.md |
| data.table vs polars | ./references/polars-strings-dates-factors.md |
| Lazy evaluation (polars LazyFrame vs R lazy tibble) | ./references/polars-dplyr.md |
| nest/unnest equivalents | ./references/polars-dplyr.md |
| Window functions (over vs mutate + group_by) | ./references/polars-dplyr.md |
| Coordinate systems (coord_flip, coord_polar) | ./references/visualization.md |
| Stat layers (stat_smooth, stat_summary) | ./references/visualization.md |
| Color palette mapping (viridis, brewer) | ./references/visualization.md |
| Multi-panel layouts (patchwork vs subplot) | ./references/visualization.md |
| Staggered DiD estimators | ./references/causal-inference.md |
| Parallel trends testing | ./references/causal-inference.md |
| BRR / jackknife replication weights | ./references/survey-spatial-ml.md |
| Raster data handling (terra vs rasterio) | ./references/survey-spatial-ml.md |
| Feature engineering (recipes vs sklearn Pipeline) | ./references/survey-spatial-ml.md |
| Cross-validation (rsample vs sklearn) | ./references/survey-spatial-ml.md |
| Environment/workspace differences (.RData vs nothing) | ./references/workflow-environment.md |
| Debugging workflow (browser() vs breakpoint()) | ./references/workflow-environment.md |
| R help system (?func) vs Python help(func) | ./references/workflow-environment.md |
| Cheat sheet and quick-reference links | ./references/external-resources.md |
| Community resources (Stack Overflow tags, forums) | ./references/external-resources.md |
Causal Inference: R to Python Translation
R has been the primary language for causal inference in quantitative social science for over a decade. Most influential causal methods papers (DiD, RD, synthetic control, matching) ship reference implementations as R packages first, with Python ports following months to years later. This gap has narrowed considerably: pyfixest now mirrors R fixest's DiD capabilities almost exactly, and several R package authors (Cattaneo for rdrobust, Arel-Bundock for marginaleffects) have released official Python ports of their own tools.
DAAF's Python stack provides strong coverage for the most common causal designs:
- Difference-in-differences: pyfixest (TWFE, did2s, lpdid, Sun-Abraham)
- Event studies: pyfixest (i() operator, iplot)
- Regression discontinuity: rdrobust (installable; same authors as R version)
- Instrumental variables: pyfixest (with FE) and linearmodels (LIML, GMM)
- Marginal effects: marginaleffects (same author as R version)
Some causal methods remain gaps in Python: matching/weighting (no MatchIt/WeightIt equivalent in DAAF), generalized random forests (no grf port), and multiple imputation (no mice equivalent). These are documented honestly in this reference.
Versions referenced:
Python: pyfixest 0.40.0, marginaleffects (unpinned), rdrobust (unpinned)
R: fixest 0.14.0, marginaleffects 0.32.0, rdrobust 3.0.0, did 2.3.0, did2s 1.2.1
See SKILL.md § Library Versions for the complete version table.
Sources: Cunningham, Causal Inference: The Mixtape (Yale, 2021);
Huntington-Klein, The Effect (CRC Press, 2021);
Berge, Butts, & McDermott, fixest (CRAN, v0.13);
Fischer et al., pyfixest (pyfixest.org, v0.40.0, accessed 2026-03-28);
Cattaneo, Idrobo, & Titiunik, rdrobust (rdpackages.github.io);
Arel-Bundock, Greifer, & Heiss, "How to Interpret Statistical Models Using
marginaleffects for R and Python" (JSS, 2024)
---
1. Difference-in-Differences
Traditional TWFE DiD
The simplest DiD design: all treated units adopt simultaneously, effects are homogeneous.
R (fixest):
library(fixest)
# Binary treatment indicator × post-treatment interaction
fit <- feols(y ~ treat:post | unit + time, data = df, vcov = ~unit)
summary(fit)
# Or with an explicit treatment variable
fit <- feols(y ~ treated | unit + time, data = df, vcov = ~unit)Python (pyfixest):
import pyfixest as pf
# Binary treatment indicator x post-treatment interaction
fit = pf.feols("y ~ treat:post | unit + time", data=df, vcov={"CRV1": "unit"})
fit.summary()
# Or with an explicit treatment variable
fit = pf.feols("y ~ treated | unit + time", data=df, vcov={"CRV1": "unit"})The formula syntax is identical. The only difference is the clustering syntax (~unit vs {"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.
R (did2s):
library(did2s)
fit <- 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_var = "unit"
)
summary(fit)
iplot(fit)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()Key differences:
- R argument is
cluster_var; Python iscluster - R returns a fixest object; Python returns a Feols object (both support etable/iplot)
- Formula syntax within
first_stageandsecond_stageis identical
Pooled ATT (single treatment effect):
# R
fit <- did2s(df, "y", ~ 0 | unit + time, ~ treated, "treated", "unit")# Python
fit = pf.did2s(df, "y", "~ 0 | unit + time", "~ treated", "treated", "unit")Callaway-Sant'Anna (2021)
Group-time ATTs that properly handle staggered adoption.
R (did):
library(did)
out <- att_gt(
yname = "y",
tname = "year",
idname = "unit_id",
gname = "first_treat", # Year unit first treated (0 = never)
data = df,
control_group = "nevertreated",
est_method = "dr" # Doubly robust
)
summary(out)
ggdid(out) # Event study plot
# Aggregate to overall ATT
agg <- aggte(out, type = "simple")
summary(agg)
# Dynamic (event study) aggregation
agg_dyn <- aggte(out, type = "dynamic")
summary(agg_dyn)
ggdid(agg_dyn)Python (csdid):
# Requires: 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",
)
# API mirrors R did package; check csdid documentation for
# current aggregation and plotting methodsCoverage note: The Python csdid package is a community port (d2cml-ai), not an official release by the original authors. It aims to replicate the R did package API, but may lag behind on features and bug fixes. Verify results against R when using for published research.
Sun-Abraham Saturated Estimator
Fully saturates the model with cohort-by-period indicators, then aggregates.
R (fixest):
# sunab() is a special function within feols
fit <- feols(y ~ sunab(cohort, period) | unit + period, data = df,
vcov = ~unit)
summary(fit)
iplot(fit)
# Aggregate to ATT
summary(fit, agg = "ATT")
# Aggregate by cohort
summary(fit, agg = "cohort")Python (pyfixest):
# Via event_study() with estimator="saturated"
fit = pf.event_study(
data=df,
yname="y",
idname="unit",
tname="period",
gname="cohort", # Year of treatment adoption
estimator="saturated",
att=False, # False = dynamic event study
cluster="unit",
)
fit.summary()
fit.iplot()
# Aggregate to overall ATT
agg = fit.aggregate(weighting="shares")Key difference: R uses sunab() as a formula function inside feols(). Python uses a separate event_study() function 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.
R: No widely adopted standalone R package; typically implemented manually using local projections (Jorda, 2005) applied to a DiD setting.
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
)
# Note: 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 R.
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 matplotlib or plotnine.
---
2. Event Studies
Manual Event Study with i()
R (fixest):
# Create relative-time variable
df$rel_year <- df$year - df$treatment_year
# Event study with i() — omit t=-1 as reference
fit <- feols(y ~ i(rel_year, ref = -1) | unit + year, data = df,
vcov = ~unit)
# Plot
iplot(fit)
# With joint confidence bands
iplot(fit, joint = TRUE) # Bonferroni bandsPython (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
fit.iplot(joint="both") # Bonferroni + Scheffe bandsKey differences:
| Feature | R fixest | pyfixest |
|---|---|---|
| Reference level | i(var, ref = -1) (space after =) | i(var, ref=-1) (no space required) |
| iplot call | iplot(fit) (standalone function) | fit.iplot() (method on Feols) |
| Joint bands | iplot(fit, joint = TRUE) | fit.iplot(joint="both") |
| Band types | Bonferroni (default TRUE) | "bonferroni", "scheffe", or "both" |
Unified Event Study Interface
R (fixest): Does not have a single unified function; uses sunab() or manual i() specification within feols().
Python (pyfixest):
# pf.event_study() provides a clean unified interface
fit = pf.event_study(
data=df,
yname="y",
idname="unit",
tname="year",
gname="treatment_year",
estimator="twfe", # "twfe", "did2s", or "saturated"
att=False, # False = dynamic event study
cluster="unit",
)This unified interface is a Python advantage over R, where you must choose between different function calls (feols + i(), did2s, sunab) for different estimators.
Comparing Estimators Visually
R (fixest):
fit_twfe <- feols(y ~ i(rel_year, ref = -1) | unit + year, data = df)
# did2s returns fixest object, can overlay
fit_d2s <- did2s(df, "y", ~ 0 | unit + year,
~ i(rel_year, ref = -1), "treated", "unit")
coefplot(list(fit_twfe, fit_d2s))Python (pyfixest):
fit_twfe = pf.event_study(data=df, yname="y", idname="unit",
tname="year", gname="g", estimator="twfe", att=False)
fit_did2s = pf.event_study(data=df, yname="y", idname="unit",
tname="year", gname="g", estimator="did2s", att=False)
pf.coefplot([fit_twfe, fit_did2s])Treatment Pattern Visualization
R: No built-in equivalent in fixest. Typically uses custom ggplot2 heatmaps.
Python (pyfixest):
pf.panelview(data=df, unit="unit", time="year", treat="treated")panelview() produces a heatmap showing treatment assignment across units and time. This is a pyfixest feature without a direct fixest equivalent.
---
3. Regression Discontinuity
Both R and Python implementations are maintained by the same authors (Cattaneo, Idrobo, Titiunik), ensuring very high fidelity across languages.
Sharp RD
R (rdrobust):
library(rdrobust)
# Point estimate with robust bias-corrected CI
rd <- rdrobust(Y, X, c = cutoff)
summary(rd)
# RD plot
rdplot(Y, X, c = cutoff)
# Bandwidth selection
bw <- rdbwselect(Y, X, c = cutoff)
summary(bw)Python (rdrobust):
# Requires: pip install rdrobust
from rdrobust import rdrobust, rdplot, rdbwselect
# Point estimate with robust bias-corrected CI
rd = rdrobust(Y, X, c=cutoff)
print(rd)
# RD plot
rdplot(Y, X, c=cutoff)
# Bandwidth selection
bw = rdbwselect(Y, X, c=cutoff)
print(bw)Fuzzy RD
R:
rd <- rdrobust(Y, X, c = cutoff, fuzzy = T) # T = treatment indicatorPython:
rd = rdrobust(Y, X, c=cutoff, fuzzy=T)Key Parameters
| Parameter | R | 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 = c(left, right) | h=[left, right] | R uses c(), Python uses list |
| Polynomial order | p = 1 | p=1 | Local linear is default |
| Fuzzy | fuzzy = T | fuzzy=T | Treatment indicator for fuzzy RD |
| Covariates | covs = cbind(c1, c2) | covs=covs_array | Matrix/array of covariates |
| Clustering | cluster = cl | cluster=cl | Cluster variable |
The API is virtually identical. The main syntactic differences are R's c() vs Python's [] for paired values, and R's cbind() vs numpy arrays for covariates.
Additional RD Packages
| Tool | R Package | Python Package | Install |
|---|---|---|---|
| Local polynomial RD | rdrobust | rdrobust | pip install rdrobust |
| RD plots | rdrobust::rdplot() | rdrobust.rdplot() | Included |
| Bandwidth selection | rdrobust::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 R, Python, and Stata.
---
4. Instrumental Variables
See also Section 3 of the companion regression-modeling.md reference for formula syntax details. This section focuses on the causal inference aspects.
IV with Fixed Effects
R (fixest):
# Classic IV: education instrumented by college proximity
fit <- feols(log_wage ~ experience | state + year | education ~ college_prox,
data = df, vcov = ~state)
# First-stage results
summary(fit, stage = 1)
# Diagnostics
fitstat(fit, type = "ivf") # First-stage FPython (pyfixest):
fit = pf.feols("log_wage ~ experience | state + year | education ~ college_prox",
data=df, vcov={"CRV1": "state"})
# First-stage results
fit._model_1st_stage.summary()
# Comprehensive diagnostics (effective F, Cragg-Donald, Kleibergen-Paap)
fit.IV_Diag()IV without Fixed Effects
R (ivreg):
library(ivreg)
fit <- ivreg(log_wage ~ experience + education | experience + college_prox,
data = df)
summary(fit, diagnostics = TRUE) # Includes weak instrument testsPython (linearmodels):
from linearmodels.iv import IV2SLS
fit = IV2SLS.from_formula(
"log_wage ~ 1 + experience + [education ~ college_prox]", data=df
).fit(cov_type="robust")
print(fit.summary)
# First-stage diagnostics
print(fit.first_stage)IV Diagnostic Comparison
| Diagnostic | R (fixest) | R (ivreg) | pyfixest | linearmodels |
|---|---|---|---|---|
| First-stage F | fitstat(, "ivf") | summary(, diag=T) | fit.IV_Diag() | fit.first_stage |
| Weak instrument | fitstat(, "ivwald") | Cragg-Donald F | Effective F (Olea-Pflueger) | Cragg-Donald F |
| Sargan/Hansen | fitstat(, "sargan") | summary(, diag=T) | Not built-in | fit.sargan |
| Wu-Hausman | fitstat(, "wh") | summary(, diag=T) | Not built-in | fit.wu_hausman() |
LIML and GMM (Beyond 2SLS)
R: No standard LIML/GMM package in base fixest. The ivreg package supports LIML via method = "LIML".
fit <- ivreg(y ~ x + endo | x + z1 + z2, data = df, method = "LIML")Python (linearmodels):
from linearmodels.iv import IVLIML, IVGMM
# LIML — better finite-sample properties than 2SLS
fit_liml = IVLIML.from_formula("y ~ 1 + x + [endo ~ z1 + z2]", data=df).fit()
# GMM — efficient with heteroskedasticity and overidentification
fit_gmm = IVGMM.from_formula("y ~ 1 + x + [endo ~ z1 + z2]", data=df).fit()This is a case where Python (linearmodels) has broader coverage than the standard R toolkit for IV estimation methods.
---
5. Marginal Effects and Post-Estimation Interpretation
Both R and Python implementations are by the same author (Vincent Arel-Bundock), ensuring consistent methodology and API design.
Average Marginal Effects (AME)
R (marginaleffects):
library(marginaleffects)
fit <- glm(y ~ x1 * x2 + x3, data = df, family = binomial)
# Average marginal effect of x1 (accounts for interaction with x2)
avg_slopes(fit, variables = "x1")
# All variables
avg_slopes(fit)Python (marginaleffects):
# Requires: pip install marginaleffects
from marginaleffects import avg_slopes
fit = smf.logit("y ~ x1 * x2 + x3", data=df).fit()
# Average marginal effect of x1
avg_slopes(fit, variables="x1")
# All variables
avg_slopes(fit)Predictions at Specific Values
R:
predictions(fit, newdata = datagrid(x1 = c(0, 1), x2 = mean))Python:
from marginaleffects import predictions, datagrid
predictions(fit, newdata=datagrid(x1=[0, 1], x2="mean", model=fit))Comparisons (Contrasts)
R:
# Average effect of a one-unit change in x1
avg_comparisons(fit, variables = "x1")
# Specific contrast
avg_comparisons(fit, variables = list(x1 = c(0, 1)))Python:
from marginaleffects import avg_comparisons
avg_comparisons(fit, variables="x1")
avg_comparisons(fit, variables={"x1": [0, 1]})Hypothesis Testing (Delta Method)
R:
hypotheses(fit, "x1 = x2")
hypotheses(fit, "(x1 / x2 - 1) * 100 = 0") # NonlinearPython:
from marginaleffects import hypotheses
hypotheses(fit, "x1 = x2")
hypotheses(fit, "(x1 / x2 - 1) * 100 = 0")Compatibility
| Model Source | R marginaleffects | Python marginaleffects |
|---|---|---|
| Base R lm/glm | Yes | N/A |
| fixest feols/fepois | Yes | N/A |
| statsmodels OLS/GLM | N/A | Yes |
| pyfixest Feols/Fepois | N/A | Yes |
| lme4 mixed models | Yes | N/A |
| brms Bayesian | Yes | N/A |
| scikit-learn | N/A | Yes |
The R version supports 100+ model classes. The Python version supports statsmodels, pyfixest, scikit-learn, and a growing list of other packages.
Caveat: The Python marginaleffects package is described by its author as an alpha release. There are known numerical discrepancies between R and Python results in some edge cases. For published research, verify critical marginal effects calculations against the R implementation.
---
6. Matching and Weighting
This is a significant gap in the Python ecosystem. R has mature, well-documented packages; Python alternatives are fragmented.
R Packages (Mature)
library(MatchIt)
# Propensity score matching
m.out <- matchit(treat ~ x1 + x2 + x3, data = df, method = "nearest")
summary(m.out)
matched_df <- match.data(m.out)
# Optimal full matching
m.out <- matchit(treat ~ x1 + x2 + x3, data = df, method = "full")
# Coarsened exact matching
m.out <- matchit(treat ~ x1 + x2 + x3, data = df, method = "cem")library(WeightIt)
# Inverse probability weighting
w.out <- weightit(treat ~ x1 + x2 + x3, data = df, method = "ps")
summary(w.out)
# Use weights in outcome model
fit <- lm(y ~ treat, data = df, weights = w.out$weights)Python Alternatives (Fragmented)
| R Package | Python Equivalent | Fidelity | Notes |
|---|---|---|---|
MatchIt | pymatchit-causal | Low-Medium | Community port; limited method coverage |
MatchIt | Manual with scikit-learn | Low | Build propensity model + manual matching |
WeightIt | No direct equivalent | N/A | Manual IPW with statsmodels/sklearn |
cobalt (balance) | No direct equivalent | N/A | Manual balance tables |
Recommended workaround for DAAF projects: Implement propensity score matching manually using scikit-learn for the propensity model and pandas/polars for the matching algorithm. For published work requiring formal matching diagnostics, consider running the matching step in R and importing the matched dataset.
# Manual PSM sketch (not a full replacement for MatchIt)
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors
# Estimate propensity scores
ps_model = LogisticRegression().fit(X, treatment)
ps = ps_model.predict_proba(X)[:, 1]
# Nearest-neighbor matching on propensity score
nn = NearestNeighbors(n_neighbors=1)
nn.fit(ps[treatment == 0].reshape(-1, 1))
distances, indices = nn.kneighbors(ps[treatment == 1].reshape(-1, 1))This lacks MatchIt's balance diagnostics, caliper options, exact matching constraints, and variance ratio checks. The gap is real and consequential.
---
7. Synthetic Control
R Packages
# Classic synthetic control (Abadie, Diamond, Hainmueller)
library(Synth)
synth_out <- synth(dataprep.out)
path.plot(synth_out, dataprep.out)
# Generalized synthetic control (Xu, 2017)
library(gsynth)
out <- gsynth(y ~ treat + x1, data = df, index = c("unit", "year"),
force = "two-way", r = c(0, 5))
# Augmented synthetic control (Ben-Michael, Feller, Rothstein)
library(augsynth)
aug <- augsynth(y ~ treat, unit = unit, time = year, data = df)
summary(aug)
plot(aug)Python Alternatives
| R Package | Python Equivalent | Install | Fidelity |
|---|---|---|---|
Synth | SyntheticControlMethods | pip install SyntheticControlMethods | Medium |
gsynth | No direct equivalent | N/A | Gap |
augsynth | No direct equivalent | N/A | Gap |
| N/A | CausalPy (Bayesian SC) | pip install CausalPy | Different methodology |
| N/A | synthdid | pip install synthdid | Medium (synthetic DiD) |
CausalPy (by PyMC Labs) provides a Bayesian approach to synthetic control with full uncertainty quantification via PyMC. It is methodologically different from the frequentist Synth package but achieves similar goals:
# Requires: 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()The Python synthetic control ecosystem is less mature than R's. For research requiring the exact Abadie-Diamond-Hainmueller estimator, consider running the analysis in R.
---
8. Other Causal Tools
Survival Analysis / Duration Models
R:
library(survival)
fit <- coxph(Surv(time, event) ~ x1 + x2 + strata(group), data = df)
summary(fit)Python:
# Requires: pip install lifelines
from lifelines import CoxPHFitter
cph = CoxPHFitter()
cph.fit(df, duration_col="time", event_col="event", formula="x1 + x2 + strata(group)")
cph.print_summary()lifelines is mature and well-maintained. It covers Cox PH, Kaplan-Meier, Nelson-Aalen, and parametric survival models. Good Python coverage.
Bayesian Causal Impact (Time Series Intervention)
R:
library(CausalImpact)
impact <- CausalImpact(data, pre.period, post.period)
summary(impact)
plot(impact)Python:
# Requires: 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()CausalPy's interrupted time series is the closest Python analog to R's CausalImpact, though the underlying methodology differs (Bayesian structural time series in R vs. Bayesian regression in CausalPy).
Generalized Random Forests (GRF)
R:
library(grf)
cf <- causal_forest(X, Y, W)
ate <- average_treatment_effect(cf)
cate <- predict(cf)Python: No direct equivalent in the DAAF stack. The econml package (Microsoft) provides causal forest implementations:
# Requires: pip install econml
from econml.dml import CausalForestDML
cf = CausalForestDML(model_y="auto", model_t="auto")
cf.fit(Y, T, X=X, W=W)
cate = cf.effect(X)econml is a different implementation from grf and may produce different results. The grf R package remains the reference implementation for Athey-Imbens-Wager causal forests.
Multiple Imputation
R:
library(mice)
imp <- mice(df, m = 5, method = "pmm")
fit <- with(imp, lm(y ~ x1 + x2))
pooled <- pool(fit)
summary(pooled)Python: No equivalent in the DAAF stack. This is a significant gap. Partial alternatives exist:
| Approach | Package | Limitation |
|---|---|---|
| Single imputation | sklearn.impute.IterativeImputer | No Rubin's rules; single imputation only |
| Manual MI | statsmodels + sklearn | Must implement Rubin's pooling rules manually |
miceforest | pip install miceforest | Community package; less validated than R mice |
For research requiring proper multiple imputation with Rubin's pooling rules, R's mice remains the gold standard. Consider running imputation in R and exporting the completed datasets.
---
9. Ecosystem Mapping Table
| Method | R Package | Python Equivalent | Fidelity | Install | Key Difference |
|---|---|---|---|---|---|
| OLS + FE | fixest::feols() | pf.feols() | Very High | Pre-installed | Cluster SE syntax differs |
| Poisson + FE | fixest::fepois() | pf.fepois() | Very High | Pre-installed | Identical formula syntax |
| GLM + FE | fixest::feglm() | Not supported | N/A | N/A | Major gap |
| TWFE DiD | fixest::feols() | pf.feols() | Very High | Pre-installed | Cluster SE syntax only |
| did2s | did2s::did2s() | pf.did2s() | Very High | Pre-installed | cluster_var vs cluster |
| Sun-Abraham | fixest::sunab() | pf.event_study(est="saturated") | High | Pre-installed | Different API, same estimator |
| Callaway-Sant'Anna | did::att_gt() | csdid.att_gt() | Medium | pip install csdid | Community port |
| LP-DiD | Manual/emerging | pf.lpdid() | N/A | Pre-installed | Python has better wrapper |
| Event study (iplot) | fixest::iplot() | fit.iplot() | Very High | Pre-installed | Method vs function syntax |
| RD (sharp/fuzzy) | rdrobust::rdrobust() | rdrobust.rdrobust() | Very High | pip install rdrobust | Same authors |
| RD plots | rdrobust::rdplot() | rdrobust.rdplot() | Very High | pip install rdrobust | Same authors |
| RD density test | rddensity::rddensity() | rddensity.rddensity() | Very High | pip install rddensity | Same authors |
| IV + FE | fixest::feols() | pf.feols() | Very High | Pre-installed | Identical 3-part formula |
| IV (2SLS, no FE) | ivreg::ivreg() | linearmodels.IV2SLS() | High | Pre-installed | Formula syntax differs |
| LIML | ivreg(method="LIML") | linearmodels.IVLIML() | High | Pre-installed | |
| GMM-IV | Limited | linearmodels.IVGMM() | N/A | Pre-installed | Python has broader coverage |
| Panel FE | plm::plm(model="within") | linearmodels.PanelOLS() | High | Pre-installed | MultiIndex vs pdata.frame |
| Panel RE | plm::plm(model="random") | linearmodels.RandomEffects() | High | Pre-installed | MultiIndex vs pdata.frame |
| Marginal effects | marginaleffects | marginaleffects | High | pip install marginaleffects | Same author; Python is alpha |
| Matching (PSM) | MatchIt::matchit() | pymatchit-causal | Low | pip install pymatchit-causal | Significant gap |
| Weighting (IPW) | WeightIt::weightit() | No equivalent | N/A | N/A | Significant gap |
| Synthetic control | Synth, augsynth | CausalPy (Bayesian) | Low | pip install CausalPy | Different methodology |
| Causal impact | CausalImpact | CausalPy (ITS) | Medium | pip install CausalPy | Different methodology |
| Causal forests | grf | econml | Medium | pip install econml | Different implementation |
| Survival/Cox | survival::coxph() | lifelines.CoxPHFitter() | High | pip install lifelines | Good Python coverage |
| Multiple imputation | mice::mice() | No equivalent | N/A | N/A | Significant gap |
Fidelity Legend
- Very High: Same or near-identical API, same authors, or results match to high precision
- High: Reliable port with minor syntax differences; results match
- Medium: Community port or different implementation; verify results for published work
- Low: Partial coverage; significant feature gaps
- N/A: No equivalent available; gap in the ecosystem
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);
Arel-Bundock, "marginaleffects" (marginaleffects.com, accessed 2026-03-28);
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)
External Resources for R-to-Python Translation
Curated catalog of resources for R 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, plotnine, geopandas). Entries marked with currency concerns should be cross-checked against current documentation.
Contents
- Package-Specific Documentation
- Textbooks with Dual-Language Code
- General R-to-Python Guides
- Social Science Methodology Resources
- R Package 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
- Relevance to DAAF: High
- Currency concern: None --- actively maintained, aligned with fixest 0.13+
- Key content: Complete documentation for Python's fixest-syntax regression
library. Covers OLS, IV, Poisson, and quantile regression with multi-way fixed effects. The quickstart explicitly references R fixest syntax and demonstrates identical formula notation (Y ~ X | fe1 + fe2).
- Strengths: Mirrors R fixest API closely; includes multiple estimation,
difference-in-differences estimators (TWFE, did2s, lpdid, Sun-Abraham), and publication-quality tables via etable(). Formula syntax documentation makes direct comparison to R straightforward.
- Limitations: Some R fixest features (feglm with FE) not yet implemented.
See the DAAF pyfixest skill's gotchas.md for the full gap list.
plotnine Documentation
- Author(s): Hassan Kibirige
- URL: https://plotnine.org/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Good
- Relevance to DAAF: High
- Currency concern: None --- v0.15.3 as of verification date
- Key content: Python implementation of the grammar of graphics, with syntax
intentionally mirroring ggplot2. Covers geoms, aesthetics, scales, facets, coords, and themes. The API reference is organized identically to ggplot2's function reference.
- Strengths: Near-identical syntax to ggplot2 means R users can translate
plots with minimal changes. Most ggplot2 code translates by replacing + line continuation with Python's + operator inside parentheses.
- Limitations: Coverage is not 100% of ggplot2. Some extensions (e.g.,
ggrepel, patchwork) do not have plotnine equivalents. When plotnine docs are sparse on a topic, the ggplot2 documentation remains a useful conceptual reference.
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
- Relevance to DAAF: High
- Currency concern: None --- actively maintained with bilingual R/Python support
- Key content: Free online textbook and software documentation for the
marginaleffects package (available in both R and Python). Covers predictions, comparisons (contrasts, risk ratios, odds), slopes (marginal effects), and hypothesis testing across 100+ model classes. Every page has an R/Python toggle showing equivalent code in both languages.
- Strengths: The bilingual toggle is the gold standard for R-to-Python
translation in post-estimation analysis. Covers causal inference, experiments, categorical outcomes, and ML interpretation. Published in the Journal of Statistical Software (v111, i09). Author royalties support charity.
- Limitations: Python API coverage lags slightly behind R for the most
recently added model classes. Check the GitHub issues for current status.
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
- Relevance to DAAF: Medium (pandas-to-polars, not R-to-polars directly)
- 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. Includes code comparison patterns.
- Strengths: Authoritative source on polars idioms. The emphasis on avoiding
pandas-style patterns ("if your Polars code looks like pandas code, it likely runs slower than it should") helps R users avoid the pandas intermediate step.
- Limitations: Assumes familiarity with pandas, not R. R users benefit more
from the tidyverse-to-polars guides listed below, then using this as a conceptual supplement.
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
- Relevance to DAAF: High (for regression discontinuity work)
- Currency concern: None --- maintained across R, Python, and Stata
- Key content: Unified documentation hub for the rdrobust family of packages
providing local polynomial RD estimation, bandwidth selection (rdbwselect), and RD plots (rdplot). Available on CRAN (R) and PyPI (Python) with identical APIs and function names.
- Strengths: Truly parallel implementation --- same function names, same
arguments, same output structure across R and Python. The translation is nearly mechanical. Academic references (Calonico, Cattaneo, Titiunik 2014, 2015) provide rigorous methodological grounding.
- Limitations: Python version documentation is sparser than R's CRAN
vignettes. Consult the R manual for detailed parameter explanations, then apply directly to the Python version.
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
- Relevance to DAAF: High
- Currency concern: None --- v0.14.6 stable
- Key content: Comprehensive statistical modeling library covering OLS, WLS,
GLS, GLM (logit, probit, Poisson, negative binomial), mixed effects, time series, and hypothesis testing. Supports R-style formulas via the statsmodels.formula.api module.
- Strengths: The formula API (
smf.ols("y ~ x1 + x2", data=df)) is
intentionally modeled on R's formula interface, making translation straightforward. Extensive diagnostic methods (influence plots, residual tests, specification tests) mirror what R users expect.
- Limitations: Documentation can be dense and assumes familiarity with the
library's architecture. The two-step pattern (specify model, then .fit()) differs from R's single-call approach. No built-in high-dimensional FE support --- use pyfixest for that.
scikit-learn Documentation
- Author(s): scikit-learn developers
- URL: https://scikit-learn.org/stable/
- Type: Documentation
- Last verified: 2026-03-28
- Quality: Excellent
- Relevance to DAAF: Medium (prediction-focused, not inference-focused)
- Currency concern: None --- v1.8.0 stable
- Key content: Machine learning library covering classification, regression,
clustering, dimensionality reduction, model selection, and preprocessing. Comprehensive user guide, API reference, and tutorials.
- Strengths: Best-in-class documentation for ML workflows. The consistent
fit()/predict()/transform() API is easy to learn. Extensive examples for every estimator.
- Limitations: Not designed for statistical inference --- no standard errors,
no p-values, no confidence intervals by default. R users expecting summary() output from a regression will be disappointed. For causal inference and hypothesis testing, use pyfixest or statsmodels instead.
---
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
- Relevance to DAAF: High
- Currency concern: None --- 2nd edition available; code examples 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).
- Strengths: The triple-language code examples make this the best single
resource for seeing how the same causal inference method is implemented across ecosystems. Conceptual explanations are exceptionally clear. Free online access.
- Limitations: Python examples tend to use pandas rather than polars.
Translation to the DAAF polars stack requires an additional step, but the methodology and logic transfer directly.
Using R, Python, and Julia for Introductory Econometrics
- Author(s): Florian Heiss, Daniel Brunner
- URL: https://www.urfie.net/
- Type: Book (free online)
- Last verified: 2026-03-28
- Quality: Good
- Relevance to DAAF: High
- Currency concern: Minor --- examples use standard econometrics libraries,
which are stable
- Key content: Three parallel textbooks implementing Wooldridge's
"Introductory Econometrics" examples in R, Python, and Julia respectively. Covers regression, time series, panel data, instrumental variables, and limited dependent variables. Includes Monte Carlo simulations and formula derivation demonstrations.
- Strengths: The parallel structure means every example exists in all three
languages with identical data and expected results. Excellent for verifying that your Python translation of an R analysis produces the same numbers. Built on a widely-used econometrics textbook (Wooldridge).
- Limitations: Python examples use pandas and statsmodels, not polars or
pyfixest. The books are self-published and may lag behind the latest library versions, though the core econometric content is timeless.
R for Data Science (2nd Edition)
- Author(s): Hadley Wickham, Mine Cetinkaya-Rundel, Garrett Grolemund
- URL: https://r4ds.hadley.nz/
- Type: Book (free online)
- Last verified: 2026-03-28
- Quality: Excellent
- Relevance to DAAF: Medium (R-only, but essential tidyverse reference)
- Currency concern: None --- 2nd edition published 2023, covers modern
tidyverse
- Key content: The definitive guide to the tidyverse ecosystem: data import,
tidying, transformation (dplyr), visualization (ggplot2), and communication. R-only but essential for understanding the R idioms that DAAF's Python stack translates from.
- Strengths: If an R user says "I do it the R4DS way," this book defines
what that means. Understanding dplyr patterns here maps directly to polars translations in the DAAF polars-tidyverse reference. Free online.
- Limitations: R-only. No Python code. Value is as the "source language"
reference, not the target.
Python for Data Analysis (3rd Edition)
- Author(s): Wes McKinney
- URL: https://wesmckinney.com/book/
- Type: Book (free online)
- Last verified: 2026-03-28
- Quality: Good
- Relevance to DAAF: Low-Medium (pandas-focused, DAAF uses polars)
- Currency concern: Minor --- updated for pandas 2.0 and Python 3.10, but
DAAF's primary data manipulation library is polars
- Key content: Comprehensive guide to data manipulation with pandas, NumPy,
and Jupyter. Covers data loading, cleaning, transformation, time series, and visualization. Written by the creator of pandas.
- Strengths: Authoritative pandas reference. Useful when R users encounter
pandas code in examples, documentation, or Stack Overflow answers and need to understand it before translating to polars. Free HTML version available.
- Limitations: Entirely pandas-focused. DAAF uses polars as its primary
data manipulation library, so this book is a secondary reference rather than a primary guide. R users should learn polars directly rather than going through pandas as an intermediate step.
Causal Inference: The Mixtape
- Author(s): Scott Cunningham
- URL: https://mixtape.scunning.com/
- Type: Book (free online)
- Last verified: 2026-03-28
- Quality: Good
- Relevance to DAAF: Medium
- 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.
- Strengths: Accessible writing style with real-world examples. The
companion Mixtape Sessions workshops (mixtapesessions.io) provide hands-on training with Python and Stata implementations. Community Python translations are available on GitHub (alexanderthclark/Causal-Inference-Mixtape).
- Limitations: Official book does not include Python code --- the Python
notebooks are community-contributed and may not be fully maintained. Prefer "The Effect" (above) for native three-language support.
---
General R-to-Python Guides
Coding for Economists: Coming from R
- Author(s): Arthur Turrell and contributors
- URL: https://aeturrell.github.io/coding-for-economists/coming-from-r.html
- Type: Guide (chapter in free online book)
- Last verified: 2026-03-28
- Quality: Excellent
- Relevance to DAAF: High
- Currency concern: None --- actively maintained
- Key content: Dedicated chapter for R users transitioning to Python for
economics work. Provides a detailed package equivalency table mapping R packages to Python counterparts, side-by-side code comparisons for common operations, and guidance on fundamental language differences (0-based indexing, = assignment, general-purpose vs statistical language). Recommends polars as the dplyr-like option.
- Strengths: Written specifically for economists, not generic data
scientists. Covers the exact package stack relevant to DAAF (polars, plotnine/lets-plot, statsmodels). The broader book covers econometrics, causal inference, time series, and reproducibility in Python.
- Limitations: The broader book is opinionated toward pandas in some
chapters, though the "Coming from R" section correctly identifies polars as the more dplyr-like option.
Polars' Rgonomic Patterns
- Author(s): Emily Riederer
- URL: https://www.emilyriederer.com/post/py-rgo-polars/
- Type: Blog
- Last verified: 2026-03-28
- Quality: Excellent
- Relevance to DAAF: High
- Currency concern: None --- published January 2024; polars API is stable
- Key content: Deep analysis of how polars mirrors dplyr's ergonomic design
patterns. Covers basic operations (select, filter, mutate, summarize), row-wise operations, dynamic column selectors, window functions (over() as equivalent to grouped mutate), and nested data structures.
- Strengths: Goes beyond surface syntax comparison to analyze why polars
feels natural to R users. The emphasis on "complex transformations precisely, concisely, and expressively" captures what dplyr users actually value. Written by a well-known data science practitioner with deep R expertise.
- Limitations: A single blog post, not a comprehensive reference. Best used
as conceptual orientation alongside the DAAF polars-tidyverse reference file.
Tidyverse to Polars: My Notes
- Author(s): Ken Koon Wong
- URL: https://www.kenkoonwong.com/blog/polars/
- Type: Blog
- Last verified: 2026-03-28
- Quality: Good
- Relevance to DAAF: High
- Currency concern: None --- practical examples use current polars API
- Key content: Hands-on notes from an R user learning polars by translating
familiar tidyverse operations. Covers filtering, selection, summarization, mutation, string extraction, conditional logic (case_when to when/then), grouping, joining, and pivoting with side-by-side R and Python code.
- Strengths: Written from the learner's perspective, capturing the exact
"I tried this R pattern, here's what polars needs instead" moments that are most useful for transitioning users. Practical and example-driven.
- Limitations: Not exhaustive. Covers common operations but skips advanced
topics like lazy evaluation, window functions, and complex joins.
A Tidyverse R and Polars Python Side-by-Side
- Author(s): Robert Mitchell
- URL: https://robertmitchellv.com/blog/2022-07-r-python-side-by-side/r-python-side-by-side.html
- Type: Blog
- Last verified: 2026-03-28
- Quality: Good
- Relevance to DAAF: Medium
- Currency concern: Minor --- published 2022; polars API has evolved but core
patterns remain valid
- Key content: Side-by-side demonstration of tidyverse and polars for data
manipulation and visualization using the gapminder dataset. Covers filtering, aggregation, conditional logic, and interactive plotting with plotly in both languages.
- Strengths: Clear visual layout with R and Python code blocks adjacent.
Good introduction to the "think in R, write in polars" approach.
- Limitations: Uses an older polars API version. Some method names may have
changed (e.g., groupby to group_by). Cross-reference with current polars documentation.
Tidy Data Manipulation: dplyr vs polars
- Author(s): Christoph Scheuch (Tidy Intelligence)
- URL: https://blog.tidy-intelligence.com/posts/dplyr-vs-polars/
- Type: Blog
- Last verified: 2026-03-28 (returned 403 on fetch; site appears intermittently restricted)
- Quality: Good
- Relevance to DAAF: High
- Currency concern: Minor --- verify accessibility before relying on it
- Key content: Systematic comparison of dplyr and polars covering mutate vs
with_columns, filter, select, arrange vs sort, group_by/summarize vs group_by/agg, and pivot operations. Highlights the non-standard evaluation difference (dplyr uses bare column names; polars requires pl.col()).
- Strengths: Methodical function-by-function comparison. Identifies the key
conceptual difference that dplyr allows referencing new columns in the same mutate block while polars does not.
- Limitations: Site returned 403 on some access attempts. Content may be
behind access restrictions intermittently.
Comparing dplyr with polars
- Author(s): krz (GitHub user)
- URL: https://krz.github.io/Comparing-dplyr-with-polars/
- Type: Guide
- Last verified: 2026-03-28
- Quality: Fair
- Relevance to DAAF: Medium
- Currency concern: Minor --- verify against current polars API
- Key content: Concise comparison of dplyr and polars operations including
selection, filtering, mutation, summarization, and joins.
- Strengths: Brief and focused. Good for a quick lookup of "how do I do
this dplyr thing in polars?"
- Limitations: Less detailed than the Riederer or Wong posts. Author
background is unclear. Use as a quick-reference supplement, not a primary learning resource.
Python and R for the Modern Data Scientist
- Author(s): Rick J. Scavetta, Boyan Angelov
- URL: https://www.oreilly.com/library/view/python-and-r/9781492093398/
- Type: Book (O'Reilly, paid)
- Last verified: 2026-03-28
- Quality: Fair
- Relevance to DAAF: Low
- Currency concern: Minor --- published 2021; predates polars adoption
- Key content: Guides data scientists from either the R or Python community
toward bilingual proficiency. Covers parallel structures, where each language excels, and practical examples of using both together.
- Strengths: Well-organized, addresses the cultural differences between R
and Python communities. Some reviewers praise its clarity and balance.
- Limitations: Reviews are mixed --- some find it "long on opinions and short
on well-supported arguments." The practical substance is concentrated in the final chapters. Predates polars and modern pyfixest. Not free. Not recommended as a primary resource for DAAF users.
---
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
- Relevance to DAAF: High
- Currency concern: None --- regularly updated
- Key content: Curated collection of econometrics learning materials including
data manipulation tutorials (R and Python), data access packages, animated causal inference visualizations, video lecture series, and links to major econometrics textbooks. Includes an "Econometrics Navigator" and links to R-for-Economists video series.
- Strengths: Maintained by the author of "The Effect." Covers both R and
Python (with more emphasis on R). The animated causal inference plots are uniquely valuable for building intuition. Comprehensive data access package list (wbstats, tidycensus, fredr, ipumsr, etc.).
- Limitations: Python coverage is less extensive than R. Best used alongside
"The Effect" textbook for methodology, with DAAF skill files for Python implementation specifics.
Tidy Fixed Effects Regressions: fixest vs pyfixest
- Author(s): Christoph Scheuch (Tidy Intelligence)
- URL: https://blog.tidy-intelligence.com/posts/fixed-effects-regressions/
- Type: Blog
- Last verified: 2026-03-28 (site returned 403 on some attempts)
- Quality: Good
- Relevance to DAAF: High
- Currency concern: Minor --- verify accessibility
- Key content: Direct side-by-side comparison of R fixest and Python pyfixest
for fixed effects regression, covering model specification, standard errors, and output formatting.
- Strengths: Practical, focused comparison of the exact R-to-Python
translation that DAAF users need most for econometric work.
- Limitations: Site access may be intermittent (403 errors observed).
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
- Relevance to DAAF: Medium
- 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. The flagship Causal Inference I and II courses cover potential outcomes, matching, IV, RD, DiD, and synthetic control. Materials are publicly available on GitHub (Mixtape-Sessions organization).
- Strengths: Taught by leading applied econometricians. Workshop format
provides structured learning with real-world exercises. Python implementations are actively maintained. Materials are open-access after the workshop.
- Limitations: Paid workshops (materials free afterward). Python code uses
pandas, not polars. Focus is on methodology, not data manipulation workflow.
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
- Relevance to DAAF: High
- Currency concern: None
- Key content: Comprehensive Python guide for economists covering programming
basics, data handling, visualization, econometrics (OLS, IV, causal inference), time series, machine learning, text analysis, geospatial analysis, and reproducible research. Designed to take economists from zero coding experience to productive Python users.
- Strengths: The most complete single resource for economists learning Python.
Covers the full research workflow, not just data manipulation. Includes chapters on reproducibility and software engineering practices that align with DAAF's philosophy.
- Limitations: Some chapters lean toward pandas over polars. The econometrics
coverage is introductory --- for advanced methods, use DAAF's pyfixest and statsmodels skill files.
---
R Package Documentation (for Reference)
These are the authoritative R package documentation sites. R users should know these as the "source" for the patterns that DAAF's Python stack translates.
dplyr (tidyverse)
- URL: https://dplyr.tidyverse.org/
- Type: R package documentation
- Last verified: 2026-03-28
- DAAF Python equivalent: polars (
pl.col(),.filter(),.with_columns(),
.group_by().agg(), .join(), .sort())
- Key mapping:
mutate()towith_columns(),filter()tofilter(),
select() to select(), arrange() to sort(), group_by() %>% summarize() to group_by().agg(), left_join() to join(how="left")
ggplot2 (tidyverse)
- URL: https://ggplot2.tidyverse.org/
- Type: R package documentation
- Last verified: 2026-03-28
- DAAF Python equivalent: plotnine (near-identical syntax)
- Key mapping: Syntax is almost 1:1. Main differences: Python requires
parentheses around the full plot expression, imports from plotnine instead of loading library(ggplot2), and uses = instead of <- for assignment.
fixest
- URL: https://lrberge.github.io/fixest/
- Type: R package documentation
- Last verified: 2026-03-28
- DAAF Python equivalent: pyfixest (intentionally parallel API)
- Key mapping:
feols()topf.feols(),fepois()topf.fepois(),
etable() to pf.etable(), coefplot() to pf.coefplot(). Formula syntax is identical: Y ~ X1 + X2 | fe1 + fe2. Note: feglm() with FE is not yet supported in pyfixest.
survey
- URL: https://cran.r-project.org/package=survey
- Homepage: http://r-survey.r-forge.r-project.org/survey/
- Type: R package documentation
- Last verified: 2026-03-28
- DAAF Python equivalent: svy (see DAAF svy skill)
- Key mapping: No single Python package replicates the full survey package.
DAAF's svy skill documents the multi-library approach needed for design-based inference in Python.
sf (r-spatial)
- URL: https://r-spatial.github.io/sf/
- Type: R package documentation
- Last verified: 2026-03-28
- DAAF Python equivalent: geopandas
- Key mapping:
st_read()togpd.read_file(),st_join()to
gpd.sjoin(), st_transform() to gdf.to_crs(), st_buffer() to gdf.buffer(). Both libraries build on GDAL/GEOS/PROJ. See the DAAF geopandas skill for detailed translation patterns.
R-to-Python Gotchas and False Friends
Common mistakes R users make when writing Python, organized from most dangerous/frequent to least. Each entry documents what R users expect, what actually happens in Python, and the correct approach.
This reference focuses on the DAAF Python stack: polars for data manipulation, pyfixest/statsmodels for modeling, and plotnine for visualization.
Versions referenced:
Python: polars 1.38.1, pyfixest 0.40.0, statsmodels 0.14.6
R: R 4.5.3
See SKILL.md § Library Versions for the complete version table.
Contents
- False Friends: Syntax
- Data Manipulation Traps
- Modeling Traps
- Environment Traps
- Common Error Messages Translated
False Friends: Syntax
These are constructs that look similar between R and Python but behave differently. Each one has caught experienced R users off guard.
| R | Python Attempt | Trap | Correct Python |
|---|---|---|---|
df[1,] (first row) | df[1] | Polars does not support bracket indexing on DataFrames | df.head(1) or df.row(0) |
df$column | df.column | AttributeError --- polars DataFrames are not namespaces | df["column"] or pl.col("column") in expressions |
TRUE / FALSE | TRUE / FALSE | NameError --- Python is case-sensitive | True / False |
T / F | T / F | R allows abbreviation; Python has no such aliases | True / False |
NA | NA | Three distinct missingness types in Python | None, float("nan"), or pl.col("x").is_null() |
NULL | NULL | NameError | None |
c(1, 2, 3) | c(1, 2, 3) | No c() function in Python | [1, 2, 3] or pl.Series([1, 2, 3]) |
<- | <- | Parsed as < - (less-than negative), not assignment | = for assignment |
x <- x %>% ... | x = x.pipe(...) | No pipe operator; method chaining or reassignment | x = x.filter(...).with_columns(...) |
paste0(a, b) | paste0(a, b) | No paste0 in Python | f"{a}{b}" or a + b for strings |
paste(a, b) | N/A | paste() joins with space by default | f"{a} {b}" or " ".join([a, b]) |
%% (modulo) | %% | SyntaxError in Python | % |
%/% (integer division) | %/% | SyntaxError in Python | // |
& / ` | ` (vectorized) | & / ` | ` |
&& / ` | ` (scalar) | && / ` | |
1:10 | 1:10 | Slice notation, not sequence generation | range(1, 11) or list(range(1, 11)) |
x %in% c(1,2,3) | x in [1,2,3] | Works for scalars; for polars columns use .is_in() | pl.col("x").is_in([1, 2, 3]) |
!x (logical NOT) | !x | SyntaxError for scalar; ~ for polars expressions | not x (scalar) or ~pl.col("x") (expression) |
nrow(df) | nrow(df) | No nrow() function | df.height or len(df) |
ncol(df) | ncol(df) | No ncol() function | df.width |
names(df) | names(df) | No names() for DataFrames | df.columns |
Data Manipulation Traps
1. Forgetting .alias() in with_columns()
Severity: Very high --- silent overwrites or cryptic errors.
What R users expect: In dplyr, mutate(new_col = expr) names the column via the left-hand side of the =.
What happens in Python:
# WRONG: overwrites the source column or produces unnamed result
df.with_columns(pl.col("x") * 2)
# RIGHT: explicit naming with .alias()
df.with_columns((pl.col("x") * 2).alias("x_doubled"))Without .alias(), polars uses the input column name as the output name, silently replacing the original column. This is the single most common gotcha for R-to-polars transitions.
2. Using == to Check for Nulls
Severity: Very high --- silently returns no matches.
What R users expect: is.na(x) is a dedicated function, but some R users also use x == NA (which also does not work in R, returning NA).
What happens in Python:
# WRONG: null is not equal to anything, including itself
df.filter(pl.col("x") == None)
# RIGHT: dedicated null check
df.filter(pl.col("x").is_null())
df.filter(pl.col("x").is_not_null())3. Direct Column Assignment on DataFrames
Severity: High --- immediate error, but confusing for newcomers.
What R users expect: df$col <- value or df[,"col"] <- value for in-place assignment.
What happens in Python:
# WRONG: polars DataFrames are immutable
df["new_col"] = some_series # TypeError
# RIGHT: create a new DataFrame with the column added
df = df.with_columns(pl.lit("constant").alias("new_col"))Polars enforces immutability by design. Every transformation returns a new DataFrame. This is a fundamental paradigm shift from R's copy-on-modify semantics.
4. Forgetting Parentheses in Compound Filters
Severity: High --- operator precedence error.
What R users expect: filter(df, x > 5 & y < 10) works because R's & has lower precedence than comparison operators.
What happens in Python:
# WRONG: & binds tighter than > and < in Python
df.filter(pl.col("x") > 5 & pl.col("y") < 10) # TypeError
# RIGHT: parentheses around each comparison
df.filter((pl.col("x") > 5) & (pl.col("y") < 10))Python's bitwise & has higher precedence than comparison operators. This is the opposite of what R users expect and produces confusing TypeError messages about incompatible types.
5. Expecting group_by().agg() to Keep All Columns
Severity: Medium --- unexpected column loss.
What R users expect: dplyr's group_by() keeps all columns; only summarize() drops non-grouped, non-aggregated columns.
What happens in Python:
# Returns ONLY grouping columns + explicitly aggregated columns
result = df.group_by("state").agg(pl.col("income").mean())
# result has columns: ["state", "income"] --- nothing else
# To keep other columns, aggregate them too or use a different approach
result = df.group_by("state").agg(
pl.col("income").mean().alias("mean_income"),
pl.col("population").first()
)6. Bare Literals in Expressions
Severity: Medium --- error or silent wrong behavior.
What R users expect: mutate(df, x = "constant") just works.
What happens in Python:
# WRONG: bare string in expression context
df.with_columns(pl.col("x") + "suffix") # May error
# RIGHT: wrap scalar values in pl.lit()
df.with_columns(pl.lit("constant").alias("label"))
df.with_columns((pl.col("x") + pl.lit(100)).alias("x_plus"))Use pl.lit() to wrap any scalar value (string, number, boolean) that appears in a polars expression context.
7. Expecting group_by() to Preserve Row Order
Severity: Medium --- silently reordered output.
What R users expect: dplyr's group_by() %>% summarize() preserves the order groups first appeared.
What happens in Python:
# group_by() does NOT guarantee output order
result = df.group_by("category").agg(pl.col("value").sum())
# Row order is non-deterministic
# To get deterministic order, sort explicitly
result = df.group_by("category").agg(
pl.col("value").sum()
).sort("category")Polars sort() is stable, but group_by() is not order-preserving. If order matters, always sort after aggregation.
8. Expecting library() Semantics from import
Severity: Low --- immediate errors, easy to fix.
What R users expect: library(dplyr) makes filter(), mutate(), select() etc. available as bare functions.
What happens in Python:
import polars as pl
# WRONG: no bare filter() or select()
filter(df, condition) # This calls Python's built-in filter(), not polars
# RIGHT: always use the pl prefix or method syntax
df.filter(pl.col("x") > 5)
df.select("col1", "col2")9. Referring to Newly Created Columns in the Same Step
Severity: Medium --- error or stale values.
What R users expect: dplyr's mutate() allows referencing a column created earlier in the same call: mutate(a = x + 1, b = a * 2).
What happens in Python:
# WRONG: "a" does not exist yet within this with_columns call
df.with_columns(
(pl.col("x") + 1).alias("a"),
(pl.col("a") * 2).alias("b") # ColumnNotFoundError
)
# RIGHT: chain two with_columns calls
df = df.with_columns((pl.col("x") + 1).alias("a"))
df = df.with_columns((pl.col("a") * 2).alias("b"))Polars evaluates all expressions in a single with_columns() in parallel, so newly created columns are not visible to sibling expressions.
Modeling Traps
1. statsmodels Requires Explicit .fit()
What R users expect: lm(y ~ x, data = df) returns a fitted model.
What happens in Python:
import statsmodels.formula.api as smf
# WRONG: model is unfitted, results will error
model = smf.ols("y ~ x", data=df)
model.summary() # AttributeError
# RIGHT: call .fit() to get results
results = smf.ols("y ~ x", data=df).fit()
results.summary() # WorksThis two-step pattern (specify, then fit) applies to statsmodels, pyfixest, and scikit-learn. R's lm(), glm(), and feols() combine both steps. pyfixest's pf.feols() does auto-fit, matching R behavior.
2. Polars-to-Pandas Conversion for Modeling
What R users expect: Data flows directly into model functions.
What happens in Python:
import pyfixest as pf
# WRONG: pyfixest expects pandas
fit = pf.feols("y ~ x | fe", data=df_polars) # TypeError
# RIGHT: convert first
fit = pf.feols("y ~ x | fe", data=df_polars.to_pandas())pyfixest, statsmodels, and scikit-learn all expect pandas DataFrames (or numpy arrays). Always call .to_pandas() before passing polars data to modeling functions.
3. Intercept Handling Differs Across Libraries
What R users expect: lm(y ~ x) includes an intercept by default.
What happens in Python:
| Library | Default | Suppress Intercept |
|---|---|---|
| statsmodels (formula API) | Intercept included | y ~ x - 1 or y ~ 0 + x |
| pyfixest | Intercept included (absorbed into FE when present) | y ~ x - 1 |
| scikit-learn | Intercept included (fit_intercept=True is default) | fit_intercept=False to suppress |
The real intercept gotcha is with statsmodels' array API: sm.OLS(y, X) does NOT add an intercept — you must use sm.add_constant(X) explicitly. The formula API (smf.ols("y ~ x")) includes it by default, matching R.
4. Factor/Categorical Handling in Formulas
What R users expect: R auto-creates dummy variables from factors in formulas. lm(y ~ factor(x)) just works.
What happens in Python:
# statsmodels (patsy): use C() for categorical
results = smf.ols("y ~ C(region)", data=df).fit()
# pyfixest: use i() for interactions, C() for main effects
fit = pf.feols("y ~ i(treat, ref=0)", data=df)Python will not auto-detect categorical columns from the data. You must explicitly wrap categorical variables in C() (statsmodels) or i() (pyfixest) in the formula.
5. Default Standard Error Types
What R users expect: lm() defaults to classical/IID SEs. feols() in fixest also defaults to IID (since fixest 0.13).
What happens in Python:
| Library | Default SE | Notes |
|---|---|---|
| pyfixest (v0.40+) | IID | Aligned with R fixest 0.13 |
| statsmodels OLS | Non-robust (IID) | Use .get_robustcov_results() for robust |
| scikit-learn | No SEs provided | Not a statistical inference tool |
pyfixest's alignment with fixest means R users get familiar defaults. However, pre-v0.40 pyfixest defaulted to clustering by the first FE, so older code may produce different results.
Environment Traps
1. Working Directory Assumptions
What R users expect: setwd() changes the working directory globally; read.csv("data.csv") reads from the working directory.
What to do in Python: Avoid os.chdir(). DAAF enforces absolute paths for all file operations. Use pathlib.Path or string constants for paths:
BASE_DIR = "/path/to/project"
df = pl.read_parquet(f"{BASE_DIR}/data/raw/dataset.parquet")2. Random Seeds
What R users expect: set.seed(42) before stochastic operations.
What to do in Python:
import numpy as np
np.random.seed(42) # NumPy operations
# Or, for newer NumPy:
rng = np.random.default_rng(42)The seed must be set before each stochastic call if exact reproducibility is needed. Different libraries (numpy, random, scipy) have independent RNG states.
3. Auto-Printing in Scripts
What R users expect: Typing df at the console prints it. In R scripts, the last expression in a block auto-prints.
What happens in Python: In scripts (which DAAF always uses), nothing prints unless you explicitly call print():
df.head() # Computes but displays nothing in a script
print(df.head()) # Actually shows the outputThis is critical for DAAF's file-first execution pattern --- every validation must use explicit print() or assert to appear in the captured output.
4. Return Values and Last-Expression Evaluation
What R users expect: The last expression in an R function is its return value. No explicit return needed.
What happens in Python: Functions require explicit return. However, DAAF uses sequential inline scripts without function definitions, so this is rarely encountered. Be aware of it when reading library source code or documentation examples.
5. 1-Based vs 0-Based Indexing
What R users expect: x[1] is the first element.
What happens in Python: x[0] is the first element. This applies to lists, tuples, numpy arrays, and string indexing. Polars avoids this issue by using named column access and expression-based row selection rather than numeric indexing.
Common Error Messages Translated
| R Error | Python Equivalent | Meaning |
|---|---|---|
object 'x' not found | NameError: name 'x' is not defined | Variable does not exist in scope |
could not find function "f" | AttributeError or ImportError | Package not imported or function misspelled |
arguments imply differing number of rows | ShapeError (polars) | Column lengths do not match |
non-numeric argument to binary operator | TypeError | Wrong types in arithmetic operation |
replacement has X rows, data has Y | ShapeError (polars) | Length mismatch in column assignment |
$ operator is invalid for atomic vectors | TypeError: 'int' object is not subscriptable | Trying to index a scalar |
Error in if (...) : missing value where TRUE/FALSE needed | TypeError with polars expressions | Null in a boolean context |
cannot coerce type 'character' to ... | InvalidOperationError: ... cast ... | Type conversion failure |
subscript out of bounds | IndexError: list index out of range | Index exceeds collection length |
unused argument | TypeError: got an unexpected keyword argument | Wrong parameter name |
cannot open connection / No such file | FileNotFoundError | File path is wrong |
package 'x' is not available | ModuleNotFoundError: No module named 'x' | Package not installed |
Quick Diagnostic Table
| Problem | Quick Fix |
|---|---|
| Column silently overwritten | Add .alias("new_name") |
| Null check finds nothing | Use .is_null() not == None |
| Compound filter fails | Wrap each comparison in parentheses |
| New column not visible in same step | Chain separate .with_columns() calls |
| Model function returns unfitted object | Call .fit() on the model |
| Polars DataFrame rejected by model | Call .to_pandas() before passing |
| Bare literal in expression | Wrap with pl.lit() |
group_by output missing columns | Explicitly aggregate every column you need |
| Print produces no output in script | Use explicit print() |
| Off-by-one in sequence | Python uses 0-based indexing; range() excludes the endpoint |
Paradigm Differences: R vs Python for Quantitative Social Science
This reference documents the fundamental language and paradigm differences between R and Python (with polars) as they affect quantitative social science data analysis. It is the foundational reference that other translation files build upon.
Versions referenced:
Python: Python 3.12, polars 1.38.1
R: R 4.5.3
See SKILL.md § Library Versions for the complete version table.
Contents
- Indexing
- Missing Values
- Formula Interfaces
- Assignment and Mutability
- Vectorized Operations
- Factor / Categorical Handling
- Type System
- Package Ecosystem Philosophy
- Data Frame Philosophy
- String Handling
- Date/Time Handling
- File I/O
- Environment and Scoping
---
Indexing
R uses 1-based indexing; Python uses 0-based. Polars discourages positional indexing entirely and favors named or expression-based access.
| Operation | R | Python / Polars |
|---|---|---|
| First element | x[1] | x[0] |
| First row | df[1, ] | df.head(1) or df.row(0) |
| Rows 1-5 | df[1:5, ] (inclusive) | df.head(5) or df.slice(0, 5) |
| Last element | x[length(x)] | x[-1] |
| Column by name | df$col | df.select("col") or df["col"] |
What R users expect: df[1, ] returns the first row. What happens in Python: df[1] would try to index column "1" in polars. Use df.row(0) for a tuple, or df.head(1) for a one-row DataFrame.
# R — 1-based, inclusive ranges
x <- c(10, 20, 30, 40)
x[1] # 10
x[2:4] # 20, 30, 40 (inclusive on both ends)# Python — 0-based, exclusive upper bound
x = [10, 20, 30, 40]
x[0] # 10
x[1:4] # [20, 30, 40] (exclusive upper bound)
# Polars — expression-based, positional access discouraged
df.row(0) # first row as tuple
df.row(0, named=True) # first row as dict
df.slice(1, 3) # 3 rows starting at offset 1Sources: R Language Definition -- Indexing (CRAN, accessed 2026-03-28);
Polars User Guide -- Coming from Pandas (docs.pola.rs, accessed 2026-03-28)
---
Missing Values
This is the single largest source of translation bugs. R has one unified missing value system; Python has three distinct representations that do not behave alike.
R: Unified NA
R has a single sentinel NA with typed variants (NA_real_, NA_character_, NA_integer_). All share consistent behavior:
NApropagates through arithmetic:1 + NAyieldsNANApropagates through comparison:NA == NAyieldsNA(notTRUE)- Logical short-circuit:
TRUE | NAyieldsTRUE;FALSE & NAyieldsFALSE - Universal detection:
is.na(x)works on any type - Aggregation control:
mean(x, na.rm = TRUE)skips NAs
Python: Three Kinds of Missing
| Representation | Scope | Detection | Behavior |
|---|---|---|---|
None | Python-level | x is None | Coerced to null in polars |
float("nan") / np.nan | Float only | math.isnan(x) | NaN + 1 = NaN; NaN != NaN is True |
null (polars) | All types | .is_null() | Skipped by aggregations |
Critical trap: NaN and null are different in polars. null is true missingness (aggregations skip it). NaN is a valid IEEE 754 float (aggregations propagate it). fill_null() does NOT fill NaN; fill_nan() does NOT fill null.
# R — one system
x <- c(1, NA, 3)
is.na(x) # FALSE, TRUE, FALSE
mean(x, na.rm = TRUE) # 2# Polars — null is the primary missing representation
df = pl.DataFrame({"x": [1, None, 3]})
df.filter(pl.col("x") == None) # WRONG — returns empty DataFrame
df.filter(pl.col("x").is_null()) # RIGHT — returns the null row
# NaN vs null — the dangerous case
df = pl.DataFrame({"x": [1.0, float("nan"), None]})
df.select(pl.col("x").mean()) # NaN (propagates!)
df.select(pl.col("x").fill_nan(None).mean()) # 1.0 (safe pattern)Common Translation Patterns
| R | Python / Polars |
|---|---|
is.na(x) | pl.col("x").is_null() |
!is.na(x) | pl.col("x").is_not_null() |
na.rm = TRUE | Default in polars aggregations (nulls skipped) |
complete.cases(df) | df.drop_nulls() |
replace(x, is.na(x), 0) | pl.col("x").fill_null(0) |
coalesce(x, y) (dplyr) | pl.coalesce("x", "y") |
Sources: R Language Definition -- NA (stat.ethz.ch/R-manual, accessed 2026-03-28);
Polars User Guide -- Missing Data (docs.pola.rs, accessed 2026-03-28);
Wickham, Advanced R 2nd ed., Ch. 3.5.1 (2019)
---
Formula Interfaces
R has one universal formula system. Python has three incompatible dialects.
R: Universal Formula
lm(y ~ x1 + x2, data = df) # OLS
glm(y ~ x1 + x2, data = df, family = binomial) # logit
fixest::feols(y ~ x1 + x2 | fe1, data = df) # FE regression
survey::svyglm(y ~ x1 + x2, design = svy_design) # survey-weightedThe formula y ~ x1 + x2 auto-includes an intercept, auto-dummies factors, and generates interactions with x1:x2 or x1*x2. One syntax everywhere.
Python: Three Dialects
1. patsy (statsmodels) -- R-like formulas via smf:
model = smf.ols("y ~ x1 + x2", data=pdf).fit() # OLS
model = smf.ols("y ~ x1 + x2 + C(group)", data=pdf).fit() # with factor- Auto-adds intercept;
C(var)marks categoricals; requires pandas DataFrame
2. formulaic (pyfixest) -- closest to R's fixest:
model = pf.feols("y ~ x1 + x2 | fe1", data=pdf) # FE
model = pf.feols("y ~ 1 | fe1 | x_endog ~ z1", data=pdf) # IV with FE- Supports
i(),C(),sw(),csw(); accepts polars or pandas
3. No formula (scikit-learn) -- matrix-based:
X = df.select("x1", "x2").to_numpy()
y = df.select("y").to_numpy().ravel()
model = LinearRegression().fit(X, y)- Manual feature matrix; manual dummy coding; no automatic intercept
The Same Model in Four Syntaxes
# R (fixest)
fixest::feols(wage ~ education + experience | industry, data = df)# pyfixest — closest to R
pf.feols("wage ~ education + experience | industry", data=pdf)
# statsmodels — no built-in FE, must dummy-code
smf.ols("wage ~ education + experience + C(industry)", data=pdf).fit()
# scikit-learn — fully manual
X = pd.get_dummies(pdf[["education", "experience", "industry"]], drop_first=True)
LinearRegression().fit(X, pdf["wage"])Sources: patsy docs -- How formulas work (patsy.readthedocs.io, accessed 2026-03-28);
statsmodels 0.14 -- R-style formulas (statsmodels.org, accessed 2026-03-28);
pyfixest docs -- Formula syntax (pyfixest.org, accessed 2026-03-28)
---
Assignment and Mutability
R's copy-on-modify semantics create independent copies by default. Python's reference semantics create aliases to the same object.
# R — copy-on-modify: modifying df2 never changes df
df2 <- df
df2$new_col <- 1 # triggers a copy; df is unchanged# Python — reference semantics: df2 IS df
df2 = df
# df2[0, "col"] = 1 would modify df too (in pandas)
# Fix: explicit copy
df2 = df.clone() # polars
pdf2 = pdf.copy() # pandasPolars mitigates this: Most polars operations return new DataFrames rather than modifying in place (with_columns(), filter(), etc.), which is closer to R's behavior. The risk surfaces when mixing polars with pandas or mutable Python objects.
Sources: Wickham, Advanced R 2nd ed., Ch. 2.3 -- Copy-on-modify (2019);
Polars API -- DataFrame.clone (docs.pola.rs, accessed 2026-03-28)
---
Vectorized Operations
R implicitly vectorizes nearly all operations. Polars requires the expression system inside a context.
# R — implicit vectorization, everything "just works"
x <- c(1, 2, 3, 4, 5)
x * 2 # c(2, 4, 6, 8, 10)
ifelse(x > 3, "high", "low") # vectorized conditional
df$z <- df$x * 2 + df$y # direct column arithmetic# Polars — expressions inside contexts
df = df.with_columns(
(pl.col("x") * 2).alias("x_doubled"),
pl.when(pl.col("x") > 3)
.then(pl.lit("high"))
.otherwise(pl.lit("low"))
.alias("category"),
)The expression system is the single biggest paradigm shift for R users:
- Columns are referenced via
pl.col("name"), not bare names - Results need
.alias()to name the output column - Expressions must live inside a context:
select(),with_columns(),
filter(), or group_by().agg()
- Expressions outside a context are lazy blueprints, not evaluated values
| R | Python / Polars |
|---|---|
df$z <- df$x * 2 | df = df.with_columns((pl.col("x") * 2).alias("z")) |
ifelse(cond, a, b) | pl.when(cond).then(a).otherwise(b) |
case_when(...) | Chained pl.when().then().when().then().otherwise() |
pmax(x, y) | pl.max_horizontal("x", "y") |
cumsum(x) | pl.col("x").cum_sum() |
rowSums(df[, cols]) | pl.sum_horizontal(cols) |
Sources: R Language Definition -- Vectorized operations (CRAN, accessed 2026-03-28);
Polars User Guide -- Expressions and contexts (docs.pola.rs, accessed 2026-03-28)
---
Factor / Categorical Handling
R factors are a first-class statistical type with automatic dummy coding. Polars categoricals are a storage optimization with no regression integration.
# R — factors participate directly in modeling
x <- factor(c("low", "med", "high"), levels = c("low", "med", "high"))
lm(y ~ x, data = df) # auto-creates x[med] and x[high] dummies
contrasts(df$x) # shows the coding scheme
relevel(x, ref = "med") # change reference level# Polars — Categorical/Enum for storage, not modeling
df = df.with_columns(pl.col("group").cast(pl.Categorical))
size_type = pl.Enum(["small", "medium", "large"]) # ordered, known categories
df = df.with_columns(pl.col("size").cast(size_type))Polars Categorical and Enum reduce memory but do not auto-generate dummies. Use Enum when categories are fixed and known; Categorical otherwise.
Getting R-like Factor Behavior in Python
# pyfixest — closest to R
pf.feols("y ~ C(group)", data=pdf)
pf.feols("y ~ i(group, ref='low')", data=pdf)
# statsmodels — C() with explicit contrast
smf.ols("y ~ C(group, Treatment(reference='low'))", data=pdf).fit()
# Manual — when no formula interface is available
pdf_dummies = pd.get_dummies(pdf, columns=["group"], drop_first=True)Sources: UCLA Statistical Consulting -- Contrast coding (stats.oarc.ucla.edu, accessed 2026-03-28);
statsmodels 0.14 -- Contrast Coding Systems (statsmodels.org, accessed 2026-03-28);
Polars User Guide -- Categorical data and enums (docs.pola.rs, accessed 2026-03-28)
---
Type System
R coerces types implicitly along a hierarchy. Python and polars require explicit conversion.
| Behavior | R | Python / Polars |
|---|---|---|
| Bool + int | TRUE + 1 = 2 | True + 1 = 2 (bool subclasses int) |
| Mixed vector | c(1, "a") = c("1", "a") | TypeError or explicit cast |
| String to num | as.numeric("5") = 5 | int("5") or .cast(pl.Int64) |
| String + num | "5" + 1 = Error | "5" + 1 = TypeError |
R's coercion hierarchy: logical < integer < double < complex < character. Mixed types silently promote to the more general type.
# Polars — strict types, explicit .cast()
df = df.with_columns(
pl.col("str_number").cast(pl.Int64).alias("number"),
)
# pl.col("str_col") + pl.col("int_col") raises ComputeError| R | Python / Polars |
|---|---|
as.numeric(x) | pl.col("x").cast(pl.Float64) |
as.integer(x) | pl.col("x").cast(pl.Int64) |
as.character(x) | pl.col("x").cast(pl.Utf8) |
as.logical(x) | pl.col("x").cast(pl.Boolean) |
as.numeric(factor(x)) | pl.col("x").to_physical() (integer codes) |
Sources: R Language Definition -- Coercion (CRAN, accessed 2026-03-28);
Python docs -- Built-in Types (docs.python.org, accessed 2026-03-28);
Polars API -- Expr.cast (docs.pola.rs, accessed 2026-03-28)
---
Package Ecosystem Philosophy
R packages are comprehensive toolkits. Python packages are specialized, requiring composition of multiple libraries for equivalent coverage.
# R — one package (fixest) does everything
library(fixest)
feols(y ~ x1 | fe1, data = df) # OLS with FE
fepois(y ~ x1 | fe1, data = df) # Poisson with FE
feols(y ~ x1 | fe1 | endog ~ z1) # IV
etable(m1, m2, m3) # publication table
iplot(model) # coefficient plotThe same coverage in Python requires multiple packages:
| R (single package) | Python (DAAF) | Coverage |
|---|---|---|
fixest::feols() | pyfixest.feols() | OLS/FE/IV/Poisson |
fixest::etable() | pyfixest.etable() | Regression tables |
fixest::sunab() | pyfixest with sunab() | Sun-Abraham DiD |
survey::svyglm() | svy | Survey-weighted |
lme4::lmer() | statsmodels.MixedLM | Mixed effects |
survival::coxph() | lifelines.CoxPHFitter | Survival analysis |
marginaleffects::slopes() | marginaleffects (Python port) | Marginal effects |
R scripts typically have 2-3 library() calls; equivalent Python scripts may have 6-10 import statements. This fragmentation is normal and expected.
Sources: fixest CRAN vignette (cran.r-project.org, accessed 2026-03-28);
pyfixest documentation (pyfixest.org, accessed 2026-03-28)
---
Data Frame Philosophy
R uses one data frame type everywhere. DAAF uses polars for wrangling but must convert to pandas for most modeling packages -- a boundary with no R equivalent.
# R — same tibble everywhere: wrangle, model, plot
df %>% filter(x > 2) %>% mutate(z = x * 2)
lm(z ~ x, data = df)
ggplot(df, aes(x, z)) + geom_point()# DAAF pattern: polars → pandas → model → polars
df = pl.read_parquet("data.parquet") # polars for wrangling
df = df.filter(pl.col("x") > 2).with_columns(
(pl.col("x") * 2).alias("z")
)
pdf = df.to_pandas() # convert for modeling
model = smf.ols("z ~ x", data=pdf).fit() # statsmodels needs pandas
# pyfixest accepts polars directly:
model = pf.feols("z ~ x", data=df)The polars-pandas Boundary
1. Load and wrangle in polars (fast, expressive, memory-efficient) 2. Convert to pandas for modeling: pdf = df.to_pandas() 3. Convert back for storage: pl.from_pandas(result)
Pandas DataFrames carry an index (row labels) that polars lacks. When converting, reset_index() before pl.from_pandas() if the index contains meaningful data.
Sources: Wickham & Grolemund, R for Data Science 2nd ed. -- Tibbles (2023);
Polars User Guide -- Coming from Pandas (docs.pola.rs, accessed 2026-03-28)
---
String Handling
R's stringr uses str_* prefix functions; polars uses .str.* namespace methods.
| Operation | R (stringr) | Polars |
|---|---|---|
| Detect | str_detect(x, "abc") | pl.col("x").str.contains("abc") |
| Replace first | str_replace(x, "old", "new") | pl.col("x").str.replace("old", "new") |
| Replace all | str_replace_all(x, "old", "new") | pl.col("x").str.replace_all("old", "new") |
| Extract | str_extract(x, "\\d+") | pl.col("x").str.extract(r"(\d+)", 1) |
| Split | str_split(x, ",") | pl.col("x").str.split(",") |
| Trim | str_trim(x) | pl.col("x").str.strip_chars() |
| Upper/lower | str_to_upper(x) | pl.col("x").str.to_uppercase() |
| Length | str_length(x) | pl.col("x").str.len_chars() |
| Concatenate | str_c(x, y, sep="_") | pl.concat_str(["x", "y"], separator="_") |
# R — stringr pipe chain
df$clean <- df$name %>% str_to_lower() %>% str_trim() %>% str_replace_all("[^a-z ]", "")# Polars — .str namespace chain inside with_columns()
df = df.with_columns(
pl.col("name").str.to_lowercase().str.strip_chars()
.str.replace_all(r"[^a-z ]", "").alias("clean")
)Key difference: R's str_extract() returns the full match. Polars' .str.extract() requires a capture group and group index.
Sources: Wickham, R for Data Science 2nd ed., Ch. 14 (2023);
Polars API -- Expr.str (docs.pola.rs, accessed 2026-03-28)
---
Date/Time Handling
R's lubridate uses intuitive named parsers. Polars uses strftime format strings and a .dt namespace.
| Operation | R (lubridate) | Polars |
|---|---|---|
| Parse date | ymd("2024-01-15") | pl.col("d").str.to_date("%Y-%m-%d") |
| Parse datetime | ymd_hms(...) | pl.col("d").str.to_datetime("%Y-%m-%d %H:%M:%S") |
| Year | year(date) | pl.col("date").dt.year() |
| Month | month(date) | pl.col("date").dt.month() |
| Floor to month | floor_date(date, "month") | pl.col("date").dt.truncate("1mo") |
| Difference | difftime(d2, d1, units="days") | (pl.col("d2") - pl.col("d1")).dt.total_days() |
| Add days | date + days(30) | pl.col("date") + pl.duration(days=30) |
# R — lubridate, auto-detects separators
df$date <- ymd(df$date_str)
df$year <- year(df$date)
df$month_start <- floor_date(df$date, "month")# Polars — explicit format string required
df = df.with_columns(
pl.col("date_str").str.to_date("%Y-%m-%d").alias("date")
).with_columns(
pl.col("date").dt.year().alias("year"),
pl.col("date").dt.truncate("1mo").alias("month_start"),
)Key difference: R's ymd(), mdy(), dmy() auto-detect separators. Polars requires an explicit format string. Wrong format strings silently produce nulls -- always validate parsed dates with a null check.
Sources: Grolemund & Wickham, R for Data Science 2nd ed., Ch. 17 (2023);
Polars User Guide -- Temporal data (docs.pola.rs, accessed 2026-03-28)
---
File I/O
Both ecosystems handle common formats. DAAF standardizes on parquet.
| Format | R | Python / Polars |
|---|---|---|
| CSV | readr::read_csv() | pl.read_csv() |
| Parquet | arrow::read_parquet() | pl.read_parquet() |
| Parquet (lazy) | arrow::open_dataset() | pl.scan_parquet() |
| Stata | haven::read_dta() | pd.read_stata() |
| Excel | readxl::read_excel() | pl.read_excel() |
| RDS | readRDS() | No equivalent; use pyreadr |
# Polars — native parquet with lazy scanning
df = pl.read_parquet("data/raw/schools.parquet")
df.write_parquet("data/processed/schools_clean.parquet")
# Lazy scan for large files (predicate/projection pushdown)
lf = pl.scan_parquet("data/raw/large.parquet")
result = lf.filter(pl.col("state") == "CA").select("id", "name").collect()DAAF convention: All data stored in parquet format. No CSV, no Excel.
Sources: Polars User Guide -- I/O (docs.pola.rs, accessed 2026-03-28);
arrow R package docs (arrow.apache.org, accessed 2026-03-28)
---
Environment and Scoping
R attaches packages to a global search path. Python uses explicit module imports with namespace prefixes.
# R — library() makes all exports available unqualified
library(dplyr)
df %>% filter(x > 5) %>% mutate(y = str_to_lower(name))
# Name collisions resolved by load order (last wins)
library(dplyr) # exports filter()
library(stats) # also exports filter() — masks dplyr::filter()
dplyr::filter(df, x > 5) # disambiguate with explicit namespace# Python — explicit imports, namespaced access always
import polars as pl
import pyfixest as pf
df = df.filter(pl.col("x") > 5) # pl.DataFrame.filter(), not built-in filter()
model = pf.feols("y ~ x", data=pdf)What R users expect: After importing, functions are available by bare name. What actually happens: Every function lives in its package namespace. filter() alone is Python's built-in (on iterables), not polars' DataFrame filter.
| R Pattern | Python Equivalent | Notes |
|---|---|---|
library(dplyr) | import polars as pl | Conventional alias |
library(fixest) | import pyfixest as pf | Conventional alias |
library(ggplot2) | import plotnine as p9 | from X import * is discouraged |
dplyr::filter() | df.filter() (method) | Always namespaced |
.GlobalEnv | Module-level scope | Top of script |
Sources: R Language Definition -- Scope (CRAN, accessed 2026-03-28);
Wickham, R Packages 2nd ed., Ch. 10 (2023);
Python docs -- The import system (docs.python.org, accessed 2026-03-28)
Related skills
FAQ
What does the r-python-translation skill do?
It maps R data-analysis packages, idioms, and workflows to their Python equivalents, covering data wrangling, regression, visualization, causal inference, surveys, and spatial analysis.
Who is it for?
Researchers with an R background auditing or learning Python analysis code, and agents annotating Python output with R-equivalent comments.