Nothing
testthat::skip_on_cran()
# ==============================================================================
# Tests for cluster_choice() and the cluster_choice class.
#
# Coverage:
# 1. Single-axis sweep (k only) returns expected columns and row count.
# 2. Dissimilarity sweep (fixed k) and method sweep (fixed k).
# 3. Cartesian product of k x dissimilarity gives length(k) * length(diss)
# rows and the right columns vary across them.
# 4. "all" sentinel expands dissimilarity / method to the full canonical
# list (9 entries each).
# 5. Per-row math: silhouette matches a manual build_clusters() call;
# sizes / size_ratio are consistent.
# 6. Print: header adapts (single-axis -> [<method> / <dissim>]; multi-axis
# -> "sweep: ...") and the silhouette-best row is marked.
# 7. Summary returns a data.frame with $best populated on the right row.
# 8. Plot returns a ggplot for each shape (k-only, dissim-only, k x dissim).
# 9. weighted = TRUE rejected up-front when sweeping non-hamming dissims.
# ==============================================================================
# ---- Synthetic data ----------------------------------------------------------
.make_choice_data <- function(n_per = 20, n_cols = 6, seed = 11) {
set.seed(seed)
g1 <- matrix(sample(c("A", "B", "C"), n_per * n_cols, replace = TRUE,
prob = c(0.7, 0.2, 0.1)), ncol = n_cols)
g2 <- matrix(sample(c("A", "B", "C"), n_per * n_cols, replace = TRUE,
prob = c(0.1, 0.2, 0.7)), ncol = n_cols)
out <- as.data.frame(rbind(g1, g2), stringsAsFactors = FALSE)
colnames(out) <- paste0("T", seq_len(n_cols))
out
}
# ==============================================================================
# Sweep shapes
# ==============================================================================
test_that("k-only sweep returns one row per k with all metric columns", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
expect_s3_class(ch, "cluster_choice")
expect_s3_class(ch, "data.frame")
expect_equal(nrow(ch), 3L)
expect_setequal(names(ch),
c("k", "dissimilarity", "method",
"silhouette", "mean_within_dist",
"min_size", "max_size", "size_ratio"))
expect_equal(ch$k, 2:4)
expect_true(all(ch$dissimilarity == "hamming"))
expect_true(all(ch$method == "ward.D2"))
})
test_that("dissimilarity-only sweep returns one row per dissimilarity", {
d <- .make_choice_data()
diss <- c("hamming", "lcs", "jaccard")
ch <- cluster_choice(d, k = 3, dissimilarity = diss, method = "ward.D2")
expect_equal(nrow(ch), length(diss))
expect_setequal(ch$dissimilarity, diss)
expect_true(all(ch$k == 3))
})
test_that("method-only sweep returns one row per method", {
d <- .make_choice_data()
meths <- c("ward.D2", "complete", "average")
ch <- cluster_choice(d, k = 3, method = meths,
dissimilarity = "hamming")
expect_equal(nrow(ch), length(meths))
expect_setequal(ch$method, meths)
})
test_that("k x dissimilarity grid produces the cartesian product", {
d <- .make_choice_data()
diss <- c("hamming", "lcs")
ks <- 2:4
ch <- cluster_choice(d, k = ks, dissimilarity = diss,
method = "ward.D2")
expect_equal(nrow(ch), length(ks) * length(diss))
# Each (k, dissimilarity) appears exactly once
combos <- paste(ch$k, ch$dissimilarity, sep = "_")
expect_equal(length(unique(combos)), length(combos))
})
# ==============================================================================
# "all" sentinel
# ==============================================================================
test_that("dissimilarity = 'all' expands to the 9 canonical metrics", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 3, dissimilarity = "all",
method = "ward.D2")
full <- c("hamming", "osa", "lv", "dl", "lcs", "qgram",
"cosine", "jaccard", "jw")
expect_equal(nrow(ch), length(full))
expect_setequal(ch$dissimilarity, full)
})
test_that("method = 'all' expands to the 9 canonical methods", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 3, method = "all",
dissimilarity = "hamming")
full <- c("pam", "ward.D2", "ward.D", "complete", "average",
"single", "mcquitty", "median", "centroid")
expect_equal(nrow(ch), length(full))
expect_setequal(ch$method, full)
})
test_that("'all' mixed with explicit names deduplicates", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 3,
dissimilarity = c("hamming", "all"),
method = "ward.D2")
full <- c("hamming", "osa", "lv", "dl", "lcs", "qgram",
"cosine", "jaccard", "jw")
expect_equal(nrow(ch), length(full))
expect_setequal(ch$dissimilarity, full)
})
# ==============================================================================
# Per-row math
# ==============================================================================
test_that("silhouette matches a manual build_clusters() call", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 3, method = "ward.D2",
dissimilarity = "hamming", seed = 1)
ref <- build_clusters(d, k = 3, method = "ward.D2",
dissimilarity = "hamming", seed = 1)
expect_equal(ch$silhouette, ref$silhouette, tolerance = 1e-8)
# Sizes also align
expect_equal(c(ch$min_size, ch$max_size),
c(min(ref$sizes), max(ref$sizes)))
})
test_that("each swept row matches the corresponding build_clusters fit", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:3, method = "ward.D2",
dissimilarity = "hamming", seed = 1)
ref2 <- build_clusters(d, k = 2, method = "ward.D2",
dissimilarity = "hamming", seed = 1)
ref3 <- build_clusters(d, k = 3, method = "ward.D2",
dissimilarity = "hamming", seed = 1)
expect_equal(ch$silhouette, c(ref2$silhouette, ref3$silhouette),
tolerance = 1e-8)
expect_equal(ch$min_size, c(min(ref2$sizes), min(ref3$sizes)))
expect_equal(ch$max_size, c(max(ref2$sizes), max(ref3$sizes)))
expect_equal(ch$size_ratio,
c(max(ref2$sizes) / min(ref2$sizes),
max(ref3$sizes) / min(ref3$sizes)))
})
test_that("size_ratio is max_size / min_size", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2")
expect_equal(ch$size_ratio, ch$max_size / ch$min_size,
tolerance = 1e-9)
})
# ==============================================================================
# weighted validation
# ==============================================================================
test_that("weighted = TRUE with non-hamming sweep is rejected up-front", {
d <- .make_choice_data()
expect_error(
cluster_choice(d, k = 3,
dissimilarity = c("hamming", "lcs"),
weighted = TRUE),
"weighted = TRUE requires dissimilarity"
)
})
test_that("k sweep rejects non-whole or non-finite values before fitting", {
d <- .make_choice_data()
expect_error(cluster_choice(d, k = c(2, 2.9)),
"'k' must be a non-empty vector of whole finite numbers")
expect_error(cluster_choice(d, k = NA_real_),
"'k' must be a non-empty vector of whole finite numbers")
})
# ==============================================================================
# Print
# ==============================================================================
test_that("print: any swept axis triggers the (sweep: ...) header", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
out <- capture.output(print(ch))
expect_match(out[1L], "^Cluster Choice \\(sweep: k\\)")
})
test_that("print: zero-axis sweep (everything fixed) reads '[<method> / <dissim>]'", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 3, method = "ward.D2",
dissimilarity = "hamming")
out <- capture.output(print(ch))
expect_match(out[1L],
"^Cluster Choice \\[ward\\.D2 / hamming\\]")
})
test_that("print: multi-axis header reads '(sweep: ...)' and shows axis names", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:3, dissimilarity = c("hamming", "lcs"),
method = "ward.D2")
out <- capture.output(print(ch))
expect_match(out[1L], "^Cluster Choice \\(sweep:")
expect_true(any(grepl("k", out[1L])))
expect_true(any(grepl("dissimilarity", out[1L])))
})
test_that("print: silhouette-best row carries the marker", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
out <- capture.output(print(ch))
expect_true(any(grepl("<-- best", out)))
})
test_that("print: constant axis columns drop out of the table", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
out <- capture.output(print(ch))
# data.frame header for the table: should include `k` and metric
# columns, but not the constant axes.
data_hdr <- out[grep("^\\s*k\\s+silhouette", out)]
expect_length(data_hdr, 1L)
expect_false(grepl("dissimilarity", data_hdr))
expect_false(grepl("method", data_hdr))
})
test_that("print rejects unsupported dots and invalid digits", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
expect_error(print(ch, typo_arg = TRUE),
"unsupported argument: typo_arg")
expect_error(print(ch, digits = NA_real_),
"'digits' must be a single non-negative whole")
expect_error(print(ch, digits = 1.5),
"'digits' must be a single non-negative whole")
})
# ==============================================================================
# Summary
# ==============================================================================
test_that("summary returns data.frame with $best on silhouette-max row", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
s <- summary(ch)
expect_s3_class(s, "data.frame")
expect_true("best" %in% names(s))
best_idx <- which(s$best == "silhouette")
expect_length(best_idx, 1L)
expect_equal(best_idx, which.max(ch$silhouette))
})
test_that("summary rejects unsupported dots", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
expect_error(summary(ch, typo_arg = TRUE),
"unsupported argument: typo_arg")
})
# ==============================================================================
# Plot
# ==============================================================================
test_that("plot(type = 'auto') returns a ggplot for each sweep shape", {
d <- .make_choice_data()
ch1 <- cluster_choice(d, k = 2:4, method = "ward.D2")
expect_true(inherits(plot(ch1), "ggplot"))
ch2 <- cluster_choice(d, k = 3,
dissimilarity = c("hamming", "lcs", "jaccard"))
expect_true(inherits(plot(ch2), "ggplot"))
ch3 <- cluster_choice(d, k = 2:4,
dissimilarity = c("hamming", "lcs"))
expect_true(inherits(plot(ch3), "ggplot"))
})
# ==============================================================================
# Explicit type dispatch
# ==============================================================================
test_that("plot accepts each explicit type when the data supports it", {
d <- .make_choice_data()
ch_full <- cluster_choice(d, k = 2:4,
dissimilarity = c("hamming", "lcs"),
method = c("ward.D2", "complete"))
for (t in c("lines", "bars", "tradeoff", "facet", "heatmap")) {
p <- if (t == "heatmap") {
# heatmap doesn't use the k axis -- pass a fixed-k object.
plot(cluster_choice(d, k = 4,
dissimilarity = c("hamming", "lcs"),
method = c("ward.D2", "complete")),
type = t)
} else {
plot(ch_full, type = t)
}
expect_true(inherits(p, "ggplot"),
info = sprintf("type = %s", t))
}
})
test_that("plot type validation: unsupported type points at the alternative", {
d <- .make_choice_data()
# heatmap requires both dissim and method swept
ch_one <- cluster_choice(d, k = 2:4, method = "ward.D2",
dissimilarity = "hamming")
expect_error(plot(ch_one, type = "heatmap"),
"requires both dissimilarity and method")
# facet requires k + 2 categoricals
expect_error(plot(ch_one, type = "facet"),
"requires k plus two categorical")
# lines requires k swept
ch_fixed_k <- cluster_choice(d, k = 4,
dissimilarity = c("hamming", "lcs"))
expect_error(plot(ch_fixed_k, type = "lines"),
"requires k to be swept")
})
test_that("plot rejects unsupported dots and invalid abbrev", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 2:4, method = "ward.D2")
expect_error(plot(ch, typo_arg = TRUE),
"unsupported argument: typo_arg")
expect_error(plot(ch, abbrev = NA),
"'abbrev' must be TRUE or FALSE")
expect_error(plot(ch, abbrev = c(TRUE, FALSE)),
"'abbrev' must be TRUE or FALSE")
})
# ==============================================================================
# abbrev display
# ==============================================================================
test_that("abbrev = TRUE shortens dissimilarity / method labels", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 4,
dissimilarity = c("hamming", "cosine", "jaccard"),
method = c("ward.D2", "complete"))
p_short <- plot(ch, type = "heatmap", abbrev = TRUE)
# ggplot stores the data for the geoms; the displayed dissimilarity
# column should now carry abbreviations.
expect_true(inherits(p_short, "ggplot"))
expect_setequal(unique(p_short$data$dissimilarity),
c("ham", "cos", "jac"))
expect_setequal(unique(p_short$data$method),
c("wD2", "cmp"))
# abbrev = FALSE leaves the canonical names unchanged.
p_long <- plot(ch, type = "heatmap", abbrev = FALSE)
expect_setequal(unique(p_long$data$dissimilarity),
c("hamming", "cosine", "jaccard"))
expect_setequal(unique(p_long$data$method),
c("ward.D2", "complete"))
})
test_that("abbrev does not mutate the underlying cluster_choice object", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 4,
dissimilarity = c("hamming", "cosine"),
method = "ward.D2")
before <- ch
plot(ch, type = "bars", abbrev = TRUE)
expect_identical(ch, before)
})
# ==============================================================================
# print no longer carries the editorial within_dist note
# ==============================================================================
test_that("print does not emit interpretive 'units of dissimilarity' note", {
d <- .make_choice_data()
ch <- cluster_choice(d, k = 4,
dissimilarity = c("hamming", "cosine", "jaccard"))
out <- capture.output(print(ch))
# The footer note we used to print would have read e.g. 'within_dist is
# in the units of...'. Make sure no such guidance leaks back in.
expect_false(any(grepl("units of", out)))
expect_false(any(grepl("apples to", out)))
expect_false(any(grepl("only meaningful", out)))
})
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.