|
24 | 24 |
|
25 | 25 | #' Discretized Restricted Mean Duration calculation for Partitioned Survival Model
|
26 | 26 | #' Calculate restricted mean duration (RMD) in PF, PD and OS states under a Partitioned Survival Model structure.
|
| 27 | +#' @param ptdata Dataset of patient level data. Must be a tibble with columns named: |
| 28 | +#' - ptid: patient identifier |
| 29 | +#' - pfs.durn: duration of PFS from baseline |
| 30 | +#' - pfs.flag: event flag for PFS (=1 if progression or death occurred, 0 for censoring) |
| 31 | +#' - os.durn: duration of OS from baseline |
| 32 | +#' - os.flag: event flag for OS (=1 if death occurred, 0 for censoring) |
| 33 | +#' - ttp.durn: duration of TTP from baseline (usually should be equal to pfs.durn) |
| 34 | +#' - ttp.flag: event flag for TTP (=1 if progression occurred, 0 for censoring). |
| 35 | +#' |
| 36 | +#' Survival data for all other endpoints (time to progression, pre-progression death, post-progression survival) are derived from PFS and OS. |
27 | 37 | #' @param dpam List of survival regressions for model endpoints. These must include time to progression (TTP) and pre-progression death (PPD).
|
| 38 | +#' @param psmtype Either "simple" or "complex" PSM formulation |
28 | 39 | #' @param Ty Time duration over which to calculate (defaults to 10 years). Assumes input is in years, and patient-level data is recorded in weeks.
|
29 | 40 | #' @param discrate Discount rate (%) per year (defaults to zero).
|
30 | 41 | #' @param lifetable Optional. The lifetable must be a dataframe with columns named time and lx. The first entry of the time column must be zero. Data should be sorted in ascending order by time, and all times must be unique.
|
|
48 | 59 | #' pps_cf = find_bestfit_spl(fits$pps_cf, "aic")$fit,
|
49 | 60 | #' pps_cr = find_bestfit_spl(fits$pps_cr, "aic")$fit
|
50 | 61 | #' )
|
51 |
| -#' drmd_psm(dpam=params) |
| 62 | +#' drmd_psm(ptdata=bosonc, dpam=params) |
52 | 63 | #' # Add a lifetable constraint
|
53 | 64 | #' ltable <- tibble::tibble(lttime=0:20, lx=1-lttime*0.05)
|
54 |
| -#' drmd_psm(dpam=params, lifetable=ltable) |
55 |
| -drmd_psm <- function(dpam, Ty=10, discrate=0, lifetable=NA, timestep=1) { |
| 65 | +#' drmd_psm(ptdata=bosonc, dpam=params, lifetable=ltable) |
| 66 | +drmd_psm <- function(ptdata, dpam, psmtype="simple", Ty=10, discrate=0, lifetable=NA, timestep=1) { |
56 | 67 | # Declare local variables
|
57 | 68 | Tw <- tvec <- pfprob <- osprob <- adjosprob <- adjfac <- adjprob <- vn <- NULL
|
58 | 69 | # Time horizon in weeks (ceiling)
|
59 | 70 | Tw <- convert_yrs2wks(Ty)
|
60 | 71 | # Create time vector, with half-cycle addition
|
61 |
| - tvec <- timestep*(1:floor(Tw/timestep)) + timestep/2 |
62 |
| - # Membership probabilities with lifetable constraint |
| 72 | + tvec <- timestep*(0:floor(Tw/timestep)) + timestep/2 |
| 73 | + # Obtain all the hazards |
| 74 | + allh <- calc_haz_psm(timevar=tvec, ptdata=ptdata, dpam=dpam, psmtype=psmtype)$adj |
| 75 | + # PFS and OS probabilties from PSM |
63 | 76 | pfprob <- prob_pf_psm(tvec, dpam)
|
64 | 77 | osprob <- prob_os_psm(tvec, dpam)
|
65 |
| - adjosprob <- constrain_survprob(osprob, lifetable=lifetable, timevec=tvec) |
66 |
| - adjfac <- adjosprob/osprob |
67 |
| - adjfac[is.na(adjfac)] <- 1 |
68 |
| - adjpfprob <- pfprob * adjfac |
| 78 | + # OS and PFS may be constrained already by definitions of PPD and PPS |
| 79 | + maxos <- exp(-cumsum(allh$os)) |
| 80 | + maxpfs <- exp(-cumsum(allh$pfs)) |
| 81 | + # Further constrain OS by lifetable |
| 82 | + conos <- constrain_survprob(survprob1=maxos, survprob2=osprob, lifetable=lifetable, timevec=tvec) |
| 83 | + conpfs <- constrain_survprob(survprob1=maxpfs, survprob2=pmin(pfprob, osprob), lifetable=lifetable, timevec=tvec) |
69 | 84 | # Discount factor
|
70 | 85 | vn <- (1+discrate)^(-convert_wks2yrs(tvec+timestep/2))
|
71 | 86 | # Calculate RMDs
|
72 |
| - pf <- sum(adjpfprob*vn) * timestep |
73 |
| - os <- sum(adjosprob*vn) * timestep |
| 87 | + pf <- sum(conpfs*vn) * timestep |
| 88 | + os <- sum(conos*vn) * timestep |
74 | 89 | # Return values
|
75 | 90 | return(list(pf=pf, pd=os-pf, os=os))
|
76 | 91 | }
|
@@ -99,30 +114,37 @@ drmd_psm <- function(dpam, Ty=10, discrate=0, lifetable=NA, timestep=1) {
|
99 | 114 | #' drmd_stm_cf(dpam=params, lifetable=ltable)
|
100 | 115 | drmd_stm_cf <- function(dpam, Ty=10, discrate=0, lifetable=NA, timestep=1) {
|
101 | 116 | # Declare local variables
|
102 |
| - Tw <- tvec <- ppd.ts <- ttp.ts <- sppd <- sttp <- sos <- NULL |
| 117 | + Tw <- tvec <- ppd.ts <- ttp.ts <- pps.ts <- NULsppd <- sttp <- sos <- NULL |
103 | 118 | adjsppd <- adjos <- vn <- pf <- os <- NULL
|
104 | 119 | # Time horizon in weeks (ceiling)
|
105 | 120 | Tw <- convert_yrs2wks(Ty)
|
106 | 121 | # Create time vector, with half-cycle addition
|
107 |
| - tvec <- timestep*(1:floor(Tw/timestep)) + timestep/2 |
| 122 | + tvec <- timestep*(0:floor(Tw/timestep)) + timestep/2 |
108 | 123 | # Pull out type and spec for PPD and TTP
|
109 | 124 | ppd.ts <- convert_fit2spec(dpam$ppd)
|
110 | 125 | ttp.ts <- convert_fit2spec(dpam$ttp)
|
111 |
| - # Calculate S_PPD, S_TTP and S_OS |
| 126 | + pps.ts <- convert_fit2spec(dpam$pps_cf) |
| 127 | + # Obtain hazard and survival functions |
| 128 | + hppd <- tvec |> purrr::map_dbl(~calc_haz(.x, ppd.ts$type, ppd.ts$spec)) |
| 129 | + http <- tvec |> purrr::map_dbl(~calc_haz(.x, ttp.ts$type, ttp.ts$spec)) |
| 130 | + hpps <- tvec |> purrr::map_dbl(~calc_haz(.x, pps.ts$type, pps.ts$spec)) |
112 | 131 | sppd <- tvec |> purrr::map_dbl(~calc_surv(.x, ppd.ts$type, ppd.ts$spec))
|
113 | 132 | sttp <- tvec |> purrr::map_dbl(~calc_surv(.x, ttp.ts$type, ttp.ts$spec))
|
114 |
| - # Next line is the difference with STM-CR |
115 |
| - sos <- prob_os_stm_cf(tvec, dpam) |
116 |
| - # Apply constraints to S_PPD and S_OS |
117 |
| - adjsppd <- constrain_survprob(sppd, lifetable=lifetable, timevec=tvec) |
118 |
| - adjos <- constrain_survprob(sos, lifetable=lifetable, timevec=tvec) |
| 133 | + spps <- tvec |> purrr::map_dbl(~calc_surv(.x, pps.ts$type, pps.ts$spec)) |
| 134 | + # Derive the constrained S_PPD and S_PFS |
| 135 | + con.sppd <- constrain_survprob(sppd, lifetable=lifetable, timevec=tvec) |
| 136 | + con.spps <- constrain_survprob(spps, lifetable=lifetable, timevec=tvec) |
| 137 | + # Partial prob PD |
| 138 | + con.partprobpd <- sttp*con.sppd*http/con.spps |
| 139 | + con.partprobpd[con.spps==0] <- 0 |
| 140 | + con.probpd <- con.spps * cumsum(con.partprobpd) |
119 | 141 | # Discount factor
|
120 | 142 | vn <- (1+discrate)^(-convert_wks2yrs(tvec+timestep/2))
|
121 | 143 | # Calculate RMDs
|
122 |
| - pf <- sum(sttp*adjsppd*vn) * timestep |
123 |
| - os <- sum(adjos*vn) * timestep |
| 144 | + pf <- sum(sttp*con.sppd*vn) * timestep |
| 145 | + pd <- sum(con.probpd*vn) * timestep |
124 | 146 | # Return values
|
125 |
| - return(list(pf=pf, pd=os-pf, os=os)) |
| 147 | + return(list(pf=pf, pd=pd, os=pf+pd)) |
126 | 148 | }
|
127 | 149 |
|
128 | 150 | #' Discretized Restricted Mean Duration calculation for State Transition Model Clock Reset structure
|
@@ -154,25 +176,44 @@ drmd_stm_cr <- function(dpam, Ty=10, discrate=0, lifetable=NA, timestep=1) {
|
154 | 176 | # Time horizon in weeks (ceiling)
|
155 | 177 | Tw <- convert_yrs2wks(Ty)
|
156 | 178 | # Create time vector, with half-cycle addition
|
157 |
| - tvec <- timestep*(1:floor(Tw/timestep)) + timestep/2 |
| 179 | + tvec <- timestep*(0:floor(Tw/timestep)) + timestep/2 |
158 | 180 | # Pull out type and spec for PPD and TTP
|
159 | 181 | ppd.ts <- convert_fit2spec(dpam$ppd)
|
160 | 182 | ttp.ts <- convert_fit2spec(dpam$ttp)
|
161 |
| - # Calculate S_PPD, S_TTP and S_OS |
| 183 | + pps.ts <- convert_fit2spec(dpam$pps_cr) |
| 184 | + # Obtain unconstrained survival functions |
162 | 185 | sppd <- tvec |> purrr::map_dbl(~calc_surv(.x, ppd.ts$type, ppd.ts$spec))
|
163 | 186 | sttp <- tvec |> purrr::map_dbl(~calc_surv(.x, ttp.ts$type, ttp.ts$spec))
|
164 |
| - # Next line is the difference with STM-CF |
165 |
| - sos <- prob_os_stm_cr(tvec, dpam) |
166 |
| - # Apply constraints to S_PPD and S_OS |
167 |
| - adjsppd <- constrain_survprob(sppd, lifetable=lifetable, timevec=tvec) |
168 |
| - adjos <- constrain_survprob(sos, lifetable=lifetable, timevec=tvec) |
| 187 | + # Derive the constrained S_PPD |
| 188 | + c.sppd <- constrain_survprob(sppd, lifetable=lifetable, timevec=tvec) |
| 189 | + # Integrand with constraints on S_PPD and S_PPS |
| 190 | + integrand <- function(u, t) { |
| 191 | + i.http <- calc_haz(u, ttp.ts$type, ttp.ts$spec) |
| 192 | + i.sttp <- calc_surv(u, ttp.ts$type, ttp.ts$spec) |
| 193 | + i.u.sppd <- calc_surv(u, ppd.ts$type, ppd.ts$spec) |
| 194 | + i.u.spps <- calc_surv(t-u, pps.ts$type, pps.ts$spec) |
| 195 | + i.slxu <- calc_ltsurv(convert_wks2yrs(u), lifetable=lifetable) |
| 196 | + i.slxt <- calc_ltsurv(convert_wks2yrs(t), lifetable=lifetable) |
| 197 | + i.c.sppd <- pmin(i.u.sppd, i.slxu) |
| 198 | + i.c.spps <- pmin(i.u.spps, i.slxt/i.slxu) |
| 199 | + i.c.spps[i.slxu==0] <- 0 |
| 200 | + i.c.sppd * i.sttp * i.http * i.c.spps |
| 201 | + } |
| 202 | + integrand <- Vectorize(integrand, "u") |
| 203 | + # PD membership probability is the integral |
| 204 | + probpd <- function(t) { |
| 205 | + stats::integrate(integrand, lower=0, upper=t, t=t)$value |
| 206 | + } |
| 207 | + probpd <- Vectorize(probpd, "t") |
| 208 | + # Calculate the PD membership probability for each time |
| 209 | + c.probpd <- probpd(tvec) |
169 | 210 | # Discount factor
|
170 | 211 | vn <- (1+discrate)^(-convert_wks2yrs(tvec+timestep/2))
|
171 | 212 | # Calculate RMDs
|
172 |
| - pf <- sum(sttp*adjsppd*vn) * timestep |
173 |
| - os <- sum(adjos*vn) * timestep |
| 213 | + pf <- sum(sttp*c.sppd*vn) * timestep |
| 214 | + pd <- sum(c.probpd*vn) * timestep |
174 | 215 | # Return values
|
175 |
| - return(list(pf=pf, pd=os-pf, os=os)) |
| 216 | + return(list(pf=pf, pd=pd, os=pf+pd)) |
176 | 217 | }
|
177 | 218 |
|
178 | 219 |
|
0 commit comments