| Title: | Variational Bayes Psychometric Models |
|---|---|
| Description: | Variational Bayes estimation for a family of psychometric measurement models. Two models are provided. Variational Bayes factor analysis (vbfa) is a regularized partially confirmatory factor model spanning the confirmatory-exploratory continuum via spike-and-slab priors on the loadings (Chen, Guo, Zhang, and Pan, 2021 <doi:10.1037/met0000293>; Chen, 2023 <doi:10.3758/s13428-022-01884-7>; Jin and Chen, 2025 <doi:10.1080/10705511.2024.2432612>), with an optional dynamic (warm-started) regularization path, an orthogonal bifactor parameterization, and optional sparse residual (local dependence) estimation through a graphical spike-and-slab prior solved by QUIC (Jin, Chen, Yan, and Zhang, 2026 <doi:10.31234/osf.io/dehtv_v2>). Regularized MIMIC (vbmimic) extends this to multiple-indicators multiple-causes models, placing spike-and-slab priors on both the measurement and the structural part (Jin and Chen, 2025 <doi:10.1080/00273171.2025.2483253>). Companion tools compute SEM-like fit statistics, and sweep a factor-count window to report candidate fit, criterion, and between-candidate loading-correspondence measurements without selecting a count (Chen and Jin, 2026 <doi:10.48550/arXiv.2607.07159>). Data generators for either model family are also provided. |
| Authors: | Jinsong Chen [aut, cre] (ORCID: <https://orcid.org/0000-0002-0157-5469>), Yi Jin [aut] (ORCID: <https://orcid.org/0000-0002-8604-5992>) |
| Maintainer: | Jinsong Chen <[email protected]> |
| License: | GPL-3 |
| Version: | 0.9.1 |
| Built: | 2026-10-06 11:45:08 UTC |
| Source: | https://github.com/jinsong-chen/vbpm |
For a vbfa() fit, returns the loading matrix. For a vbmimic() fit,
returns a list with the measurement loadings A and the structural
coefficients B.
## S3 method for class 'vbpm_fit' coef(object, ...)## S3 method for class 'vbpm_fit' coef(object, ...)
object |
|
... |
Ignored. |
A matrix (vbfa) or a list of two matrices (vbmimic).
sim <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q) round(coef(fit), 2)sim <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q) round(coef(fit), 2)
Computes structural-equation-model fit statistics for a fitted vbfa()
object under hard selection (an unspecified loading counts as active when its
posterior inclusion probability is at least tau). Absolute indices
(RMSEA, SRMR, CFI, TLI) and information criteria (AIC, BIC) are returned,
together with the model's ELBO.
fit_stats(object, ...) ## S3 method for class 'vbfa' fit_stats( object, Y = NULL, Q = object$Q, tau = 0.5, gamma = 0.5, orthogonal = NULL, rank_adjust = FALSE, rank_max_J = 100, ... ) ## S3 method for class 'vbmimic' fit_stats(object, Y = NULL, Q = NULL, tau = 0.5, ...) ## S3 method for class 'vbpm_fit' fit_stats(object, ...)fit_stats(object, ...) ## S3 method for class 'vbfa' fit_stats( object, Y = NULL, Q = object$Q, tau = 0.5, gamma = 0.5, orthogonal = NULL, rank_adjust = FALSE, rank_max_J = 100, ... ) ## S3 method for class 'vbmimic' fit_stats(object, Y = NULL, Q = NULL, tau = 0.5, ...) ## S3 method for class 'vbpm_fit' fit_stats(object, ...)
object |
A fitted |
... |
Further arguments passed to methods. Unused by the methods supplied with the package. |
Y |
The |
Q |
The |
tau |
Hard-selection threshold on the posterior inclusion probability
(default |
gamma |
Reserved (extended BIC tuning); currently unused in the returned vector. |
orthogonal |
Logical; whether the fit is an orthogonal (bifactor)
model, in which case factor correlations are not free parameters. The
default |
rank_adjust |
Logical; if |
rank_max_J |
Largest number of observed variables for which an explicit
Jacobian-rank calculation is allowed (default |
An object of class vbpm_fit_stats (a named numeric vector) with
t_nom, t, t_S (parameter counts), RMSEA, SRMR, CFI, TLI,
AIC, BIC (hard selection), AIC_S, BIC_S (soft selection), ELBO,
and objective. The last is the model's comparability score: equal to
ELBO for a diagonal fit, and the terminal VECM objective under local
dependence, where ELBO is NA. It is comparable only across fits
sharing the objective_type attribute, which is also attached.
The vbmimic method returns the same shape with n_active_coef in place
of a full soft count and typed NA elsewhere; see vbmimic().
Chen, J., & Jin, Y. (2026). Recovering latent structures after variational Bayesian variable selection: Fit assessment and factor-number selection in partially exploratory factor analysis. arXiv preprint arXiv:2607.07159.
Jin, Y., & Chen, J. (2025). Regularized variational approximation for partially confirmatory factor analysis. Structural Equation Modeling: A Multidisciplinary Journal, 32(3), 437-449. doi:10.1080/10705511.2024.2432612
sim <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q) round(fit_stats(fit), 3) ## bifactor: orthogonal is read from the fit object, so the same call works Qb <- cbind(1L, Q) fb <- vbfa(sim$dat, Qb, orthogonal = TRUE) round(fit_stats(fb), 3)sim <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q) round(fit_stats(fit), 3) ## bifactor: orthogonal is read from the fit object, so the same call works Qb <- cbind(1L, Q) fb <- vbfa(sim$dat, Qb, orthogonal = TRUE) round(fit_stats(fb), 3)
Responses of 3,458 individuals to 27 mixed-type items, with 1.12% missing
data. Carried over from the LAWBL package (which vbpm supersedes) for
empirical illustration of the estimators.
nlsy27nlsy27
A list with three components:
A 3458 x 27 data frame of item responses (17 polytomous
items, 10 continuous), containing NAs.
A 27 x 3 initial design matrix with two to three specified
loadings per factor, in the -1/0/1 coding used by vbfa().
Indices of the 17 polytomous items.
Bureau of Labor Statistics, U.S. Department of Labor. National Longitudinal Survey of Youth 1997 cohort. Previously distributed in the LAWBL package (Chen, 2022).
data(nlsy27) dim(nlsy27$dat) nlsy27$Qdata(nlsy27) dim(nlsy27$dat) nlsy27$Q
Fits vbfa() for every integer K in a fixed consecutive window, holding
the partially specified backbone Q0 fixed and appending regularized
exploratory columns. It reports candidate summaries and between-candidate
loading measurements.
pefa( Q0, Y, Kmin, Kmax, bifactor = FALSE, general = 1, v0 = c(0.01, 0.005, 0.002, 0.001), max_it = 10000, convChk = FALSE, tolVal = 1e-04, tau = 0.5, rank_adjust = FALSE, rank_max_J = 100, verbose = TRUE )pefa( Q0, Y, Kmin, Kmax, bifactor = FALSE, general = 1, v0 = c(0.01, 0.005, 0.002, 0.001), max_it = 10000, convChk = FALSE, tolVal = 1e-04, tau = 0.5, rank_adjust = FALSE, rank_max_J = 100, verbose = TRUE )
Q0 |
A |
Y |
A numeric |
Kmin, Kmax
|
Inclusive window, whole numbers with
|
bifactor |
Fit an orthogonal bifactor parameterization. |
general |
Scalar or length- |
v0, max_it, convChk, tolVal
|
Optimizer controls passed to |
tau |
Loading-inclusion threshold in |
rank_adjust, rank_max_J
|
Rank-adjustment controls for |
verbose |
Print one progress line per candidate. |
pefa() chooses nothing: no factor count, threshold, persistence
horizon, collision policy, or stopping rule. Those belong to the analysis
that consumes the object.
Ordinary candidates are oblique with diagonal residuals. In bifactor mode
K counts group factors and vbfa() adds one general column, so a stored
matrix has K + 1 columns; every between-candidate comparison removes that
column first and works on the K group columns.
One private comparator produces every between-candidate number, and both
transitions and persistence are read from its stored results. The
accessors do no matching of their own: ssl(), print(), summary(), and
plot() only describe what is already stored, and none of them rematches
columns or applies an analysis rule. There is no persistence() accessor:
x$persistence$phi, $rmsd, and $collision are already the three
stored matrices.
An object of class "pefa"; see The returned object below.
A list of class "pefa" with exactly these components, in order.
sweepOne row per candidate, in column order. K is integer;
ELBO, AIC, and BIC are required finite doubles; RMSEA, SRMR,
CFI, and TLI are descriptive doubles; t is a double; iter is a
nonnegative integer, or NA_integer_ when a well-formed engine result
reports none; converged is a nonmissing logical. AIC and BIC are
hard-selection values at the resolved tau, and t is their effective
parameter count – a finite, nonnegative, whole-valued count stored as
a double – rank-adjusted when rank_adjust = TRUE. Each descriptive
index is NA_real_ exactly when its own definition does not apply, and
is never silently substituted.
transitionsOne row per adjacent pair: the integers K_from and
K_to, the doubles ELBO_gain_pct and BIC_gain_pct, then the seven
structural facts – the doubles phi_min, rmsd, rmsd_max, ari,
pip_rmsd, unmatched_ssl and the logical collision. Positive gains
favor K_to. Each percentage divides by the largest positive gain on
its own path, or is NA_real_ when no positive gain exists. Raw
criteria stay in sweep.
persistenceThree upper-triangular W by W matrices over the
fitted K values, named and ordered phi, rmsd, collision: phi
holds phi_min, rmsd holds rmsd_max, and collision is the
logical reuse mask. Diagonal and lower triangle are NA. Every cell is
a direct endpoint comparison, never a chain through intervening
candidates.
loadings, pips
Named K-indexed lists of candidate matrices
with items in rows and factors in columns: J by K, or J by
K + 1 with the general column first in bifactor mode. Loadings are
finite and PIPs lie in [0, 1].
Q0, settings
The canonical J by K0 integer backbone, and
the resolved controls bifactor, general, v0, max_it, convChk,
tolVal, tau, rank_adjust, rank_max_J, in that order. general
is NULL outside bifactor mode. settings records fitting and
fit-statistic controls only: no verbose, and no analysis rule.
The loading and PIP matrices and the criteria ELBO, AIC, BIC, and t
are required. A fitting failure or a malformed required quantity aborts the
call with an error naming the K; there is no status taxonomy and no
partially populated object. A candidate that merely failed to converge is
retained with converged = FALSE, and one aggregate warning names every
affected K.
For source column a and target column b, let s = 1 when a'b >= 0 and
-1 otherwise, and let d(a, b) = ||a - s b||^2. Backbone columns pair by
position. Each exploratory source independently selects the exploratory
target minimizing d, which is Equation 17 of Chen (2023) made invariant to
whole-column reflection. Ties break by larger finite absolute Tucker
congruence, then by smaller target index.
The assignment is independent, not one-to-one, so two sources may select the
same target. That reuse is a collision: it is recorded rather than
repaired, the pair keeps every otherwise defined measurement, and the target
left unselected still counts toward unmatched_ssl. There is no salience
screen: every column participates, and a comparison always contains exactly
K_from scored pairs.
A zero-norm column is no exception. Its aligned distance is defined, so it
takes part in the assignment and in rmsd and rmsd_max, but its Tucker
congruence is 0/0 and therefore undefined. An undefined congruence is
reported as NA_real_ and makes that comparison's phi_min NA_real_;
it is never dropped by an na.rm.
Because phi_min is a minimum over every column, an overextracted
candidate carrying a near-empty column will report a low phi_min and often
collision = TRUE. Read that with ssl(): it usually means the candidate
contains a column with nothing in it, not that the retained structure moved.
pefa() imposes no upper bound on K relative to J. The Ledermann bound
K <= (2J + 1 - sqrt(8J + 1)) / 2 orients the choice of Kmax, but it is
derived for an unrestricted factor model and does not constrain a
regularized partially confirmatory candidate, so applying it as a package
check would be wrong.
Beyond that bound expect candidates that do not converge and fit indices
that become NA: RMSEA and TLI are undefined at non-positive degrees of
freedom. Both are reported evidence about the window rather than failures.
rank_adjust = TRUE is the sharper diagnostic, since it counts the rank of
the Jacobian of the unique covariance elements and therefore tests local
identification directly; that count cannot exceed J(J + 1)/2, so its
degrees of freedom saturate at zero instead of going negative.
select_K_elbow(), the pefa() arguments cuts, sustain, and
stability_eps, and the $selected_K and $boundary components existed
in 0.8.3 and are gone. That is an intentional pre-1.0 breaking change rather
than a deprecation cycle: the package owns no count rule, so it can own
neither a selector nor a stability cutoff. The replacement is ordinary
analysis code over the stored path, as in the last chunk of the Examples,
and such code has to say which variant it is, because look-ahead and
boundary-fallback variants disagree on the same path. NEWS.md lists the
removals in full.
Chen, J., & Jin, Y. (2026). Recovering latent structures after variational Bayesian variable selection: Fit assessment and factor-number selection in partially exploratory factor analysis. arXiv preprint arXiv:2607.07159.
Chen, J. (2023). Fully and partially exploratory factor analysis with bi-level Bayesian regularization. Behavior Research Methods, 55(4), 2125–2142. doi:10.3758/s13428-022-01884-7
sim <- sim_fa(N = 300, K = 3, ipf = 4, lam = .7, lac = .3, rseed = 1) Q0 <- matrix(-1L, ncol(sim$dat), 2) groups <- rep(1:3, each = 4) for (k in 1:2) Q0[which(groups == k)[1:2], k] <- 1L fit <- pefa(Q0, sim$dat, 2, 4, verbose = FALSE) fit$sweep fit$transitions[, c("K_from", "K_to", "ELBO_gain_pct", "phi_min")] fit$persistence$phi ssl(fit) ## A count rule is the analysis's, never the package's. This one stops at ## the first ELBO gain under 20% and looks no further ahead; a ## full-look-ahead variant can answer differently on the same path. g <- fit$transitions$ELBO_gain_pct fit$transitions$K_from[which(g < 20)[1]]sim <- sim_fa(N = 300, K = 3, ipf = 4, lam = .7, lac = .3, rseed = 1) Q0 <- matrix(-1L, ncol(sim$dat), 2) groups <- rep(1:3, each = 4) for (k in 1:2) Q0[which(groups == k)[1:2], k] <- 1L fit <- pefa(Q0, sim$dat, 2, 4, verbose = FALSE) fit$sweep fit$transitions[, c("K_from", "K_to", "ELBO_gain_pct", "phi_min")] fit$persistence$phi ssl(fit) ## A count rule is the analysis's, never the package's. This one stops at ## the first ELBO gain under 20% and looks no further ahead; a ## full-look-ahead variant can answer differently on the same path. g <- fit$transitions$ELBO_gain_pct fit$transitions$K_from[which(g < 20)[1]]
Descriptive readers for the object returned by pefa(). They report what
was fitted and measured and nothing else: no method selects, recommends, or
marks a factor count, and none draws a threshold, because the package owns
no count rule. Count rules, persistence thresholds, and collision policies
belong to the analysis that consumes this object.
## S3 method for class 'pefa' print(x, ...) ## S3 method for class 'pefa' summary(object, ...) ## S3 method for class 'summary.pefa' print(x, digits = 3, ...) ## S3 method for class 'pefa' plot( x, type = c("objective", "gain", "fit"), criterion = c("ELBO", "AIC", "BIC"), pct = FALSE, ... )## S3 method for class 'pefa' print(x, ...) ## S3 method for class 'pefa' summary(object, ...) ## S3 method for class 'summary.pefa' print(x, digits = 3, ...) ## S3 method for class 'pefa' plot( x, type = c("objective", "gain", "fit"), criterion = c("ELBO", "AIC", "BIC"), pct = FALSE, ... )
... |
Further arguments; for |
object, x
|
A result returned by |
digits |
Number of decimals used when printing measured quantities. |
type |
Display: criterion trajectories ( |
criterion |
Criterion path. |
pct |
Gain view only. |
print() and plot() return their input invisibly.
summary() returns a list of class "summary.pefa" with exactly the
components window (named integer Kmin, Kmax, K0), sweep,
transitions, persistence, ssl, settings, and nonconverged_K
(an integer vector of the K values whose candidate did not converge,
integer(0) when all converged).
Prints a short summary – model type, dimensions, convergence, timing, and
selection results – instead of dumping the full list. Access the parts as
usual (fit$Lam etc.; see vbpm_fit); str(fit) shows everything.
## S3 method for class 'vbpm_fit' print(x, tau = 0.5, ...)## S3 method for class 'vbpm_fit' print(x, tau = 0.5, ...)
x |
|
tau |
Threshold on the posterior inclusion probability used only for
the "active loadings" line (default |
... |
Ignored (present for compatibility with the |
x, invisibly (the R convention for print methods, so print(fit)
can be used in pipes without losing the object).
sim <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q) fit # dispatches here: compact summary, not a list dump fit$Lam[1:3, ] # the object is still an ordinary list underneathsim <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q) fit # dispatches here: compact summary, not a list dump fit$Lam[1:3, ] # the object is still an ordinary list underneath
sim_fa can simulate data based on factor analysis or
item response models with different response formats (continuous or categorical),
loading patterns and residual covariance (local dependence) structures.
sim_fa() and sim_lvm() are complementary rather than
interchangeable: sim_fa() is the structure-side generator, driven
by the loading pattern itself – items per factor (ipf), alternating-sign
cross-loadings (alt_sign), or minor factors (minor, definition-based
per Auerswald & Moshagen, 2019) – and is the generator for vbfa().
sim_lvm() is the model-side generator, adding observed/latent
predictors (P/b, K1/ph1/b1) and mixed response formats (ilvl),
and is the generator for vbmimic(). Both generators also accept a
population loading matrix directly (mla) with identical semantics.
sim_fa( N = 1000, mla = NULL, K = 3, ipf = 8, cpf = 2, lam = 0.7, lac = 0.3, alt_sign = TRUE, phi = 0.5, ph12 = -1, gamma = NULL, ecr = 0, ome_out = FALSE, cati = NULL, noc = c(4), misp = 0, rseed = 333, necw = K, necb = K, add_ind = c(), add_la = 0, add_phi = 0, add1_ind = c(), add1_la = 0, add1_phi = 0, zero_it = 0, minor = NULL, digits = 4 )sim_fa( N = 1000, mla = NULL, K = 3, ipf = 8, cpf = 2, lam = 0.7, lac = 0.3, alt_sign = TRUE, phi = 0.5, ph12 = -1, gamma = NULL, ecr = 0, ome_out = FALSE, cati = NULL, noc = c(4), misp = 0, rseed = 333, necw = K, necb = K, add_ind = c(), add_la = 0, add_phi = 0, add1_ind = c(), add1_la = 0, add1_phi = 0, zero_it = 0, minor = NULL, digits = 4 )
N |
Sample size. |
mla |
Population loading matrix (J x K). If supplied, the pattern
arguments ( |
K |
Number of factors. |
ipf |
Items per factor. |
cpf |
Cross-loadings per factor. |
lam |
Number of formal iterations for posterior sampling. |
lac |
Number of iterations to update the sampling information. |
alt_sign |
Logical; alternate the sign of the cross-loadings across the
cross-loading items (default |
phi |
Homogeneous correlations between any two factors. |
ph12 |
Correlation between factor 1 and 2 (if it's different from
|
gamma |
Optional second-order loadings for direct higher-order
generation: a scalar or length- |
ecr |
Residual correlation (local dependence). |
ome_out |
Output factor score or not. |
cati |
The set of categorical (polytomous) items in sequence number (i.e., 1 to |
noc |
Number of categories for categorical items |
misp |
Proportion of missingness. |
rseed |
An integer for the random seed. |
necw |
Number of within-factor local dependence. |
necb |
Number of between-factor local dependence. |
add_ind |
(Additional) minor factor with cross-loadings. |
add_la |
Value of cross-loadings on (Additional) minor factor. |
add_phi |
Correlations between (Additional) minor factor and other factors. |
add1_ind |
A second (additional) minor factor with cross-loadings,
given as item indices; |
add1_la |
Value of the cross-loadings on the second minor factor. |
add1_phi |
Correlations between the second minor factor and other factors. |
zero_it |
Surplus items with zero loading. |
minor |
Optional character vector requesting orthogonal minor factors
loading on every indicator, each |
digits |
Number of significant digits to print when printing numeric values. |
An object of class list containing the data, loading, and factorial correlation matrix.
# for continuous data with cross-loadings and local dependence effect .3 out <- sim_fa(N=1000,K=3,ipf=6,lam = .7, lac=.3,ecr=.3) summary(out$dat) out$MLA out$ofd_ind # for categorical data with cross-loadings .4 and 10% missingness out <- sim_fa(N=1000,K=3,ipf=6,lam = .7, lac=.4,cati=-1,noc=4,misp=.1) summary(out$dat) out$MLA out$ofd_ind ## matrix-driven: supply the population loading matrix directly mla <- matrix(0, 12, 2) mla[1:6, 1] <- .7; mla[7:12, 2] <- c(.8, .7, .6, .7, .8, .6) s <- sim_fa(N = 300, mla = mla, phi = .3, rseed = 4) s$MLA # the generating matrix, as supplied ## higher-order (testlet) data, generated directly: 3 group factors driven ## by one general factor with second-order loadings .8, .7, .6 s <- sim_fa(N = 500, K = 3, ipf = 6, lam = .7, gamma = c(.8, .7, .6)) round(s$MLA, 2) # J x 4 Schmid-Leiman bifactor matrix (general first) s$gamma # the second-order loadings# for continuous data with cross-loadings and local dependence effect .3 out <- sim_fa(N=1000,K=3,ipf=6,lam = .7, lac=.3,ecr=.3) summary(out$dat) out$MLA out$ofd_ind # for categorical data with cross-loadings .4 and 10% missingness out <- sim_fa(N=1000,K=3,ipf=6,lam = .7, lac=.4,cati=-1,noc=4,misp=.1) summary(out$dat) out$MLA out$ofd_ind ## matrix-driven: supply the population loading matrix directly mla <- matrix(0, 12, 2) mla[1:6, 1] <- .7; mla[7:12, 2] <- c(.8, .7, .6, .7, .8, .6) s <- sim_fa(N = 300, mla = mla, phi = .3, rseed = 4) s$MLA # the generating matrix, as supplied ## higher-order (testlet) data, generated directly: 3 group factors driven ## by one general factor with second-order loadings .8, .7, .6 s <- sim_fa(N = 500, K = 3, ipf = 6, lam = .7, gamma = c(.8, .7, .6)) round(s$MLA, 2) # J x 4 Schmid-Leiman bifactor matrix (general first) s$gamma # the second-order loadings
Generates factor-analytic or item-response data with optional observed and latent predictors (MIMIC / structural models), cross-loadings, local dependence, categorical or mixed response formats, and missingness.
sim_lvm( N = 1000, mla = NULL, K = 3, J = 18, cpf = 0, lam = 0.7, lac = 0.3, phi = 0.3, ph12 = -1, phx = NULL, ecr = 0, P = 0, b = 0.3, K1 = 0, ph1 = 0.2, b1 = 0.3, phd = 0, ilvl = NULL, cati = NULL, noc = c(4), misp = 0, ome_out = FALSE, necw = K, necb = K, add_ind = c(), add_la = 0.5, add_phi = 0, zero_it = 0, rseed = 333, digits = 4 )sim_lvm( N = 1000, mla = NULL, K = 3, J = 18, cpf = 0, lam = 0.7, lac = 0.3, phi = 0.3, ph12 = -1, phx = NULL, ecr = 0, P = 0, b = 0.3, K1 = 0, ph1 = 0.2, b1 = 0.3, phd = 0, ilvl = NULL, cati = NULL, noc = c(4), misp = 0, ome_out = FALSE, necw = K, necb = K, add_ind = c(), add_la = 0.5, add_phi = 0, zero_it = 0, rseed = 333, digits = 4 )
N |
Sample size. |
mla |
Population loading matrix. If supplied, |
K |
Number of factors (used when |
J |
Number of items (used when |
cpf |
Number of cross-loadings per factor (used when |
lam |
Value of the primary loadings. |
lac |
Value of the cross-loadings. |
phi |
Homogeneous correlation between any two factors. |
ph12 |
Correlation between factors 1 and 2, when it differs from |
phx |
Correlation among the observed covariates. Defaults to |
ecr |
Residual correlation (local dependence). |
P |
Number of observed predictors (MIMIC model); |
b |
Coefficients of the observed predictors: a scalar, or a
|
K1 |
Number of latent predictors (MIMIC / structural model). |
ph1 |
Correlation among the latent predictors. |
b1 |
Coefficients of the latent predictors: a scalar, or a
|
phd |
Correlation among the structural disturbances (the residuals of
the regression of factors on predictors). |
ilvl |
Levels of every item, length |
cati |
Polytomous items by index; |
noc |
Number of categories for the polytomous items. |
misp |
Proportion of missingness, applied to the |
ome_out |
Return the factor scores. |
necw |
Number of within-factor local dependencies. |
necb |
Number of between-factor local dependencies. |
add_ind |
Indices of items loading on an additional minor factor. |
add_la |
Loading value on the additional minor factor. |
add_phi |
Correlation between the additional minor factor and the others. |
zero_it |
Number of surplus items with zero loadings. |
rseed |
Random seed. |
digits |
Significant digits used while the function is running. |
sim_lvm() and sim_fa() are complementary rather than interchangeable.
Both accept a population loading matrix directly (mla, identical
semantics in either function); where they differ is what drives the
pattern-based path when mla = NULL, and what else each one models:
sim_lvm()is the model-side generator. Use it when the design
involves predictors – observed covariates (P, b) or latent
predictors (K1, ph1, b1) – or mixed response formats per
item (ilvl). This is the generator for vbmimic().
sim_fa()is the structure-side generator. Use it when the
question is about the loading pattern itself: items per factor (ipf),
alternating-sign cross-loadings (alt_sign), or minor factors
(minor, definition-based per Auerswald & Moshagen, 2019). This is the
generator for vbfa().
The two generators share ph12 for the factor 1-2 correlation. ph1
exists only in sim_lvm(), where it means something different: the
correlation among latent predictors (K1), not the factor 1-2
correlation.
A list with the data (dat; items first, then any covariates),
the loading matrix (MLA), factor correlations (PHI), residual
covariance (PSX), the indices of the local-dependence pairs
(ofd_ind), and, when predictors are present, the coefficient matrices
(mb, mb1) and covariate correlation (PHX).
Chen, J. (2022). LAWBL: Latent (variable) analysis with Bayesian learning (R package version 1.5.0).
Jin, Y., & Chen, J. (2025). Regularized variational Bayesian approximations for variable selection in extended multiple-indicators multiple-causes models. Multivariate Behavioral Research. doi:10.1080/00273171.2025.2483253
sim_fa() for the structure-side generator, vbmimic() and
vbfa() for the estimators.
## MIMIC data: 18 items on 3 factors, 9 covariates in disjoint blocks of 3. ## Supply `b` as a matrix for a sparse structural design; a scalar `b` makes ## every covariate predict every factor, which quickly leaves no disturbance ## variance (the function stops if it does). B <- matrix(0, 3, 9) for (k in 1:3) B[k, ((k - 1) * 3 + 1):(k * 3)] <- .3 s <- sim_lvm(N = 500, K = 3, J = 18, P = 9, b = B, phx = 0, rseed = 1) dim(s$dat) # 500 x 27 -- items first, covariates last s$mb # the generating structural coefficients ## correlated disturbances (unrestricted factor residual covariance) s2 <- sim_lvm(N = 500, K = 3, J = 18, P = 9, b = B, phx = 0, phd = .3, rseed = 1) round(s2$PHI, 2) ## matrix-driven: supply the population loading matrix directly mla <- matrix(0, 12, 2) mla[1:6, 1] <- .7; mla[7:12, 2] <- c(.8, .7, .6, .7, .8, .6) s3 <- sim_lvm(N = 300, mla = mla, phi = .3, rseed = 4) s3$MLA # the generating matrix, as supplied## MIMIC data: 18 items on 3 factors, 9 covariates in disjoint blocks of 3. ## Supply `b` as a matrix for a sparse structural design; a scalar `b` makes ## every covariate predict every factor, which quickly leaves no disturbance ## variance (the function stops if it does). B <- matrix(0, 3, 9) for (k in 1:3) B[k, ((k - 1) * 3 + 1):(k * 3)] <- .3 s <- sim_lvm(N = 500, K = 3, J = 18, P = 9, b = B, phx = 0, rseed = 1) dim(s$dat) # 500 x 27 -- items first, covariates last s$mb # the generating structural coefficients ## correlated disturbances (unrestricted factor residual covariance) s2 <- sim_lvm(N = 500, K = 3, J = 18, P = 9, b = B, phx = 0, phd = .3, rseed = 1) round(s2$PHI, 2) ## matrix-driven: supply the population loading matrix directly mla <- matrix(0, 12, 2) mla[1:6, 1] <- .7; mla[7:12, 2] <- c(.8, .7, .6, .7, .8, .6) s3 <- sim_lvm(N = 300, mla = mla, phi = .3, rseed = 4) s3$MLA # the generating matrix, as supplied
Post-processes an orthogonal bifactor vbfa() fit into the three
summaries used throughout the bifactor vignette: (i) the
Schmid-Leiman (SL) proportionality check – the within-group
coefficient of variation (CV) of the group-to-general loading ratio, per
group factor and item-weighted overall; (ii) the (approximate)
higher-order parameterization – second-order loadings
from each group's mean ratio
, and first-order loadings
; and (iii)
testlet/special effect sizes by Eq. 16 of Zhang and Chen (2024): the
average squared loading on each group factor, counting specified loadings
always and unspecified ones only when their posterior inclusion
probability exceeds tau.
special_effects(object, tau = 0.5) ## S3 method for class 'special_effects' print(x, digits = 3, ...)special_effects(object, tau = 0.5) ## S3 method for class 'special_effects' print(x, digits = 3, ...)
object |
A fitted |
tau |
Hard-selection threshold on the posterior inclusion
probability (default |
x |
A |
digits |
Number of decimals in the printed table. |
... |
Unused. |
All output is descriptive. Near-zero CVs signal SL proportionality –
the fitted bifactor is then (approximately) a higher-order model, and by
the same token a testlet model, so the higher-order parameters are a
faithful re-expression. Values near .1 are conventionally read as
approximately higher-order, but the cutoff is suggestive, not a test; the
higher-order parameters and effect sizes are reported regardless, and
with clearly non-constant ratios they are a deliberately lossy summary of
a genuinely richer bifactor. The ratio inherits the sampling softness of
the general/group split, so CVs are noisy below (roughly) a few thousand
observations – see the bifactor vignette for demonstrations, caveats,
and the covariance geometry behind all three summaries.
Which fits are accepted. A fit with bifactor = TRUE is used as-is
(general column first, group columns after). A legacy hand-built fit is
also accepted when it has orthogonal = TRUE and exactly one
all-specified column in Q, which is taken as the general factor. Group
membership of each item is the group column where it is anchored
(Q == 1) or, for unspecified rows, its single active group column
(inclusion probability above tau). Items with no active group loading
are excluded from the ratio summaries and reported; items active on
several group columns are assigned to the anchored one (else the largest
absolute loading) and reported. Ratios use absolute loadings; items whose
general loading is fixed at zero (bifactor-(S-1)-style designs) carry no
ratio and are reported.
An object of class special_effects: a list with
groups |
A data frame with one row per group factor: |
mean_cv |
The item-weighted mean CV across group factors. |
items |
A data frame with one row per item: assigned |
gamma |
Named vector of second-order loadings (same as in
|
unassigned, crossloaded, no_general
|
Integer vectors of item indices flagged by the membership rules above. |
tau, source, call
|
Bookkeeping ( |
Schmid, J., & Leiman, J. M. (1957). The development of hierarchical factor solutions. Psychometrika, 22(1), 53-61.
Yung, Y.-F., Thissen, D., & McLeod, L. D. (1999). On the relationship between the higher-order factor model and the hierarchical factor model. Psychometrika, 64(2), 113-128.
Zhang, Y., & Chen, J. (2024). Accommodating and extending various models for special effects within the generalized partially confirmatory factor analysis framework. Applied Psychological Measurement, 48(4-5), 208-229. doi:10.1177/01466216241261704
vbfa(), and the bifactor vignette for the full workflow.
## higher-order data: the ratios are near-constant and gamma is recovered sim <- sim_fa(N = 500, K = 3, ipf = 6, lam = .8, gamma = c(.8, .7, .6), rseed = 11) Q <- matrix(-1L, 18, 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q, bifactor = TRUE, v0 = .001) special_effects(fit)## higher-order data: the ratios are near-constant and gamma is recovered sim <- sim_fa(N = 500, K = 3, ipf = 6, lam = .8, gamma = c(.8, .7, .6), rseed = 11) Q <- matrix(-1L, 18, 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q[a, ] <- 0L; Q[a, k] <- 1L } fit <- vbfa(sim$dat, Q, bifactor = TRUE, v0 = .001) special_effects(fit)
Descriptive column sizes for every candidate in a pefa() sweep:
colSums(Lam^2) on the stored loading matrix of each candidate. They are
not Gram eigenvalues, they take part in no correspondence or selection
rule, and no cutoff is applied to them. summary() calls this function, so
there is one definition.
ssl(x)ssl(x)
x |
A result returned by |
A low phi_min in $transitions or $persistence$phi is a weakest-link
reading over every source column, including one the fit has driven to
nearly zero. Such a column matches the smallest-norm target and carries an
essentially arbitrary congruence. ssl() is what separates "this candidate
contains a column with nothing in it" from "the retained structure moved".
A list with one element per candidate, named as x$loadings is
(the candidate K as a character string). Each element is the double
vector colSums(L^2) of that candidate's loading matrix and keeps the
stored loading-column names when the matrix has them. In bifactor mode
each vector covers all K + 1 stored columns with the general factor
first, so the group-block scale used by every between-candidate
comparison is ssl(x)[[k]][-1].
Fits a partially confirmatory factor-analytic measurement model by
regularized mean-field variational Bayes. Loadings on specified factors
are anchored by the design matrix Q; loadings on unspecified factors are
selected from the data through continuous spike-and-slab priors. Optional
features are a warm-started dynamic regularization path, an orthogonal
bifactor parameterization, and sparse residual (local-dependence)
estimation.
vbfa( Y, Q, ld = FALSE, Qe = NULL, orthogonal = FALSE, bifactor = FALSE, general = 1, v0 = c(0.01, 0.005, 0.002, 0.001), max_it = 5000, convChk = FALSE, tolVal = 1e-04, ld_control = list() )vbfa( Y, Q, ld = FALSE, Qe = NULL, orthogonal = FALSE, bifactor = FALSE, general = 1, v0 = c(0.01, 0.005, 0.002, 0.001), max_it = 5000, convChk = FALSE, tolVal = 1e-04, ld_control = list() )
Y |
Numeric |
Q |
Integer |
ld |
Logical switch for local-dependence (sparse residual) estimation.
|
Qe |
Residual design matrix ( |
orthogonal |
Logical. If |
bifactor |
Logical. If |
general |
Design of the general column when |
v0 |
Loading-spike variance. A decreasing vector gives a
warm-started dynamic regularization path (each stage runs to convergence
and warm-starts the next); a scalar gives a single fixed spike. The
default path ends at For a gentler path that aids convergence, pass a finer, longer,
log-spaced descent starting from a larger |
max_it |
Maximum variational iterations per stage. Default |
convChk |
Logical; report per-iteration relative error via
|
tolVal |
Convergence tolerance on the maximum relative error. |
ld_control |
A named list of local-dependence controls, read only when
|
An object of class c("vbfa", "vbpm_fit") – a named list (see
vbpm_fit for what the class does and does not change) with elements:
J x K posterior mean loadings.
Posterior inclusion probabilities of the unspecified loadings
(specified entries are reported as coded in Q).
N x K posterior mean factor scores.
Factor correlation matrix; the identity when
orthogonal = TRUE.
Residual precisions; NULL when ld = TRUE.
Evidence lower bound; NA when ld = TRUE.
Posterior inclusion rate of the unspecified loadings.
Posterior variances of the loadings.
Posterior covariance of the factor scores.
Posterior mean of the factor precision, E[Phi^-1].
Total iterations, 1 if the final stage
converged within max_it (else 0), and elapsed time.
The design matrices and settings the model
was fit with (Qe is NULL when ld = FALSE); read by fit_stats()
and print.vbpm_fit() so they never need repeating.
Bifactor bookkeeping: whether the
fit declared a bifactor structure, how many general columns it has,
and the general design (NULL unless bifactor = TRUE).
Local-dependence results (NULL
when ld = FALSE): residual precision, its inverse, residual-edge
inclusion probabilities, edge shrinkage, and the LD proportion.
The v0 schedule (and xi0 under LD) with per-stage
iteration counts.
Centering and scaling used, n_missing, the response
type, and missing_mask – the mask is stored only when the data
actually contain missing values, and is NULL otherwise.
The sample correlation matrix the fit statistics use.
Every quantity appears exactly once: as of 0.7.0 the fit no longer
carries $coefficients, $design, $posterior, or $settings, which
duplicated the elements above.
The diagonal-residual estimator is deterministic (fixed initialization,
no random numbers), so results are reproducible without a seed; there is no
rseed argument. (The local-dependence branch's QUIC solver shuffles its
active set with the RNG, but that is a convex solve, so the shuffle order
does not change the solution.)
Missing continuous responses are supported (see Y). Support for
categorical or mixed responses is not yet implemented.
Under local dependence ELBO is NA by design, and this is not a
placeholder: the residual precision and the mixing proportion are point
updates rather than variational factors, so no single joint mean-field
bound over all unknowns is defined. The quantities that are defined are
returned under their own names — objective (the terminal VECM objective,
with objective_type = "vecm") and ELBO_conditional (the conditional
bound given the terminal precision and mixing proportion). Use objective
for model comparison, and only across fits sharing one objective_type.
Chen, J., Guo, Z., Zhang, L., & Pan, J. (2021). A partially confirmatory approach to scale development with the Bayesian Lasso. Psychological Methods, 26(2), 210-235. doi:10.1037/met0000293
Chen, J. (2021). A generalized partially confirmatory factor analysis framework with mixed Bayesian Lasso methods. Multivariate Behavioral Research, 57(6), 879-894. doi:10.1080/00273171.2021.1925520
Jin, Y., & Chen, J. (2025). Regularized variational approximation for partially confirmatory factor analysis. Structural Equation Modeling: A Multidisciplinary Journal, 32(3), 437-449. doi:10.1080/10705511.2024.2432612
Jin, Y., Chen, J., Yan, Z., & Zhang, Y. (2026). Sparse residual estimation
in partially confirmatory factor analysis. PsyArXiv preprint.
doi:10.31234/osf.io/dehtv_v2 — the warm-started v0 path and the
ld = TRUE sparse-residual (local dependence) branch.
Rockova, V., & George, E. I. (2018). The spike-and-slab LASSO. Journal of the American Statistical Association, 113(521), 431-444. doi:10.1080/01621459.2016.1260469
## partially confirmatory: two anchors per factor, the rest exploratory sim <- sim_fa(N = 200, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2]; Q[a, ] <- 0; Q[a, k] <- 1 } fit <- vbfa(sim$dat, Q) fit$flag # 1 if converged round(fit$Lam, 2) # loadings round(fit$pi, 2) # posterior inclusion probabilities ## orthogonal bifactor, preferred form: Q holds only the GROUP design and ## K counts group factors (comparable to an oblique K-factor model) fb <- vbfa(sim$dat, Q, bifactor = TRUE, v0 = .001) # fixed spike: fast demo fb # prints "1 general + 3 group factors" fb$Phi # identity by construction ## equivalent long form: vbfa(sim$dat, cbind(1L, Q), orthogonal = TRUE) ## local dependence: simulate correlated residuals, then set ld = TRUE simLD <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, ecr = .3, rseed = 2) fLD <- vbfa(simLD$dat, Q, ld = TRUE, max_it = 300, tolVal = 1e-3) ## largest recovered residual edges vs the planted ones Poff <- abs(fLD$Psi); Poff[lower.tri(Poff, diag = TRUE)] <- 0 which(Poff >= sort(Poff, decreasing = TRUE)[3], arr.ind = TRUE) simLD$ofd_ind ## empirical illustration: NLSY 1997, 27 items treated as continuous. ## A 500-row subset keeps the example quick; the vignette fits all 3,458. data(nlsy27) Yn <- as.matrix(nlsy27$dat)[1:500, ] sum(is.na(Yn)) # incomplete cells, imputed in-loop fn <- vbfa(Yn, nlsy27$Q) round(fn$Lam, 2)## partially confirmatory: two anchors per factor, the rest exploratory sim <- sim_fa(N = 200, K = 3, ipf = 6, lam = .7, lac = .3, rseed = 1) Q <- matrix(-1L, ncol(sim$dat), 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2]; Q[a, ] <- 0; Q[a, k] <- 1 } fit <- vbfa(sim$dat, Q) fit$flag # 1 if converged round(fit$Lam, 2) # loadings round(fit$pi, 2) # posterior inclusion probabilities ## orthogonal bifactor, preferred form: Q holds only the GROUP design and ## K counts group factors (comparable to an oblique K-factor model) fb <- vbfa(sim$dat, Q, bifactor = TRUE, v0 = .001) # fixed spike: fast demo fb # prints "1 general + 3 group factors" fb$Phi # identity by construction ## equivalent long form: vbfa(sim$dat, cbind(1L, Q), orthogonal = TRUE) ## local dependence: simulate correlated residuals, then set ld = TRUE simLD <- sim_fa(N = 300, K = 3, ipf = 6, lam = .7, lac = .3, ecr = .3, rseed = 2) fLD <- vbfa(simLD$dat, Q, ld = TRUE, max_it = 300, tolVal = 1e-3) ## largest recovered residual edges vs the planted ones Poff <- abs(fLD$Psi); Poff[lower.tri(Poff, diag = TRUE)] <- 0 which(Poff >= sort(Poff, decreasing = TRUE)[3], arr.ind = TRUE) simLD$ofd_ind ## empirical illustration: NLSY 1997, 27 items treated as continuous. ## A 500-row subset keeps the example quick; the vignette fits all 3,458. data(nlsy27) Yn <- as.matrix(nlsy27$dat)[1:500, ] sum(is.na(Yn)) # incomplete cells, imputed in-loop fn <- vbfa(Yn, nlsy27$Q) round(fn$Lam, 2)
Fits a partially confirmatory multiple-indicators multiple-causes (MIMIC)
model by a variational Bayes EM algorithm, with continuous spike-and-slab
regularization on both the measurement and the structural part. Items
load on factors through a measurement design matrix Q_A, while covariates
predict the factors through a structural design matrix Q_B; entries of
either that are left unspecified are selected from the data.
vbmimic( Y, X, Q_A, Q_B, v0 = 0.001, standardize = FALSE, max_it = 1000, convChk = FALSE, tolVal = 1e-05 )vbmimic( Y, X, Q_A, Q_B, v0 = 0.001, standardize = FALSE, max_it = 1000, convChk = FALSE, tolVal = 1e-05 )
Y |
Numeric |
X |
Numeric |
Q_A |
Integer |
Q_B |
Integer |
v0 |
Spike variance. A scalar gives a single fixed spike; a decreasing
vector gives a warm-started regularization path, each stage initialized at
the previous stage's solution (Rockova & George, 2018). The same schedule
is applied to the measurement and structural parts. |
standardize |
Logical; standardize |
max_it |
Maximum number of variational iterations per stage. |
convChk |
Logical; print per-iteration convergence information via
|
tolVal |
Convergence tolerance on the maximum absolute change in the weighted residual vector. |
This is the MIMIC member of the vbpm family and is the companion of
vbfa(), which fits the measurement part alone. The two share the -1/0/1
design-matrix convention and the spike-and-slab formulation.
An object of class c("vbmimic", "vbpm_fit") – a named list (see
vbpm_fit) with components:
J x K posterior mean measurement loadings.
K x P posterior mean structural coefficients.
Posterior inclusion probabilities for the unspecified
entries of Q_A and Q_B (specified entries are held at 1, fixed
zeros at 0).
The design matrices the model was fit with.
N x K posterior mean factor scores.
K x K factor correlation matrix implied by Sig.
Factor covariance matrix and its inverse.
Length-J residual precisions.
Inclusion rates for the measurement and structural parts.
Posterior variances of the loadings and structural coefficients.
Total iterations, 1 if the final stage
converged within max_it (else 0), and elapsed time.
NA_real_; see Note.
The standardize setting the model was fit with.
The v0 schedule and per-stage iteration counts.
n_missing, the response type, and missing_mask –
stored only when the data contain missing values, NULL otherwise.
Every quantity appears exactly once: as of 0.7.0 the fit no longer
carries $coefficients, $design, $posterior, or $settings, which
duplicated the elements above.
The evidence lower bound is not currently returned. The published
algorithm converges on the weighted residual vector rather than on the
bound, and the bound's implementable closed form has not been assembled.
fit_stats() does accept a vbmimic fit, but returns a deliberately
limited result: n_active_coef (soft-selected measurement and structural
coefficients) plus typed NA for every covariance-based index and
parameter count, with an explanatory note attribute. pefa() does not
accept vbmimic fits at all: its sweep reports criterion gains across a
factor-count window, which requires one comparable objective for every
candidate in that window.
Jin, Y., & Chen, J. (2025). Regularized variational Bayesian approximations for variable selection in extended multiple-indicators multiple-causes models. Multivariate Behavioral Research. doi:10.1080/00273171.2025.2483253
Rockova, V., & George, E. I. (2018). The spike-and-slab LASSO. Journal of the American Statistical Association, 113(521), 431-444.
vbfa() for the measurement model alone, sim_lvm() to simulate
MIMIC data.
## 3 factors, 18 items, 9 covariates in disjoint blocks of 3 B <- matrix(0, 3, 9) for (k in 1:3) B[k, ((k - 1) * 3 + 1):(k * 3)] <- .3 sim <- sim_lvm(N = 300, K = 3, J = 18, P = 9, b = B, phx = 0, rseed = 1) Y <- sim$dat[, 1:18] X <- sim$dat[, 19:27] ## two anchored items per factor; the structural part left exploratory. ## Anchoring at least one part is advisable: with both Q_A and Q_B fully ## exploratory the solution can be rotationally ambiguous. Q_A <- matrix(-1L, 18, 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q_A[a, ] <- 0L; Q_A[a, k] <- 1L } Q_B <- matrix(-1L, 3, 9) fit <- vbmimic(Y, X, Q_A, Q_B) fit$flag # 1 if converged round(fit$B, 2) # structural coefficients## 3 factors, 18 items, 9 covariates in disjoint blocks of 3 B <- matrix(0, 3, 9) for (k in 1:3) B[k, ((k - 1) * 3 + 1):(k * 3)] <- .3 sim <- sim_lvm(N = 300, K = 3, J = 18, P = 9, b = B, phx = 0, rseed = 1) Y <- sim$dat[, 1:18] X <- sim$dat[, 19:27] ## two anchored items per factor; the structural part left exploratory. ## Anchoring at least one part is advisable: with both Q_A and Q_B fully ## exploratory the solution can be rotationally ambiguous. Q_A <- matrix(-1L, 18, 3) for (k in 1:3) { a <- which(rep(1:3, each = 6) == k)[1:2] Q_A[a, ] <- 0L; Q_A[a, k] <- 1L } Q_B <- matrix(-1L, 3, 9) fit <- vbmimic(Y, X, Q_A, Q_B) fit$flag # 1 if converged round(fit$B, 2) # structural coefficients
Every estimator in vbpm returns its results with an S3 class attached:
vbfa() returns c("vbfa", "vbpm_fit") and vbmimic() returns
c("vbmimic", "vbpm_fit"). This page explains what that means in
practice, because "S3 class" sounds like more machinery than it is.
An S3 class is nothing more than a label attached to an ordinary R
object. A vbpm_fit is still a plain named list – the class attribute
changes none of its contents and none of the ways you already use it:
fit$Lam, fit$eta, fit$ELBO, ... work exactly as before;
names(fit) lists every component;
str(fit) shows the full structure;
unclass(fit) strips the label and gives back the bare list, should you
ever want it.
The label does exactly two things. First, generic functions such as
print() can now dispatch: typing fit at the console no longer
floods the screen with every matrix in the object, but shows a compact
summary (model type, dimensions, convergence, ELBO, active loadings).
Second, functions receiving a fit can recognize what it is: this is
how fit_stats() knows a bifactor fit from an oblique one without being told
(the fit carries its own orthogonal and ld settings), and how it sends a
vbmimic() fit to its own method. That method, fit_stats.vbmimic(),
returns a populated but deliberately limited result – NA for every
SEM-like index, plus a single n_active_coef count of soft-selected
measurement and structural coefficients – because the MIMIC objective and
joint covariance derivation those indices need are not available yet. It is
the family fallback fit_stats.vbpm_fit() that stops instead of computing
something meaningless, and it applies to classes with no method of their
own.
The specific class ("vbfa", "vbmimic") comes first, the family class
"vbpm_fit" second. Methods written for vbpm_fit therefore apply to
every model in the family unless a model overrides them – one print
method serves all estimators, and future family members (e.g. further
variational models) inherit sensible behaviour on day one.
A natural-looking alternative is a single "vbpm_fit" class with a field
saying which model it is. The field exists – every fit carries
$model ("vbfa" or "vbmimic"), and it is the right thing to read
when a script wants to branch on model type. But it cannot replace the
class vector, because the class vector is what R's method dispatch runs
on. With the two-element class, model-specific behaviour is added by
writing a method (fit_stats.vbmimic()), the family default by writing
fit_stats.vbpm_fit(), and a future model (say vblvm) joins by
returning c("vblvm", "vbpm_fit") and adding only the methods where it
differs – including from outside this package. With a single class, every
generic would have to contain a hand-written switch(object$model, ...)
that this package alone can extend, and inherits(fit, "vbfa") would stop
working. This is the same convention base R uses: a glm object has class
c("glm", "lm"), not class "lm" with a family field. So: dispatch on the
classes, read $model for display and bookkeeping.
Some R packages wrap results in opaque objects whose internals are
discouraged territory. vbpm does the opposite: the list components are
documented in each estimator's help page (?vbfa, ?vbmimic) and are
part of the public API. The class adds convenience on top; it takes
nothing away.
vbfa(), vbmimic(), print.vbpm_fit()