tests/testthat/test-tte_sim.R

# sim_accrual --------------------------------------------------------

test_that("sim_accrual returns correct number of values", {
  n <- 100
  result <- sim_accrual(n = n, accrual_periods = c(6, 8), accrual_props = c(0.5, 0.5))

  expect_equal(length(result), n)
  expect_true(is.numeric(result))
})

test_that("sim_accrual returns values within expected range", {
  # Test with multiple periods
  result <- sim_accrual(n = 1000,
                             accrual_periods = c(6, 8),
                             accrual_props = c(0.5, 0.5))

  expect_true(all(result >= 0))
  expect_true(all(result <= 8))

  # Values should be distributed across both periods
  period1_count <- sum(result < 6)
  period2_count <- sum(result >= 6)

  # Allow some flexibility due to randomness, but should be roughly 50/50
  expect_true(period1_count > 475)
  expect_true(period2_count > 475)
})

test_that("sim_accrual works with single accrual period", {
  result <- sim_accrual(n = 100,
                             accrual_periods = c(10),
                             accrual_props = c(1))

  expect_equal(length(result), 100)
  expect_true(all(result >= 0))
  expect_true(all(result <= 10))
})

test_that("sim_accrual works with unequal accrual proportions", {
  set.seed(123) # For reproducibility
  result <- sim_accrual(n = 1000,
                             accrual_periods = c(5, 10, 15),
                             accrual_props = c(0.2, 0.3, 0.5))

  # Roughly 20% should be in first period
  period1_count <- sum(result < 5)
  # Roughly 30% should be in second period
  period2_count <- sum(result >= 5 & result < 10)
  # Roughly 50% should be in third period
  period3_count <- sum(result >= 10)

  # Allow flexibility due to randomness
  expect_true(abs(period1_count/1000 - 0.2) < 0.05)
  expect_true(abs(period2_count/1000 - 0.3) < 0.05)
  expect_true(abs(period3_count/1000 - 0.5) < 0.05)
})


test_that("sim_accrual handles extreme probability distributions", {
  # Test with very skewed probability
  result <- sim_accrual(n = 1000,
                             accrual_periods = c(5, 10, 15),
                             accrual_props = c(0.01, 0.01, 0.98))

  # Most values should be in the third period (if perfect would be 980, there is wiggle room)
  expect_true(sum(result >= 10) > 950)
})

test_that("sim_accrual handles uneven inputs", {
  expect_error(sim_accrual(n = 1000,
                             accrual_periods = c(5, 10, 15),
                             accrual_props = c(0.01, 0.01)),
               "`accrual_periods` and `accrual_props` should have equal lengths")
})


# sim_pw_const_haz -------------------------------------------------------


test_that("sim_pw_const_haz returns correct number and type of values", {
  n <- 100
  hazard_periods <- c(5, 10)
  hazard_values <- c(0.1, 0.2, 0.3)

  # Run the function
  set.seed(123)
  result <- sim_pw_const_haz(n = n, hazard_periods = hazard_periods, hazard_values = hazard_values)

  # Check results
  expect_equal(length(result), n)
  expect_true(is.numeric(result))
  expect_true(all(result >= 0))
})

test_that("sim_pw_const_haz handles constant hazard case", {
  n <- 1000
  hazard_value <- 0.1

  # Run with single hazard value (constant hazard)
  set.seed(123)
  result <- sim_pw_const_haz(n = n, hazard_values = hazard_value)

  # For exponential distribution with rate 0.1, mean should be approximately 10
  expect_gt(mean(result), 9)
  expect_lt(mean(result), 11)
})

test_that("sim_pw_const_haz handles piecewise constant hazard", {
  n <- 1000
  hazard_periods <- c(5)
  hazard_values <- c(0.05, 0.8)  # Low hazard then high hazard

  # Run the function
  set.seed(123)
  result <- sim_pw_const_haz(n = n, hazard_periods = hazard_periods, hazard_values = hazard_values)

  # Count events in different periods
  early_events <- sum(result <= 5)
  late_events <- sum(result > 5)

  # With higher hazard in second period, expect more events per unit time
  # Calculate event density (events per unit time)
  max_time <- max(result)
  early_density <- early_events / 5
  late_density <- late_events / (max_time - 5)

  # Higher hazard should lead to higher event density
  expect_gt(late_density, early_density)
})

