Nothing
# ESEM-within-CFA / B-ESEM-within-CFA: exactness of the re-expression ---------
# HolzingerSwineford1939 (9 items, 3 factors) plus a quartile-cut copy for the
# ordered path. The CFA/ESEM/B-ESEM trio is slow, so each estimator's trio is
# fitted once and shared across tests.
skip_on_cran()
hs <- lavaan::HolzingerSwineford1939
fl <- list(Vis = c("x1", "x2", "x3"), Tex = c("x4", "x5", "x6"), Spd = c("x7", "x8", "x9"))
hs_ord <- hs
for (it in paste0("x", 1:9))
hs_ord[[it]] <- cut(hs[[it]], quantile(hs[[it]], c(0, .25, .5, .75, 1)),
include.lowest = TRUE, labels = FALSE)
quiet <- function(expr) suppressWarnings(suppressMessages(expr))
spec_c <- do.call(specify_model, c(fl, list(data = hs, ordered = FALSE, label = "HS")))
spec_o <- do.call(specify_model, c(fl, list(data = hs_ord, ordered = TRUE, label = "HS ord")))
res_c <- quiet(run_comparison(spec_c, mplus_folder = NULL, run_alignment = FALSE))
res_o <- quiet(run_comparison(spec_o, mplus_folder = NULL, run_alignment = FALSE))
k <- length(fl) + 1L # G + specific factors in the B-ESEM
fm <- function(fit, ordered) {
keys <- if (ordered) c("chisq.scaled", "df.scaled") else c("chisq", "df")
unname(lavaan::fitMeasures(fit, keys))
}
fvar <- function(fit, factors) {
pe <- lavaan::parameterEstimates(fit)
pe[pe$op == "~~" & pe$lhs == pe$rhs & pe$lhs %in% factors, c("lhs", "est", "se")]
}
fcov <- function(fit, factors) {
pe <- lavaan::parameterEstimates(fit)
pe[pe$op == "~~" & pe$lhs != pe$rhs & pe$lhs %in% factors & pe$rhs %in% factors, ]
}
ref_loading_free <- function(fit, factor, item) {
pt <- lavaan::parameterTable(fit)
pt$free[pt$op == "=~" & pt$lhs == factor & pt$rhs == item] != 0L
}
# -- Plain EWC: both identification modes must return the ESEM's chi2 and df --
test_that("fixed-variance EWC re-expresses the ESEM exactly (guard)", {
for (est in list(list(res_c, spec_c, FALSE), list(res_o, spec_o, TRUE))) {
ewc <- quiet(fit_ewc(est[[1]]$fit_esem, est[[2]], var_fixed = TRUE))
expect_equal(fm(ewc$lavaan_fit, est[[3]]), fm(est[[1]]$fit_esem$lavaan_fit, est[[3]]),
tolerance = 1e-4)
}
})
test_that("free-variance EWC re-expresses a continuous ESEM exactly", {
ewc <- quiet(fit_ewc(res_c$fit_esem, spec_c, var_fixed = FALSE))
expect_equal(fm(ewc$lavaan_fit, FALSE), fm(res_c$fit_esem$lavaan_fit, FALSE), tolerance = 1e-4)
v <- fvar(ewc$lavaan_fit, names(fl))
expect_true(all(v$se > 0)) # variances estimated, not fixed
expect_equal(v$est, rep(1, 3), tolerance = 0.01) # same metric as the std.lv ESEM
for (f in names(fl)) expect_false(ref_loading_free(ewc$lavaan_fit, f, ewc$referents[[f]]))
})
test_that("free-variance EWC re-expresses an ordered ESEM exactly, with unit variances", {
ewc <- quiet(fit_ewc(res_o$fit_esem, spec_o, var_fixed = FALSE))
expect_equal(fm(ewc$lavaan_fit, TRUE), fm(res_o$fit_esem$lavaan_fit, TRUE), tolerance = 1e-4)
v <- fvar(ewc$lavaan_fit, names(fl))
expect_true(all(v$se > 0))
expect_equal(v$est, rep(1, 3), tolerance = 0.01)
})
# -- B-EWC: df is the B-ESEM df + k(k-1)/2 by construction (guard) -------------
test_that("B-EWC has k(k-1)/2 more df than the B-ESEM and the same chi-square under ML", {
fb <- res_c$fit_besem
b <- quiet(fit_ewc(fb, spec_c))
expect_equal(fm(b$lavaan_fit, FALSE)[2], fm(fb$lavaan_fit, FALSE)[2] + k * (k - 1) / 2)
expect_equal(fm(b$lavaan_fit, FALSE)[1], fm(fb$lavaan_fit, FALSE)[1], tolerance = 1e-3)
cv <- fcov(b$lavaan_fit, c(fb$g_name, names(fl)))
expect_equal(nrow(cv), k * (k - 1) / 2)
expect_true(all(cv$est == 0 & cv$se == 0))
})
# -- B-EWC: var_fixed = FALSE must be honoured, not silently ignored ----------
test_that("B-EWC honours var_fixed = FALSE", {
fb <- res_c$fit_besem
facs <- c(fb$g_name, names(fl))
b1 <- quiet(fit_ewc(fb, spec_c))
b2 <- quiet(fit_ewc(fb, spec_c, var_fixed = FALSE))
expect_false(b2$var_fixed)
expect_false(identical(b1$syntax, b2$syntax))
v <- fvar(b2$lavaan_fit, facs)
expect_true(all(v$se > 0))
expect_equal(v$est, rep(1, k), tolerance = 0.01)
cv <- fcov(b2$lavaan_fit, facs)
expect_true(all(cv$est == 0 & cv$se == 0)) # orthogonality kept
expect_equal(fm(b2$lavaan_fit, FALSE), fm(b1$lavaan_fit, FALSE), tolerance = 1e-4) # same model
for (f in facs) expect_false(ref_loading_free(b2$lavaan_fit, f, b2$referents[[f]]))
})
# -- Ordered B-EWC: report the B-EWC's own fit, not the source B-ESEM's -------
test_that("ordered B-EWC reports its own df rather than the source B-ESEM's", {
fb <- res_o$fit_besem
b <- quiet(fit_ewc(fb, spec_o))
own_df <- fb$wlsmv_stats$df + k * (k - 1) / 2
expect_null(b$wlsmv_stats)
expect_equal(fm(b$lavaan_fit, TRUE)[2], own_df)
expect_output(quiet(print(b)), sprintf("X2\\(%d\\)", own_df))
})
# -- WLSMV SRMR: EWC must report the Mplus-denominator SRMR, like the pipeline
test_that("ordered EWC reports the same SRMR as the ESEM it re-expresses", {
ewc <- quiet(fit_ewc(res_o$fit_esem, spec_o))
ct <- compare_ewc(res_o, ewc)
srmr_esem <- as.numeric(ct$ESEM_R[ct$Index == "SRMR"])
expect_equal(ct$EWC_varfix[ct$Index == "SRMR"], srmr_esem, tolerance = 1e-3)
expect_output(quiet(print(ewc)), sprintf("SRMR = %.3f", srmr_esem))
})
# -- MLR: EWC must report the robust (scaled) fit that the pipeline and Mplus report
test_that("continuous EWC reports the robust MLR fit, like the pipeline", {
ewc <- quiet(fit_ewc(res_c$fit_esem, spec_c))
ct <- compare_ewc(res_c, ewc)
for (i in c("CFI", "TLI", "RMSEA", "SRMR", "X2"))
expect_equal(ct$EWC_varfix[ct$Index == i], as.numeric(ct$ESEM_R[ct$Index == i]),
tolerance = 1e-3, label = i)
x2 <- as.numeric(ct$ESEM_R[ct$Index == "X2"]); df <- as.numeric(ct$ESEM_R[ct$Index == "df"])
expect_output(quiet(print(ewc)), sprintf("X2\\(%d\\) = %.3f", df, x2))
})
test_that("continuous ESEM and B-ESEM print the robust MLR chi-square the pipeline reports", {
ct <- res_c$comparison_table
x2 <- function(col) as.numeric(ct[[col]][ct$Index == "X2"])
expect_output(quiet(print(res_c$fit_esem)), sprintf("= %.3f", x2("ESEM_R")), fixed = TRUE)
expect_output(quiet(print(res_c$fit_besem)), sprintf("= %.3f", x2("BESEM_R")), fixed = TRUE)
})
# -- Custom ordered B-ESEM: SRMR by the Mplus definition (Asparouhov & Muthen
# 2018): polychoric residuals over n_pairs + total number of categories
# (thresholds are saturated here, so their residuals are 0).
test_that("ordered B-ESEM (custom DWLS path) reports the Mplus-definition SRMR", {
fb <- res_o$fit_besem
L <- as.matrix(fb$rotated_loadings)
Phi <- if (is.null(fb$factor_correlations)) diag(ncol(L)) else as.matrix(fb$factor_correlations)
R <- as.matrix(fb$polychoric)[rownames(L), rownames(L)]
imp <- L %*% Phi %*% t(L)
lt <- lower.tri(R)
n_cat <- sum(vapply(hs_ord[, rownames(L)], function(v) length(unique(v)), 1L))
srmr_mplus <- sqrt(sum((R[lt] - imp[lt])^2) / (sum(lt) + n_cat))
expect_equal(fb$wlsmv_stats$srmr, srmr_mplus, tolerance = 1e-6)
})
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.