Repository: ddsjoberg/gtsummary
Pull request #2211: Updating for multinomial models
Author: login: ddsjoberg
Created: 2025-04-17T03:15:23Z | Merged: 2025-04-17T17:12:21Z

**What changes are proposed in this pull request?**
* `tbl_regression()` now takes advantage of a new feature in {broom.helpers} to support multicomponent models, such as, multinomial models and mixed-effects models. Review `broom.helpers::tidy_group_by()` for details. (#1540)

**If there is an GitHub issue associated with this pull request, please provide link.**
closes #1540 
closes #1684
closes #1608

--------------------------------------------------------------------------------

Reviewer Checklist (if item does not apply, mark is as complete)

- [ ] PR branch has pulled the most recent updates from main branch.
- [ ] If a bug was fixed, a unit test was added.
- [ ] Run `pkgdown::build_site()`. Check the R console for errors, and review the rendered website.
- [ ] Code coverage is suitable for any new functions/features: `devtools::test_coverage()`
- [ ] `usethis::use_spell_check()` runs with no spelling errors in documentation
- [ ] **All** GitHub Action workflows pass with a :white_check_mark:

When the branch is ready to be merged into master:
- [ ] Update `NEWS.md` with the changes from this pull request under the heading "`# gtsummary (development version)`". If there is an issue associated with the pull request, reference it in parentheses at the end update (see `NEWS.md` for examples).
- [ ] Increment the version number using `usethis::use_version(which = "dev")` 
- [ ] Run `usethis::use_spell_check()` again
- [ ] Approve Pull Request
- [ ] Merge the PR. Please use "Squash and merge".



Linked issue #1540: Feature request: Support for multi-components models
Dear @ddsjoberg 

I'm currently adding support for zero-inflated models in `broom.helpers`, cf. https://github.com/larmarange/broom.helpers/pull/233

With such models, you end-up with two sets of coefficients, for the two components of the model (see `component` column in the tidy table).

The situation is quite similar with multinomial models and their `"y.level"` column. Could we add support in `tbl_regression()` using the code already written for multinomial models?

Best regards
Linked issue comments: user: login: ddsjoberg
created_at: 2023-08-04T17:45:12Z
body: Absolutely! 🚀 🚀 🚀 
author_association: OWNER
user: login: larmarange
created_at: 2023-08-05T09:54:47Z
body: betarag models are also multicomponent models. Therefore, we should be more general.

More precisely, we should check (1) if there is a `"component"` column in the result returned by `tidy_plus_plus()`, (2) if there is at least two different values in this col (with the selection of certain variables, removal of intercepts, etc. we could end up with just one value). If this is the case, then the tbl should be presented in a similar way as multinomial models.
author_association: COLLABORATOR
user: login: larmarange
created_at: 2025-02-18T06:48:42Z
body: Dear @ddsjoberg 

Would it be a good time to reopen this discussion and to see how to make tbl_regression() more general for multi components models and to multinomial models other than multinom()?

Best regards
author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-02-18T19:13:22Z
body: Let's do it!

I think there are two general classes of multicomponent models: multinomial outputs (like polytomous regression) and combination models (like the zero-inflated models or the joint longitudinal-survival models). Does that sound right to you?

I think the easiest way to make these work with `tbl_regression()` would be if `broom.helpers` had consistent column names for these model types to identify them as multi-component models, then `tbl_regression()` could simply look for these column names and have a slightly different print for them (ie print them in two or more sections).

Is there anything else we need to cnosider?
author_association: OWNER
user: login: larmarange
created_at: 2025-02-19T10:20:29Z
body: Thanks, @ddsjoberg, for your feedback.

You are right; there are polytomous regression (resulting with an `y.level` column) and multi-component models with a `component` column.  We must keep in mind that in some cases, a user may need to group results differently (eg #1567 related to mixed models where the user wanted to group according to the `effect` column).

My suggestions are as follows.

## on broom.helpers side

New function `tidy_group_by()` and new option `group_by` in `tidy_plus_plus()`.

This function will be in charge of preparing the results and, if needed, returning an additional `group_by` column in the results.

**By default** (`group_by = "auto"`), if there is a `y.level` column and this column has a minimum of 2 different values, then `group_by` will be populated with this column, or, if there is a `component` column and this column has a minimum of 2 different values, then `group_by` will be populated with `component`, otherwise no grouping will be performed. It will cover most standard use cases.

`group_by = NULL` to force no grouping.

`group_by = "effect"` (or another column name) indicating that we want to group according to a specific column returned by the tidier.

An option `group_labels` could be implemented to allow the user to rename the different groups.

Such an approach should be pretty flexible. It even allows the development of custom tidiers with other grouping options.

## on gtsummary side

No need to have methods according to the type of model. Simply look at the existence of a `group_by` column, and if there is a `group_by` column, simply add group headers (similarly to the variable group headers that you just implemented).

It would be nice to have `bold_groups()` and `italicize_groups()` functions.

Do you think it would be possible to natively include a `pivot_wider()` function (inspired from https://stackoverflow.com/questions/64463878/multinomial-logistic-regression-results-table-in-wide-format-using-the-gtsummary )

What do you think?

NB: in parralel, related to #1684, we could contact VGAM team to discuss if they could introduce tidiers in their package.

author_association: COLLABORATOR
user: login: larmarange
created_at: 2025-02-24T18:20:00Z
body: @ddsjoberg  If you want to have a look, a first implementation is https://github.com/larmarange/broom.helpers/pull/289

The `group_by` is a factor and results are already ordered.
author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-02-24T23:53:03Z
body: @larmarange this looks great! It will be nice to have a consistent format to expect the results in.

I think I may keep the S3 methods for these models and expose the `group_by` and `group_label` arguments. And for some models where the user may not want the groupings be default (maybe the mixed-effects models?) we could use `group_by=NULL` default? Also, we could then add another argument o pivot wide for the display. And lastly, since the other `add_*.tbl_regression()` functions sometimes won't work on these structures, we can add messaging about that like we do now for the nnet multinomial models.

Thanks for adding! I think this will be a small update on the gtsummary side.
author_association: OWNER
user: login: larmarange
created_at: 2025-02-25T14:53:31Z
body: Some examples:

``` r
library(broom.helpers)

# nnet::multinom()
library(nnet)
mod <- multinom(Species ~ Sepal.Length + Sepal.Width, data = iris)

mod |> tidy_plus_plus()
#> # A tibble: 4 × 20
#>   group_by   y.level    term   variable var_label var_class var_type var_nlevels
#>   <fct>      <chr>      <chr>  <chr>    <chr>     <chr>     <chr>          <int>
#> 1 versicolor versicolor Sepal… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 2 versicolor versicolor Sepal… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> 3 virginica  virginica  Sepal… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 4 virginica  virginica  Sepal… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> # ℹ 12 more variables: contrasts <chr>, contrasts_type <chr>,
#> #   reference_row <lgl>, label <chr>, n_obs <dbl>, n_event <dbl>,
#> #   estimate <dbl>, std.error <dbl>, statistic <dbl>, p.value <dbl>,
#> #   conf.low <dbl>, conf.high <dbl>
mod |> tidy_plus_plus(group_by = auto_group_by()) # default
#> # A tibble: 4 × 20
#>   group_by   y.level    term   variable var_label var_class var_type var_nlevels
#>   <fct>      <chr>      <chr>  <chr>    <chr>     <chr>     <chr>          <int>
#> 1 versicolor versicolor Sepal… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 2 versicolor versicolor Sepal… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> 3 virginica  virginica  Sepal… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 4 virginica  virginica  Sepal… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> # ℹ 12 more variables: contrasts <chr>, contrasts_type <chr>,
#> #   reference_row <lgl>, label <chr>, n_obs <dbl>, n_event <dbl>,
#> #   estimate <dbl>, std.error <dbl>, statistic <dbl>, p.value <dbl>,
#> #   conf.low <dbl>, conf.high <dbl>
mod |> tidy_plus_plus(group_by = y.level)
#> # A tibble: 4 × 20
#>   group_by   y.level    term   variable var_label var_class var_type var_nlevels
#>   <fct>      <chr>      <chr>  <chr>    <chr>     <chr>     <chr>          <int>
#> 1 versicolor versicolor Sepal… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 2 versicolor versicolor Sepal… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> 3 virginica  virginica  Sepal… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 4 virginica  virginica  Sepal… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> # ℹ 12 more variables: contrasts <chr>, contrasts_type <chr>,
#> #   reference_row <lgl>, label <chr>, n_obs <dbl>, n_event <dbl>,
#> #   estimate <dbl>, std.error <dbl>, statistic <dbl>, p.value <dbl>,
#> #   conf.low <dbl>, conf.high <dbl>
mod |> tidy_plus_plus(group_by = NULL)
#> # A tibble: 4 × 19
#>   y.level    term    variable var_label var_class var_type var_nlevels contrasts
#>   <chr>      <chr>   <chr>    <chr>     <chr>     <chr>          <int> <chr>    
#> 1 versicolor Sepal.… Sepal.L… Sepal.Le… numeric   continu…          NA <NA>     
#> 2 versicolor Sepal.… Sepal.W… Sepal.Wi… numeric   continu…          NA <NA>     
#> 3 virginica  Sepal.… Sepal.L… Sepal.Le… numeric   continu…          NA <NA>     
#> 4 virginica  Sepal.… Sepal.W… Sepal.Wi… numeric   continu…          NA <NA>     
#> # ℹ 11 more variables: contrasts_type <chr>, reference_row <lgl>, label <chr>,
#> #   n_obs <dbl>, n_event <dbl>, estimate <dbl>, std.error <dbl>,
#> #   statistic <dbl>, p.value <dbl>, conf.low <dbl>, conf.high <dbl>
mod |> tidy_plus_plus(group_by = variable)
#> # A tibble: 4 × 20
#>   group_by     y.level   term  variable var_label var_class var_type var_nlevels
#>   <fct>        <chr>     <chr> <chr>    <chr>     <chr>     <chr>          <int>
#> 1 Sepal.Length versicol… Sepa… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 2 Sepal.Length virginica Sepa… Sepal.L… Sepal.Le… numeric   continu…          NA
#> 3 Sepal.Width  versicol… Sepa… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> 4 Sepal.Width  virginica Sepa… Sepal.W… Sepal.Wi… numeric   continu…          NA
#> # ℹ 12 more variables: contrasts <chr>, contrasts_type <chr>,
#> #   reference_row <lgl>, label <chr>, n_obs <dbl>, n_event <dbl>,
#> #   estimate <dbl>, std.error <dbl>, statistic <dbl>, p.value <dbl>,
#> #   conf.low <dbl>, conf.high <dbl>

# multgee::nomLORgee()

library(multgee)
#> Le chargement a nécessité le package : gnm
h <- housing
h$status <- factor(
  h$y,
  labels = c("street", "community", "independant")
)
mod <- multgee::nomLORgee(
  status ~ factor(time) * sec,
  data = h,
  id = id,
  repeated = time,
)

mod |> tidy_plus_plus()
#> ℹ multgee model detected.
#> ✔ `tidy_multgee()` used instead.
#> ℹ Add `tidy_fun = broom.helpers::tidy_multgee` to quiet these messages.
#> # A tibble: 16 × 23
#>    group_by  y.level   term  original_term variable var_label var_class var_type
#>    <fct>     <chr>     <chr> <chr>         <chr>    <chr>     <chr>     <chr>   
#>  1 street    street    fact… <NA>          factor(… factor(t… factor    categor…
#>  2 street    street    fact… factor(time)… factor(… factor(t… factor    categor…
#>  3 street    street    fact… factor(time)… factor(… factor(t… factor    categor…
#>  4 street    street    fact… factor(time)… factor(… factor(t… factor    categor…
#>  5 street    street    sec   sec:1         sec      sec       integer   continu…
#>  6 street    street    fact… factor(time)… factor(… factor(t… <NA>      interac…
#>  7 street    street    fact… factor(time)… factor(… factor(t… <NA>      interac…
#>  8 street    street    fact… factor(time)… factor(… factor(t… <NA>      interac…
#>  9 community community fact… <NA>          factor(… factor(t… factor    categor…
#> 10 community community fact… factor(time)… factor(… factor(t… factor    categor…
#> 11 community community fact… factor(time)… factor(… factor(t… factor    categor…
#> 12 community community fact… factor(time)… factor(… factor(t… factor    categor…
#> 13 community community sec   sec:2         sec      sec       integer   continu…
#> 14 community community fact… factor(time)… factor(… factor(t… <NA>      interac…
#> 15 community community fact… factor(time)… factor(… factor(t… <NA>      interac…
#> 16 community community fact… factor(time)… factor(… factor(t… <NA>      interac…
#> # ℹ 15 more variables: var_nlevels <int>, contrasts <chr>,
#> #   contrasts_type <chr>, reference_row <lgl>, label <chr>, n_obs <dbl>,
#> #   n_event <dbl>, estimate <dbl>, std.error <dbl>, conf.level <dbl>,
#> #   conf.low <dbl>, conf.high <dbl>, statistic <dbl>, df.error <dbl>,
#> #   p.value <dbl>
mod |> tidy_plus_plus(group_by = NULL)
#> ℹ multgee model detected.
#> ✔ `tidy_multgee()` used instead.
#> ℹ Add `tidy_fun = broom.helpers::tidy_multgee` to quiet these messages.
#> # A tibble: 16 × 22
#>    y.level term  original_term variable var_label var_class var_type var_nlevels
#>    <chr>   <chr> <chr>         <chr>    <chr>     <chr>     <chr>          <int>
#>  1 street  fact… <NA>          factor(… factor(t… factor    categor…           4
#>  2 street  fact… factor(time)… factor(… factor(t… factor    categor…           4
#>  3 street  fact… factor(time)… factor(… factor(t… factor    categor…           4
#>  4 street  fact… factor(time)… factor(… factor(t… factor    categor…           4
#>  5 street  sec   sec:1         sec      sec       integer   continu…          NA
#>  6 street  fact… factor(time)… factor(… factor(t… <NA>      interac…          NA
#>  7 street  fact… factor(time)… factor(… factor(t… <NA>      interac…          NA
#>  8 street  fact… factor(time)… factor(… factor(t… <NA>      interac…          NA
#>  9 commun… fact… <NA>          factor(… factor(t… factor    categor…           4
#> 10 commun… fact… factor(time)… factor(… factor(t… factor    categor…           4
#> 11 commun… fact… factor(time)… factor(… factor(t… factor    categor…           4
#> 12 commun… fact… factor(time)… factor(… factor(t… factor    categor…           4
#> 13 commun… sec   sec:2         sec      sec       integer   continu…          NA
#> 14 commun… fact… factor(time)… factor(… factor(t… <NA>      interac…          NA
#> 15 commun… fact… factor(time)… factor(… factor(t… <NA>      interac…          NA
#> 16 commun… fact… factor(time)… factor(… factor(t… <NA>      interac…          NA
#> # ℹ 14 more variables: contrasts <chr>, contrasts_type <chr>,
#> #   reference_row <lgl>, label <chr>, n_obs <dbl>, n_event <dbl>,
#> #   estimate <dbl>, std.error <dbl>, conf.level <dbl>, conf.low <dbl>,
#> #   conf.high <dbl>, statistic <dbl>, df.error <dbl>, p.value <dbl>

# svrepmisc::svymultinom
# custom tidier creating a y.level column
library(svrepmisc)
des <- iris |> srvyr::as_survey() |> srvyr::as_survey_rep()
mod <- svymultinom(Species ~ Petal.Length + Petal.Width, design = des)
tidy_svymultinom <- function(x, ...) {
  broom::tidy(x, ...) |> 
    tidyr::separate(term, into = c("y.level", "term"), extra = "merge")
}
tidy_plus_plus(mod, disambiguate_terms = TRUE, tidy_fun = tidy_svymultinom)
#> ✖ Unable to identify the list of variables.
#> 
#> This is usually due to an error calling `stats::model.frame(x)`or `stats::model.matrix(x)`.
#> It could be the case if that type of model does not implement these methods.
#> Rarely, this error may occur if the model object was created within
#> a functional programming framework (e.g. using `lappy()`, `purrr::map()`, etc.).
#> # A tibble: 6 × 19
#>   group_by   y.level    term   variable var_label var_class var_type var_nlevels
#>   <fct>      <chr>      <chr>  <chr>    <chr>         <int> <chr>          <int>
#> 1 versicolor versicolor Inter… Interce… Intercep…        NA unknown           NA
#> 2 versicolor versicolor Petal… Petal.L… Petal.Le…        NA unknown           NA
#> 3 versicolor versicolor Petal… Petal.W… Petal.Wi…        NA unknown           NA
#> 4 virginica  virginica  Inter… Interce… Intercep…        NA unknown           NA
#> 5 virginica  virginica  Petal… Petal.L… Petal.Le…        NA unknown           NA
#> 6 virginica  virginica  Petal… Petal.W… Petal.Wi…        NA unknown           NA
#> # ℹ 11 more variables: contrasts <chr>, contrasts_type <chr>,
#> #   reference_row <lgl>, label <chr>, estimate <dbl>, std.error <dbl>,
#> #   statistic <dbl>, p.value <dbl>, conf.low <dbl>, conf.high <dbl>, n <dbl>

# lm model

mod <- lm(Sepal.Length ~ Sepal.Width + Petal.Length + Species, data = iris)
mod |> tidy_plus_plus()
#> # A tibble: 5 × 17
#>   term              variable  var_label var_class var_type var_nlevels contrasts
#>   <chr>             <chr>     <chr>     <chr>     <chr>          <int> <chr>    
#> 1 Sepal.Width       Sepal.Wi… Sepal.Wi… numeric   continu…          NA <NA>     
#> 2 Petal.Length      Petal.Le… Petal.Le… numeric   continu…          NA <NA>     
#> 3 Speciessetosa     Species   Species   factor    categor…           3 contr.tr…
#> 4 Speciesversicolor Species   Species   factor    categor…           3 contr.tr…
#> 5 Speciesvirginica  Species   Species   factor    categor…           3 contr.tr…
#> # ℹ 10 more variables: contrasts_type <chr>, reference_row <lgl>, label <chr>,
#> #   n_obs <dbl>, estimate <dbl>, std.error <dbl>, statistic <dbl>,
#> #   p.value <dbl>, conf.low <dbl>, conf.high <dbl>
mod |> tidy_plus_plus(group_by = var_type)
#> # A tibble: 5 × 18
#>   group_by    term   variable var_label var_class var_type var_nlevels contrasts
#>   <fct>       <chr>  <chr>    <chr>     <chr>     <chr>          <int> <chr>    
#> 1 continuous  Sepal… Sepal.W… Sepal.Wi… numeric   continu…          NA <NA>     
#> 2 continuous  Petal… Petal.L… Petal.Le… numeric   continu…          NA <NA>     
#> 3 categorical Speci… Species  Species   factor    categor…           3 contr.tr…
#> 4 categorical Speci… Species  Species   factor    categor…           3 contr.tr…
#> 5 categorical Speci… Species  Species   factor    categor…           3 contr.tr…
#> # ℹ 10 more variables: contrasts_type <chr>, reference_row <lgl>, label <chr>,
#> #   n_obs <dbl>, estimate <dbl>, std.error <dbl>, statistic <dbl>,
#> #   p.value <dbl>, conf.low <dbl>, conf.high <dbl>

# pscl::zeroinfl()
library(pscl)
#> Classes and Methods for R originally developed in the
#> Political Science Computational Laboratory
#> Department of Political Science
#> Stanford University (2002-2015),
#> by and under the direction of Simon Jackman.
#> hurdle and zeroinfl functions by Achim Zeileis.
data("bioChemists", package = "pscl")
mod <- zeroinfl(art ~ fem + mar + phd | fem + mar + phd, data = bioChemists)
mod |> tidy_plus_plus()
#> ℹ <zeroinfl> model detected.
#> ✔ `tidy_zeroinfl()` used instead.
#> ℹ Add `tidy_fun = broom.helpers::tidy_zeroinfl` to quiet these messages.
#> # A tibble: 10 × 22
#>    group_by  component term  original_term variable var_label var_class var_type
#>    <fct>     <chr>     <chr> <chr>         <chr>    <chr>     <chr>     <chr>   
#>  1 conditio… conditio… femM… <NA>          fem      fem       factor    dichoto…
#>  2 conditio… conditio… femW… count_femWom… fem      fem       factor    dichoto…
#>  3 conditio… conditio… marS… <NA>          mar      mar       factor    dichoto…
#>  4 conditio… conditio… marM… count_marMar… mar      mar       factor    dichoto…
#>  5 conditio… conditio… phd   count_phd     phd      phd       numeric   continu…
#>  6 zero_inf… zero_inf… femM… <NA>          fem      fem       factor    dichoto…
#>  7 zero_inf… zero_inf… femW… zero_femWomen fem      fem       factor    dichoto…
#>  8 zero_inf… zero_inf… marS… <NA>          mar      mar       factor    dichoto…
#>  9 zero_inf… zero_inf… marM… zero_marMarr… mar      mar       factor    dichoto…
#> 10 zero_inf… zero_inf… phd   zero_phd      phd      phd       numeric   continu…
#> # ℹ 14 more variables: var_nlevels <int>, contrasts <chr>,
#> #   contrasts_type <chr>, reference_row <lgl>, label <chr>, n_obs <dbl>,
#> #   estimate <dbl>, std.error <dbl>, conf.level <dbl>, conf.low <dbl>,
#> #   conf.high <dbl>, statistic <dbl>, df.error <dbl>, p.value <dbl>
mod |> tidy_plus_plus(group_by = NULL)
#> ℹ <zeroinfl> model detected.
#> ✔ `tidy_zeroinfl()` used instead.
#> ℹ Add `tidy_fun = broom.helpers::tidy_zeroinfl` to quiet these messages.
#> # A tibble: 10 × 21
#>    component     term       original_term  variable var_label var_class var_type
#>    <chr>         <chr>      <chr>          <chr>    <chr>     <chr>     <chr>   
#>  1 conditional   femMen     <NA>           fem      fem       factor    dichoto…
#>  2 conditional   femWomen   count_femWomen fem      fem       factor    dichoto…
#>  3 conditional   marSingle  <NA>           mar      mar       factor    dichoto…
#>  4 conditional   marMarried count_marMarr… mar      mar       factor    dichoto…
#>  5 conditional   phd        count_phd      phd      phd       numeric   continu…
#>  6 zero_inflated femMen     <NA>           fem      fem       factor    dichoto…
#>  7 zero_inflated femWomen   zero_femWomen  fem      fem       factor    dichoto…
#>  8 zero_inflated marSingle  <NA>           mar      mar       factor    dichoto…
#>  9 zero_inflated marMarried zero_marMarri… mar      mar       factor    dichoto…
#> 10 zero_inflated phd        zero_phd       phd      phd       numeric   continu…
#> # ℹ 14 more variables: var_nlevels <int>, contrasts <chr>,
#> #   contrasts_type <chr>, reference_row <lgl>, label <chr>, n_obs <dbl>,
#> #   estimate <dbl>, std.error <dbl>, conf.level <dbl>, conf.low <dbl>,
#> #   conf.high <dbl>, statistic <dbl>, df.error <dbl>, p.value <dbl>

# mixed model 
library(glmmTMB)
#> Warning in check_dep_version(dep_pkg = "TMB"): package version mismatch: 
#> glmmTMB was built with TMB package version 1.9.15
#> Current TMB package version is 1.9.16
#> Please re-install glmmTMB from source or restore original 'TMB' package (see '?reinstalling' for more information)
mod <- glmmTMB(
  count ~ spp + mined + cover + (1 + cover | site),
  ziformula = ~mined + (1 | site),
  family = poisson(),
  data = Salamanders
)
#> Warning in finalizeTMB(TMBStruc, obj, fit, h, data.tmb.old): Model convergence
#> problem; non-positive-definite Hessian matrix. See vignette('troubleshooting')
#> Warning in finalizeTMB(TMBStruc, obj, fit, h, data.tmb.old): Model convergence
#> problem; singular convergence (7). See vignette('troubleshooting'),
#> help('diagnose')
mod |> tidy_plus_plus() # by default, by component
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> # A tibble: 16 × 22
#>    group_by component term   original_term variable var_label var_class var_type
#>    <fct>    <chr>     <chr>  <chr>         <chr>    <chr>     <chr>     <chr>   
#>  1 cond     cond      sppGP  <NA>          spp      spp       factor    categor…
#>  2 cond     cond      sppPR  sppPR         spp      spp       factor    categor…
#>  3 cond     cond      sppDM  sppDM         spp      spp       factor    categor…
#>  4 cond     cond      sppEC… sppEC-A       spp      spp       factor    categor…
#>  5 cond     cond      sppEC… sppEC-L       spp      spp       factor    categor…
#>  6 cond     cond      sppDE… sppDES-L      spp      spp       factor    categor…
#>  7 cond     cond      sppDF  sppDF         spp      spp       factor    categor…
#>  8 cond     cond      mined… <NA>          mined    mined     factor    dichoto…
#>  9 cond     cond      mined… minedno       mined    mined     factor    dichoto…
#> 10 cond     cond      cover  cover         cover    cover     numeric   continu…
#> 11 cond     cond      site.… sd__(Interce… site.sd… site.sd_… <NA>      ran_pars
#> 12 cond     cond      site.… sd__cover     site.sd… site.sd_… <NA>      ran_pars
#> 13 cond     cond      site.… cor__(Interc… site.co… site.cor… <NA>      ran_pars
#> 14 zi       zi        mined… <NA>          mined    mined     factor    dichoto…
#> 15 zi       zi        mined… minedno       mined    mined     factor    dichoto…
#> 16 zi       zi        site.… sd__(Interce… site.sd… site.sd_… <NA>      ran_pars
#> # ℹ 14 more variables: var_nlevels <int>, contrasts <chr>,
#> #   contrasts_type <chr>, reference_row <lgl>, label <chr>, n_obs <dbl>,
#> #   effect <chr>, group <chr>, estimate <dbl>, std.error <dbl>,
#> #   statistic <dbl>, p.value <dbl>, conf.low <dbl>, conf.high <dbl>
mod |> tidy_plus_plus(group_by = effect)
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> # A tibble: 16 × 22
#>    group_by component term   original_term variable var_label var_class var_type
#>    <fct>    <chr>     <chr>  <chr>         <chr>    <chr>     <chr>     <chr>   
#>  1 fixed    cond      sppGP  <NA>          spp      spp       factor    categor…
#>  2 fixed    cond      sppPR  sppPR         spp      spp       factor    categor…
#>  3 fixed    cond      sppDM  sppDM         spp      spp       factor    categor…
#>  4 fixed    cond      sppEC… sppEC-A       spp      spp       factor    categor…
#>  5 fixed    cond      sppEC… sppEC-L       spp      spp       factor    categor…
#>  6 fixed    cond      sppDE… sppDES-L      spp      spp       factor    categor…
#>  7 fixed    cond      sppDF  sppDF         spp      spp       factor    categor…
#>  8 fixed    cond      mined… <NA>          mined    mined     factor    dichoto…
#>  9 fixed    cond      mined… minedno       mined    mined     factor    dichoto…
#> 10 fixed    cond      cover  cover         cover    cover     numeric   continu…
#> 11 fixed    zi        mined… <NA>          mined    mined     factor    dichoto…
#> 12 fixed    zi        mined… minedno       mined    mined     factor    dichoto…
#> 13 ran_pars cond      site.… sd__(Interce… site.sd… site.sd_… <NA>      ran_pars
#> 14 ran_pars cond      site.… sd__cover     site.sd… site.sd_… <NA>      ran_pars
#> 15 ran_pars cond      site.… cor__(Interc… site.co… site.cor… <NA>      ran_pars
#> 16 ran_pars zi        site.… sd__(Interce… site.sd… site.sd_… <NA>      ran_pars
#> # ℹ 14 more variables: var_nlevels <int>, contrasts <chr>,
#> #   contrasts_type <chr>, reference_row <lgl>, label <chr>, n_obs <dbl>,
#> #   effect <chr>, group <chr>, estimate <dbl>, std.error <dbl>,
#> #   statistic <dbl>, p.value <dbl>, conf.low <dbl>, conf.high <dbl>
mod |> tidy_plus_plus(group_by = c(component, effect))
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(vv)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> Warning in sqrt(diag(object$cov.fixed)): Production de NaN
#> # A tibble: 16 × 22
#>    group_by  component term  original_term variable var_label var_class var_type
#>    <fct>     <chr>     <chr> <chr>         <chr>    <chr>     <chr>     <chr>   
#>  1 cond_fix… cond      sppGP <NA>          spp      spp       factor    categor…
#>  2 cond_fix… cond      sppPR sppPR         spp      spp       factor    categor…
#>  3 cond_fix… cond      sppDM sppDM         spp      spp       factor    categor…
#>  4 cond_fix… cond      sppE… sppEC-A       spp      spp       factor    categor…
#>  5 cond_fix… cond      sppE… sppEC-L       spp      spp       factor    categor…
#>  6 cond_fix… cond      sppD… sppDES-L      spp      spp       factor    categor…
#>  7 cond_fix… cond      sppDF sppDF         spp      spp       factor    categor…
#>  8 cond_fix… cond      mine… <NA>          mined    mined     factor    dichoto…
#>  9 cond_fix… cond      mine… minedno       mined    mined     factor    dichoto…
#> 10 cond_fix… cond      cover cover         cover    cover     numeric   continu…
#> 11 cond_ran… cond      site… sd__(Interce… site.sd… site.sd_… <NA>      ran_pars
#> 12 cond_ran… cond      site… sd__cover     site.sd… site.sd_… <NA>      ran_pars
#> 13 cond_ran… cond      site… cor__(Interc… site.co… site.cor… <NA>      ran_pars
#> 14 zi_fixed  zi        mine… <NA>          mined    mined     factor    dichoto…
#> 15 zi_fixed  zi        mine… minedno       mined    mined     factor    dichoto…
#> 16 zi_ran_p… zi        site… sd__(Interce… site.sd… site.sd_… <NA>      ran_pars
#> # ℹ 14 more variables: var_nlevels <int>, contrasts <chr>,
#> #   contrasts_type <chr>, reference_row <lgl>, label <chr>, n_obs <dbl>,
#> #   effect <chr>, group <chr>, estimate <dbl>, std.error <dbl>,
#> #   statistic <dbl>, p.value <dbl>, conf.low <dbl>, conf.high <dbl>
```

<sup>Created on 2025-02-25 with [reprex v2.1.1](https://reprex.tidyverse.org)</sup>

author_association: COLLABORATOR
user: login: larmarange
created_at: 2025-02-25T15:32:52Z
body: Hey @ddsjoberg. 

See the previous examples. The default is `group_by = auto_group_by()` that will determine if it should begrouped and according to which column. Othervise, the user can provided any column name or group of column names to force grouping, or `group_by = NULL` to force ungrouping (and forcing the absence of a `group_by` column in the results).

> I think I may keep the S3 methods for these models and expose the `group_by` and `group_label` arguments. And for some models where the user may not want the groupings be default (maybe the mixed-effects models?) we could use `group_by=NULL` default? Also, we could then add another argument o pivot wide for the display. And lastly, since the other `add_*.tbl_regression()` functions sometimes won't work on these structures, we can add messaging about that like we do now for the nnet multinomial models.

I would separate the two issues (when to group, and messages when grouped).

The problem with an S3 method is that you could have exotic cases / models where grouping would be relevant, but not cover by your S3 method. See for example `svrepmisc::svymultinom()` models in the previous examples. Or see examples with mixed models, or grouping the results of a linear model by type of variables...
My position would be to let the tidier / broom.helpers manage if and when it should be grouped. So, no need to update `gtsummary` each time that an additional class of models should be covered by grouping. Simply test the presence of a `group_by` column.
If you really want to keep an S3 method, at least, one of them should be exported (eg `tgl_regression.grouped()`) to let advanced users to use grouping. 

In case of grouping, it is still possible to attach an attribute to the tbl to customize messaging at the next steps, in particular, any warnings by `add_*()` functions.

To be noted, some functions works properly with `nnet::multinom(`), e.g. `add_global_p()`. It's just that the global p-values are repeated. See here and example of wide presentation with global p-values:

https://larmarange.github.io/guide-R/analyses_avancees/regression-logistique-multinomiale.html#affichage-des-r%C3%A9sultats-du-mod%C3%A8le

```r
tbl |> 
  add_global_p() |> 
  multinom_pivot_wider() |> 
  modify_column_hide(starts_with("p.value")) |> 
  modify_column_unhide(p.value_2) |> 
  modify_spanning_header(p.value_2 ~ "**Likelihood-ratio test**")
```
author_association: COLLABORATOR
user: login: larmarange
created_at: 2025-02-27T17:49:16Z
body: Hi @ddsjoberg 

`tidy_group_by()` has been integrated in dev version of `broom.helpers` and I have also added better support of `VGAM::vglm()` modems (including polytomial and multi-components models).

Could you have a llok at it? My plan would be to release soon a new version of `broom.hlepers` so you could refer to it in `gtsummary`.

Cheers
author_association: COLLABORATOR
user: login: larmarange
created_at: 2025-03-04T17:28:42Z
body: Hi @ddsjoberg any comment before I release a new version of broom helpers?
author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-03-04T17:39:03Z
body: > Hi [@ddsjoberg](https://github.com/ddsjoberg) any comment before I release a new version of broom helpers?

I've only had a chance to take a quick look so far, and it looks great! I can take a deeper dive, but it may not be until the weekend?
author_association: OWNER
user: login: larmarange
created_at: 2025-03-04T21:14:30Z
body: No problem. 😉
author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-03-05T01:16:53Z
body: I took a look and this looks great. I agree with you about the S3 methods. I think I'll add the auto handling of the group_by column when found to the default `tbl_regression.default()` method. But for the objects classes we already have S3 methods for, I'll expose the `group_by` and `group_labels` arguments.

Thanks for the great update! I just made a gtsummary release, and don't have a timeline for the next one, but I would imagine it'll be on CRAN in the next 1-3 months.
author_association: OWNER
user: login: ddsjoberg
created_at: 2025-03-05T01:24:17Z
body: I think it would be great to add an example to tbe `tidy_group_by()` function using the `group_labels` argument. Also, I've been using the `@examplesIf` tag lately to conditionally run the examples code only when the needed pkgs are available. It's great because it doesn't include the `if () { ... }` fence when the example is printed in the help file. Could be a small, nice visual improvement?  
author_association: OWNER
user: login: larmarange
created_at: 2025-03-05T17:12:22Z
body: Thanks for the feedback. I have updated doc to use `@examplesIf`.

For `tidy_group_by()`, there is already an example with `group_labels`. cf. https://larmarange.github.io/broom.helpers/dev/reference/tidy_group_by.html  Do you have the feeling that additional examples would be relevant?
author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-03-05T19:53:03Z
body: Ah, oops. I glanced quickly and didn't see that example the first time. I think it's good as it is!
author_association: OWNER
user: login: larmarange
created_at: 2025-03-06T11:21:07Z
body: `broom.helpers` 1.20.0 is now on CRAN. ;-)
author_association: COLLABORATOR
user: login: larmarange
created_at: 2025-04-04T20:05:35Z
body: Just reopening the question 😉

Let me know how you would like to proceed

Cheers
author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-04-04T20:32:08Z
body: > Just reopening the question 😉

Sorry, I just need to get it to the top of my list, and it's pretty full at the moment 😭 
author_association: OWNER
user: login: larmarange
created_at: 2025-04-04T21:17:36Z
body: Don't worry 😉 I totally understand
author_association: COLLABORATOR

Diff:
diff --git a/DESCRIPTION b/DESCRIPTION
index 41f8a8f104..b89e6f6bbc 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,6 +1,6 @@
 Package: gtsummary
 Title: Presentation-Ready Data Summary and Analytic Result Tables
-Version: 2.2.0.9002
+Version: 2.2.0.9003
 Authors@R: c(
     person("Daniel D.", "Sjoberg", , "danield.sjoberg@gmail.com", role = c("aut", "cre"),
            comment = c(ORCID = "0000-0003-0862-2018")),
@@ -59,7 +59,7 @@ Imports:
 Suggests: 
     aod (>= 1.3.3),
     broom (>= 1.0.5),
-    broom.helpers (>= 1.17.0),
+    broom.helpers (>= 1.20.0),
     broom.mixed (>= 0.2.9),
     car (>= 3.0-11),
     cardx (>= 0.2.4),
diff --git a/NAMESPACE b/NAMESPACE
index bd3a6950a6..87492d6f42 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -73,7 +73,6 @@ S3method(tbl_regression,lmerMod)
 S3method(tbl_regression,mipo)
 S3method(tbl_regression,mira)
 S3method(tbl_regression,model_fit)
-S3method(tbl_regression,multinom)
 S3method(tbl_regression,stanreg)
 S3method(tbl_regression,survreg)
 S3method(tbl_regression,workflow)
diff --git a/NEWS.md b/NEWS.md
index ec49e3323f..c2394a46f7 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,7 @@
 # gtsummary (development version)
 
+* The `tbl_regression()` function now takes advantage of a new feature in {broom.helpers} to support multicomponent models, such as, multinomial models and mixed-effects models. Review `broom.helpers::tidy_group_by()` for details. (#1540)
+
 * Fix in `gather_ard()` for `tbl_regression()` and `tbl_uvregression()`. Previously, only the ARD for the primary regression model(s) would be returned, and now all ARDs are returned including those from subsequent calls to `add_*()` functions. (#2208)
 
 * Fix in `tbl_merge(merge_vars)` argument when a table contained a `"row_type"` column and the tables were not merged by this variable (which is included in the default). (#2205)
diff --git a/R/tbl_regression.R b/R/tbl_regression.R
index 913cb46262..d819058d46 100644
--- a/R/tbl_regression.R
+++ b/R/tbl_regression.R
@@ -223,6 +223,9 @@ tbl_regression.default <- function(x,
     get_theme_element("tbl_regression-fn:addnl-fn-to-run", default = identity) |>
     do.call(list(res))
 
+  # running function for multimodel models (and similar) for grouped results ---
+  res <- .regression_grouped_results(res)
+
   # return results -------------------------------------------------------------
   res <- res |>
     modify_table_styling(
@@ -234,3 +237,47 @@ tbl_regression.default <- function(x,
   res$cards <- list(tbl_regression = NULL)
   res
 }
+
+.regression_grouped_results <- function(x) {
+  # if grouped model, then adjusting output accordingly
+  if ("group_by" %in% names(x$table_body)) {
+    # rename the grouping column
+    x$table_body <-
+      x$table_body |>
+      dplyr::rename(groupname_col = "group_by") |>
+      dplyr::relocate("groupname_col", .before = "label")
+
+    # assign header label
+    x <- x |>
+      modify_table_styling(
+        columns = "groupname_col",
+        hide = FALSE,
+        label = .grouped_data_header(x$inputs$x),
+        align = "left"
+      )
+
+    # warning about multinomial models
+    cli::cli_inform(
+      c("i" = "Multinomial models, multi-component models and other groups models
+               have a different
+               underlying structure than the models gtsummary was designed for.",
+        "*" = "Functions designed to work with {.fun tbl_regression} objects may yield unexpected results.",
+        "i" = "Suppress this message with {.help suppressMessages}.")
+    )
+  }
+
+  # return regression tbl
+  x
+}
+
+.grouped_data_header <- function(x) {
+  UseMethod(".grouped_data_header")
+}
+
+.grouped_data_header.default <- function(x) {
+  "**Group**"
+}
+
+.grouped_data_header.multinom <- function(x) {
+  "**Outcome**"
+}
diff --git a/R/tbl_regression_methods.R b/R/tbl_regression_methods.R
index f1bcff618f..0349537edc 100644
--- a/R/tbl_regression_methods.R
+++ b/R/tbl_regression_methods.R
@@ -49,11 +49,6 @@ tbl_regression.survreg <- function(x, tidy_fun = function(x, ...) broom::tidy(x,
 tbl_regression.mira <- function(x, tidy_fun = pool_and_tidy_mice, ...) {
   tbl <- tbl_regression.default(x = x, tidy_fun = tidy_fun, ...)
 
-  # adding outcome levels to multinomial models
-  if (inherits(x$analyses[[1]], "multinom")) {
-    tbl <- .multinom_modifations(tbl)
-  }
-
   tbl
 }
 
@@ -113,35 +108,3 @@ tbl_regression.crr <- function(x, ...) {
   tbl_regression.default(x = x, ...)
 }
 
-#' @export
-#' @rdname tbl_regression_methods
-tbl_regression.multinom <- function(x, ...) {
-  result <- tbl_regression.default(x = x, ...)
-
-  # grouping by outcome, and printing warning message
-  .multinom_modifations(result)
-}
-
-.multinom_modifations <- function(x) {
-  # adding a grouped header for the outcome levels
-  x$table_body <-
-    x$table_body |>
-    mutate(groupname_col = .data$y.level, .before = 1L)
-
-  x <- modify_table_styling(
-      x = x,
-      columns = all_of("groupname_col"),
-      hide = FALSE,
-      label = "**Outcome**",
-      align = "left"
-    )
-
-  # warning about multinomial models
-  cli::cli_inform(
-    c("i" = "Multinomial models have a different underlying structure than the
-             models gtsummary was designed for.",
-      "*" = "Functions designed to work with {.fun tbl_regression} objects may yield unexpected results.")
-  )
-
-  x
-}
diff --git a/man/tbl_regression_methods.Rd b/man/tbl_regression_methods.Rd
index cbf512b009..39c6d49e92 100644
--- a/man/tbl_regression_methods.Rd
+++ b/man/tbl_regression_methods.Rd
@@ -15,7 +15,6 @@
 \alias{tbl_regression.brmsfit}
 \alias{tbl_regression.gam}
 \alias{tbl_regression.crr}
-\alias{tbl_regression.multinom}
 \title{Methods for tbl_regression}
 \usage{
 \method{tbl_regression}{model_fit}(x, ...)
@@ -72,8 +71,6 @@
 \method{tbl_regression}{gam}(x, tidy_fun = tidy_gam, ...)
 
 \method{tbl_regression}{crr}(x, ...)
-
-\method{tbl_regression}{multinom}(x, ...)
 }
 \arguments{
 \item{x}{(regression model)\cr
diff --git a/tests/testthat/_snaps/tbl_regression_methods.md b/tests/testthat/_snaps/tbl_regression_methods.md
index e4542ed72e..47a4d5755f 100644
--- a/tests/testthat/_snaps/tbl_regression_methods.md
+++ b/tests/testthat/_snaps/tbl_regression_methods.md
@@ -105,6 +105,21 @@
         **Characteristic** **Beta**  **95% CI**
       1                 hp    -0.03 -0.06, 0.00
 
+---
+
+    Code
+      as.data.frame(tbl_regression(lme4::lmer(mpg ~ hp + (1 | cyl), mtcars), tidy = broom.mixed::tidy,
+      group_by = "effect", group_label = c(fixed = "Fixed Effects", ran_pars = "Random Effects")))
+    Message
+      i Multinomial models, multi-component models and other groups models have a different underlying structure than the models gtsummary was designed for.
+      * Functions designed to work with `tbl_regression()` objects may yield unexpected results.
+      i Suppress this message with `?suppressMessages()`.
+    Output
+             **Group**       **Characteristic** **Beta**  **95% CI**
+      1  Fixed Effects                       hp    -0.03 -0.06, 0.00
+      2 Random Effects      cyl.sd__(Intercept)      4.0        <NA>
+      3           <NA> Residual.sd__Observation      3.1        <NA>
+
 # tbl_regression.gam()
 
     Code
@@ -151,8 +166,9 @@
       final  value 29.311125 
       converged
     Message
-      i Multinomial models have a different underlying structure than the models gtsummary was designed for.
+      i Multinomial models, multi-component models and other groups models have a different underlying structure than the models gtsummary was designed for.
       * Functions designed to work with `tbl_regression()` objects may yield unexpected results.
+      i Suppress this message with `?suppressMessages()`.
     Output
         **Outcome** **Characteristic** **log(OR)**  **95% CI** **p-value**
       1           6                 am        -1.3  -3.3, 0.73         0.2
diff --git a/tests/testthat/test-tbl_regression_methods.R b/tests/testthat/test-tbl_regression_methods.R
index 759b2f31ec..53a269e4a6 100644
--- a/tests/testthat/test-tbl_regression_methods.R
+++ b/tests/testthat/test-tbl_regression_methods.R
@@ -66,6 +66,17 @@ test_that("tbl_regression.lmerMod()", {
       tbl_regression() |>
       as.data.frame()
   )
+
+  expect_snapshot(
+    lme4::lmer(mpg ~ hp + (1 | cyl), mtcars) |>
+      tbl_regression(
+        tidy = broom.mixed::tidy,
+        group_by = "effect",
+        group_label = c(fixed = "Fixed Effects",
+                        ran_pars = "Random Effects")
+      ) |>
+      as.data.frame()
+  )
 })
 
 test_that("tbl_regression.gam()", {


Reviews:
user: login: larmarange
body: Thanks a lot
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T09:04:00Z
user: login: ddsjoberg
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-17T13:46:27Z
user: login: ddsjoberg
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-17T13:47:38Z
user: login: ddsjoberg
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-17T16:16:58Z
user: login: larmarange
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-17T17:07:14Z

Review comments:
diff_hunk: @@ -234,3 +237,45 @@ tbl_regression.default <- function(x,
   res$cards <- list(tbl_regression = NULL)
   res
 }
+
+.regression_grouped_results <- function(x) {
+  # if grouped model, then adjusting output accordingly
+  if ("group_by" %in% names(x$table_body)) {
+    # rename the grouping column
+    x$table_body <-
+      x$table_body |>
+      dplyr::rename(groupname_col = "group_by") |>
+      dplyr::relocate("groupname_col", .before = "label")
+
+    # assign header label
+    x <- x |>
+      modify_table_styling(
+        columns = "groupname_col",
+        hide = FALSE,
+        label = .grouped_data_header(x$inputs$x),
+        align = "left"
+      )
+
+    # warning about multinomial models
+    cli::cli_inform(
+      c("i" = "Multinomial models and other grouped models have a different
path: R/tbl_regression.R
user: login: larmarange
body: - [x] To be more general, maybe the expression "Multinomial models, multi-component models and other groups models".
- [x] How can we remove this message? (for an officiel report for example). Is there an option `inform = FALSE` to consider? 
- [x] The way to quiet this message should be indicated in the message.
created_at: 2025-04-17T08:56:34Z
author_association: COLLABORATOR
diff_hunk: @@ -234,3 +237,45 @@ tbl_regression.default <- function(x,
   res$cards <- list(tbl_regression = NULL)
   res
 }
+
+.regression_grouped_results <- function(x) {
+  # if grouped model, then adjusting output accordingly
+  if ("group_by" %in% names(x$table_body)) {
+    # rename the grouping column
+    x$table_body <-
+      x$table_body |>
+      dplyr::rename(groupname_col = "group_by") |>
+      dplyr::relocate("groupname_col", .before = "label")
+
+    # assign header label
+    x <- x |>
+      modify_table_styling(
+        columns = "groupname_col",
+        hide = FALSE,
+        label = .grouped_data_header(x$inputs$x),
+        align = "left"
+      )
+
+    # warning about multinomial models
+    cli::cli_inform(
+      c("i" = "Multinomial models and other grouped models have a different
+               underlying structure than the models gtsummary was designed for.",
+        "*" = "Functions designed to work with {.fun tbl_regression} objects may yield unexpected results.")
+    )
+  }
+
+  # return regression tbl
+  x
+}
+
+.grouped_data_header <- function(x) {
+  UseMethod(".grouped_data_header")
+}
+
+.grouped_data_header.default <- function(x) {
+  "**Group**"
+}
+
+.grouped_data_header.default <- function(x) {
path: R/tbl_regression.R
user: login: larmarange
body: There ate two defaults??
created_at: 2025-04-17T08:57:14Z
author_association: COLLABORATOR
diff_hunk: @@ -234,3 +237,45 @@ tbl_regression.default <- function(x,
   res$cards <- list(tbl_regression = NULL)
   res
 }
+
+.regression_grouped_results <- function(x) {
+  # if grouped model, then adjusting output accordingly
+  if ("group_by" %in% names(x$table_body)) {
+    # rename the grouping column
+    x$table_body <-
+      x$table_body |>
+      dplyr::rename(groupname_col = "group_by") |>
+      dplyr::relocate("groupname_col", .before = "label")
+
+    # assign header label
+    x <- x |>
+      modify_table_styling(
+        columns = "groupname_col",
+        hide = FALSE,
+        label = .grouped_data_header(x$inputs$x),
+        align = "left"
+      )
+
+    # warning about multinomial models
+    cli::cli_inform(
+      c("i" = "Multinomial models and other grouped models have a different
+               underlying structure than the models gtsummary was designed for.",
+        "*" = "Functions designed to work with {.fun tbl_regression} objects may yield unexpected results.")
+    )
+  }
+
+  # return regression tbl
+  x
+}
+
+.grouped_data_header <- function(x) {
+  UseMethod(".grouped_data_header")
+}
+
+.grouped_data_header.default <- function(x) {
+  "**Group**"
+}
+
+.grouped_data_header.default <- function(x) {
path: R/tbl_regression.R
user: login: larmarange
body: Visibile only when you apply `as.data.frame()`.  Not in the gt version. Is it expected?

created_at: 2025-04-17T08:59:40Z
author_association: COLLABORATOR
diff_hunk: @@ -234,3 +237,45 @@ tbl_regression.default <- function(x,
   res$cards <- list(tbl_regression = NULL)
   res
 }
+
+.regression_grouped_results <- function(x) {
+  # if grouped model, then adjusting output accordingly
+  if ("group_by" %in% names(x$table_body)) {
+    # rename the grouping column
+    x$table_body <-
+      x$table_body |>
+      dplyr::rename(groupname_col = "group_by") |>
+      dplyr::relocate("groupname_col", .before = "label")
+
+    # assign header label
+    x <- x |>
+      modify_table_styling(
+        columns = "groupname_col",
+        hide = FALSE,
+        label = .grouped_data_header(x$inputs$x),
+        align = "left"
+      )
+
+    # warning about multinomial models
+    cli::cli_inform(
+      c("i" = "Multinomial models and other grouped models have a different
path: R/tbl_regression.R
user: login: ddsjoberg
body: Good ideas! Message is updated with your suggested text and a note about `suppressMessages()`
created_at: 2025-04-17T13:46:27Z
author_association: OWNER
diff_hunk: @@ -234,3 +237,45 @@ tbl_regression.default <- function(x,
   res$cards <- list(tbl_regression = NULL)
   res
 }
+
+.regression_grouped_results <- function(x) {
+  # if grouped model, then adjusting output accordingly
+  if ("group_by" %in% names(x$table_body)) {
+    # rename the grouping column
+    x$table_body <-
+      x$table_body |>
+      dplyr::rename(groupname_col = "group_by") |>
+      dplyr::relocate("groupname_col", .before = "label")
+
+    # assign header label
+    x <- x |>
+      modify_table_styling(
+        columns = "groupname_col",
+        hide = FALSE,
+        label = .grouped_data_header(x$inputs$x),
+        align = "left"
+      )
+
+    # warning about multinomial models
+    cli::cli_inform(
+      c("i" = "Multinomial models and other grouped models have a different
+               underlying structure than the models gtsummary was designed for.",
+        "*" = "Functions designed to work with {.fun tbl_regression} objects may yield unexpected results.")
+    )
+  }
+
+  # return regression tbl
+  x
+}
+
+.grouped_data_header <- function(x) {
+  UseMethod(".grouped_data_header")
+}
+
+.grouped_data_header.default <- function(x) {
+  "**Group**"
+}
+
+.grouped_data_header.default <- function(x) {
path: R/tbl_regression.R
user: login: ddsjoberg
body: This header is visible when exporting to flextable, huxtable, kableExtra, kable, and data frames.

It's just a default, and users can change it as needed with `modify_header()`.
created_at: 2025-04-17T13:47:38Z
author_association: OWNER
diff_hunk: @@ -234,3 +237,45 @@ tbl_regression.default <- function(x,
   res$cards <- list(tbl_regression = NULL)
   res
 }
+
+.regression_grouped_results <- function(x) {
+  # if grouped model, then adjusting output accordingly
+  if ("group_by" %in% names(x$table_body)) {
+    # rename the grouping column
+    x$table_body <-
+      x$table_body |>
+      dplyr::rename(groupname_col = "group_by") |>
+      dplyr::relocate("groupname_col", .before = "label")
+
+    # assign header label
+    x <- x |>
+      modify_table_styling(
+        columns = "groupname_col",
+        hide = FALSE,
+        label = .grouped_data_header(x$inputs$x),
+        align = "left"
+      )
+
+    # warning about multinomial models
+    cli::cli_inform(
+      c("i" = "Multinomial models and other grouped models have a different
+               underlying structure than the models gtsummary was designed for.",
+        "*" = "Functions designed to work with {.fun tbl_regression} objects may yield unexpected results.")
+    )
+  }
+
+  # return regression tbl
+  x
+}
+
+.grouped_data_header <- function(x) {
+  UseMethod(".grouped_data_header")
+}
+
+.grouped_data_header.default <- function(x) {
+  "**Group**"
+}
+
+.grouped_data_header.default <- function(x) {
path: R/tbl_regression.R
user: login: ddsjoberg
body: OH I see what you meant! I didn't change the class name for the multinomial models. Updated!
created_at: 2025-04-17T16:16:58Z
author_association: OWNER

PR comments:
user: login: ddsjoberg
created_at: 2025-04-17T03:16:08Z
body: @larmarange thanks for adding this functionality. Apologies it took so long to implement here 😬 Could you review before we merge?
author_association: OWNER
user: login: larmarange
created_at: 2025-04-17T09:08:14Z
body: Thanks a lot @ddsjoberg 

- [ ] Just to be noted that `bold_labels()` doesn't work on a grouped table. Is it a bug?
- [ ] Should we consider a `bold_group_headers()` function?
- [ ] Once this PR finalized, would you consider a `groups_pivot_wider()` function or do you think it is not in the scope of `gtsummary`? (in the spirit of https://gist.github.com/ddsjoberg/a55afa74ac58e1f895862fcabab62406 )


author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-04-17T13:51:02Z
body: > * [ ]  Just to be noted that `bold_labels()` doesn't work on a grouped table. Is it a bug?

Nah, it's a different structure that `bold_labels()` doesn't work with.

> * [ ]  Should we consider a `bold_group_headers()` function?

I think it's ok to leave it to users to bold if they prefer. There is a similar function (https://mskcc-epi-bio.github.io/bstfun/reference/bold_italicize_group_labels.html), but the process of bolding requires the user to pick an output type which I prefer to avoid here.

> * [ ]  Once this PR finalized, would you consider a `groups_pivot_wider()` function or do you think it is not in the scope of `gtsummary`? (in the spirit of https://gist.github.com/ddsjoberg/a55afa74ac58e1f895862fcabab62406 )

I personally don't want to add it and maintain it. 😬 We can discuss further if you feel strongly?

author_association: OWNER
user: login: ddsjoberg
created_at: 2025-04-17T17:12:33Z
body: Thank you @larmarange !!!
author_association: OWNER
user: login: larmarange
created_at: 2025-04-17T17:39:24Z
body: > > * [ ]   Just to be noted that `bold_labels()` doesn't work on a grouped table. Is it a bug?
> 
> Nah, it's a different structure that `bold_labels()` doesn't work with.

OK, I just understood why. `bold_labels()` bold the first unhidden column, in such  case, `groupname_col`, which is not rendered when rendering as gt. I didn't realise what the rendering of group tables differed according to the output type.

I just found a workaround when exporting to gt: `tbl |> modify_column_hide(groupname_col) |> bold_labels()`. 

> 
> > * [ ]   Should we consider a `bold_group_headers()` function?
> 
> I think it's ok to leave it to users to bold if they prefer. There is a similar function (https://mskcc-epi-bio.github.io/bstfun/reference/bold_italicize_group_labels.html), but the process of bolding requires the user to pick an output type which I prefer to avoid here.

OK. I understand better now. It is done after converting to gt (i.e. with `gt::tab_options(row_group.font.weight = "bold")`). 

> 
> > * [ ]   Once this PR finalized, would you consider a `groups_pivot_wider()` function or do you think it is not in the scope of `gtsummary`? (in the spirit of https://gist.github.com/ddsjoberg/a55afa74ac58e1f895862fcabab62406 )
> 
> I personally don't want to add it and maintain it. 😬 We can discuss further if you feel strongly?

It is clearly a recurring need on my side but I would perfectly understand that you do no want it within gtsummary. I have other possibilities to have a version somewhere else.
author_association: COLLABORATOR
user: login: larmarange
created_at: 2025-04-17T17:39:44Z
body: > Thank you @larmarange !!!

Thanks to you
author_association: COLLABORATOR
user: login: ddsjoberg
created_at: 2025-04-17T17:55:03Z
body: > It is clearly a recurring need on my side but I would perfectly understand that you do no want it within gtsummary. I have other possibilities to have a version somewhere else.

Perhaps we can link to that location in the documentation somewhere?
author_association: OWNER