Skip to content
Merged
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: semptools
Title: Customizing Structural Equation Modelling Plots
Version: 0.3.2.1
Version: 0.3.2.2
Authors@R: c(
person(given = "Shu Fai",
family = "Cheung",
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export(is_dv_residvar)
export(keep_nodes)
export(lavaan_indicator_order)
export(layout_matrix)
export(mark_ci)
export(mark_se)
export(mark_sig)
export(move_node)
Expand Down
7 changes: 6 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# semptools 0.3.2.1
# semptools 0.3.2.2

## New Features

- Added `mark_ci()` for adding confidence
intervals to a plot. (0.3.2.2)

## Improvement

Expand Down
251 changes: 165 additions & 86 deletions R/mark_se.R
Original file line number Diff line number Diff line change
@@ -1,116 +1,157 @@
#'@title Add Standard Error Estimates to Parameter Estimates (Edge
#' Labels)
#' @title Add Standard Error/Confidence Interval Estimates to Parameter
#' Estimates (Edge Labels)
#'
#'@description Add standard error estimates, in parentheses, to
#' parameter estimates (edge labels) in a [qgraph::qgraph] object.
#' @description Add standard error or confidence interval estimates,
#' in parentheses, to parameter estimates (edge labels) in a
#' [qgraph::qgraph] object.
#'
#'@details Modify a [qgraph::qgraph] object generated by
#' @details Modify a [qgraph::qgraph] object generated by
#' \code{\link[semPlot]{semPaths}} (currently in parentheses) to the
#' labels. Require either the original object used in the semPaths call,
#' or a data frame with the standard error for each parameter. The latter
#' option is for standard errors not computed by lavaan but by
#' other functions.
#' or a data frame with the standard error (or confidence interval) for
#' each parameter. The latter option is for standard errors/confidence
#' interval not computed by lavaan but by other functions.
#'
#'Currently supports only plots based on \code{\link[lavaan]{lavaan}}
#'output.
#' Currently supports only plots based on \code{\link[lavaan]{lavaan}}
#' output.
#'
#'This function is a variant of, and can be combined with, the
#' \code{\link{mark_sig}} function.
#' This function is a variant of, and can be combined with, the
#' \code{\link{mark_sig}} function.
#'
#'@return If the input is a [qgraph::qgraph] object, the function
#' returns a qgraph based on the original one, with standard error
#' estimates appended. If the input is a list of qgraph objects, the
#' function returns a list of the same length.
#' @return If the input is a [qgraph::qgraph] object, the function
#' returns a qgraph based on the original one, with standard error
#' or confidence interval estimates appended. If the input is a list
#' of qgraph objects, the function returns a list of the same length.
#'
#'@param semPaths_plot A qgraph object generated by
#' @param semPaths_plot A qgraph object generated by
#' \code{\link[semPlot]{semPaths}}, or a similar qgraph object
#' modified by other [semptools] functions.
#'
#'@param object The object used by semPaths to generate the plot. Use
#' @param object The object used by semPaths to generate the plot. Use
#' the same argument name used in \code{\link[semPlot]{semPaths}} to
#' make the meaning of this argument obvious. Currently only object
#' of class `lavaan` is supported.
#'
#'@param sep A character string to separate the coefficient and the
#' @param sep A character string to separate the coefficient and the
#' standard error (in parentheses). Default to " " (one space). Use
#' \code{"\n"} to enforce a line break.
#'
#'@param digits Integer indicating number of decimal places for the
#' @param digits Integer indicating number of decimal places for the
#' appended standard errors. Default is 2L.
#'
#'@param ests A data.frame from the
#' @param ests A data.frame from the
#' \code{\link[lavaan]{parameterEstimates}} function, or
#' from other function with these columns:? `lhs`, `op`,
#' `rhs`, and `se`. Only used when
#' \code{object} is not specified.
#' from other function with these columns: `lhs`, `op`,
#' `rhs`, `se` (for SE), and `ci.lower` and
#' `ci.upper` (for CI). Only used when \code{object} is
#' not specified.
#'
#'@param std_type If standardized solution is used in the plot,
#' @param std_type If standardized solution is used in the plot,
#' set this either to the type of standardization (e.g., `"std.all"`)
#' or to `TRUE`. It will be passed to [lavaan::standardizedSolution()]
#' to compute the standard errors for the standardized solution.
#' Used only if standard errors are not supplied directly
#' through `ests`.
#'
#'@examples
#'mod_pa <-
#' 'x1 ~~ x2
#' x3 ~ x1 + x2
#' x4 ~ x1 + x3
#' @examples
#' mod_pa <-
#' 'x1 ~~ x2
#' x3 ~ x1 + x2
#' x4 ~ x1 + x3
#' '
#' fit_pa <- lavaan::sem(mod_pa, pa_example)
#' lavaan::parameterEstimates(fit_pa)[ , c("lhs", "op", "rhs",
#' "est", "pvalue", "se")]
#' m <- matrix(c("x1", NA, NA,
#' NA, "x3", "x4",
#' "x2", NA, NA), byrow = TRUE, 3, 3)
#' p_pa <- semPlot::semPaths(fit_pa, whatLabels = "est",
#' style = "ram",
#' nCharNodes = 0, nCharEdges = 0,
#' layout = m)
#' p_pa2 <- mark_se(p_pa, fit_pa)
#' plot(p_pa2)
#'
#' mod_cfa <-
#' 'f1 =~ x01 + x02 + x03
#' f2 =~ x04 + x05 + x06 + x07
#' f3 =~ x08 + x09 + x10
#' f4 =~ x11 + x12 + x13 + x14
#' '
#'fit_pa <- lavaan::sem(mod_pa, pa_example)
#'lavaan::parameterEstimates(fit_pa)[ , c("lhs", "op", "rhs",
#' "est", "pvalue", "se")]
#'m <- matrix(c("x1", NA, NA,
#' NA, "x3", "x4",
#' "x2", NA, NA), byrow = TRUE, 3, 3)
#'p_pa <- semPlot::semPaths(fit_pa, whatLabels = "est",
#' style = "ram",
#' nCharNodes = 0, nCharEdges = 0,
#' layout = m)
#'p_pa2 <- mark_se(p_pa, fit_pa)
#'plot(p_pa2)
#'
#'mod_cfa <-
#' fit_cfa <- lavaan::sem(mod_cfa, cfa_example)
#' lavaan::parameterEstimates(fit_cfa)[ , c("lhs", "op", "rhs",
#' "est", "pvalue", "se")]
#' p_cfa <- semPlot::semPaths(fit_cfa, whatLabels = "est",
#' style = "ram",
#' nCharNodes = 0, nCharEdges = 0)
#' # Place standard errors on a new line
#' p_cfa2 <- mark_se(p_cfa, fit_cfa, sep = "\n")
#' plot(p_cfa2)
#'
#' mod_sem <-
#' 'f1 =~ x01 + x02 + x03
#' f2 =~ x04 + x05 + x06 + x07
#' f3 =~ x08 + x09 + x10
#' f4 =~ x11 + x12 + x13 + x14
#' f3 ~ f1 + f2
#' f4 ~ f1 + f3
#' '
#'fit_cfa <- lavaan::sem(mod_cfa, cfa_example)
#'lavaan::parameterEstimates(fit_cfa)[ , c("lhs", "op", "rhs",
#' "est", "pvalue", "se")]
#'p_cfa <- semPlot::semPaths(fit_cfa, whatLabels = "est",
#' style = "ram",
#' nCharNodes = 0, nCharEdges = 0)
#'# Place standard errors on a new line
#'p_cfa2 <- mark_se(p_cfa, fit_cfa, sep = "\n")
#'plot(p_cfa2)
#'
#'mod_sem <-
#' 'f1 =~ x01 + x02 + x03
#' f2 =~ x04 + x05 + x06 + x07
#' f3 =~ x08 + x09 + x10
#' f4 =~ x11 + x12 + x13 + x14
#' f3 ~ f1 + f2
#' f4 ~ f1 + f3
#' '
#'fit_sem <- lavaan::sem(mod_sem, sem_example)
#'lavaan::parameterEstimates(fit_sem)[ , c("lhs", "op", "rhs",
#' "est", "pvalue", "se")]
#'p_sem <- semPlot::semPaths(fit_sem, whatLabels = "est",
#' style = "ram",
#' nCharNodes = 0, nCharEdges = 0)
#'# Mark significance, and then add standard errors
#'p_sem2 <- mark_sig(p_sem, fit_sem)
#'p_sem3 <- mark_se(p_sem2, fit_sem, sep = "\n")
#'plot(p_sem3)
#'
#'@importFrom rlang .data
#'@export
#' fit_sem <- lavaan::sem(mod_sem, sem_example)
#' lavaan::parameterEstimates(fit_sem)[ , c("lhs", "op", "rhs",
#' "est", "pvalue", "se")]
#' p_sem <- semPlot::semPaths(fit_sem, whatLabels = "est",
#' style = "ram",
#' nCharNodes = 0, nCharEdges = 0)
#' # Mark significance, and then add standard errors
#' p_sem2 <- mark_sig(p_sem, fit_sem)
#' p_sem3 <- mark_se(p_sem2, fit_sem, sep = "\n")
#' plot(p_sem3)
#'
#' # Add confidence intervals
#' p_sem4 <- mark_ci(p_sem, fit_sem, sep = "\n")
#' plot(p_sem4)
#'
#' @importFrom rlang .data
#' @export

mark_se <- function(semPaths_plot, object = NULL, sep = " ", digits = 2L,
ests = NULL,
std_type = FALSE) {
.mark_se_ci(
semPaths_plot = semPaths_plot,
object = object,
what = "se",
sep = sep,
digits = digits,
ests = ests,
std_type = std_type
)
}

mark_se <- function(semPaths_plot, object, sep = " ", digits = 2L,
#' @rdname mark_se
#' @export

mark_ci <- function(semPaths_plot, object = NULL, sep = " ", digits = 2L,
ests = NULL,
std_type = FALSE) {
.mark_se_ci(
semPaths_plot = semPaths_plot,
object = object,
what = "ci",
sep = sep,
digits = digits,
ests = ests,
std_type = std_type
)
}

.mark_se_ci <- function(semPaths_plot, object = NULL,
what = c("se", "ci"),
sep = " ", digits = 2L,
ests = NULL,
std_type = FALSE) {
argg <- c(as.list(environment()))
what <- match.arg(what)
if ("triangle" %in% semPaths_plot$graphAttributes$Nodes$shape) {
rlang::inform(paste("The semPaths plot seems to have one or",
"more intercepts. Support for models with",
Expand All @@ -120,12 +161,12 @@ mark_se <- function(semPaths_plot, object, sep = " ", digits = 2L,
}
if (is.null(ests)) {
if (isFALSE(std_type)) {
ests <- lavaan::parameterEstimates(object, se = TRUE, ci = FALSE,
ests <- lavaan::parameterEstimates(object, se = TRUE, ci = what == "ci",
zstat = FALSE, pvalue = FALSE)
} else {
if (isTRUE(std_type)) std_type <- "std.all"
ests <- lavaan::standardizedSolution(object, type = std_type,
se = TRUE, ci = FALSE,
se = TRUE, ci = what == "ci",
zstat = FALSE, pvalue = FALSE)
}
}
Expand All @@ -134,10 +175,13 @@ mark_se <- function(semPaths_plot, object, sep = " ", digits = 2L,
rlang::abort(paste("length of qgraph list does not match",
"number of groups in model fit object."))
}
ests_list <- split(ests, ests$group)
mapply(mark_se, semPaths_plot, ests = ests_list, SIMPLIFY = FALSE)
argg$ests <- split(ests, ests$group)
.mapply(.mark_se_ci, dots = argg[c("semPaths_plot", "ests")],
MoreArgs = argg[
setdiff(names(argg), c("semPaths_plot", "object", "ests"))
])
} else {
if (!missing(object) && lavaan::lavInspect(object, "ngroups") > 1) {
if (!is.null(object) && lavaan::lavInspect(object, "ngroups") > 1) {
rlang::abort(paste("length of qgraph list does not match",
"number of groups in model fit object."))
}
Expand Down Expand Up @@ -167,11 +211,25 @@ mark_se <- function(semPaths_plot, object, sep = " ", digits = 2L,
# Remove ~*~. Not used.
to_keep <- to_keep & (ests$op != "~*~")

ests_ses <- ests[to_keep, c("lhs", "rhs", "se")]
if (what == "se") {
ests_cols <- c("lhs", "rhs", "se")
cols_rev <- c("se", "se_rev")
} else if (what == "ci") {
ests_cols <- c("lhs", "rhs", "ci.lower", "ci.upper")
cols_rev <- c("ci.lower", "ci.upper", "ci.lower_rev", "ci.upper_rev")
}
ests_ses <- ests[to_keep, ests_cols]
ests_ses_rev <- ests_ses
colnames(ests_ses_rev) <- gsub("\\<se\\>",
"se_rev",
colnames(ests_ses_rev))
colnames(ests_ses_rev) <- gsub("\\<ci\\.lower\\>",
"ci.lower_rev",
colnames(ests_ses_rev))

colnames(ests_ses_rev) <- gsub("\\<ci\\.upper\\>",
"ci.upper_rev",
colnames(ests_ses_rev))
ests_ses_tmp <- ests_ses
colnames(ests_ses_tmp) <- gsub("\\<rhs\\>",
"from_names",
Expand Down Expand Up @@ -200,19 +258,40 @@ mark_se <- function(semPaths_plot, object, sep = " ", digits = 2L,
all.x = TRUE,
all.y = FALSE,
sort = FALSE)
all_na <- apply(edge_ses[, c("se", "se_rev")],
all_na <- apply(edge_ses[, cols_rev],
MARGIN = 1,
FUN = function(x) all(is.na(x)))
edge_ses$se <- suppressWarnings(
if (what == "se") {
edge_ses$se <- suppressWarnings(
apply(edge_ses[, c("se", "se_rev")],
MARGIN = 1,
FUN = max,
na.rm = TRUE))
edge_ses$se[all_na] <- NA
edge_ses$se[all_na] <- NA
} else if (what == "ci") {
edge_ses$ci.lower <- suppressWarnings(
apply(edge_ses[, c("ci.lower", "ci.lower_rev")],
MARGIN = 1,
FUN = min,
na.rm = TRUE))
edge_ses$ci.upper <- suppressWarnings(
apply(edge_ses[, c("ci.upper", "ci.upper_rev")],
MARGIN = 1,
FUN = max,
na.rm = TRUE))
edge_ses[all_na, c("ci.lower", "ci.upper")] <- NA
}
edge_ses <- edge_ses[order(edge_ses$id), ]
labels_old <- semPaths_plot$graphAttributes$Edges$labels
if (what == "se") {
to_add <- formatC(edge_ses$se, digits, format = "f")
} else if (what == "ci") {
to_add <- paste(formatC(edge_ses$ci.lower, digits, format = "f"),
formatC(edge_ses$ci.upper, digits, format = "f"),
sep = ", ")
}
labels_new <- paste0(labels_old, sep,
"(", formatC(edge_ses$se, digits, format = "f"), ")")
"(", to_add, ")")
semPaths_plot$graphAttributes$Edges$labels <- labels_new
semPaths_plot
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![R-CMD-check](https://github.com/sfcheung/semptools/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/sfcheung/semptools/actions/workflows/R-CMD-check.yaml)
<!-- badges: end -->

(Version 0.3.2.1, updated on 2025-07-20, [release history](https://sfcheung.github.io/semptools/news/index.html))
(Version 0.3.2.2, updated on 2025-08-17, [release history](https://sfcheung.github.io/semptools/news/index.html))

# semptools <img src="man/figures/logo.png" align="right" height="150" />

Expand Down
Loading
Loading