Skip to content

Probabilities by default for ordinal contrasts and slopes (emmeans backend) - #684

Merged
strengejacke merged 12 commits into
easystats:mainfrom
jmgirard:emmeans-ordinal-contrasts-slopes
Oct 18, 2026
Merged

strengejacke merged 12 commits into
easystats:mainfrom
jmgirard:emmeans-ordinal-contrasts-slopes

Conversation

@jmgirard

Copy link
Copy Markdown
Contributor

This follows #669, which added the ordinal mode types to estimate_means() with the emmeans backend. For ordinal models (MASS::polr(), ordinal::clm(), ordinal::clmm(), and glmmTMB() with family = ordinal()), estimate_contrasts() and estimate_slopes() with backend = "emmeans" returned latent-scale values. The contrasts footer said "Contrasts are on the response-scale." emmeans ignores type = "prob", so predict = "prob" also returned latent values, with the footer "prob-scale".

Now both functions pass the emmeans mode argument, and probabilities are the default (also for predict = "response"), as in estimate_means():

  • Contrasts compare the focal levels within each response category (column Response). With "cum.prob", "exc.prob" or "linear.predictor", they compare within each threshold (column Threshold). "latent" and "mean.class" have neither column. For clmm models, the columns show the level names, not the codes 1, 2, 3, as for estimate_means().
  • Slopes are the slopes of the probability of each response category. emtrends() with mode = "prob" or "mean.class" failed with "Irregular reference grid" on some polr fits, and gave slopes from -62 to 38 for a clmm fit. So these slopes are computed from the "cum.prob" trends, as linear contrasts. The probability of category k is P(y <= k) - P(y <= k - 1). The mean class is K minus the sum of the cumulative probabilities. Contrasts of slopes use the same slopes.
  • The contrasts footer names the mode ("Contrasts are on the prob-scale."). The slopes footer gets the line "Slopes are on the -scale." for these models only.
  • Plots of ordinal emmeans slopes color by Response or Threshold, as for the marginaleffects backend.
  • predict = "latent" gives the former results.

This is a breaking change, with a NEWS entry under "Breaking Changes". The predict docs (shared by estimate_means(), estimate_contrasts() and estimate_slopes()) now name the modes.

Related changes:

  • Ordinal contrasts keep the emmeans row order (grouped by response category). Before, merge() sorted the categories alphabetically (High, Low, Medium). Latent contrasts keep the sorted order.
  • The rename to Response/Threshold now runs before the other column renames. Before, a response named response, rate, prob or ratio was renamed to the estimate name.
  • A predictor named Response keeps its column, and the categories become Response.1, as in estimate_means().

Tests in test-ordinal.R:

  • Contrasts of polr, clm, clmm and glmmTMB fits against emmeans::contrast(emmeans(..., mode = "prob"), "revpairwise"). For polr and clm, also against the same-category rows of the marginaleffects backend. Each of the six modes for polr.
  • Slopes of two polr fits against the differences of the "cum.prob" trends (1e-6). Also against the marginaleffects backend (mean relative difference 1e-2), and against emtrends() for the other modes. The slopes of each group sum to 0. The 1e-2 tolerance is needed because emtrends() uses a forward difference with a step of 0.001 times the range of x. For the simulated fit, single slopes differ by up to 1.2% (emmeans 2.0.4, marginaleffects 1.0.0). A central finite difference matches marginaleffects.
  • clmm slopes against a central finite difference of the probabilities. glmmTMB slopes against the same polr model.
  • Level names, the footers, predict = "response", slopes with no by, the names above, and the former latent contrasts. The pinned latent contrasts in the "default predict" test now check that the default equals predict = "prob".

test-visualisation_recipe.R: the plot of ordinal emmeans slopes colors by Response or Threshold.

Not done here: brms ordinal models, comparison = "joint" and effectsize for these contrasts, mode passed through ..., and by filters on response categories. Such filters are now ignored in contrasts, where the call errored before.

devtools::test() gives 0 failures and 0 errors locally. devtools::check() gives 0 errors and 0 warnings. The version is bumped to 0.17.0.16, as in #683, so the PR merged second needs 0.17.0.17.

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@strengejacke
strengejacke requested a lite review from Copilot October 16, 2026 16:42

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

5 open findings
Files not reviewed (4)
  • man/estimate_contrasts.Rd: Generated file
  • man/estimate_means.Rd: Generated file
  • man/estimate_slopes.Rd: Generated file
  • man/get_emmeans.Rd: Generated file

🧠 Review effort: Lite

Comment thread R/get_emmeans.R
Comment thread DESCRIPTION
Comment thread R/get_emcontrasts.R
Comment thread R/get_emtrends.R
Comment thread NEWS.md Outdated
@strengejacke
strengejacke merged commit e634aa1 into easystats:main Oct 18, 2026
8 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.

3 participants