in longitudinal randomised controlled trials
Tuesday, the 19th of March, 2024
Regarding the analysis of longitudinal RCT data there is a debate going on whether an adjustment for the baseline value of the outcome variable should be made. When an adjustment is made, there is a lot of misunderstanding regarding the way this should be done. Therefore, the aims to:
to explain different methods used to estimate treatment effects in RCTs
to illustrate the different methods with a real data example
to give an advise on how to analyse RCT data
Effectiveness of a long-term homocysteine-lowering treatment with folic acid plus pyridoxine in reducing systolic blood.
In this 24 months , randomized, placebo-controlled trial, a baseline measurement and two follow-up measurements (after 12 and after 24 months) were performed. At each time-point systolic blood pressure was measured four times and the average value was used in the analysis.
add_by_n <- function(data, variable, by, tbl, ...) {
   data %>%
      select(all_of(c(variable, by))) %>%
      dplyr::group_by(.data[[by]]) %>%
      dplyr::summarise_all(~sum(!is.na(.))) %>%
      rlang::set_names(c("by", "variable")) %>%
      dplyr::left_join(
         tbl$df_by %>% select(by, by_col),
         by = "by"
      ) %>%
      mutate(
         by_col = paste0("add_n_", by_col),
         variable = style_number(variable)
      ) %>%
      select(-by) %>%
      tidyr::pivot_wider(names_from = by_col, 
                         values_from = variable)}
tbl_summary(data = dat_twisk_w %>% mutate(TRT01P = fct_rev(TRT01P)),
            by = TRT01P , 
            include = starts_with('SYS_'),
            digit = list(everything() ~ c(1,1)),
            statistic = list(all_continuous() ~ "{mean} ({sd})"),
            missing = 'no',
            label = list( SYS_0 ~ 'Week 0 (Randomization)',
                          SYS_1 ~ 'Week 12',
                          SYS_2 ~ 'Week 24')) %>% 
   add_stat( fns = everything() ~ add_by_n ) %>%
   add_difference(test = list(all_continuous() ~ "t.test"),
                  pvalue_fun = ~style_pvalue(.x, digits = 2),
                  estimate_fun = list(all_continuous() ~ function(x) style_number(x, digits=1)) ) %>% 
   bold_labels() %>% 
   modify_column_merge(pattern = "{estimate} ({ci})") %>% 
   modify_header(
      label = '',
      estimate = '**DIFF (95% CI)**',
      all_stat_cols(FALSE) ~ "**{level}**<br>n = {n}",
      starts_with("add_n_stat") ~ "**n**") %>% 
   modify_table_body(
      ~ .x %>%
         dplyr::relocate(add_n_stat_1, .before = stat_1) %>%
         dplyr::relocate(add_n_stat_2, .before = stat_2)
   ) %>% 
   modify_spanning_header(
      c(all_stat_cols(F), starts_with("add_n_stat")) ~ "**Treatment**") %>% 
   as_gt() %>% 
   gtsave('tbl/tbl_1.png', zoom = 4)
knitr::include_graphics('tbl/tbl_1.png')pacman::p_load(gtreg)
tbl_reg_summary(data = dat_twisk_w %>% mutate(TRT01P = fct_rev(TRT01P)),
                by = TRT01P , 
                include = starts_with('SYS_'),
                label = list( SYS_0 ~ 'Week 0 (Randomization)',
                              SYS_1 ~ 'Week 12',
                              SYS_2 ~ 'Week 24'),
                digits = everything() ~ c(0, 1, 1, 0, 0, 0, 0, 0) ) %>% 
   add_overall() %>% 
   bold_labels() %>% 
   modify_header(
      label = '',
      all_stat_cols(TRUE) ~ "**{level}**" ) %>% 
   modify_spanning_header(
      c(all_stat_cols(F), starts_with("add_n_stat")) ~ "**Treatment**")%>% 
   as_gt() %>% 
   gtsave('tbl/tbl_2.png', zoom = 3)
knitr::include_graphics('tbl/tbl_2.png')ggplot(data = dat_twisk_l %>% drop_na(AVAL),
       aes(x = AVISITN, y = AVAL, color = TRT01P)) +
   geom_quasirandom(dodge.width = 0.5, alpha = 0.5, size = 2) +
   scale_colour_brewer(palette = "Set1",
                       name = 'Treatment') +
   scale_y_continuous(name = 'Systolic Blood Pressure (mmHg)',
                      limits = c(80, 200)) +
   scale_x_continuous(name = '',
                      breaks = c(0, 1, 2),
                      labels = c('Week 0\nRandomization', 'Week 12', 'Week 24')) +
   theme_bw(base_size = 18) + 
   theme(panel.grid.minor = element_blank(),
         legend.position      = c(0.99, 0.99),
         legend.justification = c('right','top'))m <- mmrm(AVAL ~ TRT01P * AVISIT + us(AVISIT | USUBJID), data = dat_twisk_l)
