longitudinal data table in gtsummary

Viewed 53
library(data.table)
library(gtsummary)    
mm<-structure(list(time = c(1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 
    2, 3, 1, 2, 3, 1, 2, 3), meas1 = c(59, 0, 0, 109, 6.8, 0, 98, 
    11, 2.7, 121, 9.9, 0, 136, 63, 22.9, 130, 0, 0, 86, 23, 0), ID = c("001", 
    "001", "001", "002", "002", "002", "003", "003", "003", "004", 
    "004", "004", "005", "005", "005", "006", "006", "006", "007", 
    "007", "007"), gr = c("A", "A", "A", "B", "B", "B", "A", "A", 
    "A", "C", "C", "C", "C", "C", "C", "B", "B", "B", "B", "B", "B"
    )), row.names = c(NA, -21L), class = c("data.table", 
    "data.frame"))

Now, I´d like to generate a table with cells containing the median and IQR for the different levels of group (gr) at each of the different time points.

g <- function(x) {
  k <- as.list(quantile(x, (1 : 3) / 4, na.rm=TRUE))
  names(k) <- .q(Q1, Q2, Q3)
  k
}
mm2 <- mm[, g(meas1), by=.(gr, time)]
mm2[,iqr:=paste0(Q2,"(",Q1,"-",Q3,")")]
dcast(mm2,time~gr,value.var = "iqr")

Is there an easy way to do this in gtsummary? Preferably without inflating the N in the column header and extra bonus if the range could be with a newline centered on the median?

===================================================================

Edit after including the answer from @Daniel D. Sjoberg

tba <- unique(mm$time) %>% 
  sort() %>%
  purrr::map(
    ~ mm %>%
      dplyr::filter(time %in% .x) %>%
      tbl_summary(
        by = gr, 
        include = meas1,
        type = meas1 ~ "continuous2",
        statistic = meas1 ~ c("{median} ({p25}, {p75})", "{min}, {max}"),
        label = list(meas1 = paste("Measure 1 at Timepoint", .x))
      ) 
  ) %>%
  tbl_stack() 

fac <- as.list(as.data.frame(combn(levels(as.factor(mm$gr)),2)))
tbb <- lapply(fac,function(x){
  hh <- mm[gr%in%x]
  unique(hh$time) %>% 
    sort() %>% 
    purrr::map(
      ~hh %>% dplyr::filter(time %in% .x) %>% 
        tbl_summary( by = gr,
                     include = meas1,
                     type = meas1 ~ "continuous2",
                     statistic = meas1 ~ c("{median}","({p25}, {p75})"),
                     label = list(meas1 = paste("Measure 1 at Timepoint", .x))) %>%
        add_difference()) %>% 
    tbl_stack() %>% modify_column_hide(all_stat_cols())
})

tb_b <- tbb %>% tbl_merge(tab_spanner = unlist(lapply(fac,function(x) paste0("**",x[[1]]," vs. ",x[[2]],"**"))))  

tbl_merge(list(tba,tb_b)) 

tba results in desired output: enter image description here

tb_b also results in desired output: enter image description here

But- when merging these, the spanner from tb_b is lost identifying the contrasts.enter image description here Any way of that I can retain the original spanners from tb_b when doing tbl_merge?

Also - the add_q helper is likely wrong when doing these stacked tables. Any way of fixing this?

1 Answers

To get multi-row summaries and p-value, you'll need to construct a table for each timepoint, then stack them.

library(data.table)
library(gtsummary)  

mm<-structure(list(time = c(1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 
                            2, 3, 1, 2, 3, 1, 2, 3), 
                   meas1 = c(59, 0, 0, 109, 6.8, 0, 98, 
                             11, 2.7, 121, 9.9, 0, 136, 63, 22.9, 130, 0, 0, 86, 23, 0), 
                   ID = c("001", 
                          "001", "001", "002", "002", "002", "003", "003", "003", "004", 
                          "004", "004", "005", "005", "005", "006", "006", "006", "007", 
                          "007", "007"), gr = c("A", "A", "A", "B", "B", "B", "A", "A", 
                                                "A", "C", "C", "C", "C", "C", "C", "B", "B", "B", "B", "B", "B"
                          )), row.names = c(NA, -21L), class = c("data.table", 
                                                                 "data.frame"))



unique(mm$time) %>% 
  sort() %>%
  purrr::map(
    ~ mm %>%
      dplyr::filter(time %in% .x) %>%
      tbl_summary(
        by = gr, 
        include = meas1,
        type = meas1 ~ "continuous2",
        statistic = meas1 ~ c("{median} ({p25}, {p75})", "{min}, {max}"),
        label = list(meas1 = paste("Measure 1 at Timepoint", .x))
      ) %>%
      add_p()
  ) %>%
  tbl_stack() %>%
  as_kable() # convert to kable to show on SO
Characteristic A, N = 2 B, N = 3 C, N = 2 p-value
Measure 1 at Timepoint 1 0.2
Median (IQR) 78 (69, 88) 109 (98, 120) 128 (125, 132)
Range 59, 98 86, 130 121, 136
Measure 1 at Timepoint 2 0.5
Median (IQR) 6 (3, 8) 7 (3, 15) 36 (23, 50)
Range 0, 11 0, 23 10, 63
Measure 1 at Timepoint 3 0.4
Median (IQR) 1.4 (0.7, 2.0) 0.0 (0.0, 0.0) 11.4 (5.7, 17.2)
Range 0.0, 2.7 0.0, 0.0 0.0, 22.9

Created on 2022-09-05 with reprex v2.0.2

Related