How to report marginal effects on custom scales and with user-defined transforms¶
Prerequisites¶
Tutorial: First steps with smmargins — fitting a model and computing a basic AME
Explanation: Prediction scales — the relationship between response scale, linear predictor, and transformed scales
Problem statement¶
You want marginal effects on a scale other than the default response scale. You might need the linear predictor (Stata’s xb), the odds-ratio scale for logit, or a completely custom nonlinear transformation of the prediction. You need to know which built-in scales are available and how to define your own.
Minimal working solution¶
Pass a string ("linear", "or", "exp", etc.) or a Transform object to the scale= parameter of predict or dydx.
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
from smmargins import Margins, Transform
rng = np.random.default_rng(7)
N = 5_000
df = pd.DataFrame({
"age": rng.normal(45, 12, N).clip(18, 90),
"income": rng.lognormal(10.5, 0.4, N),
"educ": rng.choice(["hs", "college", "grad"], N, p=[0.4, 0.4, 0.2]),
"female": rng.integers(0, 2, N),
"region": rng.choice(["north", "south", "east", "west"], N),
})
df["voted"] = (rng.uniform(0, 1, N) < 1 / (1 + np.exp(-(
-4 + 0.05 * df.age + 0.00001 * df.income
+ 0.8 * (df.educ == "college") + 1.4 * (df.educ == "grad")
+ 0.3 * df.female - 0.0004 * df.age * df.female
)))).astype(int)
fit = smf.logit("voted ~ age + income + C(educ) + female + age:female", data=df).fit(disp=False)
M = Margins(fit)
# Built-in scales
print("Response scale (default):")
print(M.dydx("age", scale="response"))
print("\nLinear predictor scale (Stata 'xb'):")
print(M.dydx("age", scale="linear"))
print("\nOdds-ratio scale (logit only):")
print(M.dydx("age", scale="or"))
# Custom Transform: square of the linear predictor
square = Transform(
value=lambda e: e ** 2,
grad=lambda e: 2 * e,
hess=lambda e: np.full_like(e, 2.0),
name="square",
)
print("\nCustom 'square' transform:")
print(M.dydx("age", scale=square))
Variations¶
All built-in scales on predictions¶
for s in ["response", "linear", "pr", "or", "exp", "log"]:
try:
est = M.predict(scale=s).estimate[0]
print(f" {s:10s}: {est:.6f}")
except ValueError as e:
print(f" {s:10s}: raises — {e}")
Custom transform for predictions only (no hessian needed)¶
# For predict(), only grad= is required
logistic = Transform(
value=lambda e: 1 / (1 + np.exp(-e)),
grad=lambda e: np.exp(-e) / (1 + np.exp(-e)) ** 2,
hess=None, # OK for predict
name="logistic",
)
print(M.predict(scale=logistic))
Elasticity on a custom scale¶
# Compose elasticity methods with custom scales
print(M.dydx("age", method="eyex", scale=square))
print(M.dydx("age", method="eyex", scale="linear"))
⚠️ Trade-off: Built-in scales are validated against the model family (e.g.,
scale="or"raises on Poisson). Custom transforms bypass these checks — you are responsible for ensuring the transform is mathematically valid for your model. Custom transforms fordydxrequire an analytic second derivative (hess=); there is no autodiff.
When to use this¶
Use scale="linear" when you want marginal effects on the latent-index scale (the coefficient scale). Use scale="or" for logit odds-ratio interpretation. Use a custom Transform when you need a scale that is not built-in, such as a policy-relevant transformation or a link function from a different family.
When NOT to use this¶
⚠️ Trade-off: Do not use
scale="or"on non-logit models — it raises. Do not use custom transforms without providinghess=fordydxcalls — it raises with a clear error. For prediction-only calls (predict()),hesscan beNone. Do not define transforms with discontinuous derivatives unless you understand the delta-method implications.
See also¶
Reference: Built-in scales — complete list of built-in scale strings and their definitions
Reference: Transform class — full API for custom transforms
How to compute elasticities — composing elasticity methods with scales