Skip to content

Commit

Permalink
Factor analysis minor improvements (#877)
Browse files Browse the repository at this point in the history
* Revert hotfix and fail informatively #268

* minor

* add max_per_dimension arg to efa_to_cfa

* comments

* news, version

* minor

---------

Co-authored-by: Daniel <[email protected]>
  • Loading branch information
DominiqueMakowski and strengejacke authored Sep 11, 2023
1 parent 3415c02 commit 90ce679
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 25 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Type: Package
Package: parameters
Title: Processing of Model Parameters
Version: 0.21.1.7
Version: 0.21.1.8
Authors@R:
c(person(given = "Daniel",
family = "Lüdecke",
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Changes

* Minor improvements to factor analysis functions.

* The `ci_digits` argument of the `print()` method for `model_parameters()` now
defaults to the same value of `digits`.

Expand Down
6 changes: 6 additions & 0 deletions R/cluster_analysis.R
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ visualisation_recipe.cluster_analysis_summary <- function(x, ...) {
#' @export
visualisation_recipe.cluster_analysis <- function(x, show_data = "text", ...) {
ori_data <- stats::na.omit(attributes(x)$data)

# Check number of columns: if more than 2, display PCs, if less, fail
if (ncol(ori_data) <= 2) {
insight::format_error("Less than 2 variables in the dataset. Cannot compute enough principal components to represent clustering.")
}

# Get 2 PCA Components
pca <- principal_components(ori_data, n = 2)
data <- stats::predict(pca)
Expand Down
47 changes: 36 additions & 11 deletions R/convert_efa_to_cfa.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
#' Confirmatory Factor Analysis (CFA) `lavaan`-ready structure.
#'
#' @param model An EFA model (e.g., a `psych::fa` object).
#' @inheritParams principal_components
#' @param names Vector containing dimension names.
#' @param max_per_dimension Maximum number of variables to keep per dimension.
#' @inheritParams principal_components
#'
#' @examples
#' \donttest{
Expand All @@ -15,10 +16,12 @@
#'
#' model1 <- efa_to_cfa(efa)
#' model2 <- efa_to_cfa(efa, threshold = 0.3)
#' model3 <- efa_to_cfa(efa, max_per_dimension = 2)
#'
#' suppressWarnings(anova(
#' lavaan::cfa(model1, data = attitude),
#' lavaan::cfa(model2, data = attitude)
#' lavaan::cfa(model2, data = attitude),
#' lavaan::cfa(model3, data = attitude)
#' ))
#' }
#' }
Expand All @@ -33,8 +36,15 @@ convert_efa_to_cfa <- function(model, ...) {
#' @rdname convert_efa_to_cfa
#' @inheritParams model_parameters.principal
#' @export
convert_efa_to_cfa.fa <- function(model, threshold = "max", names = NULL, ...) {
.efa_to_cfa(model_parameters(model, threshold = threshold, ...), names = names, ...)
convert_efa_to_cfa.fa <- function(model,
threshold = "max",
names = NULL,
max_per_dimension = NULL,
...) {
.efa_to_cfa(model_parameters(model, threshold = threshold, ...),
names = names,
max_per_dimension = max_per_dimension,
...)
}

#' @export
Expand All @@ -46,12 +56,13 @@ convert_efa_to_cfa.fa.ci <- convert_efa_to_cfa.fa
convert_efa_to_cfa.parameters_efa <- function(model,
threshold = NULL,
names = NULL,
max_per_dimension = NULL,
...) {
if (!is.null(threshold)) {
model <- model_parameters(attributes(model)$model, threshold = threshold, ...)
}

.efa_to_cfa(model, names = names, ...)
.efa_to_cfa(model, names = names, max_per_dimension = max_per_dimension, ...)
}

#' @export
Expand All @@ -67,7 +78,7 @@ efa_to_cfa <- convert_efa_to_cfa


#' @keywords internal
.efa_to_cfa <- function(loadings, names = NULL, ...) {
.efa_to_cfa <- function(loadings, names = NULL, max_per_dimension=NULL, ...) {
loadings <- attributes(loadings)$loadings_long

# Get dimension names
Expand All @@ -78,17 +89,31 @@ efa_to_cfa <- convert_efa_to_cfa
# Catch error
if (length(names) != insight::n_unique(loadings$Component)) {
insight::format_error(
paste0("The `names` vector must be of same length as the number of dimensions, in this case ", length(unique(loadings$Component)), ".")
paste0(
"The `names` vector must be of same length as the number of dimensions, in this case ",
length(unique(loadings$Component)),
"."
)
)
}

cfa <- NULL
# Iterate over dimensions
for (i in seq_along(names)) {
cfa <- c(
cfa,
paste0(names[i], " =~ ", paste(as.character(loadings[loadings$Component == unique(loadings$Component)[i], "Variable"]), collapse = " + "))
)

# Find correct subset
items <- loadings[loadings$Component == unique(loadings$Component)[i], ]

# Find corresponding items
items <- as.character(loadings[loadings$Component == unique(loadings$Component)[i], "Variable"])

# Subset if need be to keep only a certain number
if (!is.null(max_per_dimension) && max_per_dimension > 0) {
items <- as.character(stats::na.omit(items[1:max_per_dimension]))
}

# Append that list
cfa <- c(cfa, paste0(names[i], " =~ ", paste(items, collapse = " + ")))
}

cfa <- paste0(cfa, collapse = "\n")
Expand Down
2 changes: 1 addition & 1 deletion R/factor_analysis.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ factor_analysis.data.frame <- function(x,
...) {
# Standardize
if (standardize && is.null(cor)) {
x <- as.data.frame(scale(x))
x <- datawizard::standardize(x, ...)
}

# N factors
Expand Down
48 changes: 42 additions & 6 deletions R/n_factors.R
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,23 @@ n_factors <- function(x,
package <- package[!package %in% c("pcdimension", "PCDimension")]
} else if (is.matrix(x) || inherits(x, "easycormatrix")) {
insight::format_error(
"Please input the correlation matrix via the `cor = ...` argument and the number of rows / observations via the first argument."
"Please input the correlation matrix via the `cor` argument and the number of rows / observations via the first argument."
)
}
} else {
nobs <- nrow(x)
}

# Get only numeric
x <- x[vapply(x, is.numeric, TRUE)]
numerics <- vapply(x, is.numeric, TRUE)
if (!all(numerics)) {
insight::format_warning(paste0(
"Some variables are not numeric (",
toString(names(x)[!numerics]),
"). Dropping them."
))
}
x <- x[numerics]

# Correlation matrix
if (is.null(cor)) {
Expand Down Expand Up @@ -247,7 +255,7 @@ n_factors <- function(x,
out <- rbind(
out,
tryCatch(.n_factors_ega(x, cor, nobs, eigen_values, type),
warning = function(w) data.frame(),
# warning = function(w) data.frame(),
error = function(e) data.frame()
)
)
Expand All @@ -268,7 +276,7 @@ n_factors <- function(x,
out <- rbind(
out,
tryCatch(.n_factors_vss(x, cor, nobs, type, rotation, algorithm),
warning = function(w) data.frame(),
# warning = function(w) data.frame(),
error = function(e) data.frame()
)
)
Expand Down Expand Up @@ -300,7 +308,7 @@ n_factors <- function(x,
}
}

# fit -------------------------------------------
# pcdimension -------------------------------------------
if ("pcdimension" %in% tolower(package)) {
insight::check_if_installed("PCDimension")

Expand Down Expand Up @@ -564,6 +572,32 @@ print.n_clusters <- print.n_factors

# psych ------------------------

#' @keywords internal
.n_factors_parallel <- function(x = NULL,
cor = NULL,
nobs = NULL,
type = "FA") {

# Altnerative version of parralel analysis
# Not used because already included in nFactors

if (tolower(type) %in% c("fa", "factor", "efa")) {
fa <- "fa"
} else {
fa <- "pc"
}

insight::check_if_installed("psych")
out <- psych::fa.parallel(cor, n.obs = nobs, fa = fa, plot = FALSE, fm = "ml")


.data_frame(
n_Factors = as.numeric(stats::na.omit(c(out$nfact, out$ncomp))),
Method = "Parallel",
Family = "psych"
)
}

#' @keywords internal
.n_factors_vss <- function(x = NULL,
cor = NULL,
Expand All @@ -580,6 +614,7 @@ print.n_clusters <- print.n_factors
}


insight::check_if_installed("psych")
# Compute VSS
vss <- psych::VSS(
cor,
Expand Down Expand Up @@ -631,6 +666,7 @@ print.n_clusters <- print.n_factors
}
}

insight::check_if_installed("psych")
rez <- data.frame()
for (n in 1:(ncol(cor) - 1)) {
if (tolower(type) %in% c("fa", "factor", "efa")) {
Expand Down Expand Up @@ -738,7 +774,7 @@ print.n_clusters <- print.n_factors
.n_factors_PCDimension <- function(x = NULL, type = "PCA") {
# This package is a strict dependency of PCDimension so if users have the
# former they should have it
insight::check_if_installed("ClassDiscovery")
insight::check_if_installed(c("ClassDiscovery", "PCDimension"))

# Only applies to PCA with full data
if (tolower(type) %in% c("fa", "factor", "efa") || !is.data.frame(x)) {
Expand Down
4 changes: 0 additions & 4 deletions R/principal_components.R
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,6 @@ principal_components.data.frame <- function(x,
} else if (n >= ncol(x)) {
n <- ncol(x) - 1
}
# sanity check - we need at least two factors
if (n < 2 && ncol(x) >= 2) {
n <- 2
}
n
}

Expand Down
14 changes: 12 additions & 2 deletions man/convert_efa_to_cfa.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 90ce679

Please sign in to comment.