e <- emmeans(m, revpairwise ~ TRT01P | AVISIT, adjust = 'none') %>% 
   as.data.frame() %>% 
   filter(contrast != '.') %>% 
   data.frame()
f_a <- ggplot(data = dat_twisk_l %>% drop_na(AVAL),
              aes(x = AVISITN, y = AVAL, color = TRT01P)) +
   geom_quasirandom(dodge.width=1, alpha = 0.2, size = 2.5, show.legend = FALSE) +
   geom_boxplot(aes(group = interaction(AVISITN, TRT01P)),
                fill = 'transparent',
                outlier.shape = NA,
                show.legend = FALSE) +
   scale_colour_brewer(palette = "Set1",
                       name = 'Treatment') +
   stat_summary(fun = mean,
                geom = "point",
                size = 3,
                position = position_dodge(width = 1)) +
   scale_y_continuous(name = 'Systolic Blood Pressure (mmHg)',
                      limits = c(80, 200)) +
   scale_x_continuous(name = NULL,
                      breaks = c(0, 1, 2),
                      labels = c('Week 0 (R)', 'Week 12', 'Week 24')) +
   theme_bw(base_size = 14) + 
   theme(panel.grid.minor = element_blank(),
         legend.position      = c(0.99, 0.99),
         legend.justification = c('right','top'))
f_b <- ggplot(data = e,
              aes(x = AVISIT, y = emmean, group = contrast)) + 
   geom_hline(yintercept  = 0, color = 'gray50') +
   geom_point(size = 4) +
   geom_line(linewidth = 1) +
   geom_linerange(aes(ymin = lower.CL,
                      ymax = upper.CL),
                  size = 1.5) +
   geom_text(aes( y = -14.5,
                  label = gtsummary::style_pvalue(p.value, prepend_p = TRUE) ) ) +
   scale_color_discrete(name = 'Treatment') +
   scale_y_continuous(name = 'DIFF (95% CI)',
                      limits = c(-15, 2)) +
   scale_x_discrete(name = '') +
   theme_bw(base_size = 14) +
   theme(panel.grid.minor = element_blank(),
         axis.text.x=element_blank())
f_a / f_b + plot_layout( heights = c(2, 1))ggplot(data = dat_twisk_l %>% drop_na(AVAL),
       aes(x = AVISIT, y = AVAL, color = TRT01P , group = USUBJID )) +
   geom_quasirandom(dodge.width = 0.5, alpha = 0.5, size = 2.5) +
   geom_line(alpha = 0.5,
             position=position_quasirandom(dodge.width = 0.5)) +
   facet_wrap(. ~ TRT01P  ) +
   scale_colour_brewer(palette = "Set1",
                       name = 'Treatment') +
   scale_y_continuous(name = 'Systolic Blood Pressure (mmHg)',
                      limits = c(80, 200)) +
   labs(x = NULL) +
   theme_bw(base_size = 18) + 
   theme(panel.grid.minor = element_blank(),
         legend.position      = c(0.99, 0.99),
         legend.justification = c('right','top'))Trelliscope is an open source project that provides tools for data scientists to build and share interactive collections of visualizations for detailed data exploration and more.
pacman::p_load(trelliscope)
summary_df <- dat_twisk_l %>% 
   select(USUBJID, TRT01P, AVISITN, AVISIT, AVAL, CHG) %>%
   drop_na(AVAL) %>% 
   summarise(
      mean_AVAL = mean(AVAL),
      sd_AVAL = sd(AVAL),
      max_AVAL  = max(AVAL),
      mean_CHG  = mean(CHG),
      sd_CHG  = sd(CHG),
      max_CHG  = max(CHG),
      .by = c(USUBJID, TRT01P))
panels_df <- (
   ggplot(dat_twisk_l %>% drop_na(AVAL) %>% mutate(TRT01P = factor(TRT01P, levels = c('TRT', 'CNT'))),
          aes(x = AVISIT, y = AVAL, color = TRT01P, group = USUBJID) ) +
      geom_point(size = 4) +
      geom_line(linewidth = 1) +
      scale_colour_manual(values = c( 'TRT'  = '#377eb8',
                                      'CNT'  = '#e41a1c') ) +
      scale_y_continuous(name = 'Systolic Blood Pressure (mmHg)',
                         limits = c(80, 200)) +
      scale_x_discrete(name = NULL) +
      theme_bw(base_size = 14) +
      theme(legend.position = "none",
            panel.grid.minor = element_blank()) +
      facet_panels( vars(USUBJID, TRT01P), scales = 'same') ) %>% 
   as_panels_df()
