tests/testthat/test-pre_calibrated.R

# =============================================================================
# Tests for pre_calibrated parameter (TSQCA v1.3.0)
# =============================================================================
# Test coverage:
#   [BC] Backward compatibility  — pre_calibrated = NULL produces v1.2.0 output
#   [T]  Functional tests        — T-01 through T-06 from design spec
#   [V]  Validation tests        — V-01 through V-04 from design spec
#   [C]  Consistency test        — manual binarization equivalence
#   [E]  extract_mode="essential" x pre_calibrated (confirmation item 2)
# =============================================================================

# ---------------------------------------------------------------------------
# Helper: build test data with one pre-calibrated (fuzzy) column
# ---------------------------------------------------------------------------
make_test_dat <- function() {
  data("sample_data", package = "TSQCA")
  dat <- sample_data
  # X2_fuzzy: manually fuzzy-calibrate X2 (range ~1-10) into [0,1]
  # simple linear rescale so values stay in [0,1]
  x2_raw <- dat$X2
  dat$X2_fuzzy <- (x2_raw - min(x2_raw)) / (max(x2_raw) - min(x2_raw))
  # X3_bin: hard binary (already 0/1-like after threshold)
  dat$X3_bin <- ifelse(dat$X3 >= 7, 1L, 0L)
  dat
}

# =============================================================================
# [BC] Backward Compatibility Tests
# =============================================================================

test_that("[BC] otSweep: pre_calibrated=NULL produces identical output to v1.2.0", {
  data("sample_data", package = "TSQCA")
  dat <- sample_data
  thrX <- c(X1 = 7, X2 = 7, X3 = 7)

  res_default <- otSweep(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_range = 6:8, thrX = thrX, return_details = FALSE
  )
  res_null <- otSweep(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_range = 6:8, thrX = thrX,
    pre_calibrated = NULL, return_details = FALSE
  )

  expect_equal(res_default, res_null)
})

test_that("[BC] dtSweep: pre_calibrated=NULL produces identical output to v1.2.0", {
  data("sample_data", package = "TSQCA")
  dat <- sample_data

  res_default <- dtSweep(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_list_X = list(X1 = 6:7, X2 = 7, X3 = 7),
    sweep_range_Y = 6:7, return_details = FALSE
  )
  res_null <- dtSweep(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_list_X = list(X1 = 6:7, X2 = 7, X3 = 7),
    sweep_range_Y = 6:7,
    pre_calibrated = NULL, return_details = FALSE
  )

  expect_equal(res_default, res_null)
})

test_that("[BC] ctSweepS: pre_calibrated=NULL produces identical output to v1.2.0", {
  data("sample_data", package = "TSQCA")
  dat <- sample_data

  res_default <- ctSweepS(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_var = "X3", sweep_range = 6:8, thrY = 7, thrX_default = 7,
    return_details = FALSE
  )
  res_null <- ctSweepS(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_var = "X3", sweep_range = 6:8, thrY = 7, thrX_default = 7,
    pre_calibrated = NULL, return_details = FALSE
  )

  expect_equal(res_default, res_null)
})

test_that("[BC] ctSweepM: pre_calibrated=NULL produces identical output to v1.2.0", {
  data("sample_data", package = "TSQCA")
  dat <- sample_data

  res_default <- ctSweepM(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_list = list(X1 = 6:7, X2 = 7, X3 = 7),
    thrY = 7, return_details = FALSE
  )
  res_null <- ctSweepM(
    dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
    sweep_list = list(X1 = 6:7, X2 = 7, X3 = 7),
    thrY = 7, pre_calibrated = NULL, return_details = FALSE
  )

  expect_equal(res_default, res_null)
})

# =============================================================================
# [T] Functional Tests (T-01 to T-06)
# =============================================================================

