Introduction

smmargins is a small module that fills in the marginal-effects gaps in StatsModels: adjusted predictions and marginal effects at user-specified covariate profiles, with delta-method standard errors, for any fitted model that exposes params, cov_params(), and a predict(params, exog) method.

The design target is Stata’s margins command: the same statistics, the same parameter names where they translate, and the same answers to the precision both tools agree on.

Why another margins module?

StatsModels ships Results.get_margeff, but it is limited:

  • only marginal effects, not adjusted predictions;

  • atexog is keyed by column index, not variable name;

  • no at(...) profiles, no representative-value contrasts;

  • no joint covariance across statistics, so you cannot form contrasts like a difference-in-differences without re-deriving the delta method by hand;

  • no support for difference-in-differences on the response scale (the Ai & Norton 2003 issue).

Installation

pip install smmargins

Requires Python ≥3.9. Dependencies (numpy, pandas, statsmodels, scipy, patsy) are installed automatically.

Quickstart

import statsmodels.formula.api as smf
from smmargins import Margins

fit = smf.logit(
    "voted ~ age + income + C(educ) + female + age:female",
    data=df,
).fit()
M = Margins(fit)

# Adjusted predictions
M.predict()                                    # AAP
M.predict(at="mean")                           # APM (margins, atmeans)
M.predict(atexog={"age": [25, 45, 65]})        # APR

# Marginal effects on the response (probability) scale
M.dydx("age")                                  # AME
M.dydx("age", at="mean")                       # MEM
M.dydx("age", atexog={"female": [0, 1]})       # MER, by sex
M.dydx("educ", reference="college")            # discrete contrasts

# Difference-in-differences on the response scale
res = M.did("group", "preexist_Y",
            group_levels=["A", "B"], condition_levels=[0, 1])
print(res)                                     # cells, simple effects, DiD

# Alternative VCEs and robust covariance
M.dydx("age", cov_type="HC3")                  # heteroskedastic-robust
M.dydx("age", vce="simulation", n_sims=2000,
       sim_seed=0)                             # Krinsky–Robb
M.dydx("age", vce="bootstrap", n_boot=1000,
       boot_seed=0)                            # pairs bootstrap

# Simultaneous CIs across a family of three predictions
M.predict(atexog={"age": [25, 45, 65]},
          vce="simulation", n_sims=4000,
          ci_method="sup-t")

Each call returns a MarginsResult with .estimate, .se, .vcov, .ci_lower, .ci_upper, .pvalue, plus .summary() returning a pandas.DataFrame. Pass use_t=True to the Margins constructor for t-distribution inference (uses results.df_resid).

Observation weights

Pass weights at construction. weight_type="sampling" (default) or "frequency". WLS-fitted results respect their fit-time weights when weights=None; explicit weights= overrides and warns. Bootstrap resampling uses weight-proportional draws under sampling weights.

M = Margins(fit, weights=w)
M = Margins(fit, weights=counts, weight_type="frequency")

Where to next

Tutorials — learn by doing.

How-to guides — task-focused recipes.

Explanations — theory and design.

Reference.

  • API reference — every public class and method.

  • Demos — full Williams-style and DiD walkthroughs from the repository root.