Nothing
test_that("corrPrune validates data argument", {
expect_error(
corrPrune(data = NULL),
"'data' must be a data.frame"
)
expect_error(
corrPrune(data = matrix(1:10, ncol = 2)),
"'data' must be a data.frame"
)
expect_error(
corrPrune(data = data.frame()),
"'data' must contain at least one column"
)
})
test_that("corrPrune handles single-column input under default mode = 'auto' (#34)", {
# Regression test for issue #34: mode = "auto" resolved to "exact" for any
# p <= max_exact_p, including p == 1, but exact mode routes through
# MatSelect(), which refuses ncol < 2 -- so single-predictor input, which
# corrPrune()'s own Step 7A logic treats as a trivially valid answer,
# crashed with MatSelect()'s internal "mat must have at least two columns"
# message. mode = "auto" now degrades to greedy for p < 2.
set.seed(9340)
df <- data.frame(x = rnorm(10))
result <- corrPrune(df, threshold = 0.7)
expect_s3_class(result, "data.frame")
expect_equal(names(result), "x")
expect_equal(attr(result, "mode"), "greedy")
})
test_that("corrPrune requires mode = 'exact' to have >= 2 columns, with a corrPrune-specific message (#34)", {
df <- data.frame(x = rnorm(10))
expect_error(
corrPrune(df, threshold = 0.7, mode = "exact"),
"mode = 'exact' requires at least two variables"
)
# mode = "greedy" already handles single-column input.
expect_s3_class(corrPrune(df, threshold = 0.7, mode = "greedy"), "data.frame")
})
test_that("corrPrune handles threshold = 0 under default mode = 'auto' (#34)", {
# Regression test for issue #34: threshold = 0 is documented as valid
# ("must be non-negative") and mode = "greedy" already handled it, but
# mode = "auto" resolved to "exact", which routes through MatSelect()'s
# stricter threshold in (0, 1] contract and crashed with MatSelect()'s
# internal range-check message. mode = "auto" now degrades to greedy for
# threshold == 0.
set.seed(9341)
df <- data.frame(x = rnorm(10), y = rnorm(10))
result <- corrPrune(df, threshold = 0)
expect_s3_class(result, "data.frame")
expect_equal(attr(result, "mode"), "greedy")
})
test_that("corrPrune requires mode = 'exact' to have threshold > 0, with a corrPrune-specific message (#34)", {
df <- data.frame(x = rnorm(10), y = rnorm(10))
expect_error(
corrPrune(df, threshold = 0, mode = "exact"),
"mode = 'exact' requires 'threshold' > 0"
)
# mode = "greedy" already handles threshold = 0.
expect_s3_class(corrPrune(df, threshold = 0, mode = "greedy"), "data.frame")
})
test_that("corrPrune rejects data with duplicate column names (#28)", {
df <- data.frame(x = 1:10, y = 1:10, z = 1:10)
names(df) <- c("a", "a", "b")
expect_error(
corrPrune(df),
"duplicate column names"
)
})
test_that("corrPrune deduplicates a repeated force_in name (#31)", {
set.seed(9310)
n <- 30
df <- data.frame(a = rnorm(n), b = rnorm(n), c = rnorm(n))
result <- corrPrune(df, threshold = 0.9, force_in = c("a", "a"))
expect_true("a" %in% names(result))
expect_false(anyDuplicated(names(result)) > 0)
})
test_that("corrPrune validates threshold argument", {
df <- data.frame(x = 1:10, y = 1:10)
expect_error(
corrPrune(df, threshold = "0.7"),
"'threshold' must be a single numeric value"
)
expect_error(
corrPrune(df, threshold = c(0.5, 0.7)),
"'threshold' must be a single numeric value"
)
expect_error(
corrPrune(df, threshold = -0.5),
"'threshold' must be in the range \\[0, 1\\]"
)
expect_error(
corrPrune(df, threshold = NA),
"'threshold' must be a single numeric value"
)
})
test_that("corrPrune rejects threshold > 1 consistently across modes (#90)", {
# Regression test for #90: mode = "auto" resolved to "exact" and routed
# through MatSelect()'s own (0, 1] check for threshold > 1, but
# mode = "greedy" had no such check and silently accepted it. corrPrune()
# now validates the upper bound itself, before mode dispatch, so both
# modes fail the same way on the same out-of-range input.
set.seed(9342)
df <- data.frame(x = rnorm(10), y = rnorm(10))
expect_error(
corrPrune(df, threshold = 1.5, mode = "auto"),
"'threshold' must be in the range \\[0, 1\\]"
)
expect_error(
corrPrune(df, threshold = 1.5, mode = "greedy"),
"'threshold' must be in the range \\[0, 1\\]"
)
expect_error(
corrPrune(df, threshold = 1.5, mode = "exact"),
"'threshold' must be in the range \\[0, 1\\]"
)
})
test_that("corrPrune validates measure argument", {
df <- data.frame(x = 1:10, y = 1:10)
expect_error(
corrPrune(df, measure = 123),
"'measure' must be a single character string"
)
expect_error(
corrPrune(df, measure = c("pearson", "spearman")),
"'measure' must be a single character string"
)
})
test_that("corrPrune validates mode argument", {
df <- data.frame(x = 1:10, y = 1:10)
expect_error(
corrPrune(df, mode = 123),
"'mode' must be a single character string"
)
expect_error(
corrPrune(df, mode = "invalid"),
"'mode' must be one of: 'auto', 'exact', 'greedy'"
)
expect_error(
corrPrune(df, mode = c("exact", "greedy")),
"'mode' must be a single character string"
)
})
test_that("corrPrune validates force_in argument", {
df <- data.frame(x = 1:10, y = 1:10, z = 1:10)
expect_error(
corrPrune(df, force_in = 123),
"'force_in' must be a character vector of variable names"
)
expect_error(
corrPrune(df, force_in = c("x", "missing_var")),
"'force_in' variable\\(s\\) not found in data: missing_var"
)
expect_error(
corrPrune(df, force_in = c("missing1", "missing2")),
"'force_in' variable\\(s\\) not found in data: missing1, missing2"
)
})
test_that("corrPrune validates by argument", {
df <- data.frame(x = 1:10, y = 1:10, group = rep(1:2, each = 5))
expect_error(
corrPrune(df, by = 123),
"'by' must be a character vector of variable names"
)
expect_error(
corrPrune(df, by = "missing_var"),
"'by' variable\\(s\\) not found in data: missing_var"
)
expect_error(
corrPrune(df, by = c("group", "missing")),
"'by' variable\\(s\\) not found in data: missing"
)
})
test_that("corrPrune validates group_q argument", {
df <- data.frame(x = 1:10, y = 1:10)
expect_error(
corrPrune(df, group_q = "0.9"),
"'group_q' must be a single numeric value"
)
expect_error(
corrPrune(df, group_q = c(0.5, 0.9)),
"'group_q' must be a single numeric value"
)
expect_error(
corrPrune(df, group_q = 0),
"'group_q' must be in the interval \\(0, 1\\]"
)
expect_error(
corrPrune(df, group_q = -0.5),
"'group_q' must be in the interval \\(0, 1\\]"
)
expect_error(
corrPrune(df, group_q = 1.5),
"'group_q' must be in the interval \\(0, 1\\]"
)
expect_error(
corrPrune(df, group_q = NA),
"'group_q' must be a single numeric value"
)
})
test_that("corrPrune validates max_exact_p argument", {
df <- data.frame(x = 1:10, y = 1:10)
expect_error(
corrPrune(df, max_exact_p = "20"),
"'max_exact_p' must be a single numeric value"
)
expect_error(
corrPrune(df, max_exact_p = c(10, 20)),
"'max_exact_p' must be a single numeric value"
)
expect_error(
corrPrune(df, max_exact_p = 0),
"'max_exact_p' must be >= 1"
)
expect_error(
corrPrune(df, max_exact_p = -5),
"'max_exact_p' must be >= 1"
)
expect_error(
corrPrune(df, max_exact_p = NA),
"'max_exact_p' must be a single numeric value"
)
})
# ===========================================================================
# Functional tests for corrPrune (exact mode)
# ===========================================================================
test_that("corrPrune works with simple numeric data in exact mode", {
set.seed(42)
# Create data with known correlation structure
n <- 50
x1 <- rnorm(n)
x2 <- x1 + rnorm(n, sd = 0.1) # highly correlated with x1
x3 <- rnorm(n) # independent
x4 <- rnorm(n) # independent
df <- data.frame(x1 = x1, x2 = x2, x3 = x3, x4 = x4)
# With low threshold, should exclude x2 (correlated with x1)
result <- corrPrune(df, threshold = 0.5, mode = "exact")
expect_s3_class(result, "data.frame")
expect_true(ncol(result) >= 1)
expect_true(ncol(result) <= ncol(df))
expect_true(all(names(result) %in% names(df)))
# Check attributes
expect_true("selected_vars" %in% names(attributes(result)))
expect_true("mode" %in% names(attributes(result)))
expect_true("measure" %in% names(attributes(result)))
expect_equal(attr(result, "mode"), "exact")
expect_equal(attr(result, "measure"), "pearson")
expect_equal(attr(result, "threshold"), 0.5)
})
test_that("corrPrune respects force_in in exact mode", {
set.seed(123)
n <- 50
x1 <- rnorm(n)
x2 <- rnorm(n)
x3 <- rnorm(n)
df <- data.frame(x1 = x1, x2 = x2, x3 = x3)
result <- corrPrune(df, threshold = 0.7, force_in = "x1", mode = "exact")
expect_true("x1" %in% names(result))
expect_true("x1" %in% attr(result, "selected_vars"))
})
test_that("corrPrune errors when force_in violates threshold", {
set.seed(456)
n <- 50
x1 <- rnorm(n)
x2 <- x1 + rnorm(n, sd = 0.01) # very highly correlated
df <- data.frame(x1 = x1, x2 = x2)
expect_error(
corrPrune(df, threshold = 0.3, force_in = c("x1", "x2"), mode = "exact"),
"Variables in 'force_in' violate the threshold constraint"
)
})
test_that("corrPrune auto-selects mode based on max_exact_p", {
set.seed(789)
df_small <- data.frame(matrix(rnorm(100), ncol = 10))
# Small dataset should use exact mode by default
result_small <- corrPrune(df_small, threshold = 0.7, max_exact_p = 20)
expect_equal(attr(result_small, "mode"), "exact")
})
test_that("corrPrune handles different correlation measures", {
set.seed(101)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
# Pearson
result_pearson <- corrPrune(df, threshold = 0.7, measure = "pearson", mode = "exact")
expect_equal(attr(result_pearson, "measure"), "pearson")
# Spearman
result_spearman <- corrPrune(df, threshold = 0.7, measure = "spearman", mode = "exact")
expect_equal(attr(result_spearman, "measure"), "spearman")
# Kendall
result_kendall <- corrPrune(df, threshold = 0.7, measure = "kendall", mode = "exact")
expect_equal(attr(result_kendall, "measure"), "kendall")
})
test_that("corrPrune returns deterministic results", {
set.seed(202)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n)
)
# Run twice with same data
result1 <- corrPrune(df, threshold = 0.7, mode = "exact")
result2 <- corrPrune(df, threshold = 0.7, mode = "exact")
expect_equal(names(result1), names(result2))
expect_equal(attr(result1, "selected_vars"), attr(result2, "selected_vars"))
})
test_that("corrPrune preserves row count", {
set.seed(303)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.7, mode = "exact")
expect_equal(nrow(result), nrow(df))
})
test_that("corrPrune handles missing values with warning", {
set.seed(404)
n <- 50
df <- data.frame(
x1 = c(NA, rnorm(n - 1)),
x2 = rnorm(n),
x3 = rnorm(n)
)
expect_warning(
result <- corrPrune(df, threshold = 0.7, mode = "exact"),
"Removed .* row.*with missing values"
)
# corrPrune returns original data with pruned columns, including NA rows
expect_equal(nrow(result), n)
})
test_that("corrPrune tie-breaking is deterministic", {
set.seed(505)
n <- 100
# Create data where multiple subsets might have same size
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n),
x5 = rnorm(n)
)
# Low threshold to potentially get multiple max-size subsets
result1 <- corrPrune(df, threshold = 0.8, mode = "exact")
result2 <- corrPrune(df, threshold = 0.8, mode = "exact")
# Should always select the same subset
expect_identical(attr(result1, "selected_vars"), attr(result2, "selected_vars"))
})
# ===========================================================================
# Functional tests for corrPrune (greedy mode)
# ===========================================================================
test_that("corrPrune greedy mode works with simple numeric data", {
set.seed(42)
n <- 50
x1 <- rnorm(n)
x2 <- x1 + rnorm(n, sd = 0.1) # highly correlated with x1
x3 <- rnorm(n)
x4 <- rnorm(n)
df <- data.frame(x1 = x1, x2 = x2, x3 = x3, x4 = x4)
result <- corrPrune(df, threshold = 0.5, mode = "greedy")
expect_s3_class(result, "data.frame")
expect_true(ncol(result) >= 1)
expect_true(ncol(result) <= ncol(df))
expect_equal(attr(result, "mode"), "greedy")
expect_equal(attr(result, "measure"), "pearson")
})
test_that("corrPrune greedy mode respects force_in", {
set.seed(123)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.7, force_in = "x1", mode = "greedy")
expect_true("x1" %in% names(result))
expect_true("x1" %in% attr(result, "selected_vars"))
})
test_that("corrPrune greedy mode is deterministic", {
set.seed(202)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n)
)
result1 <- corrPrune(df, threshold = 0.7, mode = "greedy")
result2 <- corrPrune(df, threshold = 0.7, mode = "greedy")
expect_equal(names(result1), names(result2))
expect_identical(attr(result1, "selected_vars"), attr(result2, "selected_vars"))
})
test_that("corrPrune auto-selects greedy mode for large datasets", {
set.seed(303)
# Create dataset with > 20 variables (default max_exact_p)
df_large <- data.frame(matrix(rnorm(50 * 25), ncol = 25))
result <- corrPrune(df_large, threshold = 0.7, mode = "auto", max_exact_p = 20)
expect_equal(attr(result, "mode"), "greedy")
})
test_that("corrPrune greedy mode handles different thresholds", {
set.seed(404)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n),
x5 = rnorm(n)
)
# Strict threshold should remove more variables
result_strict <- corrPrune(df, threshold = 0.3, mode = "greedy")
# Lenient threshold should keep more variables
result_lenient <- corrPrune(df, threshold = 0.9, mode = "greedy")
expect_true(ncol(result_lenient) >= ncol(result_strict))
})
test_that("corrPrune greedy vs exact give similar results on small data", {
set.seed(505)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n)
)
result_exact <- corrPrune(df, threshold = 0.7, mode = "exact")
result_greedy <- corrPrune(df, threshold = 0.7, mode = "greedy")
# Sizes should be close (within 1 variable)
expect_true(abs(ncol(result_exact) - ncol(result_greedy)) <= 1)
})
test_that("corrPrune greedy mode handles highly correlated data", {
set.seed(606)
n <- 50
x1 <- rnorm(n)
# Create block of highly correlated variables
df <- data.frame(
x1 = x1,
x2 = x1 + rnorm(n, sd = 0.05),
x3 = x1 + rnorm(n, sd = 0.05),
x4 = x1 + rnorm(n, sd = 0.05),
x5 = rnorm(n) # independent
)
result <- corrPrune(df, threshold = 0.5, mode = "greedy")
# Should remove most of the correlated block
expect_true(ncol(result) < ncol(df))
expect_true(ncol(result) >= 1)
})
test_that("corrPrune greedy mode handles all independent variables", {
set.seed(707)
n <- 50
# All independent
df <- data.frame(matrix(rnorm(n * 10), ncol = 10))
result <- corrPrune(df, threshold = 0.7, mode = "greedy")
# Should keep most/all variables
expect_true(ncol(result) >= 8) # Allow for some random correlation
})
test_that("corrPrune greedy mode errors when force_in violates threshold", {
set.seed(808)
n <- 50
x1 <- rnorm(n)
x2 <- x1 + rnorm(n, sd = 0.01) # very highly correlated
df <- data.frame(x1 = x1, x2 = x2)
# This should be caught in Step 5 (feasibility check)
expect_error(
corrPrune(df, threshold = 0.3, force_in = c("x1", "x2"), mode = "greedy"),
"Variables in 'force_in' violate the threshold constraint"
)
})
test_that("corrPrune greedy mode works with different correlation measures", {
set.seed(909)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result_spearman <- corrPrune(df, threshold = 0.7, measure = "spearman", mode = "greedy")
expect_equal(attr(result_spearman, "measure"), "spearman")
result_kendall <- corrPrune(df, threshold = 0.7, measure = "kendall", mode = "greedy")
expect_equal(attr(result_kendall, "measure"), "kendall")
})
# ===========================================================================
# Additional data handling tests for corrPrune
# ===========================================================================
# Note: For full mixed-type data support, use assocSelect() which is designed
# for that purpose. corrPrune works best with numeric-only data.
test_that("corrPrune handles integer columns (converted to numeric internally, kept integer on output) (#88)", {
set.seed(1004)
n <- 30
df <- data.frame(
int1 = as.integer(sample(1:100, n, replace = TRUE)),
int2 = as.integer(sample(1:100, n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.7, mode = "exact")
expect_s3_class(result, "data.frame")
expect_equal(attr(result, "measure"), "pearson")
# Regression test for #88: the internal integer -> numeric conversion used
# for association computation must not leak into the returned columns.
expect_true(is.integer(result$int1))
expect_true(is.integer(result$int2))
expect_identical(result$int1, df$int1)
expect_identical(result$int2, df$int2)
})
test_that("corrPrune errors on unsupported column types", {
df <- data.frame(
num1 = 1:10,
date1 = Sys.Date() + 1:10
)
expect_error(
corrPrune(df, threshold = 0.7),
"Unsupported column types"
)
})
test_that("corrPrune errors on unsupported measure for numeric data", {
set.seed(1005)
df <- data.frame(x = rnorm(10), y = rnorm(10))
expect_error(
corrPrune(df, threshold = 0.7, measure = "eta"),
"must be one of"
)
})
test_that("corrPrune handles all rows with NA (errors)", {
df <- data.frame(
x1 = c(NA, NA, NA),
x2 = c(NA, NA, NA)
)
# Should error - either "no complete element pairs|All rows contain missing values" or NA in matrix
expect_error(
corrPrune(df, threshold = 0.7)
)
})
test_that("corrPrune handles constant-factor pairs, matching assocSelect (#33)", {
# Regression test for issue #33: corrPrune()'s mixed-type dispatch had no
# constant-column gate for the Cramer's V (factor-factor) branch, so a
# single-level factor produced an undefined (NA) association via
# table()/chisq.test() and tripped the "surface NA explicitly" stop, even
# though assocSelect() -- which corrPrune()'s own docs claim to mirror --
# already treats a constant categorical variable as association = 0.
set.seed(9330)
df <- data.frame(
const_fac = factor(rep("A", 20)),
other_fac = factor(sample(c("X", "Y", "Z"), 20, TRUE))
)
expect_no_error(res <- corrPrune(df, threshold = 0.9))
expect_s3_class(res, "data.frame")
expect_true(all(c("const_fac", "other_fac") %in% names(res)))
# assocSelect() already handled this case; corrPrune() should now agree.
expect_no_error(assocSelect(df, threshold = 0.9))
})
test_that("corrPrune handles a constant numeric column in a mixed-type data frame (#33)", {
set.seed(9331)
n <- 20
df <- data.frame(
const_num = rep(5, n),
other_num = rnorm(n),
fac = factor(sample(c("X", "Y", "Z"), n, replace = TRUE))
)
expect_no_error(res <- corrPrune(df, threshold = 0.9))
expect_s3_class(res, "data.frame")
})
test_that("corrPrune still errors on genuinely all-missing columns after the #33 fix", {
# The constant-column gate added for #33 is restricted to fully-observed
# columns (no NA) so it doesn't mask genuinely undefined all-missing data
# as a false "association = 0".
df <- data.frame(x1 = c(NA, NA, NA), x2 = c(NA, NA, NA))
expect_error(corrPrune(df, threshold = 0.7))
df2 <- data.frame(
x = c(NA, NA, NA, NA),
y = c(NA, 1, NA, NA),
z = c(NA, NA, 2, NA)
)
expect_error(
corrPrune(df2, threshold = 0.7),
"no complete element pairs|All rows contain missing values|undefined \\(NA\\) values"
)
})
test_that("corrPrune mixed-type branch warns and reports n_rows_used on missing data, matching assocSelect (#35)", {
# Regression test for issue #35: the mixed-type association path had no
# complete-cases step (each pair type applied its own ad hoc NA policy)
# and never warned about dropped rows, unlike the all-numeric path and
# assocSelect(). n_rows_used was also computed but never attached to the
# returned object's attributes.
set.seed(9350)
n <- 60
df <- data.frame(
num1 = rnorm(n),
num2 = rnorm(n),
grp = factor(sample(c("A", "B", "C"), n, replace = TRUE))
)
df$num1[sample(n, 10)] <- NA
expect_warning(
res <- corrPrune(df, threshold = 0.9),
"Removed 10 rows with missing values"
)
expect_equal(attr(res, "n_rows_used"), 50L)
# corrPrune() only prunes columns -- the returned data keeps every row,
# including the ones with missing values that were excluded only for the
# association computation.
expect_equal(nrow(res), n)
# assocSelect() should agree on how many rows were usable.
suppressWarnings(res_assoc <- assocSelect(df, threshold = 0.9))
expect_equal(res_assoc@n_rows_used, attr(res, "n_rows_used"))
})
test_that("corrPrune attaches n_rows_used with no missing data too", {
set.seed(9351)
n <- 30
df <- data.frame(num1 = rnorm(n), grp = factor(sample(c("A", "B"), n, replace = TRUE)))
res <- corrPrune(df, threshold = 0.9)
expect_equal(attr(res, "n_rows_used"), n)
})
test_that("corrPrune handles near-constant numeric columns", {
set.seed(1013)
n <- 30
df <- data.frame(
num1 = rnorm(n),
num2 = rnorm(n),
near_const = c(5, 5, 5, 5, 5.001, rep(5, n - 5)) # Near-constant with tiny variance
)
result <- corrPrune(df, threshold = 0.8, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune returns correct attributes", {
set.seed(1014)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.7, mode = "exact")
# Check all expected attributes
expect_true(!is.null(attr(result, "selected_vars")))
expect_true(!is.null(attr(result, "removed_vars")))
expect_true(!is.null(attr(result, "mode")))
expect_true(!is.null(attr(result, "measure")))
expect_true(!is.null(attr(result, "threshold")))
expect_true(!is.null(attr(result, "n_vars_original")))
expect_true(!is.null(attr(result, "n_vars_selected")))
# Verify consistency
expect_equal(attr(result, "n_vars_selected"), ncol(result))
expect_equal(length(attr(result, "selected_vars")), ncol(result))
})
test_that("corrPrune removes highly correlated variables", {
set.seed(1015)
n <- 100
x1 <- rnorm(n)
x2 <- x1 + rnorm(n, sd = 0.01) # Very highly correlated with x1
x3 <- rnorm(n) # Independent
df <- data.frame(x1 = x1, x2 = x2, x3 = x3)
result <- corrPrune(df, threshold = 0.5, mode = "exact")
# Should have removed either x1 or x2 due to high correlation
expect_true(ncol(result) < ncol(df))
expect_false(all(c("x1", "x2") %in% names(result)))
})
# ===========================================================================
# Additional coverage tests for corrPrune.R
# ===========================================================================
# Note: corrPrune's mixed-type support is limited compared to assocSelect.
# For full mixed-type support, use assocSelect() instead.
test_that("corrPrune handles factor-factor pairs", {
set.seed(1104)
n <- 30
df <- data.frame(
cat1 = factor(sample(c("A", "B", "C"), n, replace = TRUE)),
cat2 = factor(sample(c("X", "Y"), n, replace = TRUE)),
cat3 = factor(sample(c("M", "N", "O", "P"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.99, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune handles numeric-factor pairs (eta)", {
set.seed(1107)
n <- 30
df <- data.frame(
num1 = rnorm(n),
cat1 = factor(sample(c("A", "B", "C"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.9, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune single force_in variable works in exact mode", {
set.seed(1108)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.9, force_in = "x2", mode = "exact")
expect_true("x2" %in% names(result))
expect_true("x2" %in% attr(result, "selected_vars"))
})
test_that("corrPrune greedy mode with single force_in", {
set.seed(1109)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.9, force_in = "x2", mode = "greedy")
expect_true("x2" %in% names(result))
expect_true("x2" %in% attr(result, "selected_vars"))
})
test_that("corrPrune handles eta with constant categorical variable", {
set.seed(1110)
n <- 30
df <- data.frame(
num1 = rnorm(n),
cat_const = factor(rep("A", n)) # Constant factor
)
result <- corrPrune(df, threshold = 0.99, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune handles eta with constant numeric variable", {
set.seed(1111)
n <- 30
df <- data.frame(
num_const = rep(5, n), # Constant numeric (ss_tot = 0)
cat1 = factor(sample(c("A", "B"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.99, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune grouped pruning works with by parameter", {
set.seed(1112)
# Create data with three uncorrelated numeric variables and a grouping factor
n <- 60
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n),
group = rep(c("A", "B"), each = n/2)
)
# With grouped pruning using quantile aggregation
result <- corrPrune(df, threshold = 0.5, by = "group", group_q = 0.5)
expect_s3_class(result, "data.frame")
expect_true("selected_vars" %in% names(attributes(result)))
# Should keep all 3 numeric variables since they are uncorrelated
expect_equal(length(attr(result, "selected_vars")), 3)
})
test_that("corrPrune grouped mode genuinely dispatches to exact search, not silently greedy (#82)", {
set.seed(1112)
n <- 60
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n),
group = rep(c("A", "B"), each = n / 2)
)
result <- corrPrune(df, threshold = 0.5, by = "group", group_q = 0.5, mode = "exact")
expect_equal(attr(result, "mode"), "exact")
result_greedy <- corrPrune(df, threshold = 0.5, by = "group", group_q = 0.5, mode = "greedy")
expect_equal(attr(result_greedy, "mode"), "greedy")
})
test_that("corrPrune falls back to a single variable when all vars are correlated", {
set.seed(1113)
n <- 50
x1 <- rnorm(n)
# All variables perfectly correlated
df <- data.frame(
x1 = x1,
x2 = x1,
x3 = x1
)
# With very low threshold, no pair can coexist -- the pairwise constraint
# still holds vacuously for a single variable, so corrPrune() keeps exactly
# one (lexicographically first among the tied singleton candidates).
result <- corrPrune(df, threshold = 0.1, mode = "exact")
expect_equal(attr(result, "selected_vars"), "x1")
expect_equal(sort(attr(result, "removed_vars")), c("x2", "x3"))
})
test_that("corrPrune with balanced factors", {
set.seed(1114)
n <- 40
df <- data.frame(
cat1 = factor(rep(c("A", "B"), each = n/2)),
cat2 = factor(rep(c("X", "Y"), n/2))
)
result <- corrPrune(df, threshold = 0.99, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune handles threshold = 1 (all variables valid)", {
set.seed(1116)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result <- corrPrune(df, threshold = 1.0, mode = "exact")
# All variables should be selected
expect_equal(ncol(result), ncol(df))
})
test_that("corrPrune attributes are consistent", {
set.seed(1117)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.7, mode = "greedy")
# Check attribute consistency
selected <- attr(result, "selected_vars")
removed <- attr(result, "removed_vars")
expect_equal(length(selected) + length(removed), ncol(df))
expect_equal(attr(result, "n_vars_original"), ncol(df))
expect_equal(attr(result, "n_vars_selected"), length(selected))
expect_setequal(c(selected, removed), names(df))
})
# ===========================================================================
# Additional tests for lexicographic tiebreaker and edge cases
# ===========================================================================
test_that("corrPrune exact mode selects the lowest-avg-correlation size-2 subset (#84)", {
# Despite the two-block correlation structure here, no two candidate
# size-2 subsets share the same avg correlation (0.218-0.606, all
# distinct) -- this is plain sort-by-avg-correlation selection, not a
# tie-break; {V2, V3} is the unique lowest-avg-correlation candidate.
set.seed(7001)
n <- 200
base1 <- rnorm(n)
base2 <- rnorm(n)
df <- data.frame(
V1 = base1,
V2 = base1 * 0.3 + rnorm(n, sd = sqrt(1 - 0.3^2)),
V3 = base2,
V4 = base2 * 0.3 + rnorm(n, sd = sqrt(1 - 0.3^2))
)
df$V3 <- df$V3 + 0.6 * df$V1
df$V4 <- df$V4 + 0.6 * df$V2
result <- corrPrune(df, threshold = 0.5, mode = "exact")
expect_s3_class(result, "data.frame")
expect_equal(sort(names(result)), c("V2", "V3"))
})
test_that("corrPrune exact mode keeps every variable when all pairs are mutually compatible (#84)", {
# With threshold well above every pairwise correlation, the only maximal
# subset is the full variable set -- there is nothing to choose between
# (no tie, no lexicographic ordering involved), so the checkable claim is
# simply that nothing gets dropped.
set.seed(7002)
n <- 50
df <- data.frame(
a1 = rnorm(n),
a2 = rnorm(n),
b1 = rnorm(n),
b2 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.99, mode = "exact")
expect_s3_class(result, "data.frame")
expect_equal(sort(names(result)), sort(names(df)))
})
test_that("corrPrune character columns only", {
set.seed(7003)
n <- 30
# All factor columns - should work
df <- data.frame(
char1 = factor(sample(c("apple", "banana", "cherry"), n, replace = TRUE)),
char2 = factor(sample(c("red", "green", "blue"), n, replace = TRUE)),
char3 = factor(sample(c("small", "large"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune factor-only data", {
set.seed(7004)
n <- 30
df <- data.frame(
f1 = factor(sample(c("A", "B", "C"), n, replace = TRUE)),
f2 = factor(sample(c("X", "Y"), n, replace = TRUE))
)
# Factor-only should use cramersv internally
result <- corrPrune(df, threshold = 0.8, mode = "exact")
expect_s3_class(result, "data.frame")
})
test_that("corrPrune integer columns stay integer in greedy mode output (#88)", {
set.seed(7005)
n <- 30
df <- data.frame(
int1 = as.integer(sample(1:100, n, replace = TRUE)),
int2 = as.integer(sample(1:100, n, replace = TRUE)),
num1 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.8, mode = "greedy")
expect_s3_class(result, "data.frame")
for (nm in names(result)) {
expect_identical(result[[nm]], df[[nm]])
}
})
test_that("corrPrune exact mode with single variable forced in", {
set.seed(8004)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.9, force_in = "x2", mode = "exact")
expect_true("x2" %in% names(result))
})
test_that("corrPrune greedy mode with multiple force_in variables", {
set.seed(8006)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.9, force_in = c("x1", "x3"), mode = "greedy")
expect_true(all(c("x1", "x3") %in% names(result)))
})
test_that("corrPrune exact mode finds largest subset", {
set.seed(8008)
n <- 100
x1 <- rnorm(n)
df <- data.frame(
x1 = x1,
x2 = x1 + rnorm(n, sd = 0.05), # High correlation with x1
x3 = rnorm(n),
x4 = rnorm(n),
x5 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.5, mode = "exact")
# Should have at least 3 variables (x3, x4, x5 + one of x1/x2)
expect_true(ncol(result) >= 3)
})
test_that("corrPrune greedy removes variable with most violations first", {
set.seed(8009)
n <- 100
x1 <- rnorm(n)
df <- data.frame(
bad = x1, # Correlated with many
x2 = x1 + rnorm(n, sd = 0.1),
x3 = x1 + rnorm(n, sd = 0.1),
good1 = rnorm(n),
good2 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.5, mode = "greedy")
# "bad" should likely be removed since it violates threshold with x2 and x3
expect_s3_class(result, "data.frame")
})
test_that("corrPrune returns consistent n_vars_original attribute", {
set.seed(8010)
n <- 50
df <- data.frame(
x1 = rnorm(n),
x2 = rnorm(n),
x3 = rnorm(n),
x4 = rnorm(n)
)
result <- corrPrune(df, threshold = 0.8, mode = "exact")
expect_equal(attr(result, "n_vars_original"), 4)
expect_equal(attr(result, "n_vars_selected"), ncol(result))
})
# ===========================================================================
# Grouped pruning tests
# ===========================================================================
test_that("corrPrune grouped pruning aggregates with different quantiles", {
set.seed(2001)
n <- 100
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n),
group = rep(c("A", "B", "C", "D"), each = n/4)
)
# Test with median (0.5)
result1 <- corrPrune(df, threshold = 0.5, by = "group", group_q = 0.5)
expect_s3_class(result1, "data.frame")
# Test with max (1.0)
result2 <- corrPrune(df, threshold = 0.5, by = "group", group_q = 1.0)
expect_s3_class(result2, "data.frame")
# Test with minimum (0.0)
result3 <- corrPrune(df, threshold = 0.5, by = "group", group_q = 0.01)
expect_s3_class(result3, "data.frame")
})
test_that("corrPrune grouped pruning returns only numeric columns", {
set.seed(2002)
n <- 90
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n),
group = factor(rep(1:3, each = n/3))
)
result <- corrPrune(df, threshold = 0.8, by = "group")
# Grouping column is not included in result (only numeric columns)
expect_false("group" %in% names(result))
# Selected variables should all be numeric
expect_true(all(attr(result, "selected_vars") %in% c("x", "y", "z")))
})
test_that("corrPrune grouped pruning validates by parameter", {
set.seed(2003)
df <- data.frame(x = rnorm(20), y = rnorm(20), z = factor(1:20))
expect_error(
corrPrune(df, threshold = 0.5, by = "nonexistent"),
"not found"
)
})
test_that("corrPrune grouped pruning with interaction of multiple by columns", {
set.seed(2004)
n <- 80
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
grp1 = rep(c("A", "B"), each = n/2),
grp2 = rep(c("X", "Y"), n/2)
)
result <- corrPrune(df, threshold = 0.5, by = c("grp1", "grp2"))
expect_s3_class(result, "data.frame")
})
# ===========================================================================
# Grouped pruning edge case tests
# ===========================================================================
test_that("corrPrune grouped pruning with single group warns and continues", {
set.seed(3001)
n <- 50
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n),
group = factor(rep("A", n)) # Only one group
)
expect_warning(
result <- corrPrune(df, threshold = 0.5, by = "group"),
"Only one group found"
)
expect_s3_class(result, "data.frame")
})
test_that("corrPrune grouped pruning with insufficient rows in a group warns", {
set.seed(3002)
n <- 50
df <- data.frame(
x = c(1, rnorm(n - 1)),
y = c(NA, rnorm(n - 1)), # First row incomplete
z = rnorm(n),
group = factor(c("small", rep("large", n - 1))) # One group has only 1 row
)
expect_warning(
result <- corrPrune(df, threshold = 0.8, by = "group"),
"fewer than 2 complete rows"
)
expect_s3_class(result, "data.frame")
})
# ===========================================================================
# Tests for optional package measures (bicor, distance, maximal)
# ===========================================================================
test_that("corrPrune with bicor measure works", {
skip_if_not(requireNamespace("WGCNA", quietly = TRUE))
set.seed(4001)
n <- 50
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n)
)
result <- corrPrune(df, threshold = 0.5, measure = "bicor")
expect_s3_class(result, "data.frame")
expect_equal(attr(result, "measure"), "bicor")
})
test_that("corrPrune with distance correlation works", {
skip_if_not(requireNamespace("energy", quietly = TRUE))
set.seed(4002)
n <- 30 # Smaller for speed
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n)
)
result <- corrPrune(df, threshold = 0.5, measure = "distance")
expect_s3_class(result, "data.frame")
expect_equal(attr(result, "measure"), "distance")
})
test_that("corrPrune with maximal information coefficient works", {
skip_if_not(requireNamespace("minerva", quietly = TRUE))
set.seed(4003)
n <- 30 # Smaller for speed
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n)
)
result <- corrPrune(df, threshold = 0.5, measure = "maximal")
expect_s3_class(result, "data.frame")
expect_equal(attr(result, "measure"), "maximal")
})
test_that("corrPrune combines an optional-package measure with by grouping (#82)", {
skip_if_not(requireNamespace("WGCNA", quietly = TRUE))
set.seed(4015)
n <- 60
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n),
group = rep(c("A", "B"), each = n / 2)
)
result <- corrPrune(df, threshold = 0.5, measure = "bicor", by = "group", group_q = 0.5)
expect_s3_class(result, "data.frame")
expect_equal(attr(result, "measure"), "bicor")
})
test_that("corrPrune combines the maximal-information-coefficient measure with by grouping (#82)", {
skip_if_not(requireNamespace("minerva", quietly = TRUE))
set.seed(4016)
n <- 40 # smaller for speed
df <- data.frame(
x = rnorm(n),
y = rnorm(n),
z = rnorm(n),
group = rep(c("A", "B"), each = n / 2)
)
result <- corrPrune(df, threshold = 0.5, measure = "maximal", by = "group", group_q = 0.5)
expect_s3_class(result, "data.frame")
expect_equal(attr(result, "measure"), "maximal")
})
# ===========================================================================
# Tests for mixed-type data in corrPrune
# ===========================================================================
test_that("corrPrune handles numeric-ordered pairs", {
set.seed(4004)
n <- 50
df <- data.frame(
num1 = rnorm(n),
num2 = rnorm(n),
ord1 = ordered(sample(1:3, n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8)
expect_s3_class(result, "data.frame")
})
test_that("corrPrune handles ordered-ordered pairs", {
set.seed(4005)
n <- 50
df <- data.frame(
ord1 = ordered(sample(1:4, n, replace = TRUE)),
ord2 = ordered(sample(1:4, n, replace = TRUE)),
ord3 = ordered(sample(1:4, n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8)
expect_s3_class(result, "data.frame")
})
test_that("corrPrune handles factor-factor pairs with sparse tables", {
set.seed(4006)
n <- 50
df <- data.frame(
fac1 = factor(sample(c("A", "B"), n, replace = TRUE)),
fac2 = factor(sample(c("X", "Y"), n, replace = TRUE)),
fac3 = factor(sample(c("P", "Q"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8)
expect_s3_class(result, "data.frame")
})
test_that("corrPrune handles numeric-factor pairs", {
set.seed(4007)
n <- 50
df <- data.frame(
num1 = rnorm(n),
num2 = rnorm(n),
fac1 = factor(sample(c("A", "B", "C"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8)
expect_s3_class(result, "data.frame")
})
test_that("corrPrune handles ordered-factor pairs", {
set.seed(4008)
n <- 50
df <- data.frame(
ord1 = ordered(sample(1:3, n, replace = TRUE)),
fac1 = factor(sample(c("A", "B"), n, replace = TRUE)),
fac2 = factor(sample(c("X", "Y"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8)
expect_s3_class(result, "data.frame")
})
test_that("corrPrune reports assoc_methods_used for mixed-type data (#82)", {
set.seed(4020)
n <- 50
df <- data.frame(
num1 = rnorm(n),
num2 = rnorm(n),
fac1 = factor(sample(c("A", "B"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8)
methods_used <- attr(result, "assoc_methods_used")
expect_type(methods_used, "list")
expect_true("numeric_numeric" %in% names(methods_used))
expect_true("numeric_factor" %in% names(methods_used))
expect_equal(methods_used[["numeric_numeric"]], "pearson")
expect_equal(methods_used[["numeric_factor"]], "eta")
})
test_that("corrPrune's non-default measure only scopes numeric-numeric pairs on mixed-type data (#103)", {
set.seed(4021)
n <- 50
df <- data.frame(
num1 = rnorm(n),
num2 = rnorm(n),
ord1 = factor(sample(1:3, n, replace = TRUE), ordered = TRUE),
fac1 = factor(sample(c("A", "B"), n, replace = TRUE))
)
result <- corrPrune(df, threshold = 0.8, measure = "kendall")
methods_used <- attr(result, "assoc_methods_used")
# measure applies only to numeric-numeric ...
expect_equal(methods_used[["numeric_numeric"]], "kendall")
# ... every other pair type is unaffected by the override, per docs
expect_equal(methods_used[["numeric_ordered"]], "spearman")
expect_equal(methods_used[["numeric_factor"]], "eta")
expect_equal(methods_used[["ordered_factor"]], "cramersv")
})
# ===========================================================================
# Test for chi-squared edge case
# ===========================================================================
test_that("corrPrune handles chi-squared NA gracefully", {
set.seed(4010)
n <- 30
# Create factors where chi-squared might fail
df <- data.frame(
fac1 = factor(c(rep("A", n-1), "B")), # Very unbalanced
fac2 = factor(sample(c("X", "Y"), n, replace = TRUE)),
num1 = rnorm(n)
)
# Should not error
result <- corrPrune(df, threshold = 0.9)
expect_s3_class(result, "data.frame")
})
# ===========================================================================
# Edge case: All rows have NA in grouped aggregation
# ===========================================================================
test_that("corrPrune grouped pruning handles groups with all NA", {
set.seed(5002)
n <- 40
# Create data where one group has only NA values
df <- data.frame(
x = c(rnorm(30), rep(NA, 10)),
y = c(rnorm(30), rep(NA, 10)),
z = rnorm(n),
group = rep(c("good", "bad"), c(30, 10))
)
# Should handle the NA group gracefully
expect_warning(
result <- corrPrune(df, threshold = 0.8, by = "group"),
"fewer than 2 complete rows"
)
expect_s3_class(result, "data.frame")
})
# ===========================================================================
# Lexicographic tie-breaking: lines 502-508 in corrPrune
# Need: multiple subsets with SAME size AND SAME avg correlation
# ===========================================================================
test_that("corrPrune exact mode selects the lowest-avg-correlation candidate among four same-size subsets (#84)", {
# a,b are highly correlated (0.9), c,d are highly correlated (0.9), and
# all cross pairs (a-c, a-d, b-c, b-d) are only approximately uncorrelated
# (finite-sample noise, not exactly 0) -- giving four size-2 maximal
# subsets {a,c}, {a,d}, {b,c}, {b,d} whose avg correlations are close but
# NOT exactly tied. This originally claimed to test the lexicographic
# tie-break with an inline comment asserting "a,c" wins -- neither claim
# holds: the actual winner (verified below) is determined by sort-by-
# avg-correlation, and it is "b,d", not "a,c". See
# "corrPrune exact mode resolves an exact avg-correlation tie
# lexicographically" elsewhere in this file for a genuine tie,
# constructed so the four candidates are exactly equal.
n <- 200
set.seed(9001)
base1 <- rnorm(n)
base2 <- rnorm(n)
a <- base1
b <- base1 + rnorm(n, sd = 0.1)
c <- base2
d <- base2 + rnorm(n, sd = 0.1)
df <- data.frame(a = a, b = b, c = c, d = d)
cor_mat <- cor(df)
expect_true(abs(cor_mat["a", "b"]) > 0.9)
expect_true(abs(cor_mat["c", "d"]) > 0.9)
expect_true(abs(cor_mat["a", "c"]) < 0.3)
result <- corrPrune(df, threshold = 0.5, mode = "exact")
expect_s3_class(result, "data.frame")
expect_equal(sort(names(result)), c("b", "d"))
result2 <- corrPrune(df, threshold = 0.5, mode = "exact")
expect_identical(names(result), names(result2))
})
# ===========================================================================
# Chi-squared returning NA: line 350 in corrPrune
# Need: contingency table where chisq.test returns NA
# ===========================================================================
test_that("corrPrune handles chi-squared NA from degenerate table", {
set.seed(9002)
n <- 50
# Create factors where chi-squared might produce NA
# This happens with very sparse tables
df <- data.frame(
fac1 = factor(c(rep("A", 48), "B", "C")), # Very unbalanced
fac2 = factor(c(rep("X", 48), "Y", "Z")), # Very unbalanced
num1 = rnorm(n)
)
# Should handle gracefully
result <- corrPrune(df, threshold = 0.95)
expect_s3_class(result, "data.frame")
})
# ===========================================================================
# All NA in grouped aggregation: line 410 in corrPrune
# Need: all groups produce NA for a variable pair
# ===========================================================================
# ===========================================================================
# assocSelect: single-level factor (line 197) and constant numeric (line 199)
# ===========================================================================
test_that("assocSelect handles eta with single-level factor", {
set.seed(9004)
n <- 30
df <- data.frame(
num1 = rnorm(n),
num2 = rnorm(n),
fac_single = factor(rep("only_level", n))
)
# eta computation should return 0 for single-level factor
result <- assocSelect(df, threshold = 0.9)
expect_true(inherits(result, "CorrCombo"))
})
test_that("assocSelect handles eta with constant numeric", {
set.seed(9005)
n <- 30
df <- data.frame(
const_num = rep(42, n), # Constant - ss_tot = 0
fac1 = factor(sample(c("A", "B", "C"), n, replace = TRUE)),
num2 = rnorm(n)
)
# eta computation should handle ss_tot = 0
result <- assocSelect(df, threshold = 0.9)
expect_true(inherits(result, "CorrCombo"))
})
# ===========================================================================
# corrSelect: bicor, distance, maximal with installed packages
# ===========================================================================
test_that("corrSelect with bicor computes correctly", {
skip_if_not(requireNamespace("WGCNA", quietly = TRUE))
set.seed(9006)
n <- 50
x <- rnorm(n)
df <- data.frame(
a = x,
b = x + rnorm(n, sd = 0.5),
c = rnorm(n)
)
result <- corrSelect(df, threshold = 0.7, cor_method = "bicor")
expect_true(inherits(result, "CorrCombo"))
expect_true(length(result@subset_list) >= 1)
})
test_that("corrSelect with distance computes correctly", {
skip_if_not(requireNamespace("energy", quietly = TRUE))
set.seed(9007)
n <- 30
df <- data.frame(a = rnorm(n), b = rnorm(n), c = rnorm(n))
result <- corrSelect(df, threshold = 0.5, cor_method = "distance")
expect_true(inherits(result, "CorrCombo"))
})
test_that("corrSelect with maximal computes correctly", {
skip_if_not(requireNamespace("minerva", quietly = TRUE))
set.seed(9008)
n <- 30
df <- data.frame(a = rnorm(n), b = rnorm(n), c = rnorm(n))
result <- corrSelect(df, threshold = 0.5, cor_method = "maximal")
expect_true(inherits(result, "CorrCombo"))
})
# ===========================================================================
# VIF: predictor column matching edge case
# ===========================================================================
# ===========================================================================
# corrPrune: Deep edge case tests
# ===========================================================================
test_that("corrPrune errors when all rows have missing values", {
df <- data.frame(
x = c(NA, NA, NA, NA),
y = c(NA, 1, NA, NA),
z = c(NA, NA, 2, NA)
)
expect_error(
corrPrune(df, threshold = 0.7),
"no complete element pairs|All rows contain missing values"
)
})
test_that("corrPrune errors clearly on a single-row data frame with no missing values (#64)", {
# Before this fix, a single complete row fell through to
# .numeric_assoc_matrix()'s constant-column check (sd() of one value is
# NA, not 0/FALSE) and surfaced as an opaque "missing value where
# TRUE/FALSE needed" rather than a corrPrune-specific message.
df <- data.frame(x = 1, y = 2, z = 3)
expect_error(
corrPrune(df, threshold = 0.7),
"Fewer than two complete-case rows"
)
})
test_that("corrPrune auto mode switches from exact to greedy exactly at the default max_exact_p boundary (#64)", {
# Exercises the actual documented default (100), not an overridden small
# value -- distinct from the auto-mode tests elsewhere in this file that
# explicitly pass a small max_exact_p to force a fast switch. n = 200 (not
# a smaller n) keeps sampling noise low enough that the p = 95 exact-mode
# compatibility graph stays dense (few chance correlations above 0.3), so
# exact search stays fast rather than risking combinatorial blowup.
set.seed(4001)
n <- 200
df_under <- as.data.frame(matrix(rnorm(n * 95), n, 95)) # p = 95 <= 100
df_over <- as.data.frame(matrix(rnorm(n * 105), n, 105)) # p = 105 > 100
res_under <- corrPrune(df_under, threshold = 0.3, mode = "auto")
res_over <- corrPrune(df_over, threshold = 0.3, mode = "auto")
expect_equal(attr(res_under, "mode"), "exact")
expect_equal(attr(res_over, "mode"), "greedy")
})
test_that("corrPrune converts character columns to factor internally but returns the original character column (#88)", {
set.seed(12001)
n <- 20
df <- data.frame(
char_col = sample(c("apple", "banana", "cherry"), n, replace = TRUE),
num_col = rnorm(n),
stringsAsFactors = FALSE
)
res <- corrPrune(df, threshold = 0.9)
expect_s3_class(res, "data.frame")
expect_true(is.character(res$char_col))
expect_identical(res$char_col, df$char_col)
})
test_that("corrPrune converts logical columns to factor internally but returns the original logical column (#88)", {
set.seed(12002)
n <- 20
df <- data.frame(
bool_col = sample(c(TRUE, FALSE), n, replace = TRUE),
num_col = rnorm(n)
)
res <- corrPrune(df, threshold = 0.9)
expect_s3_class(res, "data.frame")
expect_true(is.logical(res$bool_col))
expect_identical(res$bool_col, df$bool_col)
})
test_that("corrPrune converts integer columns to numeric internally but returns the original integer column (#88)", {
set.seed(12003)
n <- 20
df <- data.frame(
int_col = as.integer(sample(1:100, n, replace = TRUE)),
num_col = rnorm(n)
)
res <- corrPrune(df, threshold = 0.9)
expect_s3_class(res, "data.frame")
expect_true(is.integer(res$int_col))
expect_identical(res$int_col, df$int_col)
})
# ===========================================================================
# corrPrune: Mixed-type chi2 edge cases
# ===========================================================================
test_that("corrPrune handles factor-factor with sparse contingency table", {
set.seed(16001)
n <- 15
df <- data.frame(
f1 = factor(c(rep("A", 12), rep("B", 3))),
f2 = factor(c(rep("X", 13), rep("Y", 2))),
num = rnorm(n)
)
res <- corrPrune(df, threshold = 0.9)
expect_s3_class(res, "data.frame")
})
test_that("corrPrune handles chi2 NA in mixed-type matrix computation", {
set.seed(16002)
# Create very degenerate factor-factor pair
n <- 10
df <- data.frame(
f1 = factor(c(rep("A", 9), "B")),
f2 = factor(c(rep("X", 9), "Y")),
num = rnorm(n)
)
res <- corrPrune(df, threshold = 0.95)
expect_s3_class(res, "data.frame")
})
test_that("corrPrune all-numeric with all rows NA errors correctly", {
# All-numeric data where complete.cases returns nothing
df <- data.frame(
x = c(1, NA, NA),
y = c(NA, 2, NA),
z = c(NA, NA, 3)
)
expect_error(
corrPrune(df, threshold = 0.7, measure = "pearson"),
"no complete element pairs|All rows"
)
})
# ===========================================================================
# Recovery-style and reference-verified tests (closes coverage gaps flagged
# in issue #27: prior tests mostly checked "does not error", not that the
# documented guarantees -- grouped aggregation math, exact tie-break order,
# greedy/exact agreement -- actually hold against an independently computed
# expectation).
# ===========================================================================
test_that("corrPrune grouped group_q aggregation matches hand-computed quantiles", {
# Group A: x,y perfectly correlated. Group B: x,y correlated at exactly 0.3
# (verified below via cor() directly, independent of corrPrune's internals).
dfA <- data.frame(x = c(1, 2, 3, 4, 5), y = c(1, 2, 3, 4, 5), group = "A")
dfB <- data.frame(x = c(1, 2, 3, 4, 5), y = c(3, 1, 5, 2, 4), group = "B")
cor_a <- abs(cor(dfA$x, dfA$y))
cor_b <- abs(cor(dfB$x, dfB$y))
expect_equal(cor_a, 1)
expect_equal(round(cor_b, 4), 0.3)
df <- rbind(dfA, dfB)
threshold <- (cor_a + cor_b) / 2 # strictly between the two group correlations
# group_q = 1 aggregates by the max across groups (cor_a = 1), which
# exceeds `threshold` -- x and y cannot coexist.
res_max <- corrPrune(df, threshold = threshold, by = "group", group_q = 1)
expect_equal(attr(res_max, "selected_vars"), "x")
# group_q near 0 aggregates toward the min across groups (cor_b = 0.3),
# which is below `threshold` -- x and y can coexist.
res_min <- corrPrune(df, threshold = threshold, by = "group", group_q = 0.01)
expect_setequal(attr(res_min, "selected_vars"), c("x", "y"))
})
test_that("corrPrune grouped mode errors when a factor level is unused within one group (#55)", {
# region has an "East" level, but group A only ever has North/South -- the
# eta/Cramer's V computation for group A therefore returns NA for any pair
# involving region, even though group A had plenty of complete rows. This
# must not be silently dropped from the group_q aggregation (which would
# let group_q = 1's "holds in all groups" guarantee pass unverified).
set.seed(1)
n <- 30
site <- rep(c("A", "B"), each = 15)
region <- factor(rep(NA_character_, n), levels = c("North", "South", "East"))
region[site == "A"] <- sample(c("North", "South"), 15, replace = TRUE)
region[site == "B"] <- sample(c("North", "South", "East"), 15, replace = TRUE)
df <- data.frame(x1 = rnorm(n), x2 = rnorm(n), region = region, site = site)
expect_error(
corrPrune(df, threshold = 0.9, by = "site", group_q = 1),
"undefined"
)
})
test_that("corrPrune grouped mode warns about NA rows confined to a single group (#55)", {
df <- data.frame(x1 = rnorm(20), x2 = rnorm(20), grp = rep(c("A", "B"), each = 10))
df$x1[c(1, 2, 3)] <- NA # all within group A
expect_warning(
corrPrune(df, threshold = 0.9, by = "grp"),
"Removed 3 row"
)
})
test_that("corrPrune grouped mode's n_rows_used excludes skipped groups (#55)", {
df <- data.frame(x1 = rnorm(10), x2 = rnorm(10),
grp = c(rep("A", 8), "B", "C")) # B, C have 1 row each: skipped
res <- suppressWarnings(corrPrune(df, threshold = 0.9, by = "grp"))
expect_equal(attr(res, "n_rows_used"), 8)
})
test_that("corrPrune grouped mode warns when the by column itself has NA (#55)", {
df <- data.frame(x1 = rnorm(20), x2 = rnorm(20), grp = rep(c("A", "B"), each = 10))
df$grp[c(1, 2)] <- NA
expect_warning(
corrPrune(df, threshold = 0.9, by = "grp"),
"missing values in the grouping variable"
)
})
test_that("corrPrune exact mode resolves an exact avg-correlation tie lexicographically", {
# Construct a,b,c,d so that a-b and c-d are perfectly anti-correlated
# (excluded by the threshold) while all four cross pairs (a-c, a-d, b-c,
# b-d) share the exact same absolute correlation, by making a=-b and c=-d
# from two exactly orthogonal base vectors. This forces a genuine tie in
# both size and avg correlation across all four candidate pairs, isolating
# the documented lexicographic tie-break (not just "gives a reproducible
# answer", but specifically the alphabetically-first one).
set.seed(42)
n <- 20
Q <- qr.Q(qr(matrix(rnorm(n * 2), n, 2)))
z1 <- Q[, 1]; z2 <- Q[, 2]
df <- data.frame(a = z1, b = -z1, c = z2, d = -z2)
m <- abs(cor(df))
expect_equal(m["a", "c"], m["a", "d"], tolerance = 1e-10)
expect_equal(m["a", "c"], m["b", "c"], tolerance = 1e-10)
expect_equal(m["a", "c"], m["b", "d"], tolerance = 1e-10)
result <- corrPrune(df, threshold = 0.5, mode = "exact")
expect_equal(sort(attr(result, "selected_vars")), c("a", "c"))
})
test_that("corrPrune greedy mode removes the variable with the most violations first", {
# a is built to correlate with both b (via base1) and c (via base2), while
# b and c share nothing and are uncorrelated with each other. So a has 2
# threshold violations (with b and with c) while b and c each have only 1
# (with a) -- isolating the primary greedy tie-break criterion cleanly.
set.seed(11)
n <- 40
base1 <- rnorm(n)
base2 <- rnorm(n)
df <- data.frame(
a = base1 + base2,
b = base1 + rnorm(n, sd = 0.05),
c = base2 + rnorm(n, sd = 0.05),
d = rnorm(n)
)
m <- abs(cor(df))
expect_true(m["a", "b"] > 0.5 && m["a", "c"] > 0.5 && m["b", "c"] < 0.5)
result <- corrPrune(df, threshold = 0.5, mode = "greedy")
expect_equal(sort(attr(result, "selected_vars")), c("b", "c", "d"))
expect_equal(attr(result, "removed_vars"), "a")
})
test_that("corrPrune greedy mode's 2nd tie-break (highest max association) determines the final subset (#63)", {
# A and C tie on violation count (2 each: A via B,C; C via A,D) but A's max
# association (0.95, with B) clearly exceeds C's (0.6) -- no near-tie
# epsilon involved, a plain "which max is bigger" decision. Removing A
# first (correct) vs. C first (what a level-2 comparison bug would do)
# leads to two entirely different survivor sets, so this is observable
# through the public API, not just internal removal order.
m <- matrix(c(
1, 0.95, 0.6, 0.1,
0.95, 1, 0.05, 0.2,
0.6, 0.05, 1, 0.6,
0.1, 0.2, 0.6, 1
), 4, 4, byrow = TRUE)
colnames(m) <- rownames(m) <- c("A", "B", "C", "D")
keep <- corrselect:::greedyPruneBackend(m, 0.5, NULL)
expect_setequal(colnames(m)[keep], c("B", "C"))
})
test_that("corrPrune greedy mode's 3rd tie-break (highest average association) determines the final subset (#63)", {
# A and B mutually violate (0.9), so their max association always ties at
# that shared edge -- isolating the 3rd tie-break (average association)
# rather than the 2nd. A's average across all its edges (0.9, 0.4, 0.3) is
# higher than B's (0.9, 0.2, 0.1), so A is judged worse and removed;
# removing B instead (what a level-3 comparison bug would do) leaves a
# different survivor set.
m <- matrix(c(
1, 0.9, 0.4, 0.3,
0.9, 1, 0.2, 0.1,
0.4, 0.2, 1, 0.1,
0.3, 0.1, 0.1, 1
), 4, 4, byrow = TRUE)
colnames(m) <- rownames(m) <- c("A", "B", "C", "D")
keep <- corrselect:::greedyPruneBackend(m, 0.5, NULL)
expect_setequal(colnames(m)[keep], c("B", "C", "D"))
})
test_that("corrPrune greedy mode's 4th tie-break (lowest column index removed) determines the final subset (#63)", {
# A, B, C, E all tie on violation count, max association, and average
# association by construction (each violates exactly one other variable
# at 0.9 and has three legal 0.1 edges) -- isolating the final tie-break,
# column index. The lowest-index variable among the tied set is removed
# each round (A, then B), matching the documented "smallest index wins"
# rule; removing the largest-index variable instead (a plausible inverted
# comparison bug) leaves a different survivor set.
m <- matrix(c(
1, 0.1, 0.9, 0.1, 0.1,
0.1, 1, 0.1, 0.1, 0.9,
0.9, 0.1, 1, 0.1, 0.1,
0.1, 0.1, 0.1, 1, 0.1,
0.1, 0.9, 0.1, 0.1, 1
), 5, 5, byrow = TRUE)
colnames(m) <- rownames(m) <- c("A", "B", "C", "D", "E")
keep <- corrselect:::greedyPruneBackend(m, 0.5, NULL)
expect_setequal(colnames(m)[keep], c("C", "D", "E"))
})
test_that("greedyPruneBackend() errors when two force_in variables mutually violate the threshold (#93)", {
# Regression test for #93: when every variable in a violating pair is
# protected by force_in, greedyPruneBackend() cannot remove either one to
# satisfy the threshold and must stop() with a specific message, rather
# than looping forever or silently returning both. This C++-level check is
# unreachable through the public corrPrune() API (its R layer pre-checks
# force_in-vs-force_in violations first), but is directly testable here,
# matching the tie-break tests above that already call the backend
# directly.
m <- matrix(c(
1, 0.95,
0.95, 1
), 2, 2, byrow = TRUE)
colnames(m) <- rownames(m) <- c("A", "B")
# force_in indices are 0-based at this direct C++ boundary.
expect_error(
corrselect:::greedyPruneBackend(m, 0.5, c(0L, 1L)),
"Cannot satisfy threshold: force_in variables violate the constraint"
)
})
test_that("corrPrune greedy and exact modes agree on an unambiguous case", {
# a, b, c are mutually near-independent; d is a near-exact blend of all
# three, so it conflicts with each of them individually. {a,b,c} is the
# unique largest valid subset -- both modes must return the same subset,
# not merely a same-sized one.
set.seed(21)
n <- 60
a <- rnorm(n); b <- rnorm(n); c <- rnorm(n)
d <- (a + b + c) + rnorm(n, sd = 0.01)
df <- data.frame(a = a, b = b, c = c, d = d)
res_exact <- corrPrune(df, threshold = 0.5, mode = "exact")
res_greedy <- corrPrune(df, threshold = 0.5, mode = "greedy")
expect_equal(sort(attr(res_exact, "selected_vars")), c("a", "b", "c"))
expect_equal(
sort(attr(res_exact, "selected_vars")),
sort(attr(res_greedy, "selected_vars"))
)
})
test_that("corrPrune force_in infeasibility is detected under grouped aggregation", {
dfA <- data.frame(x = c(1, 2, 3, 4, 5), y = c(1, 2, 3, 4, 5), group = "A")
dfB <- data.frame(x = c(1, 2, 3, 4, 5), y = c(1, 2, 3, 4, 5), group = "B")
df <- rbind(dfA, dfB)
expect_error(
corrPrune(df, threshold = 0.5, force_in = c("x", "y"), by = "group"),
"violate the threshold constraint"
)
})
test_that("corrPrune errors when force_in overlaps with by (#82)", {
df <- data.frame(x = c(1, 2, 3, 4), y = c(4, 3, 2, 1), group = c("A", "A", "B", "B"))
expect_error(
corrPrune(df, threshold = 0.5, force_in = c("x", "group"), by = "group"),
"'force_in' cannot include grouping variable\\(s\\) named in 'by'"
)
})
test_that("corrPrune recovers at-most-one-per-block structure across seeds", {
# Two independent correlated blocks of two variables each, plus one
# independent noise variable. A correct pruning should never retain both
# members of the same block (they violate the threshold with each other)
# and should always retain the independent noise variable.
n_trials <- 20
recovered <- 0
for (seed in seq_len(n_trials)) {
set.seed(1000 + seed)
n <- 50
block1_base <- rnorm(n)
block2_base <- rnorm(n)
df <- data.frame(
b1_a = block1_base + rnorm(n, sd = 0.05),
b1_b = block1_base + rnorm(n, sd = 0.05),
b2_a = block2_base + rnorm(n, sd = 0.05),
b2_b = block2_base + rnorm(n, sd = 0.05),
noise = rnorm(n)
)
result <- corrPrune(df, threshold = 0.5, mode = "exact")
sel <- attr(result, "selected_vars")
ok <- sum(c("b1_a", "b1_b") %in% sel) <= 1 &&
sum(c("b2_a", "b2_b") %in% sel) <= 1 &&
"noise" %in% sel
if (ok) recovered <- recovered + 1
}
expect_equal(recovered, n_trials)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.