Create an extended SEM with OpenMx (Boker et al., 2011) using a lavaan-style (Rosseel, 2012) syntax.
Usage
mxsem(
model,
data,
scale_loadings = TRUE,
scale_latent_variances = FALSE,
add_intercepts = TRUE,
add_variances = TRUE,
add_exogenous_latent_covariances = TRUE,
add_exogenous_manifest_covariances = TRUE,
lbound_variances = TRUE,
directed = unicode_directed(),
undirected = unicode_undirected(),
return_parameter_table = FALSE
)
Arguments
- model
model syntax similar to lavaan's syntax
- data
raw data used to fit the model. Alternatively, an object created with
OpenMx::mxData
can be used (e.g.,OpenMx::mxData(observed = cov(OpenMx::Bollen), means = colMeans(OpenMx::Bollen), numObs = nrow(OpenMx::Bollen), type = "cov")
).- scale_loadings
should the first loading of each latent variable be used for scaling?
- scale_latent_variances
should the latent variances be used for scaling?
- add_intercepts
should intercepts for manifest variables be added automatically? If set to false, intercepts must be added manually. If no intercepts are added, mxsem will automatically use just the observed covariances and not the observed means.
- add_variances
should variances for manifest and latent variables be added automatically?
- add_exogenous_latent_covariances
should covariances between exogenous latent variables be added automatically?
- add_exogenous_manifest_covariances
should covariances between exogenous manifest variables be added automatically?
- lbound_variances
should the lower bound for variances be set to 0.000001?
- directed
symbol used to indicate directed effects (regressions and loadings)
- undirected
symbol used to indicate undirected effects (variances and covariances)
- return_parameter_table
if set to TRUE, the internal parameter table is returend together with the mxModel
Value
mxModel object that can be fitted with mxRun or mxTryHard. If return_parameter_table is TRUE, a list with the mxModel and the parameter table is returned.
Details
Setting up SEM can be tedious. The lavaan (Rosseel, 2012) package provides a great syntax to
make the process easier. The objective of mxsem is to provide a similar syntax
for OpenMx. OpenMx is a flexible R package for extended SEM. However, note that
mxsem only covers a small part of the OpenMx framework by focusing on "standard"
SEM. Similar to lavaan's sem()
-function, mxsem
tries to set up parts
of the model automatically (e.g., adding variances automatically or scaling the
latent variables automatically). If you want to unlock
the full potential of OpenMx, mxsem may not be the best option.
Warning: The syntax and settings of mxsem may differ from
lavaan in some cases. See vignette("Syntax", package = "mxsem")
for more details
on the syntax and the default arguments.
Alternatives
You will find similar functions in the following packages:
metaSEM (Cheung, 2015) provides a
lavaan2RAM
function that can be combined with thecreate.mxModel
function. This combination offers more features than mxsem. For instance, constraints of the forma < b
are supported. In mxsem such constraints require algebras (e.g.,!diff; a := b - exp(diff)
).umx (Bates et al., 2019) provides the
umxRAM
andumxLav2RAM
functions that can parse single lavaan-style statements (e.g.,eta =~ y1 + y2 + y3
) or an entire lavaan models to OpenMx models.tidySEM (van Lissa, 2023) provides the
as_ram
function to translate lavaan syntax to OpenMx and also implements a unified syntax to specify both, lavaan and OpenMx models. Additionally, it works well with the tidyverse.ezMx (Bates, et al. 2014) simplifies fitting SEM with OpenMx and also provides a translation of lavaan models to OpenMx with the
lavaan.to.OpenMx
function.
Because mxsem implements the syntax parser from scratch, it can extend the lavaan syntax to account for specific OpenMx features. This enables implicit transformations with curly braces.
Citation
Cite OpenMx (Boker et al., 2011) for the modeling and lavaan for the syntax (Rosseel, 2012). mxsem itself is just a very small package and lets OpenMx do all the heavy lifting.
Defaults
By default, mxsem scales latent variables by setting the loadings on the first
item to 1. This can be changed by setting scale_loadings = FALSE
in the function
call. Setting scale_latent_variances = TRUE
sets latent variances to 1 for
scaling.
mxsem will add intercepts for all manifest variables as well as variances for all manifest and latent variables. A lower bound of 1e-6 will be added to all variances. Finally, covariances for all exogenous variables will be added. All of these options can be changed when calling mxsem.
Syntax
The syntax is, for the most part, identical to that of lavaan. The following
specifies loadings of a latent variable eta
on manifest variables y1
-y4
:
Regressions are specified with ~
:
Add covariances with ~~
Intercepts are specified with ~1
Parameter labels and constraints
Add labels to parameters as follows:
Fix parameters by using numeric values instead of labels:
Bounds
Lower and upper bounds allow for constraints on parameters. For instance, a lower bound can prevent negative variances.
xi =~ 1*x1 + l2*x2 + l3*x3
eta =~ 1*y1 + l5*y2 + l6*y3
# predict eta with xi:
eta ~ b*xi
# residual variance for x1
x1 ~~ v*x1
# bound:
v > 0
Upper bounds are specified with v < 10. Note that the parameter label must always
come first. The following is not allowed: 0 < v
or 10 > v
.
(Non-)linear constraints
Assume that latent construct eta
was observed twice, where eta1
is the first
observation and eta2
the second. We want to define the loadings of eta2
on its observations as l_1 + delta_l1
. If delta_l1
is zero, we have measurement
invariance.
eta1 =~ l1*y1 + l2*y2 + l3*y3
eta2 =~ l4*y4 + l5*y5 + l6*y6
# define new delta-parameter
!delta_1; !delta_2; !delta_3
# redefine l4-l6
l4 := l1 + delta_1
l5 := l2 + delta_2
l6 := l3 + delta_3
Alternatively, implicit transformations can be used as follows:
eta1 =~ l1*y1 + l2*y2 + l3*y3
eta2 =~ {l1 + delta_1} * y4 + {l2 + delta_2} * y5 + {l3 + delta_3} * y6
Specific labels for the transformation results can also be provided:
eta1 =~ l1*y1 + l2*y2 + l3*y3
eta2 =~ {l4 := l1 + delta_1} * y4 + {l5 := l2 + delta_2} * y5 + {l6 := l3 + delta_3} * y6
This is inspired by the approach in metaSEM (Cheung, 2015).
Definition variables
Definition variables allow for person-specific parameter constraints. Use the
data.
-prefix to specify definition variables.
Starting Values
mxsem differs from lavaan in the specification of starting values. Instead
of providing starting values in the model syntax, the set_starting_values
function is used.
References
Bates, T. C., Maes, H., & Neale, M. C. (2019). umx: Twin and Path-Based Structural Equation Modeling in R. Twin Research and Human Genetics, 22(1), 27–41. https://doi.org/10.1017/thg.2019.2
Bates, T. C., Prindle, J. J. (2014). ezMx. https://github.com/OpenMx/ezMx
Boker, S. M., Neale, M., Maes, H., Wilde, M., Spiegel, M., Brick, T., Spies, J., Estabrook, R., Kenny, S., Bates, T., Mehta, P., & Fox, J. (2011). OpenMx: An Open Source Extended Structural Equation Modeling Framework. Psychometrika, 76(2), 306–317. https://doi.org/10.1007/s11336-010-9200-6
Cheung, M. W.-L. (2015). metaSEM: An R package for meta-analysis using structural equation modeling. Frontiers in Psychology, 5. https://doi.org/10.3389/fpsyg.2014.01521
Rosseel, Y. (2012). lavaan: An R package for structural equation modeling. Journal of Statistical Software, 48(2), 1–36. https://doi.org/10.18637/jss.v048.i02
van Lissa, C. J. (2023). tidySEM: Tidy Structural Equation Modeling. R package version 0.2.4, https://cjvanlissa.github.io/tidySEM/.
Examples
# THE FOLLOWING EXAMPLE IS ADAPTED FROM LAVAAN
library(mxsem)
model <- '
# latent variable definitions
ind60 =~ x1 + x2 + x3
dem60 =~ y1 + a1*y2 + b*y3 + c1*y4
dem65 =~ y5 + a2*y6 + b*y7 + c2*y8
# regressions
dem60 ~ ind60
dem65 ~ ind60 + dem60
# residual correlations
y1 ~~ y5
y2 ~~ y4 + y6
y3 ~~ y7
y4 ~~ y8
y6 ~~ y8
'
fit <- mxsem(model = model,
data = OpenMx::Bollen) |>
mxTryHard()
#> Running untitled13 with 41 parameters
#>
#> Beginning initial fit attempt
#> Running untitled13 with 41 parameters
#>
#> Lowest minimum so far: 3096.94452072454
#>
#> Solution found
#>
#>
#> Solution found! Final fit=3096.9445 (started at 226053.29) (1 attempt(s): 1 valid, 0 errors)
#> Start values from best fit:
#> 2.17951987773111,1.81811280701136,1.44903971585263,0.604497109020104,1.2914705087076,1.17388194480817,1.30214833592763,0.898493075089598,1.1324734492235,1.20957839087818,1.91458020207026,7.40452084345933,4.99235939692914,1.32054688622592,3.15118547115015,2.17540111771095,5.0152306057039,0.0813525457778652,0.120528431147638,0.466700862872675,0.590970527832852,2.30230807957473,0.731339590208144,3.52499601408348,0.353182177765634,1.41225190814923,3.32140676951725,0.448633932250771,3.71721598526889,0.164480652182256,5.46466355896305,4.25643995945798,6.56310792761437,4.4525276703987,2.97807303177841,5.05438328541841,4.7921934164438,3.55768869770291,5.13624831577953,6.19625910503793,4.04338865506019
omxGetParameters(fit)
#> ind60→x2 ind60→x3 ind60→dem60 ind60→dem65 a1 b
#> 2.17951988 1.81811281 1.44903972 0.60449711 1.29147051 1.17388194
#> c1 dem60→dem65 a2 c2 y1↔y1 y2↔y2
#> 1.30214834 0.89849308 1.13247345 1.20957839 1.91458020 7.40452084
#> y3↔y3 y2↔y4 y4↔y4 y2↔y6 y6↔y6 x1↔x1
#> 4.99235940 1.32054689 3.15118547 2.17540112 5.01523061 0.08135255
#> x2↔x2 x3↔x3 y1↔y5 y5↔y5 y3↔y7 y7↔y7
#> 0.12052843 0.46670086 0.59097053 2.30230808 0.73133959 3.52499601
#> y4↔y8 y6↔y8 y8↔y8 ind60↔ind60 dem60↔dem60 dem65↔dem65
#> 0.35318218 1.41225191 3.32140677 0.44863393 3.71721599 0.16448065
#> one→y1 one→y2 one→y3 one→y4 one→y6 one→x1
#> 5.46466356 4.25643996 6.56310793 4.45252767 2.97807303 5.05438329
#> one→x2 one→x3 one→y5 one→y7 one→y8
#> 4.79219342 3.55768870 5.13624832 6.19625911 4.04338866
model_transformations <- '
# latent variable definitions
ind60 =~ x1 + x2 + x3
dem60 =~ y1 + a1*y2 + b1*y3 + c1*y4
dem65 =~ y5 + {a2 := a1 + delta_a}*y6 + {b2 := b1 + delta_b}*y7 + c2*y8
# regressions
dem60 ~ ind60
dem65 ~ ind60 + dem60
# residual correlations
y1 ~~ y5
y2 ~~ y4 + y6
y3 ~~ y7
y4 ~~ y8
y6 ~~ y8
'
fit <- mxsem(model = model_transformations,
data = OpenMx::Bollen) |>
mxTryHard()
#> Running untitled16 with 42 parameters
#>
#> Beginning initial fit attempt
#> Running untitled16 with 42 parameters
#>
#> Lowest minimum so far: 3095.58188681023
#>
#> Solution found
#>
#>
#> Solution found! Final fit=3095.5819 (started at 225616.79) (1 attempt(s): 1 valid, 0 errors)
#> Start values from best fit:
#> 2.18036692502419,1.8185110842876,1.48299713907554,0.572339861173599,1.25674950145635,1.05771651270941,1.26478738795161,0.837345234806707,1.26594677757215,1.89139933687739,7.37285050090153,5.06747712630144,1.31312234445956,3.14792354607904,2.15286574816779,4.95395470168421,0.0815493292024986,0.119806744784092,0.466702792790979,0.623669558954236,2.35097477433781,0.794960818250051,3.43139043112232,0.34822384350209,1.35615768074408,3.25408176784843,0.448438066317501,3.95602557364003,0.172474415796178,5.46467040485137,4.25645067885819,6.56311263149745,4.45253722882951,2.97808065434922,5.054384557668,4.7921959164682,3.55769087542126,5.13625612535699,6.19626561257264,4.0433958742259,-0.0710524345068419,0.221794335683815
omxGetParameters(fit)
#> ind60→x2 ind60→x3 ind60→dem60 ind60→dem65 a1 b1
#> 2.18036693 1.81851108 1.48299714 0.57233986 1.25674950 1.05771651
#> c1 dem60→dem65 c2 y1↔y1 y2↔y2 y3↔y3
#> 1.26478739 0.83734523 1.26594678 1.89139934 7.37285050 5.06747713
#> y2↔y4 y4↔y4 y2↔y6 y6↔y6 x1↔x1 x2↔x2
#> 1.31312234 3.14792355 2.15286575 4.95395470 0.08154933 0.11980674
#> x3↔x3 y1↔y5 y5↔y5 y3↔y7 y7↔y7 y4↔y8
#> 0.46670279 0.62366956 2.35097477 0.79496082 3.43139043 0.34822384
#> y6↔y8 y8↔y8 ind60↔ind60 dem60↔dem60 dem65↔dem65 one→y1
#> 1.35615768 3.25408177 0.44843807 3.95602557 0.17247442 5.46467040
#> one→y2 one→y3 one→y4 one→y6 one→x1 one→x2
#> 4.25645068 6.56311263 4.45253723 2.97808065 5.05438456 4.79219592
#> one→x3 one→y5 one→y7 one→y8 delta_a delta_b
#> 3.55769088 5.13625613 6.19626561 4.04339587 -0.07105243 0.22179434