Skip to content

Filter the response categories of ordinal models in list, data-frame and contrast terms (emmeans backend) - #695

Merged
strengejacke merged 8 commits into
easystats:mainfrom
jmgirard:ordinal-filter-forms
Oct 19, 2026
Merged

strengejacke merged 8 commits into
easystats:mainfrom
jmgirard:ordinal-filter-forms

Conversation

@jmgirard

Copy link
Copy Markdown
Contributor

With backend = "emmeans", the response categories of ordinal models could be filtered only with a character by such as "Sat='Low'". A list or data-frame by that named the response returned all categories, or an error in estimate_slopes(). A contrast term on the response ignored its values. A formula by listed the response under "Predictors modulated". A by filter that left one level in ordinal slope contrasts printed empty contrasts.

Example, with m <- MASS::polr(Sat ~ Infl + Type, weights = Freq, data = MASS::housing, Hess = TRUE) and m2 <- MASS::polr(Sat ~ x * Infl + Type, data = d, Hess = TRUE), where d is housing with a numeric x. All calls use backend = "emmeans":

Call Before Now
estimate_means(m, list(Infl = c("Low", "High"), Sat = "Low")) 6 rows 2 rows
estimate_means(m, data.frame(Infl = "Low", Sat = "Low")) 3 rows 1 row
estimate_slopes(m2, "x", by = list(Sat = "Low")) error: No variable named Sat in the reference grid 1 row
estimate_means(m, ~ Infl + Sat) footer "Predictors modulated: Infl, Sat" "Predictors modulated: Infl"
estimate_contrasts(m, "Sat=c('Low','High')", by = "Infl") 9 rows 3 rows (High - Low)
estimate_contrasts(m, "Sat='Low'", by = "Infl") 9 rows error: at least two response categories are needed
estimate_contrasts(m2, "x", by = "Infl='Low'") 3 rows with empty contrasts error: the filter in by leaves fewer than two slopes to compare

What changed:

  • .extract_ordinal_response_filter() (R/get_emmeans.R) now reads the response from a list element, a data-frame column or a formula, as well as from a character by. It removes the response from by and keeps the values as the filter, so the emmeans call is the same as without the response. As for other data-frame columns, the values are crossed with the other by variables. A formula has no values, so it keeps all categories.
  • A contrast term on the response with values keeps only those categories in the emmeans grid before emmeans::contrast(). So comparisons that are not pairwise, such as "poly", use only the named categories. For clmm models, the category codes of emmeans (1, 2, 3, ...) are matched to the category names first, with the same rule as .clean_pseudo_factor_emmeans_ordinal(). A single category gives an error, and so does a filter on the response in by when the response is also in contrast.
  • comparison = "joint" with such a filter calls joint_tests() as before. When only two categories are kept, joint_tests() fails, so the single contrast in each by group is tested with emmeans::test(joint = TRUE).
  • get_emcontrasts() now runs the "at least two slopes" check for ordinal models, because emmeans contrasts the slopes within each response category, so the slopes to compare are the levels of by. The marginaleffects backend still skips ordinal models.
  • The by and contrast docs, the NEWS entry, and the version bump. The NEWS entry for the slope filters now names the models that skip the check.

Tests in test-ordinal.R compare each filtered result with the matching rows of the unfiltered call (polr, clm, and clmm with letter categories). The joint test is compared with emmeans::test(joint = TRUE). The tests also cover the errors (unknown categories, predict = "cum.prob", one category, the response in both by and contrast) and the footer.

Not fixed here, the same as on main:

  • Terms such as "Sat [Low]" or "Infl [Low, High]" stop with "not found in the data" for any variable.
  • comparison = "consecutive" stops with "Contrast function 'consecutive.emmc' not found", but "consec" works.
  • clmm response contrasts label rows with the emmeans codes (rating1, rating2), not the category names.
  • In slope contrasts, a list by such as list(Infl = c("Low", "High")) does not filter.
  • With a factor() response, contrast = "S2=c('Low','High')" stops with "No variable named S2 in the reference grid".
  • For ordinal models with the marginaleffects backend, a by filter that keeps one level in slope contrasts stops with "No contrasts to show".

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.

🟡 Changes recommended

The numeric contrast path does not support list or data-frame by filters.

1 open issue
4 files not reviewed
  • man/estimate_contrasts.Rd: Generated file
  • man/estimate_means.Rd: Generated file
  • man/estimate_slopes.Rd: Generated file
  • man/get_emmeans.Rd: Generated file
What changed in this PR

Adds ordinal response-category filtering for by and contrast inputs with the emmeans backend.

Changes:

  • Supports list, data-frame, and formula response filters.
  • Filters response contrasts and handles joint tests and slope validation.
  • Adds tests, documentation, NEWS updates, and a version bump.
File Summary
tests/​testthat/​test-ordinal.R Adds filtering, contrast, joint-test, and error coverage.
R/​get_marginalcontrasts.R Extends ordinal slope-filter validation.
R/​get_jointtest.R Handles joint tests with filtered categories.
R/​get_emmeans.R Extracts and applies ordinal response filters.
R/​get_emcontrasts.R Filters contrast grids and validates slope comparisons.
R/​estimate_means.R Updates filtering documentation.
R/​estimate_contrasts.R Documents response-category contrasts.
NEWS.md Records the behavioral changes.
man/​get_emmeans.Rd Updates generated documentation.
man/​estimate_slopes.Rd Updates generated documentation.
man/​estimate_means.Rd Updates generated documentation.
man/​estimate_contrasts.Rd Updates generated documentation.
DESCRIPTION Bumps the development version.

Review effort: Lite

Comment thread R/get_emcontrasts.R Resolved
@jmgirard

Copy link
Copy Markdown
Contributor Author

#695 (review)

  • R/get_emcontrasts.R:122 (list or data-frame by in slope contrasts): no error occurs, because .parse_by_filter() finds no filter in a list or data frame. List filters on other by variables are ignored in slope contrasts, the same as on main. Left for a follow-up.

@strengejacke
strengejacke merged commit e496feb into easystats:main Oct 19, 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