tests/testthat/test-models_gipsMultQDA.R

# ==============================================================================
# Global Setup for this file
# ==============================================================================
data(iris)

# Split into Matrix (X) and Grouping Factor (Y) for testing matrix interfaces
X <- as.matrix(iris[, 1:4])
Y <- iris$Species

# ==============================================================================
# 1. Testing Model Fitting Interfaces (S3 Dispatch)
# ==============================================================================

test_that("gipsmultqda formula interface works correctly", {
  fit_formula <- gipsmultqda(Species ~ ., data = iris)

  # Check class and type
  expect_s3_class(fit_formula, "gipsmultqda")
  expect_type(fit_formula, "list")

  # Check if the result contains key components
  required_components <- c("prior", "means", "scaling", "counts")
  expect_true(all(required_components %in% names(fit_formula)))

  # Check dimensions of the means (3 classes x 4 variables)
  expect_equal(dim(fit_formula$means), c(3, 4))
})

test_that("gipsmultqda matrix interface works and matches formula", {
  # Re-fit formula for comparison
  fit_formula <- gipsmultqda(Species ~ ., data = iris)

  # Action
  fit_matrix <- gipsmultqda(x = X, grouping = Y)

  expect_s3_class(fit_matrix, "gipsmultqda")

  # The results between formula and matrix interface should be consistent
  expect_equal(fit_matrix$prior, fit_formula$prior)
  expect_equal(fit_matrix$counts, fit_formula$counts)
})

test_that("gipsmultqda data.frame interface works", {
  fit_df <- gipsmultqda(x = iris[, 1:4], grouping = Y)
  expect_s3_class(fit_df, "gipsmultqda")
})

test_that("gipsmultqda handles custom arguments", {
  fit_opts <- gipsmultqda(X, Y, MAP = FALSE, weighted_avg = TRUE)
  expect_s3_class(fit_opts, "gipsmultqda")
})

# ==============================================================================
# 2. Testing the 'print' method
# ==============================================================================

test_that("print.gipsmultqda works", {
  fit_formula <- gipsmultqda(Species ~ ., data = iris)

  # expect_output asserts that something is printed to the console
  expect_output(print(fit_formula))
})

# ==============================================================================
# 3. Testing the 'predict' method
# ==============================================================================

test_that("predict.gipsmultqda works correctly", {
  # Setup
  fit_formula <- gipsmultqda(Species ~ ., data = iris)

  # Action
  pred <- predict(fit_formula, newdata = iris)

  expect_type(pred, "list")

  # Check components
  expect_equal(names(pred), c("class", "posterior"))

  # Check dimensions matching the input data
  expect_length(pred$class, nrow(iris))
  expect_equal(nrow(pred$posterior), nrow(iris))
  expect_equal(ncol(pred$posterior), 3) # 3 classes

  # Check if posterior probabilities sum to 1 (row-wise)
  row_sums <- rowSums(pred$posterior)

  # expect_equal with a vector handles tolerance automatically
  expect_equal(unname(row_sums), rep(1, nrow(iris)),
    tolerance = 1e-6,
    ignore_attr = TRUE
  )
})

test_that("predict.gipsmultqda works on new subset", {
  # Setup
  fit_formula <- gipsmultqda(Species ~ ., data = iris)
  new_data <- iris[1:5, ]

  # Action
  pred_small <- predict(fit_formula, newdata = new_data)

  # Assert
  expect_length(pred_small$class, 5)
})

# ==============================================================================
# 4. Testing Input Validation and Error Handling (Covering gipsmultqda.default)
# ==============================================================================

test_that("gipsmultqda throws errors for invalid inputs", {
  # 1. Test infinite/NA values check
  X_na <- X
  X_na[1, 1] <- NA
  expect_error(gipsmultqda(X_na, Y), "infinite, NA or NaN values in 'x'")

  X_inf <- X
  X_inf[1, 1] <- Inf
  expect_error(gipsmultqda(X_inf, Y), "infinite, NA or NaN values in 'x'")

  # 2. Test dimension mismatch
  expect_error(
    gipsmultqda(X[1:10, ], Y),
    "nrow\\(x\\) and length\\(grouping\\) are different"
  )

  # 3. Test 'x' is not a matrix (passed as vector without dim)
  expect_error(gipsmultqda.default(1:10, rep(1, 10)), "'x' is not a matrix")

  # 4. Test small group size
  # Create a dataset where one group has fewer samples than variables (p=4)
  # Iris has 50 per group. Let's make a tiny subset.
  # We need counts < p+1 (so < 5).
  small_X <- X[c(1:4, 51:54, 101:104), ]
  small_Y <- factor(c(rep("s", 4), rep("ve", 4), rep("vi", 4)))
  expect_no_error(gipsmultqda(small_X, small_Y))
})

