Skip to content

Commit

Permalink
Merge pull request #172 from keaven/170-minor-update-toInteger()
Browse files Browse the repository at this point in the history
170 minor update to integer()
  • Loading branch information
nanxstats authored Nov 14, 2024
2 parents 0f570d3 + 7b7672f commit e353280
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 111 deletions.
81 changes: 55 additions & 26 deletions R/toInteger.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,43 @@
#' or sample size (other designs)
#'
#' @param x An object of class \code{gsDesign} or \code{gsSurv}.
#' @param ratio A non-negative integer, usually corresponding to experimental:control sample size ratio.
#' Rounding is done to a multiple of \code{ratio + 1}. If input \code{x} has class \code{gsSurv} (design for time-to-event outcome),
#' and \code{x$ratio} is a whole number, \code{ratio} is replaced by \code{x$ratio}.
#' See details.
#' @param roundUpFinal Final value in returned \code{n.I} is rounded up
#' if \code{TRUE}; otherwise, just rounded. For \code{gsSurv} input, final total sample size is also controlled by this. See details.
#' @param ratio Usually corresponds to experimental:control sample size ratio.
#' If an integer is provided, rounding is done to a multiple of
#' \code{ratio + 1}. See details.
#' If input is non integer, rounding is done to the nearest integer or
#' nearest larger integer depending on \code{roundUpFinal}.
#' @param roundUpFinal Sample size is rounded up to a value of \code{ratio + 1}
#' with the default \code{roundUpFinal = TRUE} if \code{ratio} is a
#' non-negative integer.
#' If \code{roundUpFinal = FALSE} and \code{ratio} is a non-negative integer,
#' sample size is rounded to the nearest multiple of \code{ratio + 1}.
#' For event counts, \code{roundUpFinal = TRUE} rounds final event count up;
#' otherwise, just rounded if \code{roundUpFinal = FALSE}.
#' See details.
#'
#' @return Output is an object of the same class as input \code{x}; i.e.,
#' \code{gsDesign} with integer vector for \code{n.I} or \code{gsSurv}
#' with integer vector \code{n.I} and integer total sample size. See details.
#'
#' @return Output is an object of the same class as input \code{x}; i.e., \code{gsDesign} with integer vector for \code{n.I}
#' or \code{gsSurv} with integer vector \code{n.I} and integer total sample size. See details.
#'
#' @details
#' If \code{ratio = 3}, rounding for final sample size is done to a multiple of 3 + 1 = 4.
#' For a \code{gsSurv} object input in \code{x}, event counts output in \code{n.I} are rounded to nearest integer and
#' final total sample size is rounded to a multiple of \code{ratio + 1}.
#' For other input values of \code{x} (\code{gsDesign} class), \code{n.I} is interpreted as sample size;
#' final value is rounded to a multiple of \code{ratio + 1}, with \code{roundUpFinal} controlling rounding of last value.
#' It is useful to explicitly provide the argument \code{ratio} when a
#' \code{gsDesign} object is input since \code{gsDesign} does not have a
#' \code{ratio} in return.
#' \code{ratio = 0, roundUpFinal = TRUE} will just round up the sample size
#' (also event count).
#' Rounding of event count targets is not impacted by \code{ratio}.
#' Since \code{x <- gsSurv(ratio = M)} returns a value for \code{ratio},
#' \code{toInteger(x)} will round to a multiple of \code{M + 1} if \code{M}
#' is a non-negative integer; otherwise, just rounding will occur.
#' The most common example would be if there is 1:1 randomization (2:1) and
#' the user wishes an even (multiple of 3) sample size, then \code{toInteger()}
#' will operate as expected.
#' To just round without concern for randomization ratio, set \code{ratio = 0}.
#' If \code{toInteger(x, ratio = 3)}, rounding for final sample size is done
#' to a multiple of 3 + 1 = 4; this could represent a 3:1 or 1:3
#' randomization ratio.
#' For 3:2 randomization, \code{ratio = 4} would ensure rounding sample size
#' to a multiple of 5.
#'
#' @export
#'
Expand Down Expand Up @@ -45,18 +66,27 @@
#' minfup = 8, # Planned minimum follow-up
#' ratio = 3 # Randomization ratio (experimental:control)
#' )
#' # Convert bounds to exact binomial bounds
#' toInteger(x, ratio = 3)
toInteger <- function(x, ratio = 0, roundUpFinal = TRUE) {
#' # Convert sample size to multiple of ratio + 1 = 4, round event counts.
#' # Default is to round up both event count and sample size for final analysis
#' toInteger(x)
toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) {
if (!inherits(x, "gsDesign")) stop("must have class gsDesign as input")
if (!isInteger(ratio) || ratio < 0) stop("input ratio must be a non-negative integer")
if (!(isInteger(ratio) && ratio >= 0)){
message("toInteger: rounding done to nearest integer since ratio was not specified as postive integer .")
ratio <- 0
}
counts <- round(x$n.I) # Round counts (event counts for survival; otherwise sample size)
# For time-to-event endpoint, just round final count up
# For time-to-event endpoint, just round final event count up
if (inherits(x, "gsSurv")) {
if (roundUpFinal) counts[x$k] <- ceiling(x$n.I[x$k])
if (abs(counts[x$k] - x$n.I[x$k]) <= .01){
counts[x$k] <- round(x$n.I[x$k])
} else if (roundUpFinal) counts[x$k] <- ceiling(x$n.I[x$k])
} else {
# Check if control size is close to integer multiple of ratio + 1
if (abs(x$n.I[x$k] - round(x$n.I[x$k] / (ratio + 1)) * (ratio + 1)) <= .01) {
counts[x$k] <- round(x$n.I[x$k] / (ratio + 1)) * (ratio + 1)
# For non-survival designs round sample size based on randomization ratio
if (roundUpFinal) {
}else if (roundUpFinal) {
counts[x$k] <- ceiling(x$n.I[x$k] / (ratio + 1)) * (ratio + 1) # Round up for final count
} else {
counts[x$k] <- round(x$n.I[x$k] / (ratio + 1)) * (ratio + 1)
Expand All @@ -71,18 +101,17 @@ toInteger <- function(x, ratio = 0, roundUpFinal = TRUE) {
lsTime = x$lsTime, usTime = x$usTime
)
if (max(abs(xi$n.I - counts)) > .01) warning("toInteger: check n.I input versus output")
xi$n.I <- counts # ensure these are integers as they become real in gsDesign call
xi$n.I <- counts # ensure these are integers as they became real in gsDesign call
# Non-binding futility designs have x$test.type either 4 or 6
if (x$test.type %in% c(4, 6)) {
xi$falseposnb <- as.vector(gsprob(0, xi$n.I, rep(-20, xi$k), xi$upper$bound, r = xi$r)$probhi)
}
if ("gsSurv" %in% class(x) || x$nFixSurv > 0) {
if (inherits(x, "gsSurv") || x$nFixSurv > 0) {
xi$hr0 <- x$hr0 # H0 hazard ratio
xi$hr <- x$hr # H1 hazard ratio

N <- rowSums(x$eNC + x$eNE)[x$k] # get input total sample size
N_continuous <- N
# if ratio = 0 and x$ratio is positive integer, replace ratio
if(ratio == 0 && is.wholenumber(x$ratio)) ratio <- x$ratio
# Update sample size to integer
N <- N / (ratio + 1)
if (roundUpFinal) {
Expand Down
54 changes: 38 additions & 16 deletions man/toInteger.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 68 additions & 0 deletions tests/testthat/test-developer-test-toInteger.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Test toInteger
test_that("Test: toInteger with invalid input", {
M <- "a"

expect_error(
toInteger(x = M),
info = "Test toInteger with invalid input"
)
})

test_that("Test: toInteger for fractional and negative values", {
x <- gsDesign(n.fix = 100)
M <- 55.9

expect_message(
toInteger(x, ratio = M),
info = "Test toInteger for floating values"
)
M <- -1
expect_message(
toInteger(x, ratio = M),
info = "Test toInteger for negative value of ratio"
)
# Note that x$ratio = NULL
expect_message(
toInteger(x),
info = "Test toInteger for NULL value of ratio"
)
})

test_that("Test: toInteger for even sample size", {
x <- gsDesign(n.fix = 100)

expect_true(
max(toInteger(x, ratio = 1)$n.I) %% 2 == 0,
info = "Test toInteger for floating value with decimal value 0"
)
})

test_that("Test: toInteger for multiple of 5", {
x <- gsDesign(n.fix = 100)

expect_true(
max(toInteger(x, ratio = 4)$n.I) %% 5 == 0,
info = "Test toInteger for floating value with decimal value 0"
)
})

# Now test survival endpoint
test_that("Test: toInteger for survival endpoint event count works properly", {
# This gives 252.1852 as sample size, 227.1393 as final event count
x <- gsSurvCalendar(hr = 0.64)
# Should round event counts (up for final) and
# round up final event count final sample size only as well
y <- toInteger(x)

# Interim event counts rounded
expect_equal(min(abs(round(x$n.I[1:x$k - 1]) - y$n.I[1:x$k - 1])), 0)
# Final event count round up
expect_true(
(y$n.I - x$n.I)[x$k] >= 0,
info = "Test toInteger of gsSurv rounds up final event count"
)
# Final sample size rounds to even
expect_true(as.integer((y$eNC + y$eNE)[x$k]) %% 2 == 0)
# Final event count rounds up
expect_gte((y$eNC + y$eNE - x$eNC - x$eNE)[x$k], 0)
})
Loading

0 comments on commit e353280

Please sign in to comment.