Skip to content

Commit

Permalink
Merge branch 'release-0.1.4' into github-main
Browse files Browse the repository at this point in the history
  • Loading branch information
kruscpe1 committed Mar 5, 2024
2 parents 236d51d + aba269b commit 08f98ac
Show file tree
Hide file tree
Showing 65 changed files with 17,150 additions and 9 deletions.
16 changes: 16 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
^renv$
^renv\.lock$
^.*\.Rproj$
^\.Rproj\.user$
^LICENSE\.md$
^\.gitlab-ci\.yml$
^cran-comments\.md$
^CRAN-SUBMISSION$
^app\.R$
^R/rsconnect$
^rsconnect$
^\.covrignore$
^public$
^_pkgdown\.yml$
^docs$
^pkgdown$
1 change: 1 addition & 0 deletions .Rprofile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
source("renv/activate.R")
3 changes: 3 additions & 0 deletions .covrignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
R/runApp.R
R/server.R
R/ui.R
19 changes: 12 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
/*.Rcheck/

# RStudio files
.Rproj.user/
.Rproj.user
paths
public/

# produced vignettes
vignettes/*.html
Expand All @@ -38,12 +40,15 @@ vignettes/*.pdf

# R Environment Variables
.Renviron
.Rproj.user

# pkgdown site
docs/
# images
*.jpeg
inst/doc

# translation temp files
po/*~

# RStudio Connect folder
rsconnect/
# Shiny
**/rsconnect/

# vim
*.swp
3 changes: 3 additions & 0 deletions CRAN-SUBMISSION
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Version: 0.1.4
Date: 2024-03-05 15:50:18 UTC
SHA: 1c522f9d25643b3a09c626ef648e0b56dc08c27a
34 changes: 34 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Package: monitOS
Title: Monitoring Overall Survival in Pivotal Trials in Indolent Cancers
Version: 0.1.4
Authors@R: c(
person("Thomas", "Fleming", email = "[email protected]", role ="ctb"),
person("Lisa", "Hampson", email = "[email protected]", role ="aut"),
person("Bharani", "Bharani-Dharan", email = "[email protected]", role ="ctb"),
person("Frank", "Bretz", email = "[email protected]", role = "ctb"),
person("Arunava", "Chakravartty", email = "[email protected]", role = "ctb"),
person("Thibaud", "Coroller", email = "[email protected]", role = c("aut", "cre")),
person("Evanthia", "Koukouli", email = "[email protected]", role = "aut"),
person("Janet", "Wittes", email = "[email protected]", role = "ctb"),
person("Nigel", "Yateman", email = "[email protected]", role = "ctb"),
person("Emmanuel", "Zuber", email = "[email protected]", role = "ctb"),
person("Novartis", "Pharma AG", role = "cph")
)
Description: These guidelines are meant to provide a pragmatic, yet rigorous, help to drug developers and decision makers, since they are shaped by three fundamental ingredients: the clinically determined margin of detriment on OS that is unacceptably high (delta null); the benefit on OS that is plausible given the mechanism of action of the novel intervention (delta alt); and the quantity of information (i.e. survival events) it is feasible to accrue given the clinical and drug development setting. The proposed guidelines facilitate transparent discussions between stakeholders focusing on the risks of erroneous decisions and what might be an acceptable trade-off between power and the false positive error rate.
License: GPL (>= 3)
Maintainer: Thibaud Coroller <[email protected]>
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3
Imports:
stats,
glue,
shiny,
shinydashboard,
Suggests:
testthat (>= 3.0.0),
covr,
knitr,
rmarkdown,
pkgdown
VignetteBuilder: knitr
595 changes: 595 additions & 0 deletions LICENSE.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Generated by roxygen2: do not edit by hand

export(bounds)
export(run_app)
import(shiny)
import(shinydashboard)
importFrom(glue,glue)
importFrom(stats,pnorm)
importFrom(stats,qnorm)
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# monitOS 0.1.4

- Added a `NEWS.md` file to track changes to the package.
- Added `Shiny` section in `README.md` file.
- Improved `monitOS::run_app()`: new layout, new descriptions, new use case.
- Created `pkgdown` public website.

