Getting started with suest

suest() combines two or more separately fitted models so you can test whether predictions, marginal effects, or other quantities differ across the models. It supplies the joint robust covariance matrix those tests need, and leaves calculating and presenting the quantities themselves to marginaleffects.

Functions at a glance

Installation and setup

remotes::install_github("tdmize/suest")
library(suest)
library(marginaleffects)

Basic workflow

dat <- mtcars
dat$am <- factor(dat$am)

base <- glm(am ~ wt, family = binomial("logit"), data = dat)
adjusted <- glm(am ~ wt + hp, family = binomial("logit"), data = dat)
combined <- suest(base, adjusted, model_names = c("Base", "Adjusted"))
combined
Seemingly Unrelated Estimation
Models: Base + Adjusted 
Model types: Base=logit, Adjusted=logit 
Model engines: Base=stats::glm, Adjusted=stats::glm 
Comparison scale: predicted probabilities 
Observations: Base=32, Adjusted=32 
Overlapping observations: 32 
Union observations: 32 
Parameters: 5
effects <- avg_comparisons(combined, variables = "wt", newdata = dat)
effects
    Group Estimate Std. Error     z Pr(>|z|)     S  2.5 % 97.5 %
 Base       -0.277     0.0232 -11.9   <0.001 106.5 -0.322 -0.231
 Adjusted   -0.355     0.0184 -19.3   <0.001 273.3 -0.391 -0.319

Term: wt
Type: response
Comparison: +1
hypotheses(effects, hypothesis = difference ~ revpairwise)
          Hypothesis Estimate Std. Error    z Pr(>|z|)    S  2.5 % 97.5 %
 (Base) - (Adjusted)   0.0779     0.0141 5.54   <0.001 25.0 0.0503  0.105

The last command tests the difference between the two effects directly. That is different from checking whether one effect is statistically significant and the other is not.

Supported models

Single-level models

Model Fitted with
Linear regression lm()
Binary logit, probit, and complementary log-log glm(), glm2::glm2()
Ordered logit and probit MASS::polr(), ordinal::clm()
Multinomial logit nnet::multinom()
Poisson regression glm(), glm2::glm2()
Negative binomial regression MASS::glm.nb()
Zero-inflated Poisson and negative binomial pscl::zeroinfl()
Other GLMs with identity, log, logit, probit, complementary log-log, or log-log links glm(), glm2::glm2()
Tobit regression censReg::censReg(); predictions are the latent mean
Interval, censored, and parametric survival regression survival::survreg() with a common scale
Truncated regression truncreg::truncreg()
Fractional response glm() with quasibinomial(): logit, probit, complementary log-log, or log-log link
Beta regression betareg::betareg() with a logit, probit, complementary log-log, or log-log link
Heteroskedastic probit and logit Rchoice::hetprob()
Bivariate probit mvProbit::mvProbit() with the same predictors in both equations
Instrumental-variable regression (two-stage least squares) fixest::feols(), unweighted and without absorbed fixed effects
Instrumental-variable probit Rchoice::ivpml(); predictions are average structural probabilities
Generalized ordered logit (gologit2), including partial proportional odds VGAM::vglm() with cumulative(reverse = TRUE)
Heckman selection (maximum likelihood) sampleSelection::selection(method = "ml")

Panel models

Model Fitted with
Linear fixed-, between-, and random-effects plm::plm() with individual effects
Random-effects logit and probit pglm::pglm(model = "random", effect = "individual")
Random-effects Poisson pglm::pglm(model = "random", effect = "individual", other = "sd")
Fixed-effects Poisson fixest::fepois(y ~ x | id)
Population-averaged (GEE) linear, logit, probit, complementary log-log, and Poisson geepack::geeglm() with independence or exchangeable correlation

Multilevel models

Model Fitted with
Linear random-intercept nlme::lme(method = "ML")
Random-intercept logit, probit, cloglog, Poisson, negative binomial, linear, and Gamma (log link) glmmTMB::glmmTMB()
Random intercept and slope, (1 + x | id), for the same families glmmTMB::glmmTMB()
Linear random intercept or slope (ML) lme4::lmer(REML = FALSE)
Random-intercept or slope logit, probit, cloglog, and Poisson lme4::glmer(), any nAGQ
Random-intercept ordered logit and probit ordinal::clmm()

Survey models

Model Fitted with
Linear regression survey::svyglm()
Binary logit, probit, and cloglog survey::svyglm() with quasibinomial()
Poisson survey::svyglm() with quasipoisson()

See Survey data below.

Combining models

Models can use the same, partially overlapping, or completely different samples. Any single-level models can be combined, including models for different outcomes and models for categorical outcomes. Results on different scales are labeled separately rather than treated as directly comparable. Random-effects models (nlme::lme(), pglm(), glmmTMB(), lme4, clmm()) can be combined with each other, including different families, and with linear, binary, count, and ordered models, such as a logit model with a random-intercept logit model. They must share one grouping variable, which the system is clustered on, and must be unweighted. Other panel models (plm(), geeglm()) can be combined only with models of the same kind. Offsets are supported, except in pglm() and glmmTMB() models.

A few models have settings worth knowing about; see Notes on specific models.

Different samples or groups

When the models are fitted to different samples, such as separate groups, use suest_newdata() so each model’s effects are averaged over its own sample:

four <- lm(mpg ~ wt + hp, data = mtcars, subset = cyl == 4)
other <- lm(mpg ~ wt + hp, data = mtcars, subset = cyl != 4)
fit_groups <- suest(four, other, model_names = c("Four cylinders", "Other"))

effects_groups <- avg_comparisons(fit_groups, variables = "wt",
                                  newdata = suest_newdata(fit_groups))
