Skip to content

Apply by filters to contrasts of slopes with estimate = "average" and the emmeans backend - #694

Merged
strengejacke merged 10 commits into
easystats:mainfrom
jmgirard:slope-contrast-filters
Oct 19, 2026
Merged

strengejacke merged 10 commits into
easystats:mainfrom
jmgirard:slope-contrast-filters

Conversation

@jmgirard

Copy link
Copy Markdown
Contributor

For contrasts of slopes, estimate_contrasts() applied filter values in by only with the default estimate. With estimate = "average" and with backend = "emmeans", it compared the slopes of all levels and ignored the filter. When a filter left only one slope, the default estimate stopped with an error from marginaleffects.

Example, with cyl and am as factors in mtcars, m <- lm(mpg ~ wt * cyl * am) and m2 <- lm(mpg ~ wt * cyl):

Call Before Now
estimate_contrasts(m, "wt", by = c("cyl='4'", "am")) 1 row 1 row (unchanged)
estimate_contrasts(m, "wt", by = c("cyl='4'", "am"), estimate = "average") 15 rows 1 row
estimate_contrasts(m, "wt", by = c("cyl='4'", "am"), backend = "emmeans") 15 rows 1 row
estimate_contrasts(m2, "wt", by = "cyl='4'") error: Items of 'old' not found in column names: [estimate] error: the filter in by leaves fewer than two slopes to compare
estimate_contrasts(m2, "wt", by = "cyl='4'", estimate = "average") 3 rows the same error

What changed:

  • get_marginalcontrasts(): for estimate = "average", the slopes are computed on the data rows that pass the by filters, so the hypothesis compares only those slopes. Slopes are averaged within each by group, so their values do not change. by keeps the filter, so the contrast labels show only the filtered levels.
  • get_emcontrasts(): the filter values go into at for emmeans::emtrends(). All by variables must be factors or character vectors for this. emmeans holds numeric variables at their mean, and at does not work for factors made in the formula (factor(cyl)). For these cases the output is the same as on main.
  • New helpers in R/get_marginalcontrasts.R parse the filter and drop values that are not levels. If fewer than two slopes remain, they stop with an error. For estimate = "average", only combinations with data count. Ordinal and multinomial models, and custom hypotheses ("b1 = b2", functions, matrices), skip this check.
  • NEWS entry and version bump.

Tests in the new test-estimate_contrasts_slopes_filter.R compare each result with pairs(emmeans::emtrends(..., at = ...), adjust = "none") at a tolerance of 1e-5. They cover filters on the first or second by variable, with one or two values and with values in reverse order. They use a model with an additive covariate, and they run "typical", "average", emmeans and the unfiltered case. They also cover the errors, filters with spaces around =, and filter values that are not levels. The file has no skip_on_os().

Not fixed here, the same as on main:

  • Numeric by variables in contrasts of slopes. For lm(mpg ~ wt * hp + cyl) with by = "hp", emmeans returns 1 row and marginaleffects returns 45.
  • With estimate = "average" and a newdata or data argument, the filters are ignored.
  • With the default estimate, a filter value that is not a level stops with "Unable to compute predicted values".
  • For ordinal models with the marginaleffects backend, a filter on a predictor stops with "No contrasts to show". With "average", main returned all contrasts here.

@strengejacke
strengejacke merged commit dc15db2 into easystats:main Oct 19, 2026
9 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants