Stata Migration Guide

Complete translation reference for economists migrating from Stata's margins command

Basic Command Translation

Core Margins Commands

Stata CommandMargins.jl EquivalentNotes
marginspopulation_margins(model, data; type=:predictions)Average adjusted predictions
margins, dydx(*)population_margins(model, data; type=:effects)Average marginal effects (AME)
margins, at(means)profile_margins(model, data, means_grid(data); type=:predictions)Predictions at sample means
margins, at(means) dydx(*)profile_margins(model, data, means_grid(data); type=:effects)Marginal effects at means (MEM)
margins, dydx(*) atmeansprofile_margins(model, data, means_grid(data); type=:effects)Alternative MEM syntax

Variable Selection

Stata CommandMargins.jl EquivalentNotes
margins, dydx(x1 x2)population_margins(model, data; type=:effects, vars=[:x1, :x2])Specific variables only
margins, dydx(_continuous)population_margins(model, data; type=:effects)All continuous variables (automatic)
margins, eyex(x1)population_margins(model, data; type=:effects, vars=[:x1], measure=:elasticity)Elasticities

Grouping and Stratification

Basic Grouping

Stata CommandMargins.jl EquivalentNotes
margins educationpopulation_margins(model, data; groups=:education)Group predictions
margins education, dydx(*)population_margins(model, data; type=:effects, groups=:education)Group effects
margins, over(education)population_margins(model, data; groups=:education)Alternative syntax
margins education genderpopulation_margins(model, data; groups=[:education, :gender])Cross-tabulation
margins education#genderpopulation_margins(model, data; groups=[:education, :gender])Interaction syntax

Nested Analysis

Stata CommandMargins.jl EquivalentNotes
by region: margins educationpopulation_margins(model, data; groups=:region => :education)Nested grouping
margins education, over(region)population_margins(model, data; groups=[:education, :region])Cross-tabulation alternative

Scenario Analysis (at() Specification)

Basic Scenarios

Stata CommandMargins.jl EquivalentNotes
margins, at(x=0)profile_margins(model, data, cartesian_grid(x=[0]); type=:predictions)Single scenario
margins, at(x=(0 1 2))profile_margins(model, data, cartesian_grid(x=[0, 1, 2]); type=:predictions)Multiple values
margins, at(x=0 y=1)profile_margins(model, data, cartesian_grid(x=[0], y=[1]); type=:predictions)Multiple variables

Population-Level Counterfactuals

Key Difference: Stata's at() creates evaluation points, while Margins.jl's scenarios creates population counterfactuals.

Stata ApproachMargins.jl Population ApproachNotes
margins, at(treatment=(0 1))population_margins(model, data; scenarios=(treatment=[0, 1]))Everyone untreated vs everyone treated
margins education, at(policy=(0 1))population_margins(model, data; groups=:education, scenarios=(policy=[0, 1]))Policy effects by education

Profile vs Population Interpretation

# Stata: margins, at(treatment=(0 1))
# → Effects at two evaluation points

# Margins.jl Profile Equivalent
profile_results = profile_margins(model, data, 
    cartesian_grid(treatment=[0, 1]);
    type=:effects)

# Margins.jl Population Alternative (often more relevant)  
population_results = population_margins(model, data;
    scenarios=(treatment=[0, 1]),
    type=:effects)

Skip Rule: dydx(x) with over(x) (and scenarios)

Unlike Stata, population_margins intentionally skips computing the effect of a variable when that same variable appears in groups (Stata over()) or in scenarios (Stata at()). This avoids the contradiction of “compute the effect of x while holding x fixed” or “using x both as an effect variable and a grouping key.”

Recommended translations:

# 1) Stata: margins, dydx(x) over(x)
# → Profile-style alternative: evaluate derivatives at specific x values
mem_like = profile_margins(model, data,
    cartesian_grid(x=[-2.0, 0.0, 2.0]);
    type=:effects,
    vars=[:x])

# 2) Population stratification by x without contradiction:
#    Create a derived bin variable and group by it, not by :x directly
df.x_bin = cut(df.x, 4)  # quartiles via user code; or use groups=(:x, 4)
by_xbins = population_margins(model, df;
    type=:effects,
    vars=[:x],
    groups=:x_bin)  # allowed since groups variable ≠ :x

