Bifactor ESEM Tutorial
Background
Within relational turbulence theory (RTT; Solomon et al., 2016), relational uncertainty is conceptualized as three facets of doubts and insecurities one experiences within a romantic association related to the self, the partner, and the relationship. Conventionally, RTT recognizes self and partner uncertainty as antecedents of relationship uncertainty, which in turn leads to a global characterization of the relationship as turbulent, and this causal model was supported by meta-analytic evidence (Goodboy et al., 2020). However, the correlations among these variables are consistently strong and positive, indicating that they may be influenced by a common factor. In Goodboy et al. (2021), they constrasted multiple measurement models for relational uncertainty with two samples from prior studies, and found the bifactor exploratory structural equation model to be the superior measurement model for the construct (data and Mplus code are available as supplemental materials of this article). I replicate these models in R using the lavaan package with their college sample data.
Goodboy, A. K., Bolkan, S., Brisini, K., & Solomon, D. H. (2021). Relational uncertainty within relational turbulence theory: The bifactor exploratory structural equation model. Journal of Communication, 71(3), 403-430. https://doi.org/10.1093/joc/jqab009
Goodboy, A. K., Bolkan, S., Sharabi, L. L., Myers, S. A., & Baker, J. P. (2020). The relational turbulence model: A meta-analytic review. Human Communication Research, 46(2-3), 222-249.
Solomon, D. H., Knobloch, L. K., Theiss, J. A., & McLaren, R. M. (2016). Relational turbulence theory: Explaining variation in subjective experiences and communication within romantic relationships. Human Communication Research, 42(4), 507-532.
Prep work
Before you start, make sure you placed this .rmd file and “College Sample JOC.dat” in the same folder, so R can find and save any output files to the same place.
Reading the data into the environment.
# Reading the college sample data
college_sample<-read.csv("College-Sample-JOC.csv", header = FALSE)
# Because the data set does not contain variable names,
# we will label variables here
colnames(college_sample)<-c(paste0("Self",1:6), # Self uncertainty
paste0("Part",1:6), # Partner uncertainty
paste0("Rel",1:6), # Relationship uncertainty
paste0("Disc",1:4), # Disclosure to network
paste0("Lone",1:4), # Loneliness
paste0("Turb",1:4)) # Relational turbulence
# Coding missing data
college_sample[college_sample == -99]<-NA
# Subset the data (we only need the 18 relational uncertainty items)
college_ru<-college_sample[1:18]
# View the first 10 rows of college_ru
head(college_ru,10)## Self1 Self2 Self3 Self4 Self5 Self6 Part1 Part2 Part3 Part4 Part5 Part6 Rel1
## 1 3 4 5 4 4 5 5 5 4 4 5 4 4
## 2 1 1 1 1 2 1 2 2 2 2 1 2 2
## 3 1 1 1 1 1 1 4 4 4 4 4 3 3
## 4 1 1 1 1 1 1 1 1 1 1 1 1 1
## 5 1 1 1 1 1 1 1 1 1 1 1 1 1
## 6 1 1 1 1 1 1 1 1 1 1 1 1 1
## 7 1 2 1 1 2 1 2 1 1 2 1 2 1
## 8 1 1 1 1 1 1 2 2 1 2 1 1 1
## 9 1 1 1 1 1 1 1 1 1 1 1 1 1
## 10 1 1 2 1 1 1 6 6 6 6 1 6 6
## Rel2 Rel3 Rel4 Rel5 Rel6
## 1 4 4 3 4 4
## 2 1 1 2 2 1
## 3 1 1 1 4 1
## 4 1 1 6 1 1
## 5 1 1 1 1 1
## 6 1 1 1 1 1
## 7 3 1 1 1 1
## 8 1 1 1 1 1
## 9 1 1 1 1 1
## 10 6 5 1 6 4
Loading R packages
RStudio may prompt you at the top of this panel to install packages that you have not used before. You can also install packages manually by typing in the console: for example, install.packages(“dplyr”).
library(devtools) # For version control
library(dplyr) # For data management
library(GPArotation) # For rotation used in factor analysis
library(lavaan) # For SEM
library(lavaanPlot) # For plotting SEM
library(psych) # For data descriptives and reliability statistics
library(reshape2) # For data manipulation
library(rlang) # For string manipulation
library(semTools) # For factor analysis Data descriptives and reliabilities
We can first take a look at the item-level descriptives and inter-item correlations.
# Running descriptives
describe(college_ru)## vars n mean sd median trimmed mad min max range skew kurtosis se
## Self1 1 512 3.04 1.62 3 2.97 1.48 1 6 5 0.15 -1.25 0.07
## Self2 2 512 2.75 1.54 2 2.65 1.48 1 6 5 0.31 -1.22 0.07
## Self3 3 512 2.53 1.48 2 2.38 1.48 1 6 5 0.67 -0.77 0.07
## Self4 4 512 2.40 1.43 2 2.22 1.48 1 6 5 0.77 -0.51 0.06
## Self5 5 512 2.28 1.43 2 2.06 1.48 1 6 5 0.90 -0.31 0.06
## Self6 6 512 2.38 1.45 2 2.19 1.48 1 6 5 0.75 -0.60 0.06
## Part1 7 512 2.96 1.59 3 2.86 1.48 1 6 5 0.29 -1.09 0.07
## Part2 8 512 2.80 1.54 2 2.69 1.48 1 6 5 0.35 -1.09 0.07
## Part3 9 511 2.74 1.53 2 2.61 1.48 1 6 5 0.44 -0.98 0.07
## Part4 10 510 2.71 1.48 2 2.59 1.48 1 6 5 0.46 -0.87 0.07
## Part5 11 511 2.50 1.51 2 2.33 1.48 1 6 5 0.71 -0.69 0.07
## Part6 12 512 2.61 1.54 2 2.45 1.48 1 6 5 0.59 -0.81 0.07
## Rel1 13 512 2.78 1.55 3 2.66 1.48 1 6 5 0.41 -0.98 0.07
## Rel2 14 512 3.20 1.62 3 3.16 1.48 1 6 5 0.01 -1.24 0.07
## Rel3 15 512 2.38 1.40 2 2.19 1.48 1 6 5 0.81 -0.33 0.06
## Rel4 16 512 2.31 1.38 2 2.12 1.48 1 6 5 0.85 -0.24 0.06
## Rel5 17 512 2.77 1.57 3 2.62 1.48 1 6 5 0.51 -0.84 0.07
## Rel6 18 512 2.41 1.43 2 2.22 1.48 1 6 5 0.74 -0.51 0.06
# Bivariate correlations among items
round(cor(college_ru, use = "pairwise.complete.obs"),2)## Self1 Self2 Self3 Self4 Self5 Self6 Part1 Part2 Part3 Part4 Part5 Part6
## Self1 1.00 0.77 0.61 0.61 0.50 0.58 0.52 0.47 0.41 0.43 0.35 0.44
## Self2 0.77 1.00 0.65 0.71 0.60 0.66 0.50 0.51 0.39 0.45 0.38 0.44
## Self3 0.61 0.65 1.00 0.69 0.62 0.61 0.50 0.50 0.53 0.51 0.46 0.49
## Self4 0.61 0.71 0.69 1.00 0.67 0.66 0.47 0.48 0.39 0.44 0.41 0.43
## Self5 0.50 0.60 0.62 0.67 1.00 0.69 0.41 0.43 0.41 0.38 0.48 0.41
## Self6 0.58 0.66 0.61 0.66 0.69 1.00 0.46 0.47 0.37 0.40 0.40 0.44
## Part1 0.52 0.50 0.50 0.47 0.41 0.46 1.00 0.83 0.67 0.73 0.57 0.68
## Part2 0.47 0.51 0.50 0.48 0.43 0.47 0.83 1.00 0.73 0.81 0.62 0.75
## Part3 0.41 0.39 0.53 0.39 0.41 0.37 0.67 0.73 1.00 0.73 0.65 0.68
## Part4 0.43 0.45 0.51 0.44 0.38 0.40 0.73 0.81 0.73 1.00 0.59 0.80
## Part5 0.35 0.38 0.46 0.41 0.48 0.40 0.57 0.62 0.65 0.59 1.00 0.61
## Part6 0.44 0.44 0.49 0.43 0.41 0.44 0.68 0.75 0.68 0.80 0.61 1.00
## Rel1 0.50 0.54 0.57 0.55 0.52 0.53 0.62 0.71 0.67 0.73 0.60 0.77
## Rel2 0.52 0.55 0.49 0.49 0.46 0.49 0.61 0.62 0.58 0.64 0.49 0.64
## Rel3 0.46 0.50 0.55 0.55 0.55 0.52 0.54 0.57 0.47 0.53 0.46 0.54
## Rel4 0.37 0.38 0.44 0.47 0.46 0.47 0.44 0.49 0.48 0.48 0.46 0.49
## Rel5 0.38 0.35 0.42 0.36 0.33 0.33 0.60 0.67 0.63 0.70 0.52 0.72
## Rel6 0.39 0.42 0.49 0.50 0.46 0.47 0.47 0.50 0.57 0.53 0.48 0.49
## Rel1 Rel2 Rel3 Rel4 Rel5 Rel6
## Self1 0.50 0.52 0.46 0.37 0.38 0.39
## Self2 0.54 0.55 0.50 0.38 0.35 0.42
## Self3 0.57 0.49 0.55 0.44 0.42 0.49
## Self4 0.55 0.49 0.55 0.47 0.36 0.50
## Self5 0.52 0.46 0.55 0.46 0.33 0.46
## Self6 0.53 0.49 0.52 0.47 0.33 0.47
## Part1 0.62 0.61 0.54 0.44 0.60 0.47
## Part2 0.71 0.62 0.57 0.49 0.67 0.50
## Part3 0.67 0.58 0.47 0.48 0.63 0.57
## Part4 0.73 0.64 0.53 0.48 0.70 0.53
## Part5 0.60 0.49 0.46 0.46 0.52 0.48
## Part6 0.77 0.64 0.54 0.49 0.72 0.49
## Rel1 1.00 0.75 0.59 0.51 0.71 0.53
## Rel2 0.75 1.00 0.52 0.44 0.59 0.47
## Rel3 0.59 0.52 1.00 0.56 0.53 0.48
## Rel4 0.51 0.44 0.56 1.00 0.53 0.62
## Rel5 0.71 0.59 0.53 0.53 1.00 0.53
## Rel6 0.53 0.47 0.48 0.62 0.53 1.00
We can examine the coefficient omega for each subscale. All three scales were reliable.
# Self uncertainty
omega(college_ru[1:6], nfactors = 1)$omega.tot## [1] 0.9154015
# Partner uncertainty
omega(college_ru[7:12], nfactors = 1)$omega.tot## [1] 0.9334561
# Relationship uncertainty
omega(college_ru[13:18], nfactors = 1)$omega.tot## [1] 0.8834218
Fitting measurement models
Goodboy et al. (2021) fitted five such models, and we will replicate them here:
- Unidimensional CFA
- Independent Clusters Model-CFA (3-Factor CFA)
- Exploratory Structural Equation Model
- Bifactor CFA
- Bifactor ESEM
Unidimensional CFA
This model considers relational uncertainty to be one unidimensional construct, and all 18 items serve as equivalent indicators for this construct.
First, we write out the model in lavaan’s language.
=~represents “measured by”
~represents “regressed on”
~~represents “correlated with”
Here, we specify a latent variable relunc, and that it
is measured by all 18 variables in the scale.
# Building the model
UNI.CFA <- 'relunc =~ Self1 + Self2 + Self3 + Self4 + Self5 + Self6
+ Part1 + Part2 + Part3 + Part4 + Part5 + Part6
+ Rel1 + Rel2 + Rel3 + Rel4 + Rel5 + Rel6'# We run the model and save the results into fit.UNI.CFA
fit.UNI.CFA <- cfa(model = UNI.CFA,
data = college_ru,
# We use the maximum likelihood with robust SE estimator
estimator = "MLR",
# Full-information maximum likelihood for missingness
missing = "FIML")
# We can also look at the graphic form of the model
lavaanPlot(name = "UnidimensionalCFA",
model = fit.UNI.CFA)Next, we extract the fit statistics from the object. Because it gives a lot of different measures, we can prespecify a subset of fit measures and use them to contrast all the models we will run.
We can see the unidimensional CFA does not fit the data well.
# Creating a subset of fit indices we will examine across all models
fit.subset<-c("chisq.scaled","df","pvalue.scaled",
"rmsea.scaled","rmsea.pvalue.scale",
"rmsea.ci.lower.scaled","rmsea.ci.upper.scaled",
"cfi","tli","srmr","aic","bic")
# Extracting fit indices from the first model
fitmeasures(fit.UNI.CFA, fit.subset)## chisq.scaled df pvalue.scaled
## 1266.303 135.000 0.000
## rmsea.scaled rmsea.ci.lower.scaled rmsea.ci.upper.scaled
## 0.128 0.123 0.133
## cfi tli srmr
## 0.768 0.737 0.088
## aic bic
## 27998.329 28227.198
We will use the same workflow to fit the remaining models.
Independent Clusters Model-CFA (3-Factor CFA)
The second model specifies self, partner, and relationship uncertainty as three correlated latent variables, and each is measured by six indicators with no cross-loading. It fits the data a little bit better compared to the unidimensional model.
# Building the model
FA3.CFA <- 'self =~ Self1 + Self2 + Self3 + Self4 + Self5 + Self6
part =~ Part1 + Part2 + Part3 + Part4 + Part5 + Part6
rel =~ Rel1 + Rel2 + Rel3 + Rel4 + Rel5 + Rel6'
# We run the model and save the results into fit.UNI.CFA
fit.FA3.CFA <- cfa(model = FA3.CFA,
data = college_ru,
# We use the maximum likelihood with robust SE estimator
estimator = "MLR",
# Full-information maximum likelihood for missingness
missing = "FIML")
# We can also look at the graphic form of the model
lavaanPlot(name = "IndependentClustersModelCFA",
model = fit.FA3.CFA)# Extracting fit indices from the model
fitmeasures(fit.FA3.CFA, fit.subset)## chisq.scaled df pvalue.scaled
## 569.733 132.000 0.000
## rmsea.scaled rmsea.ci.lower.scaled rmsea.ci.upper.scaled
## 0.080 0.075 0.086
## cfi tli srmr
## 0.909 0.894 0.050
## aic bic
## 26949.169 27190.754
Exploratory Structural Equation Model
The third model specifies self, partner, and relationship uncertainty as three correlated latent variables, and all items are allowed to cross-load onto all factors, though the target rotation will keep cross-loadings as small as possible. Fitting an ESEM in R is not as straightforward as in Mplus, and there is not a simple function for it. However, it is doable.
Mateus Silvestrin’s post explains the workflow for running ESEM in R: https://msilvestrin.me/post/esem/. Overall, there are three steps involved:
- Running an exploratory factor analysis to obtain the loadings
- Specifying the ESEM model using start values
- Fitting the ESEM model
Exploratory factor analysis with target rotation (oblique)
# First, we conduct an EFA without any rotation
efa <- efaUnrotate(# Specifying the dataset
data = college_ru,
# 3 factors
nf = 3)
# Next, we construct a target matrix for the rotation
target.mat <- as.matrix(cbind(c(rep(NA,6),rep(0,12)),
c(rep(0,6),rep(NA,6),rep(0,6)),
c(rep(0,12),rep(NA,6))))
# View the target matrix
target.mat## [,1] [,2] [,3]
## [1,] NA 0 0
## [2,] NA 0 0
## [3,] NA 0 0
## [4,] NA 0 0
## [5,] NA 0 0
## [6,] NA 0 0
## [7,] 0 NA 0
## [8,] 0 NA 0
## [9,] 0 NA 0
## [10,] 0 NA 0
## [11,] 0 NA 0
## [12,] 0 NA 0
## [13,] 0 0 NA
## [14,] 0 0 NA
## [15,] 0 0 NA
## [16,] 0 0 NA
## [17,] 0 0 NA
## [18,] 0 0 NA
# Rotate the matrix of EFA loadings
rotated <- funRotate(efa,
# Oblique target rotation
fun = "targetQ",
# Specifying the target matrix
Target = target.mat)
# View the standardized loadings
rotated.loadings <- rotated@loading
# To use loadings as starting values for the ESEM, we need to
# revert them back to the metric of the items. We do so by
# multiplying the loadings by the items' standard deviations
rotated.loadings.unstd <- rotated.loadings * describe(college_ru)$sdWriting a function to generate model syntax
Because the model becomes really complex with all the items cross-loading onto each latent variable, we will write a few lines to generate our lavaan model.
# Manipulating the loadings matrix
loadings.esem <- melt(round(rotated.loadings.unstd,3))
# Naming the columns
colnames(loadings.esem) <- c("item", "latent", "value")
# Labeling our latent variables
loadings.esem$latent <- c(rep("self",18),
rep("part",18),
rep("rel",18))
# Generating the model syntax
# First, we set two anchors for each latent variables: one being the first item
# in a corresponding subscale, and another not in the same subscale
anchors <- as.data.frame(cbind(latent = c("self","part","rel","self","part","rel"),
item = c("Self1","Part1","Rel1","Part1","Rel1","Self1")))
# View the anchors dataframe
anchors## latent item
## 1 self Self1
## 2 part Part1
## 3 rel Rel1
## 4 self Part1
## 5 part Rel1
## 6 rel Self1
# I created a function for generating the model syntax "esem.lavaan.syntax"
esem.lavaan.syntax <- function (loadings.dt, anchors){
# Create the is_anchor variable in the anchors dataframe
anchors$is.anchor <- 1
# Merge the anchors dataframe with the loadings dataframe, and rearrange it
loadings.dt <- merge(anchors, loadings.dt, by = c("item","latent"), all.y = TRUE)
loadings.dt$is.anchor[is.na(loadings.dt$is.anchor)]<-0
loadings.dt <- arrange(loadings.dt, desc(is.anchor))
loadings.dt <- arrange(loadings.dt, latent)
# Make syntax column per item; syntax is different depending on is.anchor
loadings.dt <- loadings.dt %>%
mutate(syntax = ifelse(is.anchor == 1,
paste0(value,"*", item),
paste0("start(",value,")*", item)))
# Combine starting/fix values and item names to generate a line
# for each latent variable
loadings.combined <- loadings.dt %>%
group_by(latent) %>%
summarize(syntaxline = paste(syntax, collapse = "+"))
syntaxlines <- paste(loadings.combined$latent, "=~", loadings.combined$syntaxline)
# Add lines to fix latent variable variance to 1
c(syntaxlines,
paste(loadings.combined$latent, "~~ 1*", loadings.combined$latent))
}
# Let's take a look at the generated syntax
esem.lavaan.syntax(loadings.esem, anchors)## [1] "part =~ -1.145*Part1+-0.758*Rel1+start(-1.193)*Part2+start(-0.945)*Part3+start(-1.166)*Part4+start(-0.61)*Part5+start(-1.069)*Part6+start(-0.764)*Rel2+start(-0.216)*Rel3+start(-0.098)*Rel4+start(-0.937)*Rel5+start(-0.182)*Rel6+start(-0.28)*Self1+start(-0.133)*Self2+start(-0.059)*Self3+start(0.16)*Self4+start(0.326)*Self5+start(0.185)*Self6"
## [2] "rel =~ 0.482*Rel1+-0.426*Self1+start(0.001)*Part1+start(0.142)*Part2+start(0.46)*Part3+start(0.266)*Part4+start(0.569)*Part5+start(0.395)*Part6+start(0.189)*Rel2+start(0.515)*Rel3+start(0.786)*Rel4+start(0.582)*Rel5+start(0.714)*Rel6+start(-0.396)*Self2+start(0.264)*Self3+start(0.241)*Self4+start(0.555)*Self5+start(0.301)*Self6"
## [3] "self =~ 0.313*Part1+1.375*Self1+start(0.186)*Part2+start(-0.017)*Part3+start(0.001)*Part4+start(0.082)*Part5+start(0.017)*Part6+start(0.32)*Rel1+start(0.485)*Rel2+start(0.477)*Rel3+start(0.215)*Rel4+start(-0.135)*Rel5+start(0.281)*Rel6+start(1.524)*Self2+start(0.96)*Self3+start(1.138)*Self4+start(0.942)*Self5+start(1.069)*Self6"
## [4] "part ~~ 1* part"
## [5] "rel ~~ 1* rel"
## [6] "self ~~ 1* self"
The first three lines specify the measurement portion of the model.
For example, self =~ 1.375*Self1 fixes Self1’s
unstandardized loading on self uncertainty at that value, while
self =~ start(0.96)*Self2 gives the loading for Self2 a
starting value without fixing it. Lines 4-6 sets the variance of each
latent variable at 1.
Fitting the ESEM model
Now that the new function has taken care of the syntax, we can just plug the function along with the input data frames we specified above into the CFA function.
fit.FA3.ESEM <- cfa(model = esem.lavaan.syntax(loadings.esem, anchors),
data = college_ru,
# We use the maximum likelihood with robust SE estimator
estimator = "MLR",
# Full-information maximum likelihood for missingness
missing = "FIML")
# We can also look at the graphic form of the model
lavaanPlot(name = "ESEM",
model = fit.FA3.ESEM)# Let's take a look at the standardized loadings
select(arrange(arrange(parameterestimates(fit.FA3.ESEM, standardized = T)[1:54,], rhs),lhs),
# only view a subset of output of interest
lhs:est,pvalue,std.all)## lhs op rhs est pvalue std.all
## 1 part =~ Part1 -1.145 NA -0.720
## 2 part =~ Part2 -1.187 0.000 -0.769
## 3 part =~ Part3 -0.929 0.000 -0.609
## 4 part =~ Part4 -1.165 0.000 -0.790
## 5 part =~ Part5 -0.578 0.003 -0.382
## 6 part =~ Part6 -1.065 0.000 -0.692
## 7 part =~ Rel1 -0.758 NA -0.490
## 8 part =~ Rel2 -0.774 0.000 -0.479
## 9 part =~ Rel3 -0.193 0.385 -0.137
## 10 part =~ Rel4 -0.059 0.848 -0.043
## 11 part =~ Rel5 -0.927 0.000 -0.591
## 12 part =~ Rel6 -0.155 0.565 -0.108
## 13 part =~ Self1 -0.326 0.191 -0.201
## 14 part =~ Self2 -0.183 0.476 -0.119
## 15 part =~ Self3 -0.055 0.756 -0.037
## 16 part =~ Self4 0.164 0.441 0.115
## 17 part =~ Self5 0.351 0.274 0.245
## 18 part =~ Self6 0.183 0.426 0.126
## 19 rel =~ Part1 -0.001 0.989 -0.001
## 20 rel =~ Part2 0.136 0.219 0.088
## 21 rel =~ Part3 0.461 0.002 0.302
## 22 rel =~ Part4 0.241 0.172 0.164
## 23 rel =~ Part5 0.595 0.003 0.394
## 24 rel =~ Part6 0.375 0.016 0.244
## 25 rel =~ Rel1 0.482 NA 0.312
## 26 rel =~ Rel2 0.172 0.090 0.107
## 27 rel =~ Rel3 0.559 0.067 0.399
## 28 rel =~ Rel4 0.839 0.023 0.609
## 29 rel =~ Rel5 0.559 0.002 0.356
## 30 rel =~ Rel6 0.758 0.020 0.530
## 31 rel =~ Self1 -0.426 NA -0.263
## 32 rel =~ Self2 -0.387 0.003 -0.252
## 33 rel =~ Self3 0.315 0.319 0.214
## 34 rel =~ Self4 0.306 0.438 0.214
## 35 rel =~ Self5 0.643 0.241 0.449
## 36 rel =~ Self6 0.364 0.395 0.252
## 37 self =~ Part1 0.313 NA 0.197
## 38 self =~ Part2 0.182 0.002 0.118
## 39 self =~ Part3 -0.031 0.820 -0.020
## 40 self =~ Part4 0.005 0.959 0.003
## 41 self =~ Part5 0.057 0.787 0.038
## 42 self =~ Part6 0.015 0.887 0.010
## 43 self =~ Rel1 0.296 0.057 0.192
## 44 self =~ Rel2 0.484 0.000 0.299
## 45 self =~ Rel3 0.437 0.101 0.311
## 46 self =~ Rel4 0.159 0.643 0.115
## 47 self =~ Rel5 -0.148 0.341 -0.094
## 48 self =~ Rel6 0.225 0.471 0.158
## 49 self =~ Self1 1.375 NA 0.849
## 50 self =~ Self2 1.514 0.000 0.983
## 51 self =~ Self3 0.916 0.000 0.620
## 52 self =~ Self4 1.085 0.000 0.758
## 53 self =~ Self5 0.872 0.030 0.609
## 54 self =~ Self6 1.011 0.001 0.699
# Extracting fit indices from the model
fitmeasures(fit.FA3.ESEM, fit.subset)## chisq.scaled df pvalue.scaled
## 362.480 102.000 0.000
## rmsea.scaled rmsea.ci.lower.scaled rmsea.ci.upper.scaled
## 0.071 0.064 0.077
## cfi tli srmr
## 0.948 0.922 0.025
## aic bic
## 26687.249 27055.983
We can see from the standardized loadings that items mainly loaded onto the latent variables they belong to, while cross-loadings are permitted in this model. Note that the loadings for partner uncertainty are negative due to factor indeterminacy. We can reverse the signs on those loadings when we generate the model syntax.
Bifactor CFA
The fourth model specifies a bifactor configuration: a general relational uncertainty factor, and three residualized specific factors corresponding to self, partner, and relationship. This model requires factors be orthogonal to one another.
# Building the model
BIFA.CFA <- 'general =~ NA*Self1 + Self2 + Self3 + Self4 + Self5 + Self6
+ Part1 + Part2 + Part3 + Part4 + Part5 + Part6
+ Rel1 + Rel2 + Rel3 + Rel4 + Rel5 + Rel6
self =~ NA*Self1 + Self2 + Self3 + Self4 + Self5 + Self6
part =~ NA*Part1 + Part2 + Part3 + Part4 + Part5 + Part6
rel =~ NA*Rel1 + Rel2 + Rel3 + Rel4 + Rel5 + Rel6
# Factors are orthogonal
general ~~ 0*self + 0*part + 0*rel
self ~~ 0*part + 0*rel
part ~~ 0*rel
# Setting latent variable variance at 1
general ~~ 1*general
self ~~ 1*self
part ~~ 1*part
rel ~~ 1*rel'
# We run the model and save the results into fit.UNI.CFA
fit.BIFA.CFA <- cfa(model = BIFA.CFA,
data = college_ru,
# We use the maximum likelihood with robust SE estimator
estimator = "MLR",
# Full-information maximum likelihood for missingness
missing = "FIML")
# We can also look at the graphic form of the model
lavaanPlot(name = "BifactorCFA",
model = fit.BIFA.CFA)# Extracting fit indices from the model
fitmeasures(fit.BIFA.CFA, fit.subset)## chisq.scaled df pvalue.scaled
## 443.610 117.000 0.000
## rmsea.scaled rmsea.ci.lower.scaled rmsea.ci.upper.scaled
## 0.074 0.068 0.080
## cfi tli srmr
## 0.933 0.912 0.046
## aic bic
## 26782.997 27088.156
Bifactor ESEM
Here, we will follow the same steps as the previous ESEM. The difference between the two mostly stems from how latent factors are related in these models. In regular ESEM, factors correlate, so we use an oblique rotation for the EFA portion of the process, while bifactor ESEM sets factor correlations at 0, so orthogonal rotation is used in the EFA step.
Exploratory factor analysis with target rotation (orthogonal)
# First, we conduct an EFA without any rotation
efa.bifac <- efaUnrotate(data = college_ru,
# 4 factors
nf = 4)
# Constructing a target matrix
target.mat.bifac <- as.matrix(cbind(rep(NA,18),
c(rep(NA,6),rep(0,12)),
c(rep(0,6),rep(NA,6),rep(0,6)),
c(rep(0,12),rep(NA,6))))
#View the target matrix
target.mat.bifac## [,1] [,2] [,3] [,4]
## [1,] NA NA 0 0
## [2,] NA NA 0 0
## [3,] NA NA 0 0
## [4,] NA NA 0 0
## [5,] NA NA 0 0
## [6,] NA NA 0 0
## [7,] NA 0 NA 0
## [8,] NA 0 NA 0
## [9,] NA 0 NA 0
## [10,] NA 0 NA 0
## [11,] NA 0 NA 0
## [12,] NA 0 NA 0
## [13,] NA 0 0 NA
## [14,] NA 0 0 NA
## [15,] NA 0 0 NA
## [16,] NA 0 0 NA
## [17,] NA 0 0 NA
## [18,] NA 0 0 NA
# Using the target matrix to rotate the loadings
rotated.bifac <- funRotate(# The EFA results to be rotated
efa.bifac,
# Target rotation (orthogonal)
fun = "targetT",
# Specify the target matrix
Target = target.mat.bifac)
# To use loadings as starting values for the ESEM, we need to
# revert them back to the metric of the items. We do so by
# multiplying the loadings by the items' standard deviations
rotated.bifac.loadings.unstd <- rotated.bifac@loading*describe(college_ru)$sd
# Manipulate the loading matrix, name columns, label factors
loadings.esem.bifac <- melt(round(rotated.bifac.loadings.unstd,3))
colnames(loadings.esem.bifac) <- c("item","latent","value")
loadings.esem.bifac$latent<-c(rep("general",18),
rep("self",18),
rep("part",18),
rep("rel",18))
# Setting anchors for the bifactor ESEM
anchors.bifac <- as.data.frame(cbind(latent = c("general","general","general",
"self","part","rel"),
item = c("Self1","Part1","Rel1",
"Self1","Part1","Rel1")))
#Examine the generated syntax
esem.lavaan.syntax(loadings.esem.bifac,anchors.bifac)## [1] "general =~ 1.135*Part1+1.363*Rel1+0.858*Self1+start(1.224)*Part2+start(1.17)*Part3+start(1.201)*Part4+start(1.062)*Part5+start(1.278)*Part6+start(1.205)*Rel2+start(0.978)*Rel3+start(0.924)*Rel4+start(1.226)*Rel5+start(0.978)*Rel6+start(0.873)*Self2+start(0.978)*Self3+start(0.905)*Self4+start(0.903)*Self5+start(0.887)*Self6"
## [2] "part =~ -0.825*Part1+start(-0.752)*Part2+start(-0.401)*Part3+start(-0.505)*Part4+start(-0.228)*Part5+start(-0.304)*Part6+start(0.002)*Rel1+start(-0.122)*Rel2+start(-0.021)*Rel3+start(0.091)*Rel4+start(-0.185)*Rel5+start(0.051)*Rel6+start(-0.209)*Self1+start(-0.137)*Self2+start(0.019)*Self3+start(0.102)*Self4+start(0.242)*Self5+start(0.112)*Self6"
## [3] "rel =~ 0.355*Rel1+start(-0.005)*Part1+start(0.019)*Part2+start(-0.033)*Part3+start(0.162)*Part4+start(-0.169)*Part5+start(0.274)*Part6+start(0.439)*Rel2+start(-0.158)*Rel3+start(-0.316)*Rel4+start(0.195)*Rel5+start(-0.267)*Rel6+start(0.298)*Self1+start(0.242)*Self2+start(-0.106)*Self3+start(-0.145)*Self4+start(-0.335)*Self5+start(-0.184)*Self6"
## [4] "self =~ 0.958*Self1+start(0.162)*Part1+start(0.032)*Part2+start(-0.139)*Part3+start(-0.135)*Part4+start(-0.052)*Part5+start(-0.166)*Part6+start(0.02)*Rel1+start(0.189)*Rel2+start(0.257)*Rel3+start(0.064)*Rel4+start(-0.281)*Rel5+start(0.109)*Rel6+start(1.069)*Self2+start(0.628)*Self3+start(0.774)*Self4+start(0.625)*Self5+start(0.725)*Self6"
## [5] "general ~~ 1* general"
## [6] "part ~~ 1* part"
## [7] "rel ~~ 1* rel"
## [8] "self ~~ 1* self"
fit.BIFA.ESEM <- cfa(model = c(esem.lavaan.syntax(loadings.esem.bifac,anchors.bifac),
# Adding orthogonal constraints on factors
'general ~~ 0*self + 0*part + 0*rel
self ~~ 0*part + 0*rel
part ~~ 0*rel'),
data = college_ru,
# We use the maximum likelihood with robust SE estimator
estimator = "MLR",
# Full-information meximum likelihood for missingness
missing = "FIML")
# We can take a look at the graphic form of the model
lavaanPlot(name = "BifactorESEM",
model = fit.BIFA.ESEM)# Examine the loadings
select(arrange(arrange(parameterestimates(fit.BIFA.ESEM,standardized = T)[1:72,], rhs),lhs),
# only view a subset of output of interest
lhs:est,pvalue,std.all)## lhs op rhs est pvalue std.all
## 1 general =~ Part1 1.135 NA 0.715
## 2 general =~ Part2 1.223 0.000 0.793
## 3 general =~ Part3 1.170 0.000 0.767
## 4 general =~ Part4 1.201 0.000 0.815
## 5 general =~ Part5 1.060 0.000 0.701
## 6 general =~ Part6 1.279 0.000 0.832
## 7 general =~ Rel1 1.363 NA 0.883
## 8 general =~ Rel2 1.201 0.000 0.744
## 9 general =~ Rel3 0.975 0.000 0.695
## 10 general =~ Rel4 0.916 0.000 0.665
## 11 general =~ Rel5 1.219 0.000 0.777
## 12 general =~ Rel6 0.969 0.000 0.678
## 13 general =~ Self1 0.858 NA 0.530
## 14 general =~ Self2 0.873 0.000 0.567
## 15 general =~ Self3 0.973 0.000 0.659
## 16 general =~ Self4 0.900 0.000 0.629
## 17 general =~ Self5 0.899 0.000 0.628
## 18 general =~ Self6 0.885 0.000 0.612
## 19 part =~ Part1 -0.825 NA -0.519
## 20 part =~ Part2 -0.742 0.000 -0.482
## 21 part =~ Part3 -0.395 0.016 -0.259
## 22 part =~ Part4 -0.493 0.000 -0.335
## 23 part =~ Part5 -0.216 0.283 -0.143
## 24 part =~ Part6 -0.294 0.138 -0.191
## 25 part =~ Rel1 0.014 0.945 0.009
## 26 part =~ Rel2 -0.119 0.729 -0.074
## 27 part =~ Rel3 -0.017 0.900 -0.012
## 28 part =~ Rel4 0.090 0.725 0.065
## 29 part =~ Rel5 -0.178 0.400 -0.114
## 30 part =~ Rel6 0.040 0.852 0.028
## 31 part =~ Self1 -0.212 0.764 -0.131
## 32 part =~ Self2 -0.138 0.850 -0.089
## 33 part =~ Self3 0.015 0.962 0.010
## 34 part =~ Self4 0.099 0.800 0.069
## 35 part =~ Self5 0.240 0.458 0.168
## 36 part =~ Self6 0.107 0.755 0.074
## 37 rel =~ Part1 -0.012 0.986 -0.008
## 38 rel =~ Part2 0.017 0.978 0.011
## 39 rel =~ Part3 -0.035 0.930 -0.023
## 40 rel =~ Part4 0.166 0.715 0.113
## 41 rel =~ Part5 -0.164 0.470 -0.109
## 42 rel =~ Part6 0.272 0.365 0.177
## 43 rel =~ Rel1 0.355 NA 0.230
## 44 rel =~ Rel2 0.427 0.047 0.264
## 45 rel =~ Rel3 -0.163 0.443 -0.116
## 46 rel =~ Rel4 -0.336 0.131 -0.244
## 47 rel =~ Rel5 0.191 0.511 0.122
## 48 rel =~ Rel6 -0.292 0.284 -0.204
## 49 rel =~ Self1 0.285 0.688 0.176
## 50 rel =~ Self2 0.242 0.720 0.157
## 51 rel =~ Self3 -0.118 0.752 -0.080
## 52 rel =~ Self4 -0.157 0.687 -0.110
## 53 rel =~ Self5 -0.330 0.253 -0.230
## 54 rel =~ Self6 -0.179 0.588 -0.124
## 55 self =~ Part1 0.153 0.766 0.096
## 56 self =~ Part2 0.023 0.960 0.015
## 57 self =~ Part3 -0.141 0.603 -0.092
## 58 self =~ Part4 -0.138 0.557 -0.094
## 59 self =~ Part5 -0.048 0.838 -0.031
## 60 self =~ Part6 -0.164 0.128 -0.107
## 61 self =~ Rel1 0.019 0.923 0.013
## 62 self =~ Rel2 0.196 0.251 0.121
## 63 self =~ Rel3 0.261 0.057 0.186
## 64 self =~ Rel4 0.068 0.688 0.049
## 65 self =~ Rel5 -0.280 0.001 -0.179
## 66 self =~ Rel6 0.109 0.514 0.076
## 67 self =~ Self1 0.958 NA 0.592
## 68 self =~ Self2 1.066 0.000 0.693
## 69 self =~ Self3 0.631 0.000 0.427
## 70 self =~ Self4 0.776 0.000 0.542
## 71 self =~ Self5 0.628 0.000 0.439
## 72 self =~ Self6 0.723 0.000 0.500
# Extracting fit indices from the model
fitmeasures(fit.BIFA.ESEM, fit.subset)## chisq.scaled df pvalue.scaled
## 248.553 87.000 0.000
## rmsea.scaled rmsea.ci.lower.scaled rmsea.ci.upper.scaled
## 0.060 0.053 0.068
## cfi tli srmr
## 0.969 0.945 0.021
## aic bic
## 26546.623 26978.932
Notice that these loadings are within rounding errors with the parameters reported in Goodboy et al. (2021). We have successfully replicated the bifactor ESEM model.
Model Evaluation
Next, we can juxtapose the fit statistics of all five models. Again, some of these values slightly differ from Goodboy et al. (2021; Table 1, top panel), but this does not impact the substantive conclusions we can draw from this set of results.
fit.summary <- round(rbind(fitmeasures(fit.UNI.CFA, fit.subset),
fitmeasures(fit.FA3.CFA, fit.subset),
fitmeasures(fit.FA3.ESEM, fit.subset),
fitmeasures(fit.BIFA.CFA, fit.subset),
fitmeasures(fit.BIFA.ESEM, fit.subset)),3)
rownames(fit.summary)<-c("Uni CFA",
"ICM-CFA",
"ESEM",
"Bifactor CFA",
"Bifactor ESEM")
fit.summary## chisq.scaled df pvalue.scaled rmsea.scaled rmsea.ci.lower.scaled
## Uni CFA 1266.303 135 0 0.128 0.123
## ICM-CFA 569.733 132 0 0.080 0.075
## ESEM 362.480 102 0 0.071 0.064
## Bifactor CFA 443.610 117 0 0.074 0.068
## Bifactor ESEM 248.553 87 0 0.060 0.053
## rmsea.ci.upper.scaled cfi tli srmr aic bic
## Uni CFA 0.133 0.768 0.737 0.088 27998.33 28227.20
## ICM-CFA 0.086 0.909 0.894 0.050 26949.17 27190.75
## ESEM 0.077 0.948 0.922 0.025 26687.25 27055.98
## Bifactor CFA 0.080 0.933 0.912 0.046 26783.00 27088.16
## Bifactor ESEM 0.068 0.969 0.945 0.021 26546.62 26978.93
Additional Information
I created this tutorial with a system environment and versions of R and packages that might be different from yours. If R reports errors when you attempt to run this tutorial, running the code chunk below and comparing your output and the tutorial posted on the website may be helpful.
session_info(pkgs = c("attached"))## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.2.0 (2022-04-22)
## os macOS Big Sur/Monterey 10.16
## system x86_64, darwin17.0
## ui X11
## language (EN)
## collate en_US.UTF-8
## ctype en_US.UTF-8
## tz America/New_York
## date 2022-08-25
## pandoc 2.18 @ /Applications/RStudio.app/Contents/MacOS/quarto/bin/tools/ (via rmarkdown)
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date (UTC) lib source
## devtools * 2.4.3 2021-11-30 [1] CRAN (R 4.2.0)
## dplyr * 1.0.9 2022-04-28 [1] CRAN (R 4.2.0)
## GPArotation * 2022.4-1 2022-04-16 [1] CRAN (R 4.2.0)
## lavaan * 0.6-12 2022-07-04 [1] CRAN (R 4.2.0)
## lavaanPlot * 0.6.2 2021-08-13 [1] CRAN (R 4.2.0)
## psych * 2.2.5 2022-05-10 [1] CRAN (R 4.2.0)
## reshape2 * 1.4.4 2020-04-09 [1] CRAN (R 4.2.0)
## rlang * 1.0.2 2022-03-04 [1] CRAN (R 4.2.0)
## semTools * 0.5-6 2022-05-10 [1] CRAN (R 4.2.0)
## usethis * 2.1.6 2022-05-25 [1] CRAN (R 4.2.0)
##
## [1] /Library/Frameworks/R.framework/Versions/4.2/Resources/library
##
## ──────────────────────────────────────────────────────────────────────────────