library(testthat) library(DecisionDrift) # --------------------------------------------------------------------------- # Shared helper # --------------------------------------------------------------------------- make_panel <- function(n = 20, w = 5, seed = 1L, trend = 0, group = FALSE) { set.seed(seed) p <- pmax(0.05, pmin(0.95, seq(0.3, 0.3 + trend * (w - 1), length.out = w))) dat <- data.frame( id = rep(seq_len(n), each = w), time = rep(seq_len(w), times = n), decision = rbinom(n * w, 1L, rep(p, n)) ) if (group) { dat$group <- rep(c("A", "B"), ceiling(n / 2), each = 1)[seq_len(n * w)] } dat } # --------------------------------------------------------------------------- # dd_build # --------------------------------------------------------------------------- test_that("dd_build returns drift_panel with correct metadata", { dp <- dd_build(make_panel(), id, time, decision) expect_s3_class(dp, "drift_panel") expect_equal(dp$n_units, 20L) expect_equal(dp$n_waves, 5L) expect_false(dp$has_group) expect_null(dp$event_time) }) test_that("dd_build stores group variable correctly", { dp <- dd_build(make_panel(group = TRUE), id, time, decision, group = group) expect_true(dp$has_group) expect_equal(dp$group_var, "group") }) test_that("dd_build stores event_time", { dp <- dd_build(make_panel(), id, time, decision, event_time = 3L) expect_equal(dp$event_time, 3L) }) test_that("dd_build drops units below min_waves and reports count", { dat <- make_panel(n = 10, w = 4) # Give units 1-3 only one wave each dat <- dat[!(dat$id %in% 1:3 & dat$time > 1L), ] dp <- dd_build(dat, id, time, decision, min_waves = 2L) expect_equal(dp$n_dropped, 3L) expect_equal(dp$n_units, 7L) }) test_that("dd_build rejects non-binary decision column", { dat <- make_panel() dat$decision[1L] <- 5L expect_error(dd_build(dat, id, time, decision), regexp = "0, 1") }) test_that("dd_build accepts logical decision column", { dat <- make_panel() dat$decision <- as.logical(dat$decision) dp <- dd_build(dat, id, time, decision) expect_s3_class(dp, "drift_panel") }) test_that("dd_build errors on missing required column", { dat <- make_panel() names(dat)[names(dat) == "decision"] <- "outcome" expect_error(dd_build(dat, id, time, decision)) }) # --------------------------------------------------------------------------- # dd_prevalence # --------------------------------------------------------------------------- test_that("dd_prevalence returns correct class and structure", { dp <- dd_build(make_panel(trend = 0.05), id, time, decision) prev <- dd_prevalence(dp) expect_s3_class(prev, "dd_prevalence") expect_true(is.numeric(prev$DDI)) expect_equal(nrow(prev$wave_rates), 5L) expect_true(all(c("time", "rate", "n") %in% names(prev$wave_rates))) }) test_that("dd_prevalence mean_rate is in [0, 1]", { dp <- dd_build(make_panel(), id, time, decision) prev <- dd_prevalence(dp) expect_gte(prev$mean_rate, 0) expect_lte(prev$mean_rate, 1) }) test_that("dd_prevalence bootstrap CI is ordered and non-null", { dp <- dd_build(make_panel(n = 40, w = 6, trend = 0.05, seed = 2L), id, time, decision) prev <- suppressWarnings(dd_prevalence(dp, bootstrap = TRUE, R = 100L)) expect_false(is.null(prev$DDI_ci)) expect_lt(prev$DDI_ci["lower"], prev$DDI_ci["upper"]) }) test_that("dd_prevalence smooth returns smooth tibble when requested", { dp <- dd_build(make_panel(n = 30, w = 10), id, time, decision) prev <- suppressWarnings(dd_prevalence(dp, smooth = TRUE)) expect_false(is.null(prev$smooth)) expect_true("smooth_rate" %in% names(prev$smooth)) }) # --------------------------------------------------------------------------- # dd_transition # --------------------------------------------------------------------------- test_that("dd_transition returns correct class and wave-pair count", { dp <- dd_build(make_panel(), id, time, decision) tr <- dd_transition(dp) expect_s3_class(tr, "dd_transition") expect_true(is.numeric(tr$TDI)) expect_equal(nrow(tr$transitions), 4L) # 5 waves -> 4 pairs }) test_that("dd_transition TDI is non-negative", { dp <- dd_build(make_panel(), id, time, decision) tr <- dd_transition(dp) expect_gte(tr$TDI, 0) }) test_that("dd_transition trend list has expected names", { dp <- dd_build(make_panel(), id, time, decision) tr <- dd_transition(dp, test_trend = TRUE) expect_true(all(c("p11", "p10", "p01", "p00") %in% names(tr$trends))) }) test_that("dd_transition with test_trend = FALSE returns NULL trends", { dp <- dd_build(make_panel(), id, time, decision) tr <- dd_transition(dp, test_trend = FALSE) expect_null(tr$trends) }) # --------------------------------------------------------------------------- # dd_entropy_trend # --------------------------------------------------------------------------- test_that("dd_entropy_trend works with binary method", { dp <- dd_build(make_panel(n = 25, w = 6), id, time, decision) et <- dd_entropy_trend(dp, method = "binary") expect_s3_class(et, "dd_entropy_trend") expect_true("entropy" %in% names(et$rolling_entropy)) }) test_that("dd_entropy_trend works with path method", { dp <- dd_build(make_panel(n = 25, w = 6), id, time, decision) et <- dd_entropy_trend(dp, method = "path", window = 3L) expect_s3_class(et, "dd_entropy_trend") }) test_that("dd_entropy_trend rejects invalid window", { dp <- dd_build(make_panel(), id, time, decision) expect_error(dd_entropy_trend(dp, window = 1L)) }) # --------------------------------------------------------------------------- # dd_group_drift # --------------------------------------------------------------------------- test_that("dd_group_drift errors without group variable", { dp <- dd_build(make_panel(), id, time, decision) expect_error(dd_group_drift(dp), regexp = "group") }) test_that("dd_group_drift returns correct class and GDD table", { dp <- dd_build(make_panel(group = TRUE), id, time, decision, group = group) gd <- dd_group_drift(dp) expect_s3_class(gd, "dd_group_drift") expect_true("GDD" %in% names(gd$GDD_table)) expect_true("direction" %in% names(gd$GDD_table)) }) test_that("dd_group_drift gap_trajectory has one row per wave", { dp <- dd_build(make_panel(w = 5, group = TRUE), id, time, decision, group = group) gd <- dd_group_drift(dp) expect_equal(nrow(gd$gap_trajectory), 5L) }) test_that("dd_group_drift errors on unknown reference group", { dp <- dd_build(make_panel(group = TRUE), id, time, decision, group = group) expect_error(dd_group_drift(dp, reference = "Z")) }) # --------------------------------------------------------------------------- # dd_changepoint # --------------------------------------------------------------------------- test_that("dd_changepoint returns correct class and components", { set.seed(5) p <- c(rep(0.2, 3), rep(0.8, 3)) dat <- data.frame( id = rep(1:30, each = 6), time = rep(1:6, times = 30), decision = rbinom(180, 1, rep(p, 30)) ) dp <- dd_build(dat, id, time, decision, event_time = 4L) cp <- dd_changepoint(dp) expect_s3_class(cp, "dd_changepoint") expect_false(is.null(cp$cusum)) expect_false(is.null(cp$segmented)) expect_false(is.null(cp$event)) }) test_that("dd_changepoint cusum break_wave is a valid time point", { set.seed(5) p <- c(rep(0.2, 3), rep(0.8, 3)) dat <- data.frame( id = rep(1:30, each = 6), time = rep(1:6, times = 30), decision = rbinom(180, 1, rep(p, 30)) ) dp <- dd_build(dat, id, time, decision) cp <- dd_changepoint(dp, method = "cusum") expect_true(cp$cusum$break_wave %in% dp$times) }) test_that("dd_changepoint runs with single method", { dp <- dd_build(make_panel(n = 30, w = 6), id, time, decision) cp <- dd_changepoint(dp, method = "cusum") expect_null(cp$segmented) expect_null(cp$event) }) # --------------------------------------------------------------------------- # dd_indices # --------------------------------------------------------------------------- test_that("dd_indices returns all four numeric values", { dp <- dd_build(make_panel(group = TRUE), id, time, decision, group = group) idx <- dd_indices(dp) expect_s3_class(idx, "dd_indices") for (nm in c("DDI", "TDI", "GDD", "CDB")) { expect_true(nm %in% names(idx)) expect_true(is.numeric(idx[[nm]])) } }) test_that("dd_indices GDD is NA when no group variable", { dp <- dd_build(make_panel(), id, time, decision) idx <- dd_indices(dp) expect_true(is.na(idx$GDD)) }) test_that("dd_indices CDB is non-negative", { dp <- dd_build(make_panel(), id, time, decision) idx <- dd_indices(dp) expect_gte(idx$CDB, 0) }) # --------------------------------------------------------------------------- # dd_robustness # --------------------------------------------------------------------------- test_that("dd_robustness returns correct class and fragility_index", { dp <- dd_build(make_panel(n = 25, trend = 0.04), id, time, decision) rob <- dd_robustness(dp, variants = c("lopo", "min_waves")) expect_s3_class(rob, "dd_robustness") expect_true(is.numeric(rob$fragility_index)) expect_gte(rob$fragility_index, 0) expect_lte(rob$fragility_index, 1) }) test_that("dd_robustness table has more rows than baseline alone", { dp <- dd_build(make_panel(n = 25, trend = 0.04), id, time, decision) rob <- dd_robustness(dp, variants = c("lopo", "min_waves")) expect_gt(nrow(rob$table), 1L) }) test_that("dd_robustness baseline DDI matches dd_prevalence DDI", { dp <- dd_build(make_panel(n = 25, trend = 0.04), id, time, decision) rob <- dd_robustness(dp, variants = "lopo") prev <- dd_prevalence(dp) expect_equal(rob$baseline$DDI, prev$DDI, tolerance = 1e-10) }) # --------------------------------------------------------------------------- # dd_sensitivity # --------------------------------------------------------------------------- test_that("dd_sensitivity returns correct class and tipping_point", { dp <- dd_build(make_panel(n = 25, trend = 0.04), id, time, decision) sen <- dd_sensitivity(dp, scenarios = "miscoding", miscoding_rates = c(0.05, 0.10), n_draws = 10L) expect_s3_class(sen, "dd_sensitivity") expect_true("miscoding" %in% names(sen$tipping_point)) }) test_that("dd_sensitivity table has expected columns", { dp <- dd_build(make_panel(n = 25), id, time, decision) sen <- dd_sensitivity(dp, scenarios = "missingness", missing_rates = 0.10, n_draws = 10L) expect_true(all(c("scenario", "level", "mean_DDI", "sd_DDI", "prop_sign_flip") %in% names(sen$table))) }) test_that("dd_sensitivity prop_sign_flip is in [0, 1]", { dp <- dd_build(make_panel(n = 25), id, time, decision) sen <- dd_sensitivity(dp, scenarios = "miscoding", miscoding_rates = 0.05, n_draws = 10L) expect_gte(min(sen$table$prop_sign_flip), 0) expect_lte(max(sen$table$prop_sign_flip), 1) }) # --------------------------------------------------------------------------- # dd_audit # --------------------------------------------------------------------------- test_that("dd_audit completes and returns a valid verdict", { dp <- dd_build(make_panel(n = 25, w = 6, trend = 0.05, group = TRUE), id, time, decision, group = group, event_time = 4L) aud <- dd_audit(dp, include_robustness = TRUE, include_sensitivity = FALSE, verbose = FALSE) expect_s3_class(aud, "dd_audit") expect_true(aud$verdict %in% c("no drift detected", "marginal drift", "moderate drift", "strong drift")) }) test_that("dd_audit with group returns non-null group_drift", { dp <- dd_build(make_panel(n = 25, w = 6, group = TRUE), id, time, decision, group = group) aud <- dd_audit(dp, include_robustness = FALSE, verbose = FALSE) expect_false(is.null(aud$group_drift)) }) test_that("dd_audit without group returns null group_drift", { dp <- dd_build(make_panel(n = 25, w = 6), id, time, decision) aud <- dd_audit(dp, include_robustness = FALSE, verbose = FALSE) expect_null(aud$group_drift) }) test_that("dd_audit with include_robustness = TRUE returns robustness", { dp <- dd_build(make_panel(n = 25, w = 6, trend = 0.05), id, time, decision) aud <- dd_audit(dp, include_robustness = TRUE, verbose = FALSE) expect_false(is.null(aud$robustness)) }) test_that("dd_audit with include_robustness = FALSE returns null robustness", { dp <- dd_build(make_panel(n = 25, w = 6), id, time, decision) aud <- dd_audit(dp, include_robustness = FALSE, verbose = FALSE) expect_null(aud$robustness) }) test_that("dd_audit all sub-components have expected classes", { dp <- dd_build(make_panel(n = 25, w = 6, trend = 0.04), id, time, decision) aud <- dd_audit(dp, include_robustness = FALSE, verbose = FALSE) expect_s3_class(aud$prevalence, "dd_prevalence") expect_s3_class(aud$transition, "dd_transition") expect_s3_class(aud$entropy_trend, "dd_entropy_trend") expect_s3_class(aud$changepoint, "dd_changepoint") expect_s3_class(aud$indices, "dd_indices") })