Complete translation reference for economists migrating from Stata's margins command
| Stata Command | Margins.jl Equivalent | Notes |
|---|
margins | population_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(*) atmeans | profile_margins(model, data, means_grid(data); type=:effects) | Alternative MEM syntax |
| Stata Command | Margins.jl Equivalent | Notes |
|---|
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 |
| Stata Command | Margins.jl Equivalent | Notes |
|---|
margins education | population_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 gender | population_margins(model, data; groups=[:education, :gender]) | Cross-tabulation |
margins education#gender | population_margins(model, data; groups=[:education, :gender]) | Interaction syntax |
| Stata Command | Margins.jl Equivalent | Notes |
|---|
by region: margins education | population_margins(model, data; groups=:region => :education) | Nested grouping |
margins education, over(region) | population_margins(model, data; groups=[:education, :region]) | Cross-tabulation alternative |
| Stata Command | Margins.jl Equivalent | Notes |
|---|
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 |
Key Difference: Stata's at() creates evaluation points, while Margins.jl's scenarios creates population counterfactuals.
| Stata Approach | Margins.jl Population Approach | Notes |
|---|
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 |
# 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)
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.
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
| Stata Command | Margins.jl Equivalent | Notes |
|---|
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 require save=:fe when fitting the model:
model = reg(df, @formula(y ~ x1 + x2 + fe(state) + fe(year)); save=:fe)
| Stata Command | Margins.jl Equivalent | Notes |
|---|
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 |
| Stata Command | Margins.jl Equivalent | Notes |
|---|
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.
| Stata Pattern | Margins.jl Equivalent | Notes |
|---|
margins education, at(treatment=(0 1)) | population_margins(model, data; groups=:education, scenarios=(treatment=[0, 1])) | Group × scenario analysis |
Multiple margins commands | Single comprehensive call | More efficient in Julia |
Margins.jl extends far beyond Stata's capabilities with features unavailable in Stata:
* 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
* 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]))
* 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
* 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.
* 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
# 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)
* 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
# 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)
| Aspect | Stata | Margins.jl |
|---|
| Complex grouping | Multiple manual commands | Single comprehensive call |
| Scenario analysis | Manual looping/multiple commands | Automatic Cartesian products |
| Large datasets | Memory limitations | Efficient O(n) scaling |
| Custom thresholds | Manual variable creation | Automatic binning with labels |
| Hierarchical analysis | Limited native support | Unlimited nesting depth |
# 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
# 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