# Reference values from DescTools on mtcars # tab = table(factor(gear), factor(cyl)) [3x3] # tab2 = table(factor(vs), factor(am)) [2x2] tab_3x3 <- function() { data(mtcars, envir = environment()) table(factor(mtcars$gear), factor(mtcars$cyl)) } tab_2x2 <- function() { data(mtcars, envir = environment()) table(factor(mtcars$vs), factor(mtcars$am)) } # ── Default scalar return ────────────────────────────────────────────────── test_that("all functions return scalar by default", { tab <- tab_3x3() tab2 <- tab_2x2() expect_length(suppressWarnings(cramer_v(tab)), 1) expect_length(phi(tab2), 1) expect_length(suppressWarnings(contingency_coef(tab)), 1) expect_length(yule_q(tab2), 1) expect_length(suppressWarnings(lambda_gk(tab)), 1) expect_length(suppressWarnings(goodman_kruskal_tau(tab)), 1) expect_length(suppressWarnings(uncertainty_coef(tab)), 1) expect_length(suppressWarnings(gamma_gk(tab)), 1) expect_length(suppressWarnings(kendall_tau_b(tab)), 1) expect_length(suppressWarnings(kendall_tau_c(tab)), 1) expect_length(suppressWarnings(somers_d(tab, "row")), 1) }) # ── cramer_v ───────────────────────────────────────────────────────────────── test_that("cramer_v returns correct estimate and 4-element vector", { res <- suppressWarnings(cramer_v(tab_3x3(), detail = TRUE)) expect_type(res, "double") expect_length(res, 4) expect_named(res, c("estimate", "ci_lower", "ci_upper", "p_value")) expect_equal(res[["estimate"]], 0.5308655, tolerance = 1e-5) expect_true(res[["p_value"]] < 0.01) }) test_that("cramer_v with detail = TRUE, conf_level = NULL returns 2 elements", { res <- suppressWarnings(cramer_v(tab_3x3(), detail = TRUE, conf_level = NULL)) expect_length(res, 2) expect_named(res, c("estimate", "p_value")) }) test_that("cramer_v rejects non-table input", { expect_error(cramer_v(iris), "contingency table") }) # ── phi ────────────────────────────────────────────────────────────────────── test_that("phi returns correct value for 2x2 table", { res <- phi(tab_2x2(), detail = TRUE) expect_equal(res[["estimate"]], 0.1683451, tolerance = 1e-5) }) test_that("phi rejects non-2x2 tables", { expect_error(phi(tab_3x3()), "2x2") }) # ── contingency_coef ───────────────────────────────────────────────────────── test_that("contingency_coef matches DescTools", { res <- suppressWarnings(contingency_coef(tab_3x3(), detail = TRUE)) expect_equal(res[["estimate"]], 0.6003875, tolerance = 1e-5) expect_true(is.na(res[["ci_lower"]])) }) # ── yule_q ─────────────────────────────────────────────────────────────────── test_that("yule_q matches DescTools for 2x2", { res <- yule_q(tab_2x2(), detail = TRUE) expect_equal(res[["estimate"]], 0.3333333, tolerance = 1e-5) }) test_that("yule_q rejects non-2x2 tables", { expect_error(yule_q(tab_3x3()), "2x2") }) # ── lambda_gk ──────────────────────────────────────────────────────────────── test_that("lambda_gk matches DescTools for all directions", { tab <- tab_3x3() sym <- suppressWarnings(lambda_gk(tab, "symmetric", detail = TRUE)) row <- suppressWarnings(lambda_gk(tab, "row", detail = TRUE)) col <- suppressWarnings(lambda_gk(tab, "column", detail = TRUE)) expect_equal(sym[["estimate"]], 0.4857143, tolerance = 1e-5) expect_equal(row[["estimate"]], 0.5294118, tolerance = 1e-5) expect_equal(col[["estimate"]], 0.4444444, tolerance = 1e-5) # CI matches DescTools expect_equal(sym[["ci_lower"]], 0.2324839, tolerance = 1e-4) expect_equal(sym[["ci_upper"]], 0.7389447, tolerance = 1e-4) }) # ── goodman_kruskal_tau ────────────────────────────────────────────────────── test_that("goodman_kruskal_tau matches DescTools", { tab <- tab_3x3() row <- suppressWarnings(goodman_kruskal_tau(tab, "row", detail = TRUE)) col <- suppressWarnings(goodman_kruskal_tau(tab, "column", detail = TRUE)) expect_equal(row[["estimate"]], 0.3825603, tolerance = 1e-5) expect_equal(col[["estimate"]], 0.3386018, tolerance = 1e-5) expect_equal(row[["ci_lower"]], 0.1520724, tolerance = 1e-4) expect_equal(row[["ci_upper"]], 0.6130482, tolerance = 1e-4) }) # ── uncertainty_coef ───────────────────────────────────────────────────────── test_that("uncertainty_coef returns sensible values", { tab <- tab_3x3() sym <- suppressWarnings(uncertainty_coef(tab, "symmetric", detail = TRUE)) row <- suppressWarnings(uncertainty_coef(tab, "row", detail = TRUE)) col <- suppressWarnings(uncertainty_coef(tab, "column", detail = TRUE)) expect_true(sym[["estimate"]] > 0 && sym[["estimate"]] < 1) expect_true(row[["estimate"]] > 0 && row[["estimate"]] < 1) expect_true(col[["estimate"]] > 0 && col[["estimate"]] < 1) expect_length(sym, 4) }) # ── gamma_gk ───────────────────────────────────────────────────────────────── test_that("gamma_gk matches DescTools", { res <- suppressWarnings(gamma_gk(tab_3x3(), detail = TRUE)) expect_equal(res[["estimate"]], -0.6573705, tolerance = 1e-5) expect_equal(res[["ci_lower"]], -0.9960223, tolerance = 1e-4) expect_equal(res[["ci_upper"]], -0.3187187, tolerance = 1e-4) }) # ── kendall_tau_b ──────────────────────────────────────────────────────────── test_that("kendall_tau_b matches DescTools", { res <- suppressWarnings(kendall_tau_b(tab_3x3(), detail = TRUE)) expect_equal(res[["estimate"]], -0.5125435, tolerance = 1e-5) expect_equal(res[["ci_lower"]], -0.8043717, tolerance = 1e-4) expect_equal(res[["ci_upper"]], -0.2207153, tolerance = 1e-4) }) # ── kendall_tau_c ──────────────────────────────────────────────────────────── test_that("kendall_tau_c matches DescTools", { res <- suppressWarnings(kendall_tau_c(tab_3x3(), detail = TRUE)) expect_equal(res[["estimate"]], -0.4833984, tolerance = 1e-5) expect_equal(res[["ci_lower"]], -0.7422941, tolerance = 1e-4) expect_equal(res[["ci_upper"]], -0.2245028, tolerance = 1e-4) }) # ── somers_d ───────────────────────────────────────────────────────────────── test_that("somers_d matches DescTools for row and column", { tab <- tab_3x3() row <- suppressWarnings(somers_d(tab, "row", detail = TRUE)) col <- suppressWarnings(somers_d(tab, "column", detail = TRUE)) expect_equal(row[["estimate"]], -0.5015198, tolerance = 1e-5) expect_equal(col[["estimate"]], -0.5238095, tolerance = 1e-5) expect_equal(col[["ci_lower"]], -0.8383951, tolerance = 1e-4) expect_equal(col[["ci_upper"]], -0.2092240, tolerance = 1e-4) }) test_that("somers_d symmetric returns tau-b", { tab <- tab_3x3() sym <- suppressWarnings(somers_d(tab, "symmetric", detail = TRUE)) tau <- suppressWarnings(kendall_tau_b(tab, detail = TRUE)) expect_equal(sym[["estimate"]], tau[["estimate"]], tolerance = 1e-10) }) # ── assoc_measures ─────────────────────────────────────────────────────────── test_that("assoc_measures returns data.frame with correct structure", { tab <- tab_3x3() res <- suppressWarnings(assoc_measures(tab)) expect_s3_class(res, "data.frame") expect_true(all( c("measure", "estimate", "se", "ci_lower", "ci_upper", "p_value") %in% names(res) )) expect_true(nrow(res) > 0) }) test_that("assoc_measures auto returns all measures", { tab <- tab_3x3() res <- suppressWarnings(assoc_measures(tab)) expect_true("Cramer's V" %in% res$measure) expect_true("Goodman-Kruskal Gamma" %in% res$measure) expect_true("Kendall's Tau-b" %in% res$measure) }) test_that("assoc_measures type ordinal returns ordinal measures only", { tab <- tab_3x3() res <- suppressWarnings(assoc_measures(tab, type = "ordinal")) expect_true("Goodman-Kruskal Gamma" %in% res$measure) expect_true("Kendall's Tau-b" %in% res$measure) expect_false("Cramer's V" %in% res$measure) }) test_that("assoc_measures type nominal returns nominal measures only", { tab <- tab_3x3() res <- suppressWarnings(assoc_measures(tab, type = "nominal")) expect_true("Cramer's V" %in% res$measure) expect_false("Goodman-Kruskal Gamma" %in% res$measure) }) test_that("assoc_measures includes phi and yule_q for 2x2", { tab <- tab_2x2() res <- suppressWarnings(assoc_measures(tab)) expect_true("Phi" %in% res$measure) expect_true("Yule's Q" %in% res$measure) }) # ── Input validation ───────────────────────────────────────────────────────── test_that("all functions reject non-table input", { expect_error(gamma_gk(matrix(1:4, 2)), "contingency table") expect_error(kendall_tau_b(data.frame(a = 1)), "contingency table") expect_error(somers_d(1:10), "contingency table") }) test_that("detail = TRUE returns spicy_assoc_detail class", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) res <- cramer_v(tab, detail = TRUE) expect_s3_class(res, "spicy_assoc_detail") expect_named(res, c("estimate", "ci_lower", "ci_upper", "p_value")) res2 <- cramer_v(tab, detail = TRUE, conf_level = NULL) expect_s3_class(res2, "spicy_assoc_detail") expect_named(res2, c("estimate", "p_value")) }) test_that("print.spicy_assoc_detail formats output", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) res <- cramer_v(tab, detail = TRUE) out <- capture.output(print(res)) expect_length(out, 2) expect_match(out[1], "Estimate") expect_match(out[1], "\\bp\\b") }) test_that("print.spicy_assoc_detail respects digits argument", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) out4 <- capture.output(print(cramer_v(tab, detail = TRUE), digits = 4)) expect_match(out4[2], "\\.[0-9]{4}") }) test_that("digits argument in function propagates to print", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) res <- cramer_v(tab, detail = TRUE, digits = 4) expect_equal(attr(res, "digits"), 4) out <- capture.output(print(res)) expect_match(out[2], "\\.[0-9]{4}") }) test_that("assoc_measures returns spicy_assoc_table class", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) res <- assoc_measures(tab) expect_s3_class(res, "spicy_assoc_table") expect_s3_class(res, "data.frame") expect_true("measure" %in% names(res)) }) test_that("print.spicy_assoc_table formats output", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) res <- assoc_measures(tab) out <- capture.output(print(res)) expect_match(out[1], "Measure") expect_match(out[1], "\\bp\\b") expect_true(length(out) > 1) }) test_that("print.spicy_assoc_table respects digits argument", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) out4 <- capture.output(print(assoc_measures(tab), digits = 4)) expect_match(out4[2], "\\.[0-9]{4}") }) test_that("digits argument in assoc_measures propagates to print", { tab <- table(c("A", "B", "A", "B"), c("X", "X", "Y", "Y")) res <- assoc_measures(tab, digits = 2) expect_equal(attr(res, "digits"), 2) out <- capture.output(print(res)) expect_match(out[2], "\\.[0-9]{2}\\b") }) # ── Validation errors ────────────────────────────────────────────────────── test_that("validation rejects non-2D table", { t3d <- array(1:8, dim = c(2, 2, 2)) class(t3d) <- "table" expect_error(cramer_v(t3d), "two-dimensional") }) # ── conf_level = NULL returns without CI ─────────────────────────────────── test_that("cramer_v detail with conf_level = NULL omits CI", { tab <- tab_3x3() res <- cramer_v(tab, detail = TRUE, conf_level = NULL) expect_s3_class(res, "spicy_assoc_detail") expect_true(!"ci_lower" %in% names(res)) expect_true("p_value" %in% names(res)) }) test_that("phi detail with conf_level = NULL omits CI", { tab <- tab_2x2() res <- phi(tab, detail = TRUE, conf_level = NULL) expect_s3_class(res, "spicy_assoc_detail") expect_true(!"ci_lower" %in% names(res)) }) test_that("contingency_coef detail with conf_level = NULL omits CI", { tab <- tab_3x3() res <- contingency_coef(tab, detail = TRUE, conf_level = NULL) expect_s3_class(res, "spicy_assoc_detail") expect_true(!"ci_lower" %in% names(res)) }) # ── Degenerate tables ───────────────────────────────────────────────────── test_that("cramer_v warns on degenerate table", { tab <- matrix(0L, 2, 2) class(tab) <- "table" expect_warning(cramer_v(tab, detail = TRUE), "undefined") }) test_that("yule_q warns when ad + bc = 0", { tab <- matrix(c(0L, 0L, 0L, 5L), 2, 2) class(tab) <- "table" expect_warning(yule_q(tab), "undefined") }) test_that("yule_q handles zero cells in detail mode", { tab <- matrix(c(5L, 0L, 3L, 2L), 2, 2) class(tab) <- "table" res <- yule_q(tab, detail = TRUE) expect_true(is.na(res[["p_value"]])) }) test_that("gamma_gk warns when C + D = 0", { tab <- matrix(c(5L, 0L, 3L, 0L), 2, 2) class(tab) <- "table" expect_warning(gamma_gk(tab), "No concordant") }) test_that("kendall_tau_b warns on degenerate table", { tab <- matrix(c(5L, 5L, 5L, 5L), 2, 2) class(tab) <- "table" res <- kendall_tau_b(tab) expect_true(is.numeric(res)) }) test_that("kendall_tau_c warns on 1-row table", { tab <- matrix(c(5L, 3L), nrow = 1) class(tab) <- "table" expect_error(kendall_tau_c(tab), "at least") }) test_that("somers_d warns when denom = 0", { tab <- matrix(c(5L, 0L, 3L, 0L), 2, 2) class(tab) <- "table" expect_warning(somers_d(tab, "column"), "undefined") }) test_that("print.spicy_assoc_detail formats NA as --", { tab <- matrix(c(5L, 0L, 3L, 2L), 2, 2) class(tab) <- "table" res <- yule_q(tab, detail = TRUE) out <- capture.output(print(res)) expect_true(any(grepl("--", out))) }) test_that("print.spicy_assoc_detail formats p < 0.001", { tab <- matrix(c(50L, 0L, 0L, 50L), 2, 2) class(tab) <- "table" res <- cramer_v(tab, detail = TRUE) out <- capture.output(print(res)) expect_true(any(grepl("< 0.001", out))) }) test_that(".assoc_result detail = FALSE returns scalar", { tab <- tab_3x3() res <- gamma_gk(tab, detail = FALSE) expect_length(res, 1) expect_false(inherits(res, "spicy_assoc_detail")) }) test_that(".assoc_result conf_level = NULL with .include_se = TRUE", { tab <- tab_3x3() res <- gamma_gk(tab, detail = TRUE, conf_level = NULL, .include_se = TRUE) expect_s3_class(res, "spicy_assoc_detail") expect_true("se" %in% names(res)) expect_true("p_value" %in% names(res)) expect_false("ci_lower" %in% names(res)) }) test_that("cramer_v conf_level = NULL with .include_se = TRUE", { tab <- tab_3x3() res <- cramer_v(tab, detail = TRUE, conf_level = NULL, .include_se = TRUE) expect_s3_class(res, "spicy_assoc_detail") expect_true("se" %in% names(res)) }) test_that("phi conf_level = NULL with .include_se = TRUE", { tab <- tab_2x2() res <- phi(tab, detail = TRUE, conf_level = NULL, .include_se = TRUE) expect_s3_class(res, "spicy_assoc_detail") expect_true("se" %in% names(res)) }) test_that("yule_q conf_level = NULL omits CI", { tab <- tab_2x2() res <- yule_q(tab, detail = TRUE, conf_level = NULL) expect_s3_class(res, "spicy_assoc_detail") expect_true("p_value" %in% names(res)) expect_false("ci_lower" %in% names(res)) }) test_that("contingency_coef conf_level = NULL with .include_se = TRUE", { tab <- tab_3x3() res <- contingency_coef( tab, detail = TRUE, conf_level = NULL, .include_se = TRUE ) expect_s3_class(res, "spicy_assoc_detail") expect_true("se" %in% names(res)) }) test_that("kendall_tau_b warns when denom = 0", { tab <- matrix(c(0L, 0L, 3L, 7L), 2, 2) class(tab) <- "table" expect_warning(kendall_tau_b(tab, detail = TRUE), "undefined") }) test_that("kendall_tau_b se_sq clamped to 0 when tiny negative", { tab <- matrix(c(10L, 0L, 0L, 10L), 2, 2) class(tab) <- "table" res <- kendall_tau_b(tab, detail = TRUE) expect_s3_class(res, "spicy_assoc_detail") }) test_that("lambda_gk row detail covers L_row_max branch", { tab <- matrix(c(10L, 0L, 0L, 5L), 2, 2) class(tab) <- "table" res <- lambda_gk(tab, direction = "row", detail = TRUE) expect_s3_class(res, "spicy_assoc_detail") }) test_that("lambda_gk column detail covers L_col_max branch", { tab <- matrix(c(10L, 0L, 0L, 5L), 2, 2) class(tab) <- "table" res <- lambda_gk(tab, direction = "column", detail = TRUE) expect_s3_class(res, "spicy_assoc_detail") })