From 61b7094836252dfb1165c63e0c5174fd699c7e95 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Fri, 3 Nov 2023 10:54:40 +0100 Subject: [PATCH] add structural invariance --- R/confirmatoryfactoranalysis.R | 42 +++--- inst/help/ConfirmatoryFactorAnalysis.md | 7 +- inst/help/ConfirmatoryFactorAnalysis_nl.md | 11 +- inst/qml/ConfirmatoryFactorAnalysis.qml | 21 +-- .../test-confirmatoryfactoranalysis.R | 136 ++++++++++++------ 5 files changed, 132 insertions(+), 85 deletions(-) diff --git a/R/confirmatoryfactoranalysis.R b/R/confirmatoryfactoranalysis.R index bbf5d6a4..de03dcb1 100644 --- a/R/confirmatoryfactoranalysis.R +++ b/R/confirmatoryfactoranalysis.R @@ -180,13 +180,13 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. # define estimator from options estimator = switch(options[["estimator"]], - "default" = "default", - "maximumLikelihood" = "ML", - "generalizedLeastSquares" = "GLS", - "weightedLeastSquares" = "WLS", - "unweightedLeastSquares" = "ULS", - "diagonallyWeightedLeastSquares" = "DWLS" - ) + "default" = "default", + "maximumLikelihood" = "ML", + "generalizedLeastSquares" = "GLS", + "weightedLeastSquares" = "WLS", + "unweightedLeastSquares" = "ULS", + "diagonallyWeightedLeastSquares" = "DWLS" + ) cfaResult[["lav"]] <- try(lavaan::lavaan( model = mod, @@ -210,7 +210,7 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. estimator = estimator, missing = ifelse(options$naAction == "twoStageRobust", "robust.two.stage", ifelse(options$naAction == "twoStage", "two.stage", options$naAction)) - )) + )) # are there ordered variables in the data? cfaResult[["orderedVariables"]] <- any(sapply(dataset, is.ordered)) @@ -386,8 +386,8 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. lvs <- c(cfaResult[["spec"]]$latents, cfaResult[["spec"]]$soLatents) for (i in seq_along(lvs)) { lm <- paste0(lm, '\n', lvs[i], " ~ c(0,", - paste(rep(NA, length(unique(na.omit(dataset[[gv]]))) - 1), collapse = ","), - ")*1") + paste(rep(NA, length(unique(na.omit(dataset[[gv]]))) - 1), collapse = ","), + ")*1") } } else { lm <- NULL @@ -425,7 +425,9 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. "configural" = return(""), "metric" = return("loadings"), "scalar" = return(c("loadings", "intercepts")), - "strict" = return(c("loadings", "intercepts", "residuals", "residual.covariances")) + "strict" = return(c("loadings", "intercepts", "residuals", "residual.covariances")), + "structural" = return(c("loadings", "intercepts", "residuals", "residual.covariances", + "means", "lv.variances", "lv.covariances")) ) } @@ -915,9 +917,9 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. rc$addColumnInfo(name = "pvalue", title = gettext("p"), type = "number", format = "dp:3;p:.001") rc$addColumnInfo(name = "ci.lower", title = gettext("Lower"), type = "number", format = "sf:4;dp:3", - overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) + overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) rc$addColumnInfo(name = "ci.upper", title = gettext("Upper"), type = "number", format = "sf:4;dp:3", - overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) + overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) if (options$standardized != "none") rc$addColumnInfo(name = paste0("std.", standardization), @@ -950,13 +952,13 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. fi$addColumnInfo(name = "pvalue", title = gettext("p"), type = "number", format = "dp:3;p:.001") fi$addColumnInfo(name = "ci.lower", title = gettext("Lower"), type = "number", format = "sf:4;dp:3", - overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) + overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) fi$addColumnInfo(name = "ci.upper", title = gettext("Upper"), type = "number", format = "sf:4;dp:3", - overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) + overtitle = gettextf("%s%% Confidence Interval", options$ciLevel * 100)) if (options$standardized != "none") fi$addColumnInfo(name = paste0("std.", standardization), title = gettextf("Std. Est. (%s)", standardization), - type = "number", format = "sf:4;dp:3") + type = "number", format = "sf:4;dp:3") # add data fidat <- pei[pei$op == "~1" & pei$lhs %in% facNames, colSel[!colSel %in% 'rhs']] @@ -986,7 +988,7 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. if (options$standardized != "none") vi$addColumnInfo(name = paste0("std.", standardization), title = gettextf("Std. Est. (%s)", standardization), - type = "number", format = "sf:4;dp:3") + type = "number", format = "sf:4;dp:3") # add data vidat <- pei[pei$op == "~1" & !pei$lhs == "SecondOrder" & !pei$lhs %in% facNames, @@ -1049,8 +1051,8 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. mi <- try(lavaan::modindices(cfaResult[["lav"]])) jaspResults[["modind"]] <- mic <- createJaspContainer(gettext("Modification Indices"), position = 5) mic$dependOn(c("factors", "secondOrder", "residualsCovarying", "meanStructure", "modelIdentification", "factorsUncorrelated", - "packageMimiced", "estimator", "naAction", "seType", "bootstrapSamples", "group", "invarianceTesting", "modificationIndices", - "modificationIndicesCutoff")) + "packageMimiced", "estimator", "naAction", "seType", "bootstrapSamples", "group", "invarianceTesting", "modificationIndices", + "modificationIndicesCutoff")) if (isTryError(mi)) { mic$setError(.extractErrorMessage(mi)) @@ -1410,7 +1412,7 @@ confirmatoryFactorAnalysisInternal <- function(jaspResults, dataset, options, .. htmtTable <- createJaspTable(gettext("Heterotrait-monotrait ratio"), position = 4.2) htmtTable$dependOn(c("factors", "secondOrder", "residualsCovarying", "meanStructure", "modelIdentification", "factorsUncorrelated", - "packageMimiced", "estimator", "naAction", "group", "invarianceTesting", "htmt")) + "packageMimiced", "estimator", "naAction", "group", "invarianceTesting", "htmt")) if (options[["group"]] != "") { htmtTable$addColumnInfo(name = "group", title = gettext("Group"), type = "string", combine = TRUE) diff --git a/inst/help/ConfirmatoryFactorAnalysis.md b/inst/help/ConfirmatoryFactorAnalysis.md index 08cc83cf..eb224c05 100644 --- a/inst/help/ConfirmatoryFactorAnalysis.md +++ b/inst/help/ConfirmatoryFactorAnalysis.md @@ -43,9 +43,10 @@ JASP allows the factors in turn to be indicators of a second-order factor. This - Grouping variable: Select a categorical variable here to create CFA models for each group - Invariance testing: Select a level of constraining parameters over the different groups. - configural: the different groups have the same CFA structure - - metric: the factor loadings are constrained to be equal across groups - - scalar: the factor loadings and means of the indicators are constrained to be equal across groups - - strict: the factor loadings, means of the indicators, residual variances, and residual covariances are constrained to be equal across groups + - metric: same as configural and the factor loadings are constrained to be equal across groups + - scalar: same as metric and the means of the indicators (intercepts) are constrained to be equal across groups + - strict: same as scalar and the residual variances, and residual covariances are constrained to be equal across groups + - structural: same as strict and the latent means, variances, and covariances are constrained to be equal across groups ### Plots ------- diff --git a/inst/help/ConfirmatoryFactorAnalysis_nl.md b/inst/help/ConfirmatoryFactorAnalysis_nl.md index b0f0799b..c6dc0c6a 100644 --- a/inst/help/ConfirmatoryFactorAnalysis_nl.md +++ b/inst/help/ConfirmatoryFactorAnalysis_nl.md @@ -41,10 +41,13 @@ JASP staat toe dat factoren op hun beurt indicatoren worden van een tweede-orde ------ - Groepen: Selecteer hier een categorische variabele om CFA modellen voor iedere groep te creƫren. - Invariantie testen: Selecteer een niveau van beperkende parameters over de verschillende groepen. - - Configureel: De verschilllende groepen hebben dezelfde CFA structuur. - - Metrisch: De factorladingen van de groepen zijn gelijk. - - Scalar: De factorladingen en gemiddeldes van de indicatoren van de groepen zijn gelijk. - - Strikt: De factorladingen, gemiddeldes van de indicatoren, residu varianties, en residu covarianties van de groepen zijn gelijk. + - configureel: De verschilllende groepen hebben dezelfde CFA structuur. + - metrisch: hetzelfde als configureel, maar de factorladingen moeten voor alle groepen gelijk zijn + - scalair: hetzelfde als metrisch en de gemiddelden van de indicatoren (intercepts) moeten voor alle groepen gelijk zijn + - strikt: hetzelfde als scalair en de restvarianties en restcovarianties moeten gelijk zijn voor alle groepen. + - structureel: hetzelfde als strikt en de latente gemiddelden, varianties en covarianties moeten voor alle groepen gelijk zijn. + +Vertaald met www.DeepL.com/Translator (gratis versie) ### Grafieken ------- diff --git a/inst/qml/ConfirmatoryFactorAnalysis.qml b/inst/qml/ConfirmatoryFactorAnalysis.qml index 4fc0fbb6..01f5d942 100644 --- a/inst/qml/ConfirmatoryFactorAnalysis.qml +++ b/inst/qml/ConfirmatoryFactorAnalysis.qml @@ -79,21 +79,7 @@ Form Group { title: qsTr("Model Options") - CheckBox - { - label: qsTr("Include mean structure") ; - name: "meanStructure" ; - id: meanstructure - - RadioButtonGroup - { - // ChildrenOnSameRow: true - name: "interceptsFixedToZero" - RadioButton { label: qsTr("Fix latent intercepts to zero") ; name: "latent"; checked: true} - RadioButton { label: qsTr("Fix manifest intercepts to zero"); name: "manifest"} - } - - } + CheckBox { label: qsTr("Include mean structure") ; name: "meanStructure" ; id: meanstructure } CheckBox { label: qsTr("Assume factors uncorrelated") ; name: "factorsUncorrelated" } CheckBox { label: qsTr("Fix exogenous covariates") ; name: "exogenousCovariatesFixed" ; checked: true ; visible: false } DropDown @@ -167,10 +153,11 @@ Form label: qsTr("Invariance testing") name: "invarianceTesting" values: [ - { label: qsTr("Configural") , value: "configural" }, + { label: qsTr("Configural") , value: "configural"}, { label: qsTr("Metric") , value: "metric" }, { label: qsTr("Scalar") , value: "scalar" }, - { label: qsTr("Strict") , value: "strict" } + { label: qsTr("Strict") , value: "strict" }, + { label: qsTr("Structural") , value: "structural"}, ] } } diff --git a/tests/testthat/test-confirmatoryfactoranalysis.R b/tests/testthat/test-confirmatoryfactoranalysis.R index 25b01e2e..a15494f4 100644 --- a/tests/testthat/test-confirmatoryfactoranalysis.R +++ b/tests/testthat/test-confirmatoryfactoranalysis.R @@ -61,36 +61,36 @@ test_that("[CFA 3-Factor] Factor loadings table results match", { test_that("[CFA 3-Factor] Factor variances table results match", { table <- results[["results"]][["estimates"]][["collection"]][["estimates_fv"]][["data"]] jaspTools::expect_equal_tables(table, - list(1, 1, 1, "Factor 1", "", 0, "", 1, 1, 1, "Factor 2", "", 0, "", - 1, 1, 1, "Factor 3", "", 0, "")) + list(1, 1, 1, "Factor 1", "", 0, "", 1, 1, 1, "Factor 2", "", 0, "", + 1, 1, 1, "Factor 3", "", 0, "")) }) test_that("[CFA 3-Factor] Residual variances table results match", { table <- results[["results"]][["estimates"]][["collection"]][["estimates_rv"]][["data"]] jaspTools::expect_equal_tables(table, - list(0.326399894319287, 0.771706936936134, 0.549053415627711, "x1", - 1.34368055770828e-06, 0.113600822803218, 4.83318168019605, 0.934462587447057, - 1.33321007058646, 1.13383632901676, "x2", 0, 0.10172316590628, - 11.1462941495686, 0.666706476085054, 1.02194282083783, 0.844324648461441, - "x3", 0, 0.0906231817407956, 9.31687270566615, 0.277647747273574, - 0.464697982595281, 0.371172864934427, "x4", 7.32747196252603e-15, - 0.047717773591029, 7.77850341710428, 0.331807290171594, 0.560702710555418, - 0.446255000363506, "x5", 2.1316282072803e-14, 0.0583927618541264, - 7.64229993913142, 0.271855645519897, 0.44054959934542, 0.356202622432658, - "x6", 2.22044604925031e-16, 0.0430349626718039, 8.27705196700539, - 0.639885213665674, 0.958894859526553, 0.799390036596114, "x7", - 0, 0.0813815071034943, 9.82274800563124, 0.342279857209668, - 0.63311496350216, 0.487697410355914, "x8", 4.92208496183366e-11, - 0.0741939924882706, 6.57327357646934, 0.427489578257306, 0.704773115754749, - 0.566131347006028, "x9", 1.11022302462516e-15, 0.0707368961074339, - 8.00333882541577)) + list(0.326399894319287, 0.771706936936134, 0.549053415627711, "x1", + 1.34368055770828e-06, 0.113600822803218, 4.83318168019605, 0.934462587447057, + 1.33321007058646, 1.13383632901676, "x2", 0, 0.10172316590628, + 11.1462941495686, 0.666706476085054, 1.02194282083783, 0.844324648461441, + "x3", 0, 0.0906231817407956, 9.31687270566615, 0.277647747273574, + 0.464697982595281, 0.371172864934427, "x4", 7.32747196252603e-15, + 0.047717773591029, 7.77850341710428, 0.331807290171594, 0.560702710555418, + 0.446255000363506, "x5", 2.1316282072803e-14, 0.0583927618541264, + 7.64229993913142, 0.271855645519897, 0.44054959934542, 0.356202622432658, + "x6", 2.22044604925031e-16, 0.0430349626718039, 8.27705196700539, + 0.639885213665674, 0.958894859526553, 0.799390036596114, "x7", + 0, 0.0813815071034943, 9.82274800563124, 0.342279857209668, + 0.63311496350216, 0.487697410355914, "x8", 4.92208496183366e-11, + 0.0741939924882706, 6.57327357646934, 0.427489578257306, 0.704773115754749, + 0.566131347006028, "x9", 1.11022302462516e-15, 0.0707368961074339, + 8.00333882541577)) }) test_that("[CFA 3-Factor] Chi-square test table results match", { table <- results[["results"]][["maincontainer"]][["collection"]][["maincontainer_cfatab"]][["data"]] jaspTools::expect_equal_tables(table, - list(918.851589292384, 36, "Baseline model", "", 85.305521772505, 24, - "Factor model", 8.50255310602677e-09)) + list(918.851589292384, 36, "Baseline model", "", 85.305521772505, 24, + "Factor model", 8.50255310602677e-09)) }) test_that("Kaiser-Meyer-Olkin (KMO) test table results match", { @@ -167,37 +167,37 @@ test_that("[CFA Second order] Second-order factor loadings table results match", test_that("[CFA Second order] Factor variances table results match", { table <- results[["results"]][["estimates"]][["collection"]][["estimates_fv"]][["data"]] jaspTools::expect_equal_tables(table, - list(1, 1, 1, "Factor 1", "", 0, "", 1, 1, 1, "Factor 2", "", 0, "", - 1, 1, 1, "Factor 3", "", 0, "", 1, 1, 1, "Second-Order", "", - 0, "")) + list(1, 1, 1, "Factor 1", "", 0, "", 1, 1, 1, "Factor 2", "", 0, "", + 1, 1, 1, "Factor 3", "", 0, "", 1, 1, 1, "Second-Order", "", + 0, "")) }) test_that("[CFA Second order] Residual variances table results match", { table <- results[["results"]][["estimates"]][["collection"]][["estimates_rv"]][["data"]] jaspTools::expect_equal_tables(table, - list(0.326401450955574, 0.771708269797423, 0.549054860376498, "x1", - 1.34357828596165e-06, 0.11360076571671, 4.83319682673349, 0.934464040890642, - 1.33321206655867, 1.13383805372466, "x2", 0, 0.101723304308982, - 11.1462959390372, 0.666705522978852, 1.02194196604298, 0.844323744510915, - "x3", 0, 0.0906232068206831, 9.3168601524065, 0.2776477911548, - 0.464698082724097, 0.371172936939448, "x4", 7.32747196252603e-15, - 0.0477177879401676, 7.77850258701964, 0.331807404885689, 0.560702857278213, - 0.446255131081951, "x5", 2.1316282072803e-14, 0.0583927700197611, - 7.64230110904024, 0.271855604188449, 0.44054957527582, 0.356202589732134, - "x6", 2.22044604925031e-16, 0.0430349670754176, 8.27705036018498, - 0.63988809304374, 0.958898264282657, 0.799393178663198, "x7", - 0, 0.0813816411309667, 9.82277043758238, 0.342280111000572, - 0.633115637435664, 0.487697874218118, "x8", 4.92219598413612e-11, - 0.0741940996694749, 6.57327033269154, 0.427488355941731, 0.704772286642682, - 0.566130321292206, "x9", 1.11022302462516e-15, 0.0707369964162943, - 8.00331297586447)) + list(0.326401450955574, 0.771708269797423, 0.549054860376498, "x1", + 1.34357828596165e-06, 0.11360076571671, 4.83319682673349, 0.934464040890642, + 1.33321206655867, 1.13383805372466, "x2", 0, 0.101723304308982, + 11.1462959390372, 0.666705522978852, 1.02194196604298, 0.844323744510915, + "x3", 0, 0.0906232068206831, 9.3168601524065, 0.2776477911548, + 0.464698082724097, 0.371172936939448, "x4", 7.32747196252603e-15, + 0.0477177879401676, 7.77850258701964, 0.331807404885689, 0.560702857278213, + 0.446255131081951, "x5", 2.1316282072803e-14, 0.0583927700197611, + 7.64230110904024, 0.271855604188449, 0.44054957527582, 0.356202589732134, + "x6", 2.22044604925031e-16, 0.0430349670754176, 8.27705036018498, + 0.63988809304374, 0.958898264282657, 0.799393178663198, "x7", + 0, 0.0813816411309667, 9.82277043758238, 0.342280111000572, + 0.633115637435664, 0.487697874218118, "x8", 4.92219598413612e-11, + 0.0741940996694749, 6.57327033269154, 0.427488355941731, 0.704772286642682, + 0.566130321292206, "x9", 1.11022302462516e-15, 0.0707369964162943, + 8.00331297586447)) }) test_that("[CFA Second order] Chi-square test table results match", { table <- results[["results"]][["maincontainer"]][["collection"]][["maincontainer_cfatab"]][["data"]] jaspTools::expect_equal_tables(table, - list(918.851589292384, 36, "Baseline model", "", 85.3055217707089, - 24, "Factor model", 8.50255321704907e-09)) + list(918.851589292384, 36, "Baseline model", "", 85.3055217707089, + 24, "Factor model", 8.50255321704907e-09)) }) @@ -537,6 +537,8 @@ options$htmt <- TRUE options$reliability <- TRUE set.seed(1) results <- jaspTools::runAnalysis("confirmatoryFactorAnalysis", "holzingerswineford.csv", options) +results <- jaspTools::runAnalysis("confirmatoryFactorAnalysis", HolzingerSwineford1939, options) + test_that("Average variance extracted table results match", { table <- results[["results"]][["resAveTable"]][["data"]] @@ -567,3 +569,55 @@ test_that("Reliability table results match", { "total", 2, 0.832040767424991, "", "SecondOrder", 2, 0.568295638332184 )) }) + + +# structural invariance test +options <- jaspTools::analysisOptions("confirmatoryFactorAnalysis") +options$group <- "sex" +options$invarianceTesting <- "structural" + +options$factors <- list( + list(indicators = list("x1", "x2", "x3"), name = "f1", title = "Factor 1"), + list(indicators = list("x4", "x5", "x6"), name = "f2", title = "Factor 2"), + list(indicators = list("x7", "x8", "x9"), name = "f3", title = "Factor 3") +) +options$modelIdentification <- "markerVariable" +set.seed(1) +results <- jaspTools::runAnalysis("confirmatoryFactorAnalysis", "holzingerswineford.csv", options) + +test_that("Factor Covariances table results match", { + table <- results[["results"]][["estimates"]][["collection"]][["estimates_fc"]][["data"]] + jaspTools::expect_equal_tables(table, + list(0.252069643751425, 0.564393435959575, 0.4082315398555, 1, "Factor 1", + "", 2.99674828285745e-07, "Factor 2", 0.0796759008511687, + 5.12365138635909, 0.153674632748802, 0.370774902932515, 0.262224767840658, + 1, "Factor 1", "", 2.1939154322137e-06, "Factor 3", + 0.0553837396748543, 4.73468872597123, 0.0768417287748731, 0.270147907288013, + 0.173494818031443, 1, "Factor 2", "", 0.000434506858657757, + "Factor 3", 0.0493137067920418, 3.51818651076219, 0.252069643751426, + 0.564393435959575, 0.4082315398555, 2, "Factor 1", "", + 2.99674828285745e-07, "Factor 2", 0.0796759008511687, 5.1236513863591, + 0.153674632748802, 0.370774902932515, 0.262224767840658, 2, + "Factor 1", "", 2.1939154322137e-06, "Factor 3", 0.0553837396748543, + 4.73468872597123, 0.0768417287748731, 0.270147907288013, 0.173494818031443, + 2, "Factor 2", "", 0.000434506858657757, "Factor 3", + 0.0493137067920418, 3.51818651076219)) +}) + +test_that("Factor variances table results match", { + table <- results[["results"]][["estimates"]][["collection"]][["estimates_fv"]][["data"]] + jaspTools::expect_equal_tables(table, + list(0.51579922628888, 1.10282884918973, 0.809314037739304, 1, "Factor 1", + 6.50814258040811e-08, 0.149755206608709, 5.40424641030303, 0.759562812574203, + 1.19941902306092, 0.97949091781756, 1, "Factor 2", 0, 0.112210278851102, + 8.7290658917023, 0.203306550279352, 0.564190199988996, 0.383748375134174, + 1, "Factor 3", 3.06899737032573e-05, 0.0920638472329718, 4.16828523538758, + 0.51579922628888, 1.10282884918973, 0.809314037739304, 2, "Factor 1", + 6.50814258040811e-08, 0.149755206608709, 5.40424641030303, 0.759562812574203, + 1.19941902306092, 0.97949091781756, 2, "Factor 2", 0, 0.112210278851102, + 8.72906589170229, 0.203306550279352, 0.564190199988996, 0.383748375134174, + 2, "Factor 3", 3.06899737032573e-05, 0.0920638472329717, 4.16828523538759 + )) +}) + +