test_that("sim_pw_const_haz handles edge cases", {
  # Very high hazard (should result in early events)
  high_hazard_result <- sim_pw_const_haz(n = 100, hazard_values = 10)
  expect_true(mean(high_hazard_result) < 1)  # Mean should be less than 1 with hazard=10

  # Very low hazard (should result in later events)
  low_hazard_result <- sim_pw_const_haz(n = 100, hazard_values = 0.01)
  expect_true(mean(low_hazard_result) > 50)  # Mean should be greater than 50 with hazard=0.01
})

test_that("sim_pw_const_haz validates inputs", {
  # Number of hazard values should match number of periods + 1
  expect_error(
    sim_pw_const_haz(n = 100, hazard_periods = c(5, 10), hazard_values = c(0.1, 0.2)),
    "`hazard_values` should have length equal to one more than the length of `hazard_periods`"
  )

  # Negative hazard values should error
  expect_error(
    sim_pw_const_haz(n = 100, hazard_values = -0.1),
    "`hazard_values` and `hazard_periods` can only have positive values"
  )
})


# sim_weib_ph ----------------------------------------------------


test_that("sim_weib_ph returns correct format", {
  # Create a simple survreg Weibull model
  set.seed(123)
  n <- 50
  df <- data.frame(
    time = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )
  weib_model <- survival::survreg(survival::Surv(time, event) ~ cov1 + cov2, data = df, dist = "weibull")

  # Create sample data frame
  samp_df <- data.frame(
    cov1 = rnorm(10),
    cov2 = rbinom(10, 1, 0.5)
  )

  # Run the function
  result <- sim_weib_ph(weibull_ph_mod = weib_model, samp_df = samp_df)

  # Check results
  expect_length(result, nrow(samp_df))
  expect_true(all(result > 0))
  expect_type(result, "double")
})

test_that("sim_weib_ph handles conditional effects correctly", {
  # Create a simple survreg Weibull model
  set.seed(123)
  n <- 50
  df <- data.frame(
    time = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )
  weib_model <- survival::survreg(survival::Surv(time, event) ~ cov1 + cov2, data = df, dist = "weibull")

  # Create sample data frame
  set.seed(456)
  samp_df <- data.frame(
    cov1 = rnorm(100),
    cov2 = rbinom(100, 1, 0.5)
  )

  # Generate times with no drift/treatment effect
  base_times <- sim_weib_ph(weibull_ph_mod = weib_model, samp_df = samp_df)

  # Generate times with positive drift (should increase survival time)
  pos_times <- sim_weib_ph(
    weibull_ph_mod = weib_model,
    samp_df = samp_df,
    cond_drift = 0.5
  )

  # Generate times with negative treatment effect (should decrease survival time)
  neg_times <- sim_weib_ph(
    weibull_ph_mod = weib_model,
    samp_df = samp_df,
    cond_trt_effect = -0.5
  )

  # Positive drift should increase survival time (higher median)
  expect_gt(median(neg_times), median(base_times))

  # Negative treatment effect should decrease survival time
  expect_lt(median(pos_times), median(base_times))
})



test_that("sim_weib_ph handles empty data correctly", {
  # Create a simple survreg Weibull model
  set.seed(123)
  n <- 50
  df <- data.frame(
    time = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )
  weib_model <- survival::survreg(survival::Surv(time, event) ~ cov1 + cov2, data = df, dist = "weibull")

  # Create empty sample data frame with correct columns
  samp_df <- data.frame(
    cov1 = numeric(0),
    cov2 = numeric(0)
  )

  # Function should return empty vector
  result <- sim_weib_ph(weibull_ph_mod = weib_model, samp_df = samp_df)
  expect_length(result, 0)
})


