The purpose of this document is to provide exploratory plots and R code for assessing dose-response and exposure-response using the RECIST (Response Evaluation Criteria In Solid Tumors) endpoint data. These plots provide insight into whether a dose-response relationship is observable. In Phase 1, often no dose-response relationship is observed due to high inter-subject variability. In that situation, we recommened that no longitudinal, NLME modeling be performed as it is unlikely to add much value. Furthermore, even if an exposure-response relationship is detected, it must be interpreted with extreme caution when it cannot be confirmed with simple explortaory graphics.
The plots presented here are based on blinded merged RECIST data (download dataset) with nmpk and and dose history datasets (download dataset). Data specifications can be accessed on Datasets and Rmarkdown template to generate this page can be found on Rmarkdown-Template.
Standard statistical plots for summarizing recist (e.g. Waterfall) are presented elsewhere.
# remove reference to home directory in libPaths
.libPaths(grep("home", .libPaths(), value=TRUE, invert=TRUE))
.libPaths(grep("usr", .libPaths(), value=TRUE, invert=TRUE))
# add localLib to libPaths for locally installed packages
.libPaths(c("localLib", .libPaths()))
# will load from first filepath first, then look in .libPaths for more packages not in first path
# version matches package in first filepath, in the case of multiple instances of a package
# library(rmarkdown)
library(gridExtra)
library(grid)
library(ggplot2)
library(dplyr)
library(RxODE)
library(caTools)
source("../R/xgx_packages_functions.R")
theme_set(theme_bw(base_size=18))
my.data <- read.csv("../Data/Oncology_Efficacy_Data.csv")
dose_record <- read.csv("../Data/Oncology_Efficacy_Dose.csv")
my.data$DOSE_label <- paste(my.data$DOSE_ABC,"mg")
my.data$DOSE_label <- factor(my.data$DOSE_label,levels = c(paste(unique(my.data$DOSE_ABC),"mg")))
my.data.monotherapy = my.data %>% filter(COMB=="Single")
my.data.combo = my.data %>% filter(COMB=="Combo")
# Dose record data preparation for making step function plot
# in order to show any dose reduction during the study
# the idea is that at each time point, you have both the current dose and the previous dose
# the dplyr::lag command implements this
data_areaStep <- bind_rows(old = dose_record,
new = dose_record %>%
group_by(IDSHORT) %>%
mutate(DOSE = lag(DOSE)),
.id = "source") %>%
arrange(IDSHORT, TIME, source) %>%
ungroup() %>%
mutate(DOSE = ifelse(lag(IDSHORT)!=IDSHORT, NA, DOSE),
TIME = TIME/24) #convert to days
data_areaStep.monotherapy = filter(data_areaStep,COMB=="Single")
# calculate average dose intensity up to the first assessment:
# "TIME==57"" is the first assessment time in this dataset
first.assess.time = 57
dose_record <- dose_record %>%
group_by(IDSHORT) %>%
mutate(ave_dose_intensity = mean(DOSE[TIME/24 < first.assess.time]))
dose_intensity <- dose_record[c("IDSHORT","COMB","ave_dose_intensity")]
dose_intensity <- subset(dose_intensity, !duplicated(IDSHORT))
RECIST (Response Evaluation Criteria in Solid Tumor) uses an image-based assessment from X-ray Computed Tomography (CT) scans. and has three components:
An example patient and the criteria for assigning a response category, is shown below.
We recommend stratifying the spaghetti plots of target lesion kinetics by dose so that one can observe if there is more tumor shrinkage at the higher dose groups. In this case, no obvious relationship is observed and it is likely a longitudinal model will not be helpful.
my.data.monotherapy$DOSE_label <- factor(my.data.monotherapy$DOSE_label, levels=c("20 mg", "40 mg", "60 mg", "90 mg"))
gg <- ggplot(data= my.data.monotherapy,aes(x=TIME, y=psld)) +theme_bw()
gg <- gg + geom_line(aes(group=IDSHORT, color =factor(DOSE_label)))
gg <- gg + geom_point(alpha=.3,colour="black")
gg <- gg + guides(color=guide_legend(""),fill=guide_legend(""))
gg <- gg + ylab("Percent Change in\nSum of Longest Diameters")
gg <- gg + xlab("Time (Months)")
gg <- gg + geom_hline(yintercept = 0.0, linetype="dashed")
gg <- gg + scale_x_units(units.input = "day",units.output="month",t.start = 0,t.end = 15, increment = 3)
gg <- gg + theme(text = element_text(size=15))
gg
my.data.monotherapy$DOSE_label <- factor(my.data.monotherapy$DOSE_label, levels=c("20 mg", "40 mg", "60 mg", "90 mg"))
gg <- ggplot(data=my.data.monotherapy,aes(x=TIME, y=psld)) +theme_bw()
gg <- gg + geom_line(aes(group=IDSHORT))
gg <- gg + geom_point(alpha=.3,colour="black")
gg <- gg + guides(color=guide_legend(""),fill=guide_legend(""))
gg <- gg + facet_grid(~DOSE_label)
gg <- gg + ylab("Percent Change in\nSum of Longest Diameters")
gg <- gg + xlab("Time (Months)")
gg <- gg + geom_hline(yintercept = 0.0, linetype="dashed")
gg <- gg + theme(text = element_text(size=15))
gg <- gg + scale_x_units(units.input = "day",units.output="month",t.start = 0,t.end = 15, increment = 3)
gg
These plots allow one to look for subtle trends in the individual trajectories. If changes in the tumor trajectory occur often after a dose reduction, a longitudinal model may be useful for assessing dose-response. However, caution is needed because both dose reductions and resistance acquisition can occur at later times and it may not be easy to deterimne which is the cause of the change in tumor trajectory.
In the first plot below, it is difficult to see a trend of dose changes impacting response and a longitudinal tumor-size model will likely not be informative. But in the second plot below, a trend of tumor shrinkage followed by tumor growth after a dose reduction was observed and a longitudinal model was useful for characterizing this data. see reference
Example 1, longitudinal model won’t be informative:
# This part is optional to label "OR" in the plot
# "OR" can be substituted with other information, such as non-target, new target lesions
# make the OR label for the plot
my.data.label <- my.data %>%
group_by(IDSHORT) %>%
mutate(label_psld = as.numeric(ifelse(TIME==TIME_OR , psld,""))) %>%
filter(!(is.na(label_psld) | label_psld==""))
dose.shift = 50
dose.scale = 1.2
data_areaStep.monotherapy = data_areaStep.monotherapy %>%
mutate(DOSE.shift = DOSE/dose.scale+dose.shift)
dose.unique = c(0,unique(my.data.monotherapy$DOSE_ABC))
gg <- ggplot(data = my.data.monotherapy)
gg <- gg + geom_point(mapping = aes(y= psld, x= TIME))
gg <- gg + geom_text(data= my.data.label,aes(y= label_psld, x= TIME_OR, label=OR), vjust=-.5)
gg <- gg + geom_hline(aes(yintercept = 0),size=0.25,linetype="dashed", colour="red")
gg <- gg + geom_line(mapping = aes(y= psld, x= TIME))
gg <- gg + geom_ribbon(data= data_areaStep.monotherapy,
aes( ymin = 50, ymax = DOSE.shift , x= TIME),
fill="palegreen2", color = "black", alpha=0.5 )
gg <- gg + facet_wrap(~IDSHORT, ncol = 6)
gg <- gg + scale_y_continuous(
sec.axis = sec_axis(~(.-dose.shift)*dose.scale, name = "Dose(mg)", breaks = dose.unique))
gg <- gg + labs(y = "Percent Change in\nSum of Longest Diameters",
x = "Time (months)",
colour = "Parameter")
gg <- gg + scale_x_units(units.input = "day",units.output="month",t.start = 0,t.end = 15, increment = 3)
gg <- gg + theme(text = element_text(size=15))
gg
Example 2, dose reduction was followed by growth of tumor and longitudinal model would help.
To visualize dose-response (exposure-response), we recommend plotting the assigned dose group (or exposure) vs best overall change in tumor size.
Warning: If you see what appears to be a linear dose-response relationship (e.g. more tumor shrinkage with increasing dose), be very cautious when trying to extrapolate outside this range, as Emax may not have been observed yet.
gg <- ggplot()
gg <- gg + geom_point(data=my.data.monotherapy,aes(x=DOSE_ABC,y=BPCHG),color="tomato")
gg <- gg + scale_x_continuous(breaks=unique(my.data.monotherapy$DOSE_ABC))
gg <- gg + geom_hline(yintercept=0,color="grey50", linetype = "dashed")
gg <- gg + scale_y_continuous(labels=scales::percent)
gg <- gg + labs(x="ABC123 Dose (mg)", y="Best Percent Change \n in Tumor Size (%)")
gg <- gg + theme(legend.title=element_blank())
gg <- gg + theme(text = element_text(size=15))
gg
gg <- ggplot()
gg <- gg + geom_point(data=my.data.monotherapy,aes(x=auc0_24,y=BPCHG), color="tomato")
gg <- gg + geom_hline(yintercept=0,color="grey50", linetype = "dashed")
gg <- gg + scale_y_continuous(labels=scales::percent)
gg <- gg + labs(x="ABC123 AUC0-24 (ng.h/ml)", y="Best Percent Change \n in Tumor Size (%)")
gg <- gg + theme(legend.title=element_blank())
gg <- gg + theme(text = element_text(size=15))
gg
Best overall change is commonly reported in Waterfall plots. And the reason we recommend using the randomized dose group as opposed to something like average dose-intensity is that the latter can be confounded. For instance, consider the following scenario. There are two patients that both start at 10 mg. One has 30% tumor growth at the first assessment and progresses. The other responds, but at month 6, they are dose reduced to 5 mg due to an adverse event, and then followed for another 6 months. In this case, the responder would have a dose intensity of 7.5 mg while the non-responder had a dose-intensity of 10 mg, and thus one might conclude from a simple plot of these two patients that lower doses are better.
One way to avoid this issue is to plot average dose intensity up to the first assessment vs percent change at the first assessment and this could be considered if there are a large number of dose reductions before the first assessment.
# Do it only for monoteraphy, it can be repeated for combo
dose_intensity <- dose_intensity[dose_intensity$COMB=="Single",]
my.data.assessment <- my.data.monotherapy %>%
merge(dose_intensity ,by="IDSHORT")
gg <- ggplot()
gg <- gg + geom_point(data=my.data.assessment[my.data.assessment$TIME==first.assess.time,],
aes(x=my.data.assessment$ave_dose_intensity[my.data.assessment$TIME == first.assess.time],
y=my.data.assessment$psld[my.data.assessment$TIME == first.assess.time]),color="tomato")
gg <- gg + scale_x_continuous(breaks=c(20,40, 60, 74,78,90))
gg <- gg + geom_hline(yintercept=0,color="grey50", linetype = "dashed")
gg <- gg + labs(x="Average ABC123 dose (mg) \n up to the first assessment", y=" Percent Change in Tumor Size \n up to the first assessment")
gg <- gg + theme(legend.title=element_blank())
gg <- gg + theme(text = element_text(size=15))
gg
When two drugs are combined (ABC123 + DEF456), it can be a useful first step to look at the dose-response relationship of each drug. However, it should be noted that provide only a limited understanding of the data.
gg <- ggplot()
gg <- gg + geom_point(data=my.data.monotherapy,aes(x=DOSE_ABC,y=BPCHG, color="ABC123"))
gg <- gg + geom_point(data=my.data.combo,aes(x=DOSE_ABC,y=BPCHG,
color = factor(DOSE_DEF, labels = c("ABC123 + 300 mg DEF456", "ABC123 + 400 mg DEF456"))))
gg <- gg + labs(color = "DEF456 (mg) dose")
gg <- gg + geom_hline(yintercept=0,color="grey50", linetype = "dashed")
gg <- gg + scale_x_continuous(breaks=unique(my.data[my.data$COMB=="Single",]$DOSE_ABC))
gg <- gg + labs(x="ABC123 Dose (mg)",y="Best Percent Change \n in Tumor Size (%)")
gg <- gg + scale_y_continuous(labels=scales::percent)
gg <- gg + theme(legend.title=element_blank())
gg <- gg + theme(text = element_text(size=15))
gg
gg <- ggplot(data=my.data.combo,aes(x=DOSE_DEF,y=BPCHG))
gg <- gg + geom_point(aes( color= factor(DOSE_ABC,labels=c("+ 20 mg ABC123",
"+ 40 mg ABC123",
"+ 60 mg ABC123"))))
gg <- gg + geom_hline(yintercept=0,color="grey50", linetype = "dashed")
gg <- gg + scale_x_continuous(breaks=unique(my.data.combo$DOSE_DEF))
gg <- gg + labs(x="DEF456 Dose (mg)",y="Best Percent Change \n in Tumor Size (%)")
gg <- gg + scale_y_continuous(labels=scales::percent)
gg <- gg + theme(legend.title=element_blank())
gg <- gg + theme(text = element_text(size=15))
gg
For most of the oncology clinical trials, the standard primary and secondary endpoints are Overall Survival (OS), Progression Free Survival (PFS), duration without progression (PD) or death, and Overall Response Rate (ORR). Dose vs ORR at each dose can be informative.
gg <- ggplot(data = my.data.combo, aes(x=DOSE_ABC,y=PR_rate))+theme_bw()
gg <- gg + stat_summary(aes(x=DOSE_ABC,y=binary_BOR, group=DOSE_ABC), geom="errorbar",
fun.data = function(y){
data.frame(ymin = binom::binom.exact(sum(y), length(y), conf.level = 0.95)$lower,
ymax = binom::binom.exact(sum(y), length(y), conf.level = 0.95)$upper)
}, alpha=0.5, size = 1, width = 0.1)
gg <- gg + stat_summary(geom="point", fun.y=mean, size=3, color="tomato")
gg <- gg + scale_x_continuous(breaks=unique(my.data[my.data$COMB=="Combo",]$DOSE_ABC))
gg <- gg + scale_y_continuous(labels=scales::percent)
gg <- gg + ylab("Response Rate (%)")
gg <- gg + xlab("ABC123 Dose (mg)")
gg <- gg + theme(text = element_text(size=15))
gg
gg <- ggplot(data=my.data.combo,aes(x=DOSE_ABC ,y=DOSE_DEF,fill=PR_rate))
gg <- gg + scale_fill_gradient(low="orange", high="green")
gg <- gg + geom_segment(data=my.data.combo, aes(x=-Inf,xend=20,y=30,yend=30), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_segment(data=my.data.combo, aes(x=20,xend=20,y=-Inf,yend=30), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_segment(data=my.data.combo, aes(x=-Inf,xend=40,y=30,yend=30), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_segment(data=my.data.combo, aes(x=40,xend=40,y=-Inf,yend=30), color=rgb(0.5,0.5,0.5))
# gg <- gg + geom_segment(data=my.data.combo, aes(x=-Inf,xend=450,y=300,yend=300), color=rgb(0.5,0.5,0.5))
# gg <- gg + geom_segment(data=my.data.combo, aes(x=450,xend=450,y=-Inf,yend=300), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_segment(data=my.data.combo, aes(x=-Inf,xend=60,y=30,yend=30), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_segment(data=my.data.combo, aes(x=60,xend=60,y=-Inf,yend=30), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_segment(data=my.data.combo, aes(x=-Inf,xend=40,y=40,yend=40), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_segment(data=my.data.combo, aes(x=40,xend=40,y=-Inf,yend=40), color=rgb(0.5,0.5,0.5))
gg <- gg + geom_label(data=unique(my.data.combo[,c("DOSE_DEF","DOSE_ABC","PR_rate","n","count_cr_pr")]),
aes(x=DOSE_ABC ,y=DOSE_DEF*1.00,fontface=2, label=paste0(round(PR_rate,2)*100,
"% ORR \nn=",n,"\nn(CR|PR)=",count_cr_pr)))
gg <- gg + scale_x_continuous(breaks=c(20, 40, 60))
gg <- gg + scale_y_continuous(breaks=c(30, 40))
gg <- gg + ylab("DEF456 Dose (mg)")
gg <- gg + xlab("ABC123 Dose (mg)")
gg <- gg + theme(legend.title = element_blank(),
plot.title=element_text(hjust=0.5),
text = element_text(size=20),
title = element_text(size=20))
gg <- gg + guides(fill=FALSE)
gg
sessionInfo()
## R version 3.4.3 (2017-11-30)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Red Hat Enterprise Linux Server 7.4 (Maipo)
##
## Matrix products: default
## BLAS/LAPACK: /CHBS/apps/intel/17.4.196/compilers_and_libraries_2017.4.196/linux/mkl/lib/intel64_lin/libmkl_gf_lp64.so
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## attached base packages:
## [1] grid stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] reshape_0.8.7 lubridate_1.7.1 survival_2.41-3 DT_0.2
## [5] RxODE_0.6-1 bindrcpp_0.2 haven_1.1.0 readr_1.1.1
## [9] readxl_1.0.0 xtable_1.8-2 tidyr_0.7.2 caTools_1.17.1
## [13] zoo_1.8-0 dplyr_0.7.4 ggplot2_2.2.1 gridExtra_2.3
##
## loaded via a namespace (and not attached):
## [1] purrr_0.2.4 reshape2_1.4.3 splines_3.4.3
## [4] lattice_0.20-35 colorspace_1.3-2 htmltools_0.3.6
## [7] yaml_2.1.16 rlang_0.1.6 pillar_1.0.1
## [10] glue_1.2.0 RColorBrewer_1.1-2 binom_1.1-1
## [13] bindr_0.1 plyr_1.8.4 stringr_1.2.0
## [16] munsell_0.4.3 gtable_0.2.0 cellranger_1.1.0
## [19] htmlwidgets_0.9 codetools_0.2-15 evaluate_0.10.1
## [22] memoise_1.1.0 labeling_0.3 knitr_1.18
## [25] forcats_0.2.0 rex_1.1.2 markdown_0.8
## [28] Rcpp_0.12.14 scales_0.5.0 backports_1.1.2
## [31] jsonlite_1.5 hms_0.4.0 digest_0.6.13
## [34] stringi_1.1.3 rprojroot_1.3-1 tools_3.4.3
## [37] bitops_1.0-6 magrittr_1.5 lazyeval_0.2.1
## [40] tibble_1.4.1 pkgconfig_2.0.1 Matrix_1.2-12
## [43] rsconnect_0.8.5 assertthat_0.2.0 rmarkdown_1.8
## [46] R6_2.2.2 compiler_3.4.3