The goal of the forgiveness curve is to estimate an
adherence-response curve and identify if there is a level of suboptimal
adherence above which the failure risk of viral load suppression is
sufficiently low. These forgiveness curves are estimated via
longitudinal modified treatment policies that shift adherence toward a
target level (e.g., 50% PDC) with a specified strength (e.g.,
alpha=0.5). Here, we illustrate the setup for such an analysis and plot
initial results. The modified treatment policies help avoid estimation
issues due to data sparsity, but methods are still in refinement to
better estimate the curves when there are realistic positivity issues
across the range of observed adherence.
wide=readRDS(here("data/lmtp_ready_wide_data.RDS"))
#temp subset for speed
#wide <- wide[1:500, ]
A_is_char <- is.character(wide$A) || is.factor(wide$A)
## Treatment nodes (A at time 0 + PDC_0...PDC_K)
P_cols <- grep("^PDC_\\d+$", names(wide), value = TRUE)
Kmax <- max(as.integer(sub("^PDC_", "", P_cols)), na.rm = TRUE)
trt_nodes <- as.list(c("A", paste0("PDC_", 0:Kmax)))
trt_nodes[[1]] <- c("A","PDC_0") # time-0 joint: baseline drug A plus PDC_0
## Time-varying L nodes *before* each block’s exposure (no C_* here)
## Use VL_k, CD4_k, and M_k (monitoring)
Lnodes <- lapply(0:Kmax, function(k) c(paste0("VL_log10_", k), paste0("CD4_", k), paste0("M_", k)))
## Lightweight SL for demo
sl_lib <- "SL.glm"
## choose the horizon you want; here, last block
Tidx <- max(as.integer(sub("^Y_", "", grep("^Y_\\d+$", names(wide), value = TRUE))))
## If Kmax == 5 (i.e., 0..5 present)
trt_nodes <- c(list(c("A","PDC_0")), as.list(paste0("PDC_", 1:5)))
colnames(wide)
Y_nodes <- paste0("Y_", 0:Tidx)
C_nodes <- paste0("C_", 0:Tidx)
death_nodes <- paste0("death_", 0:Tidx)
## helper: returns a named list as lmtp expects (one entry per 'trt' element)
set_cols <- function(data, trt, new_A = NULL, new_PDC = NULL){
out <- vector("list", length(trt)); names(out) <- trt
for(nm in trt){
if(nm == "A" && !is.null(new_A)) out[[nm]] <- rep(new_A, nrow(data))
else if(grepl("^PDC", nm) && !is.null(new_PDC)) out[[nm]] <- new_PDC[[nm]]
else out[[nm]] <- data[[nm]]
}
out
}
gap_vec <- sapply(0:Kmax, function(k){
pdc_col <- paste0("PDC_", k)
mean(wide[[pdc_col]][wide$A == 1], na.rm = TRUE) -
mean(wide[[pdc_col]][wide$A == 0], na.rm = TRUE)
})
names(gap_vec) <- paste0("PDC_", 0:Kmax)
## block-wise adherence remap to Drug-A style (cap to [0,1])
Astyle_PDC <- function(data, trt){
L <- lapply(trt[grepl("^PDC", trt)], function(col){
pmin(1, pmax(0, data[[col]] + gap_vec[col]))
})
names(L) <- trt[grepl("^PDC", trt)]
L
}
#Shrink PDC to set amount
point_shrink <- function(data, trt, alpha = 0.0, PDC=0.5) {
# a* = (1 - alpha)*a + alpha*0.5 ; alpha in (0,1) pulls toward 0.5 but preserves support
n <- nrow(data)
L <- lapply(trt[grepl("^PDC", trt)], function(col) {
a <- data[[col]]
pmin(1, pmax(0, (1 - alpha) * a + alpha * PDC))
})
names(L) <- trt[grepl("^PDC", trt)]
L
}
A_point_shrink <- function(data, trt) set_cols(data, trt, new_A = 1, new_PDC = point_shrink(data, trt, alpha = 0.8))
B_point_shrink <- function(data, trt) set_cols(data, trt, new_A = 0, new_PDC = point_shrink(data, trt, alpha = 0.8))
## --- MTP shift toward a chosen target tau with alpha=0.8 ---
point_toward <- function(data, trt, tau, alpha = 0.8) {
L <- lapply(trt[grepl("^PDC", trt)], function(col) {
a <- data[[col]]
pmin(1, pmax(0, (1 - alpha) * a + alpha * tau))
})
names(L) <- trt[grepl("^PDC", trt)]
L
}
## Factory to build the LMTP 'shift' function for a given tau and drug (A=1 or 0)
make_mtp_toward <- function(tau, alpha = 0.8, drug = 1L) {
force(tau); force(alpha); force(drug)
function(data, trt) {
new_PDC <- point_toward(data, trt, tau = tau, alpha = alpha)
new_A <- if (A_is_char) drug else drug
set_cols(data, trt, new_A = new_A, new_PDC = new_PDC)
}
}
library(data.table)
library(ggplot2)
## Grid of target adherences (adjust as desired)
targets <- seq(0.10, 0.90, by = 0.05)
## Wrapper to fit LMTP-SDR risk under a given (tau, drug)
fit_under_target <- function(tau, drug) {
shift_fun <- make_mtp_toward(tau = tau, alpha = 0.5, drug = drug)
lmtp_sdr(
data = as.data.frame(wide),
trt = trt_nodes,
outcome = paste0("Y_", 0:Tidx), # final risk at horizon Tidx
baseline = c("age","sex","cd4_base"),
time_vary = Lnodes,
shift = shift_fun,
mtp = TRUE, # continuous PDC intervention
outcome_type = "survival",
folds = 2,
learners_trt = sl_lib,
learners_outcome = sl_lib
)
}
summary(wide$PDC_0)
summary(wide$PDC_5)
## Compute risks across the grid for Drug A and Drug B
targets <- c(0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85,0.875, 0.9,0.925, 0.95, 0.975, 1)
fits_B=fits_A=list()
for(i in 1:length(targets)){
cat(i,"\n")
resA=NULL
resB=NULL
resA=try(fit_under_target(targets[i], drug = 1))
resB=try(fit_under_target(targets[i], drug = 0))
fits_A[[i]] =resA
fits_B[[i]] =resB
}
library(data.table)
library(zoo)
library(here)
library(lmtp)
## Compute risks across the grid for Drug A and Drug B
targets <- c(0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85,0.875, 0.9,0.925, 0.95, 0.975, 1)
#load results for speed
fits_A= readRDS(file=here("results/forgiveness_mtp_05_A.RDS"))
fits_B = readRDS(file=here("results/forgiveness_mtp_05_B.RDS"))
## Tidy results
res_A=NULL
for(i in 1:length(fits_A)){
res=NULL
if(fits_A[[i]] %>% class() == "try-error"){
res=data.frame(estimate=NA, std.error=NA, conf.low=NA, conf.high=NA, alpha=targets[i], drug="A")
}else{
res=tidy(fits_A[[i]]) %>% mutate( alpha=targets[i], drug="A")
}
res_A=bind_rows(res_A, res)
}
res_B=NULL
for(i in 1:length(fits_B)){
res=NULL
if(fits_B[[i]] %>% class() == "try-error"){
res=data.frame(estimate=NA, std.error=NA, conf.low=NA, conf.high=NA, alpha=targets[i], drug="A")
}else{
res=tidy(fits_B[[i]]) %>% mutate( alpha=targets[i], drug="B")
}
res_B=bind_rows(res_B, res)
}
## Combined
res_all <- rbind(res_A, res_B) %>%
rename(theta = estimate,
lcl = conf.low,
ucl = conf.high,
target = alpha) %>%
as.data.table()
ggplot(res_all, aes(x = target, y = theta, color = drug, fill = drug, group=drug)) +
geom_smooth(size = 2, se=F) +
geom_ribbon(aes(ymin = lcl, ymax = ucl), alpha = 0.15, color = NA) +
coord_cartesian(xlim=c(0.4,1), ylim=c(0,0.95), expand = FALSE) +
labs(title = "Forgiveness curves under an MTP(α=0.5) shift toward target PDC",
subtitle = "Each curve shows the cumulative incidence after 36 months under Drug A (red)\nor Drug B (blue) with all PDC_t shifted 50% toward target.") +
xlab("Target Percent Days Covered") + ylab("Cumulative incidence") +
theme_bw() +
theme(legend.position = "none",
margin = margin(0, 0, 0, 0),
plot.title = element_text(face = "bold"))