library(learnr) library(learnSEM) knitr::opts_chunk$set(echo = FALSE) library(lavaan) library(semPlot) data(resdata) library(knitr)
Multigroup confirmatory factor analysis (MGCFA) is a useful tool for examining the fit and structure of latent variable models across groups. You will learn how to test if groups show the same configuration, loadings, intercepts, and residuals. With this information, we can begin to think about how groups may show different results on a measure because of the qualities of the measurement, rather than experimental manipulation. You will learn the following outcomes:
You can use vignette("lecture_mgcfa", "learnSEM")
to view these notes in R.
What is in the dataset? This dataset has data on gender, ethnicity, and a resiliency scale for practicing factor analysis and other structural equation modeling topics like multigroup CFA. We've used this data in a few class examples. You can remind yourself what the questions were by using ?resdata
in a code block.
?resdata
In this next section, you will answer questions using the R code blocks provided. Be sure to use the solution
option to see the answer if you need it!
Please enter your name for submission. If you do not need to submit, just type anything you'd like in this box.
question_text( "Student Name:", answer("Your Name", correct = TRUE), incorrect = "Thanks!", try_again_button = "Modify your answer", allow_retry = TRUE )
Using the same RS14 dataset from class, examine the multigroup analysis using Ethnicity
as the grouping variable of interest. In this example, code 1 as Black and 2 as White. Be sure to first change the variable to a factor with labels.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White"))
In this section, build the overall one-factor CFA for the RS14. You can use the example we did in class to get started. Include the cfa()
and summary()
of your model here. You should call this model overall.model
and the latent variable should be labeled RS
.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White"))
overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) summary(overall.fit, rsquare = T, standardized = T, fit.measures = T)
Create a white.fit
of the model for only White participants. Include the summary()
of this model. Note that all missing data has been excluded for the rest of the analysis.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE)
#lots of ways to subset here's one white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) summary(white.fit, rsquare = T, standardized = T, fit.measures = T)
Create a black.fit
of the model for only Black participants. Include the summary()
of this model.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE)
#lots of ways to subset here's one black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE) summary(black.fit, rsquare = T, standardized = T, fit.measures = T)
In the code below, we've started a table for our fit indices. Fill in the section labeled #update here
with the same round(fitmeasures(...))
code used in the first row.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE)
table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", #update here ) table_fit[3, ] <- c("Black Model", #update here ) kable(table_fit)
table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", round(fitmeasures(white.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[3, ] <- c("Black Model", round(fitmeasures(black.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) kable(table_fit)
question_text( "Does your model appear to fit ok overall and within each individual group?", answer("Both models appear to fit ok.", correct = TRUE), incorrect = "Both models appear to fit ok.", try_again_button = "Modify your answer", allow_retry = TRUE )
Create a configural.fit
model using Ethnicity
as your group variable. Include a summary()
of this model. Add in the configural.fit
to your table.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " #models overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE) #kable table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", round(fitmeasures(white.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[3, ] <- c("Black Model", round(fitmeasures(black.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3))
#put in the cfa #put in the summary #add to the table table_fit[4, ] <- c("Configural Model", #update here ) kable(table_fit)
#put in the cfa configural.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity") #put in the summary summary(configural.fit, rsquare = T, standardized = T, fit.measures = T) #add to the table table_fit[4, ] <- c("Configural Model", round(fitmeasures(configural.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) kable(table_fit)
In this section, create metric.fit
, summarize the model, and add the model information to your table.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " #models overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE) configural.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity") #kable table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", round(fitmeasures(white.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[3, ] <- c("Black Model", round(fitmeasures(black.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[4, ] <- c("Configural Model", round(fitmeasures(configural.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3))
#put in the cfa #put in the summary #add to the table table_fit[5, ] <- c("Metric Model", #update here ) kable(table_fit)
#put in the cfa metric.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings")) #put in the summary summary(metric.fit, rsquare = T, standardized = T, fit.measures = T) #add to the table table_fit[5, ] <- c("Metric Model", round(fitmeasures(metric.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) kable(table_fit)
In this section, create scalar.fit
, summarize the model, and add the model information to your table.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " #models overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE) configural.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity") metric.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings")) #kable table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", round(fitmeasures(white.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[3, ] <- c("Black Model", round(fitmeasures(black.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[4, ] <- c("Configural Model", round(fitmeasures(configural.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[5, ] <- c("Metric Model", round(fitmeasures(metric.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3))
#put in the cfa #put in the summary #add to the table table_fit[6, ] <- c("Scalar Model", #update here ) kable(table_fit)
#put in the cfa scalar.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts")) #put in the summary summary(metric.fit, rsquare = T, standardized = T, fit.measures = T) #add to the table table_fit[6, ] <- c("Scalar Model", round(fitmeasures(scalar.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) kable(table_fit)
Last, add strict.fit
by including error variances in your multigroup model, summarize that model, and include the fit information in the table.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " #models overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE) configural.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity") metric.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings")) scalar.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts")) #kable table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", round(fitmeasures(white.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[3, ] <- c("Black Model", round(fitmeasures(black.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[4, ] <- c("Configural Model", round(fitmeasures(configural.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[5, ] <- c("Metric Model", round(fitmeasures(metric.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[6, ] <- c("Scalar Model", round(fitmeasures(scalar.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3))
#put in the cfa #put in the summary #add to the table table_fit[7, ] <- c("Strict Model", #update here ) kable(table_fit)
#put in the cfa strict.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts", "residuals")) #put in the summary summary(strict.fit, rsquare = T, standardized = T, fit.measures = T) #add to the table table_fit[7, ] <- c("Strict Model", round(fitmeasures(strict.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) kable(table_fit)
question_text( "Looking at your results, do you see anywhere that indicates non-invariance (i.e., has a larger change than CFI = .01)? Where did it appear to break down?", answer("At the strict step!", correct = TRUE), incorrect = "The model stops being invariant at the strict step.", try_again_button = "Modify your answer", allow_retry = TRUE )
Use the code provided from class to calculate the potential pathway that should be freed to improve our model fit for partial invariance. The code has been partially provided for you. Fill in the sections labeled #fill in here
with the appropriate code. For the partial_synatx
you should fill in the lavaan
syntax for the step that broken down (i.e., =~
for loadings, ~1
for intercepts, or ~~
for residuals). In the model section, fill in group.equal
with the appropriate step for where your model broken down.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " #models overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE) configural.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity") metric.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings")) scalar.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts")) strict.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts", "residuals")) #kable table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", round(fitmeasures(white.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[3, ] <- c("Black Model", round(fitmeasures(black.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[4, ] <- c("Configural Model", round(fitmeasures(configural.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[5, ] <- c("Metric Model", round(fitmeasures(metric.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[6, ] <- c("Scalar Model", round(fitmeasures(scalar.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[7, ] <- c("Strict Model", round(fitmeasures(strict.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3))
##write out partial codes partial_syntax <- paste(colnames(res.data)[3:16], #all the columns ##fill in here colnames(res.data)[3:16]) #all columns again partial_syntax CFI_list <- 1:length(partial_syntax) names(CFI_list) <- partial_syntax for (i in 1:length(partial_syntax)){ temp <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = #fill in here, group.partial = partial_syntax[i]) CFI_list[i] <- fitmeasures(temp, "cfi") } which.max(CFI_list)
##write out partial codes partial_syntax <- paste(colnames(resdata)[3:16], #all the columns "~~", colnames(resdata)[3:16]) #all columns again partial_syntax CFI_list <- 1:length(partial_syntax) names(CFI_list) <- partial_syntax for (i in 1:length(partial_syntax)){ temp <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts", "residuals"), group.partial = partial_syntax[i]) CFI_list[i] <- fitmeasures(temp, "cfi") } which.max(CFI_list)
Create partial.fit
by updating your model to allow for the freed parameter. Summarize the model, and add this model's fit information to your table.
resdata$Ethnicity <- factor(resdata$Ethnicity, levels = c(1,2), labels = c("Black", "White")) resdata <- na.omit(resdata) overall.model <- " RS =~ RS1 + RS2 + RS3 + RS4 + RS5 + RS6 + RS7 + RS8 + RS9 + RS10 + RS11 + RS12 + RS13 + RS14 " #models overall.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE) white.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "White", ], meanstructure = TRUE) black.fit <- cfa(model = overall.model, data = resdata[resdata$Ethnicity == "Black", ], meanstructure = TRUE) configural.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity") metric.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings")) scalar.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts")) strict.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts", "residuals")) #kable table_fit <- matrix(NA, nrow = 8, ncol = 6) colnames(table_fit) = c("Model", "X2", "df", "CFI", "RMSEA", "SRMR") table_fit[1, ] <- c("Overall Model", round(fitmeasures(overall.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[2, ] <- c("White Model", round(fitmeasures(white.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[3, ] <- c("Black Model", round(fitmeasures(black.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[4, ] <- c("Configural Model", round(fitmeasures(configural.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[5, ] <- c("Metric Model", round(fitmeasures(metric.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[6, ] <- c("Scalar Model", round(fitmeasures(scalar.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) table_fit[7, ] <- c("Strict Model", round(fitmeasures(strict.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3))
#put in the cfa #put in the summary #add to the table table_fit[8, ] <- c("Partial Model", #update here ) kable(table_fit)
#put in the cfa partial.fit <- cfa(model = overall.model, data = resdata, meanstructure = TRUE, group = "Ethnicity", group.equal = c("loadings", "intercepts", "residuals"), group.partial = c("RS3 ~~ RS3")) #put in the summary summary(partial.fit, rsquare = T, standardized = T, fit.measures = T) #add to the table table_fit[8, ] <- c("Partial Model", round(fitmeasures(partial.fit, c("chisq", "df", "cfi", "rmsea", "srmr")),3)) kable(table_fit)
question_text( "Does the freeing of this one parameter bring your model up to partial invariance for that step?", answer("No not quite. We could run this again and check for another parameter.", correct = TRUE), incorrect = "No not quite. We could run this again and check for another parameter.", try_again_button = "Modify your answer", allow_retry = TRUE )
question_text( "What does the result of the partial invariance step imply for interpreting this scale? ", answer("Blacks and Whites have different variances for RS3.", correct = TRUE), incorrect = "Blacks and Whites have different variances for RS3 (and maybe one more), otherwise, the scale is invariant.", try_again_button = "Modify your answer", allow_retry = TRUE )
On this page, you will create the submission for your instructor (if necessary). Please copy this report and submit using a Word document or paste into the text window of your submission page. Click "Generate Submission" to get your work!
encoder_logic()
encoder_ui()
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.