tests/testthat/test-iptw.R

# Tests for IPTW functionality

# Create test data (keeping this synthetic for testing purposes)
create_iptw_test_data <- function(n = 500) {
  set.seed(42)

  # Generate correlated covariates similar to cachar_sample structure
  age <- round(rnorm(n, 45, 12))
  age <- pmax(18, pmin(75, age))  # Realistic age range

  sex <- factor(sample(c("male", "female"), n, replace = TRUE, prob = c(0.76, 0.24)),
                levels = c("male", "female"))

  residence <- factor(sample(c("rural", "urban", "urban slum"), n, replace = TRUE, prob = c(0.87, 0.10, 0.03)),
                      levels = c("rural", "urban", "urban slum"))

  # Generate treatment with realistic propensity score model (areca nut use)
  logit_ps <- -1.2 + 0.02 * (age - 45) + 0.3 * (sex == "male") +
    0.2 * (residence == "urban") + rnorm(n, 0, 0.3)
  ps_true <- plogis(logit_ps)
  areca_nut <- factor(rbinom(n, 1, ps_true), levels = c(0, 1), labels = c("No", "Yes"))

  # Generate outcome (abnormal screening)
  logit_outcome <- -2.8 + 0.8 * (areca_nut == "Yes") + 0.015 * (age - 45) +
    0.4 * (sex == "male") + 0.2 * (residence == "urban")
  abnormal_screen <- rbinom(n, 1, plogis(logit_outcome))

  data.frame(
    id = 1:n,
    age = age,
    sex = sex,
    residence = residence,
    areca_nut = areca_nut,
    abnormal_screen = abnormal_screen
  )
}

test_that("calc_iptw_weights works with basic inputs", {
  data <- create_iptw_test_data()

  result <- calc_iptw_weights(
    data = data,
    treatment = "areca_nut",
    covariates = c("age", "sex", "residence")
  )

  expect_s3_class(result, "iptw_result")
  expect_true(is.list(result))
  expect_true(all(c("data", "ps_model", "weights", "ps", "diagnostics") %in% names(result)))
  expect_equal(length(result$weights), nrow(data))
  expect_true(all(result$weights > 0))
})

test_that("calc_iptw_weights handles different weight types", {
  data <- create_iptw_test_data()

  # Test ATE weights
  result_ate <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), weight_type = "ATE")
  expect_equal(result_ate$weight_type, "ATE")

  # Test ATT weights
  result_att <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), weight_type = "ATT")
  expect_equal(result_att$weight_type, "ATT")

  # Test ATC weights
  result_atc <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), weight_type = "ATC")
  expect_equal(result_atc$weight_type, "ATC")

  # Weights should be different
  expect_false(identical(result_ate$weights, result_att$weights))
  expect_false(identical(result_ate$weights, result_atc$weights))
})

test_that("calc_iptw_weights handles different propensity score methods", {
  data <- create_iptw_test_data()

  # Test logistic (default)
  result_logit <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), method = "logistic")
  expect_equal(result_logit$method, "logistic")

  # Test probit
  result_probit <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), method = "probit")
  expect_equal(result_probit$method, "probit")

  # Test cloglog
  result_cloglog <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), method = "cloglog")
  expect_equal(result_cloglog$method, "cloglog")
})

test_that("calc_risk_diff_iptw works with basic inputs", {
  data <- create_iptw_test_data()

  result <- calc_risk_diff_iptw(
    data = data,
    outcome = "abnormal_screen",
    treatment = "areca_nut",
    covariates = c("age", "sex", "residence")
  )

  expect_s3_class(result, "riskdiff_iptw_result")
  expect_true(is.data.frame(result))
  expect_equal(nrow(result), 1)
  expect_true(all(c("treatment_var", "rd_iptw", "ci_lower", "ci_upper", "p_value", "weight_type") %in% names(result)))
  expect_true(!is.na(result$rd_iptw))
})

test_that("calc_risk_diff_iptw works with pre-calculated weights", {
  data <- create_iptw_test_data()

  # Calculate weights separately
  iptw_result <- calc_iptw_weights(data, "areca_nut", c("age", "sex"))

  # Use pre-calculated weights
  result <- calc_risk_diff_iptw(
    data = data,
    outcome = "abnormal_screen",
    treatment = "areca_nut",
    covariates = c("age", "sex"),
    iptw_weights = iptw_result$weights
  )

  expect_s3_class(result, "riskdiff_iptw_result")
  expect_true(!is.na(result$rd_iptw))
})

test_that("calc_risk_diff_iptw works with bootstrap CI", {
  data <- create_iptw_test_data(n = 200)  # Smaller sample for faster testing

  result <- calc_risk_diff_iptw(
    data = data,
    outcome = "abnormal_screen",
    treatment = "areca_nut",
    covariates = c("age", "sex"),
    bootstrap_ci = TRUE,
    boot_n = 100  # Small number for testing
  )

  expect_s3_class(result, "riskdiff_iptw_result")
  expect_true(attr(result, "bootstrap"))
  expect_equal(attr(result, "boot_n"), 100)
})

test_that("check_iptw_assumptions works", {
  data <- create_iptw_test_data()

  iptw_result <- calc_iptw_weights(data, "areca_nut", c("age", "sex"))
  assumptions <- check_iptw_assumptions(iptw_result, verbose = FALSE)

  expect_s3_class(assumptions, "iptw_assumptions")
  expect_true(is.list(assumptions))
  expect_true(all(c("overall_assessment", "positivity", "balance", "weights", "recommendations") %in% names(assumptions)))
  expect_true(assumptions$overall_assessment %in% c("PASS", "CAUTION", "FAIL"))
})

