From bf7df6dc6bc156a7102d16b4f385665a2bac4c1a Mon Sep 17 00:00:00 2001 From: Sunthud Pornprasertmanit Date: Sun, 15 Feb 2026 09:41:18 +0700 Subject: [PATCH 1/5] Update Bonferroni adjustment in epcEquivFit() and rename internal helper functions for clarity --- semTools/R/miPowerFit.R | 123 +++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 52 deletions(-) diff --git a/semTools/R/miPowerFit.R b/semTools/R/miPowerFit.R index 685704e..d0fdecc 100644 --- a/semTools/R/miPowerFit.R +++ b/semTools/R/miPowerFit.R @@ -1,5 +1,5 @@ ### Sunthud Pornprasertmanit; with contributions by Terrence D. Jorgensen -### Last updated: 3 February 2026 +### Last updated: 15 February 2026 #' EPC Equivalence Fit Evaluation Using Modification Indices @@ -50,6 +50,16 @@ #' arguments. #' @param cilevel Confidence level for EPC confidence intervals used in #' CI-based equivalence testing. +#' @param mialpha Significance level used for evaluating modification +#' indices in the power-based decision rule (Method 1). Default is 0.05. +#' @param mipower Desired statistical power for detecting a misspecification +#' of the specified SESOI in the power-based decision rule. Default is 0.80. +#' @param adjust.method Multiplicity adjustment method applied to both +#' modification index tests and EPC confidence intervals. Currently +#' supported options are \code{"none"} (no adjustment) and +#' \code{"bonferroni"}. When \code{"bonferroni"} is used, the MI +#' significance level and CI level are adjusted across all evaluated +#' fixed parameters. #' @param \dots Additional arguments passed to #' \code{\link[lavaan]{modificationIndices}}. #' @@ -59,6 +69,10 @@ #' fixed parameters are substantively misspecified relative to a SESOI, #' rather than whether a model fits exactly. #' +#' When \code{adjust.method = "bonferroni"}, familywise error control +#' is applied across all evaluated fixed parameters by adjusting the +#' MI significance threshold and widening EPC confidence intervals. +#' #' Models with categorical indicators or unsupported constraints may #' not be fully supported. #' @@ -122,9 +136,15 @@ epcEquivFit <- function(lavaanObj, stdIntcept = 0.2, stdSesoi = NULL, sesoi = NULL, - cilevel = 0.90, ...) { + cilevel = 0.90, + mialpha = 0.05, + mipower = 0.80, + adjust.method = "none", ...) { dots <- list(...) + adjustmethodargs <- c("none", "bonferroni") + adjust.method <- match.arg(adjust.method, adjustmethodargs) + df_model <- lavaan::fitMeasures(lavaanObj, "df") if (is.na(df_model) || df_model <= 0) { @@ -175,35 +195,40 @@ epcEquivFit <- function(lavaanObj, corLatent = corLatent, corResidual = corResidual, stdBeta = stdBeta, stdIntcept = stdIntcept) if (length(stdSesoi) == 1) stdSesoi <- rep(stdSesoi, nrow(mi)) - sesoi <- unstandardizeEpc(mi, stdSesoi, lavInspectTotalVar(lavaanObj), lavInspectResidualVar(lavaanObj)) + sesoi <- unstandardizeFixedParam(mi, stdSesoi, lavInspectTotalVar(lavaanObj), lavInspectResidualVar(lavaanObj)) } - if (length(sesoi) == 1) sesoi <- rep(sesoi, nrow(mi)) + m <- nrow(mi) + if (length(sesoi) == 1) sesoi <- rep(sesoi, m) ncp <- (sesoi / sigma)^2 - alpha <- 0.05 - desiredPow <- 0.80 + ncp[!is.finite(ncp)] <- NA_real_ + alpha <- ifelse(adjust.method=="bonferroni", mialpha/m, mialpha) cutoff <- stats::qchisq(1 - alpha, df = 1) pow <- 1 - stats::pchisq(cutoff, df = 1, ncp = ncp) sigMI <- mi[,"mi"] > cutoff - highPow <- pow > desiredPow + highPow <- pow > mipower group <- rep(1, nrow(mi)) if ("group" %in% colnames(mi)) group <- mi[ , "group"] decision <- mapply(decisionMIPow, sigMI = sigMI, highPow = highPow, epc = mi[ , "epc"], trivialEpc = sesoi) - if (is.null(stdSesoi)) stdSesoi <- standardizeEpc(mi, lavInspectTotalVar(lavaanObj), + if (is.null(stdSesoi)) stdSesoi <- standardizeFixedParam(mi, lavInspectTotalVar(lavaanObj), lavInspectResidualVar(lavaanObj), - sesoi = sesoi) + value = sesoi) result <- cbind(mi[ , 1:3], group, as.numeric(mi[ , "mi"]), mi[ , "epc"], sesoi, mi[ , "sepc.all"], stdSesoi, sigMI, highPow, decision) - # New method - crit <- abs(stats::qnorm((1 - cilevel)/2)) - seepc <- abs(result[,6]) / sqrt(abs(result[,5])) - lowerepc <- result[,6] - crit * seepc - upperepc <- result[,6] + crit * seepc - stdlowerepc <- standardizeEpc(mi, lavInspectTotalVar(lavaanObj), - lavInspectResidualVar(lavaanObj), sesoi = lowerepc) - stdupperepc <- standardizeEpc(mi, lavInspectTotalVar(lavaanObj), - lavInspectResidualVar(lavaanObj), sesoi = upperepc) + alpha_ci <- 1 - cilevel + if (adjust.method == "bonferroni") { + alpha_ci <- alpha_ci / m + } + crit <- stats::qnorm(1 - alpha_ci/2) + seepc <- abs(sigma) + seepc[!is.finite(seepc)] <- NA_real_ + lowerepc <- mi[,"epc"] - crit * seepc + upperepc <- mi[,"epc"] + crit * seepc + stdlowerepc <- standardizeFixedParam(mi, lavInspectTotalVar(lavaanObj), + lavInspectResidualVar(lavaanObj), value = lowerepc) + stdupperepc <- standardizeFixedParam(mi, lavInspectTotalVar(lavaanObj), + lavInspectResidualVar(lavaanObj), value = upperepc) isVar <- mi[,"op"] == "~~" & mi[,"lhs"] == mi[,"rhs"] decisionci <- mapply(decisionCIEpc, targetval = as.numeric(stdSesoi), lower = stdlowerepc, upper = stdupperepc, @@ -759,15 +784,12 @@ getTrivialEpc <- function( } -# unstandardizeEpc() +# unstandardizeFixedParam() # ------------------------------------------------------------------ # Internal utility used by epcEquivFit() and related EPC diagnostics. -# Converts standardized effect-size thresholds (SESOI) back to the -# unstandardized EPC scale using total and residual variances of the -# involved variables. The transformation is operator-specific -# (e.g., loadings, regressions, covariances, intercepts) and provides -# unstandardized quantities required for EPC evaluation. -unstandardizeEpc <- function(mi, sesoi, totalVar, residualVar) { +# Converts standardized fixed-parameter values (e.g., standardized +# SESOI thresholds or CI bounds) back to the unstandardized EPC scale. +unstandardizeFixedParam <- function(mi, value, totalVar, residualVar) { name <- names(totalVar[[1]]) lhsPos <- match(mi[,"lhs"], name) rhsPos <- match(mi[,"rhs"], name) @@ -780,41 +802,38 @@ unstandardizeEpc <- function(mi, sesoi, totalVar, residualVar) { lhsVarRes <- mapply(getVarRes, pos=lhsPos, group=group) rhsVarRes <- mapply(getVarRes, pos=rhsPos, group=group) - FUN <- function(op, lhsVar, rhsVar, lhsVarRes, rhsVarRes, sesoi) { + FUN <- function(op, lhsVar, rhsVar, lhsVarRes, rhsVarRes, value) { if(op == "|") return(NA) lhsSD <- sqrt(lhsVar) rhsSD <- sqrt(rhsVar) lhsSDRes <- sqrt(lhsVarRes) rhsSDRes <- sqrt(rhsVarRes) - if(!is.numeric(sesoi)) sesoi <- as.numeric(sesoi) + if(!is.numeric(value)) value <- as.numeric(value) if(op == "=~") { - return((rhsSD * sesoi) / lhsSD) + return((rhsSD * value) / lhsSD) } else if (op == "~~") { - return(lhsSDRes * sesoi * rhsSDRes) + return(lhsSDRes * value * rhsSDRes) } else if (op == "~1") { - return(lhsSD * sesoi) + return(lhsSD * value) } else if (op == "~") { - return((lhsSD * sesoi) / rhsSD) + return((lhsSD * value) / rhsSD) } else { return(NA) } } - sesoi <- mapply(FUN, op=mi[,"op"], lhsVar=lhsVar, rhsVar=rhsVar, - lhsVarRes=lhsVarRes, rhsVarRes=rhsVarRes, sesoi=sesoi) - return(sesoi) + unstdValue <- mapply(FUN, op=mi[,"op"], lhsVar=lhsVar, rhsVar=rhsVar, + lhsVarRes=lhsVarRes, rhsVarRes=rhsVarRes, value=value) + return(unstdValue) } -# standardizeEpc() +# standardizeFixedParam() # ------------------------------------------------------------------ # Internal utility used by epcEquivFit() and related EPC diagnostics. -# Transforms unstandardized EPCs or SESOI values into standardized -# effect-size metrics based on total and residual variances of the -# involved variables. The standardization is operator-specific -# (e.g., loadings, regressions, covariances, intercepts) and produces -# standardized quantities suitable for comparison against standardized -# SESOI thresholds. -standardizeEpc <- function(mi, totalVar, residualVar, sesoi = NULL) { - if(is.null(sesoi)) sesoi <- mi[,"epc"] +# Converts unstandardized values of fixed parameters (e.g., EPCs, +# SESOI thresholds, or CI bounds) into standardized effect-size +# metrics. +standardizeFixedParam <- function(mi, totalVar, residualVar, value = NULL) { + if(is.null(value)) value <- mi[,"epc"] name <- names(totalVar[[1]]) lhsPos <- match(mi[,"lhs"], name) rhsPos <- match(mi[,"rhs"], name) @@ -826,31 +845,31 @@ standardizeEpc <- function(mi, totalVar, residualVar, sesoi = NULL) { rhsVar <- mapply(getVar, pos=rhsPos, group=group) lhsVarRes <- mapply(getVarRes, pos=lhsPos, group=group) rhsVarRes <- mapply(getVarRes, pos=rhsPos, group=group) - FUN <- function(op, lhsVar, rhsVar, lhsVarRes, rhsVarRes, sesoi) { + FUN <- function(op, lhsVar, rhsVar, lhsVarRes, rhsVarRes, value) { lhsSD <- sqrt(lhsVar) rhsSD <- sqrt(rhsVar) lhsSDRes <- sqrt(lhsVarRes) rhsSDRes <- sqrt(rhsVarRes) - if(!is.numeric(sesoi)) sesoi <- as.numeric(sesoi) + if(!is.numeric(value)) value <- as.numeric(value) if(op == "=~") { #stdload = beta * sdlatent / sdindicator = beta * lhs / rhs - return((sesoi / rhsSD) * lhsSD) + return((value / rhsSD) * lhsSD) } else if (op == "~~") { #r = cov / (sd1 * sd2) - return(sesoi / (lhsSDRes * rhsSDRes)) + return(value / (lhsSDRes * rhsSDRes)) } else if (op == "~1") { #d = meanDiff/sd - return(sesoi / lhsSD) + return(value / lhsSD) } else if (op == "~") { #beta = b * sdX / sdY = b * rhs / lhs - return((sesoi / lhsSD) * rhsSD) + return((value / lhsSD) * rhsSD) } else { return(NA) } } - stdSesoi <- mapply(FUN, op=mi[,"op"], lhsVar=lhsVar, rhsVar=rhsVar, - lhsVarRes=lhsVarRes, rhsVarRes=rhsVarRes, sesoi=sesoi) - return(stdSesoi) + stdValue <- mapply(FUN, op=mi[,"op"], lhsVar=lhsVar, rhsVar=rhsVar, + lhsVarRes=lhsVarRes, rhsVarRes=rhsVarRes, value=value) + return(stdValue) } # decisionMIPow() From 5175d7cf90889d9db5a18219ac1f164dcc41dc5f Mon Sep 17 00:00:00 2001 From: Sunthud Pornprasertmanit Date: Sun, 15 Feb 2026 10:53:06 +0700 Subject: [PATCH 2/5] Replace recommendation terminology with compensatory-effect labels in epcEquivCheck() --- semTools/R/miPowerFit.R | 84 +++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/semTools/R/miPowerFit.R b/semTools/R/miPowerFit.R index d0fdecc..4f8501d 100644 --- a/semTools/R/miPowerFit.R +++ b/semTools/R/miPowerFit.R @@ -256,17 +256,22 @@ miPowerFit <- function(...) { epcEquivFit(...) } -#' EPC Equivalence Feasibility Check for Standardized Parameters +#' EPC Equivalence Compensatory-Effect Check for Standardized Parameters #' -#' Performs an EPC-based feasibility check to assess whether a set of -#' standardized population parameters defines a valid population -#' covariance matrix and whether trivially misspecified parameters -#' remain within a user-defined smallest effect size of interest (SESOI). -#' Feasibility is evaluated by constructing implied population models -#' under targeted parameter perturbations and examining EPC behavior -#' using \code{\link{epcEquivFit}}. +#' Performs an EPC-based compensatory-effect diagnostic to assess whether +#' standardized population parameters define a valid population covariance +#' matrix and whether trivially misspecified parameters (relative to a +#' smallest effect size of interest; SESOI) can generate EPCs exceeding +#' the SESOI. #' -#' This function focuses on standardized parameters and supports +#' The compensatory effect is evaluated by constructing implied population +#' models under targeted standardized parameter perturbations and examining +#' resulting EPC behavior. If EPCs exceed the SESOI under perturbations that +#' are trivial in magnitude (e.g., 75% of the SESOI), substantial EPC +#' classifications may reflect inflation due to compensatory distortions +#' rather than genuine substantive misspecification. +#' +#' This function operates on standardized parameters and currently supports #' recursive SEMs with continuous indicators only. #' #' @param lavaanObj A fitted \code{lavaan} object representing the target model. @@ -274,7 +279,8 @@ miPowerFit <- function(...) { #' magnitude of the standardized perturbation to be evaluated. The #' default value of 0.75 indicates that perturbations equal to 75\% of #' the SESOI are treated as trivial. If EPCs exceed the SESOI under -#' such perturbations, EPC equivalence testing is not recommended. +#' such perturbations, the compensatory effect is classified as +#' \code{"PRONOUNCED"}. #' @param stdLoad Standardized factor loading used to define the SESOI #' for loading misspecifications. #' @param cor Standardized correlation used as a default SESOI for @@ -291,25 +297,29 @@ miPowerFit <- function(...) { #' the SESOI for structural misspecifications. #' #' @details -#' The procedure first checks whether the standardized parameters imply -#' a positive definite population covariance matrix. It then evaluates -#' EPC behavior under both positive and negative trivial -#' misspecifications by repeatedly constructing implied population -#' covariance matrices with perturbed parameters -#' (\code{minRelEffect} \eqn{\times} SESOI), refitting the model, and -#' re-evaluating EPCs. +#' The procedure first verifies whether the standardized parameter values +#' imply a positive definite population covariance matrix. It then evaluates +#' EPC behavior under both positive and negative trivial misspecifications +#' by repeatedly constructing implied population covariance matrices with +#' perturbed parameters (\code{minRelEffect} \eqn{\times} SESOI), refitting +#' the model, and re-evaluating EPC classifications. +#' +#' If at least one trivial perturbation produces an EPC exceeding the SESOI, +#' the compensatory effect is labeled \code{"PRONOUNCED"}. Otherwise, it is +#' labeled \code{"NOT PRONOUNCED"}. #' -#' Models with categorical indicators, formative indicators, or -#' multiple-group structures are not supported. +#' Models with categorical indicators, formative indicators, mean structures, +#' or multiple-group structures are not supported. #' #' @return An object of class \code{"epcEquivCheckStd"} containing: #' \itemize{ #' \item \code{feasible}: Logical indicator of whether a valid #' standardized population model exists. #' \item \code{any_M}: Logical indicator of whether any EPC exceeded -#' the SESOI under the evaluated misspecifications. -#' \item \code{recommendation}: Character string summarizing feasibility -#' (e.g., \code{"RECOMMENDED"}, \code{"NOT RECOMMENDED"}). +#' the SESOI under the evaluated perturbations. +#' \item \code{compensatory}: Character string summarizing the presence +#' of the compensatory effect (e.g., \code{"NOT PRONOUNCED"}, +#' \code{"PRONOUNCED"}, or \code{"NOT APPLICABLE"}). #' \item \code{M_table}: Data frame summarizing EPCs exceeding the SESOI, #' if any. #' \item \code{testeffect}: Data frame reporting the smallest tested @@ -321,7 +331,6 @@ miPowerFit <- function(...) { #' @seealso \code{\link{epcEquivFit}} #' #' @examples -#' #' library(lavaan) #' #' one.model <- ' onefactor =~ x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 ' @@ -345,7 +354,7 @@ epcEquivCheck <- function(lavaanObj, out <- list( feasible = FALSE, any_M = NA, - recommendation = "NOT APPLICABLE", + compensatory = "NOT APPLICABLE", reason = reason, M_table = NULL, testeffect = NULL @@ -657,18 +666,18 @@ epcEquivCheck <- function(lavaanObj, feasible <- TRUE any_M <- any(resultall == "M", na.rm = TRUE) - recommendation <- if (!feasible) { + compensatory <- if (!feasible) { "NOT APPLICABLE" } else if (any_M) { - "NOT RECOMMENDED" + "PRONOUNCED" } else { - "RECOMMENDED" + "NOT PRONOUNCED" } out <- list( feasible = feasible, any_M = any_M, - recommendation = recommendation, + compensatory = compensatory, M_table = M_all, testeffect = T_all ) @@ -1048,23 +1057,26 @@ extract_M_table <- function(result_mat, miout, sepc_mat, direction) { # print.epcEquivCheckStd() # ------------------------------------------------------------------ # Internal print method for epcEquivCheckStd objects. -# Formats and displays feasibility and recommendation results from +# Formats and displays feasibility and compensatory-effect results from # standardized-parameter EPC equivalence checks. #' @export print.epcEquivCheckStd <- function(x, ...) { - cat("EPC Equivalence Feasibility (Standardized Parameters)\n") + cat("EPC Equivalence Compensatory Effect (Standardized Parameters)\n") cat("----------------------------------------------------\n") cat("Feasible standardized population:", x$feasible, "\n") - cat("Any EPC exceeding SESOI:", x$any_M, "\n") - cat("Recommendation:", x$recommendation, "\n\n") - if (x$recommendation == "NOT RECOMMENDED") { - cat("Non-equivalent EPCs detected (summary):\n") + if(!x$feasible) { + cat("Any EPC exceeding SESOI:", x$any_M, "\n") + cat("Compensatory Effect:", x$compensatory, "\n\n") + return(invisible(x)) + } + if (x$compensatory == "PRONOUNCED") { + cat("EPCs exceeding SESOI under tested perturbations (summary):\n") print(x$M_table) - } else if (x$recommendation == "RECOMMENDED") { - cat("No EPC exceeded the SESOI under tested misspecifications.\n") + } else if (x$compensatory == "NOT PRONOUNCED") { + cat("No EPC exceeded the SESOI under tested perturbations.\n") } else { cat("Standardized parameters do not define a valid population model.\n") } From 7233a707f53a88bddd4e47864bc4472db01d262f Mon Sep 17 00:00:00 2001 From: Sunthud Pornprasertmanit Date: Sun, 15 Feb 2026 11:09:51 +0700 Subject: [PATCH 3/5] Regenerate documentation with roxygen --- semTools/man/epcEquivCheck.Rd | 55 ++++++++++++++++++++--------------- semTools/man/epcEquivFit.Rd | 20 ++++++++++++- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/semTools/man/epcEquivCheck.Rd b/semTools/man/epcEquivCheck.Rd index f5505aa..c615718 100644 --- a/semTools/man/epcEquivCheck.Rd +++ b/semTools/man/epcEquivCheck.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/miPowerFit.R \name{epcEquivCheck} \alias{epcEquivCheck} -\title{EPC Equivalence Feasibility Check for Standardized Parameters} +\title{EPC Equivalence Compensatory-Effect Check for Standardized Parameters} \usage{ epcEquivCheck(lavaanObj, minRelEffect = 0.75, stdLoad = 0.4, cor = 0.1, corLatent = NULL, corResidual = NULL, stdBeta = 0.1) @@ -14,7 +14,8 @@ epcEquivCheck(lavaanObj, minRelEffect = 0.75, stdLoad = 0.4, cor = 0.1, magnitude of the standardized perturbation to be evaluated. The default value of 0.75 indicates that perturbations equal to 75\\% of the SESOI are treated as trivial. If EPCs exceed the SESOI under -such perturbations, EPC equivalence testing is not recommended.} +such perturbations, the compensatory effect is classified as +\code{"PRONOUNCED"}.} \item{stdLoad}{Standardized factor loading used to define the SESOI for loading misspecifications.} @@ -41,9 +42,10 @@ An object of class \code{"epcEquivCheckStd"} containing: \item \code{feasible}: Logical indicator of whether a valid standardized population model exists. \item \code{any_M}: Logical indicator of whether any EPC exceeded -the SESOI under the evaluated misspecifications. -\item \code{recommendation}: Character string summarizing feasibility -(e.g., \code{"RECOMMENDED"}, \code{"NOT RECOMMENDED"}). +the SESOI under the evaluated perturbations. +\item \code{compensatory}: Character string summarizing the presence +of the compensatory effect (e.g., \code{"NOT PRONOUNCED"}, +\code{"PRONOUNCED"}, or \code{"NOT APPLICABLE"}). \item \code{M_table}: Data frame summarizing EPCs exceeding the SESOI, if any. \item \code{testeffect}: Data frame reporting the smallest tested @@ -51,31 +53,38 @@ standardized perturbations in each direction. } } \description{ -Performs an EPC-based feasibility check to assess whether a set of -standardized population parameters defines a valid population -covariance matrix and whether trivially misspecified parameters -remain within a user-defined smallest effect size of interest (SESOI). -Feasibility is evaluated by constructing implied population models -under targeted parameter perturbations and examining EPC behavior -using \code{\link{epcEquivFit}}. +Performs an EPC-based compensatory-effect diagnostic to assess whether +standardized population parameters define a valid population covariance +matrix and whether trivially misspecified parameters (relative to a +smallest effect size of interest; SESOI) can generate EPCs exceeding +the SESOI. } \details{ -This function focuses on standardized parameters and supports +The compensatory effect is evaluated by constructing implied population +models under targeted standardized parameter perturbations and examining +resulting EPC behavior. If EPCs exceed the SESOI under perturbations that +are trivial in magnitude (e.g., 75\% of the SESOI), substantial EPC +classifications may reflect inflation due to compensatory distortions +rather than genuine substantive misspecification. + +This function operates on standardized parameters and currently supports recursive SEMs with continuous indicators only. -The procedure first checks whether the standardized parameters imply -a positive definite population covariance matrix. It then evaluates -EPC behavior under both positive and negative trivial -misspecifications by repeatedly constructing implied population -covariance matrices with perturbed parameters -(\code{minRelEffect} \eqn{\times} SESOI), refitting the model, and -re-evaluating EPCs. +The procedure first verifies whether the standardized parameter values +imply a positive definite population covariance matrix. It then evaluates +EPC behavior under both positive and negative trivial misspecifications +by repeatedly constructing implied population covariance matrices with +perturbed parameters (\code{minRelEffect} \eqn{\times} SESOI), refitting +the model, and re-evaluating EPC classifications. + +If at least one trivial perturbation produces an EPC exceeding the SESOI, +the compensatory effect is labeled \code{"PRONOUNCED"}. Otherwise, it is +labeled \code{"NOT PRONOUNCED"}. -Models with categorical indicators, formative indicators, or -multiple-group structures are not supported. +Models with categorical indicators, formative indicators, mean structures, +or multiple-group structures are not supported. } \examples{ - library(lavaan) one.model <- ' onefactor =~ x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 ' diff --git a/semTools/man/epcEquivFit.Rd b/semTools/man/epcEquivFit.Rd index 56781d1..e16e1cd 100644 --- a/semTools/man/epcEquivFit.Rd +++ b/semTools/man/epcEquivFit.Rd @@ -8,7 +8,8 @@ \usage{ epcEquivFit(lavaanObj, stdLoad = 0.4, cor = 0.1, corLatent = NULL, corResidual = NULL, stdBeta = 0.1, stdIntcept = 0.2, stdSesoi = NULL, - sesoi = NULL, cilevel = 0.9, ...) + sesoi = NULL, cilevel = 0.9, mialpha = 0.05, mipower = 0.8, + adjust.method = "none", ...) \method{summary}{epcequivfit.data.frame}(object, ..., top = 5, ssv = FALSE) } @@ -46,6 +47,19 @@ arguments.} \item{cilevel}{Confidence level for EPC confidence intervals used in CI-based equivalence testing.} +\item{mialpha}{Significance level used for evaluating modification +indices in the power-based decision rule (Method 1). Default is 0.05.} + +\item{mipower}{Desired statistical power for detecting a misspecification +of the specified SESOI in the power-based decision rule. Default is 0.80.} + +\item{adjust.method}{Multiplicity adjustment method applied to both +modification index tests and EPC confidence intervals. Currently +supported options are \code{"none"} (no adjustment) and +\code{"bonferroni"}. When \code{"bonferroni"} is used, the MI +significance level and CI level are adjusted across all evaluated +fixed parameters.} + \item{\dots}{Additional arguments passed to \code{\link[lavaan]{modificationIndices}}.} @@ -114,6 +128,10 @@ to traditional exact-fit evaluation. It is designed to assess whether fixed parameters are substantively misspecified relative to a SESOI, rather than whether a model fits exactly. +When \code{adjust.method = "bonferroni"}, familywise error control +is applied across all evaluated fixed parameters by adjusting the +MI significance threshold and widening EPC confidence intervals. + Models with categorical indicators or unsupported constraints may not be fully supported. } From ca72e46c8e54c6fb4783b79fc67d9cb27b37bae7 Mon Sep 17 00:00:00 2001 From: Sunthud Pornprasertmanit Date: Sun, 15 Feb 2026 13:23:39 +0700 Subject: [PATCH 4/5] Fix logic error in print.epcEquivCheck() --- semTools/R/miPowerFit.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/semTools/R/miPowerFit.R b/semTools/R/miPowerFit.R index 4f8501d..6ce466e 100644 --- a/semTools/R/miPowerFit.R +++ b/semTools/R/miPowerFit.R @@ -1067,11 +1067,11 @@ print.epcEquivCheckStd <- function(x, ...) { cat("Feasible standardized population:", x$feasible, "\n") - if(!x$feasible) { + if(x$feasible) { cat("Any EPC exceeding SESOI:", x$any_M, "\n") cat("Compensatory Effect:", x$compensatory, "\n\n") - return(invisible(x)) } + if (x$compensatory == "PRONOUNCED") { cat("EPCs exceeding SESOI under tested perturbations (summary):\n") print(x$M_table) From b3c05fde61ea3c0e286296b29a8d90cfb5c1dfa9 Mon Sep 17 00:00:00 2001 From: Sunthud Pornprasertmanit Date: Sun, 15 Feb 2026 21:44:37 +0700 Subject: [PATCH 5/5] Add underpowered parameter list to summary.epcEquivFit() output --- semTools/R/miPowerFit.R | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/semTools/R/miPowerFit.R b/semTools/R/miPowerFit.R index 6ce466e..495d7cb 100644 --- a/semTools/R/miPowerFit.R +++ b/semTools/R/miPowerFit.R @@ -1120,7 +1120,7 @@ summary.epcequivfit.data.frame <- function(object, ..., top = 5, ssv = FALSE) { # ---- Derived quantities ---- miout$severity <- abs(miout$std.epc / miout$std.sesoi) - + miout$ci_width <- with(miout, abs(upper.std.epc - lower.std.epc)) miout$ci_gap <- with(miout, pmin( abs(lower.std.epc - std.sesoi), abs(upper.std.epc - std.sesoi), @@ -1150,6 +1150,12 @@ summary.epcequivfit.data.frame <- function(object, ..., top = 5, ssv = FALSE) { top_I_ci <- top_I_ci[order(top_I_ci$ci_gap, decreasing = TRUE), ] if (nrow(top_I_ci) > top) top_I_ci <- top_I_ci[1:top, ] + # CI-underpowered EPCs (exclude NM) + top_U_ci <- miout[miout$decision.ci == "U", ] + top_U_ci <- top_U_ci[is.finite(top_U_ci$ci_gap), ] + top_U_ci <- top_U_ci[order(top_U_ci$ci_width, decreasing = TRUE), ] + if (nrow(top_U_ci) > top) top_U_ci <- top_U_ci[1:top, ] + # ---- SSV / power-based diagnostics (secondary) ---- ssv_out <- list( n_M = sum(miout$decision.pow %in% c("M", "EPC:M"), na.rm = TRUE), @@ -1175,6 +1181,7 @@ summary.epcequivfit.data.frame <- function(object, ..., top = 5, ssv = FALSE) { ssv = ssv_out, top_non_equiv = top_M, top_inconclusive_ci = top_I_ci, + top_underpowered_ci = top_U_ci, top_non_pow = top_M_pow, top_inconclusive_pow = top_I_pow, show_ssv = ssv @@ -1265,6 +1272,18 @@ print.summaryEpcEquivFit <- function(x, ...) { cat("\n") } + if (x$epc_equivalence$n_U > 0) { + cat("1.3 Top CI-underpowered EPCs\n") + cat("(ranked by CI width):\n") + print( + x$top_underpowered_ci[ + , c("lhs","op","rhs","lower.std.epc","upper.std.epc","ci_width"), + drop = FALSE + ] + ) + cat("\n") + } + if(isTRUE(x$show_ssv)) { ## ---- SSV / power-based diagnostics (secondary) ---- cat("[2. Saris, Satorra, Van der Veld (2009) / Power-based Diagnostics]\n")