Welcome to ClientVPS Mirrors

Working with different models in lvmPlot

Working with different models in lvmPlot

Feng Ji

The mouse editor is not limited to CFA. The examples below start with several different model objects, draw a first diagram, and then open that diagram for editing. The fitting code runs before Shiny starts. Moving a node or a number never changes the fitted model.

For the basic editing gestures, see Mouse editing in lvmPlot. This tutorial concentrates on what each input contains and what its diagram can reasonably show. Most examples use fitted objects. The bifactor example near the end is deliberately only a model specification.

Packages and example data

Install only the model packages you intend to use. The editor additionally needs shiny and jsonlite; svglite and ragg provide SVG and PNG devices.

install.packages(c("lvmPlot", "lavaan", "shiny", "jsonlite", "svglite", "ragg"))
# Optional packages for the later sections:
install.packages(c("psych", "mirt", "eRm", "mclust", "OpenMx", "semPlot"))
library(lvmPlot)

The table records the packages available when this HTML file was built. A missing package leaves its example code visible but omits the computed figure. The interactive launch calls are not run during document construction.

Package Version
lavaan 0.6.19
psych 2.4.6.26
mirt 1.46.1
eRm 1.0.9
mclust 6.1.2
OpenMx 2.21.13
semPlot 1.1.6

CFA: the starting point

The nine test scores in HolzingerSwineford1939 give us a small, familiar example. Keep this fitted object: the ordinal, multigroup, EFA, and OpenMx sections reuse the same data, not the same estimates.

cfa_model <- '
  visual  =~ x1 + x2 + x3
  textual =~ x4 + x5 + x6
  speed   =~ x7 + x8 + x9
'
hs <- lavaan::HolzingerSwineford1939
cfa_fit <- lavaan::cfa(cfa_model, data = hs)
plot_lvm(cfa_fit, diagram = "all", label = "std", stars = FALSE)

lvmPlot(cfa_fit, mode = "edit", diagram = "all", label = "std",
        stars = FALSE, export_name = "cfa", launch = TRUE)

Move the visual factor first, then drag one loading label away from a nearby arrow. diagram = "all" keeps the factor covariances in this example. It does not automatically add every residual variance or intercept to the drawing.

SEM: measurement and structural paths

Here the same three factors also predict one another. This small example keeps the measurement blocks from the CFA so that the structural paths are easy to identify. These regressions illustrate the plotting workflow; fitting them to these cross-sectional scores does not establish a causal ordering.

sem_model <- '
  visual  =~ x1 + x2 + x3
  textual =~ x4 + x5 + x6
  speed   =~ x7 + x8 + x9
  textual ~ visual
  speed ~ visual + textual
'
sem_fit <- lavaan::sem(sem_model, data = hs)
plot_lvm(sem_fit, diagram = "all", label = "std", stars = FALSE)

lvmPlot(sem_fit, mode = "edit", diagram = "all", label = "std",
        stars = FALSE, export_name = "sem", launch = TRUE)

Try shifting a whole measurement block rather than moving each indicator independently. Select its nodes with Shift-click, move the selection, then adjust the structural-path labels. Check Export preview before deciding that the paths between the three factors have enough room. Models with many correlated residuals can take substantially longer to arrange; this example does not exercise that more demanding case.

Ordinal CFA

The adapter also accepts a lavaan fit with ordered indicators. To make this example self-contained, we divide each score into four categories. This is a coding demonstration, not a recommendation to discretize continuous scores in an analysis.

ordinal_data <- hs
items <- paste0("x", 1:9)
for (item in items) {
  ordinal_data[[item]] <- ordered(cut(hs[[item]],
    breaks = stats::quantile(hs[[item]], seq(0, 1, length.out = 5)),
    include.lowest = TRUE))
}
ordinal_fit <- lavaan::cfa(cfa_model, data = ordinal_data, ordered = items)
plot_lvm(ordinal_fit, diagram = "all", label = "std", stars = FALSE)

lvmPlot(ordinal_fit, mode = "edit", diagram = "all", label = "std",
        stars = FALSE, export_name = "ordinal-cfa", launch = TRUE)

The figure shows the factor-indicator structure and standardized loadings. It is not a threshold table. Inspect the thresholds separately with lavaan::parameterEstimates(ordinal_fit).

Linear growth

An intercept factor and a slope factor share the repeated measurements. The fixed slope loadings encode time, so unstandardized labels are the useful starting point here: 0, 1, 2, and 3 remain recognizable.

growth_model <- '
  i =~ 1*t1 + 1*t2 + 1*t3 + 1*t4
  s =~ 0*t1 + 1*t2 + 2*t3 + 3*t4
'
growth_fit <- lavaan::growth(growth_model, data = lavaan::Demo.growth)
plot_lvm(growth_fit, diagram = "all", label = "est", stars = FALSE)

lvmPlot(growth_fit, mode = "edit", diagram = "all", label = "est",
        stars = FALSE, export_name = "growth", launch = TRUE)

Keep t1 through t4 in time order. Move the two growth factors apart if their arrows crowd the middle of the diagram, then place the fixed-loading labels. The factor means and their uncertainty still belong in the model results; the path diagram is not a complete growth-model report.