test_that("[T-01] otSweep: single fuzzy condition runs and returns valid output", {
  dat <- make_test_dat()

  res <- otSweep(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2_fuzzy", "X3"),
    sweep_range = 6:8,
    thrX        = c(X1 = 7, X2_fuzzy = 0.5, X3 = 7),
    pre_calibrated = c("X2_fuzzy"),
    return_details = TRUE
  )

  expect_s3_class(res, "otSweep_result")
  expect_true(nrow(res$summary) > 0)
  expect_true(all(c("expression", "inclS", "covS") %in% names(res$summary)))
  # pre_calibrated stored in params
  expect_equal(res$params$pre_calibrated, c("X2_fuzzy"))
})

test_that("[T-01] otSweep: fuzzy condition produces different result from crisp binarization", {
  dat <- make_test_dat()
  thrX_crisp  <- c(X1 = 7, X2 = 7,   X3 = 7)
  thrX_fuzzy  <- c(X1 = 7, X2_fuzzy = 0.5, X3 = 7)

  res_crisp <- otSweep(
    dat = dat, outcome = "Y",
    conditions = c("X1", "X2", "X3"),
    sweep_range = 6:8, thrX = thrX_crisp, return_details = FALSE
  )
  res_fuzzy <- otSweep(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2_fuzzy", "X3"),
    sweep_range = 6:8, thrX = thrX_fuzzy,
    pre_calibrated = c("X2_fuzzy"), return_details = FALSE
  )

  # Results should differ because X2_fuzzy retains graded membership
  # (at least one expression or fit metric will differ)
  expect_false(identical(res_crisp$expression, res_fuzzy$expression) &&
               identical(res_crisp$inclS,      res_fuzzy$inclS) &&
               identical(res_crisp$covS,        res_fuzzy$covS))
})

test_that("[T-02] otSweep: all conditions fuzzy runs without error", {
  dat <- make_test_dat()
  dat$X1_fuzzy <- (dat$X1 - min(dat$X1)) / (max(dat$X1) - min(dat$X1))

  res <- otSweep(
    dat = dat, outcome = "Y",
    conditions  = c("X1_fuzzy", "X2_fuzzy", "X3_bin"),
    sweep_range = 6:8,
    thrX        = c(X1_fuzzy = 0.5, X2_fuzzy = 0.5, X3_bin = 1),
    pre_calibrated = c("X1_fuzzy", "X2_fuzzy", "X3_bin"),
    return_details = FALSE
  )

  expect_s3_class(res, "data.frame")
  expect_true(nrow(res) > 0)
})

test_that("[T-03] dtSweep: mixed fuzzy/crisp conditions run without error", {
  dat <- make_test_dat()

  res <- dtSweep(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2_fuzzy", "X3"),
    sweep_list_X  = list(X1 = 6:7, X2_fuzzy = 0.5, X3 = 7),
    sweep_range_Y = 6:7,
    pre_calibrated = c("X2_fuzzy"),
    return_details = FALSE
  )

  expect_s3_class(res, "data.frame")
  expect_true(nrow(res) > 0)
})

test_that("[T-04] ctSweepS: pre_calibrated non-swept conditions pass through", {
  dat <- make_test_dat()

  res <- ctSweepS(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2_fuzzy", "X3"),
    sweep_var   = "X1",
    sweep_range = 6:8,
    thrY        = 7, thrX_default = 7,
    pre_calibrated = c("X2_fuzzy"),
    return_details = TRUE
  )

  expect_s3_class(res, "ctSweepS_result")
  expect_true(nrow(res$summary) > 0)
  expect_equal(res$params$pre_calibrated, c("X2_fuzzy"))
})

test_that("[T-05] ctSweepM: pre_calibrated non-swept conditions pass through", {
  dat <- make_test_dat()

  res <- ctSweepM(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2_fuzzy", "X3"),
    sweep_list  = list(X1 = 6:7, X2_fuzzy = 0.5, X3 = 7),
    thrY        = 7,
    pre_calibrated = c("X2_fuzzy"),
    return_details = TRUE
  )

  expect_s3_class(res, "ctSweepM_result")
  expect_true(nrow(res$summary) > 0)
  expect_equal(res$params$pre_calibrated, c("X2_fuzzy"))
})

