Multivariate model specification

I’m struggling a bit with the formula notation of a multivariate model in brms. I know how to add effects of an independent variable on each dependent variable, but what if I want to add also an overall effect of an independent variable.

For example, imagine 2 dependent variables (Score1 and Score2) & an independent variable x1 for which I assume that the slope may be different for Score1 & Score2. Then the code for the formula looks for instance like this:

brm(
  mvbind(Score1, Score2) ~ x1,
  data = ...
)

But what if I want to add another variable x2 for which I want to model only one slope over both dependent variables? Or put differently I have good reasons to assume that the effect of x2 on Score1 is similar to the effect of x2 on Score2 and I want a parsimonious model. Can this be specified within the brmsformula ?

Sorry if the answer is somewhere in the substantial documentation, but I can’t seem to find it.

Hey Sven! Sorry for the delayed answer! Welcome to the Stan forum!

I’m definitely not a brms expert, but I have asked a similar question a while ago. Not sure if it has been implemented yet, but I guess not since brms 3 is not out, @paul.buerkner ? If you are willing to work in Stan directly this is pretty easy to do – you could for example use brms::make_stancode and go from there.

Cheers,
Max

No problem Max! Thanks for the answer. At least now I know that it is probably not implemented in brms yet. It would be a great feature though, but I can imagine that @paul.buerkner has work enough ;-). Time for me to learn some more Stan code…

True.

It’s worth it :)

Btw, I highly recommend going with cmdstanr!

Cheers,
Max

1 Like

Yes, this will only be possible in brms 3.

1 Like

Dear Max (or anyone else). I didn’t had the opportunity earlier to take a next step in this. Last week I’ve been trying to figure it out how to do this in Stan code, but I’m stuck… Can’t work my head around it.

My model input looks like this in brms:

Model <- brm( 
    bf(Score_1 ~ 1 + Gender) +
    bf(Score_2 ~ 1 + Gender) +
    bf(Score_3 ~ 1 + Gender) +
  set_rescor(rescor = TRUE),
data = Data)

make_stancode() results in

