Skip to content

Commit dc64c2a

Browse files
committed
Refactor syntax creation for indirect paths with moderators
1 parent 81b5b67 commit dc64c2a

File tree

2 files changed

+146
-35
lines changed

2 files changed

+146
-35
lines changed

R/classicProcess.R

Lines changed: 142 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -958,42 +958,33 @@ ClassicProcess <- function(jaspResults, dataset = NULL, options) {
958958
return(modOut)
959959
}
960960

961-
.procMedEffectsSyntaxModPars <- function(pathEdge, sourceNode, contrFacVars, graph, modProbes) {
961+
.procMedEffectsSyntaxModPars <- function(pathEdge, sourceNode, contrFacVars, graph) {
962962
# Get moderator parameters for two-way interactions
963963
modPars <- lapply(pathEdge$modVars[[1]], function(v) {
964964
# Get edge for two way interaction between X and M
965965
twoWayEdge <- igraph::E(graph)[paste(sourceNode, v, sep = "__") %--% pathEdge$target]$parName
966966

967-
# Concatenate two way edge parName with moderator probes
968-
return(apply(expand.grid(
969-
twoWayEdge,
970-
format(modProbes[[v]], digits = 3)
971-
), 1, paste, collapse = "*"))
967+
# Concatenate two way edge parName with moderator
968+
return(paste(twoWayEdge, v, sep = "*"))
972969
})
973970

974971
# Concatenate dummy variables for factor moderators
975972
modPars <- .procMedEffectsSyntaxMergeFacMods(pathEdge$modVars[[1]], contrFacVars, modPars, sep = " + ")
976973

977-
# Get all possible combinations of probes from different moderators
978-
modPars <- apply(expand.grid(modPars), 1, paste, collapse = " + ")
979-
980974
# Get name of potential three-way interaction
981975
threeWayInt <- paste(c(pathEdge$source, pathEdge$modVars[[1]]), collapse = "__")
982976

983977
if (length(pathEdge$modVars[[1]]) > 1 && threeWayInt %in% igraph::E(graph)$source) { # If three-way int
984978
# Get edge of three way interaction X x M1 x M2
985979
threeWayEdge <- igraph::E(graph)[threeWayInt %--% pathEdge$target]
986-
# Combine three way int parName with moderator probes
980+
# Combine three way int parName with moderator
987981
threeWayModPars <- paste(
988982
threeWayEdge$parName,
989-
apply(
990-
expand.grid(lapply(pathEdge$modVars[[1]], function(v) format(modProbes[[v]], digits = 3))),
991-
1, paste, collapse = "*"
992-
),
983+
paste(pathEdge$modVars[[1]], collapse = "*"),
993984
sep = "*"
994985
)
995-
# Add to previous moderator probes
996-
modPars <- paste(modPars, threeWayModPars, sep = "+")
986+
# Add to previous moderator terms
987+
modPars <- append(modPars, threeWayModPars)
997988
}
998989
return(modPars)
999990
}
@@ -1067,8 +1058,11 @@ ClassicProcess <- function(jaspResults, dataset = NULL, options) {
10671058
sourceFacVars <- numeric(0)
10681059
}
10691060

1061+
rhs <- c()
1062+
uniqueMods <- c()
1063+
10701064
# Right hand side of lavaan syntax
1071-
rhs <- lapply(2:length(path), function(i) {
1065+
for (i in 2:length(path)) {
10721066
# Get edge from path[-1] to path[i]
10731067
if (any(sourceInContrFacVars) && names(path)[i-1] %in% sourceFacVars) {
10741068
# Get all vars with factor levels as source nodes
@@ -1081,24 +1075,141 @@ ClassicProcess <- function(jaspResults, dataset = NULL, options) {
10811075
# Edge from previous node to current node
10821076
pathEdge <- igraph::E(graph)[sourceNode %--% names(path)[i]]
10831077

1078+
modVars <- unlist(pathEdge$modVars)
1079+
10841080
# If no moderators on edge, return only parName
1085-
if(any(is.na(unlist(pathEdge$modVars)))) return(pathEdge$parName)
1081+
if(any(is.na(modVars))) {
1082+
pathEdgeRhs <- pathEdge$parName
10861083

1087-
# Get pars from moderators
1088-
modPars <- .procMedEffectsSyntaxModPars(pathEdge, sourceNode, contrFacVars, graph, modProbes)
1089-
1090-
# If indirect path add parentheses
1091-
if (i > 1) {
1092-
return(paste0("(", pathEdge$parName, " + ", modPars, ")"))
1084+
if (length(rhs) > 0) {
1085+
# Multiply effects on indirect paths
1086+
rhs <- paste(rhs, pathEdgeRhs, sep = "*")
1087+
} else {
1088+
rhs <- pathEdgeRhs
1089+
}
1090+
} else {
1091+
# Get pars from moderators
1092+
modPars <- .procMedEffectsSyntaxModPars(pathEdge, sourceNode, contrFacVars, graph)
1093+
1094+
# Get sum of moderator terms
1095+
modPars <- .doCallPaste(modPars, sep = " + ")
1096+
1097+
# If indirect path add parentheses
1098+
if (i > 1) {
1099+
pathEdgeRhs <- paste0("(", pathEdge$parName, " + ", unlist(modPars), ")")
1100+
} else {
1101+
pathEdgeRhs <- paste(pathEdge$parName, unlist(modPars), sep = " + ")
1102+
}
1103+
1104+
if (length(rhs) > 0) {
1105+
# Get all possible combinations with new moderators
1106+
if (all(!modVars %in% uniqueMods)) {
1107+
rhs <- apply(expand.grid(rhs, pathEdgeRhs), 1, paste, collapse = "*")
1108+
} else {
1109+
rhs <- paste(rhs, pathEdgeRhs, sep = "*")
1110+
}
1111+
} else {
1112+
rhs <- pathEdgeRhs
1113+
}
1114+
uniqueMods <- unique(c(uniqueMods, modVars))
10931115
}
1094-
# Concanenate path edge parName with moderator probes
1095-
return(paste(pathEdge$parName, modPars, sep = " + "))
1096-
})
1097-
1098-
# If indirect paths, multiply their steps
1099-
rhs <- .doCallPaste(rhs, sep = "*")
1116+
}
11001117

1101-
return(rhs)
1118+
modProbes <- modProbes[uniqueMods]
1119+
1120+
# Substitute moderator variables with probe values
1121+
rhsProbes <- c()
1122+
1123+
if (any(sourceInContrFacVars) && length(modProbes) > 0) {
1124+
# Identify which modProbes are factor dummies
1125+
modIdx <- sapply(names(modProbes), function(nm) {
1126+
idx <- which(sapply(contrFacVars, function(fac) nm %in% fac))
1127+
1128+
if (length(idx) == 0) return(FALSE)
1129+
1130+
return(idx)
1131+
})
1132+
# Remove non-factor moderators that have index zero
1133+
facModIdx <- modIdx[modIdx > 0]
1134+
1135+
facProbeList <- list()
1136+
facGroups <- list()
1137+
if (any(facModIdx)) {
1138+
# Group factor dummies by factor (with >2 levels)
1139+
facGroups <- contrFacVars[unique(facModIdx)]
1140+
names(facGroups) <- as.character(unique(facModIdx))
1141+
# Concatenate levels of the same factor
1142+
facProbeList <- lapply(facGroups, function(grp) {
1143+
probes <- Reduce(cbind, modProbes[grp])
1144+
1145+
if (is.null(dim(probes))) return(probes)
1146+
1147+
return(apply(probes, 1, paste, collapse = "_"))
1148+
})
1149+
}
1150+
# Now, collect all non-factor moderators
1151+
nonFacMods <- names(modProbes)[modIdx == 0]
1152+
nonFacProbeList <- modProbes[nonFacMods]
1153+
# Combine all for expansion
1154+
expandList <- c(facProbeList, nonFacProbeList)
1155+
modGrid <- expand.grid(expandList, stringsAsFactors = FALSE)
1156+
# Assign column names for substitution
1157+
colNames <- c(names(facProbeList), nonFacMods)
1158+
colnames(modGrid) <- colNames
1159+
1160+
# Split concatenated factor columns back into original dummy columns
1161+
if (length(facProbeList) > 0) {
1162+
for (k in 1:length(facProbeList)) {
1163+
facCol <- names(facProbeList)[k]
1164+
# Get the original dummy variable names for this factor
1165+
origDummies <- facGroups[[facCol]]
1166+
if (length(origDummies) == 1) {
1167+
splitVals <- matrix(modGrid[[facCol]], ncol = 1)
1168+
} else {
1169+
# Split the concatenated string into separate columns
1170+
splitVals <- do.call(rbind, strsplit(modGrid[[facCol]], "_", fixed = TRUE))
1171+
# If only one dummy, keep as vector
1172+
}
1173+
1174+
# Assign columns with original dummy names
1175+
for (d in 1:length(origDummies)) {
1176+
modGrid[[origDummies[d]]] <- splitVals[, d]
1177+
}
1178+
# Remove the concatenated column
1179+
modGrid[[facCol]] <- NULL
1180+
}
1181+
}
1182+
1183+
for (j in 1:nrow(modGrid)) { # Probe combinations
1184+
for (i in 1:length(rhs)) { # Path edges
1185+
rowRhs <- rhs[i]
1186+
# Replace factor dummies as a group
1187+
for (p in 1:ncol(modGrid)) {
1188+
rowRhs <- gsub(colnames(modGrid)[p], format(modGrid[j, p], digits = 3), rowRhs)
1189+
}
1190+
rhsProbes <- c(rhsProbes, rowRhs)
1191+
}
1192+
}
1193+
} else {
1194+
# Default: iterate over rhs first, then probes
1195+
for (i in 1:length(rhs)) { # Path edges
1196+
modInPath <- sapply(names(modProbes), function(v) grepl(v, rhs[i]))
1197+
if (any(modInPath)) {
1198+
modGrid <- expand.grid(modProbes[modInPath])
1199+
for (j in 1:nrow(modGrid)) { # Probe combinations
1200+
rowRhs <- rhs[i]
1201+
for (p in 1:ncol(modGrid)) { # Moderator vars
1202+
rowRhs <- gsub(colnames(modGrid)[p], format(modGrid[j, p], digits = 3), rowRhs)
1203+
}
1204+
rhsProbes <- c(rhsProbes, rowRhs)
1205+
}
1206+
} else {
1207+
rhsProbes <- c(rhsProbes, rhs[i])
1208+
}
1209+
}
1210+
}
1211+
1212+
return(rhsProbes)
11021213
}
11031214

11041215
.procMedEffectsSyntaxSinglePath <- function(path, graph, modProbes, contrasts) {

tests/testthat/test-classic-process-unit.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,8 @@ test_that("Test that .procMedEffectsSyntaxModPars works - no contrasts", {
480480
pathEdge <- igraph::E(graph)["contGamma" %--% "contNormal"]
481481
sourceNode <- "contGamma"
482482

483-
modPars <- jaspProcess:::.procMedEffectsSyntaxModPars(pathEdge, sourceNode, list(), graph, modProbes)
484-
expect_equal(modPars, c("c3*0.1", "c3*0.5", "c3*0.9"))
483+
modPars <- jaspProcess:::.procMedEffectsSyntaxModPars(pathEdge, sourceNode, list(), graph)
484+
expect_equal(modPars, list("c3*contcor2"))
485485
})
486486

487487
test_that("Test that .procMedEffectsSyntaxModPars works - with contrasts", {
@@ -502,8 +502,8 @@ test_that("Test that .procMedEffectsSyntaxModPars works - with contrasts", {
502502
pathEdge <- igraph::E(graph)["contGammaA" %--% "contNormal"]
503503
sourceNode <- "contGammaA"
504504

505-
modPars <- jaspProcess:::.procMedEffectsSyntaxModPars(pathEdge, sourceNode, contrFacVars, graph, modProbes)
506-
expect_equal(modPars, c("c4*0.1", "c4*0.5", "c4*0.9"))
505+
modPars <- jaspProcess:::.procMedEffectsSyntaxModPars(pathEdge, sourceNode, contrFacVars, graph)
506+
expect_equal(modPars, list("c4*contcor2"))
507507
})
508508

509509
test_that("Test that .procMedEffectsSyntaxGetLhs works - no contrasts", {

0 commit comments

Comments
 (0)