rgtsummary

How can I make sure to display "zero rows" when using gtsummary::tbl_hierarchical?


I want to count serious adverse events per subject per treatment arm, and display the occurrence for each preferred term (PT) within the corresponding system organ class (SOC). For this, I take the code proposed in the cardinal template catalog for creating safety tables.

However, it drops levels of SOC and PT which are 0 for all treatment arms. Is there a way to force displaying all factor levels in the table?

Compared to the code linked above, I tried to re-assign the factor levels in the data preprocessing step, but this has no effect. It still procudes the table as shown in the link, meaning it drops "empty" factor levels. I would love to produce the same table but having all factor levels displayed with "0" in all variables. This is the code I tried


# Load packages & data -------------------------------------
library(dplyr)
library(gtsummary)

adsl <- random.cdisc.data::cadsl
adae <- random.cdisc.data::cadae

# Capture factor levels
lev_soc <- levels(adae[["AESOC"]])
lev_decod <- levels(adae[["AEDECOD"]])

# Pre-processing --------------------------------------------

adae <- adae %>%
  filter(
    # only serious adverse events
    AESER == "Y"
  ) %>%
  # Re-assign factor levels
  mutate(
    AESOC = factor(AESOC, levels = lev_soc),
    AEDECOD = factor(AEDECOD, levels = lev_decod)
  )


# Generate table ---------------------------------------------

tbl <- adae |>
  tbl_hierarchical(
    variables = c(AESOC, AEDECOD), 
    by = ARM, 
    id = USUBJID,
    denominator = adsl,
    overall_row = TRUE,
    label = "..ard_hierarchical_overall.." ~ "Any SAE"
  )
tbl

Solution

  • The tbl_hierarchical() function, by design, removes unobserved combinations, as this is the typical need. But to get all combinations, we can merge the serious AEs table with a table containing all the AEs. After the merge, we'll have all combinations.

    # Load packages & data -------------------------------------
    library(dplyr, warn.conflicts = FALSE)
    library(gtsummary)
    
    adsl <- random.cdisc.data::cadsl
    adae <- random.cdisc.data::cadae[1:5,]
    
    # Pre-processing --------------------------------------------
    adae_ser <- adae %>%
      # only serious adverse events
      filter(AESER == "Y")
    
    # Generate table ---------------------------------------------
    # build table of serious events
    tbl_ser <- adae_ser |>
      tbl_hierarchical(
        variables = c(AESOC, AEDECOD), 
        by = ARM, 
        id = USUBJID,
        denominator = adsl,
        overall_row = TRUE,
        label = "..ard_hierarchical_overall.." ~ "Any SAE"
      )
    
    # build table of all AEs
    tbl_all <- adae |>
      tbl_hierarchical(
        variables = c(AESOC, AEDECOD), 
        id = USUBJID,
        denominator = adsl,
        overall_row = TRUE,
        label = "..ard_hierarchical_overall.." ~ "Any SAE"
      ) |> 
      # hide statistics columns (since we do not need to see these results)
      modify_column_hide(all_stat_cols())
    
    # merge the tables 
    tbl_final <-
      list(tbl_all, tbl_ser) |> 
      tbl_merge(tab_spanner = FALSE) |> 
      # the missing cells are the non-serious AEs, fill them with zeros
      modify_missing_symbol(
        symbol = "0 (0%)",
        columns = all_stat_cols(),
        rows = TRUE
      )
    

    enter image description here Created on 2025-08-01 with reprex v2.1.1