Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: lavaanExtra
Title: Convenience Functions for Package 'lavaan'
Version: 0.2.1
Version: 0.2.1.1
Date: 2024-07-01
Authors@R:
person("Rémi", "Thériault", , "remi.theriault@mail.mcgill.ca", role = c("aut", "cre"),
Expand Down
4 changes: 2 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# lavaanExtra 0.2.2
* Incoming ✨

# lavaanExtra 0.2.1.1
* Incoming ✨
## lavaanExtra 0.2.1.1
* `write_lavaan()`: add new `total` argument for automatic calculation of total effects (direct + indirect effects), particularly useful for time series and panel data models (#4)

# lavaanExtra 0.2.1
* New CRAN submission
Expand Down
93 changes: 92 additions & 1 deletion R/write_lavaan.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
#' and "DV" (dependent variables), `write_lavaan` attempts to
#' write indirect effects automatically. In this case, the
#' `mediation` argument must be specified too.
#' @param total Total effect indicators (`:=` symbol: "total
#' effect defined as"). If a named list is provided,
#' with names "IV" (independent variables) and "DV" (dependent
#' variables), `write_lavaan` attempts to write total effects
#' automatically by combining direct and indirect effects. In this
#' case, the `mediation` and `regression` arguments must be
#' specified too. This is particularly useful for time series and
#' panel data models where effects propagate across time points.
#' @param latent Latent variable indicators (`=~` symbol: "is measured by").
#' @param intercept Intercept indicators (`~ 1` symbol: "intercept").
#' @param threshold Threshold indicators (`|` symbol: "threshold").
Expand Down Expand Up @@ -53,6 +61,7 @@ write_lavaan <- function(mediation = NULL,
regression = NULL,
covariance = NULL,
indirect = NULL,
total = NULL,
latent = NULL,
intercept = NULL,
threshold = NULL,
Expand Down Expand Up @@ -133,6 +142,88 @@ write_lavaan <- function(mediation = NULL,
"[--------Mediations (indirect effects)---------]"
)
}
#### AUTOMATIC TOTAL EFFECTS!!! ####
if (!is.null(total)) {
if (all(names(total) %in% c("IV", "DV"))) {
# Generate total effects by combining direct and indirect effects
total_expressions <- list()

for (iv in total$IV) {
for (dv in total$DV) {
effect_name <- paste0(iv, "_total_", dv)
paths <- c()

# Look for direct path
if (!is.null(regression) && dv %in% names(regression) && iv %in% regression[[dv]]) {
if (isTRUE(label)) {
if (isTRUE(use.letters)) {
# Find position of iv in regression for dv
iv_pos <- which(regression[[dv]] == iv)
direct_path <- paste0(letters[iv_pos], "_", dv)
} else {
direct_path <- paste0(iv, "_", dv)
}
} else {
# No labels, just use coefficient reference
direct_path <- paste0(iv, "_", dv)
}
paths <- c(paths, direct_path)
}

# Look for indirect paths via mediators
if (!is.null(mediation)) {
# Find mediators that iv predicts
iv_to_mediators <- names(mediation)[sapply(mediation, function(x) iv %in% x)]
# Find mediators that predict dv
mediators_to_dv <- character(0)
if (dv %in% names(mediation)) {
mediators_to_dv <- mediation[[dv]]
}

# Find common mediators (mediators in both paths)
common_mediators <- intersect(iv_to_mediators, mediators_to_dv)

if (length(common_mediators) > 0) {
for (mediator in common_mediators) {
if (isTRUE(label)) {
if (isTRUE(use.letters)) {
# Find position of iv in mediator's predictors
iv_pos <- which(mediation[[mediator]] == iv)
iv_to_m_path <- paste0(letters[iv_pos], "_", mediator)

# Find position of mediator in dv's predictors
m_pos <- which(mediation[[dv]] == mediator)
m_to_dv_path <- paste0(letters[m_pos], "_", dv)
} else {
iv_to_m_path <- paste0(iv, "_", mediator)
m_to_dv_path <- paste0(mediator, "_", dv)
}
} else {
iv_to_m_path <- paste0(iv, "_", mediator)
m_to_dv_path <- paste0(mediator, "_", dv)
}

indirect_path <- paste0(iv_to_m_path, " * ", m_to_dv_path)
paths <- c(paths, indirect_path)
}
}
}

# Only create total effect if there are paths
if (length(paths) > 0) {
total_expressions[[effect_name]] <- paste(paths, collapse = " + ")
}
}
}

total <- total_expressions
}
total <- process_vars(
total,
symbol = ":=", collapse = " + ", title =
"[-----------Total effects (direct + indirect)----------]"
)
}
if (!is.null(mediation)) {
mediation <- process_vars(
mediation,
Expand Down Expand Up @@ -201,7 +292,7 @@ write_lavaan <- function(mediation = NULL,
header <- paste0(hashtag, paste0("# ", title, "\n\n"))
custom <- paste0(header, custom)
}
paste0(latent, mediation, regression, covariance, indirect, intercept,
paste0(latent, mediation, regression, covariance, indirect, total, intercept,
threshold, constraint, custom,
collapse = ""
)
Expand Down
122 changes: 119 additions & 3 deletions tests/testthat/_snaps/write_lavaan.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,121 @@

y1 + y2 ~ f1 + f2 + x1 + x2

# write_lavaan using total effects

Code
cat(write_lavaan(mediation = mediation_simple, regression = regression_simple,
total = total_simple, label = TRUE))
Output
##################################################
# [-----------Mediations (named paths)-----------]

speed ~ visual_speed*visual
visual ~ ageyr_visual*ageyr

##################################################
# [---------Regressions (Direct effects)---------]

speed ~ ageyr

##################################################
# [-----------Total effects (direct + indirect)----------]

ageyr_total_speed := ageyr_speed + ageyr_visual * visual_speed


# write_lavaan using total effects multiple IVs and DVs

Code
cat(write_lavaan(mediation = mediation_multi, regression = regression_multi,
total = total_multi, label = TRUE))
Output
##################################################
# [-----------Mediations (named paths)-----------]

speed ~ visual_speed*visual
textual ~ visual_textual*visual
visual ~ ageyr_visual*ageyr + grade_visual*grade

##################################################
# [---------Regressions (Direct effects)---------]

speed ~ ageyr + grade
textual ~ ageyr + grade

##################################################
# [-----------Total effects (direct + indirect)----------]

ageyr_total_speed := ageyr_speed + ageyr_visual * visual_speed
ageyr_total_textual := ageyr_textual + ageyr_visual * visual_textual
grade_total_speed := grade_speed + grade_visual * visual_speed
grade_total_textual := grade_textual + grade_visual * visual_textual


# write_lavaan using total effects with letters

Code
cat(write_lavaan(mediation = mediation_letters, regression = regression_letters,
total = total_letters, label = TRUE, use.letters = TRUE))
Output
##################################################
# [-----------Mediations (named paths)-----------]

speed ~ a_speed*visual
visual ~ a_visual*ageyr

##################################################
# [---------Regressions (Direct effects)---------]

speed ~ ageyr

##################################################
# [-----------Total effects (direct + indirect)----------]

ageyr_total_speed := a_speed + a_visual * a_speed


# write_lavaan using total effects direct only

Code
cat(write_lavaan(regression = regression_direct, total = total_direct, label = TRUE))
Output
##################################################
# [---------Regressions (Direct effects)---------]

speed ~ ageyr

##################################################
# [-----------Total effects (direct + indirect)----------]

ageyr_total_speed := ageyr_speed


# write_lavaan using total effects indirect only

Code
cat(write_lavaan(mediation = mediation_indirect, total = total_indirect, label = TRUE))
Output
##################################################
# [-----------Mediations (named paths)-----------]

speed ~ visual_speed*visual
visual ~ ageyr_visual*ageyr

##################################################
# [-----------Total effects (direct + indirect)----------]

ageyr_total_speed := ageyr_visual * visual_speed


# write_lavaan using everything

Code
cat(write_lavaan(mediation = mediation, regression = regression, covariance = covariance,
indirect = indirect, latent = latent, intercept = intercept, threshold = threshold,
constraint.equal = constraint.equal, constraint.smaller = constraint.smaller,
constraint.larger = constraint.larger, custom = custom, label = TRUE))
indirect = indirect, total = total, latent = latent, intercept = intercept,
threshold = threshold, constraint.equal = constraint.equal,
constraint.smaller = constraint.smaller, constraint.larger = constraint.larger,
custom = custom, label = TRUE))
Output
##################################################
# [-----Latent variables (measurement model)-----]
Expand Down Expand Up @@ -222,6 +330,14 @@
grade_visual_speed := grade_visual * visual_speed
grade_visual_textual := grade_visual * visual_textual

##################################################
# [-----------Total effects (direct + indirect)----------]

ageyr_total_speed := ageyr_speed + ageyr_visual * visual_speed
ageyr_total_textual := ageyr_textual + ageyr_visual * visual_textual
grade_total_speed := grade_speed + grade_visual * visual_speed
grade_total_textual := grade_textual + grade_visual * visual_textual

##################################################
# [------------------Intercepts------------------]

Expand Down
66 changes: 66 additions & 0 deletions tests/testthat/test-write_lavaan.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ latent <- list(
# Define indirect effects object
indirect <- list(IV = IV, M = M, DV = DV)

# Define total effects object
total <- list(IV = IV, DV = DV)

# Special cases
intercept <- c("mpg", "cyl", "disp")
constraint.equal <- list(b1 = "(b2 + b3)^2")
Expand Down Expand Up @@ -103,12 +106,75 @@ test_that("write_lavaan using custom", {
expect_snapshot(cat(write_lavaan(custom = custom)))
})

test_that("write_lavaan using total effects", {
# Simple total effects
total_simple <- list(IV = "ageyr", DV = "speed")
mediation_simple <- list(speed = "visual", visual = "ageyr")
regression_simple <- list(speed = "ageyr")
expect_snapshot(cat(write_lavaan(
mediation = mediation_simple,
regression = regression_simple,
total = total_simple,
label = TRUE
)))
})

test_that("write_lavaan using total effects multiple IVs and DVs", {
# Multiple IVs and DVs
total_multi <- list(IV = c("ageyr", "grade"), DV = c("speed", "textual"))
mediation_multi <- list(speed = "visual", textual = "visual", visual = c("ageyr", "grade"))
regression_multi <- list(speed = c("ageyr", "grade"), textual = c("ageyr", "grade"))
expect_snapshot(cat(write_lavaan(
mediation = mediation_multi,
regression = regression_multi,
total = total_multi,
label = TRUE
)))
})

test_that("write_lavaan using total effects with letters", {
# Total effects with letter labeling
total_letters <- list(IV = "ageyr", DV = "speed")
mediation_letters <- list(speed = "visual", visual = "ageyr")
regression_letters <- list(speed = "ageyr")
expect_snapshot(cat(write_lavaan(
mediation = mediation_letters,
regression = regression_letters,
total = total_letters,
label = TRUE,
use.letters = TRUE
)))
})

test_that("write_lavaan using total effects direct only", {
# Total effects when only direct paths exist (no mediators)
total_direct <- list(IV = "ageyr", DV = "speed")
regression_direct <- list(speed = "ageyr")
expect_snapshot(cat(write_lavaan(
regression = regression_direct,
total = total_direct,
label = TRUE
)))
})

test_that("write_lavaan using total effects indirect only", {
# Total effects when only indirect paths exist (no direct paths)
total_indirect <- list(IV = "ageyr", DV = "speed")
mediation_indirect <- list(speed = "visual", visual = "ageyr")
expect_snapshot(cat(write_lavaan(
mediation = mediation_indirect,
total = total_indirect,
label = TRUE
)))
})

test_that("write_lavaan using everything", {
expect_snapshot(cat(write_lavaan(
mediation = mediation,
regression = regression,
covariance = covariance,
indirect = indirect,
total = total,
latent = latent,
intercept = intercept,
threshold = threshold,
Expand Down
Loading