Skip to content

Commit b2ea74e

Browse files
hidekojicursoragent
andcommitted
fix(factanal): retry polychoric with correct=0 before Pearson fallback
Same sparse-table psych failure as Cronbach's Alpha: keep the requested polychoric/tetrachoric/mixed estimate when continuity correction 0 works, instead of degrading straight to Pearson. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1ba7c7d commit b2ea74e

2 files changed

Lines changed: 64 additions & 16 deletions

File tree

R/factanal_correlation.R

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -432,23 +432,44 @@ build_factor_correlation <- function(data, correlation_type = c("pearson", "poly
432432
type = "pearson", smoothed = FALSE, warnings = character(), failed = FALSE))
433433
}
434434

435+
# psych::polychoric / tetrachoric / mixedCor can fail on sparse contingency tables when the
436+
# continuity correction (default correct=0.5) produces empty pairwise results. psych itself
437+
# suggests retrying with correct=0; without that retry we used to degrade straight to Pearson
438+
# even though the polychoric-family estimate was recoverable (same root cause as the
439+
# Cronbach's Alpha fix).
435440
captured <- character()
436-
result <- withCallingHandlers(
437-
tryCatch({
438-
switch(correlation_type,
439-
polychoric = psych::polychoric(data, correct = correct),
440-
tetrachoric = psych::tetrachoric(data, correct = correct),
441-
mixed = psych::mixedCor(data))
442-
}, error = function(e) {
443-
captured <<- c(captured, conditionMessage(e))
444-
NULL
445-
}),
446-
warning = function(w) {
447-
captured <<- c(captured, conditionMessage(w))
448-
invokeRestart("muffleWarning")
449-
})
450-
451-
if (is.null(result) || is.null(result$rho) || anyNA(result$rho)) {
441+
run_psych_correlation <- function(corr) {
442+
withCallingHandlers(
443+
tryCatch({
444+
switch(correlation_type,
445+
polychoric = psych::polychoric(data, correct = corr),
446+
tetrachoric = psych::tetrachoric(data, correct = corr),
447+
mixed = psych::mixedCor(data, correct = corr))
448+
}, error = function(e) {
449+
captured <<- c(captured, conditionMessage(e))
450+
NULL
451+
}),
452+
warning = function(w) {
453+
captured <<- c(captured, conditionMessage(w))
454+
invokeRestart("muffleWarning")
455+
})
456+
}
457+
correlation_ok <- function(obj) {
458+
!is.null(obj) && !is.null(obj$rho) && !anyNA(obj$rho)
459+
}
460+
461+
result <- run_psych_correlation(correct)
462+
if (!correlation_ok(result) && correct != 0) {
463+
result <- run_psych_correlation(0)
464+
if (correlation_ok(result)) {
465+
captured <- c(
466+
captured,
467+
sprintf("%s used continuity correction of 0 because the default correction failed.",
468+
factanal_correlation_label(correlation_type)))
469+
}
470+
}
471+
472+
if (!correlation_ok(result)) {
452473
# Degrade to Pearson rather than aborting the whole analysis. A partially-NA matrix counts as a
453474
# failure too: psych::fa() refuses to run on one ("missing values (NAs) in the correlation
454475
# matrix do not allow me to continue"), so keeping it would abort with a raw psych error.

tests/testthat/test_factanal_correlation.R

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,33 @@ test_that("verification pass 4 findings (issue #26623)", {
507507
expect_equal(zero_diagnostics$Judgement[[2]], "Detected in 1 variable")
508508
})
509509

510+
test_that("build_factor_correlation retries with correct=0 before degrading", {
511+
# Same sparse contingency pattern that makes psych::polychoric(correct=0.5) throw
512+
# "attempt to set 'rownames' on an object with no dimensions", while correct=0 succeeds.
513+
set.seed(1)
514+
df <- data.frame(lapply(1:8, function(i) as.integer(sample(1:5, 12, replace = TRUE))))
515+
names(df) <- paste0("q", 1:8)
516+
517+
expect_true(inherits(
518+
suppressWarnings(tryCatch(psych::polychoric(df, correct = 0.5), error = function(e) e)),
519+
"error"))
520+
expect_false(inherits(
521+
suppressWarnings(tryCatch(psych::polychoric(df, correct = 0), error = function(e) e)),
522+
"error"))
523+
524+
result <- build_factor_correlation(df, "polychoric")
525+
expect_equal(result$type, "polychoric")
526+
expect_false(result$failed)
527+
expect_equal(dim(result$correlation), c(8L, 8L))
528+
expect_true(any(grepl("continuity correction of 0", result$warnings)))
529+
530+
# End-to-end: exp_factanal must keep polychoric rather than degrade to Pearson.
531+
fit <- exp_factanal(df, q1, q2, q3, q4, q5, q6, q7, q8, nfactors = 2, rotate = "varimax",
532+
cor_type = "polychoric", parallel_n_iter = 3)$model[[1]]
533+
expect_equal(fit$correlation_type, "polychoric")
534+
expect_equal(fit$correlation_degraded_from, "")
535+
})
536+
510537
test_that("a failed estimation degrades to Pearson end to end (issue #26623)", {
511538
# The degrade path mutates `resolved` after construction and re-points the diagnostics and the
512539
# parallel analysis; exercise it through exp_factanal, not just the helper.

0 commit comments

Comments
 (0)