Usage

A mixed model of repeated measures (MMRM) analyzes longitudinal clinical trial data. In a longitudinal dataset, there are multiple patients, and each patient has multiple observations at a common set of discrete points in time.

1 Data

To use the brms.mmrm package, begin with a longitudinal dataset with one row per patient observation and columns for the response variable, treatment group indicator, discrete time point indicator, patient ID variable, and optional baseline covariates such as age and region. If you do not have a real dataset of your own, you can simulate one from the package. The following dataset has the raw response variable and only the most essential factor variables. In general, the outcome variable can either be the raw response or change from baseline.

library(brms.mmrm)
library(dplyr)
set.seed(0L)
raw_data <- brm_simulate(
  n_group = 3,
  n_patient = 100,
  n_time = 4
)$data

raw_data
#> # A tibble: 1,200 × 4
#>    response group   patient   time  
#>       <dbl> <chr>   <chr>     <chr> 
#>  1    1.03  group 1 patient 1 time 1
#>  2    3.15  group 1 patient 1 time 2
#>  3    1.74  group 1 patient 1 time 3
#>  4   -0.173 group 1 patient 1 time 4
#>  5    1.40  group 1 patient 2 time 1
#>  6    2.24  group 1 patient 2 time 2
#>  7    1.75  group 1 patient 2 time 3
#>  8   -0.212 group 1 patient 2 time 4
#>  9    1.14  group 1 patient 3 time 1
#> 10    2.27  group 1 patient 3 time 2
#> # ℹ 1,190 more rows

Next, create a special classed dataset that the package will recognize. The classed data object contains a pre-processed version of the data, along with attributes to declare the outcome variable, whether the outcome is response or change from baseline, the treatment group variable, the discrete time point variable, and other details.

data <- brm_data(
  data = raw_data,
  outcome = "response",
  role = "response",
  group = "group",
  patient = "patient",
  time = "time"
)

data
#> # A tibble: 1,200 × 4
#>    response group   time   patient    
#>       <dbl> <chr>   <chr>  <chr>      
#>  1    1.03  group.1 time.1 patient 1  
#>  2    3.15  group.1 time.2 patient 1  
#>  3    1.74  group.1 time.3 patient 1  
#>  4   -0.173 group.1 time.4 patient 1  
#>  5   -0.224 group.1 time.1 patient 10 
#>  6    2.36  group.1 time.2 patient 10 
#>  7    0.232 group.1 time.3 patient 10 
#>  8   -3.36  group.1 time.4 patient 10 
#>  9   -0.232 group.1 time.1 patient 100
#> 10    2.31  group.1 time.2 patient 100
#> # ℹ 1,190 more rows

class(data)
#> [1] "brm_data"   "tbl_df"     "tbl"        "data.frame"

roles <- attributes(data)
roles$row.names <- NULL
str(roles)
#> List of 12
#>  $ names           : chr [1:4] "response" "group" "time" "patient"
#>  $ class           : chr [1:4] "brm_data" "tbl_df" "tbl" "data.frame"
#>  $ brm_outcome     : chr "response"
#>  $ brm_role        : chr "response"
#>  $ brm_group       : chr "group"
#>  $ brm_time        : chr "time"
#>  $ brm_patient     : chr "patient"
#>  $ brm_covariates  : chr(0) 
#>  $ brm_levels_group: chr [1:3] "group.1" "group.2" "group.3"
#>  $ brm_levels_time : chr [1:4] "time.1" "time.2" "time.3" "time.4"
#>  $ brm_labels_group: chr [1:3] "group 1" "group 2" "group 3"
#>  $ brm_labels_time : chr [1:4] "time 1" "time 2" "time 3" "time 4"

Above, the levels of the group and time columns are automatically cleaned with make.names() to ensure alignment between the data and brms output. Whenever brms.mmrm calls make.names(), it always sets unique = FALSE and allow_ = TRUE.

