Latent variable modelling when observed variables are ordinal

Wondering how people are doing latent variable modelling when the observed variables are ordinal. In the blog I link below this paragraph, an example in brms is given, but the observed variables related to the underlying construct are simulated as continuous with rnorm(). In my field, it’s more likely that we measure such things with ordinal survey data.

Let’s say I have an underlying construct X that is well-established in the field. It’s at least very practically useful to consider this as something that really exists. We target this construct in a survey with three questions (q1, q2, and q3) that are answered with a 5-point ordinal rating scale. We then want to predict y with our latent variable.

Borrowing from the blog post I linked to, a way to do that in brms would be to set up the model in the following way. However, when I make it so that my ordinal variables are ordered factors, the model won’t run as it is expecting continuous variables.

# mi(x) tells brms that it is missing
bf1 <- bf(q1 ~ 0 + mi(X)) 
bf2 <- bf(q2 ~ 0 + mi(X)) 
bf3 <- bf(q3 ~ 0 + mi(X))
bf4 <- bf(X | mi() ~ 0)
bf5 <- bf(y ~ mi(X))

# Fitting the model
fit3 <- brm(bf1 + bf2 + bf3 + bf4 + bf5 + set_rescor(FALSE),  data = d, 
            prior = c(prior(normal(1, 0.000001), coef = miX, resp = q1),
                      prior(normal(1, 1), coef = miX, resp = q2),
                      prior(normal(1, 1), coef = miX, resp = q3),
                      prior(normal(0, 1), coef = miX, resp = y)), ...)

The only issue I’m having here is that I can’t find resources on how to fit these types of models that properly account for the fact that q1, q2, and q3 are ordinal variables. It seems like people are often just treating ordinal variables as continuous variables, which is something I wouldn’t want to do. Any direction to materials or recommendations would be greatly appreciated!

1 Like

I have been doing this very thing in Stan recently, even relating multiple latent constructs in linear models on the latent level. I am not sure how you would do this in brms. In Stan, the gist of it is something along these lines.
Suppose you have N people who respond to 3 Likert items each. These 3 Likert items make up your latent continuous construct x, which is of length N. Thus the number of total Likert observations is 3*N = N_obs. You have an index of length N_obs, latent_idx, that denotes which x each Likert observation, likert_x, corresponds too (i.e. the Likert observations are concatenated and you have 3 Likert responses for every single value of x). You have ncut the number of cutpoints in the ordinal model. You have alpha which is a vector of values for the induced-dirichlet prior. The induced-dirichlet, defined in the prior model and functions, allows you to specify a dirichlet prior on the cutpoints. The below implementation derives cutpoints from a simplex (with much of the function pulled from rstanarm backend), but you can also use an implementation with Jacobian correction. See Ordinal Regression for a discussion of this prior. Betancourt uses the implementation with a Jacobian correction, but the discussion is applicable.

functions {
  vector make_cutpoints(vector probabilities, int ncuts) {
    vector[ncuts] cutpoints;
    real running_sum = 0;
      for (n in 1:ncuts) {
        running_sum += probabilities[n];
        cutpoints[n] = inv_Phi(running_sum);
      }
    return cutpoints;
  }
}
data {
  int<lower=1> N;
  int<lower=1> N_obs;
  vector[N] y;
  int[N_obs] likert_x;
  int[N_obs] latent_idx;
  int<lower=2> ncut;
  vector[ncut + 1] alpha;
}
parameters {
  simplex[ncut + 1] pi;
  vector[N] x;
  real beta;
  real<lower=0> sigma;
}
transformed parameters {
  vector[ncut] cutpoints = make_cutpoints(pi, ncut);
}
model {
  // prior model
  pi ~ dirichlet(alpha);
  x ~ normal(0, 1);
  beta ~ normal(0, 1);
  sigma ~ normal(0, 0.3);

  // latent model
  y ~ normal(beta * x, sigma);

  // observational model
  likert_x ~ ordered_probit(x[latent_idx], cutpoints);

}
generated quantities {
  vector[N_obs] pred_likert_x;
  for (n in 1:N_obs) {
    pred_likert_x[n] = ordered_probit_rng(x[latent_idx[n]], cutpoints);
  }
}

Essentially what this model does is map 3 Likert outcomes to a single affinity parameter (in the terms of Betancourt’s above case study), which is your latent continuous construct. The outcome y is then predicted by the latent construct x in a simple linear normal model with parameters beta and sigma. Note that if the latent_idx part is confusing, just realize that this is a way to write more compact code. You could do away with the index and separate out the 3 Likert outcomes into their own arrays of length N and write the observational model in 3 lines like

likert_item1 ~ ordered_probit(x, cutpoints);
likert_item2 ~ ordered_probit(x, cutpoints);
likert_item3 ~ ordered_probit(x, cutpoints);

instead of a single line.
The model can become much more complicated with varying cutpoints by item or person and/or discrimination parameters (with positive constraint) that weight the influence of the affinity on the cutpoint shift. These varying cutpoints can even be modeled hierarchically. You could also model multiple latent constructs if you have them. For more information, I strongly recommend Betancourt’s latest case study Bae’s Theorem which, although doesn’t implement exactly what you are looking for, is pretty inspiring for modeling this type of data, as it models Likert item ratings with hierarchical affinities and cutpoints as I just described.
In summary, you can model the affinity for each person with one or more Likert items in ordinal model(s). You can then use this affinity as the latent construct in any other model that you need.

2 Likes

This is fantastic, thank you so much for taking the time to post! I’ve done a bit of modelling in Stan, but I can read it well enough to tell that this is definitely similar to what I am trying to do.

1 Like

You might also look at blavaan, which uses a different approach to handling ordinal variables. The link below gives a nice summary of the method.

https://ecmerkle.github.io/blavaan/articles/ordinal.html

2 Likes