Nothing
#' @title Measurement Invariance Models
#'
#' @description Generate metric and
#' scalar invariance models and their
#' partial invariance versions.
#'
#' @details
#' This a helper function to generate,
#' based on a multigroup confirmatory
#' factor analysis (CFA) model with
#' no between-group equality constraints,
#' the following models:
#'
#' - A metric invariance model (loadings
#' constrained to be equal across
#' groups).
#'
#' - A scalar invariance model (intercepts
#' and loadings constrained to be equal
#' across groups).
#'
#' - Partial invariance versions of the
#' previous two models, such as a
#' model with the loadings of all items,
#' except for one, constrained to be
#' equal across groups.
#'
#' The models generated can then be used
#' in `model_set()` to compute BPPs.
#'
#' ## Requirements
#'
#' The model used as the input needs to
#' be fitted with no between group
#' constrains, that is, it is a
#' configural invariance model. Although
#' not a must, it is advised to use
#' the default way to identify each
#' factor (that is, fixing a loading
#' to one).
#'
#' ## Implementation
#'
#' This function simply use the
#' `group.partial` and `group.equal`
#' argument of [lavaan::cfa()] to
#' generate the models.
#'
#' @param cfa_out The output of
#' [lavaan::cfa()].
#'
#' @param max_free The maximum number of
#' constraints to be released when
#' generating the partial invariance
#' models. For example, if set to 1,
#' then only the partial metric invariance
#' model only has at most one item allowed
#' to have different loadings across
#' group. Default is 1. If set to zero,
#' then no partial invariance models
#' will be generated.
#'
#' @param metric Logical. If `TRUE`,
#' the default, then metric invariance
#' model and its partial invariance
#' versions are generated.
#'
#' @param scalar Logical. If `TRUE`,
#' the default, then scalar invariance
#' model and its partial invariance
#' versions are generated.
#'
#' @param progress Logical. If `TRUE`,
#' the default, progress bars will be
#' displayed when fitting partial
#' invariance models.
#'
#' @return A list of [lavaan::cfa()]
#' output. The names are automatically
#' generated to indicate whether a model
#' is configural, metric, or scalar
#' invariance, or the item(s) without
#' between-group constraints on the
#' loadings (for partial metric
#' invariance) or intercepts (for
#' partial scalar invariance).
#'
#' @author Shu Fai Cheung <https://orcid.org/0000-0002-9871-9448>
#'
#' @seealso [model_set()]
#'
#' @examples
#'
#' library(lavaan)
#' # For illustration, only one factor is used,
#' # with one item from another factor added
#' # just to make the model not saturated.
#' HSmod <-
#' "
#' spatial =~ x1 + x2 + x3 + x4
#' "
#' fit_config <- cfa(model = HSmod,
#' data = HolzingerSwineford1939,
#' group = "school")
#' fit_mi <- measurement_invariance_models(fit_config)
#' names(fit_mi)
#' # Need to add 'skip_check_sem_out = TRUE' to use multigroup models.
#' out <- model_set(sem_out = fit_mi,
#' skip_check_sem_out = TRUE,
#' progress = FALSE,
#' parallel = FALSE)
#' print(out)
#'
#' @export
#'
measurement_invariance_models <- function(cfa_out,
max_free = 1,
metric = TRUE,
scalar = TRUE,
progress = TRUE) {
pt <- lavaan::parameterTable(cfa_out)
if (!metric && !scalar) {
return(list(config = cfa_out))
}
if (lavaan::lavInspect(cfa_out, "ngroups") == 1) {
stop("Cannot be used on single-group models.")
}
opt <- lavaan::lavInspect(cfa_out, "options")
if (length(opt$group.equal) != 0) {
stop("Cannot be used models with 'group.equal' set.")
}
if (length(opt$group.partial) != 0) {
stop("Cannot be used models with 'group.partial' set.")
}
cfa_out@call$model <- eval(cfa_out@call$model,
envir = parent.frame())
if (metric) {
fit_metric <- lavaan::update(cfa_out,
group.equal = "loadings")
fit_pi_metric <- partial_invariance(fit_metric,
pars = "loadings",
max_free = max_free,
keep_original = FALSE,
progress = progress)
fit_metric <- list(metric = fit_metric)
} else {
fit_metric <- NULL
fit_pi_metric <- NULL
}
if (scalar) {
fit_scalar <- lavaan::update(cfa_out,
group.equal = c("loadings", "intercepts"))
fit_pi_scalar <- partial_invariance(fit_scalar,
pars = "intercepts",
max_free = max_free,
keep_original = FALSE,
progress = progress)
fit_scalar <- list(scalar = fit_scalar)
} else {
fit_scalar <- NULL
fit_pi_scalar <- NULL
}
out <- c(list(config = cfa_out),
fit_metric,
fit_scalar,
fit_pi_metric,
fit_pi_scalar)
out
}
#' @noRd
partial_invariance <- function(cfa_out,
pars = c("loadings", "intercepts"),
max_free = 1,
keep_original = TRUE,
progress = TRUE) {
if (max_free < 1) {
return(NULL)
}
pars <- match.arg(pars)
opt <- lavaan::lavInspect(cfa_out, "options")
if ((pars == "loadings") &&
opt$group.equal != "loadings") {
stop("To use pars = 'loadings', group.equal must be 'loadings'.")
}
chk <- opt$group.equal
if ((pars == "intercepts") &&
!setequal(opt$group.equal, c("loadings", "intercepts"))) {
stop("To use pars = 'intercepts', group.equal must be c('loadings', 'intecepts').")
}
free_pars <- switch(pars,
loadings = get_free_loadings(cfa_out),
intercepts = get_free_intercepts(cfa_out))
to_release <- lapply(seq_len(max_free), function(xx) {
utils::combn(free_pars, m = xx, simplify = FALSE)
})
to_release <- unlist(to_release, recursive = FALSE)
gp_eq <- switch(pars,
loadings = c("loadings"),
intercepts = c("loadings", "intercepts"))
pt <- lavaan::parameterTable(cfa_out)
fit_i <- function(x) {
sem_out_update <- lavaan::update(cfa_out,
group.equal = gp_eq,
group.partial = x)
sem_out_update
}
if (progress) {
cat("\nFitting", length(to_release),
"partial",
switch(pars, loadings = "metric",
intercepts = "scalar"),
"invariance models:\n")
pt_out <- pbapply::pblapply(to_release, fit_i)
} else {
pt_out <- lapply(to_release, fit_i)
}
pt_out_names <- sapply(to_release, paste, collapse = ";")
names(pt_out) <- pt_out_names
if (keep_original) {
if (pars == "loadings") {
pt_out$metric <- cfa_out
}
if (pars == "intercepts") {
pt_out$scalar <- cfa_out
}
}
pt_out
}
#' @noRd
get_free_loadings <- function(cfa_out) {
pt <- lavaan::parameterTable(cfa_out)
loadings <- pt[(pt$op == "=~") & (pt$free != 0) & (pt$group == 1), ]
out <- sapply(split(loadings, seq_len(nrow(loadings))), function(xx) {
paste0(xx$lhs, xx$op, xx$rhs)
})
out
}
#' @noRd
get_free_intercepts <- function(cfa_out) {
pt <- lavaan::parameterTable(cfa_out)
intercepts <- pt[(pt$op == "~1") & (pt$free != 0) & (pt$group == 1), ]
out <- sapply(split(intercepts, seq_len(nrow(intercepts))), function(xx) {
paste0(xx$lhs, xx$op)
})
out
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.