Contents

Overview Experimental (0.18)

The cvm family models structural equation models that contain both latent variables (common factors) and composites. It implements the factor- and composite-based SEM framework of Schamberger, Schuberth, Henseler and Rosseel (2025), and extends it in the way psychonetrics extends other model families: every variance–covariance structure in the model can also be modeled as a network.

Experimental: this family is new in psychonetrics 0.18. The estimates have been checked against lavaan and in simulations, but arguments, defaults and output may still change. Please report unexpected behavior on GitHub.

Latent Variables and Composites

A latent variable is a common cause of its indicators and is measured with error. A composite is the opposite: an exact weighted sum of its indicators, without any error. Both are called constructs below. Every observed variable is either an indicator of latent variables ($\boldsymbol{y}_f$) or an indicator of exactly one composite ($\boldsymbol{y}_c$):

$$\boldsymbol{y}_f = \boldsymbol{\nu}_f + \boldsymbol{\Lambda}\,\boldsymbol{\eta}_f + \boldsymbol{\varepsilon}, \qquad \boldsymbol{\eta}_c = \boldsymbol{W}^{\top} \boldsymbol{y}_c, \qquad \boldsymbol{\eta} = \boldsymbol{\nu}_\eta + \boldsymbol{B}\,\boldsymbol{\eta} + \boldsymbol{\zeta}$$

The composite model does not explain why the indicators of a composite are correlated: these variances and covariances are free parameters. What it does impose is that the indicators of a composite relate to everything else in the model only through the composite. That restriction is what makes the model testable.

Observed variables in the structural model: a composite with a single indicator is that observed variable itself. This is the way to use an observed variable as a predictor or outcome, as in the first example below. A latent variable with a single indicator (one loading in lambda) gives the same model. A variable cannot load directly on a composite: effects between constructs are always specified in beta.

Model Matrices and the _c Suffix

The family has three covariance structures, each with its own type argument:

ArgumentWhat it modelsSuffixTypes
latent (Residual) variances and covariances of the constructs _zeta "cov", "ggm"
residual Residuals of the indicators of latent variables _epsilon "cov", "chol", "prec", "ggm", "cor"
composite Variances and covariances of the indicators of composites _c "cov", "chol", "prec", "ggm", "cor"

The matrices with the suffix _c (sigma_c, omega_c, delta_c, kappa_c, lowertri_c, rho_c, SD_c) describe the indicators of composites. They are block diagonal with one block per composite: elements within a block are parameters, whereas elements that connect indicators of different composites are always zero, because those covariances are implied by the relations between the composites.

Note: the suffix _c does not refer to relations between composites. Those are modeled, like relations between latent variables, in beta and the _zeta matrices.

Some quantities are computed rather than estimated. The variance of a composite follows from its weights and the covariances of its indicators, so the (residual) variances and the intercepts of composites are not parameters. They are shown in the parameter table without standard errors. getmatrix() also returns a few derived matrices: the composite loadings (in the columns of lambda that belong to composites), the implied covariances and means of the constructs (sigma_eta, mu_eta), and the implied residual covariances of the composite indicators (sigma_epsilon_c).

Example: Composites Predicting an Observed Variable

The StarWars data (271 respondents) contain three questions on the original trilogy (Q5–Q7), three on the sequel trilogy (Q8–Q10), and the item “I am a huge Star Wars fan!” (Q1). We form one composite per trilogy and use both to predict being a fan:

library("psychonetrics")
library("dplyr")
data(StarWars)

vars <- c("Q1", paste0("Q", 5:10))

# Weights: which variables form which composite. Q1 is a composite with a
# single indicator, which represents the observed variable itself:
weights <- matrix(0, 7, 3)
weights[1, 1]   <- 1   # Fan
weights[2:4, 2] <- 1   # Originals
weights[5:7, 3] <- 1   # Sequels

# Both composites predict being a fan (they are correlated by default):
beta <- matrix(0, 3, 3)
beta[1, 2:3] <- 1

mod <- cvm(StarWars, weights = weights, beta = beta, vars = vars,
           latents = c("Fan", "Originals", "Sequels")) %>% runmodel

mod %>% fit          # chi-square = 2.92, df = 8, p = 0.94
mod %>% parameters

getmatrix(mod, "weights")   # estimated weights
getmatrix(mod, "lambda")    # composite loadings

The model fits well. The weights and effects are:

ParameterEstimateSE
Q5 → Originals1 (fixed)
Q6 → Originals2.720.89
Q7 → Originals0.810.39
Q8 → Sequels1 (fixed)
Q9 → Sequels−0.230.22
Q10 → Sequels1.220.37
Originals → Fan0.170.050
Sequels → Fan0.130.040

The first weight of every composite is fixed to 1 to set its scale, so the other weights are relative to that indicator. Weights are estimated from the relations of a composite with the rest of the model, and therefore tend to have larger standard errors than factor loadings.

The Composite Network Model: cnm()

With composite = "ggm", or the wrapper cnm(), the indicators within each composite are modeled as a Gaussian graphical model: omega_c contains one partial correlation network per composite. With all edges included this is the same model as above in another parameterization. The networks can then be made sparse with the usual tools:

mod_cnm <- cnm(StarWars, weights = weights, beta = beta, vars = vars,
               latents = c("Fan", "Originals", "Sequels")) %>% runmodel

# Remove non-significant edges within the composites:
mod_cnm_pruned <- mod_cnm %>% prune(alpha = 0.05)

compare(full = mod_cnm, pruned = mod_cnm_pruned)
#   model DF     AIC     BIC RMSEA Chisq Chisq_diff DF_diff p_value
#    full  8 5799.02 5896.27   ~ 0  2.92
#  pruned 10 5800.94 5890.99   ~ 0  8.84       5.92       2   0.052

