# --------------------------------------------------------------
# Packages
# --------------------------------------------------------------
<- c("survival", "cmprsk", "survminer", "msm", "mstate",
packages "tidyverse","Hmisc", "gt", "survRM2", "PBIR",
"gtsummary", "labelled", "swimplot", "ggplot2")
for (i in 1:length(packages)){library(packages[i], character.only = TRUE)}
Duration of and time to response in oncology clinical trials from the perspective of the estimand framework
Code accompanying the paper: Case Study in Mantle Cell Lymphoma (Section 4)
1 Background
Code to illustrate Mantle Cell Lymphoma case study in Section 4 of the paper Weber et al. (2023+) written by the duration of response task force of the oncology estimand WG.
2 Purpose of this document
This R markdown file provides easy accessible code to reproduce computations in the paper. The github repository where this document is hosted is available here.
3 Setup
3.1 Packages
3.2 Get data
The source data provide the response status at time points \(C_2, \ldots, C_{22}\). The data set has no deaths. For assessing time to response (TTR), duration of response (DOR), or overall response rate (ORR) deaths are handled in the same way as progression. The dataset also has no data on intercurrent events such as e.g. start of new therapy. Thus, we assume none occurred. A treatment cycle has 28 days.
# --------------------------------------------------------------
# Data
# --------------------------------------------------------------
# Reading response assessments
<- read_csv("Case_study.csv", show_col_types = FALSE)
dat
# Reading start of new therapies
# New antineoplastic therapies are not recorded after documentation of progression.
# After start of new antineoplastic therapies no response assessment is performed.
<- read_csv("Case_study_ANP.csv", show_col_types = FALSE)
anp
# Source data
gt(dat) %>%
tab_header(title = "Listing: Responses by cycle") %>%
tab_style(
style = list(cell_text(weight = "bold")),
locations = cells_title()) %>%
tab_style(
style = list(cell_text(weight = "bold")),
locations = cells_column_labels())
Listing: Responses by cycle | |||||||||
ID | C2 | C4 | C6 | C8 | C10 | C13 | C16 | C19 | C22 |
---|---|---|---|---|---|---|---|---|---|
1 | SD | NA | NA | NA | NA | NA | NA | NA | NA |
2 | SD | PD | NA | NA | NA | NA | NA | NA | NA |
3 | SD | SD | PD | NA | NA | NA | NA | NA | NA |
4 | SD | PR | CR | CR | CR | CR | CR | CR | CR |
5 | PR | PR | CR | CR | CR | CR | CR | CR | CR |
6 | SD | SD | SD | SD | SD | NA | NA | NA | NA |
7 | PR | PR | CR | CR | CR | CR | CR | CR | NA |
8 | SD | SD | SD | PR | PR | PR | PR | PD | NA |
9 | SD | SD | PR | PR | PR | PR | PR | NA | NA |
10 | PR | PD | NA | NA | NA | NA | NA | NA | NA |
11 | PR | PR | CR | CR | CR | CR | CR | NA | NA |
12 | SD | PR | PR | CR | CR | CR | CR | NA | NA |
13 | SD | NA | NA | NA | NA | NA | NA | NA | NA |
14 | PR | CR | CR | CR | CR | CR | CR | CR | NA |
15 | CR | CR | NA | NA | NA | NA | NA | NA | NA |
16 | SD | PR | PR | CR | CR | CR | CR | CR | NA |
17 | PR | PR | PD | NA | NA | NA | NA | NA | NA |
18 | PR | PR | PR | PR | PR | PR | PR | PR | NA |
19 | PD | NA | NA | NA | NA | NA | NA | NA | NA |
20 | SD | SD | SD | CR | CR | CR | CR | CR | NA |
21 | SD | PR | CR | NA | NA | NA | NA | NA | NA |
22 | SD | SD | SD | SD | SD | NA | NA | NA | NA |
23 | PR | PR | PR | PR | PR | PR | PR | PR | NA |
24 | PR | PR | PR | PR | PR | PR | PR | PR | NA |
25 | PR | PR | PR | CR | CR | CR | CR | CR | NA |
26 | CR | CR | PD | NA | NA | NA | NA | NA | NA |
27 | PR | PR | CR | CR | CR | CR | CR | NA | NA |
30 | PR | PR | PR | PR | PR | PR | PR | NA | NA |
31 | PR | PR | PR | PR | CR | CR | CR | NA | NA |
32 | PR | PR | CR | CR | CR | CR | NA | NA | NA |
gt(anp) %>%
tab_header(title = "Listing: Start of new therapies") %>%
tab_style(
style = list(cell_text(weight = "bold")),
locations = cells_title()) %>%
tab_style(
style = list(cell_text(weight = "bold")),
locations = cells_column_labels())
Listing: Start of new therapies | |||||||||
ID | C2 | C4 | C6 | C8 | C10 | C13 | C16 | C19 | C22 |
---|---|---|---|---|---|---|---|---|---|
1 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
2 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
3 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
4 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
5 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
6 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
7 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
8 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
9 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
10 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
11 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
12 | NA | NA | NA | NA | NA | NA | NA | ANP | NA |
13 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
14 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
15 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
16 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
17 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
18 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
19 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
20 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
21 | NA | NA | NA | ANP | NA | NA | NA | NA | NA |
22 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
23 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
24 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
25 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
26 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
27 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
30 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
31 | NA | NA | NA | NA | NA | NA | NA | NA | NA |
32 | NA | NA | NA | NA | ANP | NA | NA | NA | NA |
3.3 Data preparation
# --------------------------------------------------------------
# Data preparation
# --------------------------------------------------------------
# Don't need Patient ID column
<- dat[, -1]
data <- anp[, -1]
anpa
# Time of assessment in Cycles
<- as.numeric(sub('.', '', colnames(data)))
cycles
# cycle duration in days
<- 28
cycle.duration
<- dim(data)[1]
n
# data for ignoring responses after start of new therapies
# Start of new therapies?
<- function(x){
Any.anp # x: data matrix
ifelse(any(x == "ANP", na.rm=TRUE), TRUE, FALSE)
}<- apply(anp, 1, Any.anp)
any.anp
<- numeric(n)
anpr for(i in 1:n){
if(any.anp[i]){
<- min(which(anpa[i, ] %in% c("ANP"))) }
anpr[i] else{
<- c(NA)}
anpr[i]
}
<-data
datax<-length(data)
mfor(i in 1:n){
for (ii in 1:m){
<- ifelse(!is.na(anpr[i]) & anpr[i] <= ii,NA,data[i, ii])
datax[i, ii] }}
3.4 Endpoint derivations
Endpoint derivations below work for
- complete data,
- with no missing assessments prior to last assessment,
- no assessments after progression and
- all patients have at least 1 assessment before drop-out/death.
This is the case with the data set in the case study.
# --------------------------------------------------------------
# Endpoint derivations
# --------------------------------------------------------------
# Pick the best response by patient
<- function(x){
BOR # x: data matrix
if(any(x == "CR", na.rm = TRUE)){out <- "CR"}
else{
if(any(x == "PR", na.rm = TRUE)){out <- "PR"}
else{
if(any(x == "SD", na.rm = TRUE)){out <- "SD"}
else{
<- "PD"
out
}
}
}
}
<- apply(data, 1, BOR)
bor
# Best overall response is CR or PR
<- function(x){
OR # x: BOR vector
ifelse(x == "CR" | x == "PR", TRUE, FALSE)
}
<- OR(bor)
or
# TTANP
# Time when ANP is given the first time
<- numeric(n)
ttanp for(i in 1:n){
if(any.anp[i]){
<- cycles[anpa[i, ] %in% c("ANP")] }
ttanp[i] else {
<- NA}
ttanp[i]
}
# For displaying ANP we move the time right after the last response assessment
# depending on the visit schedule
<- ifelse(ttanp <= 10, ttanp - 2, ttanp - 3)
ttanp.disp
# Any assessment with PD?
<- function(x){
Any.PD # x: data matrix
ifelse(any(x == "PD", na.rm = TRUE), TRUE, FALSE)
}
<- apply(data, 1, Any.PD)
any.pd
# OR and an assessment with PD?
<- ifelse(or == TRUE & any.pd, TRUE, FALSE)
or.pd
# No OR but an assessment with PD?
<- ifelse(or == FALSE & any.pd, TRUE, FALSE)
noor.pd
# No OR or an assessment with PD?
<- ifelse(or == FALSE | any.pd, TRUE, FALSE)
noor.orpd
# Last assessment in study (cycle number)
# last = last assessment regardless of ANP
<- numeric(n)
last for(i in 1:n){
<- cycles[length(data[i, which(!is.na(data[i,]))])]
last[i]
}
# last.anp = last assessment prior to start of new therapies
<- numeric(n)
last.anp for(i in 1:n){
<- cycles[length(datax[i, which(!is.na(datax[i,]))])]
last.anp[i]
}
# TTR: Time to response.
# For patients with OR use date of first assessment with OR (response event)
# For patients with no OR nor PD use last assessment (censored)
# For patients with no OR and a PD use last assessment (censored, competing risk PD).
<- numeric(n)
ttr for(i in 1:n){
if(or[i]){
<- cycles[min(which(data[i, ] %in% c("CR", "PR")))]}
ttr[i] else{
<- last[i]}
ttr[i]
}
# DOR: Duration of response
# For patients with no response: 0
# For patients with response and no PD: Date of last assessment - Date of
# response (censored).
# For patients with response and PD:
# Date of last assessment - Date of response (event)
<- ifelse(or, last - ttr, 0)
dor
# DOR: ANP
<- ifelse(or, last.anp - ttr, 0)
dor.anp
# For time in response: status
<- ifelse(noor.orpd, 1, 0)
tir.status <- ifelse(or.pd, 1, tir.status)
tir.status
# For competing risks: TTR status 1 = OR, 2 = no OR but PD, 0 = no OR, no PD
<- ifelse(or, 1, 0)
ttr.status !or & noor.pd] <- 2
ttr.status[
# TTP: Time to progression (equivalent to PFS in this dataset)
# For patients with PD based on PD assessment date (=event)
# For patients without PD based on last assessment date (=censored)
# Note that in this data set, if PD occurs, it is at the last assessment date.
# Note that in this data set there are no deaths.
# Therefore in this data set, ttp = pfs = last assessment
<- ifelse(any.pd, 1, 0)
ttp.status <- last
ttp <- last.anp
ttp.anp
# Use last observed follow-up as date of TTR if progression (Table 3, estimand 2)
<- ifelse(any.pd == 1 & or == 0, max(ttp), ttr)
sttr <- ifelse(any.pd == 1 & or == 0, max(ttp.anp), ttr)
sttr.anp
# TTP censored at response
# For patients with OR: response date
# For all other patients: last
<- ifelse(or, ttr, last)
ttp.cens
# Progression at study end
<- ifelse(any.pd == TRUE, ttp, NA)
prog <- ifelse(any.pd == TRUE, ttp, NA) # no patient with both PD and ANP
prog.anp
<- ifelse(any.pd == TRUE, "Progression", NA)
Progression <- ifelse(any.anp == TRUE, "ANP", NA)
ANP
# Progression, death, ANP or response events
<- ifelse(!is.na(Progression), Progression, ANP)
EoFUP <- ifelse(!is.na(prog), prog, ttanp.disp)
EoFUP.time <- cbind(dat, bor, or, any.pd, or.pd, noor.pd, noor.orpd, last,
data.ext
ttr, sttr, dor, ttp, ttp.status, ttp.cens, tir.status,
any.anp, ttanp, ttanp.disp, prog, Progression, ANP, EoFUP,EoFUP.time)<- cbind(data.frame(dat[,1]), data.frame(datax), bor, or, any.pd, or.pd, noor.pd, noor.orpd, last.anp,
data.anp.ext
ttr, sttr.anp, dor.anp, ttp.anp, ttp.status, ttp.cens, tir.status, any.anp, ttanp, ttanp.disp, prog.anp, Progression, ANP, EoFUP, EoFUP.time)
4 BOR and ORR
Consider any best response of PR or CR.
# --------------------------------------------------------------
# Best overall response
# --------------------------------------------------------------
<- data.ext %>%
rr select(or)
<- rr %>%
orr tbl_summary(label = or ~ "Overall response rate",
digits = list(all_categorical() ~ c(0, 1)))
as_gt(orr)
Characteristic | N = 301 |
---|---|
Overall response rate | 23 (76.7%) |
1 n (%) |
5 Duration of response
5.1 Conditional cDOR (KM analysis, responders only)
# --------------------------------------------------------------
# Conditional duration of response
# --------------------------------------------------------------
# Estimand 1a (KM estimate)
<- subset(data.anp.ext, or == TRUE) # responders only
data.lim
# EoFUP=event indicator, TRUE (1) = event
<- survfit(Surv(dor.anp, !is.na(EoFUP)) ~ 1, data = data.lim)
fit_cDOR1
# Figure 2
ggsurvplot(fit_cDOR1, data = data.lim, risk.table = TRUE, conf.int = F,
title = "Conditional DOR (responders only, hypothetical strategy for ANP)",
submain = "Kaplan-Meier estimates",
break.x.by = 3,
ylab = "Prob(progression or death)",
xlab = "Months from onset of response",
legend.title = "")
# KM estimates
as_gt(tbl_survfit(fit_cDOR1, times = c(6, 9, 12), label_header = "**Month {time}**"))
Characteristic | Month 6 | Month 9 | Month 12 |
---|---|---|---|
Overall | 78% (62%, 97%) | 78% (62%, 97%) | 68% (50%, 91%) |
# Estimand 1b (Proportion of pts in response at time points of interest)
<- subset(data.anp.ext, or == TRUE) # responders only
data.lim $dor6 <- ifelse(data.lim$dor.anp >= 6, 1, 0)
data.lim$dor9 <- ifelse(data.lim$dor.anp >= 9, 1, 0)
data.lim$dor12 <- ifelse(data.lim$dor.anp >= 12, 1, 0)
data.lim
<- data.lim %>%
rr select(dor6, dor9, dor12)
<- rr %>%
r tbl_summary(label = list(dor6 ~ "Proportion of patients in DOR at Month 6",
~ "Proportion of patients in DOR at Month 9",
dor9 ~ "Proportion of patients in DOR at Month 12"),
dor12 digits = list(all_categorical() ~ c(0, 1))) %>%
add_ci(pattern = "{stat} ({ci})",
method = list(dor6 ~ "exact", dor9 ~ "exact", dor12 ~ "exact"))
r
Characteristic | N = 23 (95% CI)1,2 |
---|---|
Proportion of patients in DOR at Month 6 | 18 (78.3%) (56%, 93%) |
Proportion of patients in DOR at Month 9 | 17 (73.9%) (52%, 90%) |
Proportion of patients in DOR at Month 12 | 14 (60.9%) (39%, 80%) |
1 n (%) | |
2 CI = Confidence Interval |
# Estimand 2a
<- subset(data.ext, or == TRUE) # responders only
data.lim
# or.pd=event indicator, TRUE (1) = event
<- survfit(Surv(dor, or.pd) ~ 1, data = data.lim)
fit_cDOR2
# Figure 2
ggsurvplot(fit_cDOR2, data = data.lim, risk.table = TRUE, conf.int = F,
title = "Conditional DOR (responders only, treatment policy)",
submain = "Kaplan-Meier estimates",
break.x.by = 3,
ylab = "Prob(progression or death)",
xlab = "Months from onset of response",
legend.title = "")
as_gt(tbl_survfit(fit_cDOR2, times = c(6, 9, 12), label_header = "**Month {time}**"))
Characteristic | Month 6 | Month 9 | Month 12 |
---|---|---|---|
Overall | 86% (73%, 100%) | 86% (73%, 100%) | 81% (66%, 100%) |
# Estimand 2b (Proportion of pts in response at time points of interest)
<- subset(data.ext, or == TRUE) # responders only
data.lim $dor6 <- ifelse(data.lim$dor >= 6, 1, 0)
data.lim$dor9 <- ifelse(data.lim$dor >= 9, 1, 0)
data.lim$dor12 <- ifelse(data.lim$dor >= 12, 1, 0)
data.lim
<- data.lim %>%
rr select(dor6, dor9, dor12)
<- rr %>%
r tbl_summary(label = list(dor6 ~ "Proportion of patients in DOR at Month 6",
~ "Proportion of patients in DOR at Month 9",
dor9 ~ "Proportion of patients in DOR at Month 12"),
dor12 digits = list(all_categorical() ~ c(0, 1))) %>%
add_ci(pattern = "{stat} ({ci})",
method = list(dor6 ~ "exact", dor9 ~ "exact", dor12 ~ "exact"))
r
Characteristic | N = 23 (95% CI)1,2 |
---|---|
Proportion of patients in DOR at Month 6 | 18 (78.3%) (56%, 93%) |
Proportion of patients in DOR at Month 9 | 18 (78.3%) (56%, 93%) |
Proportion of patients in DOR at Month 12 | 14 (60.9%) (39%, 80%) |
1 n (%) | |
2 CI = Confidence Interval |
# Estimand 3
# Analysis of DOR while-not-using-ANP (All patients, competing risks analysis)
# For competing risks: DOR3 status 1=PD, 2=no PD but ANP, 0=no PD, no ANP
<- ifelse(any.pd, 1, 0)
DOR3.status !any.pd & !any.anp] <- 2
DOR3.status[
<- cmprsk::cuminc(ftime = dor.anp,fstatus = DOR3.status)
fit1 <-timepoints(fit1,times = c(6, 9, 12))
z<-z$est - sqrt(z$var) * (-qnorm(.025))
zcil<-z$est + sqrt(z$var) * (-qnorm(.025))
zciuc("response (95% CI):", 1 - round(c(z$est[1], zcil[1], zciu[1]), 3))
[1] "response (95% CI):" "0.797" "0.944"
[4] "0.65"
ggcompetingrisks(fit1, conf.int = FALSE, multiple_panels = FALSE,
xlab=c("Months from onset of response"))
5.2 Probability of being in response (all patients)
# Estimand 4
# --------------------------------------------------------------
# Probability of being in response [months] (PBIR package)
# --------------------------------------------------------------
<- PBIR1(t2PROGRESSION = data.ext$ttp,
fit_PBIR STATUS_PROGRESSION = data.ext$any.pd,
t2RESPONSE = data.ext$ttr,
STATUS_RESPONSE = data.ext$or,
time = c(6, 9))
# Table 4, row 3
%>%
fit_PBIR gt() %>%
fmt_number(decimals = 2, columns = c(time, PBIR, std, ci.low, ci.up)) %>%
tab_style(
style = list(cell_text(weight = "bold")),
locations = cells_column_labels())
time | PBIR | std | ci.low | ci.up |
---|---|---|---|---|
6.00 | 0.63 | 0.07 | 0.48 | 0.75 |
9.00 | 0.70 | 0.07 | 0.55 | 0.82 |
<- PBIR1(t2PROGRESSION = data.ext$ttp,
fit_PBIR_pl STATUS_PROGRESSION = data.ext$or.pd ,
t2RESPONSE = data.ext$ttr,
STATUS_RESPONSE = data.ext$or)
<- fit_PBIR_pl$time
tt <- fit_PBIR_pl$PBIR
diff <- length(tt) + 1
B <- c(0, tt)
tt <- c(0, diff)
diff
<- rep(tt, rep(2, B))[-1]
tt <- rep(diff, rep(2, B))[-(2 * B)]
diff
plot(range(c(0, max(tt))), range(c(0, 1)),
xlab = "Months from start of treatment", ylab = "PBIR",
lwd = 2, type = "n", main = "Probability of being in response (PIBR)")
lines(tt, diff, lwd = 2, col = 3)
5.3 Mean duration of response (all patients)
# --------------------------------------------------------------
# Mean duration of response [months] (PBIR package)
# --------------------------------------------------------------
# Table 4, row 4
mduration(t2PROGRESSION = data.ext$ttp,
STATUS_PROGRESSION = as.numeric(data.ext$any.pd),
t2RESPONSE = data.ext$ttr,
STATUS_RESPONSE = as.numeric(data.ext$or))
$meandor.est
[1] 5.044052
$meandor.se
[1] 0.4311143
$time.truncation
[1] 10
5.4 Time in response (EMA definition, all patients)
# --------------------------------------------------------------
# Time in response (all patients): EMA definition
# --------------------------------------------------------------
# Estimand 5
# tir.status = event indicator, TRUE (1) = event
<- survfit(Surv(dor, tir.status) ~ 1, data = data.ext)
fit_DOR
ggsurvplot(fit_DOR, data = data.ext, risk.table = TRUE, conf.int = F,
title = "Time in Response (EMA definition, all patients)",
submain = "Kaplan-Meier estimates",
break.x.by = 3,
xlab = "Months from onset of response",
legend.title = "")
as_gt(tbl_survfit(fit_DOR, times = c(6, 9, 12), label_header = "**Month {time}**"))
Characteristic | Month 6 | Month 9 | Month 12 |
---|---|---|---|
Overall | 66% (51%, 86%) | 66% (51%, 86%) | 62% (47%, 83%) |
6 Time to Response
6.1 cTTR (responders only, KM analysis)
# --------------------------------------------------------------
# Conditional TTR
# --------------------------------------------------------------
# Estimand 1
<- subset(data.anp.ext, data.ext$or == 1)
data.res
# or=event indicator, TRUE (1)=event
<- survfit(Surv(data.res$ttr, data.res$or) ~ 1, data = data.res)
fit_cTTR
surv_median(fit_cTTR)
strata median lower upper
1 All 2 2 4
surv_summary(fit_cTTR)
time n.risk n.event n.censor surv std.err upper lower
1 2 23 16 0 0.30434783 0.3152442 0.5645553 0.16407178
2 4 7 4 0 0.13043478 0.5383819 0.3746838 0.04540691
3 6 3 1 0 0.08695652 0.6756639 0.3269101 0.02313002
4 8 2 2 0 0.00000000 Inf NA NA
# KM plot cTTR (Figure 3)
ggsurvplot(fit_cTTR, data = data.res,
risk.table = TRUE,
conf.int = FALSE,
surv.median.line = "h",
main = "Time to Response", submain = "Kaplan-Meier estimates",
xlim = c(0, 10),
break.x.by = 1, ylab = "Prob(achieving response)",
xlab = "Months from start of treatment",
legend.title = "")
6.2 cTTR censoring at last patient last visit (KM analysis)
For this analysis we set the censoring date to the maximum follow-up date observed in the study for patients with progression prior to achieving response.
# --------------------------------------------------------------
# Alternative time to response
# --------------------------------------------------------------
# Estimand 2
# or = event indicator, TRUE (1) = event
<- survfit(Surv(sttr, or) ~ 1, data = data.ext)
fit_cTTR_TP
ggsurvplot(fit_cTTR_TP, data = data, risk.table = TRUE,
title = "Time to Response (censor at last visit, responders only)",
conf.int = FALSE, submain = "Kaplan-Meier estimates",
xlab = "Months from start of treatment",
legend.title = "")
surv_median(fit_cTTR_TP)
strata median lower upper
1 All 2 2 8
surv_summary(fit_cTTR_TP)
time n.risk n.event n.censor surv std.err upper lower
1 2 30 16 2 0.4666667 0.1951800 0.6841389 0.31832390
2 4 12 4 0 0.3111111 0.2824215 0.5411444 0.17886193
3 6 8 1 0 0.2722222 0.3124405 0.5021962 0.14756174
4 8 7 2 0 0.1944444 0.3933979 0.4203939 0.08993622
5 10 5 0 2 0.1944444 0.3933979 0.4203939 0.08993622
6 22 3 0 3 0.1944444 0.3933979 0.4203939 0.08993622
6.3 TTR (while-progression free and alive, all patients, competing risks analysis)
Cumulative incidence function (months). In the figure below, event = 1 represents response, event = 2 represents PD without response. We derive the estimate at Month 6 with confidence interval.
# --------------------------------------------------------------
# Cumulative incidence function (cmprsk package)
# --------------------------------------------------------------
# Estimand 3
<- cmprsk::cuminc(ftime = ttr, fstatus = ttr.status)
fit_CIF
<- timepoints(fit_CIF, times = c(6))
z6 <- z6$est - sqrt(z6$var) * (-qnorm(.025))
z6cil <- z6$est + sqrt(z6$var) * (-qnorm(.025))
z6ciu c("response (95% CI):", 1 - round(c(z6$est[1], z6cil[1], z6ciu[1]), 3))
[1] "response (95% CI):" "0.27" "0.438"
[4] "0.101"
# CIF plot
<- ggcompetingrisks(fit_CIF, conf.int = FALSE, multiple_panels = FALSE,
cif title = "Time to response (CIF, all patients)", palette = "jco",
xlab = "Months from start of treatment")
<-cif +
cif2 scale_color_manual(name = "Event", values = c(1, 2),
labels = c("Response", "Progression without response"))
ggpar(cif2, xlim = c(0, 9.5), xticks.by = 1)
7 6 Swimmer plot
# --------------------------------------------------------------
# Swimmer plot (swimplot package)
# --------------------------------------------------------------
# Figure 4
# Response continues at study end (no progression)
$cont <- ifelse(data.ext$any.pd == FALSE & data.ext$any.anp == FALSE, data.ext$ttp, NA)
data.ext
<- swimmer_plot(df = data.ext, id = 'ID', end = 'last', width = .85,
follow_p name_fill = 'bor')
<- follow_p + swimmer_lines(df_lines = data.ext, id = 'ID', start = 'ttr',
resp end = 'ttp', size = 1, col = c("black")) +
swimmer_points(df_points=data.ext, id = 'ID', time = 'prog', size = 2.5,
fill = 'white', col = 'black', name_shape = "Progression") +
scale_y_continuous(name = "Months from start of treatment",
breaks = c(0, 2, 4, 6, 8, 10, 13, 16, 19, 22)) +
swimmer_arrows(df_arrows = data.ext, id = 'ID', arrow_start = 'cont',
cont = 'any.pd', type = "open", cex = 1, name_col = 'bor') +
scale_fill_manual(name = "Best overall response",
values = c("CR" = "#4daf4a",
"PR" = "#377eb8",
"SD" = 'orange',
"PD" = '#e41a1c')) +
scale_color_manual(name = "Best overall response",
values=c("CR" = "#4daf4a",
"PR" = "#377eb8",
"SD" = 'orange',
"PD" = '#e41a1c')) +
swimmer_points(df_points=data.ext,id='ID',time='EoFUP.time',
name_shape ='EoFUP',size=2.5,fill='white',col='black') +
annotate("text", x = 3.5, y = 28.45,
label = "Continued follow-up", size = 3.25) +
annotate("text", x = 2.5, y = 28.25,
label = sprintf('\u2192'), size = 8.25) +
coord_flip(clip = 'off', ylim = c(0, 23))
resp