# 3) Effects of other variables within x strata (population approach)
effects_in_xbins = population_margins(model, data;
    type=:effects,
    vars=[:z, :w],
    groups=(:x, 4))

# 4) Counterfactual predictions as x changes (not effects of x)
preds_under_x = population_margins(model, data;
    type=:predictions,
    scenarios=(x=[-2.0, 0.0, 2.0]))

See also: “Skip Rule” note in the Population Grouping docs for rationale and guidance.

Short example: grouping by x_bin to compute dydx(x) across strata

using Random
using DataFrames, CategoricalArrays
using Statistics  # for quantile
using GLM
using Margins

Random.seed!(42)
n = 500
df = DataFrame(
    y = rand(Bool, n),
    x = randn(n),
    z = randn(n)
)

# Fit a simple model
m = glm(@formula(y ~ x + z), df, Binomial(), LogitLink())

# Create quartile bins for x as a separate column "x_bin"
edges = quantile(df.x, 0:0.25:1.0)
labels = ["Q1", "Q2", "Q3", "Q4"]
df.x_bin = cut(df.x, edges; labels=labels, extend=true)

# Now compute population AME of x within x_bin strata (no contradiction)
res = population_margins(m, df;
    type=:effects,
    vars=[:x],
    groups=:x_bin)

DataFrame(res)  # Shows dydx(x) by Q1..Q4

Fixed Effects Models (reghdfe / ivregress)

Basic Fixed Effects

Stata CommandMargins.jl EquivalentNotes
reghdfe y x1 x2, absorb(state year)reg(df, @formula(y ~ x1 + x2 + fe(state) + fe(year)))Uses FixedEffectModels.jl
margins, dydx(*) (after reghdfe)population_margins(model, df; type=:effects)AME with absorbed FEs
margins, dydx(x1) (after reghdfe)population_margins(model, df; type=:effects, vars=[:x1])Single variable
margins, at(means) dydx(*)profile_margins(model, df, means_grid(df); type=:effects)MEM with absorbed FEs

Predictions with Fixed Effects

Predictions require save=:fe when fitting the model:

model = reg(df, @formula(y ~ x1 + x2 + fe(state) + fe(year)); save=:fe)
Stata CommandMargins.jl EquivalentNotes
margins (after reghdfe)population_margins(model, df; type=:predictions)Requires save=:fe
margins, at(x1=(0 1 2))profile_margins(model, df, cartesian_grid(x1=[0,1,2]); type=:predictions)FEs averaged
(predict at specific FE level)profile_margins(model, df, DataFrame(x1=[0.0], state=["CA"]); type=:predictions)FE level specified in grid

Instrumental Variables

Stata CommandMargins.jl EquivalentNotes
ivregress 2sls y x2 (x1=z)reg(df, @formula(y ~ x2 + (x1 ~ z)))IV estimation
margins, dydx(*) (after ivregress)population_margins(model, df; type=:effects)Structural coefficients

See Fixed Effects Models for full documentation.

Combined Grouping and Scenarios

Complex Analysis Patterns

Stata PatternMargins.jl EquivalentNotes
margins education, at(treatment=(0 1))population_margins(model, data; groups=:education, scenarios=(treatment=[0, 1]))Group × scenario analysis
Multiple margins commandsSingle comprehensive callMore efficient in Julia

Advanced Patterns Beyond Stata

Margins.jl extends far beyond Stata's capabilities with features unavailable in Stata:

Continuous Variable Binning

* Stata approach (manual and cumbersome):
gen income_q = .
_pctile income, nq(4)
replace income_q = 1 if income <= r(r1)
replace income_q = 2 if income > r(r1) & income <= r(r2)
replace income_q = 3 if income > r(r2) & income <= r(r3)
replace income_q = 4 if income > r(r3)
margins income_q
# Julia approach (automatic):
population_margins(model, data; groups=(:income, 4))  # Automatic Q1-Q4 quartiles

Custom Policy Thresholds

* Stata approach:
gen income_bracket = .
replace income_bracket = 1 if income < 25000
replace income_bracket = 2 if income >= 25000 & income < 50000
replace income_bracket = 3 if income >= 50000 & income < 75000
replace income_bracket = 4 if income >= 75000
margins income_bracket
# Julia approach:
population_margins(model, data; groups=(:income, [25000, 50000, 75000]))

