# Best Practices for 3lvl measurement SEM

3 posts / 0 new Offline
Joined: 06/20/2023 - 09:18
Best Practices for 3lvl measurement SEM
AttachmentSize model-xl123.jpg732.64 KB

I'm currently building a 3lvl MSEM including measurement models. The Pritikin et al. (2017) paper is relevant and helpful but whereas they have observed variables on every level, I mainly want to use levels to control for the between-part of the variance of items I measured only on the 1st level. Hence, I want to make sure I don't misclassify my model (see below). Assuming the model is right, I'm also grateful on tips how to deal with a non-convex Hessian when trying to fit it.

The overall goal: My goal is to build a model with two variables measured on the 1st "individual" level (X, Y), two variables measured on the 2nd "team" level (V, W) and a 3rd "district" level to control for shared variance between teams. All X, Y, V, W will be related through regressions on the 2nd level. I compare my models with lavaan as far as feasible (lavaan isn't able to do 3lvl MSEMs).

In this step, I start with a simple measurement model of one latent variable, X, measured through n items. I want to build a 3-lvl measurement model to do justice to the team and district structure underlying the data. After building these models also for Y, V, and W, I want to build the full regression model by adding paths between the variables.

2 Levels: I get the single-level regressions and 2lvl-measurement models to agree between OpenMx and lavaan. This means lavaan seems to make the following implicit assumptions:
- fixing first factor loading to 1 while freeing the variance of latent variables (ok)
- fixing the means of variables to zero on the within level (why is this a good choice?)
- adding variables as latent variables to the between level (sure, this is what "levels" is all about)
- adding a path from between-level (latent) variables to the corresponding manifest variables on the within-level, fixed to 1 (ok)

3 Levels: I now added a third level ("district") and followed these basic steps. Please see the attached file.
There are two modeling choices I'm not 100% sure about - if you know about best practices I'd be very happy as I wasn't able to find any. (E.g., Pritikin et al. link latent variables, as manifest variables differ on each level; I'm not an expert in MPLUS but it seems many of their assumptions regarding these choices are not made explicit when defining the model.)
- should I fix means of the items to zero on the between level? I am assuming different team means so opted not do to so (but I also thought there would be different means on the individual level)
- should I link the latent variables corresponding to the items between the district-lvl and the between-lvl (lvls 3 and 2)? Teams are clustered in districts, so this makes sense. (Or do I link lvls 3 and 1 directly as this is the actual relationship I'm interested in?)

With these decisions out of the way, here is the code I used.
Running the code I get the following warning and standard errors are through the roofs: In model 'within' Optimizer returned a non-zero status code 5. The Hessian at the solution does not appear to be convex. See ?mxCheckIdentification for possible diagnosis (Mx status RED).

Please note that I have used the fitted model to eye-ball starting values of the right order of magnitude. Overall district-level variances are very very small, several of them turn negative in the estimates. Might this be a reason why mxRun() struggles with the model?

I'm grateful for your support. I hope I've just made a simple mistake somewhere and hope it's not too stupid.

data$team <- as.factor(as.character(data$team))
data$district <- as.factor(as.character(data$district))
dataL1 <- data
dataL2 <- data[!duplicated(data$team),] dataL3 <- data[!duplicated(data$district),]
mxDataL1 <- mxData(observed=dataL1, type="raw")
mxDataL2 <- mxData(observed=dataL2, type="raw", primaryKey = "team")
mxDataL3 <- mxData(observed=dataL3, type="raw", primaryKey = "district")

nx <- 14
xItems <- paste0("x",1:nx)

# Define Paths
# Paths X lvl 1
pathXItemsVar <- mxPath(from=xItems, arrows=2, free=TRUE, values = diag(var(data[xItems], na.rm=TRUE)), labels=paste0("e_x",1:nx)) # residual variances
pathXItemsMeans <- mxPath(from = "one", to = xItems, free=TRUE, values=1, labels=paste0("m_x",1:nx)) # means items
pathXItemsMeans0 <- mxPath(from = "one", to = xItems, free = FALSE, values=0, labels=paste0("m_x",1:nx)) # manifest means, constrained to 0
pathXLatentVar <- mxPath(from="x", arrows=2, free=TRUE, values=0.2, labels="var_x") # latent variance

# Paths X lvl 2
pathXItemsVarL2 <- mxPath(from=xItems, arrows=2, free=TRUE, values = 0.1, labels=paste0("e_l2x",1:nx)) # (latent) items variance
pathXItemsMeansL2 <- mxPath(from = "one", to = xItems, free=TRUE, values=1, labels=paste0("m_l2x",1:nx)) # (latent) items means
pathXItemsMeansL20 <- mxPath(from = "one", to = xItems, free=FALSE, values=0, labels=paste0("m_l2x",1:nx)) # (latent) items means, constrained to 0 (not used here)
pathXLoadingsL2 <- mxPath(from = "xTeam", to = xItems,free = c(FALSE,rep(TRUE,nx-1)), values = 1, labels = paste0("fl_x_b",1:nx)) # latent Team factor, loadings constrained to same as on within level, first fixed to 1
pathXLatentVarL2 <- mxPath(from = "xTeam", arrows = 2, values = 0.1, free = TRUE, labels = "xTeam_var") # latent team factor, variance

# Paths X lvl 3
pathXItemsVarL3 <- mxPath(from=xItems, arrows=2, free=TRUE, values = 0.1, labels=paste0("e_l3x",1:nx)) # (latent) items variance
pathXItemsMeansL3 <- mxPath(from = "one", to = xItems, free=TRUE, values=1, labels=paste0("m_l3x",1:nx)) # (latent) items means
pathXLoadingsL3 <- mxPath(from = "xDistrict", to = xItems, values = 1, free = c(FALSE,rep(TRUE,nx-1)), labels = paste0("fl_x_d",1:nx)) # latent Team factor, loadings constrained to same as on within level, first fixed to 1
pathXLatentVarL3 <- mxPath(from = "xDistrict", arrows = 2, values = 1, free = TRUE, labels = "xDistrict_var") # latent team factor, variance

# OpenMx - 3 lvl
modelXLatentL3 <- mxModel(
model = "district",
type = "RAM",
manifestVars = c(),
latentVars = c("xDistrict",xItems),
mxData(observed = dataL3, type="raw", primaryKey = "district"),
pathXItemsVarL3,
pathXItemsMeansL3,
pathXLatentVarL3
)

modelXLatentL2 <- mxModel(
model = "between",
type = "RAM",
modelXLatentL3,
manifestVars = c(),
latentVars = c("xTeam",xItems),
mxDataL2,
mxPath(from = paste0("district.",xItems), to = xItems, values=1, free=FALSE, joinKey="district"), #linking (latent) district lvl items
pathXItemsMeansL2, pathXItemsVarL2,
)

modelXLatentL123 <- mxModel(
model = "within",
type = "RAM",
modelXLatentL2,
manifestVars = c(xItems),
latentVars = c("x"),
mxDataL1,
mxPath(from = paste0("between.",xItems), to = xItems, values=1, free=FALSE, joinKey="team"), #linking (latent) between lvl Items to manifest vars
pathXItemsMeans0, pathXItemsVar,
)

fitXLatentL123 <- mxRun(modelXLatentL123, intervals = TRUE)
summary(fitXLatentL123) Offline
Joined: 07/31/2009 - 15:26
Example

Here's an example 2-level factor model: https://github.com/OpenMx/OpenMx/blob/master/inst/models/nightly/mplus-ex9.6.R

It should be able to get you started.

Here's the core part of the model

library(OpenMx)

set.seed(1)
if (is(ex96, "try-error")) ex96 <- read.table("data/ex9.6.dat")

ex96$V8 <- as.integer(ex96$V8)
bData <- ex96[!duplicated(ex96\$V8), c('V7', 'V8')]
colnames(bData) <- c('w', 'clusterID')
wData <- ex96[,-match(c('V7'), colnames(ex96))]
colnames(wData) <- c(paste0('y', 1:4), paste0('x', 1:2), 'clusterID')

bModel <- mxModel(
'between', type="RAM",
mxData(type="raw", observed=bData, primaryKey="clusterID"),
latentVars = c("lw", "fb"),
mxPath("one", "lw", labels="data.w", free=FALSE),
mxPath("fb", arrows=2, labels="psiB"),
mxPath("lw", 'fb', labels="phi1"))

wModel <- mxModel(
'within', type="RAM", bModel,
mxData(type="raw", observed=wData),
manifestVars = paste0('y', 1:4),
latentVars = c('fw', paste0("xe", 1:2)),
mxPath("one", paste0('y', 1:4), values=runif(4),
labels=paste0("gam0", 1:4)),
mxPath("one", paste0('xe', 1:2),
labels=paste0('data.x',1:2), free=FALSE),
mxPath(paste0('xe', 1:2), "fw",
labels=paste0('gam', 1:2, '1')),
mxPath('fw', arrows=2, values=1.1, labels="varFW"),
mxPath('fw', paste0('y', 1:4), free=c(FALSE, rep(TRUE, 3)),
mxPath('between.fb', paste0('y', 1:4), values=c(1,runif(3)),
joinKey="clusterID"),
mxPath(paste0('y', 1:4), arrows=2, values=rlnorm(4),
labels=paste0("thetaW", 1:4)))

To extend this to three levels, you would make another model similar to bModel but for your district level. And then you'd add paths similar to the below:

mxPath('between.fb', paste0('y', 1:4), values=c(1,runif(3)),
joinKey="clusterID")

If that doesn't help, perhaps generate some fake data with mxGenerateData() and we can try to debug that way. 