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)
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 rowsNext, 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.
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 + timeFor 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 + timeSome 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)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).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.
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 rowsThe 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.654Finally, 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 rowsThe 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)