Stop Reading Logit Interaction Coefficients — Let margins Do the Translating
Logit interaction coefficients don't mean what you think. Stata's margins and marginsplot turn them into adjusted predictions and average marginal effects you can actually report.
27 Mar 2026, 12:45 UTC

Run a logit with an interaction, look at the coefficient on the product term, and try to say whether the two variables reinforce each other. You can't — not from that number. In nonlinear models the coefficient on an interaction term is not the interaction effect: the actual interaction in predicted probabilities varies across observations and can even flip sign. The useful takeaway: Stata's margins command exists precisely to translate coefficients into adjusted predictions and marginal effects you can defend in a report, and marginsplot turns those into a picture.
Why the raw coefficient misleads you
In a linear regression, the coefficient on x1*x2 is the interaction, full stop. In a logit or probit, the model is nonlinear in the probability scale, so the effect of one variable depends on the level of every other variable through the link function. Ai and Norton's well-known critique showed that the sign and significance of the interaction coefficient tell you nothing reliable about the sign and significance of the interaction effect on probabilities.
The fix is to stop interpreting coefficients and start interpreting quantities on the probability scale: adjusted predictions at chosen covariate values, and average marginal effects (AMEs) — the average change in predicted probability per unit change in a regressor, computed observation by observation and then averaged.
Factor variables are the foundation
margins only works its magic if Stata knows which terms are categorical, which are continuous, and how they interact. That means declaring the model with factor-variable notation:
i.treated— treattreatedas categorical (indicator variables)c.age— treatageas continuousi.treated##c.age— main effects plus the interaction
Do not hand-build a product term with generate treat_age = treated*age. If you do, Stata sees an ordinary variable and cannot compute correct derivatives for the interaction — margins output will be wrong or uninterpretable. The rule is simple: interactions belong in the model formula, not in the data.
A worked example: treatment effect by age
Suppose you model a binary outcome as a function of a treatment indicator interacted with age, controlling for income. Run these in the Stata Command window (or a do-file — no special permissions needed, just an estimation sample in memory):
logit outcome i.treated##c.age c.income
margins treated, at(age=(20(10)60))
marginsplot
margins, dydx(treated)What each line does:
logit ...fits the model with proper factor notation.margins treated, at(age=(20(10)60))computes the adjusted predicted probability for treated and untreated groups at ages 20, 30, 40, 50, and 60, averaging over the observed distribution of income. This is where interaction heterogeneity becomes visible: the gap between the two groups can widen or shrink with age.marginsplotgraphs the lastmarginsresult — the clearest way to show an audience that the treatment effect is not constant.margins, dydx(treated)collapses everything to a single average marginal effect: the average percentage-point change in the probability of the outcome associated with treatment, over your actual sample. Useful as a headline number, but it hides the age pattern, which is why you want both.
Check the output: the margins table should show one row per treatment level per age value, with confidence intervals. If marginsplot shows two parallel lines, your interaction isn't doing much on the probability scale regardless of what the coefficient said.
Average over the sample, or evaluate at the means?
By default, margins computes effects for every observation and averages them — the average marginal effect. The alternative, margins, dydx(treated) atmeans, evaluates everything at the covariate means. These answer different questions. The AME describes your actual population; the at-means effect describes a hypothetical average person who may not exist (nobody is 0.43 treated).
On skewed data the two can differ noticeably. There's no universal right answer, but there is a wrong one: picking whichever looks better. Choose based on the estimand you want, and state the choice in your methods section — "we report average marginal effects computed over the estimation sample" is one sentence that saves a referee comment.
Two gotchas worth knowing before you script this
margins overwrites your model in memory. It posts its own e() results, replacing the estimation you just ran. If you need both — say, model coefficients in one table and marginal effects in another — bracket it:
logit outcome i.treated##c.age c.income
estimates store mymodel
margins, dydx(treated)
estimates restore mymodelCurate, don't dump. margins, dydx(*) on a rich model produces a wall of numbers nobody reads. Decide which two or three effects answer your research question and report those. For reproducible pipelines, route curated results through putdocx, putexcel, or the collect system rather than retyping numbers into a manuscript.
Limitations
margins is unavailable after some estimation commands, and supported estimators and options vary by Stata release — check help margins in the version you actually run rather than assuming. With many at() combinations or large models it can be slow, because standard errors come from the delta method over every scenario. Predictions depend on your estimation sample and any if/in restrictions, so quote the sample alongside the numbers. And with survey-weighted data, margins respects svy settings, but subpopulation and weight handling change results — verify against the survey documentation before publishing.
The actionable part
Next time you fit a logit or probit with an interaction: declare it with ##, run margins over a meaningful grid of the moderating variable, graph it with marginsplot, and report one AME as the headline with the plot as the evidence. If you want to sanity-check the workflow first, reproduce it on bundled data — sysuse auto, a logit of foreign on c.mpg##c.weight, then margins and marginsplot — and confirm the output shape matches what you expect before touching your real analysis.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.