test_that("IPTW functions validate inputs correctly", {
  data <- create_iptw_test_data()

  # Missing treatment variable
  expect_error(
    calc_iptw_weights(data, "missing_treatment", c("age", "sex")),
    "Variables not found"
  )

  # Invalid weight type
  expect_error(
    calc_iptw_weights(data, "areca_nut", c("age", "sex"), weight_type = "invalid"),
    "must be one of"
  )

  # Invalid method
  expect_error(
    calc_iptw_weights(data, "areca_nut", c("age", "sex"), method = "invalid"),
    "must be one of"
  )

  # Wrong length weights
  expect_error(
    calc_risk_diff_iptw(data, "abnormal_screen", "areca_nut", c("age", "sex"),
                        iptw_weights = c(1, 2, 3)),
    "Length of iptw_weights"
  )
})

test_that("IPTW handles edge cases gracefully", {
  # Small dataset
  small_data <- create_iptw_test_data(n = 50)

  result <- calc_iptw_weights(small_data, "areca_nut", c("age", "sex"))
  expect_s3_class(result, "iptw_result")

  # Dataset with missing values
  data_missing <- create_iptw_test_data()
  data_missing$age[1:50] <- NA

  result_missing <- calc_iptw_weights(data_missing, "areca_nut", c("age", "sex"))
  expect_true(nrow(result_missing$data) < nrow(data_missing))
})

test_that("print methods work for IPTW objects", {
  data <- create_iptw_test_data()

  # Test iptw_result print
  iptw_result <- calc_iptw_weights(data, "areca_nut", c("age", "sex"))
  expect_output(print(iptw_result), "Inverse Probability of Treatment Weighting")
  expect_output(print(iptw_result), "Propensity Score Model")

  # Test riskdiff_iptw_result print
  rd_result <- calc_risk_diff_iptw(data, "abnormal_screen", "areca_nut", c("age", "sex"))
  expect_output(print(rd_result), "IPTW-Standardized Risk Difference")
  expect_output(print(rd_result), "Risk in Treated")

  # Test summary method
  expect_output(summary(rd_result), "IPTW Risk Difference Analysis Summary")
})

test_that("stabilized vs unstabilized weights differ appropriately", {
  data <- create_iptw_test_data()

  # Unstabilized weights
  result_unstab <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), stabilize = FALSE)

  # Stabilized weights
  result_stab <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), stabilize = TRUE)

  # Weights should be different
  expect_false(identical(result_unstab$weights, result_stab$weights))

  # Stabilized weights should generally have smaller variance
  expect_true(stats::var(result_stab$weights) <= stats::var(result_unstab$weights))
})

test_that("weight trimming works correctly", {
  data <- create_iptw_test_data()

  # Without trimming
  result_no_trim <- calc_iptw_weights(data, "areca_nut", c("age", "sex"), trim_weights = FALSE)

  # With trimming
  result_trim <- calc_iptw_weights(data, "areca_nut", c("age", "sex"),
                                   trim_weights = TRUE, trim_quantiles = c(0.05, 0.95))

  # Range of trimmed weights should be smaller or equal
  expect_true(diff(range(result_trim$weights)) <= diff(range(result_no_trim$weights)))

  # Check that extreme values are actually trimmed
  q05 <- stats::quantile(result_no_trim$weights, 0.05)
  q95 <- stats::quantile(result_no_trim$weights, 0.95)

  expect_true(all(result_trim$weights >= q05))
  expect_true(all(result_trim$weights <= q95))
})

test_that("create_balance_plots handles missing ggplot2 gracefully", {
  data <- create_iptw_test_data()
  iptw_result <- calc_iptw_weights(data, "areca_nut", c("age", "sex"))

  # This should work whether ggplot2 is available or not
  plots <- create_balance_plots(iptw_result, plot_type = "love")

  # Should return something (either ggplot object or message)
  expect_true(!is.null(plots))
})

test_that("different estimands give sensible results", {
  data <- create_iptw_test_data()

  # Calculate all three estimands
  rd_ate <- calc_risk_diff_iptw(data, "abnormal_screen", "areca_nut", c("age", "sex"), weight_type = "ATE")
  rd_att <- calc_risk_diff_iptw(data, "abnormal_screen", "areca_nut", c("age", "sex"), weight_type = "ATT")
  rd_atc <- calc_risk_diff_iptw(data, "abnormal_screen", "areca_nut", c("age", "sex"), weight_type = "ATC")

  # All should be valid
  expect_false(is.na(rd_ate$rd_iptw))
  expect_false(is.na(rd_att$rd_iptw))
  expect_false(is.na(rd_atc$rd_iptw))

  # They don't have to be equal (and usually won't be)
  expect_true(is.numeric(rd_ate$rd_iptw))
  expect_true(is.numeric(rd_att$rd_iptw))
  expect_true(is.numeric(rd_atc$rd_iptw))
})

test_that("alpha level affects confidence intervals appropriately", {
  data <- create_iptw_test_data()

  # 95% CI
  result_95 <- calc_risk_diff_iptw(data, "abnormal_screen", "areca_nut", c("age", "sex"), alpha = 0.05)

  # 90% CI
  result_90 <- calc_risk_diff_iptw(data, "abnormal_screen", "areca_nut", c("age", "sex"), alpha = 0.10)

  # 90% CI should be narrower
  width_95 <- result_95$ci_upper - result_95$ci_lower
  width_90 <- result_90$ci_upper - result_90$ci_lower

  expect_true(width_90 < width_95)
  expect_equal(attr(result_90, "alpha"), 0.10)
})

Try the riskdiff package in your browser

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

riskdiff documentation built on June 30, 2025, 9:07 a.m.