diff --git a/NEWS b/NEWS index efd55bfb..0f82c90b 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,8 @@ WEBSITE: https://r-ega.net + ADD: a general function called `information` to compute several information theory measures ++ UPDATE: default 'loading.method' for `net.loads` has been changed to "revised" moving forward -- the previous default in versions <= 2.0.6 can be obtained using "original" + + UPDATE: `invariance` handles more than 2 groups (plots up to 4 groups pairwise) + UPDATE: added 'signed' argument in `jsd` to allow for signed or absolute networks to be used in computations (includes downstream functions: `infoCluster`) diff --git a/R/hierEGA.R b/R/hierEGA.R index 7902ead6..1cfcfe8b 100644 --- a/R/hierEGA.R +++ b/R/hierEGA.R @@ -12,9 +12,9 @@ #' #' @param loading.method Character (length = 1). #' Sets network loading calculation based on implementation -#' described in \code{"BRM"} (Christensen & Golino, 2021) or -#' an \code{"experimental"} implementation (Christensen et al., 2024). -#' Defaults to \code{"experimental"} +#' described in \code{"original"} (Christensen & Golino, 2021) or +#' the \code{"revised"} (Christensen et al., 2024) implementation. +#' Defaults to \code{"revised"} #' #' @param rotation Character. #' A rotation to use to obtain a simpler structure. @@ -277,11 +277,11 @@ #' @export #' # Hierarchical EGA ---- -# Updated 22.07.2024 +# Updated 12.08.2024 hierEGA <- function( data, # `net.scores` arguments - loading.method = c("BRM", "experimental"), + loading.method = c("original", "revised"), rotation = NULL, scores = c("factor", "network"), loading.structure = c("simple", "full"), impute = c("mean", "median", "none"), @@ -308,7 +308,8 @@ hierEGA <- function( # Check for missing arguments (argument, default, function) ## `net.scores` - loading.method <- set_default(loading.method, "experimental", net.loads) + # loading.method <- set_default(loading.method, "experimental", net.loads) + # Push default check to `net.laods` scores <- set_default(scores, "network", hierEGA) loading.structure <- set_default(loading.structure, "simple", hierEGA) impute <- set_default(impute, "none", net.scores) diff --git a/R/net.loads.R b/R/net.loads.R index 3132787f..303f56f2 100644 --- a/R/net.loads.R +++ b/R/net.loads.R @@ -12,9 +12,9 @@ #' #' @param loading.method Character (length = 1). #' Sets network loading calculation based on implementation -#' described in \code{"BRM"} (Christensen & Golino, 2021) or -#' an \code{"experimental"} implementation. -#' Defaults to \code{"BRM"} +#' described in \code{"original"} (Christensen & Golino, 2021) or +#' the \code{"revised"} (Christensen et al., 2024) implementation. +#' Defaults to \code{"revised"} #' #' @param scaling Numeric (length = 1). #' Scaling factor for the magnitude of the \code{"experimental"} network loadings. @@ -76,22 +76,40 @@ #' Problems with centrality measures in psychopathology symptom networks: Why network psychometrics cannot escape psychometric theory. #' \emph{Multivariate Behavioral Research}, 1-25. #' +#' \strong{Revised network loadings} \cr +#' Christensen, A. P., Golino, H., Abad, F. J., & Garrido, L. E. (2024). +#' Revised network loadings. +#' \emph{PsyArXiv}. +#' #' @author Alexander P. Christensen and Hudson Golino #' #' @export #' # Network Loadings ---- -# Updated 06.04.2024 -# Default = "BRM" or `net.loads` from version 1.2.3 -# Experimental = new signs and cross-loading adjustment +# Updated 12.08.2024 net.loads <- function( - A, wc, loading.method = c("BRM", "experimental"), + A, wc, loading.method = c("original", "revised"), scaling = 2, rotation = NULL, ... ) { - # Check for missing arguments (argument, default, function) - loading.method <- set_default(loading.method, "brm", net.loads) + # Check for no input in 'loading.method' + if(length(loading.method) > 1){ + loading.method <- "revised" + }else{ + + # Switch out old calls + loading.method <- switch( + tolower(loading.method), + "brm" = "original", + "experimental" = "revised", + loading.method + ) + + # Check for missing arguments (argument, default, function) + loading.method <- set_default(loading.method, "revised", net.loads) + + } # Organize and extract input (handles argument errors) # `wc` is made to be a character vector to allow `NA` @@ -138,7 +156,17 @@ net.loads <- function( # Not all singleton dimensions, so carry on # Check for method - if(loading.method == "brm"){ + if(loading.method == "revised"){ + + # Revised unstandardized loadings + unstandardized <- revised_loadings( + A, wc, nodes, node_names, communities, unique_communities + ) + + + + + }else{ # Compute unstandardized loadings (absolute sums) unstandardized <- absolute_weights(A, wc, nodes, unique_communities) @@ -147,21 +175,6 @@ net.loads <- function( unstandardized <- old_add_signs(unstandardized, A, wc, unique_communities) - }else{ # If not "BRM", run experimental - - # Send experimental message (for now) - experimental("net.loads") - - # Experimental unstandardized loadings - # Differences: - # 1. signs are added in a more accurate way - # 2. algebraic rather than absolute sums are used - # 3. within-community sums are computed using (sums / (n - 1)) * n - # 4. standardization uses (abs(x) / (abs(x) + 1)) %*% diag(sqrt(eigenvalues)) - unstandardized <- experimental_loadings( - A, wc, nodes, node_names, communities, unique_communities - ) - } # Obtain standardized loadings @@ -253,6 +266,16 @@ net.loads <- function( # Set class class(results) <- "net.loads" + # Send message about changed defaults + if(loading.method == "revised"){ + message( + paste( + "The default 'loading.method' has changed to \"revised\" in {EGAnet} version >= 2.0.7.\n\n", + "For the previous default (version <= 2.0.6), use `loading.method = \"original\"`" + ) + ) + } + # Return results return(results) @@ -284,12 +307,12 @@ net.loads <- function( # # Estimate EGA # ega <- EGA(sim_data$data, plot.EGA = FALSE) # ega$wc[8] <- NA -# A = ega; loading.method = "brm" +# A = ega; loading.method = "revised" # rotation = "geominq" #' @exportS3Method # S3 Print Method ---- -# Updated 08.10.2023 +# Updated 12.08.2024 print.net.loads <- function(x, ...) { @@ -303,8 +326,8 @@ print.net.loads <- function(x, ...) cat( paste0( "Loading Method: ", swiftelse( - method_attributes$loading.method == "brm", - "BRM", "Experimental" + method_attributes$loading.method == "revised", + "Revised", "Original" ) ) ) @@ -458,9 +481,9 @@ obtain_signs <- function(target_network) } #' @noRd -# Experimental loadings ---- -# Updated 22.03.2024 -experimental_loadings <- function( +# Revised loadings ---- +# Updated 12.08.2024 +revised_loadings <- function( A, wc, nodes, node_names, communities, unique_communities ) @@ -567,14 +590,12 @@ experimental_loadings <- function( #' @noRd # Standardize loadings ---- -# Updated 06.04.2024 +# Updated 12.08.2024 standardize <- function(unstandardized, loading.method, A, wc, scaling) { # Check for loading method - if(loading.method == "brm"){ - return(t(t(unstandardized) / sqrt(colSums(abs(unstandardized), na.rm = TRUE)))) - }else if(loading.method == "experimental"){ + if(loading.method == "revised"){ # Get attributes community <- attr(unstandardized, "community") @@ -584,6 +605,8 @@ standardize <- function(unstandardized, loading.method, A, wc, scaling) t(t(unstandardized) / (community$community_sums^(1 / log(scaling * community$community_table)))) ) + }else if(loading.method == "original"){ + return(t(t(unstandardized) / sqrt(colSums(abs(unstandardized), na.rm = TRUE)))) } } @@ -695,9 +718,9 @@ rotation_defaults <- function(rotation, rotation_ARGS, ellipse) } -#%%%%%%%%%%%%%%%%% -# BRM Legacy ---- -#%%%%%%%%%%%%%%%%% +#%%%%%%%%%%%%%%%%%%%%% +# Original Legacy ---- +#%%%%%%%%%%%%%%%%%%%%% #' @noRd ## Absolute weights ("BRM") ---- diff --git a/R/net.scores.R b/R/net.scores.R index 312592cb..7afaaeb0 100644 --- a/R/net.scores.R +++ b/R/net.scores.R @@ -1,14 +1,14 @@ #' @title Network Scores #' #' @description This function computes network scores computed based on -#' each node's \code{strength} within each community in the network -#' (see \code{\link[EGAnet]{net.loads}}). These values are used as "network loadings" +#' each node's \code{strength} within each community in the network +#' (see \code{\link[EGAnet]{net.loads}}). These values are used as "network loadings" #' for the weights of each variable. -#' +#' #' Network scores are computed as a formative composite rather than a reflective factor. #' This composite representation is consistent with no latent factors that psychometric #' network theory proposes. -#' +#' #' Scores can be computed as a "simple" structure, which is equivalent to a weighted #' sum scores or as a "full" structure, which is equivalent to an EFA approach. #' Conservatively, the "simple" structure approach is recommended until further @@ -26,38 +26,38 @@ #' #' @param loading.method Character (length = 1). #' Sets network loading calculation based on implementation -#' described in \code{"BRM"} (Christensen & Golino, 2021) or -#' an \code{"experimental"} implementation. -#' Defaults to \code{"BRM"} -#' +#' described in \code{"original"} (Christensen & Golino, 2021) or +#' the \code{"revised"} (Christensen et al., 2024) implementation. +#' Defaults to \code{"revised"} +#' #' @param rotation Character. -#' A rotation to use to obtain a simpler structure. +#' A rotation to use to obtain a simpler structure. #' For a list of rotations, see \code{\link[GPArotation]{rotations}} for options. #' Defaults to \code{NULL} or no rotation. #' By setting a rotation, \code{scores} estimation will be #' based on the rotated loadings rather than unrotated loadings -#' +#' #' @param scores Character (length = 1). #' How should scores be estimated? #' Defaults to \code{"network"} for network scores. #' Set to other scoring methods which will be computed using #' \code{\link[psych]{factor.scores}} (see link for arguments #' and explanations for other methods) -#' +#' #' @param loading.structure Character (length = 1). #' Whether simple structure or the saturated loading matrix #' should be used when computing scores. #' Defaults to \code{"simple"} -#' -#' \code{"simple"} structure more closely mirrors sum scores and CFA; +#' +#' \code{"simple"} structure more closely mirrors sum scores and CFA; #' \code{"full"} structure more closely mirrors EFA -#' +#' #' Simple structure is the more "conservative" (established) approach #' and is therefore the default. Treat \code{"full"} as experimental #' as proper vetting and validation has not been established #' #' @param impute Character (length = 1). -#' If there are any missing data, then imputation can be implemented. +#' If there are any missing data, then imputation can be implemented. #' Available options: #' #' \itemize{ @@ -71,8 +71,8 @@ #' for that variable #' #' } -#' -#' @param ... Additional arguments to be passed on to +#' +#' @param ... Additional arguments to be passed on to #' \code{\link[EGAnet]{net.loads}} and #' \code{\link[psych]{factor.scores}} #' @@ -81,7 +81,7 @@ #' \item{scores}{A list containing the standardized (\code{std.scores}) #' rotated (\code{rot.scores}) scores. If \code{rotation = NULL}, then #' \code{rot.scores} will be \code{NULL}} -#' +#' #' \item{loadings}{Output from \code{\link[EGAnet]{net.loads}}} #' #' @examples @@ -102,21 +102,26 @@ #' Christensen, A. P., & Golino, H. (2021). #' On the equivalency of factor and network loadings. #' \emph{Behavior Research Methods}, \emph{53}, 1563-1580. -#' +#' #' \strong{Preliminary simulation for scores} \cr #' Golino, H., Christensen, A. P., Moulder, R., Kim, S., & Boker, S. M. (2021). #' Modeling latent topics in social media using Dynamic Exploratory Graph Analysis: The case of the right-wing and left-wing trolls in the 2016 US elections. #' \emph{Psychometrika}. #' +#' \strong{Revised network loadings} \cr +#' Christensen, A. P., Golino, H., Abad, F. J., & Garrido, L. E. (2024). +#' Revised network loadings. +#' \emph{PsyArXiv}. +#' #' @author Alexander P. Christensen and Hudson F. Golino #' #' @export #' # Network Scores ---- -# Updated 24.10.2023 +# Updated 12.08.2024 net.scores <- function ( - data, A, wc, - loading.method = c("BRM", "experimental"), + data, A, wc, + loading.method = c("original", "revised"), rotation = NULL, scores = c( "Anderson", "Bartlett", "components", @@ -127,16 +132,16 @@ net.scores <- function ( ... ) { - + # All argument errors are handled here or in `net.loads` - + # Error on: # 1. missing data # 2. symmetric matrix input as data if(missing(data)){ # Check for missing data .handleSimpleError( h = stop, - msg = "Input for 'data' is missing. To compute scores, the original data are necessary", + msg = "Input for 'data' is missing. To compute scores, the original data are necessary", call = "net.scores" ) }else if(is_symmetric(data)){ # Check for symmetric matrix @@ -146,35 +151,36 @@ net.scores <- function ( call = "net.scores" ) } - + # Ensure data is a matrix - data <- as.matrix(usable_data(data, verbose = TRUE)) - + data <- as.matrix(usable_data(data, verbose = TRUE)) + # Get ellipse arguments ellipse <- list(...) - + # Check for defunct "method" if("method" %in% names(ellipse)){ scores <- ellipse$method } - + # Check for missing arguments (argument, default, function) - loading.method <- set_default(loading.method, "brm", net.loads) + # loading.method <- set_default(loading.method, "brm", net.loads) + # Push default check to `net.laods` scores <- set_default(scores, "network", net.scores) loading.structure <- set_default(loading.structure, "simple", net.scores) impute <- set_default(impute, "none", net.scores) - + # Perform imputation if(impute != "none"){ data <- imputation(data, impute) } - + # Compute network loadings (will handle `EGA` objects) loadings <- net.loads( A = A, wc = wc, loading.method = loading.method, rotation = rotation, ... ) - + # Return results return( list( @@ -184,7 +190,7 @@ net.scores <- function ( loadings = loadings ) ) - + } #' @noRd @@ -192,31 +198,31 @@ net.scores <- function ( # Updated 04.08.2023 imputation <- function(data, impute) { - + # Get imputation function impute_values <- switch( impute, "mean" = colMeans(data, na.rm = TRUE), "median" = nvapply(as.data.frame(data), median, na.rm = TRUE) ) - + # Get missing data missing_data <- which(is.na(data), arr.ind = TRUE) - + # Loop over unique columns for(column in unique(missing_data[,"col"])){ - + # Obtain target rows target_rows <- missing_data[,"row"][missing_data[,"col"] == column] - + # Populate data data[target_rows, column] <- impute_values[column] - + } - + # Return data return(data) - + } #' @noRd @@ -224,32 +230,32 @@ imputation <- function(data, impute) # Consistent with hierarchical CFA # Updated 28.07.2023 zero_out <- function(loadings, wc, loading.structure){ - + # Check for loading structure if(loading.structure == "simple"){ - + # Get names of memberships wc_names <- names(wc) - + # Get loadings names loadings_names <- dimnames(loadings)[[2]] - + # Loop over unique memberships for(membership in unique(wc)){ - + # Set cross-loadings to zero loadings[ wc_names[wc == membership], loadings_names != membership ] <- 0 - + } - + } - + # Return loadings return(loadings) - + } #' @noRd @@ -259,10 +265,10 @@ compute_scores <- function( loadings, data, scores, loading.structure ) { - + # Method must exist, so continue if(scores == "network"){ - + # Compute unrotated scores unrotated <- network_scores( data = data, @@ -271,7 +277,7 @@ compute_scores <- function( loading.structure ) ) - + # Compute rotated scores (if available) if(!is.null(loadings$rotated)){ rotated <- network_scores( @@ -283,27 +289,27 @@ compute_scores <- function( ) ) }else{rotated <- NULL} - + }else{ - + # Switch lowercase method back to appropriate case scores <- switch( scores, - "anderson" = "Anderson", + "anderson" = "Anderson", "bartlett" = "Bartlett", "components" = "components", - "harman" = "Harman", + "harman" = "Harman", "tenberge" = "tenBerge", "thurstone" = "Thurstone" ) - + # Compute unrotated scores unrotated <- psych::factor.scores( x = data, f = loadings$std, method = scores )$scores - + # Compute rotated scores (if available) if(!is.null(loadings$rotated)){ rotated <- psych::factor.scores( @@ -313,9 +319,9 @@ compute_scores <- function( method = scores )$scores }else{rotated <- NULL} - + } - + # Return scores return( list( @@ -323,7 +329,7 @@ compute_scores <- function( rot.scores = rotated ) ) - + } #' @noRd diff --git a/man/hierEGA.Rd b/man/hierEGA.Rd index 7958a1a5..e7c598af 100644 --- a/man/hierEGA.Rd +++ b/man/hierEGA.Rd @@ -6,7 +6,7 @@ \usage{ hierEGA( data, - loading.method = c("BRM", "experimental"), + loading.method = c("original", "revised"), rotation = NULL, scores = c("factor", "network"), loading.structure = c("simple", "full"), @@ -29,9 +29,9 @@ Should consist only of variables to be used in the analysis \item{loading.method}{Character (length = 1). Sets network loading calculation based on implementation -described in \code{"BRM"} (Christensen & Golino, 2021) or -an \code{"experimental"} implementation (Christensen et al., 2024). -Defaults to \code{"experimental"}} +described in \code{"original"} (Christensen & Golino, 2021) or +the \code{"revised"} (Christensen et al., 2024) implementation. +Defaults to \code{"revised"}} \item{rotation}{Character. A rotation to use to obtain a simpler structure. diff --git a/man/net.loads.Rd b/man/net.loads.Rd index e04db902..829c9cff 100644 --- a/man/net.loads.Rd +++ b/man/net.loads.Rd @@ -7,7 +7,7 @@ net.loads( A, wc, - loading.method = c("BRM", "experimental"), + loading.method = c("original", "revised"), scaling = 2, rotation = NULL, ... @@ -23,9 +23,9 @@ then \code{wc} is automatically detected} \item{loading.method}{Character (length = 1). Sets network loading calculation based on implementation -described in \code{"BRM"} (Christensen & Golino, 2021) or -an \code{"experimental"} implementation. -Defaults to \code{"BRM"}} +described in \code{"original"} (Christensen & Golino, 2021) or +the \code{"revised"} (Christensen et al., 2024) implementation. +Defaults to \code{"revised"}} \item{scaling}{Numeric (length = 1). Scaling factor for the magnitude of the \code{"experimental"} network loadings. @@ -93,6 +93,11 @@ On the equivalency of factor and network loadings. Hallquist, M., Wright, A. C. G., & Molenaar, P. C. M. (2019). Problems with centrality measures in psychopathology symptom networks: Why network psychometrics cannot escape psychometric theory. \emph{Multivariate Behavioral Research}, 1-25. + +\strong{Revised network loadings} \cr +Christensen, A. P., Golino, H., Abad, F. J., & Garrido, L. E. (2024). +Revised network loadings. +\emph{PsyArXiv}. } \author{ Alexander P. Christensen and Hudson Golino diff --git a/man/net.scores.Rd b/man/net.scores.Rd index 5e064313..17edea3c 100644 --- a/man/net.scores.Rd +++ b/man/net.scores.Rd @@ -8,7 +8,7 @@ net.scores( data, A, wc, - loading.method = c("BRM", "experimental"), + loading.method = c("original", "revised"), rotation = NULL, scores = c("Anderson", "Bartlett", "components", "Harman", "network", "tenBerge", "Thurstone"), @@ -30,12 +30,12 @@ then \code{wc} is automatically detected} \item{loading.method}{Character (length = 1). Sets network loading calculation based on implementation -described in \code{"BRM"} (Christensen & Golino, 2021) or -an \code{"experimental"} implementation. -Defaults to \code{"BRM"}} +described in \code{"original"} (Christensen & Golino, 2021) or +the \code{"revised"} (Christensen et al., 2024) implementation. +Defaults to \code{"revised"}} \item{rotation}{Character. -A rotation to use to obtain a simpler structure. +A rotation to use to obtain a simpler structure. For a list of rotations, see \code{\link[GPArotation]{rotations}} for options. Defaults to \code{NULL} or no rotation. By setting a rotation, \code{scores} estimation will be @@ -53,7 +53,7 @@ Whether simple structure or the saturated loading matrix should be used when computing scores. Defaults to \code{"simple"} -\code{"simple"} structure more closely mirrors sum scores and CFA; +\code{"simple"} structure more closely mirrors sum scores and CFA; \code{"full"} structure more closely mirrors EFA Simple structure is the more "conservative" (established) approach @@ -61,7 +61,7 @@ and is therefore the default. Treat \code{"full"} as experimental as proper vetting and validation has not been established} \item{impute}{Character (length = 1). -If there are any missing data, then imputation can be implemented. +If there are any missing data, then imputation can be implemented. Available options: \itemize{ @@ -76,7 +76,7 @@ for that variable }} -\item{...}{Additional arguments to be passed on to +\item{...}{Additional arguments to be passed on to \code{\link[EGAnet]{net.loads}} and \code{\link[psych]{factor.scores}}} } @@ -91,8 +91,8 @@ rotated (\code{rot.scores}) scores. If \code{rotation = NULL}, then } \description{ This function computes network scores computed based on -each node's \code{strength} within each community in the network -(see \code{\link[EGAnet]{net.loads}}). These values are used as "network loadings" +each node's \code{strength} within each community in the network +(see \code{\link[EGAnet]{net.loads}}). These values are used as "network loadings" for the weights of each variable. Network scores are computed as a formative composite rather than a reflective factor. @@ -128,6 +128,11 @@ On the equivalency of factor and network loadings. Golino, H., Christensen, A. P., Moulder, R., Kim, S., & Boker, S. M. (2021). Modeling latent topics in social media using Dynamic Exploratory Graph Analysis: The case of the right-wing and left-wing trolls in the 2016 US elections. \emph{Psychometrika}. + +\strong{Revised network loadings} \cr +Christensen, A. P., Golino, H., Abad, F. J., & Garrido, L. E. (2024). +Revised network loadings. +\emph{PsyArXiv}. } \author{ Alexander P. Christensen and Hudson F. Golino