# monitOS 0.1.3 | `2023-10-31`

- Initial CRAN submission.
140 changes: 140 additions & 0 deletions R/bounds.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#' @title Bounds
#'
#' @description OS monitoring guidelines as proposed in manuscript "Monitoring Overall Survival in Pivotal Trials in Indolent Cancers".
#' Calculate thresholds for positivity that can be used at an analysis to judge whether emerging
#' evidence about the effect of treatment on OS is concerning or not. The threshold for positivity at any given analysis
#' is the value below which the observed hazard ratio must be in order to provide sufficient reassurance that the effect
#' on OS does not reach the selected unacceptable level of detriment (the margin hr_null).
#' Terminology follows the manuscript "Monitoring Overall Survival in Pivotal Trials in Indolent Cancers", publication submitted
#' @details Monitoring guidelines assume that the hazard ratio (HR) can adequately summarize the size of the benefits and harms of the experimental
#' intervention vs control on overall survival (OS). Furthermore, guidelines assume that an OS HR < 1 is consistent with a beneficial effect of the
#' intervention on OS (and smaller OS HRs <1 indicate increased efficacy).
#' @param events Vector. Target number of deaths at each analysis
#' @param power_int Scalar. Marginal power required at the Primary Analysis when true hazard ratio (HR) is hr_alt.
#' @param falsepos Scalar. Marginal one-sided false positive error rate we are prepared to tolerate at the Final Analysis. Determines the positivity threshold at Final Analysis
#' @param hr_null Scalar. The unacceptably large detrimental effect of treatment on OS we want to rule out (on HR scale)
#' @param hr_alt Scalar. Plausible clinically relevant beneficial effect of treatment on OS (on HR scale)
#' @param rand_ratio Integer. If patients are randomized k:1 between experimental intervention and control, rand_ratio should be inputted as k.
#' Example: if patients are randomized 1:1 between experimental and control, k=1. If patients are randomized 2:1 between experimental and control, k=2.
#' @param hr_marg_benefit Scalar. We may be uncertain about what a plausible beneficial effect of treatment on OS is. User can enter a second plausible OS benefit (on HR scale)
#' and function will evaluate the probability we meet the positivity threshold at each analysis under this HR. This second OS benefit will usually be closer to 1 than hr_alt.
#' @importFrom stats pnorm qnorm
#' @return List that contains:
#' * `lhr_null`: Scalar, unacceptable OS log-HR,
#' * `lhr_alt`: Scalar, plausible clinically relevant log-HR,
#' * `lhr_pos`: Scalar, positivity thresholds for log-HR estimates,
#' * `summary`: Dataframe, which contains:
#' * `OS HR threshold for positivity`,
#' * `One sided false positive error rate`,
#' * `Level of 2 sided CI needed to rule out hr_null`,
#' * `Probability of meeting positivity threshold under hr_alt`,
#' * `Positivity_Thres_Posterior`: Pr(true OS HR >= minimum unacceptable OS HR | current data),
#' * `Positivity_Thres_PredProb`: Pr(OS HR estimate at Final Analysis <= Final Analysis positivity threshold | current data)
#' @export
#' @examples
#' # Example 01: OS monitoring guideline retrospectively applied to Motivating Example 1
#' # with delta null = 1.3, delta alt = 0.80, gamma_FA = 0.025 and beta_PA = 0.10.
#' bounds(events=c(60, 89, 110, 131, 178),
#' power_int=0.9, # beta_PA
#' falsepos=0.025, # gamma_FA
#' hr_null = 1.3, # delta_null
#' hr_alt = 0.8, # delta_alt
#' rand_ratio = 1, # rand_ratio
#' hr_marg_benefit = NULL)
#' # Example 02: OS monitoring guideline applied to Motivating Example 2
#' # with delta null = 4/3, delta alt = 0.7, gamma_FA = 0.20 and beta_PA = 0.1.
#' bounds(events=c(60, 89, 110, 131, 178),
#' power_int=0.9, # beta_PA
#' falsepos=0.025, # gamma_FA
#' hr_null = 1.3, # delta_null
#' hr_alt = 0.8, # delta_alt
#' rand_ratio = 1, # rand_ratio
#' hr_marg_benefit = 0.95)
bounds <- function(events,
# OS events at each analysis
power_int = 0.9,
# 1-Beta PA, what power do we want to not flag a safety concern at an interim analysis if the true OS HR equals our target alternative?
falsepos = 0.025,
# Gamme FA, What is the (one-sided) type I error rate that we will accept at the final analysis?
hr_null = 1.3,
# Delta null, what is the minimum unacceptable OS HR?
hr_alt = 0.9,
# Delta alt, what is a plausible alternative OS HR consistent with OS benefit?
rand_ratio = 1,
# for every patient randomized to control, rand_ratio patients are allocated to experimental intervention
hr_marg_benefit = NULL
# evaluate probability of meeting positivity thresholds under a second plausible beneficial effect of treatment on OS (HR = hr_marg_benefit)
) {

# Log scale
lhr_null <- log(hr_null)
lhr_alt <- log(hr_alt)

# Init variables
nstage <- length(events) # total number of analyses planned
info <-
rand_ratio*events / ((rand_ratio + 1)^2) # Fisher's information for log-HR at each analysis
se <-
sqrt(1 / info) # asymptotic standard error for log-HR at each analysis

# Calculate the attained power when true HR = hr_alt at Final Analysis
power_final <-
pnorm((lhr_null - qnorm(1 - falsepos) * se[nstage] - lhr_alt) / se[nstage])

# calculate the levels of the two-sided CIs used to monitor the OS log-HR
# at each interim analysis and the corresponding one-sided false positive error rate
# assuming we want marginal power = power_int to 'rule out' hr_Lnull at required
# evidentiary level when true OS HR = hr_alt
gamma <-
2 * (1 - pnorm(((
lhr_null - lhr_alt
) / se[1:(nstage - 1)]) - qnorm(power_int)))
falsepos_all <- c(gamma / 2, falsepos)
CI_level_monit_null <- 100 * (1 - 2 * falsepos_all)
power_all <-
c(rep(power_int, times = (nstage - 1)), power_final)

lhr_pos <- lhr_null - qnorm(1 - falsepos_all) * se

# Given the positivity thresholds, re-express these via Bayesian metrics
post_pos <- calc_posterior(lhr_pos, lhr_null, events)
pred_pos <- calc_predictive(lhr_pos, events)

summary <- data.frame('Deaths' = events)

# OS HR thresholds for positivity
summary$'OS HR threshold for positivity' <- round(exp(lhr_pos), 3)

# One sided false positive error_rate at each analysis
summary$'One-sided false positive error rate' <- round(falsepos_all, 3)

# Level of 2-sided CI needed to rule out δnull at given analysis (%)
summary$'Level of 2-sided CI needed to rule out delta null' <- round(pmax(0, CI_level_monit_null), 0)

# Probability of meeting positivity threshold under plausible OS benefit
summary$'Probability of meeting positivity threshold under delta alt' <- round(power_all, 3)

# Pr(true OS HR >= detrimental OS HR | current data)
summary$'Posterior probability the true OS HR exceeds delta null given the data' <- round(post_pos, 3)
summary$'Predictive probability the OS HR estimate at Final Analysis does not exceed the positivity threshold' <- c(round(pred_pos * 100, 3), NA)

if (!is.null(hr_marg_benefit)) {
# calculate the probability of meeting positivity thresholds under lhr_marg_benefit
summary$'Probability of meeting positivity threshold under incremental benefit' <-
round(meeting_probs(
summary = summary,
lhr_pos = lhr_pos,
lhr_target = log(hr_marg_benefit),
rand_ratio = rand_ratio
),
3)
}

return(list(
lhr_null = lhr_null,
lhr_alt = lhr_alt,
lhr_pos = lhr_pos,
summary = summary
))

}
50 changes: 50 additions & 0 deletions R/posteriors.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#' Function which calculates for k=1, ..., K, Pr(log-HR >= lhr_null | theta.hat.k = lhr_con.k)
#' i.e. the posterior probability the true OS log-hr exceeds the minimum unacceptable
#' OS log-HR given the estimate of the log-hr at analysis k equals lhr_con.k (i.e. the estimate
#' is equal to the stage k 'continuation threshold').
#'
#' @param lhr_con vector of length K (# number of looks at OS data) containing 'continuation' thresholds on log-HR scale
#' @param lhr_null scalar - minumum unacceptable OS log-HR
#' @param events vector length K - number of OS events at each look at the data
#' @importFrom stats pnorm
#' @return vector of length K - continuation thresholds expressed on posterior probability scale
calc_posterior <- function(lhr_con, lhr_null, events) {
info <-
events / 4 # Fisher's information for log-HR at each analysis
se <-
sqrt(1 / info) # asymptotic standard error for log-HR at each analysis

# calculating Pr(log-hr >= lhr_null | theta.hat.k = lk)
# where lk is the threshold (for the partial likelihood estimate of the OS log-HR) for 'continuation'
post <- 1 - pnorm((lhr_null - lhr_con) / se)
return(post)
}