Two-level CFA

Within-cluster and between-cluster versions of an observed variable must not be merged into one node. This example fits the first 30 clusters of lavaan’s simulated two-level data.

twolevel_data <- subset(lavaan::Demo.twolevel, cluster <= 30)
twolevel_model <- '
  level: 1
    fw =~ y1 + y2 + y3
  level: 2
    fb =~ y1 + y2 + y3
'
twolevel_fit <- lavaan::sem(twolevel_model, data = twolevel_data, cluster = "cluster")
#> Warning: lavaan->lav_object_post_check():  
#>    some estimated ov variances are negative
plot_lvm(twolevel_fit, diagram = "all", label = "std", stars = FALSE)

lvmPlot(twolevel_fit, mode = "edit", diagram = "all", label = "std",
        stars = FALSE, export_name = "twolevel-cfa", launch = TRUE)

The displayed item names can repeat across levels. Internally, names such as L1__y1 and L2__y1 keep them distinct. Leave space between the two level blocks when editing, and use those internal names in a scripted layout.

Multigroup CFA: one figure per group

A shared measurement structure does not imply shared standardized estimates. Do not average group coefficients to make a single numerical diagram. Extract the parameter table and pass one group at a time.

group_fit <- lavaan::cfa(cfa_model, data = hs, group = "school",
                        group.equal = "loadings")
group_parameters <- lavaan::parameterEstimates(group_fit, standardized = TRUE)
group_names <- lavaan::lavInspect(group_fit, "group.label")
group_tables <- lapply(seq_along(group_names), function(g) {
  group_parameters[group_parameters$group == g, , drop = FALSE]
})
names(group_tables) <- group_names
plot_lvm(group_tables[[1]], diagram = "all", label = "std", stars = FALSE)

plot_lvm(group_tables[[2]], diagram = "all", label = "std", stars = FALSE)

lvmPlot(group_tables[[1]], mode = "edit", diagram = "all", label = "std",
        stars = FALSE, export_name = "school-1", launch = TRUE)
# Stop that editor before opening the second one.
lvmPlot(group_tables[[2]], mode = "edit", diagram = "all", label = "std",
        stars = FALSE, export_name = "school-2", launch = TRUE)

If both group diagrams have the same nodes and paths, State JSON can transfer the arrangement between them. Their estimates remain group-specific. Passing the whole multigroup fit instead gives a shared structural view with a warning and without pooled numerical labels.

EFA with psych

EFA can contain substantial cross-loadings. diagram = "auto" selects a primary-loading summary; use diagram = "all" when those other loadings are part of what you want to discuss. The following figure uses the latter.

efa_fit <- psych::fa(hs[paste0("x", 1:9)], nfactors = 3,
                     rotate = "oblimin", fm = "minres")
plot_lvm(efa_fit, diagram = "all", label = "est", stars = FALSE)

lvmPlot(efa_fit, mode = "edit", diagram = "all", label = "est",
        stars = FALSE, export_name = "efa", launch = TRUE)

Arrange the factors before working on cross-loading labels. Removing a line from a summary diagram does not remove that loading from the fitted EFA.

Two-parameter IRT with mirt

LSAT7 is supplied as response patterns and their frequencies. Expand that table before fitting the five-item model. The diagram’s a= labels are item discrimination parameters in mirt’s internal parameterization, not CFA standardized loadings and not item difficulties.

irt_data <- mirt::expand.table(mirt::LSAT7)
irt_fit <- mirt::mirt(irt_data, 1, itemtype = "2PL", verbose = FALSE,
                      quadpts = 21L, TOL = 1e-3,
                      technical = list(NCYCLES = 200L))
plot_lvm(irt_fit, diagram = "all", label = "est", stars = FALSE)

lvmPlot(irt_fit, mode = "edit", diagram = "all", label = "est",
        stars = FALSE, export_name = "irt-2pl", launch = TRUE)

A single trait with five items is easy to arrange in one row. Move individual a= labels if necessary. For difficulty, guessing, or item-response curves, inspect the fitted model separately; this diagram does not display all IRT parameters. In a real analysis, choose estimation settings for that analysis rather than copying this small demonstration’s stopping tolerance.

Binary Rasch with eRm

A Rasch model fixes discrimination. Its useful item label is difficulty, so these edges carry b= instead of estimated CFA loadings. We use six items to keep the example readable.

rasch_fit <- eRm::RM(eRm::raschdat1[, 1:6])
plot_lvm(rasch_fit, diagram = "all", label = "est", stars = FALSE)

lvmPlot(rasch_fit, mode = "edit", diagram = "all", label = "est",
        stars = FALSE, export_name = "rasch", launch = TRUE)

There is a sign convention worth checking: eRm stores betapar as item easiness. The displayed difficulty is b = -betapar. An easier item thus has a lower difficulty. This example and its numerical check concern binary RM objects, not the interpretation of polytomous category parameters.

A Gaussian mixture with mclust

Here the figure has a different purpose. It shows a latent profile variable and the indicators used to fit the mixture. It does not draw a separate profile-mean curve for each class.