effects_groups
          Group Estimate Std. Error     z Pr(>|z|)    S 2.5 % 97.5 %
 Four cylinders    -5.12      1.083 -4.72   <0.001 18.7 -7.24  -2.99
 Other             -2.56      0.575 -4.45   <0.001 16.8 -3.69  -1.43

Term: wt
Type: response
Comparison: +1
hypotheses(effects_groups, hypothesis = difference ~ revpairwise)
                 Hypothesis Estimate Std. Error     z Pr(>|z|)   S 2.5 % 97.5 %
 (Four cylinders) - (Other)    -2.55       1.23 -2.08   0.0373 4.7 -4.96  -0.15

Cluster-robust standard errors

Supply a cluster variable to get cluster-robust standard errors for the combined models:

fit_clustered <- suest(base, adjusted, cluster = "person_id")
avg_comparisons(fit_clustered, variables = "x", newdata = dat)

cluster takes one or more column names, or a list with one vector, matrix, or data frame matched to each model’s estimation sample. An observation that appears in more than one model must have the same cluster ID in each. Observations in completely different samples can belong to the same cluster, in which case the covariance between models can be nonzero.

Panel models cluster on the panel identifier by default. A cluster you supply must contain whole panels.

Probability weights

suest supports probability weights for linear, binary logit and probit, Poisson, negative binomial, ordered logit and probit, and multinomial logit models. Fit each model with weights=, and tell suest() that they are probability weights with weight_type = "pweight":

set.seed(376)
weighted <- data.frame(x = rnorm(500), z = rnorm(500))
weighted$mediator <- 0.5 * weighted$x - 0.3 * weighted$z + rnorm(500)
weighted$pw <- exp(0.2 * weighted$z + 0.1 * rnorm(500))
weighted$y <- 1 + 0.7 * weighted$x - 0.4 * weighted$z +
  0.6 * weighted$mediator + rnorm(500)

base_w <- lm(y ~ x + z, weights = pw, data = weighted)
adjusted_w <- lm(y ~ x + z + mediator, weights = pw, data = weighted)
fit_w <- suest(base_w, adjusted_w, model_names = c("Base", "Adjusted"),
               weight_type = "pweight")
fit_w
Seemingly Unrelated Estimation
Models: Base + Adjusted 
Model types: Base=lm, Adjusted=lm 
Model engines: Base=stats::lm, Adjusted=stats::lm 
Comparison scale: fitted values 
Weight type: pweight 
Observations: Base=500, Adjusted=500 
Overlapping observations: 500 
Union observations: 500 
Parameters: 9
effects_w <- avg_comparisons(fit_w, variables = "x", newdata = weighted, wts = "pw")
effects_w
    Group Estimate Std. Error    z Pr(>|z|)     S 2.5 % 97.5 %
 Base        1.003     0.0537 18.7   <0.001 255.9 0.898  1.108
 Adjusted    0.722     0.0480 15.0   <0.001 167.3 0.628  0.816

Term: x
Type: response
Comparison: +1
hypotheses(effects_w, hypothesis = difference ~ revpairwise)
          Hypothesis Estimate Std. Error    z Pr(>|z|)    S 2.5 % 97.5 %
 (Base) - (Adjusted)    0.281     0.0322 8.74   <0.001 58.6 0.218  0.345

Weights must be positive and finite. An observation that appears in more than one model must have the same weight in each; weights can differ for observations in only one model, including completely different samples. When the models have different estimation samples, suest_newdata() includes the weights as .suest_weight:

nd_w <- suest_newdata(fit_w)
avg_comparisons(fit_w, variables = "x", newdata = nd_w, wts = ".suest_weight")

Survey data

suest supports linear, binary (logit, probit, cloglog), and Poisson models fitted with survey::svyglm() from one common one-stage survey design, in any combination:

library(survey)
design <- svydesign(~psu, strata = ~strata, weights = ~w, data = dat)
m1 <- svyglm(yb ~ x, design, family = quasibinomial("logit"))
m2 <- svyglm(yb ~ x + z, design, family = quasibinomial("logit"))
fit <- suest(m1, m2, survey_design = design, observation_id = "id")
avg_slopes(fit, variables = "x", newdata = suest_newdata(fit), wts = ".suest_weight")

If either model uses a subset (domain) of the data, keep design as the full sampled design. The joint covariance accounts for strata, PSUs, and optional one-stage finite population corrections. Replicate weights, multistage designs, calibration, and PPS designs are not supported.

Multiple imputation

To combine results across imputed datasets, fit the same suest system in each one. With mice, with() does this, and marginaleffects functions then estimate within each imputation and pool the results with Rubin’s rules, including cross-model differences:

analyses <- with(imp, suest(
  glm(y ~ x, family = binomial),
  glm(y ~ x + z, family = binomial),
  model_names = c("Base", "Adjusted")
))
avg_comparisons(analyses, variables = "x",
  hypothesis = difference ~ revpairwise)

mice warns “Large sample assumed.” here. That is expected: SUEST reports large-sample statistics, so the pooled degrees of freedom are Rubin’s large-sample values.

suest_mi() pools the coefficients and their joint covariance:

pooled <- suest_mi(analyses)
summary(pooled)

marginaleffects functions give the same results on pooled as on analyses. A list of suest() results, one per imputation, or the result of with(mitools::imputationList(...), suest(...)) can also be passed to suest_mi(); marginaleffects then warns that it could not recover the original data from a mids object, which does not affect suest() results.

Examples

Six examples from Mize, Doan, and Long (2019), with code and output:

  1. Curvilinear effects and mediation/attenuation
  2. Nested logit models and mediation/attenuation
  3. Alternative predictors
  4. Different outcomes
  5. Different model types: ordinal versus nominal
  6. Separate samples or groups
Back to top