test_that("[T-06] Binary variable as pre_calibrated passes through unchanged", {
  dat <- make_test_dat()

  # X3_bin is already 0/1 — using pre_calibrated should give same result as
  # binarizing with thrX = 1
  res_pc <- otSweep(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2", "X3_bin"),
    sweep_range = 6:8,
    thrX        = c(X1 = 7, X2 = 7, X3_bin = 1),
    pre_calibrated = c("X3_bin"),
    return_details = FALSE
  )
  res_thr <- otSweep(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2", "X3_bin"),
    sweep_range = 6:8,
    thrX        = c(X1 = 7, X2 = 7, X3_bin = 1),
    return_details = FALSE
  )

  expect_equal(res_pc$expression, res_thr$expression)
  expect_equal(res_pc$inclS,      res_thr$inclS)
  expect_equal(res_pc$covS,       res_thr$covS)
})

# =============================================================================
# [V] Validation Tests (V-01 to V-04)
# =============================================================================

test_that("[V-01] Error when pre_calibrated variable not in conditions", {
  data("sample_data", package = "TSQCA")
  dat <- sample_data

  expect_error(
    otSweep(
      dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
      sweep_range = 6:8, thrX = c(X1 = 7, X2 = 7, X3 = 7),
      pre_calibrated = c("X99")
    ),
    regexp = "not found in conditions"
  )
})

test_that("[V-02] Error when pre_calibrated variable has values outside [0, 1]", {
  data("sample_data", package = "TSQCA")
  dat <- sample_data  # X1 has raw Likert values (e.g., 1-10)

  expect_error(
    otSweep(
      dat = dat, outcome = "Y", conditions = c("X1", "X2", "X3"),
      sweep_range = 6:8, thrX = c(X1 = 7, X2 = 7, X3 = 7),
      pre_calibrated = c("X1")  # X1 is raw scale, not [0,1]
    ),
    regexp = "outside \\[0, 1\\]"
  )
})

test_that("[V-03] Warning when pre_calibrated variable contains NA", {
  dat <- make_test_dat()
  dat$X2_fuzzy[1] <- NA  # inject NA

  expect_warning(
    otSweep(
      dat = dat, outcome = "Y",
      conditions  = c("X1", "X2_fuzzy", "X3"),
      sweep_range = 6:8,
      thrX        = c(X1 = 7, X2_fuzzy = 0.5, X3 = 7),
      pre_calibrated = c("X2_fuzzy")
    ),
    regexp = "contains NA"
  )
})

test_that("[V-04] Warning when pre_calibrated variable is also a sweep target in dtSweep", {
  dat <- make_test_dat()

  expect_warning(
    dtSweep(
      dat = dat, outcome = "Y",
      conditions  = c("X1", "X2_fuzzy", "X3"),
      sweep_list_X  = list(X1 = 6:7, X2_fuzzy = c(0.3, 0.5, 0.7), X3 = 7),
      sweep_range_Y = 6:7,
      pre_calibrated = c("X2_fuzzy")
    ),
    regexp = "sweep thresholds ignored"
  )
})

test_that("[V-04] Warning when pre_calibrated variable is also a sweep target in ctSweepM", {
  dat <- make_test_dat()

  expect_warning(
    ctSweepM(
      dat = dat, outcome = "Y",
      conditions  = c("X1", "X2_fuzzy", "X3"),
      sweep_list  = list(X1 = 6:7, X2_fuzzy = c(0.3, 0.5, 0.7), X3 = 7),
      thrY        = 7,
      pre_calibrated = c("X2_fuzzy")
    ),
    regexp = "sweep thresholds ignored"
  )
})

# =============================================================================
# [C] Consistency Test — manual binarization equivalence
# =============================================================================

