Title: | Specify 'OpenMx' Models with a 'lavaan'-Style Syntax |
---|---|
Description: | Provides a 'lavaan'-like syntax for 'OpenMx' models. The syntax supports definition variables, bounds, and parameter transformations. This allows for latent growth curve models with person-specific measurement occasions, moderated nonlinear factor analysis and much more. |
Authors: | Jannik H. Orzek [aut, cre, cph] |
Maintainer: | Jannik H. Orzek <[email protected]> |
License: | GPL (>= 3) |
Version: | 0.1.0 |
Built: | 2024-11-16 20:39:16 UTC |
Source: | https://github.com/jhorzek/mxsem |
takes in a lavaan style syntax and removes comments, white space, etc.
clean_syntax(syntax)
clean_syntax(syntax)
syntax |
lavaan style syntax |
vector of strings with cleaned syntax
returns a list of groups for a multi group model
get_groups(multi_group_model)
get_groups(multi_group_model)
multi_group_model |
multi group model created with mxsem_group_by |
list with data for each group
# THE FOLLOWING EXAMPLE IS ADAPTED FROM # https://openmx.ssri.psu.edu/docs/OpenMx/latest/_static/Rdoc/mxModel.html library(mxsem) model <- 'spatial =~ visual + cubes + paper verbal =~ general + paragrap + sentence math =~ numeric + series + arithmet' mg_model <- mxsem(model = model, data = OpenMx::HS.ability.data) |> # we want separate models for all combinations of grades and schools: mxsem_group_by(grouping_variables = "school") |> mxTryHard() # let's summarize the results: summarize_multi_group_model(mg_model) # let's get the groups: get_groups(mg_model)
# THE FOLLOWING EXAMPLE IS ADAPTED FROM # https://openmx.ssri.psu.edu/docs/OpenMx/latest/_static/Rdoc/mxModel.html library(mxsem) model <- 'spatial =~ visual + cubes + paper verbal =~ general + paragrap + sentence math =~ numeric + series + arithmet' mg_model <- mxsem(model = model, data = OpenMx::HS.ability.data) |> # we want separate models for all combinations of grades and schools: mxsem_group_by(grouping_variables = "school") |> mxTryHard() # let's summarize the results: summarize_multi_group_model(mg_model) # let's get the groups: get_groups(mg_model)
evaluates algebras for each subject in the data set. This function is useful if you have algebras with definition variables (e.g., in mnlfa).
get_individual_algebra_results( mxModel, algebra_names = NULL, progress_bar = TRUE )
get_individual_algebra_results( mxModel, algebra_names = NULL, progress_bar = TRUE )
mxModel |
mxModel with algebras |
algebra_names |
optional: Only compute individual algebras for a subset of the parameters |
progress_bar |
should a progress bar be shown? |
a list of data frames. The list contains data frames for each of the algebras. The data frames contain the individual specific algebra results as well as all definition variables used to predict said algebra
library(mxsem) set.seed(123) dataset <- simulate_moderated_nonlinear_factor_analysis(N = 50) model <- " xi =~ x1 + x2 + x3 eta =~ y1 + y2 + y3 eta ~ {a := a0 + data.k*a1}*xi " fit <- mxsem(model = model, data = dataset) |> mxTryHard() algebra_results <- get_individual_algebra_results(mxModel = fit, progress_bar = FALSE) # the following plot will only show two data points because there is only # two values for the definition variable k (0 or 1). plot(x = algebra_results[["a"]]$k, y = algebra_results[["a"]]$algebra_result)
library(mxsem) set.seed(123) dataset <- simulate_moderated_nonlinear_factor_analysis(N = 50) model <- " xi =~ x1 + x2 + x3 eta =~ y1 + y2 + y3 eta ~ {a := a0 + data.k*a1}*xi " fit <- mxsem(model = model, data = dataset) |> mxTryHard() algebra_results <- get_individual_algebra_results(mxModel = fit, progress_bar = FALSE) # the following plot will only show two data points because there is only # two values for the definition variable k (0 or 1). plot(x = algebra_results[["a"]]$k, y = algebra_results[["a"]]$algebra_result)
Create an extended SEM with OpenMx (Boker et al., 2011) using a lavaan-style (Rosseel, 2012) syntax.
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 )
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 )
model |
model syntax similar to lavaan's syntax |
data |
raw data used to fit the model. Alternatively, an object created
with |
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 |
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.
You will find similar functions in the following packages:
metaSEM (Cheung, 2015) provides a lavaan2RAM
function that can be combined with the create.mxModel
function. This combination
offers more features than mxsem. For instance, constraints of the form a < b
are supported. In mxsem such constraints require algebras (e.g., !diff; a := b - exp(diff)
).
umx (Bates et al., 2019)
provides the umxRAM
and umxLav2RAM
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.
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.
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.
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
:
eta =~ y1 + y2 + y3
Regressions are specified with ~
:
xi =~ x1 + x2 + x3 eta =~ y1 + y2 + y3 # predict eta with xi: eta ~ xi
Add covariances with ~~
xi =~ x1 + x2 + x3 eta =~ y1 + y2 + y3 # predict eta with xi: eta ~ xi x1 ~~ x2
Intercepts are specified with ~1
xi =~ x1 + x2 + x3 eta =~ y1 + y2 + y3 # predict eta with xi: eta ~ xi x1 ~~ x2 eta ~ 1
Add labels to parameters as follows:
xi =~ l1*x1 + l2*x2 + l3*x3 eta =~ l4*y1 + l5*y2 + l6*y3 # predict eta with xi: eta ~ b*xi
Fix parameters by using numeric values instead of labels:
xi =~ 1*x1 + l2*x2 + l3*x3 eta =~ 1*y1 + l5*y2 + l6*y3 # predict eta with xi: eta ~ b*xi
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
.
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 allow for person-specific parameter constraints. Use the
data.
-prefix to specify definition variables.
I =~ 1*y1 + 1*y2 + 1*y3 + 1*y4 + 1*y5 S =~ data.t_1 * y1 + data.t_2 * y2 + data.t_3 * y3 + data.t_4 * y4 + data.t_5 * y5 I ~ int*1 S ~ slp*1
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.
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/.
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.
# 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() omxGetParameters(fit) 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() omxGetParameters(fit)
# 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() omxGetParameters(fit) 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() omxGetParameters(fit)
creates a multi-group model from an OpenMx model.
mxsem_group_by( mxModel, grouping_variables, parameters = c(".*"), use_grepl = TRUE )
mxsem_group_by( mxModel, grouping_variables, parameters = c(".*"), use_grepl = TRUE )
mxModel |
mxModel with the entire data |
grouping_variables |
Variables used to split the data in groups |
parameters |
the parameters that should be group specific. By default all parameters are group specific. |
use_grepl |
if set to TRUE, grepl is used to check which parameters are group specific. For instance, if parameters = "a" and use_grepl = TRUE, all parameters whose label contains the letter "a" will be group specific. If use_grep = FALSE only the parameter that has the label "a" is group specific. |
mxsem_group_by creates a multi-group model by splitting the data found in an mxModel object using dplyr's group_by function. The general idea is as follows:
1. The function extracts the data from mxModel 2. The data is split using the group_by function of dplyr with the variables in grouping_variables 3. a separate model is set up for each group. All parameters that match those specified in the parameters argument are group specific
**Warning**: The multi-group model may differ from **lavaan**! For instance, **lavaan** will automatically set the latent variances for all but the first group free if the loadings are fixed to equality. Such automatic procedures are not yet implemented in **mxsem**.
mxModel with multiple groups. Use get_groups to extract the groups
# THE FOLLOWING EXAMPLE IS ADAPTED FROM # https://openmx.ssri.psu.edu/docs/OpenMx/latest/_static/Rdoc/mxModel.html library(mxsem) model <- 'spatial =~ visual + cubes + paper verbal =~ general + paragrap + sentence math =~ numeric + series + arithmet' mg_model <- mxsem(model = model, data = OpenMx::HS.ability.data) |> # we want separate models for all combinations of grades and schools: mxsem_group_by(grouping_variables = "school") |> mxTryHard() # let's summarize the results: summarize_multi_group_model(mg_model)
# THE FOLLOWING EXAMPLE IS ADAPTED FROM # https://openmx.ssri.psu.edu/docs/OpenMx/latest/_static/Rdoc/mxModel.html library(mxsem) model <- 'spatial =~ visual + cubes + paper verbal =~ general + paragrap + sentence math =~ numeric + series + arithmet' mg_model <- mxsem(model = model, data = OpenMx::HS.ability.data) |> # we want separate models for all combinations of grades and schools: mxsem_group_by(grouping_variables = "school") |> mxTryHard() # let's summarize the results: summarize_multi_group_model(mg_model)
creates a parameter table from a lavaan like syntax
parameter_table_rcpp( syntax, add_intercept, add_variance, add_exogenous_latent_covariances, add_exogenous_manifest_covariances, scale_latent_variance, scale_loading )
parameter_table_rcpp( syntax, add_intercept, add_variance, add_exogenous_latent_covariances, add_exogenous_manifest_covariances, scale_latent_variance, scale_loading )
syntax |
lavaan like syntax |
add_intercept |
should intercepts for manifest variables be automatically added? |
add_variance |
should variances for all variables be automatically added? |
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? |
scale_latent_variance |
should variances of latent variables be set to 1? |
scale_loading |
should the first loading of each latent variable be set to 1? |
parameter table
Returns the parameter estimates of an mxModel. Wrapper for omxGetParameters
parameters(mxMod)
parameters(mxMod)
mxMod |
mxModel object |
vector with parameter estimates
print the multi_group_parameters
## S3 method for class 'multi_group_parameters' print(x, ...)
## S3 method for class 'multi_group_parameters' print(x, ...)
x |
object from summarize_multi_group_model |
... |
not used |
nothing
set the starting values of an OpenMx model. This is just an interface to omxSetParameters.
set_starting_values(mx_model, values)
set_starting_values(mx_model, values)
mx_model |
model of class mxModel |
values |
vector with labeled parameter values |
mxModel with changed parameter values
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) |> set_starting_values(values = c("a1" = .4, "c1" = .6)) |> mxTryHard()
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) |> set_starting_values(values = c("a1" = .4, "c1" = .6)) |> mxTryHard()
simulate data for a latent growth curve model with five measurement occasions. The time-distance between these occasions differs between subjects.
simulate_latent_growth_curve(N = 100)
simulate_latent_growth_curve(N = 100)
N |
sample size |
data set with columns y1-y5 (observations) and t_1-t_5 (time of observation)
set.seed(123) dataset <- simulate_latent_growth_curve(N = 100) model <- " I =~ 1*y1 + 1*y2 + 1*y3 + 1*y4 + 1*y5 S =~ data.t_1 * y1 + data.t_2 * y2 + data.t_3 * y3 + data.t_4 * y4 + data.t_5 * y5 I ~ int*1 S ~ slp*1 # set intercepts of manifest variables to zero y1 ~ 0*1; y2 ~ 0*1; y3 ~ 0*1; y4 ~ 0*1; y5 ~ 0*1; " mod <- mxsem(model = model, data = dataset) |> mxTryHard()
set.seed(123) dataset <- simulate_latent_growth_curve(N = 100) model <- " I =~ 1*y1 + 1*y2 + 1*y3 + 1*y4 + 1*y5 S =~ data.t_1 * y1 + data.t_2 * y2 + data.t_3 * y3 + data.t_4 * y4 + data.t_5 * y5 I ~ int*1 S ~ slp*1 # set intercepts of manifest variables to zero y1 ~ 0*1; y2 ~ 0*1; y3 ~ 0*1; y4 ~ 0*1; y5 ~ 0*1; " mod <- mxsem(model = model, data = dataset) |> mxTryHard()
simulate data for a moderated nonlinear factor analysis.
simulate_moderated_nonlinear_factor_analysis(N)
simulate_moderated_nonlinear_factor_analysis(N)
N |
sample size |
data set with variables x1-x3 and y1-y3 representing repeated measurements of an affect measure. It is assumed that the autoregressive effect is different depending on covariate k
library(mxsem) set.seed(123) dataset <- simulate_moderated_nonlinear_factor_analysis(N = 2000) model <- " xi =~ x1 + x2 + x3 eta =~ y1 + y2 + y3 eta ~ a*xi # we need two new parameters: a0 and a1. These are created as follows: !a0 !a1 # Now, we redefine a to be a0 + k*a1, where k is found in the data a := a0 + data.k*a1 " mod <- mxsem(model = model, data = dataset) |> mxTryHard() omxGetParameters(mod)
library(mxsem) set.seed(123) dataset <- simulate_moderated_nonlinear_factor_analysis(N = 2000) model <- " xi =~ x1 + x2 + x3 eta =~ y1 + y2 + y3 eta ~ a*xi # we need two new parameters: a0 and a1. These are created as follows: !a0 !a1 # Now, we redefine a to be a0 + k*a1, where k is found in the data a := a0 + data.k*a1 " mod <- mxsem(model = model, data = dataset) |> mxTryHard() omxGetParameters(mod)
summarize the results of a multi group model created with mxsem_group_by
summarize_multi_group_model(multi_group_model)
summarize_multi_group_model(multi_group_model)
multi_group_model |
multi group model created with mxsem_group_by |
list with goup specific parameters and common parameters
# THE FOLLOWING EXAMPLE IS ADAPTED FROM # https://openmx.ssri.psu.edu/docs/OpenMx/latest/_static/Rdoc/mxModel.html library(mxsem) model <- 'spatial =~ visual + cubes + paper verbal =~ general + paragrap + sentence math =~ numeric + series + arithmet' mg_model <- mxsem(model = model, data = OpenMx::HS.ability.data) |> # we want separate models for all combinations of grades and schools: mxsem_group_by(grouping_variables = "school") |> mxTryHard() # let's summarize the results: summarize_multi_group_model(mg_model)
# THE FOLLOWING EXAMPLE IS ADAPTED FROM # https://openmx.ssri.psu.edu/docs/OpenMx/latest/_static/Rdoc/mxModel.html library(mxsem) model <- 'spatial =~ visual + cubes + paper verbal =~ general + paragrap + sentence math =~ numeric + series + arithmet' mg_model <- mxsem(model = model, data = OpenMx::HS.ability.data) |> # we want separate models for all combinations of grades and schools: mxsem_group_by(grouping_variables = "school") |> mxTryHard() # let's summarize the results: summarize_multi_group_model(mg_model)
this function returns the unicode for directed arrows
unicode_directed()
unicode_directed()
returns unicode for directed arrows
this function returns the unicode for undirected arrows
unicode_undirected()
unicode_undirected()
returns unicode for undirected arrows