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.
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)
brm_simulate(
raw_data <-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.
brm_data(
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"
attributes(data)
roles <-$row.names <- NULL
rolesstr(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
.
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.
brm_formula(
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
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.
::get_prior(data = data, formula = formula)
brms#> 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)
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.
brm_model(data = data, formula = formula, refresh = 0) model <-
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).
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:
role
to "change"
in brm_data()
.To derive posterior draws of these marginals, use the brm_marginal_draws()
function.
brm_marginal_draws(
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.
brm_marginal_summaries(draws, level = 0.95)
summaries <-
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.
brm_marginal_data(data = data, level = 0.95)
summaries_data <-
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
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)