# Load all packages:
library("psychonetrics")
library("dplyr")
library("qgraph")
library("ggplot2")
library("bootnet")
library("Rmpfr")
library("semPlot")
First we can load the data and make a vector of the observed variables:
# Load the data:
data("StarWars")
# Observed variables:
obsvars <- c("Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "Q7", "Q8", "Q9", "Q10")
Next, we can form our first model: a GGM model with all edges included:
# Form GGM model:
ggm1 <- ggm(StarWars, vars = obsvars)
# Run model:
ggm1 <- ggm1 %>% runmodel
## Estimating baseline model...
## Estimating model...
## Computing Fisher information...
## Computing modification indices...
If we are interested in the parameter values and significance of edges, we can use the following command:
# Check parameters:
ggm1 %>% parameters
Next, we can automatically prune all edges:
# Prune non-significant:
ggm1 <- ggm1 %>%
prune(adjust = "fdr", alpha = 0.01)
We can check the modification indices to see wich edges we can add now to improve fit:
# Check modification indices:
ggm1 %>% MIs
##
## Top 10 modification indices:
##
## var1 op var2 est mi pmi epc matrix row col group group_id
## Q1 ~1 <NA> 2.760148 mu 1 1 fullsample 1
## Q2 ~1 <NA> 4.188192 mu 2 1 fullsample 1
## Q3 ~1 <NA> 2.819188 mu 3 1 fullsample 1
## Q4 ~1 <NA> 3.214022 mu 4 1 fullsample 1
## Q5 ~1 <NA> 2.435424 mu 5 1 fullsample 1
## Q6 ~1 <NA> 2.686347 mu 6 1 fullsample 1
## Q7 ~1 <NA> 2.996310 mu 7 1 fullsample 1
## Q8 ~1 <NA> 2.383764 mu 8 1 fullsample 1
## Q9 ~1 <NA> 3.247232 mu 9 1 fullsample 1
## Q10 ~1 <NA> 2.118081 mu 10 1 fullsample 1
To automatically add edges at \(\alpha = 0.05\) until BIC can no longer be improved, use the following function:
# Stepup estimation:
ggm1 <- ggm1 %>% stepup(criterion = "bic", alpha = 0.05)
This process can be simplified with a single Dplyr pipe!
### Slide 33 ###
# Form GGM model:
ggm1 <- ggm(StarWars, vars = obsvars)
# Run model and estimate structure:
ggm1 <- ggm1 %>% runmodel %>%
prune(adjust = "fdr", alpha = 0.01) %>%
stepup(criterion = "bic", alpha = 0.05)
We can look at fit indices (although they will likely be good):
# Model fit:
ggm1 %>% fit
## Measure Value
## logl -4131.26
## unrestricted.logl -4109.06
## baseline.logl -4322.24
## nvar 10
## nobs 65
## npar 33
## df 32
## objective 12.11
## chisq 44.39
## pvalue 0.071
## baseline.chisq 426.35
## baseline.df 45
## baseline.pvalue ~0
## nfi 0.90
## pnfi 0.64
## tli 0.95
## nnfi 0.95
## rfi 0.85
## ifi 0.97
## rni 0.97
## cfi 0.97
## rmsea 0.038
## rmsea.ci.lower ~0
## rmsea.ci.upper 0.063
## rmsea.pvalue 0.77
## aic.ll 8328.52
## aic.ll2 8337.98
## aic.x -19.61
## aic.x2 110.39
## bic 8447.39
## bic2 8342.75
## ebic.25 8523.37
## ebic.5 8599.36
## ebic.75 8660.14
## ebic1 8751.33
Now, we can extract the network and plot it:
# Obtain network:
net1 <- getmatrix(ggm1, "omega")
# Plot:
qgraph(net1,
layout = "spring",
theme = "colorblind",
labels = obsvars)
We can also use bootnet for the model estimation:
# Estimate model using bootnet:
mod <- estimateNetwork(StarWars[,obsvars], default = "ggmModSelect")
structure <- 1*(mod$graph!=0)
# Form second GGM model with fixed structure:
ggm2 <- ggm(StarWars, omega = structure, vars = obsvars)
# ERun the model:
ggm2 <- ggm2 %>% runmodel
# Obtain network:
net2 <- getmatrix(ggm2, "omega")
This will likely be slightly different from the network estimated by psychonetrics. We can plot both networks next to each other:
# Obtain average layout (function from qgraph)
Layout <- averageLayout(net1,net2)
# plot both networks:
layout(t(1:2))
qgraph(net1, layout = Layout, theme = "colorblind", title = "psychonetrics",
labels = obsvars)
qgraph(net2, layout = Layout, theme = "colorblind", title = "ggmModSelect",
labels = obsvars)
We can even statistically compare these networks:
### Slide 36 ###
compare(psychonetrics = ggm1, ggmModSelect = ggm2)
## model DF AIC BIC RMSEA Chisq Chisq_diff DF_diff
## 1 ggmModSelect 31 8327.476 8449.948 0.03510471 41.35289 NA NA
## 2 psychonetrics 32 8328.515 8447.385 0.03780140 44.39182 3.038923 1
## p_value
## 1 NA
## 2 0.08128984
To perform a confirmatory factor analysis, we first need to specify the factor structure:
# Factor loadings matrix:
Lambda <- matrix(0, 10, 3)
Lambda[1:4,1] <- 1
Lambda[c(1,5:7),2] <- 1
Lambda[c(1,8:10),3] <- 1
print(Lambda)
## [,1] [,2] [,3]
## [1,] 1 1 1
## [2,] 1 0 0
## [3,] 1 0 0
## [4,] 1 0 0
## [5,] 0 1 0
## [6,] 0 1 0
## [7,] 0 1 0
## [8,] 0 0 1
## [9,] 0 0 1
## [10,] 0 0 1
Next, we can make a vector with names for the latent variables:
# Latents:
latents <- c("Prequels","Original","Sequels")
Now, we can form the CFA model and run it. We use the lvm
model function (latent variable model) for this:
# Make CFA model:
cfamod <- lvm(StarWars, lambda = Lambda, vars = obsvars,
identification = "variance", latents = latents)
# Run model:
cfamod <- cfamod %>% runmodel
## Estimating baseline model...
## Estimating model...
## Computing Fisher information...
## Computing modification indices...
We can look at some fit indices of this model:
cfamod@fitmeasures[c("chisq","pvalue","rmsea","cfi")]
## $chisq
## [1] 34.57911
##
## $pvalue
## [1] 0.2582765
##
## $rmsea
## [1] 0.02373261
##
## $cfi
## [1] 0.9879924
These perfectly reproduce the famous lavaan package!
library("lavaan")
## This is lavaan 0.6-6.1487
## lavaan is BETA software! Please report any bugs.
##
## Attaching package: 'lavaan'
## The following object is masked from 'package:psychonetrics':
##
## duplicationMatrix
lavmod <- '
Prequels =~ Q1 + Q2 + Q3 + Q4
Original =~ Q1 + Q5 + Q6 + Q7
Sequels =~ Q1 + Q8 + Q9 + Q10
'
lavfit <- cfa(lavmod,StarWars,std.lv=TRUE)
fitMeasures(lavfit)[c("chisq","pvalue","rmsea","cfi")]
## chisq pvalue rmsea cfi
## 34.57911327 0.25827655 0.02373261 0.98799236
Next, we can form an LNM model in the exact same way, using the lnm
function (alternatively, we can use lvm(..., latent = "ggm")
):
# Latent network model:
lnmmod <- lnm(StarWars, lambda = Lambda, vars = obsvars,
identification = "variance", latents = latents)
We can run this model and estimate a latent network structure with the same algorithm we used before:
# Run model and estimate latent structure:
lnmmod <- lnmmod %>% runmodel %>%
prune(adjust = "fdr", alpha = 0.01) %>%
stepup(criterion = "bic", alpha = 0.05)
Finally, we can compare all models:
# Compare all models:
comparison <- compare(
ggm_psychonetrics = ggm1,
ggm_ggmModSelect = ggm2,
CFA = cfamod,
LNM = lnmmod)
print(comparison)
## model DF AIC BIC RMSEA Chisq Chisq_diff DF_diff
## 1 CFA 30 8322.703 8448.777 0.02373261 34.57911 NA NA
## 2 ggm_ggmModSelect 31 8327.476 8449.948 0.03510471 41.35289 6.773781 1
## 3 LNM 31 8321.016 8443.488 0.02152480 34.89233 6.460568 0
## 4 ggm_psychonetrics 32 8328.515 8447.385 0.03780140 44.39182 9.499491 1
## p_value
## 1 NA
## 2 0.009250668
## 3 0.000000000
## 4 0.002055289
The BICs are close, so one thing we could do is transform these to posterior model probabilities:
# To posterior model probability:
BICs <- mpfr(comparison$BIC, 100)
BICtrans <- exp(-0.5 * BICs)
sumBICtrans <- sum(BICtrans)
comparison$modelProbability <- as.numeric(BICtrans / sumBICtrans)
# Make a plot:
g <- ggplot(comparison, aes(x=model, y = modelProbability)) +
geom_bar(stat="identity") + xlab("") +
ylab("Posterior model probability") + theme_bw() +
ylim(0,1)
print(g)
The latent network model performs best in this case!
Finally, we can make a path diagram. This is not very trivial… but can be done with the semPlot package:
# LISREL Model: LY = Lambda (lambda-y), TE = Theta (theta-epsilon), PS = Psi
mod_lisrel <- lisrelModel(LY = getmatrix(lnmmod, "lambda"),
PS = getmatrix(lnmmod, "sigma_zeta"),
TE = getmatrix(lnmmod, "sigma_epsilon"),
latNamesEndo = latents,
manNamesEndo = obsvars)
# Change latent to pcors:
omega <- getmatrix(lnmmod, "omega_zeta")
mod_lisrel@Pars$std[23:28] <- mod_lisrel@Pars$est[23:28] <- omega[lower.tri(omega,diag=TRUE)]
# We can make this nicer (set whatLabels = "none" to hide labels):
graph <- semPaths(mod_lisrel,
what = "std", # this argument controls what the color of edges represent. In this case, standardized parameters
whatLabels = "omit", # This argument controls what the edge labels represent. In this case, parameter estimates
# as.expression = c("nodes","edges"), # This argument draws the node and edge labels as mathematical exprssions
style = "lisrel", # This will plot residuals as arrows, closer to what we use in class
residScale = 10, # This makes the residuals larger
theme = "colorblind", # qgraph colorblind friendly theme
layout = "tree2", # tree layout options are "tree", "tree2", and "tree3"
cardinal = "lat cov", # This makes the latent covariances connet at a cardinal center point
curvePivot = TRUE, # Changes curve into rounded straight lines
sizeMan = 4, # Size of manifest variables
sizeLat = 10, # Size of latent varibales
edge.label.cex = 1,
mar = c(6,1,8,1), # Sets the margins
reorder = FALSE, # Prevents re-ordering of ovbserved variables
# filetype = "pdf", # Store to PDF
# filename = "semPlotExample1", # Set the name of the file
width = 8, # Width of the plot
height = 5, # Height of plot
groups = "latents", # Colors according to latents,
pastel = TRUE, # Pastel colors
borders = FALSE,
nCharNodes = 0, # Disable borders
DoNotPlot = TRUE # Prevents plotting for now
)
graph$Edgelist$directed[23:26] <- FALSE # We need to make the latent structure undirected not bidirectional
plot(graph) # Plot the results