#' Title"
#' @description Calculates the posterior predictive probability of 'ruling out' lhr_null at final OS analysis
#' given current estimate of OS log-HR is lhr_cont_k, for k=1, ..., K-1
#' @param lhr_con vector of length K (# number of looks at OS data) containing 'continuation' thresholds on log-HR scale
#' @param events vector length K - number of OS events at each look at the data
#' @return vector of length K-1: continuation thresholds at analyses k=1, ..., K-1 expressed on scale of
#' posterior predictive probability of ruling out lhr_null at final OS analysis
#' @importFrom stats pnorm
calc_predictive <- function(lhr_con, events) {
nstage <- length(events)
info <-
events / 4 # Fisher's information for log-HR at each analysis
se <-
sqrt(1 / info) # asymptotic standard error for log-HR at each analysis

# calculating Pr(ZK <= lK*sqrt(info.K) | Z.k = sqrt(info.k)*lk)
# where lk is the OS log-HR threshold for 'continuation' at analysis k
pred_pos <- vector(mode = "numeric", length = (nstage - 1))
for (i in 1:(nstage - 1)) {
pred_pos[i] <- pnorm(
lhr_con[nstage] * sqrt(info[nstage]),
mean = lhr_con[i] * sqrt(info[i]) * sqrt(info[nstage] /
info[i]),
sd = sqrt((info[nstage] - info[i]) / info[i])
)
}
return(pred_pos)
}
29 changes: 29 additions & 0 deletions R/probs.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#' Probabilities of meeting positivity threshold under target HR
#'
#' @param lhr_pos List. Log HRs for positive threshold
#' @param summary DataFrame. Summary dataframe from bounds.R
#' @param lhr_target Scalar. Target log HR to calculate the probability of meeting positivity thresholds
#' @param rand_ratio Integer. If patients are randomized k:1 between experimental intervention and control, rand_ratio should be inputted as k.
#' Example: if patients are randomized 1:1 between experimental and control, k=1. If patients are randomized 2:1 between experimental and control, k=2.
#'
#' @return Array. Probabilities of meeting positivity threshold under target HR
meeting_probs <-
function(summary,
lhr_pos,
lhr_target = 1,
rand_ratio = 1) {
events <- summary$Deaths
info <-
rand_ratio * events / ((rand_ratio + 1) ^ 2) # Fisher's information for log-HR at each analysis
se <-
sqrt(1 / info) # asymptotic standard error for log-HR at each analysis
prob <- list()
for (i in 1:length(events)) {
prob[i] <-
pnorm(lhr_pos[i],
mean = lhr_target,
sd = se[i],
lower.tail = TRUE)
}
return(as.numeric(prob))
}
18 changes: 18 additions & 0 deletions R/runApp.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#' @title monitOS app
#'
#' @description Runs the shiny app to guide user choice adequate settings to calculate
#' the positivity thresholds to monitor overall survival (OS)
#' @import shiny
#' @export
#' @return No return value, runs shiny app
run_app <- function() {
shinyApp(
ui = app_ui,
server = app_server,
# onStart = onStart,
# options = options,
# enableBookmarking = enableBookmarking,
# uiPattern = uiPattern
)
}

Loading

0 comments on commit 08f98ac

Please sign in to comment.