Hierarchical Grouping

* Stata approach (requires multiple commands or complex by groups):
by region: margins education
* No native support for deep nesting
# Julia approach (native hierarchical support):
population_margins(model, data; groups=:region => :education)
population_margins(model, data; groups=:country => (:region => :education))  # Deep nesting

Multi-Variable Scenarios

* Stata approach (requires multiple separate commands):
margins, at(treatment=0 policy=0)
margins, at(treatment=0 policy=1) 
margins, at(treatment=1 policy=0)
margins, at(treatment=1 policy=1)
# Julia approach (automatic Cartesian product):
population_margins(model, data; scenarios=(treatment=[0, 1], policy=[0, 1]))

Note: scenarios in Julia are population‑level counterfactuals (everyone receives each setting in turn). For Stata’s point‑evaluation semantics of at(), use profile_margins(model, data, reference_grid) with a grid builder (e.g., means_grid, cartesian_grid) or an explicit DataFrame.

Complete Workflow Examples

Example 1: Education Policy Analysis

Stata Workflow

* Fit model
logit outcome education income female urban policy_treatment

* Basic effects
margins, dydx(*)

* Effects by education
margins education, dydx(income)

* Policy scenarios (multiple commands required)
margins education, at(policy_treatment=0)
margins education, at(policy_treatment=1)

* Manual difference calculation needed for treatment effects

Julia Workflow

# Fit model  
model = glm(@formula(outcome ~ education + income + female + urban + policy_treatment),
            data, Binomial(), LogitLink())

# Basic effects
basic_effects = population_margins(model, data; type=:effects)

# Effects by education
education_effects = population_margins(model, data; 
                                     type=:effects, 
                                     vars=[:income],
                                     groups=:education)

# Policy scenarios (automatic treatment effect calculation)
policy_analysis = population_margins(model, data;
                                   type=:effects,
                                   groups=:education,
                                   scenarios=(:policy_treatment => [0, 1]))

# All results readily available as DataFrames
DataFrame(policy_analysis)

Example 2: Complex Demographic Analysis

Stata Approach (Cumbersome)

* Multiple manual commands needed:
margins education, over(region)
margins gender, over(region)  
margins education#gender, over(region)

* Income quartiles require manual creation:
xtile income_q4 = income, nq(4)
margins education, over(income_q4)

* No native support for hierarchical analysis

Julia Approach (Comprehensive)

# Single comprehensive analysis
comprehensive_results = population_margins(model, data;
    type=:effects,
    groups=:region => [:education, :gender, (:income, 4)]
)

# Results: Region × (Education + Gender + Income-Quartiles) automatically computed
# Professional Q1-Q4 labeling included
DataFrame(comprehensive_results)

Performance Comparisons

Computational Advantages

AspectStataMargins.jl
Complex groupingMultiple manual commandsSingle comprehensive call
Scenario analysisManual looping/multiple commandsAutomatic Cartesian products
Large datasetsMemory limitationsEfficient O(n) scaling
Custom thresholdsManual variable creationAutomatic binning with labels
Hierarchical analysisLimited native supportUnlimited nesting depth

Stata Command Count Reduction

# This single Julia command:
result = population_margins(model, data;
    groups=:region => [:education, (:income, 4)],
    scenarios=(treatment=[0, 1], policy=["old", "new"])
)

# Replaces ~30 individual Stata margins commands:
# 4 regions × 3 education × 4 income × 2 treatment × 2 policy = 192 combinations
# Plus manual variable creation, looping, and results compilation

Migration Best Practices

Common Translation Patterns

# Pattern 1: Simple margins → population_margins
# margins → population_margins(model, data; type=:predictions)

# Pattern 2: Effects by groups → groups parameter  
# margins education, dydx(*) → population_margins(model, data; type=:effects, groups=:education)

# Pattern 3: Multiple at() values → scenarios or profile grids
# margins, at(x=(0 1 2)) → profile_margins(model, data, cartesian_grid(x=[0, 1, 2]))
# OR population_margins(model, data; scenarios=(x=[0, 1, 2]))  # for counterfactuals

# Pattern 4: Complex manual analysis → comprehensive single call
# Multiple Stata commands → single population_margins with groups + scenarios