2 Formula

Next, choose a brms model formula for the fixed effect and variance parameters. The brm_formula() function from brms.mmrm makes this process easier. A cell means parameterization for this particular model can be expressed as follows. It specifies one fixed effect parameter for each combination of treatment group and time point, and it makes the specification of informative priors straightforward through the prior argument of brm_model().

brm_formula(
  data = data,
  intercept = FALSE,
  effect_base = FALSE,
  effect_group = FALSE,
  effect_time = FALSE,
  interaction_base = FALSE,
  interaction_group = TRUE
)
#> response ~ 0 + group:time + unstr(time = time, gr = patient) 
#> sigma ~ 0 + time

For the purposes of our example, we choose a fully parameterized analysis of the raw response.

formula <- brm_formula(
  data = data,
  intercept = TRUE,
  effect_base = FALSE,
  effect_group = TRUE,
  effect_time = TRUE,
  interaction_base = FALSE,
  interaction_group = TRUE
)

formula
#> response ~ time + group + group:time + unstr(time = time, gr = patient) 
#> sigma ~ 0 + time

3 Priors

Some analyses require informative priors, others require non-informative ones. Please use brms to construct a prior suitable for your analysis. The brms package has documentation on how its default priors are constructed and how to set your own priors. Once you have an R object that represents the joint prior distribution of your model, you can pass it to the brm_model() function described below. The get_prior() function shows the default priors for a given dataset and model formula.

brms::get_prior(data = data, formula = formula)
#>                   prior     class                    coef group resp  dpar
#>  student_t(3, 1.1, 2.5) Intercept                                         
#>                  (flat)         b                                         
#>                  (flat)         b            groupgroup.2                 
#>                  (flat)         b            groupgroup.3                 
#>                  (flat)         b              timetime.2                 
#>                  (flat)         b timetime.2:groupgroup.2                 
#>                  (flat)         b timetime.2:groupgroup.3                 
#>                  (flat)         b              timetime.3                 
#>                  (flat)         b timetime.3:groupgroup.2                 
#>                  (flat)         b timetime.3:groupgroup.3                 
#>                  (flat)         b              timetime.4                 
#>                  (flat)         b timetime.4:groupgroup.2                 
#>                  (flat)         b timetime.4:groupgroup.3                 
#>                  lkj(1)   cortime                                         
#>                  (flat)         b                                    sigma
#>                  (flat)         b              timetime.1            sigma
#>                  (flat)         b              timetime.2            sigma
#>                  (flat)         b              timetime.3            sigma
#>                  (flat)         b              timetime.4            sigma
#>  nlpar lb ub       source
#>                   default
#>                   default
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>                   default
#>                   default
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)
#>              (vectorized)

4 Model

To run an MMRM, use the brm_model() function. This function calls brms::brm() behind the scenes, using the formula and prior you set in the formula and prior arguments.

model <- brm_model(data = data, formula = formula, refresh = 0)

The result is a brms model object.