library(mclust)
#> Package 'mclust' version 6.1.2
#> Type 'citation("mclust")' for citing this R package in publications.
profile_fit <- mclust::Mclust(iris[, 1:4], G = 3, modelNames = "EII", verbose = FALSE)
plot_lvm(profile_fit, diagram = "all", label = "none")

lvmPlot(profile_fit, mode = "edit", diagram = "all", label = "none",
        export_name = "mixture", launch = TRUE)

The Profile node records the number of components. The arrows describe the model structure, not regression coefficients. Arrange the indicators and give them readable display names. Use profile_fit$parameters$mean for class-specific means and profile_fit$parameters$pro for mixing proportions. The iris example is a compact mixture demonstration, not an argument for treating species as psychological latent profiles.

OpenMx RAM: a fitted one-factor model

This example estimates a RAM model from the covariance matrix of three scores. The first loading fixes the factor’s scale. The adapter reads the model’s RAM matrices; it is not a converter for arbitrary OpenMx algebra models.

variables <- c("x1", "x2", "x3")
mx_fit <- OpenMx::mxModel("one_factor", type = "RAM",
  manifestVars = variables, latentVars = "F",
  OpenMx::mxPath(from = "F", to = variables, arrows = 1,
                free = c(FALSE, TRUE, TRUE), values = c(1, .8, .8)),
  OpenMx::mxPath(from = variables, arrows = 2, free = TRUE, values = 1),
  OpenMx::mxPath(from = "F", arrows = 2, free = TRUE, values = 1),
  OpenMx::mxData(observed = stats::cov(hs[variables]), type = "cov", numObs = nrow(hs)))
mx_fit <- OpenMx::mxRun(mx_fit, silent = TRUE)
plot_lvm(mx_fit, diagram = "all", label = "est", stars = FALSE)

lvmPlot(mx_fit, mode = "edit", diagram = "all", label = "est",
        stars = FALSE, export_name = "openmx-ram", launch = TRUE)

This is an unstandardized diagram. The fixed loading of one identifies the model; it should not be described as an estimated standardized loading.

A semPlotModel bridge

If a workflow already produces a semPlotModel, it can be passed to lvmPlot. The example constructs the bridge from the CFA fit above, so the expected nodes and paths are known.

semplot_object <- semPlot::semPlotModel(cfa_fit)
plot_lvm(semplot_object, diagram = "all", label = "est", stars = FALSE)

lvmPlot(semplot_object, mode = "edit", diagram = "all", label = "est",
        stars = FALSE, export_name = "semplot-bridge", launch = TRUE)

Compatibility here means this converted object’s parameters and structure are read correctly. It does not establish compatibility with every model class that semPlot itself can parse.

A bifactor specification without fitting

Sometimes the immediate task is explaining a proposed model. A lavaan-style parameter table can describe it without observations or fitted estimates. The general factor below loads on all nine indicators; each specific factor loads on its own three indicators.

bifactor_paths <- data.frame(
  lhs = c(rep("g", 9), rep(c("s1", "s2", "s3"), each = 3)),
  op = "=~", rhs = rep(paste0("x", 1:9), 2))
plot_lvm(bifactor_paths, diagram = "all", label = "none")

lvmPlot(bifactor_paths, mode = "edit", diagram = "all", label = "none",
        export_name = "bifactor-specification", launch = TRUE)

Keep the general factor on one side of the indicator row and the specific factors on the other. With no fitted coefficients there are no numerical labels to drag. This example checks the graphical specification; it does not fit the model, test identification, or impose orthogonality constraints in a statistical estimator.

Saving work across these models

For every example, open Export preview after arranging the nodes and labels. Download the finished figure, and keep State JSON if you will resume editing. Figure R contains a graph snapshot and code to reproduce it without the original fitted object. Neither file replaces the analysis script.

Each export request has its own prepared file. A later edit or export cannot rewrite that file. Download URLs are temporary: the session retains at most eight prepared files for two minutes, then asks for a new export rather than serving a different figure under an old URL. Files already downloaded to your computer are unaffected.

State can transfer between models only when the diagram’s node and path identities are compatible. A CFA state file is not a layout template for a Rasch model simply because both diagrams have a latent variable and items.

The following examples were evaluated in this build. Fitted-object examples, converted parameter tables, and the unfitted bifactor specification are listed separately by input type so that the latter is not mistaken for a fitted-model compatibility check.

Example Input Labels
cfa fitted object std
sem fitted object std
ordinal-cfa fitted object std
growth fitted object est
twolevel-cfa fitted object std
group-1 group-specific fitted parameter table std
group-2 group-specific fitted parameter table std
efa fitted object est
irt-2pl fitted object est
rasch fitted object est
gaussian-mixture fitted object none
openmx-ram fitted object est
semplot-bridge semPlotModel converted from a fitted CFA est
bifactor-specification unfitted parameter-table specification none

Need a high-speed mirror for your open-source project?
Contact our mirror admin team at info@clientvps.com.

This archive is provided as a free public service to the community.
Proudly supported by infrastructure from VPSPulse , RxServers , BuyNumber , UnitVPS , OffshoreName and secure payment technology by ArionPay.