This markdown is to validate trtswitch::rpsftm() with
trtswitch::shilong and trtswitch::immdef
data.
| Switching Type | Recensoring | Dataset | Psi | HR | P-value | Counterfactual Time |
|---|---|---|---|---|---|---|
| 1-way | YES | shilong | ✅ | ✅ | ✅ | ✅ |
| 1-way | NO | shilong | ✅ | ✅ | ✅ | ✅ |
| 1-way | YES | immdef | ✅ | ✅ | ✅ | ✅ |
| 1-way | NO | immdef | ✅ | ✅ | ✅ | ✅ |
| 1-way | YES | anonymised_trial_data | ✅ | ✅ | ✅ | ✅ |
| 1-way | NO | anonymised_trial_data | ✅ | ✅ | ✅ | ✅ |
| 2-way | YES | shilong | ✅ | ⚠️ | ❌ | Control: ✅; Treatment: ❌/⚠️ |
| 2-way | NO | shilong | ✅ | ⚠️ | ❌ | Control: ✅; Treatment: ❌/⚠️ |
Legend: - ✅ = Good agreement between
implementations - ❌ = Notable differences between implementations
- ⚠️ = Partial agreement or minor differences
shilong and immdef datasetslibrary(tidyverse)
library(dplyr)
library(survival)
library(rpsftm)
library(trtswitch)
library(boot)
library(ggplot2)
shilong1 <- trtswitch::shilong %>%
arrange(bras.f, id, ady) %>%
group_by(bras.f, id) %>%
slice(n()) %>%
select(-c("ps", "ttc", "tran")) %>%
ungroup() %>%
# two-way switch
mutate(rx = ifelse(co, ifelse(bras.f == "MTA", dco/ady, 1 - dco/ady),
ifelse(bras.f == "MTA", 1, 0))) %>%
# one-way switch
mutate(rx1 = ifelse(bras.f == "MTA", 1,
ifelse(co, 1 - dco/ady, 0))) %>%
mutate(xo1 = ifelse(bras.f == "MTA", FALSE, co)) %>%
mutate(arm = case_when(bras.f == "MTA" ~ 1, TRUE ~ 0)) %>%
mutate(survtime = ady, status = event, xoyrs = dco)
immdef1 <- trtswitch::immdef %>%
mutate(rx1 = 1-xoyrs/progyrs) %>%
mutate(xo1 = xo) %>%
mutate(survtime = progyrs, status = prog, arm = imm)
# Prepare internal_data dataset
anonymised_trial_data <- read_csv("processed_data.csv") %>%
mutate(event = as.numeric(event)) %>%
# one-way switch
mutate(rx1 = ifelse(arm == 1, 1,
ifelse(xo == TRUE, 1 - dco/ady, 0))) %>%
mutate(xo1 = ifelse(arm == 1, FALSE, xo)) %>%
mutate(survtime = ady, status = as.integer(event), xoyrs = dco) %>%
mutate(dcut = (ady +30)) trtswitch::rpsftm()# input data requires columns:
# ady: survival time
# event: status
# arm: 1/0
# rx: Proportion of duration of treatment switch
# dcut: the relative day of administrative cutoff (if recensor = TRUE)
validate_rpsftm <- function(data, rx = "rx1", recensor = TRUE){
fml <- as.formula(paste0("Surv(ady, event)~rand(arm,", rx, ")"))
if (recensor){
Result.RPSFT = rpsftm::rpsftm(fml, data = data, censor_time = dcut, low_psi=-3,hi_psi=3)
} else if (!recensor){
Result.RPSFT = rpsftm::rpsftm(fml, data = data, low_psi=-3, hi_psi=3)
}
tstar2 = Result.RPSFT$Sstar[,1]
event2 = Result.RPSFT$Sstar[,2]
d_add = data.frame(data,tstar2, event2)
Result.Cox=coxph(Surv(tstar2, event2)~arm , data = d_add, ties="exact")
Hazard.PointEstimate = exp(Result.Cox$coefficients)
Hazard.CI = exp(confint(Result.Cox))
Logrank = survdiff(Surv(tstar2, event2)~arm, data = d_add)
Pvalue = 1 - pchisq(Logrank$chisq, 1)
return(list("psi" = Result.RPSFT$psi,
"hazard"= as.numeric(Hazard.PointEstimate),
"pvalue"= Pvalue,
data_outcome = d_add %>% select(id, tstar2, event2)))
}shilong Datasettrtswitch::rpsftm()) with shilong dataset, recensor is
TRUE, one-way switch scenariofit1 <- trtswitch::rpsftm(
shilong1, id = "id", time = "ady", event = "event",
treat = "arm", rx = "rx1", censor_time = "dcut",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = TRUE)
# censor_time needs to be specified even if recensor = FALSE
fit1_v <- validate_rpsftm(shilong1, rx = "rx1", recensor = TRUE)psi, hr,
p-value## [1] 0.48211548 1.61653092 0.00871751
## [1] 0.4820509 1.0000598 0.9997310
The estimated causal parameter psi, the estimated hazard
ratio and p-value from the Cox model applied to counterfactual
unswitched survival times are very close between the result from
trtswitch::rpsftm() and validation result using
rpsftm::rpsftm() in one-way switch scenario.
compare_fit1 <- fit1$data_outcome %>%
left_join(fit1_v$data_outcome, by = "id")
ggplot(compare_fit1, aes(x = t_star, y = tstar2)) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison", x = "From trtswitch::rpsftm", y = "From validation")##
## 0 1
## 0 64 0
## 1 0 129
immdef
Datasettrtswitch::rpsftm()) with immdef dataset, recensor = TRUE,
one-way switch# RUN TRTSWITCH RPSFTM ON IMMDEF DATASET
fit5 <- trtswitch::rpsftm(
immdef1, id = "id", time = "progyrs", event = "prog",
treat = "imm", rx = "rx1", censor_time = "censyrs",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = TRUE)
# RUN REFERENCE IMPLEMENTATION ON IMMDEF DATASET
# First need to prepare data with correct column names for validation function
immdef_rpsftm <- immdef1 %>%
transmute(
id = id,
ady = progyrs,
event = prog,
arm = imm,
dcut = censyrs,
rx1 = rx1
)
fit5_v <- validate_rpsftm(immdef_rpsftm, rx = "rx1", recensor = TRUE)Compare RPSFTM results on immdef dataset: psi, hazard ratio, p-value
## [1] -0.18117698 0.76109922 0.02408344
## [1] -0.1814538 1.0035655 0.9760373
Compare counterfactual survival times and events
compare_fit5 <- fit5$data_outcome %>%
left_join(fit5_v$data_outcome, by = "id")
# PLOT COMPARISON OF COUNTERFACTUAL SURVIVAL TIMES
ggplot(compare_fit5, aes(x = t_star, y = tstar2)) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison - immdef dataset",
x = "From trtswitch::rpsftm",
y = "From validation")##
## 0 1
## 0 714 1
## 1 0 285
Check results for patients who DIDN’T switch (immdef dataset)
compare_fit5_nonswt <- compare_fit5 %>%
left_join(immdef1 %>% select(id, rx1, progyrs), by = "id") %>%
filter((rx1 == 1 & imm == 1) | (rx1 == 0 & imm == 0))
head(compare_fit5_nonswt, n = 5)## id t_star d_star treated imm tstar2 event2 rx1 progyrs
## 1 1 3.0000000 0 1 1 2.5021704 0 1 3.0000000
## 2 3 1.7378377 1 1 1 1.4494553 1 1 1.7378377
## 3 4 2.1662905 1 1 1 1.8068093 1 1 2.1662905
## 4 7 2.1894703 1 0 0 2.1894703 1 0 2.1894703
## 5 8 0.9226239 1 1 1 0.7695208 1 1 0.9226239
Check results for patients who DID switch (immdef dataset)
compare_fit5_swt <- compare_fit5 %>%
left_join(immdef1 %>% select(id, rx1, progyrs, xoyrs), by = "id") %>%
filter(rx1 > 0 & imm == 0)
head(compare_fit5_swt, n = 5)## id t_star d_star treated imm tstar2 event2 rx1 progyrs xoyrs
## 1 2 2.502863 0 0 0 2.502170 0 0.1157343 3.000000 2.6527972
## 2 5 2.502863 0 0 0 2.502170 0 0.2643466 2.884646 2.1220999
## 3 6 2.502863 0 0 0 2.502170 0 0.8142027 3.000000 0.5573920
## 4 13 2.502863 0 0 0 2.502170 0 0.4089745 3.000000 1.7730765
## 5 19 2.023826 1 0 0 2.023395 1 0.8006016 2.333397 0.4652756
Mathematical Check for immdef dataset: Verify the relationship between counterfactual time and psi parameter.
if(nrow(compare_fit5_swt) > 0) {
# Take first patient who switched as an example
compare_fit5_example <- compare_fit5_swt %>% slice(1)
# Calculate the scaling factor applied to the post-switch period
scaling_factor <- (compare_fit5_example$t_star - compare_fit5_example$xoyrs) /
(compare_fit5_example$progyrs - compare_fit5_example$xoyrs)
cat("Scaling factor for patient", compare_fit5_example$id, ":", scaling_factor, "\n")
cat("Expected value exp(psi):", exp(fit5$psi), "\n")
cat("Difference:", abs(scaling_factor - exp(fit5$psi)), "\n")
} else {
cat("No patients switched in the immdef dataset")
}## Scaling factor for patient 2 : -0.4318344
## Expected value exp(psi): 0.8342877
## Difference: 1.266122
trtswitch::rpsftm()) with internal_data dataset, recensor
= TRUE, one-way switch# RUN TRTSWITCH RPSFTM ON INTERNAL_DATA DATASET
fit7 <- trtswitch::rpsftm(
anonymised_trial_data, id = "id", time = "survtime", event = "status",
treat = "arm", rx = "rx1", censor_time = "dcut",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = TRUE)
# RUN REFERENCE IMPLEMENTATION ON INTERNAL_DATA DATASET
#First need to prepare data with correct column names for validation function
internal_data_rpsftm <- anonymised_trial_data %>%
transmute(
id = id,
ady = ady,
event = event,
arm = arm,
dcut = dcut,
rx1 = rx1
)
fit7_v <- validate_rpsftm(internal_data_rpsftm, rx = "rx1", recensor = TRUE)Compare RPSFTM results on internal_data dataset: psi, hazard ratio, p-value
## [1] -0.46988825 0.69457888 0.05584737
## [1] -0.4700738 1.0000633 0.9997328
Compare counterfactual survival times and events
compare_fit7 <- fit7$data_outcome %>%
left_join(fit7_v$data_outcome, by = "id")
# PLOT COMPARISON OF COUNTERFACTUAL SURVIVAL TIMES
ggplot(compare_fit7, aes(x = t_star, y = tstar2)) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison - internal_data dataset",
x = "From trtswitch::rpsftm",
y = "From validation")##
## 0 1
## 0 67 0
## 1 0 113
Check results for patients who DIDN’T switch (internal_data dataset)
compare_fit7_nonswt <- compare_fit7 %>%
left_join(anonymised_trial_data %>% select(id, rx1, ady), by = "id") %>%
filter((rx1 == 1 & arm == 1) | (rx1 == 0 & arm == 0))
head(compare_fit7_nonswt, n = 5)## id t_star d_star treated arm tstar2 event2 rx1 ady
## 1 10067 26.670376 1 1 1 16.667816 1 1 26.670376
## 2 10075 23.769947 0 1 1 14.855175 0 1 23.769947
## 3 10109 40.926910 0 1 1 25.577525 0 1 40.926910
## 4 10072 3.373891 1 1 1 2.108534 1 1 3.373891
## 5 10031 24.297373 1 1 1 15.184793 1 1 24.297373
Check results for patients who DID switch (internal_data dataset)
compare_fit7_swt <- compare_fit7 %>%
left_join(anonymised_trial_data %>% select(id, rx1, ady, xoyrs), by = "id") %>%
filter(rx1 > 0 & arm == 0)
head(compare_fit7_swt, n = 5)## id t_star d_star treated arm tstar2 event2 rx1 ady xoyrs
## 1 10191 21.72202 0 0 0 21.72043 0 0.5108907 26.86863 13.1417
## 2 10032 18.16075 1 0 0 18.15894 1 0.6496853 24.00899 8.4107
## 3 10015 13.30396 1 0 0 13.30310 1 0.4606494 16.08138 8.6735
## 4 10022 33.07687 0 0 0 33.07414 0 0.5616927 41.90097 18.3655
## 5 10126 13.14653 1 0 0 13.14583 1 0.3926068 15.41572 9.3634
Mathematical Check for internal_data dataset: Verify the relationship between counterfactual time and psi parameter.
if(nrow(compare_fit7_swt) > 0) {
# Take first patient who switched as an example
compare_fit7_example <- compare_fit7_swt %>% slice(1)
# Calculate the scaling factor applied to the post-switch period
scaling_factor <- (compare_fit7_example$t_star - compare_fit7_example$xoyrs) /
(compare_fit7_example$ady - compare_fit7_example$xoyrs)
cat("Scaling factor for patient", compare_fit7_example$id, ":", scaling_factor, "\n")
cat("Expected value exp(psi):", exp(fit7$psi), "\n")
cat("Difference:", abs(scaling_factor - exp(fit7$psi)), "\n")
} else {
cat("No patients switched in the anonymised_trial_data dataset")
}## Scaling factor for patient 10191 : 0.6250721
## Expected value exp(psi): 0.6250721
## Difference: 1.110223e-16
shilong Datasettrtswitch::rpsftm()) with shilong dataset, recensor =
FALSE, one-way switch scenariofit2 <- trtswitch::rpsftm(
shilong1, id = "id", time = "ady", event = "event",
treat = "arm", rx = "rx1", censor_time = "dcut",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = FALSE)
fit2_v<- validate_rpsftm(shilong1, rx = "rx1", recensor = FALSE) ## Warning in rpsftm::rpsftm(fml, data = data, low_psi = -3, hi_psi = 3):
## Evaluation of a limit of the Confidence Interval failed. It is set to NA
## [1] 0.578078205 1.700890346 0.003967654
## [1] 0.5780950 0.9999659 0.9998461
Similar as model with recensor, results of model without recensoring
are also very comparable between trtswitch::rpsftm() and
rpsftm::rpsftm() for one-way switch. Improvement suggestion
for trtswitch::rpsftm(): parameter censor_time
is required even recensor is FALSE.
compare_fit2 <- fit2$data_outcome %>%
left_join(fit2_v$data_outcome, by = "id")
ggplot(compare_fit2, aes(x = t_star, y = tstar2)) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison", x = "From trtswitch::rpsftm", y = "From validation")##
## 0 1
## 0 63 0
## 1 0 130
fit2
vs. fit2_v: Compare counterfactual survival time to
original survival time for subjects who didn’t switch.compare_fit2_nonswt <- compare_fit2 %>% left_join(shilong1 %>% select(id, rx1, ady), by = "id") %>% filter(rx1 == 1)
head(compare_fit2_nonswt, n = 5)## id t_star d_star treated arm tstar2 event2 rx1 ady
## 1 2 64 1 1 1 114.0889 1 1 64
## 2 4 156 1 1 1 278.0917 1 1 156
## 3 5 436 0 1 1 777.2307 0 1 436
## 4 6 500 1 1 1 891.3197 1 1 500
## 5 12 139 1 1 1 247.7869 1 1 139
In one-way switch scenario, counterfactual survival time
(t_star from trtswitch::rpsftm() and
tstar2 from rpsftm::rpsftm()) for subjects who
didn’t switch (treatment arm in the shilong dataset) is the
same as the original survival time ady, which is
expected.
fit2
vs. fit2_v: Compare counterfactual survival time to
original survival time for subjects who switched.compare_fit2_swt <- compare_fit2 %>% left_join(shilong1 %>% select(id, rx1, ady, dco), by = "id") %>% filter(rx1 < 1)
head(compare_fit2_swt, n = 5)## id t_star d_star treated arm tstar2 event2 rx1 ady dco
## 1 1 234.2175 1 0 0 234.2209 1 0.7862069 145 31
## 2 3 412.2175 1 0 0 412.2223 1 0.5574913 287 127
## 3 8 815.2611 0 0 0 815.2738 0 0.8701031 485 63
## 4 9 560.3480 1 0 0 560.3564 1 0.8157895 342 63
## 5 10 37.0000 1 0 0 37.0000 1 0.0000000 37 NA
In one-way switch scenario, counterfactual survival time
t_star from trtswitch::rpsftm() and
tstar2 from rpsftm::rpsftm() are similar. Both
of the counterfactual survival times for subjects who switched (control
arm in the shilong dataset) are larger than the original
survival time ady. Take subject id = 1 as an
example to check how counterfactual survival time is related to the
estimated psi from trtswitch::rpsftm().
compare_fit2_1 <- compare_fit2_swt %>% filter(id == "1")
(compare_fit2_1$t_star- compare_fit2_1$dco)/(compare_fit2_1$ady - compare_fit2_1$dco)## [1] 1.782609
equals to exp(-fit2$psi)
## [1] 1.782609
immdef
Datasettrtswitch::rpsftm()) with immdef dataset, recensor =
FALSE, one-way switch# RUN TRTSWITCH RPSFTM ON IMMDEF DATASET
fit6 <- trtswitch::rpsftm(
immdef1, id = "id", time = "progyrs", event = "prog",
treat = "imm", rx = "rx1", censor_time = "censyrs",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = FALSE)
# RUN REFERENCE IMPLEMENTATION ON IMMDEF DATASET
fit6_v <- validate_rpsftm(immdef_rpsftm, rx = "rx1", recensor = FALSE)Compare RPSFTM results on immdef dataset: psi,
hazard ratio, p-value
## [1] -0.18505941 0.76677041 0.01972649
## [1] -0.1848298 0.9999392 0.9995780
Compare counterfactual survival times and events
compare_fit6 <- fit6$data_outcome %>%
left_join(fit6_v$data_outcome, by = "id")
# PLOT COMPARISON OF COUNTERFACTUAL SURVIVAL TIMES
ggplot(compare_fit6, aes(x = t_star, y = tstar2)) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison - immdef dataset",
x = "From trtswitch::rpsftm",
y = "From validation")##
## 0 1
## 0 688 0
## 1 0 312
Check results for patients who DIDN’T switch (immdef dataset)
compare_fit6_nonswt <- compare_fit6 %>%
left_join(immdef1 %>% select(id, rx1, progyrs), by = "id") %>%
filter((rx1 == 1 & imm == 1) | (rx1 == 0 & imm == 0))
head(compare_fit6_nonswt, n = 5)## id t_star d_star treated imm tstar2 event2 rx1 progyrs
## 1 1 3.0000000 0 1 1 2.4937373 0 1 3.0000000
## 2 3 1.7378377 1 1 1 1.4445702 1 1 1.7378377
## 3 4 2.1662905 1 1 1 1.8007198 1 1 2.1662905
## 4 7 2.1894703 1 0 0 2.1894703 1 0 2.1894703
## 5 8 0.9226239 1 1 1 0.7669272 1 1 0.9226239
Check results for patients who DID switch (immdef dataset)
compare_fit6_swt <- compare_fit6 %>%
left_join(immdef1 %>% select(id, rx1, progyrs, xoyrs), by = "id") %>%
filter(rx1 > 0 & imm == 0)
head(compare_fit6_swt, n = 5)## id t_star d_star treated imm tstar2 event2 rx1 progyrs xoyrs
## 1 2 2.941342 0 0 0 2.941408 0 0.1157343 3.000000 2.6527972
## 2 5 2.755818 1 0 0 2.755963 1 0.2643466 2.884646 2.1220999
## 3 6 2.587333 0 0 0 2.587800 0 0.8142027 3.000000 0.5573920
## 4 13 2.792717 0 0 0 2.792951 0 0.4089745 3.000000 1.7730765
## 5 19 2.017787 1 0 0 2.018144 1 0.8006016 2.333397 0.4652756
Mathematical Check for immdef dataset:
Verify the relationship between counterfactual time and psi
parameter.
if(nrow(compare_fit6_swt) > 0) {
# Take first patient who switched as an example
compare_fit6_example <- compare_fit6_swt %>% slice(1)
# Calculate the scaling factor applied to the post-switch period
scaling_factor <- (compare_fit6_example$t_star - compare_fit6_example$xoyrs) /
(compare_fit6_example$progyrs - compare_fit6_example$xoyrs)
cat("Scaling factor for patient", compare_fit6_example$id, ":", scaling_factor, "\n")
cat("Expected value exp(psi):", exp(fit6$psi), "\n")
cat("Difference:", abs(scaling_factor - exp(fit6$psi)), "\n")
} else {
cat("No patients switched in the immdef dataset")
}## Scaling factor for patient 2 : 0.8310549
## Expected value exp(psi): 0.8310549
## Difference: 0
anonymised_trial_data Datasettrtswitch::rpsftm()) with internal_data dataset, recensor
= FALSE, one-way switch# RUN TRTSWITCH RPSFTM ON INTERNAL_DATA DATASET
fit8 <- trtswitch::rpsftm(
anonymised_trial_data, id = "id", time = "survtime", event = "status",
treat = "arm", rx = "rx1", censor_time = "ady",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = FALSE)
# RUN REFERENCE IMPLEMENTATION ON INTERNAL_DATA DATASET
fit8_v <- validate_rpsftm(internal_data_rpsftm, rx = "rx1", recensor = FALSE)Compare RPSFTM results on internal_data dataset: psi, hazard ratio, p-value
## [1] -0.47224404 0.69359797 0.05494223
## [1] -0.4700738 1.0000633 0.9997328
Compare counterfactual survival times and events
compare_fit8 <- fit8$data_outcome %>%
left_join(fit8_v$data_outcome, by = "id")
# PLOT COMPARISON OF COUNTERFACTUAL SURVIVAL TIMES
ggplot(compare_fit8, aes(x = t_star, y = tstar2)) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison - internal_data dataset",
x = "From trtswitch::rpsftm",
y = "From validation")##
## 0 1
## 0 67 0
## 1 0 113
Check results for patients who DIDN’T switch (internal_data dataset)
compare_fit8_nonswt <- compare_fit8 %>%
left_join(anonymised_trial_data %>% select(id, rx1, ady), by = "id") %>%
filter((rx1 == 1 & arm == 1) | (rx1 == 0 & arm == 0))
head(compare_fit8_nonswt, n = 5)## id t_star d_star treated arm tstar2 event2 rx1 ady
## 1 10067 26.670376 1 1 1 16.667816 1 1 26.670376
## 2 10075 23.769947 0 1 1 14.855175 0 1 23.769947
## 3 10109 40.926910 0 1 1 25.577525 0 1 40.926910
## 4 10072 3.373891 1 1 1 2.108534 1 1 3.373891
## 5 10031 24.297373 1 1 1 15.184793 1 1 24.297373
Check results for patients who DID switch (internal_data dataset)
compare_fit8_swt <- compare_fit8 %>%
left_join(anonymised_trial_data %>% select(id, rx1, ady, xoyrs), by = "id") %>%
filter(rx1 > 0 & arm == 0)
head(compare_fit8_swt, n = 5)## id t_star d_star treated arm tstar2 event2 rx1 ady xoyrs
## 1 10191 21.70183 0 0 0 21.72043 0 0.5108907 26.86863 13.1417
## 2 10032 18.13781 1 0 0 18.15894 1 0.6496853 24.00899 8.4107
## 3 10015 13.29306 1 0 0 13.30310 1 0.4606494 16.08138 8.6735
## 4 10022 33.04225 0 0 0 33.07414 0 0.5616927 41.90097 18.3655
## 5 10126 13.13763 1 0 0 13.14583 1 0.3926068 15.41572 9.3634
Mathematical Check for
anonymised_trial_data: Verify the relationship
between counterfactual time and psi parameter.
if(nrow(compare_fit8_swt) > 0) {
#Take first patient who switched as an example
compare_fit8_example <- compare_fit8_swt %>% slice(1)
# Calculate the scaling factor applied to the post-switch period
scaling_factor <- (compare_fit8_example$t_star - compare_fit8_example$xoyrs) /
(compare_fit8_example$ady - compare_fit8_example$xoyrs)
cat("Scaling factor for patient", compare_fit8_example$id, ":", scaling_factor, "\n")
cat("Expected value exp(psi):", exp(fit8$psi), "\n")
cat("Difference:", abs(scaling_factor - exp(fit8$psi)), "\n")
} else {
cat("No patients switched in the internal_data dataset")
}## Scaling factor for patient 10191 : 0.6236013
## Expected value exp(psi): 0.6236013
## Difference: 1.110223e-16
trtswitch::rpsftm()) with shilong dataset,
recensor = TRUE, two-way switchfit3 <- trtswitch::rpsftm(
shilong1, id = "id", time = "tstop", event = "event",
treat = "arm", rx = "rx", censor_time = "dcut",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = TRUE)
fit3_v <- validate_rpsftm(shilong1, rx = "rx", recensor = TRUE)## [1] 1.007842e+00 2.721078e+00 3.648658e-06
## [1] 1.0079860 0.9953629 0.9798349
In the two-way switch scenario, the estimated causal parameter
psi = 1.0078423 from trtswitch::rpsftm() is
close to psi = 1.007986 from rpsftm::rpsftm().
However, the estimated hazard ratio and p-value from the Cox model
applied to counterfactual unswitched survival times are very different
between fit3 and fit3_v.
compare_fit3 <- fit3$data_outcome %>%
left_join(fit3_v$data_outcome, by = "id") %>%
select(id, t_star, arm, tstar2, d_star, event2) %>% left_join(shilong1 %>% select(id, ady, dco, rx, status))## Joining with `by = join_by(id)`
ggplot(compare_fit3, aes(x = t_star, y = tstar2, color = factor(arm))) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison", x = "From trtswitch::rpsftm", y = "From validation")# Filter for treatment arm patients only and add switching status
compare_fit3_treatment <- compare_fit3 %>%
filter(arm == 1) %>%
mutate(switching_status = ifelse(rx == 1, "Non-switcher", "Switcher"))
ggplot(compare_fit3_treatment, aes(x = t_star, y = tstar2, color = switching_status)) +
geom_point(size = 3, alpha = 0.7) +
geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "black") +
labs(title = "Counterfactual survival time comparison - Treatment arm patients only",
subtitle = "Two-way switching scenario with recensoring",
x = "From trtswitch::rpsftm",
y = "From rpsftm::rpsftm (validation)",
color = "Switching Status") +
scale_color_manual(values = c("Switcher" = "#E31A1C", "Non-switcher" = "#1F78B4")) +
theme_minimal() +
theme(legend.position = "bottom",
plot.title = element_text(size = 14, face = "bold"),
plot.subtitle = element_text(size = 12))fit3
vs. fit3_v: Counterfactual survival time for subjects who
didn’t switch.compare_fit3_nonswt <- compare_fit3 %>% filter((arm == 0 & rx == 0) | (arm == 1 & rx == 1))
ggplot(compare_fit3_nonswt, aes(x = t_star, y = tstar2, color = factor(arm))) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison for subjects who didn't switch", x = "From trtswitch::rpsftm", y = "From validation")## id t_star arm tstar2 d_star event2 ady dco rx status delta
## 1 2 64.0000 1 175.36492 1 1 64 NA 1 1 -111.36492
## 2 19 73.0000 1 200.02562 0 0 73 NA 1 0 -127.02562
## 3 25 62.0000 1 169.88477 1 1 62 NA 1 1 -107.88477
## 4 26 329.0000 1 901.48531 1 1 329 NA 1 1 -572.48531
## 5 33 167.0000 1 457.59285 1 1 167 NA 1 1 -290.59285
## 6 37 76.0000 1 208.24585 1 1 76 NA 1 1 -132.24585
## 7 51 209.0000 1 572.67608 0 0 209 NA 1 0 -363.67608
## 8 52 65.0000 1 178.10500 1 1 65 NA 1 1 -113.10500
## 9 53 246.0000 1 674.05892 0 0 246 NA 1 0 -428.05892
## 10 55 146.0000 1 400.05123 0 0 146 NA 1 0 -254.05123
## 11 56 210.6083 1 577.00000 0 0 286 NA 1 0 -366.39171
## 12 59 177.0000 1 484.99361 0 0 177 NA 1 0 -307.99361
## 13 60 210.2433 1 576.00000 0 0 280 NA 1 1 -365.75671
## 14 62 109.0000 1 298.66838 1 1 109 NA 1 1 -189.66838
## 15 64 43.0000 1 117.82331 1 1 43 NA 1 1 -74.82331
## 16 66 146.0000 1 400.05123 0 0 146 NA 1 0 -254.05123
## 17 69 76.0000 1 208.24585 1 1 76 NA 1 1 -132.24585
## 18 70 120.0000 1 328.80923 1 1 120 NA 1 1 -208.80923
## 19 72 392.0161 1 1074.00000 0 0 510 NA 1 1 -681.98387
## 20 73 90.0000 1 246.60692 1 1 90 NA 1 1 -156.60692
## 21 77 28.0000 1 76.72215 1 1 28 NA 1 1 -48.72215
## 22 79 162.0000 1 443.89246 0 0 162 NA 1 0 -281.89246
## 23 83 198.0000 1 542.53523 1 1 198 NA 1 1 -344.53523
## 24 87 252.0000 1 690.49938 0 0 252 NA 1 0 -438.49938
## 25 89 27.0000 1 73.98208 1 1 27 NA 1 1 -46.98208
## 26 92 30.0000 1 82.20231 1 1 30 NA 1 1 -52.20231
## 27 93 150.0000 1 411.01154 1 1 150 NA 1 1 -261.01154
## 28 94 105.0000 1 287.70808 1 1 105 NA 1 1 -182.70808
## 29 95 139.0000 1 380.87069 1 1 139 NA 1 1 -241.87069
## 30 96 114.0000 1 312.36877 1 1 114 NA 1 1 -198.36877
## 31 97 98.0000 1 268.52754 1 1 98 NA 1 1 -170.52754
## 32 99 329.9652 1 904.00000 0 0 386 NA 1 0 -574.03484
## 33 101 180.0000 1 493.21385 1 1 180 NA 1 1 -313.21385
## 34 103 97.0000 1 265.78746 0 0 97 NA 1 0 -168.78746
## 35 104 49.0000 1 134.26377 1 1 49 NA 1 1 -85.26377
## 36 105 223.0000 1 611.03715 1 1 223 NA 1 1 -388.03715
## 37 106 103.0000 1 282.22792 1 1 103 NA 1 1 -179.22792
## 38 108 178.0000 1 487.73369 0 0 178 NA 1 0 -309.73369
## 39 110 202.0000 1 553.49554 0 0 202 NA 1 0 -351.49554
## 40 111 132.0000 1 361.69015 1 1 132 NA 1 1 -229.69015
## 41 113 324.0000 1 887.78492 1 1 324 NA 1 1 -563.78492
## 42 118 92.0000 1 252.08708 1 1 92 NA 1 1 -160.08708
## 43 120 314.9999 1 863.00000 0 0 315 NA 1 1 -548.00007
## 44 125 102.0000 1 279.48785 1 1 102 NA 1 1 -177.48785
## 45 126 278.0000 1 761.74138 1 1 278 NA 1 1 -483.74138
## 46 127 58.0000 1 158.92446 1 1 58 NA 1 1 -100.92446
## 47 128 368.2908 1 1009.00000 0 0 389 NA 1 1 -640.70924
## 48 129 170.0000 1 465.81308 1 1 170 NA 1 1 -295.81308
## 49 130 61.0000 1 167.14469 1 1 61 NA 1 1 -106.14469
## 50 134 196.0000 1 537.05508 1 1 196 NA 1 1 -341.05508
## 51 136 55.0000 1 150.70423 1 1 55 NA 1 1 -95.70423
## 52 139 24.0000 1 65.76185 1 1 24 NA 1 1 -41.76185
## 53 140 305.0000 1 835.72346 0 0 305 NA 1 0 -530.72346
## 54 143 55.0000 1 150.70423 0 0 55 NA 1 0 -95.70423
## 55 144 102.0000 1 279.48785 0 0 102 NA 1 0 -177.48785
## 56 146 130.0000 1 356.21000 1 1 130 NA 1 1 -226.21000
## 57 147 83.0000 1 227.42638 1 1 83 NA 1 1 -144.42638
## 58 148 210.0000 1 575.41615 1 1 210 NA 1 1 -365.41615
## 59 149 218.0000 1 597.33677 0 0 218 NA 1 0 -379.33677
## 60 151 230.6836 1 632.00000 0 0 573 NA 1 0 -401.31639
## 61 152 20.0000 1 54.80154 1 1 20 NA 1 1 -34.80154
## 62 156 93.0000 1 254.82715 1 1 93 NA 1 1 -161.82715
## 63 158 39.0000 1 106.86300 1 1 39 NA 1 1 -67.86300
## 64 159 47.0000 1 128.78362 1 1 47 NA 1 1 -81.78362
## 65 161 296.0000 1 811.06277 1 1 296 NA 1 1 -515.06277
## 66 175 66.0000 1 180.84508 1 1 66 NA 1 1 -114.84508
## 67 183 142.0000 1 389.09092 1 1 142 NA 1 1 -247.09092
## 68 184 149.2873 1 409.00000 0 0 261 NA 1 0 -259.71266
## 69 186 205.1332 1 562.00000 0 0 238 NA 1 0 -356.86679
## 70 187 77.0000 1 210.98592 1 1 77 NA 1 1 -133.98592
## 71 189 105.0000 1 287.70808 1 1 105 NA 1 1 -182.70808
## 72 192 214.6234 1 588.00000 0 0 553 NA 1 0 -373.37664
## 73 193 46.0000 1 126.04354 1 1 46 NA 1 1 -80.04354
## 74 194 193.0000 1 528.83485 1 1 193 NA 1 1 -335.83485
## 75 196 239.0000 1 654.87838 0 0 239 NA 1 0 -415.87838
There are a couple of dots that are not on the line. They are all
from treatment arm (arm = 1). Counterfactual survival time
(t_star) from trtswitch::rpsftm() for these
subjects shrunk, while results (tstar2) from
rpsftm::rpsftm() are the same as the original survival time
(ady). We notice that d_star and event2 stay consistent,
whereas the tstar2 and t_star are different. tstar2 is the same as “ady”
(original value), whereas t_star is being updated based on psi.
## id t_star arm tstar2 d_star event2 ady dco rx status
## 1 4 75.99072 1 208.2023 1 1 156 30 0.1923077 1
## 2 5 213.75200 1 585.6466 0 0 436 86 0.1972477 0
## 3 6 263.14713 1 720.9898 1 1 500 127 0.2540000 1
## 4 12 92.64542 1 253.8451 1 1 139 66 0.4748201 1
## 5 23 215.34644 1 590.0295 1 1 376 123 0.3271277 1
In treatment arm, counterfactual survival time (tstar2)
doesn’t get updated from rpsftm::rpsftm(), while
t_star from trtswitch::rpsftm() is smaller
than ady.
Take subject id = 5 from treatment arm as an example to
check how counterfactual survival time is related to the estimated
psi from trtswitch::rpsftm().
## id t_star arm tstar2 d_star event2 ady dco rx status
## 1 5 213.752 1 585.6466 0 0 436 86 0.1972477 0
## [1] 0.3650057
which equals to exp(-fit3$psi). This is consistent with our expectations.
## [1] 0.3650057
In control arm, both counterfactual survival time
(tstar2) from rpsftm::rpsftm() and
t_star from trtswitch::rpsftm() are larger
than ady.
## id t_star arm tstar2 d_star event2 ady dco rx status
## 1 1 343.3239 0 343.36877 1 1 145 31 0.7862069 1
## 2 3 565.3493 0 565.41231 1 1 287 127 0.5574913 1
## 3 8 598.0000 0 598.00000 0 0 485 63 0.8701031 0
## 4 9 827.3716 0 827.48146 1 1 342 63 0.8157895 1
## 5 11 53.4381 0 53.44046 1 1 43 37 0.1395349 1
Take subject id = 9 from control arm as an example to
check how counterfactual survival time is related to the estimated
psi from trtswitch::rpsftm().
## id t_star arm tstar2 d_star event2 ady dco rx status
## 1 9 827.3716 0 827.4815 1 1 342 63 0.8157895 1
## [1] 2.739683
equals to exp(fit3$psi)
## [1] 2.739683
## [1] 2.740077
equals to exp(fit3_v$psi)
## [1] 2.740077
shilong Datasettrtswitch::rpsftm()) with shilong dataset,
recensor = FALSE, two-way switchfit4 <- trtswitch::rpsftm(
shilong1, id = "id", time = "ady", event = "event",
treat = "arm", rx = "rx", censor_time = "dcut",
low_psi = -3, hi_psi = 3, boot = FALSE, recensor = FALSE)
fit4_v <- validate_rpsftm(shilong1, rx = "rx", recensor = FALSE)## Warning in rpsftm::rpsftm(fml, data = data, low_psi = -3, hi_psi = 3):
## Evaluation of a limit of the Confidence Interval failed. It is set to NA
## [1] 1.119231e+00 2.717309e+00 7.257647e-07
## [1] 1.1188999 1.0000278 0.9998749
compare_fit4 <- fit4$data_outcome %>%
left_join(fit4_v$data_outcome, by = "id") %>%
select(id, t_star, arm, tstar2) %>% left_join(shilong1 %>% select(id, ady, dco, rx))## Joining with `by = join_by(id)`
ggplot(compare_fit4, aes(x = t_star, y = tstar2, color = factor(arm))) +
geom_point() +
geom_abline(intercept = 0, slope = 1) +
labs(title = "Counterfactual survival time comparison", x = "From trtswitch::rpsftm", y = "From validation")shilong
dataset## Call:
## coxph(formula = Surv(ady, event) ~ arm, data = shilong1)
##
## coef exp(coef) se(coef) z p
## arm 0.2349 1.2648 0.1777 1.322 0.186
##
## Likelihood ratio test=1.75 on 1 df, p=0.1859
## n= 193, number of events= 130
immdef
dataset## Call:
## coxph(formula = Surv(progyrs, prog) ~ imm, data = immdef1)
##
## coef exp(coef) se(coef) z p
## imm -0.2171 0.8048 0.1137 -1.91 0.0561
##
## Likelihood ratio test=3.66 on 1 df, p=0.05562
## n= 1000, number of events= 312
anonymised_trial_data datasetunadj_fit_internal <- coxph(Surv(ady, event) ~ arm, data = anonymised_trial_data)
unadj_fit_internal## Call:
## coxph(formula = Surv(ady, event) ~ arm, data = anonymised_trial_data)
##
## coef exp(coef) se(coef) z p
## arm -0.2489 0.7797 0.1887 -1.319 0.187
##
## Likelihood ratio test=1.74 on 1 df, p=0.1873
## n= 180, number of events= 113
rpsftm from
trtswitch::rpsftm() and rpsftm::rpsftm()trtswitch::rpsftm() and rpsftm::rpsftm() give
very similar hazard ratio. For two-way switch scenario, the trend is
similar, but the numbers are quite different regarding hazard ratio.
Here is a summary table of HRs from different models.## Model Package::function Dataset HR
## 1 Unadjusted survival::coxph shilong1 1.2647965
## 2 One-way switch, recensor = TRUE trtswitch::rpsftm shilong1 1.6165309
## 3 One-way switch, recensor = TRUE rpsftm::rpsftm shilong1 1.0000598
## 4 One-way switch, recensor = FALSE trtswitch::rpsftm shilong1 1.7008903
## 5 One-way switch, recensor = FALSE rpsftm::rpsftm shilong1 0.9999659
## 6 Two-way switch, recensor = TRUE trtswitch::rpsftm shilong1 2.7210780
## 7 Two-way switch, recensor = TRUE rpsftm::rpsftm shilong1 0.9953629
## 8 Two-way switch, recensor = FALSE trtswitch::rpsftm shilong1 2.7173094
## 9 Two-way switch, recensor = FALSE rpsftm::rpsftm shilong1 1.0000278
## 10 RPSFTM on immdef, recensor = TRUE trtswitch::rpsftm immdef1 0.7610992
## 11 RPSFTM on immdef, recensor = TRUE rpsftm::rpsftm immdef1 1.0035655
## 12 RPSFTM on immdef, recensor = FALSE trtswitch::rpsftm immdef1 0.7667704
## 13 RPSFTM on immdef, recensor = FALSE rpsftm::rpsftm immdef1 0.9999392
In one-way switch scenario, trtswitch::rpsftm() and
rpsftm::rpsftm() give comparable numbers for estimated
psi, hazard ratio and cox p-value. The counterfactual
survival time for subjects who didn’t switch (treatment arm) are
consistent with original survival time, which is expected. The
counterfactual survival time for subjects who switched (control arm) are
larger than original survival time, which is also expected since the
unadjusted HR is greater than 1. This consistency is observed in
both the shilong and immdef datasets.
RPSFTM validation on immdef dataset: The newly
added validation using the immdef1 dataset (fit5 and fit6)
shows that trtswitch::rpsftm() and
rpsftm::rpsftm() produce consistent results across
different datasets. This cross-dataset validation strengthens confidence
in the implementation.
In two-way switch scenario (Recensor = TRUE),
trtswitch::rpsftm() and rpsftm::rpsftm() give
similar estimated psi, but very different numbers for
hazard ratio and cox p-value.
For subjects who didn’t switch: counterfactual survival time from
rpsftm::rpsftm() is the same as original survival time,
however, there are a couple of subjects in treatment arm (arm =1) whose
counterfactual survival time from trtswitch::rpsftm() is
shorter than the original survival time. This needs to be looked into
(maybe in trtswitch::rpsftm, it also recensors the
non-switchers).
For subjects who switched: counterfactual survival time from both
trtswitch::rpsftm() and rpsftm::rpsftm() are
larger than original survival time for control arm (arm = 0). This is
aligned with a worse treatment effect and a positive estimated
psi. counterfactual survival time from
trtswitch::rpsftm() for treatment arm (arm = 1) are shorter
than original survival time. This is also expected and was
double-checked with manual calculations. However, counterfactual
survival time from rpsftm::rpsftm() for treatment arm (arm
= 1) didn’t get updated. The values are the same as original survival
time, which may be a bug.
In two-way switch scenario (Recensor = FALSE), similar to the recensoring = TRUE scenario, trtswitch::rpsftm() and rpsftm::rpsftm() give comparable estimated psi, but different numbers for hazard ratio and cox p-value. This pattern suggests that the discrepancies between implementations are not driven by the recensoring mechanism, but rather by fundamental differences in the two-way switching algorithms.
The counterfactual survival time comparison plots show similar patterns to the recensoring = TRUE scenario, with points colored by treatment arm revealing systematic differences between the two implementations.
The scatter plots demonstrate that while most patients fall near the diagonal line of agreement, there are consistent deviations particularly visible when stratified by treatment arm, indicating the same underlying algorithmic differences persist regardless of recensoring setting.This consistency across recensoring settings confirms that the implementation differences observed in two-way switching scenarios are inherent to how each package handles bidirectional treatment switching, rather than being artifacts of the recensoring process.