test_that("sim_weib_ph validates cond_drift input", {
  # Create a simple survreg Weibull model
  set.seed(123)
  n <- 50
  df <- data.frame(
    time = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )
  weib_model <- survival::survreg(survival::Surv(time, event) ~ cov1 + cov2, data = df, dist = "weibull")

  # Sample data
  samp_df <- data.frame(
    cov1 = rnorm(10),
    cov2 = rbinom(10, 1, 0.5)
  )

  # Test error when cond_drift is not numeric
  expect_error(
    sim_weib_ph(weib_model, samp_df, cond_drift = "not a number"),
    "`cond_drift` must be a single number"
  )

  # Test error when cond_drift is a vector
  expect_error(
    sim_weib_ph(weib_model, samp_df, cond_drift = c(0.1, 0.2)),
    "`cond_drift` must be a single number"
  )
})

test_that("sim_weib_ph validates cond_trt_effect input", {
  # Create a simple survreg Weibull model
  set.seed(123)
  n <- 50
  df <- data.frame(
    time = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )
  weib_model <- survival::survreg(survival::Surv(time, event) ~ cov1 + cov2, data = df, dist = "weibull")

  # Sample data
  samp_df <- data.frame(
    cov1 = rnorm(10),
    cov2 = rbinom(10, 1, 0.5)
  )

  # Test error when cond_trt_effect is not numeric
  expect_error(
    sim_weib_ph(weib_model, samp_df, cond_trt_effect = "not a number"),
    "`cond_trt_effect` must be a single number"
  )

  # Test error when cond_trt_effect is a vector
  expect_error(
    sim_weib_ph(weib_model, samp_df, cond_trt_effect = c(0.1, 0.2)),
    "`cond_trt_effect` must be a single number"
  )
})

test_that("sim_weib_ph validates weibull_ph_mod input", {
  # Create sample data
  samp_df <- data.frame(
    cov1 = rnorm(10),
    cov2 = rbinom(10, 1, 0.5)
  )

  # Test error with non-survreg object
  expect_error(
    sim_weib_ph(weibull_ph_mod = "not a survreg object", samp_df = samp_df),
    "`weibull_ph_mod` must be a `survreg` object with a Wiebull distribution"
  )

  # Test error with non-Weibull survreg object
  set.seed(123)
  n <- 50
  df <- data.frame(
    time = rexp(n, 0.1),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )

  # Create an exponential model (not Weibull)
  exp_model <- survival::survreg(survival::Surv(time, event) ~ cov1 + cov2, data = df, dist = "exponential")

  # This should error because it's not a Weibull model
  expect_error(
    sim_weib_ph(weibull_ph_mod = exp_model, samp_df = samp_df),
    "`weibull_ph_mod` must be a `survreg` object with a Wiebull distribution"
  )
})


# calc_cond_weibull -------------------------------------------------------

test_that("calc_cond_weibull produces expected results format and validates inputs", {
  # Setup test data
  set.seed(123)
  n <- 50
  df <- data.frame(
    y = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5),
    cov3 = rnorm(n, mean = 0.5),
    cov4 = rbinom(n, 1, 0.7)
  )

  # Create model for testing
  weib_model <- survival::survreg(survival::Surv(y, event) ~ cov1 + cov2 + cov3 + cov4, data = df, dist = "weibull")

  # Create small test population - small to make test run faster
  pop <- data.frame(
    cov1 = rnorm(1000),
    cov2 = rbinom(1000, 1, 0.5),
    cov3 = rnorm(1000, mean = 0.5),
    cov4 = rbinom(1000, 1, 0.7)
  )

  # Test with wrong model class
  expect_error(
    calc_cond_weibull(
      population = pop,
      weibull_ph_mod = "not a survreg object",
      marg_drift = c(0),
      marg_trt_eff = c(0.1),
      analysis_time = 12
    ),
    "`weibull_ph_mod` must be a survreg object"
  )

  # Test with wrong model distribution
  exp_model <- survival::survreg(survival::Surv(y, event) ~ cov1 + cov2 + cov3 + cov4, data = df, dist = "exponential")
  expect_error(
    calc_cond_weibull(
      population = pop,
      weibull_ph_mod = exp_model,
      marg_drift = c(0),
      marg_trt_eff = c(0.1),
      analysis_time = 12
    ),
    "`weibull_ph_mod` must use a weibull distribution"
  )

  # Test with wrong population format
  expect_error(
    calc_cond_weibull(
      population = as.list(pop),
      weibull_ph_mod = weib_model,
      marg_drift = c(0),
      marg_trt_eff = c(0.1),
      analysis_time = 12
    ),
    "`population` must be a tibble or dataframe"
  )

  # Test with missing covariates in population
  pop_missing <- pop |> select(-cov4)
  expect_error(
    calc_cond_weibull(
      population = pop_missing,
      weibull_ph_mod = weib_model,
      marg_drift = c(0),
      marg_trt_eff = c(0.1),
      analysis_time = 12
    ),
    "Not all covariates in `weibull_ph_mod` are in the population"
  )

  expect_error(
    calc_cond_weibull(
      population = pop,
      weibull_ph_mod = weib_model,
      marg_drift = c(0),
      marg_trt_eff = c(0.1),
      analysis_time = c(6, 12)
    ),
    "`analysis_time` must be a single number"
  )

})