> make_stancode(  bf(Score_1 ~ 1 + Gender) +
+                     bf(Score_2 ~ 1 + Gender) +
+                     bf(Score_3 ~ 1 + Gender) +
+                     set_rescor(rescor = TRUE),
+                 data = Data)
// generated with brms 2.15.0
functions {
}
data {
  int<lower=1> N;  // total number of observations
  int<lower=1> N_Score1;  // number of observations
  vector[N_Score1] Y_Score1;  // response variable
  int<lower=1> K_Score1;  // number of population-level effects
  matrix[N_Score1, K_Score1] X_Score1;  // population-level design matrix
  int<lower=1> N_Score2;  // number of observations
  vector[N_Score2] Y_Score2;  // response variable
  int<lower=1> K_Score2;  // number of population-level effects
  matrix[N_Score2, K_Score2] X_Score2;  // population-level design matrix
  int<lower=1> N_Score3;  // number of observations
  vector[N_Score3] Y_Score3;  // response variable
  int<lower=1> K_Score3;  // number of population-level effects
  matrix[N_Score3, K_Score3] X_Score3;  // population-level design matrix
  int<lower=1> nresp;  // number of responses
  int nrescor;  // number of residual correlations
  int prior_only;  // should the likelihood be ignored?
}
transformed data {
  int Kc_Score1 = K_Score1 - 1;
  matrix[N_Score1, Kc_Score1] Xc_Score1;  // centered version of X_Score1 without an intercept
  vector[Kc_Score1] means_X_Score1;  // column means of X_Score1 before centering
  int Kc_Score2 = K_Score2 - 1;
  matrix[N_Score2, Kc_Score2] Xc_Score2;  // centered version of X_Score2 without an intercept
  vector[Kc_Score2] means_X_Score2;  // column means of X_Score2 before centering
  int Kc_Score3 = K_Score3 - 1;
  matrix[N_Score3, Kc_Score3] Xc_Score3;  // centered version of X_Score3 without an intercept
  vector[Kc_Score3] means_X_Score3;  // column means of X_Score3 before centering
  vector[nresp] Y[N];  // response array
  for (i in 2:K_Score1) {
    means_X_Score1[i - 1] = mean(X_Score1[, i]);
    Xc_Score1[, i - 1] = X_Score1[, i] - means_X_Score1[i - 1];
  }
  for (i in 2:K_Score2) {
    means_X_Score2[i - 1] = mean(X_Score2[, i]);
    Xc_Score2[, i - 1] = X_Score2[, i] - means_X_Score2[i - 1];
  }
  for (i in 2:K_Score3) {
    means_X_Score3[i - 1] = mean(X_Score3[, i]);
    Xc_Score3[, i - 1] = X_Score3[, i] - means_X_Score3[i - 1];
  }
  for (n in 1:N) {
    Y[n] = transpose([Y_Score1[n], Y_Score2[n], Y_Score3[n]]);
  }
}
parameters {
  vector[Kc_Score1] b_Score1;  // population-level effects
  real Intercept_Score1;  // temporary intercept for centered predictors
  real<lower=0> sigma_Score1;  // residual SD
  vector[Kc_Score2] b_Score2;  // population-level effects
  real Intercept_Score2;  // temporary intercept for centered predictors
  real<lower=0> sigma_Score2;  // residual SD
  vector[Kc_Score3] b_Score3;  // population-level effects
  real Intercept_Score3;  // temporary intercept for centered predictors
  real<lower=0> sigma_Score3;  // residual SD
  cholesky_factor_corr[nresp] Lrescor;  // parameters for multivariate linear models
}
transformed parameters {
}
model {
  // likelihood including constants
  if (!prior_only) {
    // initialize linear predictor term
    vector[N_Score1] mu_Score1 = Intercept_Score1 + Xc_Score1 * b_Score1;
    // initialize linear predictor term
    vector[N_Score2] mu_Score2 = Intercept_Score2 + Xc_Score2 * b_Score2;
    // initialize linear predictor term
    vector[N_Score3] mu_Score3 = Intercept_Score3 + Xc_Score3 * b_Score3;
    // multivariate predictor array
    vector[nresp] Mu[N];
    vector[nresp] sigma = transpose([sigma_Score1, sigma_Score2, sigma_Score3]);
    // cholesky factor of residual covariance matrix
    matrix[nresp, nresp] LSigma = diag_pre_multiply(sigma, Lrescor);
    // combine univariate parameters
    for (n in 1:N) {
      Mu[n] = transpose([mu_Score1[n], mu_Score2[n], mu_Score3[n]]);
    }
    target += multi_normal_cholesky_lpdf(Y | Mu, LSigma);
  }
  // priors including constants
  target += student_t_lpdf(Intercept_Score1 | 3, 10, 3);
  target += student_t_lpdf(sigma_Score1 | 3, 0, 3)
    - 1 * student_t_lccdf(0 | 3, 0, 3);
  target += student_t_lpdf(Intercept_Score2 | 3, 11, 4.4);
  target += student_t_lpdf(sigma_Score2 | 3, 0, 4.4)
    - 1 * student_t_lccdf(0 | 3, 0, 4.4);
  target += student_t_lpdf(Intercept_Score3 | 3, 14, 4.4);
  target += student_t_lpdf(sigma_Score3 | 3, 0, 4.4)
    - 1 * student_t_lccdf(0 | 3, 0, 4.4);
  target += lkj_corr_cholesky_lpdf(Lrescor | 1);
}
generated quantities {
  // actual population-level intercept
  real b_Score1_Intercept = Intercept_Score1 - dot_product(means_X_Score1, b_Score1);
  // actual population-level intercept
  real b_Score2_Intercept = Intercept_Score2 - dot_product(means_X_Score2, b_Score2);
  // actual population-level intercept
  real b_Score3_Intercept = Intercept_Score3 - dot_product(means_X_Score3, b_Score3);
  // residual correlations
  corr_matrix[nresp] Rescor = multiply_lower_tri_self_transpose(Lrescor);
  vector<lower=-1,upper=1>[nrescor] rescor;
  // extract upper diagonal of correlation matrix
  for (k in 1:nresp) {
    for (j in 1:(k - 1)) {
      rescor[choose(k - 1, 2) + j] = Rescor[j, k];
    }
  }
}

Where to start to be able to fix the effect of Gender to 1 parameter estimate rather than 3? I would really appreciate any help here. Many thanks!

I was searching the solution way way too far of course… No need to constrain (or fix) parameters to be equal (the pathway I was on myself). Just drop b_Score2 and b_Score3 from the parameters block and only use b_Score1 in the likelihood. It’s as easy as that of course.