Variational Bayes Psychometric Models
vbpm is a home for variational Bayes estimation of
psychometric measurement models. Its models are partially
confirmatory: they span the confirmatory-to-exploratory continuum,
with parameters on specified paths anchored by a design matrix
and parameters on unspecified paths selected from the data by
continuous spike-and-slab priors.
The package provides variational Bayes factor
analysis (vbfa), where a design matrix
Q governs the item loadings, and regularized
MIMIC (vbmimic), which adds covariates predicting
the factors and regularizes both the measurement part (Q_A)
and the structural part (Q_B).
vbfa() — the factor-analysis
estimator.
v0 a
decreasing vector for a warm-started regularization path (Ročková &
George, 2018; Jin, Chen, Yan, & Zhang, 2026); a scalar
v0 is a single fixed spike. v0 = 0.001
reproduces the fixed-spike estimator exactly.bifactor = TRUE fits one
general factor plus K orthogonal group factors, with
Q supplying only the group design – so K
counts group factors, comparable to an oblique K-factor
model. The general argument designs the general column
(including bifactor-(S-1)-style zeros). orthogonal = TRUE
remains available for plain orthogonal models (or hand-built bifactor
designs).ld = TRUE to
estimate a sparse residual precision by a graphical spike-and-slab prior
(QUIC) — fully exploratory by default, or restricted by a residual
design matrix Qe (Jin, Chen, Yan, & Zhang, 2026).vbmimic() — the MIMIC estimator.
Q_A, items on factors) and the
structural design (Q_B, factors on covariates).v0 path. The defaults
reproduce the published estimator exactly.fit_stats() returns a clearly marked limited result and
pefa() sweeps vbfa() candidates only.fit_stats() — SEM-like fit statistics
(RMSEA, SRMR, CFI, TLI, AIC, BIC) plus the model’s objective. An S3
generic over fitted models: it builds the implied covariance from the
residual covariance, so it is correct for diagonal and
local-dependence vbfa fits, and it reads
orthogonal/ld from the fit rather than asking
you to repeat them. Information criteria use the nominal hard-selected
parameter count by default; the numerical Jacobian-rank count is an
explicit rank_adjust = TRUE sensitivity.special_effects() — post-process a
bifactor fit: Schmid-Leiman proportionality CVs, approximate
higher-order parameters (second-order loadings per group factor), and
testlet/special effect sizes (Zhang & Chen, 2024, Eq. 16).pefa() — fit one partially exploratory
model for every K in a fixed consecutive window and report
what was measured. It selects no factor count and carries no cut,
threshold, persistence horizon, collision policy, or stopping rule;
composing the numbers into a conclusion is yours. Three tables:
$sweep — one row per candidate: K,
ELBO, AIC, BIC,
RMSEA, SRMR, CFI,
TLI, t, iter,
converged.$transitions — one row per adjacent pair:
percent-of-largest-positive ELBO and BIC gains, plus the seven
structural facts phi_min, rmsd,
rmsd_max, ari, pip_rmsd,
unmatched_ssl, collision.$persistence — three upper-triangular matrices,
phi (holding phi_min), rmsd
(holding rmsd_max), and a logical collision
mask, covering every ordered pair. Read them directly as
x$persistence$phi; there is no accessor, because selecting
a stored triangle would only be a synonym for $.a - s b, with s = 1 when
a'b >= 0 and -1 otherwise — Equation 17 of
Chen (2023) made invariant to whole-column reflection. Backbone columns
pair by position, each exploratory column then chooses independently,
and congruence only breaks exact ties. The assignment is deliberately
not one-to-one: when two columns choose the same target that reuse is a
collision, reported as evidence and never repaired into
a forced matching. Every ordered pair is compared directly from its two
endpoints, never chained through the candidates between them. Candidate
matrices live in K-named $loadings and $pips
lists (result$loadings[["4"]]). Convergence is
recorded, never used to suppress a number that is defined: a
candidate that stopped at max_it keeps its matrices and all
of its comparisons.ssl() — per-candidate sums of squared
loadings, colSums(Lam^2), on the same scale as
unmatched_ssl. It screens nothing and enters no
correspondence rule; it is what distinguishes a low phi_min
caused by a near-empty column from one caused by structure actually
moving. Compact print(), summary(), and
plot() methods complete the set; none of them announces a
selected count, because none is chosen.sim_fa() and
sim_lvm() — two complementary data
generators. sim_fa() is driven by the loading
pattern (items per factor, alternating cross-loadings, minor
factors) and can generate higher-order/testlet data
directly (gamma = second-order loadings, via the
Schmid-Leiman equivalence); sim_lvm() is driven by a
loading matrix and covers predictor designs (observed and
latent) and mixed response formats. Both simulate local dependence,
categorical items, and missingness.vbpm ships C++ code — the QUIC solver behind the
local-dependence branch — so installing it compiles that code and a C++
toolchain must be present. Many R users already have one (any package
installed from source needs it); check before installing
anything:
# install.packages("pkgbuild")
pkgbuild::has_build_tools(debug = TRUE) # TRUE = you are ready, skip to step 2If that returns FALSE:
| system | what to install | where |
|---|---|---|
| Windows | Rtools, matched to your R version — Rtools45 for R
4.5.x, Rtools44 for R 4.4.x (check with getRversion()). Run
the installer with its defaults; ≈500 MB installed. |
https://cran.r-project.org/bin/windows/Rtools/ |
| macOS | Xcode command line tools (not the full Xcode app):
run xcode-select --install in Terminal and accept the
prompt; ≈1 GB. |
https://mac.r-project.org/tools/ |
| Linux | Your distribution’s R development packages:
sudo apt install r-base-dev (Debian/Ubuntu) or
sudo dnf install R-devel gcc-c++ (Fedora/RHEL); a few tens
of MB. |
— |
Restart R afterwards, then re-run the has_build_tools()
check.
# install.packages("remotes")
remotes::install_github("Jinsong-Chen/vbpm", build_vignettes = TRUE)Or clone and install from source:
git clone https://github.com/Jinsong-Chen/vbpm.git
R CMD INSTALL vbpmThen:
library(vbpm)
vignette("vbfa") # start here
vignette("pefa") # the factor-count sweep and its three tables
vignette("bifactor") # bifactor structures, AO/AZ backbones, and sweeps
vignette("vbmimic") # covariates predicting the factors
citation("vbpm")Building the vignettes needs knitr,
rmarkdown, and pandoc (bundled with RStudio; from a plain
shell, point RSTUDIO_PANDOC at it).
library(vbpm)
sim <- sim_fa(N = 500, K = 3, ipf = 6, lam = .7, lac = .3)
Y <- sim$dat
## partially confirmatory Q: 1 = anchor, 0 = fixed zero, -1 = estimate
Q <- matrix(-1, ncol(Y), 3)
Q[1:2, ] <- 0; Q[1:2, 1] <- 1 # two anchors on factor 1 only
fit <- vbfa(Y, Q) # dynamic path on by default; quiet
fit # compact summary (S3 print method)
idx <- fit_stats(fit) # nominal hard-selected count by defaultFits are ordinary named lists with a shared vbpm_fit
class attached — fields such as fit$Lam,
fit$pi, and fit$Phi remain public API. Focused
vignettes cover vbfa, one pefa() sweep walked
table by table under overextraction, bifactor/higher-order/testlet
structures with AO and AZ backbones, and vbmimic.
MIMIC, with covariates predicting the factors:
B <- matrix(0, 3, 9) # sparse structural design: 3 covariates
for (k in 1:3) B[k, (k*3-2):(k*3)] <- .3 # per factor
sim <- sim_lvm(N = 500, K = 3, J = 18, P = 9, b = B, phx = 0)
Y <- sim$dat[, 1:18] # items first ...
X <- sim$dat[, 19:27] # ... covariates last
## anchor two items per factor; leave the structural part exploratory.
## (With BOTH parts fully exploratory the solution can be rotationally
## ambiguous -- anchor at least one part.)
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)
round(fit$B, 2) # which covariates predict which factorsAs of 0.9.1:
vbfa() and
vbmimic(). Categorical and mixed responses are out
of scope for the current line of releases — supporting them
means adding a threshold model and augmenting latent responses, which is
a modelling extension rather than a feature. Simulating them is
supported (cati/noc in sim_fa(),
ilvl in sim_lvm()); estimating from them is
not.pefa() sweeps are a research
extension. Their K counts group factors, and every
comparison removes the labelled general column before applying exactly
the loading/PIP correspondence an ordinary oblique sweep uses. General
and group columns can nevertheless redistribute common variance as
K changes, so a study that needs both modes should run them
on the same data, backbone, and window rather than assume that either
route must agree. The bifactor vignette shows one such pair
of sweeps, where the bifactor criterion path and persistence triangle
separate the candidates far less than the ordinary ones do. That is a
property of that example, not a general ranking of the two modes.vbmimic(). Covariates
predict the factors (Q_B, i.e. impact); there are
no direct covariate-to-item paths, so the model cannot currently express
or detect differential item functioning.vbmimic()
requires complete X: covariates are conditioned on rather
than modelled, so the model supplies no distribution to impute them
from.vbfa()
fits return the terminal VECM objective and LD-aware fit statistics.
ELBO is intentionally NA: calling the
point-updated precision objective a joint variational lower bound would
be misleading.vbmimic() converges on
a residual criterion rather than the bound, so ELBO is
NA. fit_stats() describes the available
parameter counts but covariance fit indices await a fully specified
joint covariance.Q_A
and Q_B left entirely unspecified the model converges
cleanly but the solution can be rotationally ambiguous. Anchor at least
one part.vbfa() reproduces its published reference output. A
stored-reference regression test checks that result during every
R CMD check, at a strict relative tolerance of
1e-8 that remains sensitive to material regression. It is
not asserted bitwise: the estimator runs through compiled BLAS/LAPACK,
whose SIMD and FMA choices differ between platforms, so the last one or
two ulps are a property of the build rather than of the estimator.
See NEWS.md for the per-version changelog.
GPL-3.