model
#>  Family: gaussian 
#>   Links: mu = identity; sigma = log 
#> Formula: response ~ time + group + group:time + unstr(time = time, gr = patient) 
#>          sigma ~ 0 + time
#>    Data: data (Number of observations: 1200) 
#>   Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
#>          total post-warmup draws = 4000
#> 
#> Correlation Structures:
#>                        Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
#> cortime(time.1,time.2)     0.39      0.05     0.29     0.48 1.00     5103
#> cortime(time.1,time.3)     0.74      0.03     0.69     0.78 1.00     4466
#> cortime(time.2,time.3)     0.28      0.05     0.17     0.38 1.00     5440
#> cortime(time.1,time.4)     0.34      0.05     0.23     0.44 1.00     5982
#> cortime(time.2,time.4)     0.08      0.06    -0.03     0.19 1.00     4990
#> cortime(time.3,time.4)     0.25      0.06     0.14     0.36 1.00     5807
#>                        Tail_ESS
#> cortime(time.1,time.2)     3274
#> cortime(time.1,time.3)     3737
#> cortime(time.2,time.3)     3073
#> cortime(time.1,time.4)     3303
#> cortime(time.2,time.4)     3064
#> cortime(time.3,time.4)     2831
#> 
#> Population-Level Effects: 
#>                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
#> Intercept                  -0.15      0.05    -0.24    -0.05 1.00     4451
#> timetime.2                  1.24      0.07     1.10     1.39 1.00     3406
#> timetime.3                  0.43      0.04     0.34     0.51 1.00     3221
#> timetime.4                 -1.51      0.09    -1.68    -1.34 1.00     3711
#> groupgroup.2                1.29      0.07     1.17     1.42 1.00     4707
#> groupgroup.3                1.41      0.07     1.29     1.54 1.00     4634
#> timetime.2:groupgroup.2     0.02      0.10    -0.18     0.22 1.00     4073
#> timetime.3:groupgroup.2    -0.05      0.06    -0.16     0.07 1.00     4309
#> timetime.4:groupgroup.2     0.01      0.12    -0.23     0.24 1.00     3954
#> timetime.2:groupgroup.3    -0.04      0.10    -0.25     0.15 1.00     4069
#> timetime.3:groupgroup.3    -0.04      0.06    -0.15     0.07 1.00     3965
#> timetime.4:groupgroup.3     0.05      0.12    -0.19     0.29 1.00     3885
#> sigma_timetime.1           -0.80      0.04    -0.88    -0.73 1.00     4211
#> sigma_timetime.2           -0.25      0.04    -0.33    -0.16 1.00     4623
#> sigma_timetime.3           -0.54      0.04    -0.61    -0.46 1.00     4560
#> sigma_timetime.4           -0.11      0.04    -0.18    -0.02 1.00     5180
#>                         Tail_ESS
#> Intercept                   3336
#> timetime.2                  3023
#> timetime.3                  3236
#> timetime.4                  3074
#> groupgroup.2                3229
#> groupgroup.3                2904
#> timetime.2:groupgroup.2     3320
#> timetime.3:groupgroup.2     3430
#> timetime.4:groupgroup.2     2960
#> timetime.2:groupgroup.3     3164
#> timetime.3:groupgroup.3     3223
#> timetime.4:groupgroup.3     3332
#> sigma_timetime.1            3539
#> sigma_timetime.2            3358
#> sigma_timetime.3            3363
#> sigma_timetime.4            3427
#> 
#> Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
#> and Tail_ESS are effective sample size measures, and Rhat is the potential
#> scale reduction factor on split chains (at convergence, Rhat = 1).

5 Marginals

Regardless of the choice of fixed effects formula, brms.mmrm performs inference on the marginal distributions at each treatment group and time point of the mean of the following quantities:

  1. Response.
  2. Change from baseline, if you set role to "change" in brm_data().
  3. Treatment difference, in terms of change from baseline.
  4. Effect size: treatment difference divided by the residual standard deviation.

To derive posterior draws of these marginals, use the brm_marginal_draws() function.

draws <- brm_marginal_draws(
  model = model,
  data = data,
  control = "group 1", # automatically cleaned with make.names()
  baseline = "time 1" # also cleaned with make.names()
)