trellis_df <- left_join(summary_df, 
                        panels_df, 
                        by = c("USUBJID", "TRT01P")) %>% 
   as_trelliscope_df(
      name = "Explore Individual Subject Data",
      path = './_trelliscope',
      description = "Effectiveness of a long-term homocysteine-lowering treatment 
      with folic acid plus pyridoxine in reducing systolic blood")%>%
   set_default_labels(c("USUBJID", "TRT01P")) %>% 
   set_default_layout(ncol = 3, page = 1) %>% 
   set_var_labels(
      mean_AVAL = 'Mean SBP') %>% 
   set_default_sort(c("sd_AVAL"), dirs = "desc") %>% 
   set_tags(
      stats = c("mean_AVAL","sd_AVAL","max_AVAL","mean_CHG","sd_CHG","max_CHG"),
      info  = c("USUBJID", "TRT01P")) %>% 
   view_trelliscope()
knitr::include_app('_trelliscope/index.html', height = "900px")pacman::p_load(scatterPlotMatrix)
d <- dat_twisk_w[,-c(1:2)] %>%
   relocate(TRT01P, .after = last_col()) %>% 
   na.omit()
scatterPlotMatrix(data = d,
                  zAxisDim = 'TRT01P',
                  categoricalCS = "Set1",
                  slidersPosition = list(
                     dimCount = 4,
                     xStartingDimIndex = 2,
                     yStartingDimIndex = 2 ),
                  columnLabels = c('W0', 'W12', 'W24','TRT'),
                  distribType = 1,
                  regressionType = 2,
                  plotProperties =  list( noCatColor= "#43665e", 
                                          watermarkColor = "#ddd", 
                                          point = list( alpha = 0.5, radius = 3 ),
                                          regression = list( strokeWidth = 1 ) ),
                  corrPlotType = "Text",
                  width = '1000px', height = '1000px')\[Y_{t} = \beta_0 + \beta_1X + \beta_2Y_{t0}\]
$emmeans
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       127 1.10 119      125      129 115.409  <.0001
 TRT       124 1.13 117      121      126 109.409  <.0001
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 
$contrasts
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -3.69 1.59 118    -6.83   -0.548  -2.325  0.0218
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 | AVAL | TRT01P | BASE | USUBJID | AVISIT | |
|---|---|---|---|---|---|
| 1 | 129 | TRT | 124 | S-00002 | Week 12 | 
| 2 | 126 | TRT | 124 | S-00002 | Week 24 | 
| 3 | 132 | CNT | 119 | S-00003 | Week 12 | 
| 4 | 114 | CNT | 119 | S-00003 | Week 24 | 
| 5 | 115 | CNT | 113 | S-00004 | Week 12 | 
| 6..248 | |||||
| 249 | 118 | TRT | 131 | S-00152 | Week 24 | 
| Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 49 | 6.91 | 7.02 | 35, 62 | <0.001 | 
| BASE | 0.61 | 0.052 | 11.7 | 0.51, 0.72 | <0.001 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -3.7 | 1.59 | -2.33 | -6.8, -0.55 | 0.022 | 
| AIC = 1,864; BIC = 1,873; Log-likelihood = -929; Deviance = 1,858 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
where AVISITN ^= 0;
class USUBJID AVISIT (ref = first) TRT01P (ref = first);
model AVAL = TRT01P BASE / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans TRT01P / diff cl;
run;\[Y_{t} = \beta_0 + \beta_1X + \beta_2Y_{t0} + \beta_3time + \beta_4X\times time\]
$emmeans
AVISIT = Week 12:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       129 1.30 127      126      131  98.565  <.0001
 TRT       124 1.34 127      121      127  92.235  <.0001
AVISIT = Week 24:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       126 1.35 115      123      128  93.294  <.0001
 TRT       123 1.36 114      120      126  90.192  <.0001
Confidence level used: 0.95 
$contrasts
AVISIT = Week 12:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -4.58 1.88 128    -8.30   -0.858  -2.434  0.0163
AVISIT = Week 24:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -2.68 1.92 115    -6.49    1.132  -1.393  0.1664
Confidence level used: 0.95 | AVAL | TRT01P | BASE | AVISIT | USUBJID | |
|---|---|---|---|---|---|
| 1 | 129 | TRT | 124 | Week 12 | S-00002 | 
| 2 | 126 | TRT | 124 | Week 24 | S-00002 | 
| 3 | 132 | CNT | 119 | Week 12 | S-00003 | 
| 4 | 114 | CNT | 119 | Week 24 | S-00003 | 
| 5 | 115 | CNT | 113 | Week 12 | S-00004 | 
| 6..248 | |||||
| 249 | 118 | TRT | 131 | Week 24 | S-00152 | 
| Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 50 | 6.98 | 7.17 | 36, 64 | <0.001 | 
| AVISIT | |||||
| Week 12 | — | — | — | — | |
| Week 12 | — | — | — | — | |
| Week 24 | -2.9 | 1.46 | -1.97 | -5.8, 0.02 | 0.052 | 
| BASE | 0.61 | 0.052 | 11.7 | 0.51, 0.72 | <0.001 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -4.6 | 1.88 | -2.43 | -8.3, -0.86 | 0.016 | 
| TRT01P * AVISIT | |||||
| TRT * Week 24 | 1.9 | 2.08 | 0.914 | -2.2, 6.0 | 0.36 | 
| AIC = 1,855; BIC = 1,863; Log-likelihood = -924; Deviance = 1,849 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
where AVISITN ^= 0;
class USUBJID AVISIT (ref = first) TRT01P (ref = first);
model AVAL = TRT01P BASE AVISIT AVISIT*TRT01P/ solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans AVISIT*TRT01P / slicediff=AVISIT diff cl;
run;\[Y_{t} = \beta_0 + \beta_1X + \beta_2time + \beta_3time\times X\]
$emmeans
ABLFL = N:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       128 1.55 133      125      132  82.619  <.0001
 TRT       122 1.59 132      119      126  77.020  <.0001
ABLFL = Y:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       130 1.80 137      127      134  72.239  <.0001
 TRT       126 1.84 137      123      130  68.592  <.0001
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 
$contrasts
ABLFL = N:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -6.07 2.22 133   -10.47    -1.68  -2.732  0.0072
ABLFL = Y:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -3.89 2.58 137    -8.99     1.21  -1.508  0.1338
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 | Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 128 | 1.55 | 82.6 | 125, 132 | <0.001 | 
| ABLFL | |||||
| N | — | — | — | — | |
| Y | 1.8 | 1.30 | 1.42 | -0.73, 4.4 | 0.16 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -6.1 | 2.22 | -2.73 | -10, -1.7 | 0.007 | 
| TRT01P * ABLFL | |||||
| TRT * Y | 2.2 | 1.86 | 1.17 | -1.5, 5.9 | 0.24 | 
| AIC = 3,009; BIC = 3,027; Log-likelihood = -1,498; Deviance = 2,997 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
class USUBJID AVISIT (ref = first) ABLFL (ref = first) TRT01P (ref = first);
model AVAL =  TRT01P ABLFL ABLFL*TRT01P  / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans TRT01P*ABLFL / slicediff=ABLFL diff cl;
run;\[Y_{t} = \beta_0 + \beta_1X + \beta_2dummy\_time_1 + \beta_3dummy\_time_2 +\\ \beta_4dumm\_time_1\times X + \beta_5dummy\_time_2 \times X\]
$emmeans
AVISIT = Week 0:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       131 1.81 137      127      134  72.003  <.0001
 TRT       127 1.85 137      123      130  68.229  <.0001
AVISIT = Week 12:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       130 1.76 134      127      134  74.083  <.0001
 TRT       123 1.80 135      119      127  68.204  <.0001
AVISIT = Week 24:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT       127 1.68 129      124      130  75.574  <.0001
 TRT       122 1.71 127      119      125  71.349  <.0001
Confidence level used: 0.95 
$contrasts
AVISIT = Week 0:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -4.15 2.59 137    -9.28    0.984  -1.598  0.1123
AVISIT = Week 12:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -7.09 2.52 134   -12.07   -2.117  -2.819  0.0055
AVISIT = Week 24:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -5.23 2.40 128    -9.97   -0.482  -2.180  0.0311
Confidence level used: 0.95 | Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 131 | 1.81 | 72.0 | 127, 134 | <0.001 | 
| AVISIT | |||||
| Week 0 | — | — | — | — | |
| Week 12 | -0.63 | 1.44 | -0.441 | -3.5, 2.2 | 0.66 | 
| Week 24 | -3.5 | 1.56 | -2.27 | -6.6, -0.45 | 0.025 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -4.1 | 2.59 | -1.60 | -9.3, 0.98 | 0.11 | 
| TRT01P * AVISIT | |||||
| TRT * Week 12 | -2.9 | 2.06 | -1.43 | -7.0, 1.1 | 0.16 | 
| TRT * Week 24 | -1.1 | 2.22 | -0.486 | -5.5, 3.3 | 0.63 | 
| AIC = 2,999; BIC = 3,017; Log-likelihood = -1,494; Deviance = 2,987 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
class USUBJID AVISIT (ref = first) TRT01P (ref = first);
model AVAL =  TRT01P AVISIT AVISIT*TRT01P  / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans TRT01P*AVISIT / slicediff=AVISIT diff cl;
run;\[Y_{t} = \beta_0 + \beta_1time + \beta_2time\times X\]
$emmeans
ABLFL = 1:
 TRT01P emmean SE df asymp.LCL asymp.UCL z.ratio p.value
 CNT    nonEst NA NA        NA        NA      NA      NA
 TRT    nonEst NA NA        NA        NA      NA      NA
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 
$contrasts
ABLFL = 1:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 CNT - TRT     -3.7 1.57 118    -6.81    -0.59  -2.356  0.0201
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 | AVAL | ABLFL | USUBJID | AVISIT | TRT01P | |
|---|---|---|---|---|---|
| 1 | 124 | 0 | S-00002 | Week 0 | TRT | 
| 2 | 129 | -1 | S-00002 | Week 12 | TRT | 
| 3 | 126 | -1 | S-00002 | Week 24 | TRT | 
| 4 | 119 | 0 | S-00003 | Week 0 | CNT | 
| 5 | 132 | -1 | S-00003 | Week 12 | CNT | 
| 6..387 | |||||
| 388 | 118 | -1 | S-00152 | Week 24 | TRT | 
| Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 128 | 1.29 | 99.1 | 126, 131 | <0.001 | 
| ABLFL | 1.1 | 1.21 | 0.905 | -1.3, 3.5 | 0.37 | 
| ABLFL * TRT01P | |||||
| ABLFL * TRT | 3.7 | 1.57 | 2.36 | 0.59, 6.8 | 0.020 | 
| AIC = 3,015; BIC = 3,033; Log-likelihood = -1,501; Deviance = 3,003 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
class USUBJID TRT01P (ref = first);
model AVAL =  ABLFLN ABLFLN*TRT01P  / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
run;\[Y_{t} = \beta_0 + \beta_1dummytime_1 + \beta_2dummytime_2 + \\ \beta_3dummytime_1 \times X + \beta_4dummytime_2 \times X\]
m2d <- mmrm(AVAL ~ I(AVISIT == "Week 12") + I(AVISIT == "Week 24") + 
               I(AVISIT == "Week 12" & TRT01P ==  "TRT") + I(AVISIT == "Week 24" & TRT01P == "TRT") + 
               us(AVISIT|USUBJID),     
            data = dat_twisk_l )
summary(m2d)$coefficient %>% 
   as.data.frame() %>% 
   rownames_to_column() %>% 
   select(-df) %>% 
   gt(rowname_col = "rownames") %>% 
   fmt_number(columns = 2:4, decimals = 2) %>% 
   fmt(columns = 5, 
       fns = function(x) gtsummary::style_pvalue(x, digits = 2)) %>% 
   cols_align_decimal() %>% 
   cols_label(rowname = '')| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 128.65 | 1.30 | 98.63 | <0.001 | 
| I(AVISIT == "Week 12")TRUE | 0.07 | 1.37 | 0.05 | 0.96 | 
| I(AVISIT == "Week 24")TRUE | −2.64 | 1.46 | −1.81 | 0.072 | 
| I(AVISIT == "Week 12" & TRT01P == "TRT")TRUE | −4.38 | 1.86 | −2.36 | 0.020 | 
| I(AVISIT == "Week 24" & TRT01P == "TRT")TRUE | −2.90 | 1.91 | −1.52 | 0.13 | 
| AVAL | I(AVISIT == "Week 12") | I(AVISIT == "Week 24") | I(AVISIT == "Week 12" & TRT01P == "TRT") | I(AVISIT == "Week 24" & TRT01P == "TRT") | USUBJID | AVISIT | |
|---|---|---|---|---|---|---|---|
| 1 | 124 | FALSE | FALSE | FALSE | FALSE | S-00002 | Week 0 | 
| 2 | 129 | TRUE | FALSE | TRUE | FALSE | S-00002 | Week 12 | 
| 3 | 126 | FALSE | TRUE | FALSE | TRUE | S-00002 | Week 24 | 
| 4 | 119 | FALSE | FALSE | FALSE | FALSE | S-00003 | Week 0 | 
| 5 | 132 | TRUE | FALSE | FALSE | FALSE | S-00003 | Week 12 | 
| 6..387 | |||||||
| 388 | 118 | FALSE | TRUE | FALSE | TRUE | S-00152 | Week 24 | 
| Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 129 | 1.30 | 98.6 | 126, 131 | <0.001 | 
| I(AVISIT == "Week 12" & TRT01P == "TRT") | |||||
| FALSE | — | — | — | — | |
| TRUE | -4.4 | 1.86 | -2.36 | -8.1, -0.71 | 0.020 | 
| I(AVISIT == "Week 12") | |||||
| FALSE | — | — | — | — | |
| TRUE | 0.07 | 1.37 | 0.049 | -2.6, 2.8 | 0.96 | 
| I(AVISIT == "Week 24" & TRT01P == "TRT") | |||||
| FALSE | — | — | — | — | |
| TRUE | -2.9 | 1.91 | -1.52 | -6.7, 0.87 | 0.13 | 
| I(AVISIT == "Week 24") | |||||
| FALSE | — | — | — | — | |
| TRUE | -2.6 | 1.46 | -1.81 | -5.5, 0.24 | 0.072 | 
| AIC = 3,006; BIC = 3,023; Log-likelihood = -1,497; Deviance = 2,994 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
class USUBJID TRT01P (ref = first);
model AVAL = Week_12 Week_24 Week_12*TRT01P Week_24*TRT01P / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
run;\[Y_{t} - Y_{t0} = \beta_0 + \beta_1X\]
$emmeans
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT     -1.85 1.31 120    -4.44     0.74  -1.415  0.1598
 TRT     -3.81 1.34 118    -6.47    -1.15  -2.835  0.0054
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 
$contrasts
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -1.96 1.88 119    -5.67     1.75  -1.044  0.2984
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 | Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | -1.9 | 1.31 | -1.41 | -4.4, 0.74 | 0.16 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -2.0 | 1.88 | -1.04 | -5.7, 1.8 | 0.30 | 
| AIC = 1,906; BIC = 1,914; Log-likelihood = -950; Deviance = 1,900 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
where AVISITN ^= 0;
class USUBJID TRT01P (ref = first);
model CHG = TRT01P / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans TRT01P / diff cl;
run;\[Y_{t} - Y_{t0} = \beta_0 + \beta_1X + \beta_2 time + \beta_3X \times time\]
$emmeans
AVISIT = Week 12:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT    -0.642 1.44 128    -3.50    2.216  -0.444  0.6575
 TRT    -3.381 1.49 128    -6.33   -0.434  -2.270  0.0249
AVISIT = Week 24:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT    -3.563 1.57 112    -6.68   -0.447  -2.265  0.0254
 TRT    -4.379 1.60 110    -7.54   -1.216  -2.744  0.0071
Confidence level used: 0.95 
$contrasts
AVISIT = Week 12:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT   -2.739 2.07 128    -6.84     1.37  -1.320  0.1891
AVISIT = Week 24:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT   -0.817 2.24 111    -5.26     3.62  -0.364  0.7162
Confidence level used: 0.95 | Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | -0.64 | 1.44 | -0.444 | -3.5, 2.2 | 0.66 | 
| AVISIT | |||||
| Week 12 | — | — | — | — | |
| Week 12 | — | — | — | — | |
| Week 24 | -2.9 | 1.46 | -1.99 | -5.8, -0.02 | 0.048 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -2.7 | 2.07 | -1.32 | -6.8, 1.4 | 0.19 | 
| TRT01P * AVISIT | |||||
| TRT * Week 24 | 1.9 | 2.08 | 0.923 | -2.2, 6.1 | 0.36 | 
| AIC = 1,896; BIC = 1,905; Log-likelihood = -945; Deviance = 1,890 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
where AVISITN ^= 0;
class USUBJID AVISIT (ref = first) TRT01P (ref = first);
model CHG = TRT01P AVISIT AVISIT*TRT01P / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans TRT01P*AVISIT / slicediff=AVISIT diff cl;
run;\[Y_{t} - Y_{t0} = \beta_0 + \beta_1X +\beta_2Y_{t0}\]
$emmeans
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT     -1.06 1.10 119    -3.24     1.12  -0.963  0.3373
 TRT     -4.75 1.13 117    -6.99    -2.52  -4.210  0.0001
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 
$contrasts
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -3.69 1.59 118    -6.83   -0.548  -2.325  0.0218
Results are averaged over the levels of: AVISIT 
Confidence level used: 0.95 | CHG | TRT01P | BASE | USUBJID | AVISIT | |
|---|---|---|---|---|---|
| 1 | 5 | TRT | 124 | S-00002 | Week 12 | 
| 2 | 2 | TRT | 124 | S-00002 | Week 24 | 
| 3 | 13 | CNT | 119 | S-00003 | Week 12 | 
| 4 | -5 | CNT | 119 | S-00003 | Week 24 | 
| 5 | 2 | CNT | 113 | S-00004 | Week 12 | 
| 6..248 | |||||
| 249 | -13 | TRT | 131 | S-00152 | Week 24 | 
| Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 49 | 6.91 | 7.02 | 35, 62 | <0.001 | 
| BASE | -0.39 | 0.052 | -7.40 | -0.49, -0.28 | <0.001 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -3.7 | 1.59 | -2.33 | -6.8, -0.55 | 0.022 | 
| AIC = 1,864; BIC = 1,873; Log-likelihood = -929; Deviance = 1,858 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
where AVISITN ^= 0;
class USUBJID TRT01P (ref = first);
model CHG = TRT01P BASE / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans TRT01P / diff cl;
run;\[Y_{t} - Y_{t0} = \beta_0 + \beta_1X + \beta_2 Y_{t0} + \beta_3X \times time\]
$emmeans
AVISIT = Week 12:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT      0.30 1.30 127    -2.28   2.8808   0.230  0.8187
 TRT     -4.28 1.34 127    -6.94  -1.6206  -3.184  0.0018
AVISIT = Week 24:
 TRT01P emmean   SE  df lower.CL upper.CL t.ratio p.value
 CNT     -2.57 1.35 115    -5.24   0.0969  -1.909  0.0588
 TRT     -5.25 1.36 114    -7.95  -2.5502  -3.851  0.0002
Confidence level used: 0.95 
$contrasts
AVISIT = Week 12:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -4.58 1.88 128    -8.30   -0.858  -2.434  0.0163
AVISIT = Week 24:
 contrast  estimate   SE  df lower.CL upper.CL t.ratio p.value
 TRT - CNT    -2.68 1.92 115    -6.49    1.132  -1.393  0.1664
Confidence level used: 0.95 | CHG | TRT01P | BASE | AVISIT | USUBJID | |
|---|---|---|---|---|---|
| 1 | 5 | TRT | 124 | Week 12 | S-00002 | 
| 2 | 2 | TRT | 124 | Week 24 | S-00002 | 
| 3 | 13 | CNT | 119 | Week 12 | S-00003 | 
| 4 | -5 | CNT | 119 | Week 24 | S-00003 | 
| 5 | 2 | CNT | 113 | Week 12 | S-00004 | 
| 6..248 | |||||
| 249 | -13 | TRT | 131 | Week 24 | S-00152 | 
| Characteristic | Beta | SE1 | Statistic | 95% CI1 | p-value | 
|---|---|---|---|---|---|
| (Intercept) | 50 | 6.98 | 7.17 | 36, 64 | <0.001 | 
| AVISIT | |||||
| Week 12 | — | — | — | — | |
| Week 12 | — | — | — | — | |
| Week 24 | -2.9 | 1.46 | -1.97 | -5.8, 0.02 | 0.052 | 
| BASE | -0.39 | 0.052 | -7.39 | -0.49, -0.28 | <0.001 | 
| TRT01P | |||||
| CNT | — | — | — | — | |
| TRT | -4.6 | 1.88 | -2.43 | -8.3, -0.86 | 0.016 | 
| TRT01P * AVISIT | |||||
| TRT * Week 24 | 1.9 | 2.08 | 0.914 | -2.2, 6.0 | 0.36 | 
| AIC = 1,855; BIC = 1,863; Log-likelihood = -924; Deviance = 1,849 | |||||
| 1 SE = Standard Error, CI = Confidence Interval | |||||
proc glimmix data=dat_twisk_l noclprint = 10;
where AVISITN ^= 0;
class USUBJID AVISIT (ref = first) TRT01P (ref = first);
model CHG = TRT01P BASE AVISIT AVISIT*TRT01P / solution ddfm=satterthwaite;
random _residual_ / subject=USUBJID type=un;
lsmeans TRT01P*AVISIT / slicediff=AVISIT diff cl;
run;| Beta | SE | Statistic | 95% CI | p-value | model | |
|---|---|---|---|---|---|---|
| (1a) Longitudinal analysis of covariance | −3.7 | 1.59 | −2.33 | (−6.8, −0.5) | 0.022 | AVAL ~ TRT01P + BASE + us(AVISIT | USUBJID) | 
| (2a) Repeated measures analysis | −6.1 | 2.22 | −2.73 | (−10.5, −1.7) | 0.007 | AVAL ~ TRT01P + ABLFL + ABLFL:TRT01P + us(AVISIT | USUBJID) | 
| (2c) Repeated measures wo/ treatment | −3.7 | 1.57 | −2.36 | (−6.8, −0.6) | 0.020 | AVAL ~ ABLFL + ABLFL:TRT01P + us(AVISIT | USUBJID) | 
| (3a) Analysis of changes (not adjusted) | −2.0 | 1.88 | −1.04 | (−5.7, 1.8) | 0.30 | CHG ~ TRT01P + us(AVISIT | USUBJID) | 
| (3c) Analysis of changes (adjusted) | −3.7 | 1.59 | −2.33 | (−6.8, −0.5) | 0.022 | CHG ~ TRT01P + BASE + us(AVISIT | USUBJID) | 
| (1a) Longitudinal analysis of covariance | (3c) Analysis of changes (adjusted) | 
|---|---|
| \[Y_{t} = \beta_0 + \beta_1X + \beta_2Y_{t0}\] | \[Y_{t} - Y_{t0} = \beta_0 + \beta_1X +\beta_2Y_{t0}\] | 
| \[Y_{t} = \beta_0 + \beta_1X +\beta_2Y_{t0} + Y_{t0}\] | |
| \[Y_{t} = \beta_0 + \beta_1X + (1 + \beta_2) Y_{t0}\] | |
| \[Y_{t} = 49 - 3.7X + 0.6Y_{t0}\] | \[Y_{t} = 49 - 3.7X - 0.4Y_{t0}\] | 
sim_1 <- import('mod/sim/sim_1.rds')
sim_w <- sim_1 %>% 
   select(-BASE, -CHG) %>% 
   pivot_wider(names_from = c(MISS, AVISIT),
               values_from = c(AVAL) )
add_by_n <- function(data, variable, by, tbl, ...) {
   data %>%
      select(all_of(c(variable, by))) %>%
      dplyr::group_by(.data[[by]]) %>%
      dplyr::summarise_all(~sum(!is.na(.))) %>%
      rlang::set_names(c("by", "variable")) %>%
      dplyr::left_join(
         tbl$df_by %>% select(by, by_col),
         by = "by"
      ) %>%
      mutate(
         by_col = paste0("add_n_", by_col),
         variable = style_number(variable)
      ) %>%
      select(-by) %>%
      tidyr::pivot_wider(names_from = by_col, 
                         values_from = variable)
}
tbl_summary(data = sim_w %>% mutate(TRT01P = fct_rev(TRT01P)),
            by = TRT01P , 
            include = starts_with('COMP_'),
            digit = list(everything() ~ c(1,1)),
            statistic = list(all_continuous() ~ "{mean} ({sd})"),
            missing = 'no',
            label = list( `COMP_Week 0` ~ 'Week 0 (Randomization)',
                          `COMP_Week 12` ~ 'Week 12',
                          `COMP_Week 24` ~ 'Week 24')) %>% 
   add_stat( fns = everything() ~ add_by_n ) %>%
   add_difference(test = list(all_continuous() ~ "t.test"),
                  pvalue_fun = ~style_pvalue(.x, digits = 2),
                  estimate_fun = list(all_continuous() ~ function(x) style_number(x, digits=1)) ) %>% 
   bold_labels() %>% 
   modify_column_merge(pattern = "{estimate} ({ci})") %>% 
   modify_header(
      label = '',
      estimate = '**DIFF (95% CI)**',
      all_stat_cols(FALSE) ~ "**{level}**",
      starts_with("add_n_stat") ~ "**n**") %>% 
   modify_table_body(
      ~ .x %>%
         dplyr::relocate(add_n_stat_1, .before = stat_1) %>%
         dplyr::relocate(add_n_stat_2, .before = stat_2)
   ) %>% 
   modify_spanning_header(
      c(all_stat_cols(F), starts_with("add_n_stat")) ~ "**Treatment**") %>% 
   as_gt() %>% 
   gtsave('tbl/sim-tbl_comp.png', zoom = 3)
knitr::include_graphics('tbl/sim-tbl_comp.png')tbl_summary(data = sim_w %>% mutate(TRT01P = fct_rev(TRT01P)),
            by = TRT01P , 
            include = starts_with('MISS_'),
            digit = list(everything() ~ c(1,1)),
            statistic = list(all_continuous() ~ "{mean} ({sd})"),
            missing = 'no',
            label = list( `MISS_Week 0` ~ 'Week 0 (Randomization)',
                          `MISS_Week 12` ~ 'Week 12',
                          `MISS_Week 24` ~ 'Week 24')) %>% 
   add_stat( fns = everything() ~ add_by_n ) %>%
   add_difference(test = list(all_continuous() ~ "t.test"),
                  pvalue_fun = ~style_pvalue(.x, digits = 2),
                  estimate_fun = list(all_continuous() ~ function(x) style_number(x, digits=1)) ) %>% 
   bold_labels() %>% 
   modify_column_merge(pattern = "{estimate} ({ci})") %>% 
   modify_header(
      label = '',
      estimate = '**DIFF (95% CI)**',
      all_stat_cols(FALSE) ~ "**{level}**",
      starts_with("add_n_stat") ~ "**n**") %>% 
   modify_table_body(
      ~ .x %>%
         dplyr::relocate(add_n_stat_1, .before = stat_1) %>%
         dplyr::relocate(add_n_stat_2, .before = stat_2)
   ) %>% 
   modify_spanning_header(
      c(all_stat_cols(F), starts_with("add_n_stat")) ~ "**Treatment**") %>% 
   as_gt() %>% 
   gtsave('tbl/sim-tbl_miss.png', zoom = 3)
knitr::include_graphics('tbl/sim-tbl_miss.png')pacman::p_load(ggdist)
sim_l <- import('mod/sim/sim_l.rds')
sim_p <- sim_l %>% 
   group_by(model, MISS) %>% 
   summarise(power = sum(I(statistic< -1.98)) / n() )
sim_g <- sim_l %>% 
   group_by(model, MISS) %>% 
   summarise(statistic = mean(statistic) )
sim_l %>%
   ggplot(aes(y = model, x = statistic )) +
   geom_vline(xintercept  = -2, color = 'gray50') +
   stat_halfeye(adjust = 0.70, alpha = 0.50) +
   facet_wrap(~ MISS,
              labeller = as_labeller(c(COMP='Completed Data', MISS='MCAR (25%)')) ) +
   scale_y_discrete(limits = rev,
                    name = '') +
   geom_text(data = sim_p,
             vjust = -1.0,
             aes(x = 0.85, label = style_percent(power, digits = 0, symbol = TRUE) ) ) +
   geom_text(data = sim_g,
             vjust = -1.0,
             aes(x = statistic, label = style_number(statistic, digits = 1) ) ) +
   theme_bw(base_size = 18) +
   theme(panel.grid.minor = element_blank(),
         axis.text.y = element_text(face = 'bold',size = 10))