test_that("calc_cond_weibull correctly calculates conditional effects", {
  # Setup test data - to avoid recomputing
  skip_on_cran() # Skip on CRAN since this is a slower test

  set.seed(123)
  n <- 100
  df <- data.frame(
    y = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )

  weib_model <- survival::survreg(survival::Surv(y, event) ~ cov1 + cov2, data = df, dist = "weibull")

  # Create a larger population for more accurate calculation
  set.seed(456)
  pop <- data.frame(
    cov1 = rnorm(10000),
    cov2 = rbinom(10000, 1, 0.5)
  )

  # Run function once and save results for multiple assertions
  result <- calc_cond_weibull(
    population = pop,
    weibull_ph_mod = weib_model,
    marg_drift = c(-0.1, 0, 0.1),
    marg_trt_eff = c(0, 0.1),
    analysis_time = 12
  )

  # Test structure
  expect_true(is.data.frame(result))
  expect_equal(nrow(result), 5) # 3 drift values × 2 treatment effects (-the case with 0 trt effect and neg drift)
  expect_equal(ncol(result), 6) # 6 columns in output

  # Check column names
  expected_cols <- c("marg_drift", "marg_trt_eff", "conditional_drift",
                     "true_control_surv_prob", "conditional_trt_eff",
                     "true_trt_surv_prob")
  expect_true(all(expected_cols %in% colnames(result)))

})

test_that("calc_cond_weibull survival probabilities match marginal effects", {
  # Use the same results object from previous test to avoid recomputation
  skip_on_cran() # Skip on CRAN since this is a slower test

  set.seed(123)
  n <- 100
  df <- data.frame(
    y = rweibull(n, shape = 1.5, scale = 5),
    event = sample(0:1, n, replace = TRUE, prob = c(0.2, 0.8)),
    cov1 = rnorm(n),
    cov2 = rbinom(n, 1, 0.5)
  )

  weib_model <- survival::survreg(survival::Surv(y, event) ~ cov1 + cov2, data = df, dist = "weibull")

  # Create a larger population for more accurate calculation
  set.seed(456)
  pop <- data.frame(
    cov1 = rnorm(5000), # Smaller to run faster in tests
    cov2 = rbinom(5000, 1, 0.5)
  )

  # Run function and save results
  result <- calc_cond_weibull(
    population = pop,
    weibull_ph_mod = weib_model,
    marg_drift = c(0),    # Testing just one value to speed up test
    marg_trt_eff = c(0.1),
    analysis_time = 12
  )

  # For drift = 0, treatment effect = 0.1
  trt_effect_row <- result[1, ]

  # The difference between treatment and control survival probabilities should be close to the marginal effect
  observed_effect <- trt_effect_row$true_trt_surv_prob - trt_effect_row$true_control_surv_prob
  expect_equal(observed_effect, trt_effect_row$marg_trt_eff, tolerance = 0.01)
})

Try the beastt package in your browser

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

beastt documentation built on June 8, 2025, 11:42 a.m.