draws
#> $response
#> # A draws_df: 1000 iterations, 4 chains, and 12 variables
#>    group.1|time.1 group.2|time.1 group.3|time.1 group.1|time.2 group.2|time.2
#> 1          -0.133            1.1            1.3            1.1            2.4
#> 2          -0.126            1.2            1.3            1.1            2.4
#> 3          -0.150            1.2            1.2            1.1            2.3
#> 4          -0.120            1.2            1.2            1.2            2.5
#> 5          -0.121            1.1            1.3            1.1            2.5
#> 6          -0.194            1.1            1.3            1.0            2.4
#> 7          -0.117            1.1            1.2            1.3            2.4
#> 8          -0.049            1.1            1.3            1.3            2.4
#> 9          -0.095            1.1            1.3            1.2            2.5
#> 10         -0.165            1.2            1.3            1.3            2.3
#>    group.3|time.2 group.1|time.3 group.2|time.3
#> 1             2.3           0.24            1.4
#> 2             2.6           0.32            1.6
#> 3             2.5           0.26            1.6
#> 4             2.5           0.35            1.5
#> 5             2.5           0.34            1.5
#> 6             2.6           0.21            1.6
#> 7             2.4           0.28            1.4
#> 8             2.4           0.33            1.5
#> 9             2.5           0.29            1.5
#> 10            2.4           0.25            1.6
#> # ... with 3990 more draws, and 4 more variables
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}
#> 
#> $change
#> # A draws_df: 1000 iterations, 4 chains, and 9 variables
#>    group.1|time.2 group.1|time.3 group.1|time.4 group.2|time.2 group.2|time.3
#> 1             1.2           0.38           -1.5            1.3           0.33
#> 2             1.2           0.45           -1.6            1.2           0.34
#> 3             1.2           0.40           -1.4            1.1           0.44
#> 4             1.3           0.47           -1.7            1.3           0.38
#> 5             1.3           0.46           -1.5            1.3           0.33
#> 6             1.2           0.41           -1.5            1.2           0.42
#> 7             1.4           0.40           -1.4            1.4           0.35
#> 8             1.3           0.38           -1.4            1.2           0.36
#> 9             1.3           0.38           -1.5            1.4           0.37
#> 10            1.4           0.42           -1.5            1.1           0.42
#>    group.2|time.4 group.3|time.2 group.3|time.3
#> 1            -1.5            1.1           0.35
#> 2            -1.5            1.3           0.34
#> 3            -1.6            1.2           0.41
#> 4            -1.6            1.3           0.37
#> 5            -1.4            1.2           0.38
#> 6            -1.6            1.3           0.39
#> 7            -1.4            1.2           0.40
#> 8            -1.5            1.1           0.42
#> 9            -1.5            1.2           0.41
#> 10           -1.5            1.2           0.35
#> # ... with 3990 more draws, and 1 more variables
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}
#> 
#> $difference
#> # A draws_df: 1000 iterations, 4 chains, and 6 variables
#>    group.2|time.2 group.2|time.3 group.2|time.4 group.3|time.2 group.3|time.3
#> 1           0.057        -0.0436        -0.0172       -0.14395        -0.0216
#> 2          -0.023        -0.1025         0.1381        0.04942        -0.1066
#> 3          -0.099         0.0349        -0.1632        0.00031         0.0029
#> 4           0.019        -0.0918         0.0717       -0.01002        -0.1083
#> 5           0.060        -0.1264         0.1523       -0.05260        -0.0823
#> 6           0.012         0.0092        -0.0710        0.08494        -0.0182
#> 7          -0.032        -0.0491         0.0456       -0.17976         0.0042
#> 8          -0.093        -0.0226        -0.0118       -0.20878         0.0357
#> 9           0.074        -0.0131        -0.0031       -0.08358         0.0278
#> 10         -0.287        -0.0016         0.0295       -0.28053        -0.0691
#>    group.3|time.4
#> 1           0.038
#> 2           0.181
#> 3          -0.092
#> 4           0.112
#> 5           0.080
#> 6           0.162
#> 7          -0.048
#> 8           0.053
#> 9          -0.011
#> 10          0.065
#> # ... with 3990 more draws
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}
#> 
#> $effect
#> # A draws_df: 1000 iterations, 4 chains, and 6 variables
#>    group.2|time.2 group.2|time.3 group.2|time.4 group.3|time.2 group.3|time.3
#> 1           0.069        -0.0807        -0.0186       -0.17425        -0.0399
#> 2          -0.032        -0.1843         0.1572        0.06791        -0.1917
#> 3          -0.133         0.0588        -0.1799        0.00042         0.0049
#> 4           0.025        -0.1608         0.0772       -0.01346        -0.1897
#> 5           0.077        -0.2318         0.1634       -0.06787        -0.1508
#> 6           0.016         0.0153        -0.0842        0.11189        -0.0304
#> 7          -0.040        -0.0892         0.0478       -0.22122         0.0077
#> 8          -0.119        -0.0392        -0.0128       -0.26933         0.0617
#> 9           0.094        -0.0218        -0.0033       -0.10621         0.0464
#> 10         -0.349        -0.0027         0.0338       -0.34090        -0.1191
#>    group.3|time.4
#> 1           0.041
#> 2           0.206
#> 3          -0.101
#> 4           0.120
#> 5           0.086
#> 6           0.192
#> 7          -0.050
#> 8           0.058
#> 9          -0.012
#> 10          0.074
#> # ... with 3990 more draws
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}

