From ad410b37c37627cb259d842e00ee41897a326e60 Mon Sep 17 00:00:00 2001 From: "Mattan S. Ben-Shachar" Date: Mon, 13 Jul 2026 12:38:23 +0300 Subject: [PATCH 1/5] re-add the warning for rank deficiency --- R/check_collinearity.R | 55 ++++++++++++++++-------- tests/testthat/test-check_collinearity.R | 21 +++++++++ 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/R/check_collinearity.R b/R/check_collinearity.R index 41ade1c89..f5ddd89b1 100644 --- a/R/check_collinearity.R +++ b/R/check_collinearity.R @@ -490,7 +490,7 @@ check_collinearity.zerocount <- function( if (inherits(x, c("clm", "clmm"))) { # names(x$beta) returns only non-singular (surviving) slopes slope_names <- names(x$beta) - keep_idx <- which(colnames(v) %in% slope_names) + keep_idx <- colnames(v) %in% slope_names # Rebuild term_assign by matching model matrix columns to surviving slopes tryCatch( @@ -507,34 +507,55 @@ check_collinearity.zerocount <- function( }, error = function(e) NULL ) - } else if (insight::has_intercept(x)) { - # Standard behavior: drop the first column/row (the singular intercept) - keep_idx <- seq_len(ncol(v))[-1] } else { - keep_idx <- seq_len(ncol(v)) - if (isTRUE(verbose)) { + keep_idx <- rep(TRUE, ncol(v)) + if (insight::has_intercept(x)) { + # Standard behavior: drop the first column/row (the singular intercept) + keep_idx[1] <- FALSE + } else if (isTRUE(verbose)) { insight::format_alert("Model without intercept. VIFs may not be sensible.") } } + # we have rank-deficiency here. remove NA columns from assignment + if (isTRUE(attr(v, "rank_deficient")) || anyNA(v)) { + if (!is.null(attr(v, "na_columns_index"))) { + # If this attribute exists, then NA values were already removed from v + term_assign <- term_assign[-attr(v, "na_columns_index")] + na_cols <- names(attr(v, "na_columns_name")) + } else if (anyNA(v)) { + # If no attribute exists, then we need to identify NA columns manually + # fixme: this should be ficef in insight::get_varcov() to avoid this step + idx_na <- apply(is.na(m), 2, all) + na_cols <- colnames(v)[idx_na] + keep_idx[idx_na] <- FALSE + } + + if (isTRUE(verbose)) { + if (length(na_cols) > 0) { + insight::format_warning( + "Model matrix is rank deficient. VIFs may not be sensible.", + paste0( + "The following coefficients have VIF = Inf / tolerance = 0: ", + paste0(na_cols, collapse = ", ") + ) + ) + } else { + insight::format_warning( + "Model matrix is rank deficient. VIFs may not be sensible." + ) + } + } + } + # Safely subset the matrix - if (length(keep_idx) < ncol(v)) { + if (any(!keep_idx) < ncol(v)) { if (!is.null(term_assign) && length(term_assign) == ncol(v)) { term_assign <- term_assign[keep_idx] } v <- v[keep_idx, keep_idx, drop = FALSE] } - # we have rank-deficiency here. remove NA columns from assignment - if (isTRUE(attributes(v)$rank_deficient) && !is.null(attributes(v)$na_columns_index)) { - term_assign <- term_assign[-attributes(v)$na_columns_index] - if (isTRUE(verbose)) { - insight::format_alert( - "Model matrix is rank deficient. VIFs may not be sensible." - ) - } - } - f <- insight::find_formula(x, verbose = FALSE) # hurdle or zeroinfl model can have no zero-inflation formula, in which case diff --git a/tests/testthat/test-check_collinearity.R b/tests/testthat/test-check_collinearity.R index fadd683a6..9ab3bb09c 100644 --- a/tests/testthat/test-check_collinearity.R +++ b/tests/testthat/test-check_collinearity.R @@ -1,5 +1,6 @@ # https://github.com/easystats/performance/pull/547 test_that("check_collinearity, correct order in print", { + data(mtcars) m <- lm(mpg ~ wt + cyl + gear + disp, data = mtcars) out <- capture.output(print(check_collinearity(m, verbose = FALSE))) expect_identical( @@ -410,3 +411,23 @@ test_that("check_collinearity, standard lm models with offset", { expect_identical(out$Term, c("wt", "cyl")) expect_false("disp" %in% out$Term) }) + + +test_that("check_collinearity, rank deficient.", { + data(mtcars) + mtcars$Z <- mtcars$wt + mtcars$cyl + + m1 <- lm(mpg ~ wt + cyl + Z, data = mtcars) + m2 <- lm(mpg ~ wt + cyl, data = mtcars) + expect_warning( + out1 <- check_collinearity(m1), + "Model matrix is rank deficient" + ) + expect_warning( + out2 <- check_collinearity(m2), + NA + ) + expect_identical(out1$VIF, out2$VIF) +}) + +debugonce(performance:::.check_collinearity) From 55d9e9ce4c517e1c2991d825c176caf6e47c08f9 Mon Sep 17 00:00:00 2001 From: "Mattan S. Ben-Shachar" Date: Mon, 13 Jul 2026 12:41:07 +0300 Subject: [PATCH 2/5] version bump + news --- DESCRIPTION | 2 +- NEWS.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 49d627990..673559bab 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: performance Title: Assessment of Regression Models Performance -Version: 0.17.1 +Version: 0.17.1.0001 Authors@R: c(person(given = "Daniel", family = "Lüdecke", diff --git a/NEWS.md b/NEWS.md index 292583bc9..c4e003e30 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,11 @@ +# performance 0.17.xxx + +## Bug fixes + +* `check_collinearity()` now properly warns when the `vcov` matrix is rank + deficient (#922). + + # performance 0.17.1 ## Changes From 441c4ed395f279a4de6282c40cb8390ff1d5ea48 Mon Sep 17 00:00:00 2001 From: "Mattan S. Ben-Shachar" Date: Mon, 13 Jul 2026 12:57:33 +0300 Subject: [PATCH 3/5] gemini suggested fixes --- R/check_collinearity.R | 4 ++-- tests/testthat/test-check_collinearity.R | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/R/check_collinearity.R b/R/check_collinearity.R index f5ddd89b1..fa100984d 100644 --- a/R/check_collinearity.R +++ b/R/check_collinearity.R @@ -526,7 +526,7 @@ check_collinearity.zerocount <- function( } else if (anyNA(v)) { # If no attribute exists, then we need to identify NA columns manually # fixme: this should be ficef in insight::get_varcov() to avoid this step - idx_na <- apply(is.na(m), 2, all) + idx_na <- apply(is.na(v), 2, all) na_cols <- colnames(v)[idx_na] keep_idx[idx_na] <- FALSE } @@ -549,7 +549,7 @@ check_collinearity.zerocount <- function( } # Safely subset the matrix - if (any(!keep_idx) < ncol(v)) { + if (sum(keep_idx) < ncol(v)) { if (!is.null(term_assign) && length(term_assign) == ncol(v)) { term_assign <- term_assign[keep_idx] } diff --git a/tests/testthat/test-check_collinearity.R b/tests/testthat/test-check_collinearity.R index 9ab3bb09c..31ef0c3d8 100644 --- a/tests/testthat/test-check_collinearity.R +++ b/tests/testthat/test-check_collinearity.R @@ -429,5 +429,3 @@ test_that("check_collinearity, rank deficient.", { ) expect_identical(out1$VIF, out2$VIF) }) - -debugonce(performance:::.check_collinearity) From 82d22851637e7e04ae2e6518b3e8b8cad4cfc9ac Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Jul 2026 10:57:08 +0200 Subject: [PATCH 4/5] minor --- DESCRIPTION | 2 +- R/check_collinearity.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 673559bab..f1b6d2a39 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: performance Title: Assessment of Regression Models Performance -Version: 0.17.1.0001 +Version: 0.17.1.1 Authors@R: c(person(given = "Daniel", family = "Lüdecke", diff --git a/R/check_collinearity.R b/R/check_collinearity.R index fa100984d..fd80f4925 100644 --- a/R/check_collinearity.R +++ b/R/check_collinearity.R @@ -525,7 +525,7 @@ check_collinearity.zerocount <- function( na_cols <- names(attr(v, "na_columns_name")) } else if (anyNA(v)) { # If no attribute exists, then we need to identify NA columns manually - # fixme: this should be ficef in insight::get_varcov() to avoid this step + ## FIXME: this should be fixed in insight::get_varcov() to avoid this step idx_na <- apply(is.na(v), 2, all) na_cols <- colnames(v)[idx_na] keep_idx[idx_na] <- FALSE From cdfb42bab1e393bb8a042980cfdb2578548b27d4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Jul 2026 10:59:41 +0200 Subject: [PATCH 5/5] minor --- tests/testthat/test-check_collinearity.R | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-check_collinearity.R b/tests/testthat/test-check_collinearity.R index 31ef0c3d8..08ace44ad 100644 --- a/tests/testthat/test-check_collinearity.R +++ b/tests/testthat/test-check_collinearity.R @@ -420,12 +420,13 @@ test_that("check_collinearity, rank deficient.", { m1 <- lm(mpg ~ wt + cyl + Z, data = mtcars) m2 <- lm(mpg ~ wt + cyl, data = mtcars) expect_warning( - out1 <- check_collinearity(m1), + { + out1 <- check_collinearity(m1) + }, "Model matrix is rank deficient" ) - expect_warning( - out2 <- check_collinearity(m2), - NA - ) + expect_silent({ + out2 <- check_collinearity(m2) + }) expect_identical(out1$VIF, out2$VIF) })