test_that("[C] Manual binarization at 0.5 equals pre_calibrated=NULL with thrX=0.5", {
  dat <- make_test_dat()

  # Method A: pass fuzzy values, let qca_bin() binarize at 0.5 (default behavior)
  res_a <- otSweep(
    dat = dat, outcome = "Y",
    conditions  = c("X1", "X2_fuzzy", "X3"),
    sweep_range = 6:8,
    thrX        = c(X1 = 7, X2_fuzzy = 0.5, X3 = 7),
    pre_calibrated = NULL,   # binarize X2_fuzzy at 0.5
    return_details = FALSE
  )

  # Method B: manually binarize X2_fuzzy at 0.5 before passing
  dat_b <- dat
  dat_b$X2_fuzzy <- ifelse(dat$X2_fuzzy >= 0.5, 1L, 0L)
  res_b <- otSweep(
    dat = dat_b, outcome = "Y",
    conditions  = c("X1", "X2_fuzzy", "X3"),
    sweep_range = 6:8,
    thrX        = c(X1 = 7, X2_fuzzy = 1, X3 = 7),
    pre_calibrated = c("X2_fuzzy"),  # already 0/1, pass through
    return_details = FALSE
  )

  expect_equal(res_a$expression, res_b$expression)
  expect_equal(res_a$inclS,      res_b$inclS,      tolerance = 1e-10)
  expect_equal(res_a$covS,       res_b$covS,        tolerance = 1e-10)
})

# =============================================================================
# [E] extract_mode="essential" x pre_calibrated (confirmation item 2)
# =============================================================================

test_that("[E] extract_mode='essential' works with pre_calibrated fuzzy condition", {
  dat <- make_test_dat()

  res <- otSweep(
    dat = dat, outcome = "Y",
    conditions   = c("X1", "X2_fuzzy", "X3"),
    sweep_range  = 6:8,
    thrX         = c(X1 = 7, X2_fuzzy = 0.5, X3 = 7),
    pre_calibrated = c("X2_fuzzy"),
    extract_mode = "essential",
    return_details = TRUE
  )

  expect_s3_class(res, "otSweep_result")
  expect_true("n_solutions" %in% names(res$summary))
  # expression column must exist and be character (EPI or "No essential prime implicants")
  expect_true(is.character(res$summary$expression))
})

test_that("[E] extract_mode='all' works with pre_calibrated fuzzy condition", {
  dat <- make_test_dat()

  res <- otSweep(
    dat = dat, outcome = "Y",
    conditions   = c("X1", "X2_fuzzy", "X3"),
    sweep_range  = 6:8,
    thrX         = c(X1 = 7, X2_fuzzy = 0.5, X3 = 7),
    pre_calibrated = c("X2_fuzzy"),
    extract_mode = "all",
    return_details = TRUE
  )

  expect_s3_class(res, "otSweep_result")
  expect_true("n_solutions" %in% names(res$summary))
})

test_that("[E] EPI identification unaffected by variable being fuzzy vs crisp", {
  dat <- make_test_dat()

  # essential mode with pre_calibrated fuzzy variable
  res_fuzzy <- otSweep(
    dat = dat, outcome = "Y",
    conditions   = c("X1", "X2_fuzzy", "X3"),
    sweep_range  = 6:8,
    thrX         = c(X1 = 7, X2_fuzzy = 0.5, X3 = 7),
    pre_calibrated = c("X2_fuzzy"),
    extract_mode = "essential",
    return_details = FALSE
  )

  # expression column must not be NA for all rows
  non_na_count <- sum(!is.na(res_fuzzy$expression))
  expect_true(non_na_count > 0)
  # selective_terms and unique_terms columns must exist
  expect_true("selective_terms" %in% names(res_fuzzy))
  expect_true("unique_terms"    %in% names(res_fuzzy))
})

Try the TSQCA package in your browser

Any scripts or data that you put into this service are public.

TSQCA documentation built on Feb. 18, 2026, 5:06 p.m.