test_that("gipsmultqda validates priors", {
  # 1. Priors don't sum to 1
  expect_error(gipsmultqda(X, Y, prior = c(0.5, 0.5, 0.5)), "invalid 'prior'")

  # 2. Negative priors
  expect_error(gipsmultqda(X, Y, prior = c(1.2, -0.1, -0.1)), "invalid 'prior'")

  # 3. Incorrect length
  expect_error(
    gipsmultqda(X, Y, prior = c(0.5, 0.5)),
    "'prior' is of incorrect length"
  )
})

# ==============================================================================
# 5. Testing Matrix Interface Specifics (Subset & NA Action)
# ==============================================================================

test_that("gipsmultqda.matrix handles subset and na.action", {
  subset_idx <- c(1:10, 51:60, 101:110)
  fit_sub <- gipsmultqda(X, Y, subset = subset_idx)
  expect_equal(fit_sub$N, 30)

  X_dirty <- X
  X_dirty[1, 1] <- NA
  safe_na_omit <- function(object) {
    attr(object, "row.names") <- seq_along(object$g)
    stats::na.omit(object)
  }
  fit_na <- gipsmultqda(X_dirty, Y, na.action = safe_na_omit)

  expect_equal(fit_na$N, 149)
})

# ==============================================================================
# 6. Testing Optimizer Logic
# ==============================================================================

test_that("optimizer selection logic works", {
  # 1. Default BF for p < 10 (Iris has p=4)
  fit_bf <- gipsmultqda(X, Y, optimizer = NULL)
  # Check internals if available, or just ensure it runs without warning
  expect_s3_class(fit_bf, "gipsmultqda")

  # 2. Manual MH optimizer selection
  fit_mh <- gipsmultqda(X, Y, optimizer = "MH", max_iter = 10)
  expect_s3_class(fit_mh, "gipsmultqda")

  # 3. Warning when MH is chosen but max_iter is missing
  expect_warning(
    gipsmultqda(X, Y, optimizer = "MH"),
    "MH optimizer set but 'max_iter' argument is unspecified"
  )
})

# ==============================================================================
# 7. Testing Prediction Methods & Edge Cases
# ==============================================================================

test_that("predict handles different methods", {
  fit <- gipsmultqda(X, Y)

  # 1. Predictive method (corresponds to 'else' block in predict function)
  pred_pred <- predict(fit, newdata = X, method = "predictive")
  expect_equal(nrow(pred_pred$posterior), 150)

  # 2. Debiased method
  pred_deb <- predict(fit, newdata = X, method = "debiased")
  expect_equal(nrow(pred_deb$posterior), 150)

  # 3. looCV (Leave-One-Out Cross Validation)
  # Note: looCV works on the training data, usually without 'newdata'
  pred_loo <- predict(fit, method = "looCV")
  expect_equal(nrow(pred_loo$posterior), 150)
})

test_that("predict throws errors for invalid scenarios", {
  fit <- gipsmultqda(X, Y)

  # 1. looCV with newdata provided (should fail)
  expect_error(
    predict(fit, newdata = X, method = "looCV"),
    "cannot have leave-one-out CV with 'newdata'"
  )

  # 2. Wrong dimensions in newdata (e.g., only 2 columns instead of 4)
  expect_error(predict(fit, newdata = X[, 1:2]), "wrong number of variables")

  # 3. Invalid prior in predict
  expect_error(
    predict(fit, newdata = X, prior = c(0.5, 0.5)),
    "'prior' is of incorrect length"
  )

  # 4. Variable name mismatch warning
  X_bad_names <- X
  colnames(X_bad_names) <- c("A", "B", "C", "D")
  expect_warning(
    predict(fit, newdata = X_bad_names),
    "variable names in 'newdata' do not match those in 'object'"
  )
})

test_that("predict works without newdata (re-substitution)", {
  # This tests the logic inside predict where it tries to reconstruct data
  # from the call object when newdata is missing.

  # Case A: Formula interface
  fit_f <- gipsmultqda(Species ~ ., data = iris)
  pred_f <- predict(fit_f) # No newdata
  expect_equal(nrow(pred_f$posterior), 150)

  # Case B: Matrix interface
  fit_m <- gipsmultqda(X, Y)
  pred_m <- predict(fit_m) # No newdata
  expect_equal(nrow(pred_m$posterior), 150)
})

test_that("predict reconstructs data when using subset (covers eval.parent)", {
  # 1. Defining subset indices
  subset_idx <- c(1:10, 51:60, 101:110)

  # 2. Creating matrix fit with subset
  fit_subset <- gipsmultqda(X, Y, subset = subset_idx)

  # 3. Without newdata, predict should reconstruct data using subset
  pred <- predict(fit_subset)

  expect_equal(nrow(pred$posterior), 30)
  expect_length(pred$class, 30)
})

Try the gipsDA package in your browser

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

gipsDA documentation built on Feb. 3, 2026, 5:07 p.m.