Skip to content

Contrasts of ordinal slopes when the by filter keeps one level - #696

Open
jmgirard wants to merge 8 commits into
easystats:mainfrom
jmgirard:ordinal-slope-one-level
Open

jmgirard wants to merge 8 commits into
easystats:mainfrom
jmgirard:ordinal-slope-one-level

Conversation

@jmgirard

Copy link
Copy Markdown
Contributor

If the filter in by kept one level, estimate_contrasts() stopped with "No contrasts to show" for slopes of ordinal models with the marginaleffects backend. Ordinal models have one slope per response category, so there are still slopes to compare at that level. The call now returns the contrasts between the response categories at that level.

Example, with d <- MASS::housing, set.seed(1), d$x <- rnorm(nrow(d)) and m <- MASS::polr(Sat ~ x * Infl + Type, data = d, Hess = TRUE):

Call 0.18.0 main Now
estimate_contrasts(m, "x", by = "Infl='Low'") error: No contrasts to show the same error 3 rows
estimate_contrasts(m, "x", by = "Infl='Low'", estimate = "average") 36 rows, filter ignored the same error 3 rows
estimate_contrasts(m, "x", by = "Infl=c('Low','High')") 15 rows 15 rows 15 rows (unchanged)

The 3 rows are Medium vs. Low, High vs. Low and High vs. Medium, with Level1 and Level2 set to Low. They equal the rows of estimate_contrasts(m, "x", by = "Infl") where both levels are Low, with the same Difference and SE. The same holds for ordinal::clm().

What changed, all in R/format.R:

  • The change is in the slope branch of format.marginaleffects_contrasts(). If the by filter leaves one level and the model has response categories, it keeps by for the labels and does not stop. The labels from marginaleffects then hold only the categories, such as "(Medium) - (Low)". The new .one_level_slope_labels() puts them in Response1 and Response2, and takes Level1 and Level2 from the data grid.
  • Custom hypotheses (comparison = "b1=b2") and group formulas (~pairwise | Infl) do not get these labels, so they keep the "No contrasts to show" error.
  • The response-model check in .split_contrast_response_levels() moved to a helper, .is_response_level_model(), which both functions use.
  • NEWS entry and version bump.

Tests in test-ordinal.R cover polr and clm, with "typical" and "average". They check the one-level filter, and a two-level filter against the unfiltered rows. They also check response levels with parentheses, the errors for custom hypotheses and group formulas, and the "fewer than two slopes" error for lm.

Not fixed here, the same as on main:

  • Multivariate models, such as lm(cbind(mpg, qsec) ~ wt * g) with by = "g='0'", still stop with "fewer than two slopes". That check exempts only ordinal, multinomial and categorical models.
  • With a group formula, such as by = c("Infl", "Type") and comparison = ~pairwise | Type, the labels of ordinal slope contrasts are wrong.
  • The emmeans backend gives the same output as on main for these calls.

@jmgirard
jmgirard requested a lite review from Copilot October 19, 2026 17:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

3 open issues
What changed in this PR

Fixes estimate_contrasts() for ordinal-model slopes (marginaleffects backend) when a by filter leaves a single level, ensuring response-category slope contrasts are still returned instead of erroring.

Changes:

  • Adjusted format.marginaleffects_contrasts() slope-label handling to support one-level by filters for response-level models.
  • Added regression tests for polr and clm covering one-level and two-level by filters plus error cases.
  • Updated NEWS and bumped package version.
File Description
R/​format.R Implements one-level by handling for ordinal-slope contrasts and refactors response-level detection into a helper.
tests/​testthat/​test-ordinal.R Adds tests validating the new behavior for ordinal slope contrasts under by filters.
NEWS.md Documents the bug fix and expected behavior change.
DESCRIPTION Version bump for the release containing the fix.

Review effort: Lite

Comment thread R/format.R
Comment thread tests/testthat/test-ordinal.R
Comment thread tests/testthat/test-ordinal.R Outdated
@jmgirard

Copy link
Copy Markdown
Contributor Author

#696 (review)

  • R/format.R:369 (safe_deparse() length): not changed, insight::safe_deparse() always returns one string.
  • tests/testthat/test-ordinal.R:1344 (set.seed()): not changed, the existing tests use set.seed() the same way.
  • tests/testthat/test-ordinal.R:1387 (exact label text): fixed in 6947213.

@strengejacke

Copy link
Copy Markdown
Member

Same here, please resolve merge conflicts, I'm not sure which parts of the test file to keep

This branch has not been deployed

No deployments
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.

3 participants