Skip to content

estimate_contrasts: split Response labels from Level1/Level2 for response-category models#648

Merged
strengejacke merged 9 commits into
mainfrom
copilot/estimate-contrasts-custom-treatment
Jul 21, 2026
Merged

estimate_contrasts: split Response labels from Level1/Level2 for response-category models#648
strengejacke merged 9 commits into
mainfrom
copilot/estimate-contrasts-custom-treatment

Conversation

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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 the Level1/Level2 strings (e.g. Level1 = "setosa 3"), confounding the contrasted factor level with the response category and making results hard to read/filter.

# Before
#> Level1 | Level2 | Difference
#> setosa 3 | setosa 2 | ...

# After
#> Level1 | Response1 | Level2 | Response2 | Difference
#> 3      | setosa    | 2      | setosa    | ...

Changes

  • R/format.R
    • Added .split_contrast_response_levels(): detects response-category models (via cached model_info attribute, falling back to insight::model_info()/insight::is_multivariate()), and splits the merged Level1/Level2 strings into new Response1/Response2 columns.
    • Added .split_response_prefix(): regex-based helper that strips the known response-level prefix from each merged string.
    • Output column order is Level1, Response1, Level2, Response2, .... Response and level factors preserve the model's original response-level ordering.
    • The split is only applied when it unambiguously succeeds for every row; otherwise the original (unsplit) output is preserved, so ordinary contrasts and non-response-category models are unaffected.

Tests

  • Added a regression test using nnet::multinom() verifying the new column names/order and cross-checking split contrasts against estimate_means() differences for pairing correctness.

Copilot AI changed the title [WIP] Add custom treatment for response models in estimate_contrasts estimate_contrasts: split Response labels from Level1/Level2 for response-category models Jul 20, 2026
Copilot AI requested a review from DominiqueMakowski July 20, 2026 07:51
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@DominiqueMakowski
DominiqueMakowski marked this pull request as ready for review July 20, 2026 08:13
Copilot AI review requested due to automatic review settings July 20, 2026 08:13

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.

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 marginaleffects contrasts formatter so Level1/Level2 no longer confound response category labels.
  • Add a regression test for nnet::multinom() verifying new columns/order and contrast correctness against estimate_means() deltas.
  • Document the user-visible output change in NEWS.md and 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.

Comment thread R/format.R Outdated
Comment thread R/format.R
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@strengejacke

Copy link
Copy Markdown
Member

@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

@DominiqueMakowski

Copy link
Copy Markdown
Member

yes, the output is now more readable and clear, and it is straightforward to manually filter the output as desired

@strengejacke
strengejacke merged commit a45bf80 into main Jul 21, 2026
12 of 20 checks passed
@strengejacke
strengejacke deleted the copilot/estimate-contrasts-custom-treatment branch July 21, 2026 06:17
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.

estimate_contrasts: custom treatment perhaps needed for response models (ordinal (cumulative); categorical; etc) models

4 participants