| Title: | (Bifactor) ESEM with Continuous (MLR) or Ordinal (WLSMV) Data |
|---|---|
| Description: | Fits bifactor exploratory structural equation models (B-ESEM), together with standard exploratory structural equation modeling (ESEM) and confirmatory factor analysis (CFA), for continuous and ordinal data. Continuous models use 'lavaan' native efa() blocks with robust maximum likelihood (MLR) estimation. Ordinal ESEM defaults to the 'lavaan' weighted least squares mean- and variance-adjusted (WLSMV) estimator; ordinal B-ESEM uses a custom diagonally weighted least squares (DWLS) path with polychoric correlations from 'lavaan', rotation-delta standard errors via 'numDeriv', and a mean- and variance-adjusted chi-square. Target, geomin, and oblimin rotations use 'GPArotation'; the bifactor ESEM approach follows Morin, Arens and Marsh (2016) <doi:10.1080/10705511.2014.961800>. Additional features include multi-group measurement invariance (configural through strict, with partial invariance), ESEM-within-CFA conversion, McDonald's omega reliability suite, and the Mehrvarz and Rouder (2026) <doi:10.31234/osf.io/95enc_v3> alignment ratio check for independent cluster model confirmatory factor analysis (ICM-CFA) misspecification. An optional 'MplusAutomation' interface allows side-by-side comparison with 'Mplus' output. |
| Authors: | Leon T. De Beer [aut, cre] |
| Maintainer: | Leon T. De Beer <[email protected]> |
| License: | AGPL-3 |
| Version: | 0.6.2 |
| Built: | 2026-11-02 07:57:25 UTC |
| Source: | https://github.com/cran/bifactory |
Rotates a fitted solution's standardized loadings by an orthogonal Q so they match a reference matrix (Procrustes) or a deterministic canonical orientation. Useful when comparing bifactor solutions across software, or when you want a reproducible orientation across reruns.
align_loadings( x, target = "canonical", level = "configural", se_method = c("approx", "none") )align_loadings( x, target = "canonical", level = "configural", se_method = c("approx", "none") )
x |
An |
target |
One of:
|
level |
Only used for |
se_method |
|
Multi-group fits and esem_invariance objects are aligned per group.
Item communalities (), reliability indices (omega,
ECV), and model fit are invariant under orthogonal rotation, so alignment
affects only the per-loading partition, not substantive conclusions.
An object of class aligned_loadings: a list with one
element per group, each containing
aligned standardized loading matrix (items x factors)
aligned SEs (matching shape) or NULL
the orthogonal rotation matrix applied
max / mean abs residual vs target (only when an external reference was supplied)
The attribute "method" is "canonical", "procrustes"
or "transfer".
data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "school" ) inv <- esem_invariance(spec) # Canonical orientation (no external reference, deterministic) aligned <- align_loadings(inv, target = "canonical", level = "configural") print(aligned) # Within-fit: align all groups to group 1's orientation aligned <- align_loadings(inv, target = "group1", level = "configural") ## Not run: # Align to Mplus output (requires a Mplus .out file + MplusAutomation) mp <- MplusAutomation::readModels("besem_inv_configural.out") tgt <- extract_mplus_loadings(mp) aligned <- align_loadings(inv, target = tgt, level = "configural") ## End(Not run)data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "school" ) inv <- esem_invariance(spec) # Canonical orientation (no external reference, deterministic) aligned <- align_loadings(inv, target = "canonical", level = "configural") print(aligned) # Within-fit: align all groups to group 1's orientation aligned <- align_loadings(inv, target = "group1", level = "configural") ## Not run: # Align to Mplus output (requires a Mplus .out file + MplusAutomation) mp <- MplusAutomation::readModels("besem_inv_configural.out") tgt <- extract_mplus_loadings(mp) aligned <- align_loadings(inv, target = tgt, level = "configural") ## End(Not run)
Tests whether a standard CFA (ICM-CFA) is appropriately specified by computing alignment ratios from the manifest correlation matrix. Based on Mehrvarz & Rouder (2026), who prove that in a correctly specified ICM-CFA, alignment ratios must be invariant across all admissible item quadruples, with the common value equal to the squared latent correlation phi^2.
alignment_check( data, clusters, cfa_fit = NULL, is_cor = FALSE, min_within_r = 0.05, log_sd_thresholds = c(slight = 0.64, moderate = 1.6) )alignment_check( data, clusters, cfa_fit = NULL, is_cor = FALSE, min_within_r = 0.05, log_sd_thresholds = c(slight = 0.64, moderate = 1.6) )
data |
A |
clusters |
A named character vector mapping item names to factor names.
Example: |
cfa_fit |
Optional. A fitted |
is_cor |
Logical. Is |
min_within_r |
Numeric. Alignment ratios whose denominator contains a
within-cluster correlation below this value are excluded (near-zero
within-cluster correlations make ratios numerically unstable). Default
|
log_sd_thresholds |
Named numeric vector of log-scale SD cutoffs for
the slight / moderate / high misalignment classification. Defaults to the
empirical terciles reported by Mehrvarz & Rouder (2026, p.24):
|
is the primary diagnostic. The log scale is
natural because alignment ratios are multiplicative objects. Mehrvarz &
Rouder (2026, p.24) report empirical terciles of from their bifactor
misalignment simulation:
s < 0.64 – Slight misalignment. ICM-CFA is approximately consistent with the data; latent correlations are roughly trustworthy.
s 0.64-1.6 – Moderate misalignment. ICM-CFA is partially misspecified; interpret latent correlations with caution.
s > 1.6 – High misalignment (top tercile of the paper's simulation, up to ~4.4). ICM-CFA is substantially misspecified and latent correlations are likely inflated. Consider ESEM.
These cutpoints are the 33rd and 66th percentiles of observed by
Mehrvarz & Rouder across a broad range of simulated loading configurations
(with ), not hard decision
boundaries; adjust log_sd_thresholds if your application calls for
stricter or looser cutoffs.
Misassignment: items are assigned to the wrong cluster. Mehrvarz & Rouder prove this necessarily inflates phi. The alignment ratios split into three plateaus at phi^2, 1, and 1/phi^2.
Misalignment: cluster assignments are correct but cross-loadings exist (or the proportionality constraint is violated in a bifactor-like DGP). The ratios are dispersed rather than invariant.
Mehrvarz & Rouder (2026, p.14) count one alignment ratio per admissible
index quadruple: . Because each
unordered quadruple admits two
between-cluster matchings ( and
), both of which equal under
correctly specified ICM-CFA but diverge under misspecification, this
function records both matchings per quadruple – yielding ratios in all_ratios – to maximise the
information available for the dispersion diagnostic.
An object of class "alignment_check" (a list) containing:
pair_resultsA data.frame with one row per factor
pair, giving: geometric mean of alignment ratios, log-scale SD, implied phi,
CFA-estimated phi (if supplied), inflation percentage, and verdict.
all_ratiosA named list of raw alignment ratio vectors, one element per factor pair.
recommendationCharacter string: overall recommendation.
cor_matrixThe manifest correlation matrix used.
clustersResolved cluster assignments (named list).
callThe matched call.
For any two items in cluster A and two items
in cluster B, the alignment ratio is:
Under a correctly specified ICM-CFA, all such ratios equal phi^2 and lie in
(Mehrvarz & Rouder, 2026, Eq. 5). Dispersion in the ratios –
measured by their log-scale standard deviation
– signals misspecification: either misassignment (items in the wrong
cluster) or misalignment (cross-loadings exist but are fixed to zero).
Mehrvarz, M., & Rouder, J. N. (2026). The geometry and brittleness of latent correlations in confirmatory factor analysis.
esem for the recommended follow-up under moderate or
high misalignment.
data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Vector form of cluster assignment clusters <- c( x1 = "Visual", x2 = "Visual", x3 = "Visual", x4 = "Textual", x5 = "Textual", x6 = "Textual", x7 = "Speed", x8 = "Speed", x9 = "Speed" ) # Run alignment check (data only) check <- alignment_check(d, clusters) print(check) # Run with a fitted CFA to quantify inflation cfa_model <- " Visual =~ x1 + x2 + x3 Textual =~ x4 + x5 + x6 Speed =~ x7 + x8 + x9 " cfa_fit <- lavaan::cfa(cfa_model, data = d, std.lv = TRUE) check <- alignment_check(d, clusters, cfa_fit = cfa_fit) print(check)data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Vector form of cluster assignment clusters <- c( x1 = "Visual", x2 = "Visual", x3 = "Visual", x4 = "Textual", x5 = "Textual", x6 = "Textual", x7 = "Speed", x8 = "Speed", x9 = "Speed" ) # Run alignment check (data only) check <- alignment_check(d, clusters) print(check) # Run with a fitted CFA to quantify inflation cfa_model <- " Visual =~ x1 + x2 + x3 Textual =~ x4 + x5 + x6 Speed =~ x7 + x8 + x9 " cfa_fit <- lavaan::cfa(cfa_model, data = d, std.lv = TRUE) check <- alignment_check(d, clusters, cfa_fit = cfa_fit) print(check)
Fits a Bifactor ESEM model – a general factor (G) loading on all
indicators plus domain-specific factors each targeting a subset of
indicators. All factors are orthogonal (uncorrelated), matching the
Mplus B-ESEM specification on continuous data via lavaan's efa()
block. With ordered indicators, the call routes to
besem_ordered (default custom DWLS/WLSMV path; see
?besem_ordered).
besem( data, specific_factors, indicators = NULL, g_name = "G", estimator = "MLR", std.lv = TRUE, ordered = NULL, group = NULL, group_equal = NULL, missing = "listwise", n_starts = 30L, ... )besem( data, specific_factors, indicators = NULL, g_name = "G", estimator = "MLR", std.lv = TRUE, ordered = NULL, group = NULL, group_equal = NULL, missing = "listwise", n_starts = 30L, ... )
data |
A |
specific_factors |
A named list mapping specific factor names to
their primary indicator names. The general factor G is added
automatically.
Example: |
indicators |
Optional character vector of all indicator names. If
|
g_name |
Character. Name for the general factor. Default |
estimator |
Character. Default |
std.lv |
Logical. Fix factor variances to 1. Default |
ordered |
Character vector of ordinal item names.
When non- |
group |
Character. Grouping variable for multi-group B-ESEM. |
group_equal |
Character vector of lavaan equality constraints. With
|
missing |
Character. Missing data handling for continuous B-ESEM.
Default |
n_starts |
Integer. Number of random orthogonal starting matrices for
the target rotation (forwarded as |
... |
Additional arguments passed to |
| ESEM | B-ESEM | |
| Factor structure | k oblique specific factors | 1 general + k orthogonal specific |
| Factor correlations | Freely estimated | All fixed to zero (orthogonal) |
| Cross-loadings | Estimated via rotation | Estimated via orthogonal target rotation |
| G factor | None | Loads freely on all items |
| Rotation | Oblique target/geomin | Orthogonal target |
B-ESEM uses orthogonal target rotation ("targetT" in lavaan),
which constrains all factors to be uncorrelated. This means:
G is uncorrelated with EX, MD, CI
EX, MD, CI are uncorrelated with each other
This matches Mplus ROTATION = TARGET (orthogonal).
G loadings: variance shared across all items regardless of domain
Specific loadings: domain-specific variance after accounting for G
omega_h (omega hierarchical): reliability of G (use psych::omega())
omega_s (omega specific): reliability of each specific factor
G EX MD CI
y1 1 1 0 0 <- G free on all; EX primary; MD/CI targeted to 0
y2 1 1 0 0
y6 1 0 1 0 <- MD item
y9 1 0 0 1 <- CI item
An object of class c("besem_fit", "esem_fit") with the
same structure as esem, plus:
g_nameName of the general factor.
specific_factorsNamed list of specific factor assignments.
Mplus continuous / ordered target syntax: ROTATION = TARGET (orthogonal); MODEL: G BY batEX1-batCI5 (*1); EX BY batEX1~1 ... batMD1~0 ... (*1); MD BY batEX1~0 ... batMD1~1 ... (*1); CI BY batEX1~0 ... batCI1~1 ... (*1);
esem and besem use lavaan::cfa() with a
native efa() block (integrated MLR estimation and rotation).
esem(ordered = ...) calls esem_ordered. Default
method = "lavaan" (lavaan WLSMV, efa() block, post-hoc rotation).
method = "rotation" uses a custom DWLS pipeline (polychoric correlations
+ GPArotation).
besem(ordered = ...) calls besem_ordered. Default
method = "rotation" (custom DWLS/WLSMV + orthogonal targetT; this
is what run_comparison uses for ordered data). method = "set-esem"
fits a lavaan WLSMV bifactor CFA with non-primary specific loadings fixed at
zero; it does not match Mplus B-ESEM loadings.
esem_invariance uses lavaan multi-group efa() models plus
explicit syntax patches for ordered B-ESEM. See that help page for scope limits.
Defaults differ by entry point; pass missing explicitly when fitting ESEM and
B-ESEM separately on the same ordered dataset.
specify_model / run_comparison: "pairwise"
for ordered data, "listwise" for continuous.
esem_ordered: "pairwise".
besem_ordered: "listwise" (pipeline still passes
missing from the spec when used via run_comparison).
For besem_ordered(method = "rotation") and
esem_ordered(method = "rotation"), $lavaan_fit is often an
auxiliary one-factor WLSMV CFA used only to extract DWLS weight matrices—not
the fitted ESEM/B-ESEM model. Use std_loadings, parameters,
and fitMeasures(x) on the esem_fit wrapper;
fitMeasures(x) reads wlsmv_stats when present.
Do not interpret summary(x$lavaan_fit), modindices(x), or
coef(x) as the rotated solution unless you know the fit used the lavaan
efa() path (method = "lavaan" or method = "set-esem" for B-ESEM).
esem for standard oblique ESEM,
besem_ordered for ordinal B-ESEM,
make_bifactor_target for the target matrix,
generate_mplus_besem_syntax for Mplus comparison.
data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] fit_b <- besem( data = d, specific_factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), n_starts = 5L ) summary(fit_b, fit.measures = TRUE, standardized = TRUE) std_loadings(fit_b) # rows = items, cols = G + specific factors factor_correlations(fit_b) # should all be ~0 (orthogonal) # Compare B-ESEM vs standard ESEM fit_esem <- esem(d, nfactors = 3) lavaan::fitMeasures(lavaan_fit(fit_b), c("cfi", "rmsea", "aic")) lavaan::fitMeasures(lavaan_fit(fit_esem), c("cfi", "rmsea", "aic"))data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] fit_b <- besem( data = d, specific_factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), n_starts = 5L ) summary(fit_b, fit.measures = TRUE, standardized = TRUE) std_loadings(fit_b) # rows = items, cols = G + specific factors factor_correlations(fit_b) # should all be ~0 (orthogonal) # Compare B-ESEM vs standard ESEM fit_esem <- esem(d, nfactors = 3) lavaan::fitMeasures(lavaan_fit(fit_b), c("cfi", "rmsea", "aic")) lavaan::fitMeasures(lavaan_fit(fit_esem), c("cfi", "rmsea", "aic"))
Fits B-ESEM on ordinal indicators. Called from besem
when ordered is set. For Mplus-aligned loadings and fit indices use
method = "rotation" (default; also used by run_comparison).
besem_ordered( data, specific_factors, indicators = NULL, g_name = "G", method = c("rotation", "set-esem"), n_starts = 30L, r_obs_override = NULL, group = NULL, group_equal = NULL, missing = "listwise", std.lv = TRUE, ... )besem_ordered( data, specific_factors, indicators = NULL, g_name = "G", method = c("rotation", "set-esem"), n_starts = 30L, r_obs_override = NULL, group = NULL, group_equal = NULL, missing = "listwise", std.lv = TRUE, ... )
data |
A |
specific_factors |
Named list of specific factor -> item assignments. |
indicators |
Character vector of all indicator names. If |
g_name |
Character. General factor name. Default |
method |
Character. |
n_starts |
Integer. Random rotation starts. Default |
r_obs_override |
Optional observed correlation matrix to fit instead of lavaan's polychoric correlations. The weight matrix still comes from lavaan, so a matrix that differs from lavaan's by more than 0.001 triggers a warning. |
group |
Character. Grouping variable for multi-group models. |
group_equal |
Character vector of lavaan equality constraints. |
missing |
Character. Missing data handling. Default |
std.lv |
Logical. Default |
... |
Additional arguments passed to |
An object of class c("besem_fit_ordered","besem_fit","esem_fit").
For method = "rotation", lavaan_fit is auxiliary (one-factor CFA);
use std_rotated_loadings, wlsmv_stats, std_loadings,
and fitMeasures(x); communalities holds
each item's communality, and a value above 1 (a Heywood case, negative
residual variance) gives a warning. For method = "set-esem",
lavaan_fit is the fitted bifactor CFA.
"set-esem" vs "rotation"
rotation estimates cross-loadings (targeted toward zero), then rotates—
same estimand as Mplus B-ESEM WLSMV. set-esem fixes non-primary
specific loadings at zero in lavaan; G and primary loadings are re-optimized
under that harder constraint, so loadings and fit differ from Mplus.
esem and besem use lavaan::cfa() with a
native efa() block (integrated MLR estimation and rotation).
esem(ordered = ...) calls esem_ordered. Default
method = "lavaan" (lavaan WLSMV, efa() block, post-hoc rotation).
method = "rotation" uses a custom DWLS pipeline (polychoric correlations
+ GPArotation).
besem(ordered = ...) calls besem_ordered. Default
method = "rotation" (custom DWLS/WLSMV + orthogonal targetT; this
is what run_comparison uses for ordered data). method = "set-esem"
fits a lavaan WLSMV bifactor CFA with non-primary specific loadings fixed at
zero; it does not match Mplus B-ESEM loadings.
esem_invariance uses lavaan multi-group efa() models plus
explicit syntax patches for ordered B-ESEM. See that help page for scope limits.
Defaults differ by entry point; pass missing explicitly when fitting ESEM and
B-ESEM separately on the same ordered dataset.
specify_model / run_comparison: "pairwise"
for ordered data, "listwise" for continuous.
esem_ordered: "pairwise".
besem_ordered: "listwise" (pipeline still passes
missing from the spec when used via run_comparison).
For besem_ordered(method = "rotation") and
esem_ordered(method = "rotation"), $lavaan_fit is often an
auxiliary one-factor WLSMV CFA used only to extract DWLS weight matrices—not
the fitted ESEM/B-ESEM model. Use std_loadings, parameters,
and fitMeasures(x) on the esem_fit wrapper;
fitMeasures(x) reads wlsmv_stats when present.
Do not interpret summary(x$lavaan_fit), modindices(x), or
coef(x) as the rotated solution unless you know the fit used the lavaan
efa() path (method = "lavaan" or method = "set-esem" for B-ESEM).
esem_ordered, besem,
run_comparison
data("HolzingerSwineford1939", package = "lavaan") # Derive ordered (5-category Likert) versions of the 9 continuous items items <- paste0("x", 1:9) ord <- as.data.frame(lapply(HolzingerSwineford1939[, items], function(v) { as.integer(cut(v, breaks = quantile(v, probs = seq(0, 1, 0.2)), include.lowest = TRUE)) })) names(ord) <- items fit_b_ord <- besem_ordered( data = ord, specific_factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), n_starts = 5L ) summary(fit_b_ord, fit.measures = TRUE, standardized = TRUE)data("HolzingerSwineford1939", package = "lavaan") # Derive ordered (5-category Likert) versions of the 9 continuous items items <- paste0("x", 1:9) ord <- as.data.frame(lapply(HolzingerSwineford1939[, items], function(v) { as.integer(cut(v, breaks = quantile(v, probs = seq(0, 1, 0.2)), include.lowest = TRUE)) })) names(ord) <- items fit_b_ord <- besem_ordered( data = ord, specific_factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), n_starts = 5L ) summary(fit_b_ord, fit.measures = TRUE, standardized = TRUE)
Opens the shipped analysis template in your editor, or copies it to a
destination of your choice. The template walks through the full bifactory
pipeline (CFA / ESEM / B-ESEM comparison, reliability, ESEM-within-CFA,
factor scores, and optional multi-group invariance) using
psych::bfi as the demo dataset.
bifactory_template(to = NULL, overwrite = FALSE)bifactory_template(to = NULL, overwrite = FALSE)
to |
Optional file path. If supplied, the template is copied there
(with overwrite protection). If |
overwrite |
Logical. Overwrite an existing file at |
Invisibly returns the path to the template (or the destination
when to is supplied).
# Find the template path system.file("templates", "template.R", package = "bifactory") # Copy the template to a file (here a temporary one) dest <- file.path(tempdir(), "my_analysis.R") bifactory_template(to = dest, overwrite = TRUE) ## Not run: # Open the template directly in your editor (interactive session only) bifactory_template() ## End(Not run)# Find the template path system.file("templates", "template.R", package = "bifactory") # Copy the template to a file (here a temporary one) dest <- file.path(tempdir(), "my_analysis.R") bifactory_template(to = dest, overwrite = TRUE) ## Not run: # Open the template directly in your editor (interactive session only) bifactory_template() ## End(Not run)
Extracts the per-group contribution to the WLSMV (or ML) chi-square at each
invariance level of an esem_invariance object. Optionally compares
against Mplus's Chi-Square Contribution From Each Group table, which
is useful for pinpointing whether a basin difference between R and Mplus is
driven by a single group or distributed across all of them.
chisq_decomp(x, mplus_dir = NULL, levels = NULL)chisq_decomp(x, mplus_dir = NULL, levels = NULL)
x |
An |
mplus_dir |
Optional directory containing Mplus output files named
|
levels |
Character vector of levels to include. Defaults to all levels
present in |
lavaan's stat.group is the raw (unscaled) per-group chi-square; Mplus
prints the scaled per-group contribution that sums to the scaled
total. To compare on the same scale, R per-group values are rescaled by the
global ratio chisq.scaled / chisq from fitMeasures.
A data frame of class "chisq_decomp" with columns
level, group, chisq_R (rescaled to scaled scale), and
when mplus_dir is supplied, mplus_group, chisq_M,
delta.
data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "school" ) inv <- esem_invariance(spec) chisq_decomp(inv) ## Not run: # Supply a folder of Mplus .out files to add side-by-side deltas. chisq_decomp(inv, mplus_dir = "validation/_bfi_g4_mplus_inv") ## End(Not run)data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "school" ) inv <- esem_invariance(spec) chisq_decomp(inv) ## Not run: # Supply a folder of Mplus .out files to add side-by-side deltas. chisq_decomp(inv, mplus_dir = "validation/_bfi_g4_mplus_inv") ## End(Not run)
Forwards to lavaan on lavaan_fit(x). On custom WLSMV rotation paths,
use parameters or std_loadings instead; see
The lavaan_fit slot on custom WLSMV fits.
## S3 method for class 'esem_fit' coef(object, standardized = FALSE, ...)## S3 method for class 'esem_fit' coef(object, standardized = FALSE, ...)
object |
An |
standardized |
Logical. Return standardized estimates? Default |
... |
Passed to |
A data.frame of parameter estimates from
lavaan::parameterEstimates(), or the standardized solution from
lavaan::standardizedsolution() when standardized = TRUE.
Appends fit indices from a fit_ewc result to the pipeline
comparison table, producing a unified data frame with CFA, ESEM, B-ESEM,
and EWC columns side by side.
compare_ewc(results, ewc)compare_ewc(results, ewc)
results |
An |
ewc |
An |
An object of class c("ewc_comparison","data.frame").
Print with print() for a formatted table.
Builds a long-format data frame with STDYX loadings, standard errors,
z-scores, p-values, and R Mplus differences for every loading
(primary and cross) across CFA, ESEM, and B-ESEM. Mplus columns are
NA when no Mplus results are present in the pipeline object.
compare_loadings(results)compare_loadings(results)
results |
An |
A data frame with columns:
modelCFA, ESEM, or BESEM.
factorFactor name (lowercase).
itemItem name (lowercase).
loading_type"primary" or "cross".
R_std, R_se, R_z, R_p
R estimates.
Mplus_std, Mplus_se, Mplus_z, Mplus_p
Mplus estimates (NA if unavailable).
diff_stdR Mplus standardised loading difference (NA if unavailable).
data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) results <- run_comparison(spec, n_starts = 5L) lc <- compare_loadings(results) head(lc) # Primary loadings only (Mplus_* columns are NA without a Mplus run) lc[lc$loading_type == "primary", ]data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) results <- run_comparison(spec, n_starts = 5L) lc <- compare_loadings(results) head(lc) # Primary loadings only (Mplus_* columns are NA without a Mplus run) lc[lc$loading_type == "primary", ]
Computes McDonald's omega reliability indices for all three models in a pipeline result, following the bifactor reporting framework recommended by Morin, Arens & Marsh (2016) and Rodriguez, Reise & Haviland (2016).
compute_indices(results)compute_indices(results)
results |
An |
For the total composite (all items):
omega_total – total reliability (rotation-invariant for
orthogonal models).
omega_H – hierarchical omega: G factor's contribution to
total-score reliability (B-ESEM only).
ECV – explained common variance: G's share of all common
variance (B-ESEM only).
H(G) – construct replicability of G (B-ESEM only).
For each subscale (items of specific factor ):
omega_S – specific factor's contribution to subscale
reliability: . G is not partialled out; its variance is
treated as an additional source of error (Morin, 2023; Morin et al.,
2020). omega_H(G|s) is the mirror image with the specific
factor treated as error.
omega_sub – total reliability of the subscale sum score
(G + specific factor combined).
omega_H_sub – G's contribution to subscale reliability.
ECV_s – G's share of common variance within the subscale.
H(Fs) – construct replicability of the specific factor.
Every composite sum uses absolute standardized loadings, the convention of
Morin, Arens and Marsh (2016): each item is scored in the direction of its
loading, so reverse-worded items and the orientation of a factor do not
change any index. Residual variances are taken from the model-implied
item variances (for correlated factors, one minus the diagonal of
).
An object of class "reliability_indices" – a named list with
elements cfa, esem, besem, each containing the
computed indices for that model, plus alpha (Cronbach's alpha per
subscale and for G). Pass to print() for a formatted table.
McDonald, R. P. (1999). Test theory: A unified treatment. Erlbaum.
Rodriguez, A., Reise, S. P., & Haviland, M. G. (2016). Evaluating bifactor models: Calculating and interpreting statistical indices. Psychological Methods, 21(2), 137-150.
Morin, A. J. S., Arens, A. K., & Marsh, H. W. (2016). A bifactor exploratory structural equation modeling framework for the identification of distinct sources of construct-relevant psychometric multidimensionality. Structural Equation Modeling, 23(1), 116-139.
Morin, A. J. S., Myers, N. D., & Lee, S. (2020). Modern factor analytic techniques: Bifactor models, exploratory structural equation modeling (ESEM) and bifactor-ESEM. In G. Tenenbaum & R. C. Eklund (Eds.), Handbook of Sport Psychology (4th ed., pp. 1044-1073). Wiley.
Morin, A. J. S. (2023). Exploratory structural equation modeling. In R. H. Hoyle (Ed.), Handbook of Structural Equation Modeling (2nd ed., pp. 503-524). Guilford.
data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) results <- run_comparison(spec, n_starts = 5L) indices <- compute_indices(results) print(indices)data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) results <- run_comparison(spec, n_starts = 5L) indices <- compute_indices(results) print(indices)
compute_indices insteadDeprecated: use compute_indices instead
compute_omega(results)compute_omega(results)
results |
An |
A reliability_indices object.
Fits ESEM using lavaan's native efa() block. For continuous
data (default), estimation and rotation are integrated in lavaan's ML
optimiser—the same single-step approach as Mplus (*1) syntax. For
ordered indicators, supply ordered and the call is routed to
esem_ordered (set-ESEM / WLSMV; not identical to continuous
esem()).
esem( data, nfactors, indicators = NULL, rotation = "geomin", target = NULL, estimator = "MLR", std.lv = TRUE, ordered = NULL, group = NULL, group_equal = NULL, missing = "listwise", factor_names = NULL, rotation_args = list(), heywood_fix = TRUE, ... )esem( data, nfactors, indicators = NULL, rotation = "geomin", target = NULL, estimator = "MLR", std.lv = TRUE, ordered = NULL, group = NULL, group_equal = NULL, missing = "listwise", factor_names = NULL, rotation_args = list(), heywood_fix = TRUE, ... )
data |
A |
nfactors |
Integer. Number of latent factors. |
indicators |
Character vector of item names to include. If |
rotation |
Character. Rotation criterion. Passed to
The full list of lavaan-supported rotations is in
|
target |
A numeric matrix (items x factors) for target rotation.
Create with |
estimator |
Character. lavaan estimator. Default |
std.lv |
Logical. Fix factor variances to 1 for identification?
Default |
ordered |
Character vector of ordinal item names.
When non- |
group |
Character. Name of a grouping variable in |
group_equal |
Character vector of lavaan parameter labels to constrain
equal across groups, e.g. |
missing |
Character. Missing data handling for continuous fits.
Default |
factor_names |
Optional character vector of length |
rotation_args |
Named list of extra arguments passed to the rotation
function (e.g. |
heywood_fix |
Logical. Retry rotation with Cholesky unrotation if a
standardised loading exceeds 1 (single-group continuous fits only).
Default |
... |
Additional arguments passed to |
| Mplus syntax | bifactory equivalent |
F1-F3 BY y1-y15 (*1); |
esem(data, nfactors = 3) |
(*1) with geomin (default) |
rotation = "geomin" |
(*1) with target |
rotation = "target", target = tgt |
GROUPING = g; |
group = "g" |
| Metric invariance | group_equal = "loadings" |
| Scalar invariance | group_equal = c("loadings", "intercepts")
|
Adding cross-loadings stepwise from modification indices is a different (exploratory CFA) approach and is not recommended: it capitalises on chance, inflates Type I error, and produces a different model on every dataset. ESEM instead estimates all cross-loadings simultaneously, with rotation acting as a mathematical penalty for complexity – giving a reproducible, theory-neutral solution.
An object of class "esem_fit" containing:
lavaan_fitThe lavaan fit object for continuous ESEM.
For ordered / custom WLSMV paths see esem_ordered and
The lavaan_fit slot on custom WLSMV fits.
syntaxThe lavaan model string that was estimated.
nfactorsNumber of factors.
rotationRotation method used.
factor_namesFactor names.
indicatorsItem names included in the model.
callThe matched call.
Many R implementations run EFA first, extract the loading matrix, then paste
it into a CFA as starting values. That is an approximation. bifactory
instead uses lavaan's efa() block syntax (available since
lavaan 0.6-12), which estimates the rotation and the SEM parameters
simultaneously – exactly as Mplus does with the (*1) syntax.
esem and besem use lavaan::cfa() with a
native efa() block (integrated MLR estimation and rotation).
esem(ordered = ...) calls esem_ordered. Default
method = "lavaan" (lavaan WLSMV, efa() block, post-hoc rotation).
method = "rotation" uses a custom DWLS pipeline (polychoric correlations
+ GPArotation).
besem(ordered = ...) calls besem_ordered. Default
method = "rotation" (custom DWLS/WLSMV + orthogonal targetT; this
is what run_comparison uses for ordered data). method = "set-esem"
fits a lavaan WLSMV bifactor CFA with non-primary specific loadings fixed at
zero; it does not match Mplus B-ESEM loadings.
esem_invariance uses lavaan multi-group efa() models plus
explicit syntax patches for ordered B-ESEM. See that help page for scope limits.
Defaults differ by entry point; pass missing explicitly when fitting ESEM and
B-ESEM separately on the same ordered dataset.
specify_model / run_comparison: "pairwise"
for ordered data, "listwise" for continuous.
esem_ordered: "pairwise".
besem_ordered: "listwise" (pipeline still passes
missing from the spec when used via run_comparison).
When heywood_fix = TRUE (default on single-group esem and
esem_ordered), std_loadings may reflect a post-hoc
rotation correction while lavaan_fit still holds the pre-correction lavaan
solution. Prefer std_loadings(x) for reported loadings in that case.
make_target for target matrices,
esem_ordered for ordinal data,
esem_compare for ESEM vs CFA comparison,
std_loadings for the standardised loading matrix.
data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Basic ESEM: 3 factors, geomin rotation (Mplus default) fit <- esem(d, nfactors = 3) round(std_loadings(fit), 2) lavaan::fitMeasures(lavaan_fit(fit), c("cfi", "tli", "rmsea", "srmr")) # Named factors fit2 <- esem(d, nfactors = 3, factor_names = c("Visual", "Textual", "Speed")) # Target rotation tgt <- make_target(list(Vis = 1:3, Txt = 4:6, Spd = 7:9), nitems = 9) fit3 <- esem(d, nfactors = 3, rotation = "target", target = tgt) # Multi-group configural ESEM fit_mg <- esem(HolzingerSwineford1939, nfactors = 3, indicators = paste0("x", 1:9), group = "sex")data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Basic ESEM: 3 factors, geomin rotation (Mplus default) fit <- esem(d, nfactors = 3) round(std_loadings(fit), 2) lavaan::fitMeasures(lavaan_fit(fit), c("cfi", "tli", "rmsea", "srmr")) # Named factors fit2 <- esem(d, nfactors = 3, factor_names = c("Visual", "Textual", "Speed")) # Target rotation tgt <- make_target(list(Vis = 1:3, Txt = 4:6, Spd = 7:9), nitems = 9) fit3 <- esem(d, nfactors = 3, rotation = "target", target = tgt) # Multi-group configural ESEM fit_mg <- esem(HolzingerSwineford1939, nfactors = 3, indicators = paste0("x", 1:9), group = "sex")
Fits a user-specified CFA model on the same data and compares it against an
esem_fit object using fit indices and, where applicable, a chi-square
difference test (when models are nested).
esem_compare(esem_model, cfa_model, data = NULL, estimator = NULL, ...)esem_compare(esem_model, cfa_model, data = NULL, estimator = NULL, ...)
esem_model |
An |
cfa_model |
A lavaan model string for the comparison CFA. All items must be the same as in the ESEM. |
data |
A |
estimator |
Estimator for the CFA model. Defaults to the same estimator
used in |
... |
Additional arguments passed to |
A list of class "esem_comparison" with:
fit_tableA data.frame of fit indices for both models.
esem_fitThe original esem_fit object.
cfa_fitThe fitted lavaan CFA object.
lavtestOutput of lavaan::lavTestLRT() if models are nested, else NULL.
data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Fit ESEM esem_result <- esem(d, nfactors = 3) # Define comparison CFA (no cross-loadings) cfa_model <- " Visual =~ x1 + x2 + x3 Textual =~ x4 + x5 + x6 Speed =~ x7 + x8 + x9 " comparison <- esem_compare(esem_result, cfa_model, data = d) print(comparison)data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Fit ESEM esem_result <- esem(d, nfactors = 3) # Define comparison CFA (no cross-loadings) cfa_model <- " Visual =~ x1 + x2 + x3 Textual =~ x4 + x5 + x6 Speed =~ x7 + x8 + x9 " comparison <- esem_compare(esem_result, cfa_model, data = d) print(comparison)
Tests configural, weak (metric), strong (scalar), and strict invariance for
an ESEM model across groups, optionally followed by latent
variance/covariance and latent mean invariance (through). Returns a
formatted table of fit indices and chi-square difference tests, analogous
to Mplus's multi-group output.
esem_invariance( spec, model = c("esem", "besem"), missing = NULL, verbose = TRUE, through = c("strict", "varcov", "means"), cores = 2L, ... )esem_invariance( spec, model = c("esem", "besem"), missing = NULL, verbose = TRUE, through = c("strict", "varcov", "means"), cores = 2L, ... )
spec |
An |
model |
Character. Which model type to test: |
missing |
Character. Missing data handling passed to |
verbose |
Logical. Print progress messages. Default |
through |
Character. Last level of the sequence to fit.
|
cores |
Integer. Worker processes for the two starting-value fits of
each level (lavaan's default start and |
... |
Additional arguments passed to the underlying fit function. Do not
pass |
An object of class "esem_invariance" containing:
tableData frame with fit indices and D-statistics.
print() renders it formatted.
modelsNamed list of fit objects, one per fitted level:
configural, weak, strong, strict, and, with
through, varcov and means.
NULL entries indicate a model that failed to fit.
lrtNamed list of lavTestLRT() outputs, one per
fitted level after configural (each compared with the level before it).
specThe original model specification.
modelCharacter: "esem" or "besem".
notesNamed list, one character vector per level that raised a
lavaan warning while being fitted or compared (singular information matrix
for the robust test statistic, non-positive-definite latent or residual
covariance matrix, negative variance). The warnings are caught, reworded
and shown by print() instead of being raised, so a run that
concludes does not end with a lavaan warning. Empty when nothing was raised.
fallback_from, fallback_note
If B-ESEM configural fails to converge, the result may be from an ESEM fallback (no general factor); not comparable df-for-df to Mplus B-ESEM.
Ordered B-ESEM: Validated against Mplus on BFI for
(max ,
, df match). At with
items a warning is issued; verify externally before reporting.
WLSMV SRMR: The invariance table applies an Mplus-style SRMR denominator correction for ordered models where implemented.
B-ESEM configural failure: After retries, may fall back to ESEM
(fallback_from = "besem"). The printed table is structurally different
from B-ESEM; do not compare to Mplus B-ESEM output.
Empty categories: Ordered multi-group fits may recode categories absent in one group (see verbose output).
See system.file("VALIDATION.md", package = "bifactory") for a summary.
| Level | Continuous (MLR) | Ordered (WLSMV/Theta) |
| Configural | free | free |
| Weak | loadings | loadings |
| Strong | loadings + intercepts | loadings + thresholds |
| Strict | + residuals | + residuals |
Latent var/cov (through = "varcov") |
+ factor variances (1) and covariances | + factor variances (1) and covariances |
Latent means (through = "means") |
+ factor means (0) | + factor means (0) |
The last two levels follow Morin, Arens and Marsh (2016): the degrees of
freedom grow by (variances and covariances of
factors) and then by (means) per non-reference group.
Simple subtraction of scaled chi-square values is not valid for MLR or
WLSMV. lavaan::lavTestLRT() is used, which applies the
Satorra-Bentler (2001) correction for MLR and a mean-variance-adjusted
difference test for WLSMV. WLSMV difference test results may differ
numerically from Mplus's DIFFTEST procedure.
At the configural level std.lv = TRUE fixes factor variances to 1
in all groups. Under weak invariance lavaan automatically frees factor
variances in Group 2+ and keeps them at 1 in Group 1. Under strong
invariance factor means are freed in Group 2+ and fixed to 0 in Group 1.
The optional latent var/cov level fixes the Group 2+ variances back to 1
and equates the covariances; the latent mean level fixes the Group 2+
means back to 0.
With ordered indicators (theta parameterization) the configural model
uses the standard identification in every group: residual variances 1,
factor means 0, all thresholds free. From the weak level on, the
non-reference residual variances and factor means are free and identified
through the equal loadings plus partial threshold equalities (two
thresholds of one referent item per factor, one threshold of every other
item), which is the Mplus convention. The same scheme is not used at the
configural level: with free loadings an item's single equated threshold
is its only link across groups, and when that threshold lies near zero in
a non-reference group the item's residual variance is empirically
unidentified (it ran to several hundred on psych::bfi, with the
chi-square drifting by 14 to 36 units between programs). The standard
identification is the equivalent, well-conditioned model.
Group 1 (the reference group) is the first sorted level of the grouping
variable, spec$group_levels, which is also the reference group in
the Mplus syntax written by run_mplus_besem_invariance. The
fits pass this order to lavaan as group.label; lavaan's own default
would be the order in which the groups appear in the data, and a different
reference group gives an equivalent model with the same fit but differently
oriented loadings and factor covariances.
At the configural level each group is rotated on its own, with the rows of
its loading matrix scaled by the model-implied item variances, as in
Mplus. From the weak level on the loadings are equal across groups and the
common loading matrix is rotated once, without row scaling
(rotation.args = list(std.ov = FALSE) in lavaan), which is what
Mplus does. lavaan's default would scale the rows by the residual variances
averaged over the groups: the same fit, but a different rotation of it,
with standardized loadings 0.03 to 0.09 away from Mplus's on
psych::bfi (0.44 in one bifactor model) and standard errors up to
0.003 away. Up to 0.6.1 bifactory used that default.
The unscaled rotation works on the loadings in the metric of the first
group. esem_invariance() passes group.label so that this is
the first sorted group level, as in the generated Mplus syntax; direct
calls of esem(), besem() or esem_ordered() with
group and group_equal = "loadings" follow lavaan's order (first
appearance in the data) unless group.label is passed through
.... The agreement with Mplus was checked for target rotation.
Mplus does not allow Kaiser row standardization with equal loadings, while
lavaan keeps Kaiser weights for varimax and promax.
With oblique target rotation (ESEM) the rotation standard errors use
lavaan's delta method (rotation.se = "delta") with
rotation.args$gpa.tol = 1e-6, which agrees more closely with Mplus
at the configural level than lavaan's default "bordered"; the
tighter tolerance keeps the delta-method standard errors from depending on
the random rotation starts. B-ESEM (orthogonal target rotation) keeps
"bordered". A rotation.se passed through ... is kept.
esem and besem use lavaan::cfa() with a
native efa() block (integrated MLR estimation and rotation).
esem(ordered = ...) calls esem_ordered. Default
method = "lavaan" (lavaan WLSMV, efa() block, post-hoc rotation).
method = "rotation" uses a custom DWLS pipeline (polychoric correlations
+ GPArotation).
besem(ordered = ...) calls besem_ordered. Default
method = "rotation" (custom DWLS/WLSMV + orthogonal targetT; this
is what run_comparison uses for ordered data). method = "set-esem"
fits a lavaan WLSMV bifactor CFA with non-primary specific loadings fixed at
zero; it does not match Mplus B-ESEM loadings.
esem_invariance uses lavaan multi-group efa() models plus
explicit syntax patches for ordered B-ESEM. See that help page for scope limits.
specify_model, esem,
esem_ordered
data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "school", label = "Holzinger-Swineford" ) # Continuous ESEM measurement invariance across schools inv <- esem_invariance(spec) print(inv) # Access individual model fits summary(inv$models$strong, fit.measures = TRUE, standardized = TRUE) lavaan::lavTestScore(inv$models$strong$lavaan_fit) # Ordered data (WLSMV) and B-ESEM invariance follow the same pattern: # add `ordered = TRUE` to specify_model(), then optionally `model = "besem"`. items <- paste0("x", 1:9) ord <- as.data.frame(lapply(HolzingerSwineford1939[, items], function(v) { as.integer(cut(v, breaks = quantile(v, probs = seq(0, 1, 0.2)), include.lowest = TRUE)) })) names(ord) <- items ord$school <- HolzingerSwineford1939$school spec_ord <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = ord, group = "school", ordered = TRUE ) inv_besem <- esem_invariance(spec_ord, model = "besem") print(inv_besem)data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "school", label = "Holzinger-Swineford" ) # Continuous ESEM measurement invariance across schools inv <- esem_invariance(spec) print(inv) # Access individual model fits summary(inv$models$strong, fit.measures = TRUE, standardized = TRUE) lavaan::lavTestScore(inv$models$strong$lavaan_fit) # Ordered data (WLSMV) and B-ESEM invariance follow the same pattern: # add `ordered = TRUE` to specify_model(), then optionally `model = "besem"`. items <- paste0("x", 1:9) ord <- as.data.frame(lapply(HolzingerSwineford1939[, items], function(v) { as.integer(cut(v, breaks = quantile(v, probs = seq(0, 1, 0.2)), include.lowest = TRUE)) })) names(ord) <- items ord$school <- HolzingerSwineford1939$school spec_ord <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = ord, group = "school", ordered = TRUE ) inv_besem <- esem_invariance(spec_ord, model = "besem") print(inv_besem)
Fits ESEM on ordinal indicators (WLSMV / Theta parameterization).
Called automatically from esem when ordered is set.
esem_ordered( data, nfactors, indicators = NULL, rotation = "target", target = NULL, factor_names = NULL, method = c("lavaan", "rotation"), n_starts = 100L, r_obs_override = NULL, std.lv = TRUE, missing = "pairwise", group = NULL, group_equal = NULL, heywood_fix = TRUE, n_obs = NULL, ... )esem_ordered( data, nfactors, indicators = NULL, rotation = "target", target = NULL, factor_names = NULL, method = c("lavaan", "rotation"), n_starts = 100L, r_obs_override = NULL, std.lv = TRUE, missing = "pairwise", group = NULL, group_equal = NULL, heywood_fix = TRUE, n_obs = NULL, ... )
data |
A |
nfactors |
Integer. Number of latent factors. |
indicators |
Character vector of item names. If |
rotation |
Character. Rotation method. Default |
target |
A target matrix from |
factor_names |
Optional character vector of factor names. |
method |
Character. |
n_starts |
Integer. Number of random rotation starts when
|
r_obs_override |
Optional observed correlation matrix to fit instead of lavaan's polychoric correlations (for reproducibility / testing). The weight matrix still comes from lavaan, so a matrix that differs from lavaan's by more than 0.001 triggers a warning. |
std.lv |
Logical. Fix factor variances to 1. Default |
missing |
Character. Missing data handling passed to lavaan.
Default |
group |
Character. Grouping variable name for multi-group models. |
group_equal |
Character vector of lavaan equality constraints. With
|
heywood_fix |
Logical. Retry rotation with Cholesky unrotation when a
standardised loading exceeds 1. Default |
n_obs |
Ignored (kept for backward compatibility). |
... |
Additional arguments passed to |
Default (method = "lavaan"): lavaan's efa() block with WLSMV
(unrestricted model, post-hoc rotation, delta-method SEs)—aligned with Mplus
ESTIMATOR = WLSMV; ROTATION = TARGET (oblique).
Alternate (method = "rotation"): custom DWLS pipeline (polychoric
correlations, weight matrix and asymptotic covariance from one auxiliary
lavaan fit, GPArotation, sandwich + rotation Jacobian SEs).
Mplus:
ANALYSIS:
ESTIMATOR = WLSMV;
ROTATION = TARGET;
PARAMETERIZATION = THETA;
MODEL:
EX MD CI BY item1-item18 (*1);
R (this function):
esem_ordered(data, nfactors = 3, rotation = "target", target = tgt,
factor_names = c("EX","MD","CI"))
method = "lavaan")lavaan's efa() block fits an unrestricted k-factor model under WLSMV,
applies post-hoc rotation, and propagates standard errors through the rotation
transformation (Asparouhov & Muthen, 2009).
An object of class c("esem_fit_ordered", "esem_fit"). For
method = "lavaan", lavaan_fit is the fitted ESEM model. For
method = "rotation", lavaan_fit is an auxiliary one-factor CFA;
use std_rotated_loadings, wlsmv_stats, and
std_loadings. Slot estimator is "WLSMV" or
"DWLS" depending on path. For method = "rotation",
communalities holds each item's communality; a value above 1 (a
Heywood case, negative residual variance) gives a warning.
esem and besem use lavaan::cfa() with a
native efa() block (integrated MLR estimation and rotation).
esem(ordered = ...) calls esem_ordered. Default
method = "lavaan" (lavaan WLSMV, efa() block, post-hoc rotation).
method = "rotation" uses a custom DWLS pipeline (polychoric correlations
+ GPArotation).
besem(ordered = ...) calls besem_ordered. Default
method = "rotation" (custom DWLS/WLSMV + orthogonal targetT; this
is what run_comparison uses for ordered data). method = "set-esem"
fits a lavaan WLSMV bifactor CFA with non-primary specific loadings fixed at
zero; it does not match Mplus B-ESEM loadings.
esem_invariance uses lavaan multi-group efa() models plus
explicit syntax patches for ordered B-ESEM. See that help page for scope limits.
Defaults differ by entry point; pass missing explicitly when fitting ESEM and
B-ESEM separately on the same ordered dataset.
specify_model / run_comparison: "pairwise"
for ordered data, "listwise" for continuous.
esem_ordered: "pairwise".
besem_ordered: "listwise" (pipeline still passes
missing from the spec when used via run_comparison).
For besem_ordered(method = "rotation") and
esem_ordered(method = "rotation"), $lavaan_fit is often an
auxiliary one-factor WLSMV CFA used only to extract DWLS weight matrices—not
the fitted ESEM/B-ESEM model. Use std_loadings, parameters,
and fitMeasures(x) on the esem_fit wrapper;
fitMeasures(x) reads wlsmv_stats when present.
Do not interpret summary(x$lavaan_fit), modindices(x), or
coef(x) as the rotated solution unless you know the fit used the lavaan
efa() path (method = "lavaan" or method = "set-esem" for B-ESEM).
When heywood_fix = TRUE (default on single-group esem and
esem_ordered), std_loadings may reflect a post-hoc
rotation correction while lavaan_fit still holds the pre-correction lavaan
solution. Prefer std_loadings(x) for reported loadings in that case.
Asparouhov, T., & Muthen, B. (2009). Exploratory structural equation modeling. Structural Equation Modeling, 16(3), 397–438.
esem for continuous data,
besem_ordered for bifactor ordered ESEM.
data("HolzingerSwineford1939", package = "lavaan") # Derive ordered (5-category Likert) versions of the 9 continuous items items <- paste0("x", 1:9) ord <- as.data.frame(lapply(HolzingerSwineford1939[, items], function(v) { as.integer(cut(v, breaks = quantile(v, probs = seq(0, 1, 0.2)), include.lowest = TRUE)) })) names(ord) <- items tgt <- make_target( list(Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9")), item_names = items ) fit_ord <- esem_ordered( data = ord, nfactors = 3, indicators = items, rotation = "target", target = tgt, factor_names = c("Visual", "Textual", "Speed"), n_starts = 5L ) summary(fit_ord, fit.measures = TRUE, standardized = TRUE) std_loadings(fit_ord) factor_correlations(fit_ord)data("HolzingerSwineford1939", package = "lavaan") # Derive ordered (5-category Likert) versions of the 9 continuous items items <- paste0("x", 1:9) ord <- as.data.frame(lapply(HolzingerSwineford1939[, items], function(v) { as.integer(cut(v, breaks = quantile(v, probs = seq(0, 1, 0.2)), include.lowest = TRUE)) })) names(ord) <- items tgt <- make_target( list(Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9")), item_names = items ) fit_ord <- esem_ordered( data = ord, nfactors = 3, indicators = items, rotation = "target", target = tgt, factor_names = c("Visual", "Textual", "Speed"), n_starts = 5L ) summary(fit_ord, fit.measures = TRUE, standardized = TRUE) std_loadings(fit_ord) factor_correlations(fit_ord)
Converts a fitted ESEM solution into explicit lavaan CFA syntax following
the ESEM-within-CFA approach (Marsh et al. 2014). The syntax uses
unstandardised ESEM loadings as starting values (start(v)*item) or
fixed values (v*item) depending on the referent scheme.
ewc_syntax(esem_fit, spec, referents = NULL, var_fixed = TRUE)ewc_syntax(esem_fit, spec, referents = NULL, var_fixed = TRUE)
esem_fit |
An |
spec |
An |
referents |
Named character vector of referent items, one per factor.
Names = factor names; values = item names. If |
var_fixed |
Logical. Identification mode:
For a B-ESEM source the same two schemes apply to G and to every specific factor; all factor covariances stay fixed at zero in both. |
A character string of lavaan model syntax. Use cat() to
inspect, or pass directly to fit_ewc via
custom_syntax.
Marsh, H. W., Morin, A. J. S., Parker, P. D., & Kaur, G. (2014). Exploratory structural equation modeling. Annual Review of Clinical Psychology, 10, 85-110.
Convenience helper for use as the target of
align_loadings(): pulls the STDYX-standardized loadings from a
MplusAutomation::readModels() result and returns one matrix per
group, in items-by-factors form with names matching common conventions.
extract_mplus_loadings( mp_out, standardized = "stdyx.standardized", items = NULL, factors = NULL )extract_mplus_loadings( mp_out, standardized = "stdyx.standardized", items = NULL, factors = NULL )
mp_out |
An object returned by |
standardized |
Which Mplus section to read. Default
|
items |
Optional character vector. If supplied, the returned matrices use this row order; otherwise the order from Mplus is used. |
factors |
Optional character vector. If supplied, the returned matrices use this column order. |
A named list of matrices, one per group, each with rownames (items) and colnames (factors).
Extracts the estimated correlation matrix among latent factors.
factor_correlations(x, digits = 3)factor_correlations(x, digits = 3)
x |
An |
digits |
Integer. Number of decimal places. Default 3. |
A symmetric matrix of factor correlations.
Returns a data frame of estimated latent factor scores, one column per
factor and one row per observation. Works with any esem_fit,
plain lavaan object, or esem_invariance result.
factor_scores( x, method = c("regression", "bartlett"), level = "auto", align = NULL, dCFI_cutoff = -0.01, ... )factor_scores( x, method = c("regression", "bartlett"), level = "auto", align = NULL, dCFI_cutoff = -0.01, ... )
x |
An |
method |
Character. Estimation method: |
level |
Character. Only used when |
align |
Optional alignment of the score columns.
Internally calls |
dCFI_cutoff |
Numeric. The |
... |
Reserved for future use. |
For multi-group ordered B-ESEM scoring, a single pooled polychoric
correlation matrix is applied to every group. A warning is issued because
scores for non-reference groups may be biased when their correlation
structures differ. If the polychoric correlation matrix or the
score-information matrix is singular, factor_scores() raises an
informative error identifying the singular matrix rather than exposing a
bare solve() failure.
A data.frame with one column per latent factor (named by
the model's factor labels) and one row per observation. A group
column is prepended automatically for multi-group models. Rows for
missing observations contain NA in all factor columns.
data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Single fit fit <- esem(d, nfactors = 3) scores <- factor_scores(fit) head(scores) # Merge back to original data d_with_scores <- cbind(d, factor_scores(fit)) # Bartlett method scores_b <- factor_scores(fit, method = "bartlett")data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] # Single fit fit <- esem(d, nfactors = 3) scores <- factor_scores(fit) head(scores) # Merge back to original data d_with_scores <- cbind(d, factor_scores(fit)) # Bartlett method scores_b <- factor_scores(fit, method = "bartlett")
For each factor, selects the primary indicator with the largest absolute unstandardised loading from the ESEM solution. This is the item that will anchor that factor's identification in the EWC model.
find_ewc_referents(esem_fit, spec)find_ewc_referents(esem_fit, spec)
esem_fit |
An |
spec |
An |
Referents are chosen solely by maximum |loading| on the primary factor.
You can override automatic selection by supplying a custom named vector
to the referents argument of ewc_syntax or
fit_ewc.
A named character vector: names are factor names, values are the
selected referent item names (in the original case from spec).
Generates EWC lavaan syntax from a fitted ESEM solution and estimates it as
a standard lavaan::cfa() model – no rotation required.
fit_ewc( esem_fit, spec, referents = NULL, var_fixed = TRUE, missing = NULL, custom_syntax = NULL, ... )fit_ewc( esem_fit, spec, referents = NULL, var_fixed = TRUE, missing = NULL, custom_syntax = NULL, ... )
esem_fit |
An |
spec |
An |
referents |
Named character vector of referent items or |
var_fixed |
Logical. Identification mode. Default |
missing |
Character. Missing data handling. Defaults to
|
custom_syntax |
Character or |
... |
Additional arguments forwarded to |
An object of class "ewc_fit":
lavaan_fitlavaan fit object; all lavaan generics work on it.
syntaxlavaan model string used.
referentsNamed vector of referent items.
var_fixedIdentification mode used.
estimatorEstimator ("MLR" for continuous, "DWLS" for ordered).
specThe spec object.
ewc_syntax, find_ewc_referents,
compare_ewc
Returns CFI, TLI, RMSEA (with 90\
character vector. Uses the .scaled variants when the fit was
estimated with MLR/WLSMV, matching Mplus's default output. For B-ESEM /
ESEM WLSMV fits that carry pre-computed Mplus-matched values in
$wlsmv_stats, those values are used directly and the RMSEA CI is
computed from the scaled chi-square via non-central chi-squared inversion
(MacCallum, Browne & Sugawara, 1996).
fit_indices(fit)fit_indices(fit)
fit |
An |
A named character vector with elements "CFI", "TLI",
"RMSEA [90% CIs]", and "SRMR". Stack rows with
rbind() (wrapped in noquote()) to build a comparison
table.
data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] fit_e <- esem(d, nfactors = 3) fit_b <- besem(d, specific_factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), n_starts = 5L) noquote(rbind( ESEM = fit_indices(fit_e), BESEM = fit_indices(fit_b) ))data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] fit_e <- esem(d, nfactors = 3) fit_b <- besem(d, specific_factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), n_starts = 5L) noquote(rbind( ESEM = fit_indices(fit_e), BESEM = fit_indices(fit_b) ))
Forwards to lavaan::fitMeasures() when the fit used lavaan's efa()
path. When wlsmv_stats is present (custom DWLS/WLSMV from
besem_ordered(method = "rotation") or
esem_ordered(method = "rotation")), returns those indices instead.
## S3 method for class 'esem_fit' fitMeasures( object, fit.measures = c("cfi", "tli", "rmsea", "rmsea.ci.lower", "rmsea.ci.upper", "srmr", "aic", "bic"), ... )## S3 method for class 'esem_fit' fitMeasures( object, fit.measures = c("cfi", "tli", "rmsea", "rmsea.ci.lower", "rmsea.ci.upper", "srmr", "aic", "bic"), ... )
object |
An |
fit.measures |
Character vector of fit index names. Default returns a standard set: CFI, TLI, RMSEA, SRMR, AIC, BIC. |
... |
Passed to |
A named numeric vector of fit indices (by default CFI, TLI, RMSEA with its confidence interval, SRMR, and, on lavaan paths, AIC and BIC).
Generates the correct Mplus syntax for Bifactor ESEM with orthogonal target rotation, using separate BY statements per factor with (*1).
generate_mplus_besem_syntax( specific_factors, all_indicators, g_name = "G", cfa_factors = NULL, regressions = NULL, covariances = NULL, data_file = "mydata.dat", missing_code = 999, output_path = NULL, ordered = FALSE, estimator = if (ordered) "WLSMV" else "MLR" )generate_mplus_besem_syntax( specific_factors, all_indicators, g_name = "G", cfa_factors = NULL, regressions = NULL, covariances = NULL, data_file = "mydata.dat", missing_code = 999, output_path = NULL, ordered = FALSE, estimator = if (ordered) "WLSMV" else "MLR" )
specific_factors |
Named list of specific factor -> item assignments. |
all_indicators |
Character vector of all ESEM indicator names. |
g_name |
Character. General factor name. Default |
cfa_factors |
Optional named list of additional CFA factors. |
regressions |
Optional character vector of regression statements. |
covariances |
Optional character vector of covariance statements. |
data_file |
Character. Data file name. Default |
missing_code |
Missing value code. Default |
output_path |
Character. Path to write .inp file. Returns syntax
invisibly if |
ordered |
Logical. |
estimator |
Character. Default |
Mplus syntax string (invisibly). Writes file if output_path
supplied.
Generates a complete Mplus .inp file for ESEM with target rotation, using the correct Mplus syntax: separate BY statements per factor, each with its own (*1) label. Primary items always appear first, followed by cross-loading items targeted to zero.
generate_mplus_syntax( factors, cfa_factors = NULL, regressions = NULL, covariances = NULL, data_file = "mydata.dat", missing_code = 999, estimator = if (ordered) "WLSMV" else "MLR", output_path = NULL, ordered = FALSE )generate_mplus_syntax( factors, cfa_factors = NULL, regressions = NULL, covariances = NULL, data_file = "mydata.dat", missing_code = 999, estimator = if (ordered) "WLSMV" else "MLR", output_path = NULL, ordered = FALSE )
factors |
Named list mapping factor names to their primary indicator
names. Example: |
cfa_factors |
Optional named list of additional CFA factors. |
regressions |
Optional character vector of regression statements. |
covariances |
Optional character vector of covariance statements. |
data_file |
Character. Name of the data file. Default |
missing_code |
Missing value code. Default |
estimator |
Character. Default |
output_path |
Character. Full path to write the .inp file. Returns
syntax string invisibly if |
ordered |
Logical. |
Correct output format:
EX BY batEX1 batEX2 ... batEX8 <- primary items first batMD1~0 ... batCI5~0 (*1); <- cross-loadings after MD BY batMD1 batMD2 ... batMD5 <- primary items first batEX1~0 ... batCI5~0 (*1); <- cross-loadings after
The Mplus syntax as a character string (invisibly). Writes file
if output_path is supplied.
# Pure syntax generation: builds a Mplus .inp string, no Mplus install needed. syntax <- generate_mplus_syntax( factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), output_path = file.path(tempdir(), "esem_model.inp") ) cat(syntax)# Pure syntax generation: builds a Mplus .inp string, no Mplus install needed. syntax <- generate_mplus_syntax( factors = list( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9") ), output_path = file.path(tempdir(), "esem_model.inp") ) cat(syntax)
Extract the Generated lavaan Syntax
get_syntax(x, cat = TRUE)get_syntax(x, cat = TRUE)
x |
An |
cat |
Logical. If |
The lavaan model string (invisibly).
Returns the raw lavaan object stored in x$lavaan_fit. For
continuous ESEM/B-ESEM and method = "lavaan" ordered fits, this is the
fitted model. For besem_ordered(method = "rotation") (and
esem_ordered(method = "rotation")), it is often an auxiliary
one-factor CFA used for DWLS weights only—see
The lavaan_fit slot on custom WLSMV fits.
Use lavaan::lavTestScore(), lavaan::lavInspect(), etc. only when
that slot represents the model you intend to analyse.
lavaan_fit(x)lavaan_fit(x)
x |
An |
A lavaan object.
data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] fit <- esem(d, nfactors = 3) lav <- lavaan_fit(fit) # Inspect the underlying lavaan model lavaan::lavInspect(lav, "cor.lv") # factor correlations lavaan::fitMeasures(lav, c("cfi", "rmsea"))data("HolzingerSwineford1939", package = "lavaan") d <- HolzingerSwineford1939[, paste0("x", 1:9)] fit <- esem(d, nfactors = 3) lav <- lavaan_fit(fit) # Inspect the underlying lavaan model lavaan::lavInspect(lav, "cor.lv") # factor correlations lavaan::fitMeasures(lav, c("cfi", "rmsea"))
Builds the target loading matrix for B-ESEM. The general factor G has target 1 for all items (free to load everywhere). Each specific factor has target 1 for its primary items and 0 for all others.
make_bifactor_target(specific_factors, indicators, g_name = "G")make_bifactor_target(specific_factors, indicators, g_name = "G")
specific_factors |
Named list of specific factor -> item assignments. |
indicators |
Character vector of all item names. |
g_name |
Character. Name for the general factor. Default |
A numeric matrix (items x factors) of class "esem_target".
Column order: G first, then specific factors in list order.
tgt <- make_bifactor_target( specific_factors = list(EX = c("y1","y2","y3"), MD = c("y4","y5","y6")), indicators = paste0("y", 1:6) ) print(tgt)tgt <- make_bifactor_target( specific_factors = list(EX = c("y1","y2","y3"), MD = c("y4","y5","y6")), indicators = paste0("y", 1:6) ) print(tgt)
Constructs a target matrix suitable for use with rotation = "target"
in esem. Items listed in keys are assigned a target
value of 1 for their primary factor; all other cells are set to
0 (penalised to be near zero) or NA (free, no penalty).
make_target(keys, nitems = NULL, item_names = NULL, cross_loading_value = 0)make_target(keys, nitems = NULL, item_names = NULL, cross_loading_value = 0)
keys |
A named list where each element is a vector of item indices
(integers) or item names (characters) that are hypothesised to load
primarily on that factor. Names become factor names.
Example: |
nitems |
Integer. Total number of items. Required when |
item_names |
Optional character vector of item names of length
|
cross_loading_value |
Numeric or |
Hard target (cross_loading_value = 0): Cross-loadings are
penalised toward zero. Use this when you have strong theory.
Soft target (cross_loading_value = NA): Only primary loadings
are targeted; cross-loadings are completely free. Use this when you are
less certain about the zero pattern.
An item can appear in multiple keys entries if it is expected to have
meaningful loadings on more than one factor. In that case its target value
will be 1 for both factors and cross_loading_value elsewhere.
A numeric matrix (items x factors) with row names set to item names
and column names set to factor names from keys.
# Simple 15-item, 3-factor target (integer keys) tgt <- make_target( keys = list(Extrav = 1:5, Agree = 6:10, Open = 11:15), nitems = 15 ) # Named items tgt2 <- make_target( keys = list(F1 = c("y1", "y2", "y3"), F2 = c("y4", "y5", "y6")), item_names = paste0("y", 1:6) ) # Soft target (cross-loadings free) tgt_soft <- make_target( keys = list(F1 = 1:5, F2 = 6:10), nitems = 10, cross_loading_value = NA )# Simple 15-item, 3-factor target (integer keys) tgt <- make_target( keys = list(Extrav = 1:5, Agree = 6:10, Open = 11:15), nitems = 15 ) # Named items tgt2 <- make_target( keys = list(F1 = c("y1", "y2", "y3"), F2 = c("y4", "y5", "y6")), item_names = paste0("y", 1:6) ) # Soft target (cross-loadings free) tgt_soft <- make_target( keys = list(F1 = 1:5, F2 = 6:10), nitems = 10, cross_loading_value = NA )
Forwards to lavaan::modindices() on lavaan_fit(x). For custom
WLSMV paths where lavaan_fit is an auxiliary one-factor CFA, indices
refer to that auxiliary model, not the rotated ESEM/B-ESEM solution; see
The lavaan_fit slot on custom WLSMV fits.
## S3 method for class 'esem_fit' modindices(object, sort. = TRUE, maximum.number = 20, ...)## S3 method for class 'esem_fit' modindices(object, sort. = TRUE, maximum.number = 20, ...)
object |
An |
sort. |
Logical. Sort by modification index value? Default |
maximum.number |
Integer. Maximum number of indices to return. Default 20. |
... |
Passed to |
A data.frame of modification indices, as returned by
lavaan::modindices().
Prints a formatted parameter table – standardized loadings, standard errors, z-values, and p-values – for one fit object or all three models in a pipeline. Analogous to the STDYX section of Mplus output.
parameters( x, model = "all", type = "loadings", suppress = 0, digits = 3, highlight_primary = TRUE )parameters( x, model = "all", type = "loadings", suppress = 0, digits = 3, highlight_primary = TRUE )
x |
An |
model |
Character. When |
type |
Character. |
suppress |
Numeric. Hide loadings with |z| below this value. Default 0 (show all). |
digits |
Integer. Decimal places. Default 3. |
highlight_primary |
Logical. Colour the target (primary) loadings using
ANSI codes. Supported in RStudio and most terminals; falls back silently to
plain output when colour is unavailable. Default |
A data frame of class bifactory_parameters holding the
standardized parameter table. Its columns are factor, item,
std, se, z, p, and loading_type; a
model column is added when x is a pipeline. With
type = "all", rows for residual variances and thresholds are also
included. Printing the object renders the formatted, colour-coded table;
assign the result to use the values without console output.
On a Heywood-corrected fit, the rotation changed after the standard errors
were computed. Consequently, se, z, and p are
NA, while the corrected standardized loadings in std are
retained. When suppress > 0, rows whose z is NA are
dropped, as are rows below the suppression threshold.
Reads a Mplus .out file and assembles the polychoric (tetrachoric)
correlation matrix that Mplus prints under SAMPLE STATISTICS when
SAMPSTAT is requested in the OUTPUT section.
parse_mplus_polychoric(out_file)parse_mplus_polychoric(out_file)
out_file |
Character. Full path to a Mplus |
The matrix can be passed directly to besem_ordered via the
r_obs_override argument so that R's DWLS optimisation uses the
same polychoric estimates as Mplus, eliminating rotational discrepancies
caused by differences in the polychoric estimators.
A named, symmetric numeric matrix of polychoric correlations with 1s on the diagonal. Row and column names are the item names as reported by Mplus (typically upper-case).
## Not run: # Reads a polychoric matrix from a Mplus .out file (SAMPSTAT output), # so it needs a .out produced by a licensed Mplus run. R_mplus <- parse_mplus_polychoric( "path/to/besem_measurement.out" ) dim(R_mplus) # 18 x 18 # Use as input to besem_ordered so R rotates from the same matrix as Mplus fit_b <- besem_ordered( data = mydata, specific_factors = factor_items, r_obs_override = R_mplus ) ## End(Not run)## Not run: # Reads a polychoric matrix from a Mplus .out file (SAMPSTAT output), # so it needs a .out produced by a licensed Mplus run. R_mplus <- parse_mplus_polychoric( "path/to/besem_measurement.out" ) dim(R_mplus) # 18 x 18 # Use as input to besem_ordered so R rotates from the same matrix as Mplus fit_b <- besem_ordered( data = mydata, specific_factors = factor_items, r_obs_override = R_mplus ) ## End(Not run)
Given a failed invariance level, uses a greedy score-test loop
(lavTestScore + group.partial) to identify and free the
minimum set of non-invariant equality constraints needed to restore
acceptable fit (DCFI >= -0.010). Downstream invariance levels are then
re-tested under the same partial constraints.
partial_invariance( inv, level, max_free = 10L, delta_cfi_cutoff = -0.01, verbose = TRUE )partial_invariance( inv, level, max_free = 10L, delta_cfi_cutoff = -0.01, verbose = TRUE )
inv |
An |
level |
Character. The invariance level to partially free:
|
max_free |
Integer. Maximum parameters to free before stopping. Default 10. Byrne et al. (1989) recommend freeing the minimum number – in practice >5 rarely recovers invariance. |
delta_cfi_cutoff |
Numeric. Stopping criterion: loop stops when DCFI >= this value. Default -0.010 (Cheung & Rensvold, 2002). |
verbose |
Logical. Print progress messages. Default |
An object of class "esem_partial_invariance" with:
$freed_paramsData frame: round, label, group_name, score (LM), delta_cfi (baseline-relative), converged.
$partial_fitesem_fit at level with freed params.
$downstreamNamed list of esem_fit for levels above target.
$downstream_lrtlavTestLRT() comparisons among downstream.
$tableCombined fit table (original + partial + downstream).
$convergedLogical: did DCFI pass cutoff?
$group_partialCharacter vector: final group.partial labels.
ESEM and B-ESEM loadings rotate as a block, so partial configural and
partial weak invariance do not exist. Partial strong releases intercepts
(continuous items) or thresholds (ordered items); partial strict releases
residual variances. Constraints are released one at a time, largest score
test first (van de Schoot, Lugtig & Hox, 2012). An intercept or threshold
is released only if every factor the item belongs to keeps at least two
items with invariant intercepts/thresholds (Byrne, Shavelson & Muthen,
1989), and a warning is issued when more than 20\
added at that level are released (Dimitrov, 2010). The search is data
driven: report every released parameter. On ordered items the search keeps
at least one threshold per item invariant (Millsap & Yun-Tein, 2004); an
item's last threshold can be released by hand through
esem_invariance(..., group.partial = "item|t1"), which then fixes
the item's residual variance at 1 in every group. A partial refit that
fits worse than the full model (a lavaan local optimum) is retried from
simple starting values.
Produces a sorted alignment ratio plot (as in Figure 2 of Mehrvarz & Rouder 2026) for one or all factor pairs. Flat horizontal spread = invariant (ICM-CFA tenable). Dispersion = misspecification.
## S3 method for class 'alignment_check' plot(x, pair = NULL, ...)## S3 method for class 'alignment_check' plot(x, pair = NULL, ...)
x |
An |
pair |
Character. Name of a specific factor pair (e.g., |
... |
Ignored. |
Called for its side effect of drawing the alignment-ratio plot;
invisibly returns NULL.
Print Method for alignment_check
## S3 method for class 'alignment_check' print(x, ...)## S3 method for class 'alignment_check' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the alignment-ratio summary.
Print Method for besem_fit
## S3 method for class 'besem_fit' print(x, ...)## S3 method for class 'besem_fit' print(x, ...)
x |
A |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
a formatted B-ESEM model overview to the console.
Renders the formatted, colour-coded parameter table produced by
parameters. Called automatically when the result of
parameters() is not assigned.
## S3 method for class 'bifactory_parameters' print(x, ...)## S3 method for class 'bifactory_parameters' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly.
Print Method for esem_comparison
## S3 method for class 'esem_comparison' print(x, ...)## S3 method for class 'esem_comparison' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the comparison table.
Print Method for esem_comparison_pipeline
## S3 method for class 'esem_comparison_pipeline' print(x, hints = TRUE, ...)## S3 method for class 'esem_comparison_pipeline' print(x, hints = TRUE, ...)
x |
An |
hints |
Logical. Print a short help block listing the accessors
available on the object (e.g. |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the comparison-pipeline overview.
Print Method for esem_fit
## S3 method for class 'esem_fit' print(x, ...)## S3 method for class 'esem_fit' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
a formatted model overview to the console.
Print Method for esem_invariance
## S3 method for class 'esem_invariance' print(x, ...)## S3 method for class 'esem_invariance' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the invariance comparison table.
Print Method for esem_partial_invariance
## S3 method for class 'esem_partial_invariance' print(x, ...)## S3 method for class 'esem_partial_invariance' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the partial-invariance summary.
Print an esem_spec Object
## S3 method for class 'esem_spec' print(x, ...)## S3 method for class 'esem_spec' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the model specification.
Displays a make_target() result in a compact, readable format,
marking primary loadings (1), penalised cells (0), and
free cells (NA).
## S3 method for class 'esem_target' print(x, ...)## S3 method for class 'esem_target' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the target loading matrix.
Displays the fit-index comparison table returned by compare_ewc.
## S3 method for class 'ewc_comparison' print(x, ...)## S3 method for class 'ewc_comparison' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x.
Compact fit summary (CFI/TLI/RMSEA/SRMR and ) for an
ESEM-within-CFA fit.
## S3 method for class 'ewc_fit' print(x, ...)## S3 method for class 'ewc_fit' print(x, ...)
x |
An |
... |
Ignored. |
Invisibly returns x.
Displays McDonald's omega and Cronbach's alpha indices following the Morin / Rodriguez et al. reporting framework.
## S3 method for class 'reliability_indices' print(x, ...) ## S3 method for class 'omega_result' print(x, ...)## S3 method for class 'reliability_indices' print(x, ...) ## S3 method for class 'omega_result' print(x, ...)
x |
A |
... |
Ignored. |
Invisibly returns x; called for the side effect of printing
the reliability table.
The bifactor target rotation criterion surface has many local minima. R and Mplus sometimes converge to different ones, leading to different partitioning of variance between G and specific factors (while fit indices remain identical because they are rotation-invariant).
refine_rotation(results)refine_rotation(results)
results |
An |
This function uses orthogonal Procrustes rotation to compute the rotation
matrix that maps R's unrotated loading matrix toward the
Mplus STDYX solution, then evaluates whether the target criterion at that
is lower than R's best random-start criterion. If so, the
better rotation is adopted. This is methodologically sound because:
The same published criterion function is still being minimised.
Procrustes only provides a well-informed starting point; the final
solution is the converged GPArotation::targetT optimum from that
start.
The approach is analogous to Mansolf and Reise's (2016) recommendation to use Schmid-Leiman solutions as warm starts.
The same results object with fit_besem updated if a
better rotation was found, otherwise unchanged. A message reports whether
the criterion improved. The comparison_table is also updated.
Mansolf, M., and Reise, S. P. (2016). Exploratory bifactor analysis: The Schmid-Leiman orthogonalization and Jennrich-Bentler analytic rotations. Multivariate Behavioral Research, 51(5), 698–717.
## Not run: # Requires run_comparison() results that include a Mplus rotation reference # (results$mplus_results non-NULL), so a licensed Mplus install is needed. results <- run_comparison(spec, mplus_folder = tempfile("mplus_")) results <- refine_rotation(results) omega <- compute_omega(results) print(omega) ## End(Not run)## Not run: # Requires run_comparison() results that include a Mplus rotation reference # (results$mplus_results non-NULL), so a licensed Mplus install is needed. results <- run_comparison(spec, mplus_folder = tempfile("mplus_")) results <- refine_rotation(results) omega <- compute_omega(results) print(omega) ## End(Not run)
Takes a model specification from specify_model and
automatically fits CFA, ESEM, and B-ESEM in R, optionally runs all
three in Mplus, and returns a comparison table plus all fitted objects.
run_comparison( spec, mplus_folder = NULL, mplus_command = "Mplus", run_alignment = TRUE, group_equal = NULL, n_starts = 30L )run_comparison( spec, mplus_folder = NULL, mplus_command = "Mplus", run_alignment = TRUE, group_equal = NULL, n_starts = 30L )
spec |
An |
mplus_folder |
Character. Path to a folder for Mplus files. If
|
mplus_command |
Character. Path or command used to invoke Mplus.
Default |
run_alignment |
Logical. Run alignment check? Default |
group_equal |
Character vector of lavaan equality constraints
(e.g. |
n_starts |
Integer. Random rotation starts for the B-ESEM rotation
search. Default |
An object of class "esem_comparison_pipeline" containing:
specThe original model specification.
fit_cfalavaan CFA fit object.
fit_esemesem_fit object.
fit_besembesem_fit object.
alignmentalignment_check result (if run).
comparison_tableData frame of fit indices.
mplus_resultsList of Mplus readModels results (if run).
data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) # Fit CFA, ESEM, and B-ESEM in R and build the comparison table results <- run_comparison(spec, n_starts = 5L) print(results) # Access individual fits summary(results$fit_esem, fit.measures = TRUE, standardized = TRUE) std_loadings(results$fit_besem, suppress = 0.10) factor_correlations(results$fit_esem) ## Not run: # Also run all three models in Mplus and compare side by side. # Requires a licensed Mplus installation reachable via `mplus_command`. results <- run_comparison(spec, mplus_folder = tempfile("mplus_")) print(results) ## End(Not run)data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) # Fit CFA, ESEM, and B-ESEM in R and build the comparison table results <- run_comparison(spec, n_starts = 5L) print(results) # Access individual fits summary(results$fit_esem, fit.measures = TRUE, standardized = TRUE) std_loadings(results$fit_besem, suppress = 0.10) factor_correlations(results$fit_esem) ## Not run: # Also run all three models in Mplus and compare side by side. # Requires a licensed Mplus installation reachable via `mplus_command`. results <- run_comparison(spec, mplus_folder = tempfile("mplus_")) print(results) ## End(Not run)
Creates complete Mplus .inp files for every level fitted in
inv (configural, weak, strong, strict and, when
esem_invariance() was run with through, latent var/cov and
latent means), runs them via MplusAutomation, then prints a
side-by-side comparison of fit statistics against the R results from
esem_invariance.
run_mplus_besem_invariance( inv, output_folder, mplus_command = "mplus", group_labels = NULL, missing_code = -999, difftest = TRUE )run_mplus_besem_invariance( inv, output_folder, mplus_command = "mplus", group_labels = NULL, missing_code = -999, difftest = TRUE )
inv |
An |
output_folder |
Character. Folder where |
mplus_command |
Character. Full path to the Mplus executable.
Default |
group_labels |
Named character vector mapping group values to Mplus
labels, e.g. |
missing_code |
Numeric. Missing value sentinel written to the data
file. Default |
difftest |
Logical. Generate |
A data frame (invisibly) with _R, _Mplus, and
delta_ columns for each fit statistic. Printed as a table.
## Not run: # Requires a licensed Mplus installation reachable via `mplus_command`. inv <- esem_invariance(spec, model = "besem") cmp <- run_mplus_besem_invariance( inv, output_folder = tempfile("mplus_inv_"), mplus_command = "C:/Program Files/Mplus/mplus.exe", group_labels = c("1" = "MALE", "2" = "FEMALE") ) ## End(Not run)## Not run: # Requires a licensed Mplus installation reachable via `mplus_command`. inv <- esem_invariance(spec, model = "besem") cmp <- run_mplus_besem_invariance( inv, output_folder = tempfile("mplus_inv_"), mplus_command = "C:/Program Files/Mplus/mplus.exe", group_labels = c("1" = "MALE", "2" = "FEMALE") ) ## End(Not run)
Writes all standard output files to output_folder:
<label>_fit_indices.csv – CFI/TLI/RMSEA/SRMR for all models.
<label>_CFA_loadings.csv – standardised loading matrix (wide).
<label>_ESEM_loadings.csv – standardised loading matrix (wide).
<label>_BESEM_loadings.csv – standardised loading matrix (wide).
<label>_loadings_comparison.csv – R vs Mplus loadings, long
format with SEs, z-scores, p-values, and diff_std.
<label>_omega.csv – Reliability indices (if
indices is supplied).
<label>_results.xlsx – Single xlsx workbook with all results
(when xlsx = TRUE).
save_results(results, omega = NULL, output_folder, label = NULL, xlsx = FALSE)save_results(results, omega = NULL, output_folder, label = NULL, xlsx = FALSE)
results |
An |
omega |
Optional. A |
output_folder |
Character. Path to the folder where files are written. Created if it does not exist. |
label |
Character. Prefix for output file names. Defaults to
|
xlsx |
Logical. If |
Invisibly returns a character vector of file paths written.
data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) results <- run_comparison(spec, n_starts = 5L) indices <- compute_indices(results) save_results(results, indices, output_folder = file.path(tempdir(), "esem_results"))data("HolzingerSwineford1939", package = "lavaan") spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford" ) results <- run_comparison(spec, n_starts = 5L) indices <- compute_indices(results) save_results(results, indices, output_folder = file.path(tempdir(), "esem_results"))
Convenience function to set specific cells of a target matrix after
initial construction with make_target.
set_target(target, items, factors, value)set_target(target, items, factors, value)
target |
An |
items |
Integer indices or character names of items to modify. |
factors |
Integer indices or character names of factors to modify. |
value |
New value: |
The modified target matrix.
Creates a model specification object that flows automatically through all subsequent steps – CFA, ESEM, B-ESEM, alignment check, Mplus syntax, and comparison tables. Define your factor structure once at the top; everything else is derived automatically.
specify_model( ..., data, label = "ESEM Model", ordered = FALSE, group = NULL, estimator = NULL, missing = NULL )specify_model( ..., data, label = "ESEM Model", ordered = FALSE, group = NULL, estimator = NULL, missing = NULL )
... |
Named character vectors, one per factor. The name becomes the
factor name and the vector contains the indicator names.
Example: |
data |
A |
label |
Optional character string labelling the model (used in
output headers and Mplus titles). Default |
ordered |
Logical or character vector. If |
group |
Character. Name of a grouping variable in |
estimator |
Character. Override the auto-selected lavaan estimator.
If |
missing |
Character. Missing-data handling. If |
An object of class "esem_spec" containing:
factorsNamed list of factor -> indicator assignments.
factor_namesCharacter vector of factor names.
all_itemsCharacter vector of all indicators in order.
nfactorsNumber of specific factors.
dataThe supplied data frame.
labelModel label.
cfa_syntaxReady-to-use lavaan CFA model string.
targetTarget matrix for ESEM target rotation.
bifactor_targetTarget matrix for B-ESEM.
data("HolzingerSwineford1939", package = "lavaan") # Continuous indicators (MLR estimator by default) spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford 3-factor" ) print(spec) # Multi-group spec (for invariance testing) spec_mg <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "sex", label = "HS 3-factor (multi-group)" ) # Run the CFA/ESEM/B-ESEM comparison from a spec results <- run_comparison(spec) print(results)data("HolzingerSwineford1939", package = "lavaan") # Continuous indicators (MLR estimator by default) spec <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, label = "Holzinger-Swineford 3-factor" ) print(spec) # Multi-group spec (for invariance testing) spec_mg <- specify_model( Visual = c("x1", "x2", "x3"), Textual = c("x4", "x5", "x6"), Speed = c("x7", "x8", "x9"), data = HolzingerSwineford1939, group = "sex", label = "HS 3-factor (multi-group)" ) # Run the CFA/ESEM/B-ESEM comparison from a spec results <- run_comparison(spec) print(results)
Returns the standardized factor loading matrix. Uses std_rotated_loadings
when present (custom WLSMV rotation path or Heywood-corrected loadings);
otherwise extracts from lavaan_fit(x). Analogous to Mplus STDYX loadings.
std_loadings(x, digits = 3, suppress = 0)std_loadings(x, digits = 3, suppress = 0)
x |
An |
digits |
Integer. Rounding digits. Default 3. |
suppress |
Numeric. Loadings with absolute value below this threshold
are replaced with |
A matrix of standardized loadings (items x factors).
Prints a full summary for an esem_fit. For models fit through the
DWLS-from-scratch path (B-ESEM ordered, or esem_ordered(method =
"rotation")), the underlying lavaan_fit slot holds an auxiliary
1-factor CFA used only for weight-matrix extraction; in that case we print
a self-contained bifactory summary built from the stored rotated
loadings, standard errors, and WLSMV statistics. Otherwise the call is
forwarded to lavaan::summary().
## S3 method for class 'esem_fit' summary(object, fit.measures = TRUE, standardized = TRUE, rsquare = FALSE, ...)## S3 method for class 'esem_fit' summary(object, fit.measures = TRUE, standardized = TRUE, rsquare = FALSE, ...)
object |
An |
fit.measures |
Logical. Include fit indices? Default |
standardized |
Logical. Include standardized solution? Default |
rsquare |
Logical. Include R-squared for endogenous variables? Default |
... |
Additional arguments passed to |
Invisibly returns object; called for the side effect of
printing the model summary.
Forwards to lavaan::lavaan-class summary on the underlying
lavaan fit, so you do not need to library(lavaan) separately.
## S3 method for class 'ewc_fit' summary( object, fit.measures = TRUE, standardized = TRUE, show_loadings = TRUE, ... )## S3 method for class 'ewc_fit' summary( object, fit.measures = TRUE, standardized = TRUE, show_loadings = TRUE, ... )
object |
An |
fit.measures |
Logical. Include fit indices. Default |
standardized |
Logical. Include standardised estimates. Default |
show_loadings |
Logical. After the lavaan summary, also print the
|
... |
Additional arguments passed to lavaan's summary method. |
Invisibly returns the lavaan summary object.