estimate_contrasts: split Response labels from Level1/Level2 for response-category models#648
Conversation
…sponse-category models
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves the readability and downstream usability of estimate_contrasts(backend = "marginaleffects") for response-category models (e.g., categorical/ordinal/multinomial and multivariate outcomes) by splitting response-category labels out of the contrasted level strings. Instead of embedding the response category inside Level1/Level2, the formatted output can include separate Response1/Response2 columns.
Changes:
- Add response/level splitting logic to the
marginaleffectscontrasts formatter soLevel1/Level2no longer confound response category labels. - Add a regression test for
nnet::multinom()verifying new columns/order and contrast correctness againstestimate_means()deltas. - Document the user-visible output change in
NEWS.mdand bump the development version.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
R/format.R |
Splits merged response-category prefixes out of contrast level strings and injects Response1/Response2 into formatted contrast outputs. |
tests/testthat/test-estimate_contrasts.R |
Adds regression coverage for multinomial response models to ensure split columns and correct pairing. |
NEWS.md |
Notes the new Response1/Response2 columns for response-category models. |
DESCRIPTION |
Bumps development version to reflect a user-visible change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
@DominiqueMakowski Can you check if this is the intended output? library(modelbased)
data(iris)
m <- nnet::multinom(Species ~ Sepal.Width, data = iris, trace = FALSE)
out <- estimate_contrasts(m, contrast = "Sepal.Width = c(2, 3)")
print(out, table_width = Inf)
#> Marginal Contrasts Analysis
#>
#> Level1 | Response1 | Level2 | Response2 | Difference | SE | 95% CI | t(146) | p
#> ------------------------------------------------------------------------------------------------
#> 3 | setosa | 2 | setosa | 0.21 | 0.04 | [ 0.12, 0.29] | 4.79 | < .001
#> 2 | versicolor | 2 | setosa | 0.85 | 0.08 | [ 0.70, 1.01] | 10.68 | < .001
#> 3 | versicolor | 2 | setosa | 0.34 | 0.05 | [ 0.25, 0.44] | 7.06 | < .001
#> 2 | virginica | 2 | setosa | 0.14 | 0.08 | [-0.01, 0.30] | 1.82 | 0.072
#> 3 | virginica | 2 | setosa | 0.45 | 0.05 | [ 0.34, 0.55] | 8.74 | < .001
#> 2 | versicolor | 3 | setosa | 0.65 | 0.09 | [ 0.47, 0.82] | 7.20 | < .001
#> 3 | versicolor | 3 | setosa | 0.14 | 0.08 | [-0.02, 0.29] | 1.75 | 0.083
#> 2 | virginica | 3 | setosa | -0.06 | 0.09 | [-0.25, 0.12] | -0.70 | 0.483
#> 3 | virginica | 3 | setosa | 0.24 | 0.08 | [ 0.08, 0.40] | 2.92 | 0.004
#> 3 | versicolor | 2 | versicolor | -0.51 | 0.10 | [-0.70, -0.32] | -5.33 | < .001
#> 2 | virginica | 2 | versicolor | -0.71 | 0.16 | [-1.02, -0.40] | -4.48 | < .001
#> 3 | virginica | 2 | versicolor | -0.41 | 0.09 | [-0.59, -0.23] | -4.41 | < .001
#> 2 | virginica | 3 | versicolor | -0.20 | 0.09 | [-0.38, -0.02] | -2.23 | 0.027
#> 3 | virginica | 3 | versicolor | 0.10 | 0.09 | [-0.07, 0.28] | 1.15 | 0.252
#> 3 | virginica | 2 | virginica | 0.30 | 0.09 | [ 0.11, 0.49] | 3.18 | 0.002
#>
#> Variable predicted: Species
#> Predictors contrasted: Sepal.Width = c(2, 3)
#> p-values are uncorrected.
#> Contrasts are on the probs-scale.
estimate_means(m, by = "Sepal.Width = c(2, 3)")
#> Estimated Marginal Means
#>
#> Sepal.Width | Response | Probability | SE | 95% CI | t(146)
#> --------------------------------------------------------------------------
#> 2 | setosa | 1.14e-03 | 1.22e-03 | [ 0.00, 0.00] | 0.93
#> 2 | versicolor | 0.85 | 0.08 | [ 0.70, 1.01] | 10.75
#> 2 | virginica | 0.14 | 0.08 | [-0.01, 0.30] | 1.82
#> 3 | setosa | 0.21 | 0.04 | [ 0.12, 0.30] | 4.73
#> 3 | versicolor | 0.34 | 0.05 | [ 0.25, 0.44] | 7.11
#> 3 | virginica | 0.45 | 0.05 | [ 0.35, 0.55] | 8.86
#>
#> Variable predicted: Species
#> Predictors modulated: Sepal.Width = c(2, 3)
#> Predictions are on the probs-scale.Created on 2026-07-20 with reprex v2.1.1 |
|
yes, the output is now more readable and clear, and it is straightforward to manually filter the output as desired |
For models predicting a value per response category (categorical, ordinal/cumulative, multinomial, or multivariate outcomes),
estimate_contrasts(backend = "marginaleffects")folded the response category label into theLevel1/Level2strings (e.g.Level1 = "setosa 3"), confounding the contrasted factor level with the response category and making results hard to read/filter.Changes
R/format.R.split_contrast_response_levels(): detects response-category models (via cachedmodel_infoattribute, falling back toinsight::model_info()/insight::is_multivariate()), and splits the mergedLevel1/Level2strings into newResponse1/Response2columns..split_response_prefix(): regex-based helper that strips the known response-level prefix from each merged string.Level1, Response1, Level2, Response2, .... Response and level factors preserve the model's original response-level ordering.Tests
nnet::multinom()verifying the new column names/order and cross-checking split contrasts againstestimate_means()differences for pairing correctness.