If you need samples from these marginals averaged across time points, e.g. an “overall effect size”, brm_marginal_draws_average() can average the draws above across discrete time points (either all or a user-defined subset).

brm_marginal_draws_average(draws = draws, data = data)
#> $response
#> # A draws_df: 1000 iterations, 4 chains, and 3 variables
#>    group.1|average group.2|average group.3|average
#> 1           -0.119             1.1             1.3
#> 2           -0.115             1.2             1.3
#> 3           -0.093             1.2             1.3
#> 4           -0.094             1.2             1.2
#> 5           -0.075             1.2             1.3
#> 6           -0.161             1.2             1.4
#> 7           -0.025             1.2             1.3
#> 8            0.018             1.2             1.3
#> 9           -0.065             1.2             1.3
#> 10          -0.078             1.2             1.3
#> # ... with 3990 more draws
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}
#> 
#> $change
#> # A draws_df: 1000 iterations, 4 chains, and 3 variables
#>    group.1|average group.2|average group.3|average
#> 1            0.019         1.8e-02          -0.023
#> 2            0.015         1.9e-02           0.056
#> 3            0.076        -4.2e-05           0.046
#> 4            0.035         3.5e-02           0.033
#> 5            0.062         9.0e-02           0.043
#> 6            0.044         2.7e-02           0.120
#> 7            0.123         1.1e-01           0.049
#> 8            0.090         4.7e-02           0.050
#> 9            0.040         5.9e-02           0.018
#> 10           0.116         2.9e-02           0.021
#> # ... with 3990 more draws
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}
#> 
#> $difference
#> # A draws_df: 1000 iterations, 4 chains, and 2 variables
#>    group.2|average group.3|average
#> 1         -0.00134         -0.0425
#> 2          0.00417          0.0412
#> 3         -0.07561         -0.0294
#> 4         -0.00045         -0.0022
#> 5          0.02846         -0.0183
#> 6         -0.01666          0.0762
#> 7         -0.01193         -0.0744
#> 8         -0.04233         -0.0400
#> 9          0.01920         -0.0223
#> 10        -0.08641         -0.0949
#> # ... with 3990 more draws
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}
#> 
#> $effect
#> # A draws_df: 1000 iterations, 4 chains, and 2 variables
#>    group.2|average group.3|average
#> 1          -0.0102          -0.058
#> 2          -0.0196           0.027
#> 3          -0.0847          -0.032
#> 4          -0.0195          -0.028
#> 5           0.0028          -0.044
#> 6          -0.0177           0.091
#> 7          -0.0270          -0.088
#> 8          -0.0571          -0.050
#> 9           0.0229          -0.024
#> 10         -0.1060          -0.129
#> # ... with 3990 more draws
#> # ... hidden reserved variables {'.chain', '.iteration', '.draw'}

