Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # An R script to predict the timeline of death events in the INSPIRE PIII
- # trial run by Onconova Therapeutics (ONTX)
- # (c) 2019 Germain Garand <germain.garand@laposte.net>
- #
- # This is for information purposes only. Any calculation performed herein could be
- # wrong and/or misrepresent reality. Use at your own risk.
- #
- # License: Creative Commons BY-NC 2.0
- #
- # Version 1.08
- #
- # History:
- # 0.01 - 2019/03/20- .initial version
- # 0.02 - 2019/03/21- .added active clinical sites data
- # 0.03 - 2019/03/21- .added overlay of ad-hoc pts per month data
- # 0.05 - 2019/03/22- .some code factoring, commenting, adding some info on CT Sites graph
- # 0.06 - 2019/03/22- .redirected output to PNG images ; slightly reworked enrollment model to better fit the curve
- # 0.08 - 2019/03/22- .output 'Population' numbers alongside 'Events' numbers.
- # .Introduced an alternative "worst case" enrollment population (see comment in code)
- # 0.09 - 2019/03/23- .add a final date in the future in Active Trial Sites data,
- # so that the graphs are plotted up to that date.
- # 0.1 - 2019/03/24- .add graph of events and population over time.
- # .Use 'floor' instead of 'ceiling' aka a patient that is "half-dead" is still alive
- # .Store full accrual of a month into the first day of the following month. Easier to graph.
- # 0.14 - - .adjust enrollment model accounting for a mid 2017 slowdown based on
- # Dr.Kumar's comment in Q3 2017 CC
- # .add explaining comment on graph
- # 0.15 - - .also massage pessimistic enrollment model (see v.14 log)
- # .add other milestone from Q3 17 cc (full original accrual of 225 by mid 2018)
- # 0.17 - 2019/03/25- .updated enrollment model to match new datapoint: 75%+ of patients
- # enrolled as of today.
- # 0.18 - 2019/03/27- .added "FY2018 ER" milestone on accrual graph
- # 0.19 - - .extend scale of possible time length, define new pessimistic scenario (see comment in code)
- # 0.2 - 2019/05/15- .add insight from Q1 2019 ER about active clinical sites (not reflected by clinicaltrials.gov)
- # "more than 140" active sites. 19 new sites recently added. 25 more anticipated.
- # 0.3 - 2019/07/01- .add VHR km curves. Plot them on ITT km graph in gray.
- # .compute and graph VHR Events and Population
- # 0.31 - 2019/07/03- .introduce an intermediate enrollment scenario based on the mean of 8 pts/month observed from Jan 2018 to March 2019
- # (uncomment relevant line to use)
- # 0.35 - 2019/08/13- .make switching between enrollment models easier and display that choice in the graphical and textual output
- #
- # 1.05 - 2019/08/16- .change the KM survival curves to use the unmodified ITT simulated curves from the Corporate Presentation.
- # chances are most of the +20% VHR we are seing in INSPIRE come from the more stringent criteria and are thus
- # already reflected in the original ITT simulation.
- # .change optimistic scenario to reflect advancement in time that makes previous scenario now unlikely.
- # 1.07 - 2019/08/16- .invariants were broken by KM curve change. Fix that by adjusting the enrollment curves
- #
- # Accrual of Active Trial Sites
- # built from clinicaltrial.org's history of 'Locations' changes for NCT02562443
- trial_sites_data <- "Date Num
- 2015/09/25 1
- 2015/11/04 2
- 2015/11/17 4
- 2016/02/01 5
- 2016/02/29 6
- 2016/03/04 8
- 2016/03/10 12
- 2016/03/24 13
- 2016/03/30 14
- 2016/04/02 20
- 2016/04/07 24
- 2016/04/13 30
- 2016/04/18 32
- 2016/04/20 33
- 2016/04/23 34
- 2016/04/29 37
- 2016/05/02 39
- 2016/05/04 41
- 2016/05/18 45
- 2016/05/19 50
- 2016/05/24 53
- 2016/05/26 55
- 2016/06/06 59
- 2016/06/13 60
- 2016/06/17 62
- 2016/06/28 63
- 2016/06/29 64
- 2016/07/01 65
- 2016/07/07 66
- 2016/07/19 95
- 2016/07/27 98
- 2016/07/28 100
- 2016/07/29 101
- 2016/08/04 104
- 2016/08/27 105
- 2016/08/30 107
- 2016/09/09 108
- 2016/09/14 110
- 2016/09/21 113
- 2016/09/26 114
- 2016/10/04 115
- 2016/10/12 117
- 2016/10/13 120
- 2016/10/20 122
- 2016/10/26 124
- 2016/10/28 125
- 2016/11/14 126
- 2016/11/28 128
- 2016/12/06 129
- 2016/12/09 130
- 2016/12/19 132
- 2017/01/19 138
- 2017/01/23 139
- 2017/01/31 140
- 2017/02/16 142
- 2017/02/23 144
- 2017/02/28 146
- 2017/03/02 148
- 2017/03/14 150
- 2017/03/17 151
- 2017/03/20 152
- 2017/03/28 156
- 2017/04/12 157
- 2017/04/21 159
- 2017/04/25 160
- 2017/05/09 162
- 2017/05/18 163
- 2017/05/22 164
- 2017/06/13 165
- 2017/07/03 166
- 2017/07/06 167
- 2017/07/11 168
- 2017/07/13 169
- 2017/07/15 170
- 2017/08/29 169
- 2017/09/11 170
- 2017/10/02 171
- 2017/10/09 172
- 2017/10/16 173
- 2017/10/25 171
- 2017/10/31 170
- 2017/11/28 172
- 2017/12/01 173
- 2017/12/22 174
- 2018/01/02 176
- 2018/01/16 174
- 2018/02/01 173
- 2018/02/05 172
- 2018/02/28 173
- 2018/03/01 172
- 2018/03/15 171
- 2018/03/21 169
- 2018/03/26 168
- 2018/03/29 165
- 2018/04/03 164
- 2018/04/05 163
- 2018/04/09 162
- 2018/04/12 161
- 2018/04/20 160
- 2018/04/30 156
- 2018/05/10 155
- 2018/05/15 154
- 2018/06/07 153
- 2018/06/18 154
- 2018/06/19 153
- 2018/07/02 152
- 2018/07/03 151
- 2018/07/18 152
- 2018/07/26 153
- 2018/08/16 152
- 2018/10/16 153
- 2019/05/14 142
- 2019/08/14 142
- 2019/12/01 162
- "
- Sys.setlocale("LC_TIME", "C")
- # The simulation might look as far ahead as (elapsed) February 2021
- d <- seq(as.Date('2016-01-01'), as.Date('2021-03-01'), by='months')
- ## Define our enrollment model, expressed in patients recruited per month, starting December 2015
- ## This is starting to look like a proper fit on the CT Sites' curve:
- ## pts <- c(1,1,1,1,1,2,3,4,5,5,6,7,7,8,8,9,9,9,10,10,11,11,12,12,13, 12,12,11,11,10,10,10,10,10,11,11,11,11,11,11,11,11,10, rep(0,7))
- #
- ## Adjust the curve accounting for a mid 2017 slowdown in enrollment based on Dr.Kumar's comment in Q3 2017 CC of Nov. 9, 2017:
- ## "Since we experienced a slowdown in enrollment mid-year, we undertook measures just for enrollment including the addition
- ## of trial sites in three new countries and changes within the CRO group.
- ## These efforts are now paying off. I'm pleased to report that these actions led to a recent increase in the enrollment rate for
- ## this trial."
- ## pts <- c(1,1,1,1,1,2,3,4,5,5,6,7,7,8,8,9,9,8,8,10,11,11,12,12,13, 13,12,12,11,11,10,10,10,10,11,11,11,11,11,11,11,11,10, rep(0,7))
- #
- ## Let's define also a pessimistic enrollment scenario (recruitment somewhat waning after Interim for whatever reason)
- ## pts <- c(1,1,1,1,1,2,3,4,5,5,6,7,7,8,8,9,9,8,8,10,11,11,12,12,13, 13,12,11,10,10,9,9,8,8,9,9,10,9,9,9,8,9,8,9,9,9, rep(0,4))
- ##
- model <- c('optimistic', 'realistic', 'pessimistic')
- # 18/03/25: 75%+ pts enrolled, means the recruitment rate experienced a big drop after the number of sites peaked out
- optimistic <- c(1,1,2,2,2,3,3,4,6,7,7,8,8,9,9,9,8,11,12,12,10,10,9,8,8, 8,8,7,8,7,6,7,7,7,7,8,8, 7,8,8,8,8,8,8,9, 10,12,14,3, rep(0,14))
- # Here is our updated pessimistic scenario
- #pessimistic <- c(1,1,2,2,2,3,3,4,6,7,7,8,8,9,9,9,8,11,12,12,10,10,9,8,7, 6,6,5,5,6,5,6,7,8,10,12,9, 7,6,5,5,6,5,6,6, 7,6,10,15,15,8,0, 0)
- pessimistic <- c(1,1,2,2,2,3,3,4,6,7,7,8,8,9,9,9,8,11,12,12,10,10,9,8,7, 6,6,5,6,6,7,6,8,7,7,6,7, 7,11,10,8,7,7,6,7, 7,7,8,8,8,8,6,0, rep(0,10))
- #pessimistic <- c(1,1,2,2,2,3,3,4,6,7,7,8,8,9,9,9,8,11,12,12,10,10,9,7,7, 2,2,2,3,3,2,2,3,2,3,9,12, 20,20,20,8,7,7,7,7, 7,7,10,14,14,0,0, 0)
- #pessimistic <- c(1,1,2,2,2,3,3,4,6,7,7,8,8,9,9,9,8,11,12,12,10,10,9,8,6, 2,2,1,2,1,2,1,2,2,2,1,2, 2,3,75,30,5,3,2,3, 5,6,6,15,20,8,0, 0)
- #pessimistic <- c(1,1,2,2,2,3,3,4,6,7,7,8,8,9,9,9,8,11,12,12,10,10,9,8,6, 3,2,3,3,4,3,4,3,4,5,8,10, 8,6,4,4,4,5,4,5, 5,6,5,8,8,8,0, 0)
- # An interesting middleground is just to stick to 8 pts/months starting from March 2019
- # 8 pts/month is indeed the observed accrual mean from interim analysis to announcement of 75% accrual back in March.
- realistic <- c(1,1,2,2,2,3,3,4,6,7,7,8,8,9,9,9,8,11,12,12,10,10,9,8,8, 8,8,7,8,7,6,7,7,7,7,8,8, 8,8,7,8,7,8,7,8,7,8,8,8, 8,3,0,0, rep(0,10))
- # Select your model here (default: realistic):
- # -------------------------------------------
- # 1) optimistic - this is the official target of the company, with enrollment ending before EOY 2019
- # 2) realistic - assumes the enrollment will be complete in February 2020, consistent with the historical enrollment rate
- # 3) pessimistic - assumes the enrollment will be complete in April 2020
- model_choice <- 3
- # -------------------------------------------
- model_name <- model[ model_choice ]
- pts <- get( model_name )
- message('Enrollment model: ', model_name)
- message("Total accrual: ", sum(pts))
- message("----")
- # Store in reverse, most recent patients first as it is how the survival curve will be applied
- e <- data.frame(rev(d),rev(pts))
- names(e) <- c("Date","Patients")
- # Plot the CT Sites data to a file..
- png("INSPIRE_Active_CT_Sites_With_Enrollment_Rate_Estimate.png")
- ct_sites <- read.table(text = trial_sites_data, header = TRUE)
- ct_sites$Date <- as.Date(ct_sites$Date, "%Y/%m/%d")
- plot(Num ~ Date, ct_sites, xaxt = "n", type = "l", col="green",main="Active INSPIRE Clinical Trial Sites",
- sub=paste("Enrollment model: ", model_name), col.sub="red",ylab="Total Number Of Sites", xlab="" )
- title(xlab="Registration Date", cex.lab=1.2, mgp=c(2,0,0))
- axis(1, ct_sites$Date, format(ct_sites$Date, "%y/%m/%d"), cex.axis = .7)
- abline(h=(seq(0,170,10)), col="lightgray", lty="dotted")
- abline(v=c(as.Date("2018/01/19")), col="blue", lty="dotted")
- text(x=as.Date("2018/03/05"),y=80, labels="Interim Analysis", cex=.8, srt=90, col="blue")
- abline(v=c(as.Date("2019/03/25")), col="darkgrey", lty="dotted")
- text(x=as.Date("2019/04/17"),y=80, labels="FY2018 ER", cex=.8, srt=90, col="darkgrey")
- abline(v=c(as.Date("2019/05/14")), col="green", lty="dotted")
- text(x=as.Date("2019/06/15"),y=80, labels="Q1 2019 ER", cex=.8, srt=90, col="green")
- # Overlay our ad-hoc model of enrollment (patients per month) on the Active Trial Sites graph
- # you might notice I introduced a slight inflexion in recruitment numbers as the number of sites climbs.
- # This matches the observations of http://www.appliedclinicaltrialsonline.com/forecast-enrollment-rate-clinical-trials-0
- # i.e. the efficacy of recruitment diminishes when the number of sites raises
- par(new=TRUE)
- plot( Patients ~ Date, e, axes=FALSE, ann=FALSE, col='red', xlim= c(head(ct_sites$Date,1), tail(ct_sites$Date,1)) )
- axis(4, e$Patients, col='red')
- legend("bottomright", inset=.05, title="Legend:", c("Num. of Active CT sites","Num. of Patients per Month"), fill=c("green","red"), cex=.8)
- #abline(v=c(as.Date("2018/01/19")), col="red", lty="longdash")
- text(x=as.Date("2017/05/01"), y=5, labels="Rate drop (cf.Q3 2017 CC)⇨", cex=.8, srt=90, col="red")
- dev.off()
- # We may look at a maximum of 60 months after first patient recruited
- x <- 1:60
- # Define our K-M curves, a bit steeper than in simulated ITT of Page 6 of Corporate Pres of January
- # because there were only 50% VHR pts in that simulation. We have 70%+ in INSPIRE.
- #
- # Note the curves were first built on good old graph paper as a medium term between ITT and VHR curves of corporate documents,
- # then converted to math functions using Eureqa - not really needed, but extremely fun!
- #
- # rig <- ifelse(x<25, 61.13 + 1.072*x + 38.95*exp(-0.058*x^2) + 0.03657*x*exp(2*log(x)) - 0.7058*x^2 - 0.000548*x^4 , 2)
- # bsc <- ifelse(x<16, 142 + 1.559*x^2 + 0.02612*exp(0.2872*x) - 25.67*x - 0.03251*x^3 - 41.99*exp(-0.9706*x) - 13.65*x^3*exp(-0.9706*x) , 0)
- # revert to using unmodified ITT curves. Thinking more about it, the 20% difference in VHR population is probably already selected
- # by the more stringent criteria used to build those curves from ONTIME data..
- rig <-c(99,95,89,82,72,62,57,51,44,44,39,37,35,31,29,26,21,15,9,7,7,7,rep(5,10),rep(2,15),rep(0,22))
- bsc <-c(98,85,65,54,40,38,35,35,29,23,20,14,14,14,9,9,rep(0,54))
- #bsc <- rig
- # End of Sept 2019: only 75% of events have occured. Ouch. We need to use softer curves ;-(
- # We'll go for Garcia Manero 2016's ONTIME curves for Primary HMA failure patients,
- # just steepened a tad to account for other factors (VHR++, age). HR is probably ~0.68
- #### Wait! TANG.. this is steeper than INSPIRE simul ITT! Something's wrong..
- #rig <- c(93,87,79,72,66,59,57,51,47.5,41.5,35.5,32,28,23,18.5,17.5,13,11,10,8,6.2,rep(5,4),rep(4.5,4),rep(3,6),rep(2,10),rep(0,15))
- #bsc <- c(93,80,64,51,42,34.5,32,29,27,25,23,21.5,20,18.5,17,13,9.7,9,8.2,5.5,5,rep(3.5,3),3,2,2,rep(0,33))
- vhr_rig <- c(100,90,76,65,60,53,47,40,31,26,20,16,12,10,8,7,5,4,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,rep(1,37))
- vhr_bsc <- c(96,73,37,25,16,11,6,3,1,rep(0,61))
- # Plot them to visually check consistency
- png("INSPIRE_ITT_simulated_km_survival_curves.png")
- plot( head(x,24), head(rig,24), col='blue', type='l',axes=FALSE, ylim=c(0,100),
- ylab="Overall Survival (%)", xlab="Months from Randomization", main="Simulation of INSPIRE's ITT K-M Curves")
- par(new=TRUE)
- plot( head(x,24), head(vhr_rig,24), col='grey', type='l', ann=FALSE, axes=FALSE, ylim=c(0,100))
- par(new=TRUE)
- plot( head(x,24), head(bsc,24), col='red', type='l', ann=FALSE, axes=FALSE, ylim=c(0,100))
- par(new=TRUE)
- plot( head(x,24), head(vhr_bsc,24), col='gray', type='l', ann=FALSE, axes=FALSE, ylim=c(0,100))
- axis(side = 1, at = seq(1,24,1))
- axis(side = 2, at = seq(0,100,10))
- box()
- abline(h=(seq(0,100,10)), col="lightgray", lty="dotted")
- abline(v=(seq(0,24,.5)), col="lightgray", lty="dotted")
- legend("topright", inset=.05, title="MOS: BSC ~4.1 mo/ RIG ~7.9 mo", c("BSC","RIG"), fill=c("red","blue"), horiz=TRUE)
- legend("right", inset=.05, col="lightgray", box.col='gray', c("VHR K-M Curves"), fill=c("gray"), horiz=TRUE)
- dev.off()
- # Main computation function
- getEventsFor <- function(date) {
- p <- e$Patients[ match(date, e$Date) : length(e$Patients) ]
- n <- length(p)
- ev <- p * ( 0.66*(1-(rig[1:n]/100)) + 0.33*(1-(bsc[1:n]/100)) )
- ev <- list(ev, p)
- names(ev) <- c("Events", "Population")
- return(ev)
- }
- getVHREventsFor <- function(date) {
- p <- e$Patients[ match(date, e$Date) : length(e$Patients) ]
- p <- p * .7
- n <- length(p)
- ev <- p * ( 0.66*(1-(vhr_rig[1:n]/100)) + 0.33*(1-(vhr_bsc[1:n]/100)) )
- ev <- list(ev, p)
- names(ev) <- c("Events", "Population")
- return(ev)
- }
- # Let's see if our simulation can compute a number of events
- # close to what was observed at Interim Review:
- #
- # If we knew when exactly the Interim target of 88 events was reached, we could tune the model with more accuracy.
- # If you have this information, please contact me.
- # We know the Interim Report was published on January 19th 2018.
- # My estimate is that the target was hit about a month before that date
- # (more precisely: around the 6th of December,as there was a significant update on the CT site that day)
- # If you have experience on CT Interim Reviews, please tell me if this timeline is adequate.
- message("Checkpoints:")
- message("------------")
- ev <- getEventsFor(as.Date("2018-01-01"))
- message("Estimated # of events by end of December 2017 (should be 94 +/-5%): ", floor(sum(ev$Events)))
- p <- e$Patients[ match(as.Date("2018-02-01"), e$Date) : length(pts) ]
- message("Estimated # of patients recruited at Interim Review (should be 175 +/-5): ", sum(p))
- p <- e$Patients[ match(as.Date("2019-04-01"), e$Date) : length(pts) ]
- message('On 19/03/25 accrual "ha[s] passed the 75% completion" (should have 277 +/-4): ', sum(p))
- p <- e$Patients[ match(as.Date("2019-11-01"), e$Date) : length(pts) ]
- message('On 19/10/24 accrual is "approaching 90 percent or 324" (should have 320 +/-4): ', sum(p))
- message("")
- message("Simulation:")
- message("-----------")
- # Now estimate the number of death events up to 1st of March 2021:
- toPredict <- seq(as.Date("2018-03-01"), as.Date("2021-03-01"), by="months")
- accEvents <- c()
- accPopulation <- c()
- accVHREvents <- c()
- accVHRPopulation <- c()
- original_full_acc=TRUE
- for (date in as.list(toPredict)) {
- ev <- getEventsFor(date)
- vhr_ev <- getVHREventsFor(date)
- accEvents <- c(accEvents, floor(sum(ev$Events)))
- accPopulation <- c(accPopulation, sum(ev$Population))
- if (original_full_acc && tail(accPopulation,1) > 225) {
- message('[ full original accrual of INSPIRE (225 pts) reached ]') # should have happened "in first half of 2018" according to Q3 2017cc
- original_full_acc=FALSE
- }
- accVHREvents <- c(accVHREvents, floor(sum(vhr_ev$Events)))
- accVHRPopulation <- c(accVHRPopulation, floor(sum(vhr_ev$Population)))
- message("Estimated number of events by 1st of ", format(date, "%B %Y"), ": ", tail(accEvents, 1), " (Population: ", tail(accPopulation, 1),')',
- " [VHR events: ", tail(accVHREvents, 1), " VHR population: ", tail(accVHRPopulation, 1), ']')
- }
- results <- data.frame(toPredict, accEvents, accPopulation)
- names(results) <- c("Date", "Events", "Population")
- # Let's graph this
- png("INSPIRE_ITT_simulated_events_and_population.png")
- plot( Events ~ Date, results, col='blue', type='l', ylim=c(0,360), xaxt="n",
- ylab="", xlab="Date", main="Simulation of INSPIRE's Events and Population Over Time", sub=paste("Enrollment model: ", model_name), col.sub="red")
- par(new=TRUE)
- plot( Population ~ Date, results, ylim=c(0,360), col='salmon', type='h', ann=FALSE, axes=FALSE)
- axis(1, results$Date, format(results$Date, "%b %y"), cex.axis = .7, las=2,)
- abline(h=(seq(0,360,10)), col="lightgray", lty="dotted")
- abline(h=c(288), col="blue", lty="longdash")
- abline(h=c(360), col="salmon",lty="longdash")
- legend("bottomleft", inset=.05, c("Number of Events","Number of Patients"), fill=c("blue","salmon"))
- text(x=as.Date("2018/12/01"),y=280, labels="Target for topline analysis", cex=.8, col="blue")
- text(x=as.Date("2018/12/01"),y=350, labels="Full accrual", cex=.8, col="salmon")
- dev.off()
- results <- data.frame(toPredict, accVHREvents, accVHRPopulation)
- names(results) <- c("Date", "Events", "Population")
- png("INSPIRE_VHR_simulated_events_and_population.png")
- plot( Events ~ Date, results, col='blue', type='l', ylim=c(0,252), xaxt="n",
- ylab="", xlab="Date", main="Simulation of INSPIRE's VHR Events and Population Over Time", sub=paste("Enrollment model: ", model_name), col.sub="red")
- par(new=TRUE)
- plot( Population ~ Date, results, ylim=c(0,252), col='salmon', type='h', ann=FALSE, axes=FALSE)
- axis(1, results$Date, format(results$Date, "%b %y"), cex.axis = .7, las=2,)
- abline(h=(seq(0,252,10)), col="lightgray", lty="dotted")
- abline(h=c(139), col="blue", lty="longdash")
- abline(h=c(252), col="salmon",lty="longdash")
- legend("bottomleft", inset=.05, c("Number of Events","Number of Patients"), fill=c("blue","salmon"))
- text(x=as.Date("2018/12/01"),y=130, labels="Target for VHR topline analysis", cex=.8, col="blue")
- text(x=as.Date("2018/12/01"),y=245, labels="Full VHR accrual (observed)", cex=.8, col="salmon")
- dev.off()
Add Comment
Please, Sign In to add comment