Best Practices for 3lvl measurement SEM
Attachment | Size |
---|---|
model-xl123.jpg | 732.64 KB |
**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
pathXLoadings <- mxPath(from="x", to=xItems, free=c(FALSE,rep(TRUE,nx-1)), values=1, labels=paste0("fl_x_w",1:nx)) # factor loadings, fixing first factor to 1
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,
pathXLoadingsL3,
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,
pathXLatentVarL2, pathXLoadingsL2
)
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,
pathXLatentVar, pathXLoadings
)
fitXLatentL123 <- mxRun(modelXLatentL123, intervals = TRUE)
summary(fitXLatentL123)
Example
It should be able to get you started.
Here's the core part of the model
library(OpenMx)
set.seed(1)
ex96 <- suppressWarnings(try(read.table("models/nightly/data/ex9.6.dat")))
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)),
values=c(1,runif(3)), labels=paste0("loadW", 1:4)),
mxPath('between.fb', paste0('y', 1:4), values=c(1,runif(3)),
free=c(FALSE, rep(TRUE, 3)), labels=paste0("loadB", 1:4),
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)),
free=c(FALSE, rep(TRUE, 3)), labels=paste0("loadB", 1:4),
joinKey="clusterID")
but for your district level.
If that doesn't help, perhaps generate some fake data with
mxGenerateData()
and we can try to debug that way.Log in or register to post comments
In reply to Example by mhunter
Thank you
I am currently taking a step back from this approach and model variables only on two levels. I might give this a go later.
Log in or register to post comments