The brm_marginal_summaries() function produces posterior summaries of these marginals, and it includes the Monte Carlo standard error (MCSE) of each estimate.

summaries <- brm_marginal_summaries(draws, level = 0.95)

summaries
#> # A tibble: 165 × 6
#>    marginal statistic group   time    value    mcse
#>    <chr>    <chr>     <chr>   <chr>   <dbl>   <dbl>
#>  1 change   lower     group.1 time.2  1.10  0.00388
#>  2 change   lower     group.1 time.3  0.345 0.00151
#>  3 change   lower     group.1 time.4 -1.68  0.00356
#>  4 change   lower     group.2 time.2  1.12  0.00357
#>  5 change   lower     group.2 time.3  0.304 0.00203
#>  6 change   lower     group.2 time.4 -1.67  0.00356
#>  7 change   lower     group.3 time.2  1.06  0.00215
#>  8 change   lower     group.3 time.3  0.309 0.00130
#>  9 change   lower     group.3 time.4 -1.63  0.00403
#> 10 change   mean      group.1 time.2  1.24  0.00126
#> # ℹ 155 more rows

The brm_marginal_probabilities() function shows posterior probabilities of the form,

\[ \begin{aligned} \text{Prob}(\text{treatment effect} > \text{threshold}) \end{aligned} \]

or

\[ \begin{aligned} \text{Prob}(\text{treatment effect} < \text{threshold}) \end{aligned} \]

brm_marginal_probabilities(
  draws = draws,
  threshold = c(-0.1, 0.1),
  direction = c("greater", "less")
)
#> # A tibble: 12 × 5
#>    direction threshold group   time   value
#>    <chr>         <dbl> <chr>   <chr>  <dbl>
#>  1 greater        -0.1 group.2 time.2 0.879
#>  2 greater        -0.1 group.2 time.3 0.829
#>  3 greater        -0.1 group.2 time.4 0.824
#>  4 greater        -0.1 group.3 time.2 0.706
#>  5 greater        -0.1 group.3 time.3 0.856
#>  6 greater        -0.1 group.3 time.4 0.895
#>  7 less            0.1 group.2 time.2 0.780
#>  8 less            0.1 group.2 time.3 0.994
#>  9 less            0.1 group.2 time.4 0.772
#> 10 less            0.1 group.3 time.2 0.918
#> 11 less            0.1 group.3 time.3 0.992
#> 12 less            0.1 group.3 time.4 0.654

Finally, the brm_marignals_data() computes marginal means and confidence intervals on the response variable in the data, along with other summary statistics.

summaries_data <- brm_marginal_data(data = data, level = 0.95)

summaries_data
#> # A tibble: 84 × 4
#>    statistic group   time     value
#>    <chr>     <chr>   <chr>    <dbl>
#>  1 lower     group.1 time.1 -0.0475
#>  2 lower     group.1 time.2  1.25  
#>  3 lower     group.1 time.3  0.406 
#>  4 lower     group.1 time.4 -1.49  
#>  5 lower     group.2 time.1  1.26  
#>  6 lower     group.2 time.2  2.56  
#>  7 lower     group.2 time.3  1.66  
#>  8 lower     group.2 time.4 -0.163 
#>  9 lower     group.3 time.1  1.30  
#> 10 lower     group.3 time.2  2.62  
#> # ℹ 74 more rows

6 Visualization

The brm_plot_compare() function compares means and intervals from many different models and data sources in the same plot. First, we need the marginals of the data.

brm_plot_compare(
  data = summaries_data,
  model1 = summaries,
  model2 = summaries
)

If you omit the marginals of the data, you can show inference on change from baseline or the treatment effect.

brm_plot_compare(
  model1 = summaries,
  model2 = summaries,
  marginal = "difference" # treatment effect
)

Finally, brm_plot_draws() can plot the posterior draws of the response, change from baseline, or treatment difference.

brm_plot_draws(draws = draws$difference)