# One network per composite:
getmatrix(mod_cnm_pruned, "omega_c")

Pruning removes the two edges that involve Q9, leaving a fully connected network of the three questions on the original trilogy (partial correlations 0.26, 0.16 and 0.38) and a single edge between Q8 and Q10 (0.37) for the sequels. prune(), stepup() and modelsearch() never add edges between indicators of different composites.

Because the unpruned cnm model is the cvm model in another parameterization, a fitted model can also be transformed from one to the other with transmod(), without estimating it again:

# cvm -> cnm (covariances of the composite indicators -> networks):
mod_cnm2 <- transmod(mod, composite = "ggm", keep_computed = TRUE)

# cnm -> cvm:
mod_cvm2 <- transmod(mod_cnm, composite = "cov", keep_computed = TRUE)

This works when every composite is saturated (all covariances or all edges among its indicators), and also for the other types ("chol", "prec", "cor"), the residual type and the latent type ("cov" or "ggm").

Combining a Latent Variable and a Composite

Latent variables and composites can be combined freely. Here the sequel questions are modeled as indicators of a latent variable, predicted by a composite of the questions on the original trilogy:

vars2 <- paste0("Q", 5:10)

lambda2 <- matrix(0, 6, 2)
lambda2[4:6, 2] <- 1      # Sequels: a latent variable

weights2 <- matrix(0, 6, 2)
weights2[1:3, 1] <- 1     # Originals: a composite

beta2 <- matrix(0, 2, 2)
beta2[2, 1] <- 1          # Originals -> Sequels

mod2 <- cvm(StarWars, lambda = lambda2, weights = weights2, beta = beta2,
            vars = vars2, latents = c("Originals", "Sequels")) %>% runmodel

mod2 %>% fit          # chi-square = 3.01, df = 6, p = 0.81
mod2 %>% parameters   # Sequels <- Originals: 0.17 (SE 0.059)

A construct is a latent variable when it has elements in lambda and a composite when it has elements in weights; it cannot have both, and a variable cannot be an indicator of a composite and load on a latent variable at the same time.

A Network of Constructs

With latent = "ggm" the relations between the constructs are modeled as a partial correlation network (omega_zeta) instead of covariances, as in the latent network model of the lvm family:

mod3 <- cvm(StarWars, weights = weights, vars = vars, latent = "ggm",
            latents = c("Fan", "Originals", "Sequels")) %>% runmodel

getmatrix(mod3, "omega_zeta")

The estimated partial correlations are 0.525 between being a fan and the composite of the original trilogy, 0.228 between being a fan and the composite of the sequels, and 0.259 between the two composites. With all three edges included, this model is equivalent to the first example (chi-square = 2.92, df = 8).

One restriction: with latent = "ggm", a composite can either be a node in the latent network or have incoming effects in beta, but not both. Such models are refused with an informative error, and the model search functions do not propose them. Which of the two a composite is follows from the model as you specify it; after adding or removing effects on composites by hand, identify() updates which parameters model search may add.

Scaling and Identification

Estimation

The estimators "ML", "FIML" (chosen automatically with missing data), "ULS", "WLS" and "DWLS" are available, for single and multiple groups with equality constraints (equal, groupequal()) and with a mean structure. The model is implemented in both the R and the C++ pipeline, which give identical results.

Composite weights are only weakly identified, which makes the likelihood flat in some directions. Estimation starts from partial least squares estimates of the weights and uses the same optimizer and settings as all other model families. The optimizer can therefore stop slightly before the optimum: the chi-square is typically accurate to about two decimals. Stricter convergence criteria can be set with setoptimizer(), for example setoptimizer(mod, "nlminb", optim.args = list(control = list(rel.tol = 1e-10, eval.max = 20000, iter.max = 10000))). A warning is given when the optimizer does not report convergence, when the indicator that scales a composite has a negligible weight, when a composite with estimated weights is not related to any other construct, or when the computed residual variance of a composite is not positive.

Penalized estimation: estimator = "PML" penalizes the network structures in the model, after which refit() gives unpenalized estimates of the selected network. This works, but the optimizer used for penalized estimation is a first-order method and can be very slow for composite models (minutes for a model with a dozen variables). For now, prune() and stepup() are the faster route to a sparse network.

Relation to lavaan

Models with covariance parameterizations (latent, residual and composite all "cov") are the models that lavaan 0.7 and later estimate with the <~ operator, and can be converted in both directions with fromlavaan() and tolavaan():

library("lavaan")

# psychonetrics -> lavaan:
lavfit <- tolavaan(mod2)
fitMeasures(lavfit, c("chisq", "df", "npar"))   # 3.01, 6, 21: as above

# lavaan -> psychonetrics:
fit <- sem(' Originals <~ Q5 + Q6 + Q7
             Sequels   =~ Q8 + Q9 + Q10
             Sequels   ~  Originals ',
           data = StarWars, meanstructure = TRUE, composites.cov = "free")
mod_from_lavaan <- fromlavaan(fit, run = TRUE)

One difference in defaults is worth knowing. lavaan by default fixes the variances and covariances of composite indicators to their sample values, whereas psychonetrics estimates them (composites.cov = "free" in lavaan). With complete data both give the same chi-square, degrees of freedom, weights, loadings and regressions with the same standard errors, but the number of parameters differs, and with it AIC and BIC. For the model above, the lavaan defaults count 9 parameters where psychonetrics counts 21: the difference is the six means of the observed variables and the six variances and covariances of Q5–Q7. Network parameterizations ("ggm", "prec") have no lavaan counterpart.

Experimental Status and Limitations

The family is new in version 0.18 and is marked experimental. What has been checked:

Current limitations:

Summary

Further Reading