Skip to content

Commit 3241077

Browse files
authored
Merge pull request #47 from Merck/feature_drmd
Revise RMD calculations
2 parents 787d8aa + ebf13b2 commit 3241077

File tree

1 file changed

+160
-75
lines changed

1 file changed

+160
-75
lines changed

R/discrmd.R

+160-75
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#' - pf: RMD in PF state
4545
#' - pd: RMD in PD state
4646
#' - os: RMD in either alive state
47+
#' @importFrom rlang .data
4748
#' @seealso [drmd_stm_cf()] [drmd_stm_cr()]
4849
#' @noRd
4950
# Examples
@@ -67,32 +68,72 @@ drmd_psm <- function(ptdata, dpam, psmtype="simple", Ty=10, discrate=0, lifetabl
6768
# Declare local variables
6869
Tw <- tvec <- pfprob <- osprob <- adjosprob <- adjfac <- adjprob <- vn <- NULL
6970
# Time horizon in weeks (ceiling)
70-
Tw <- convert_yrs2wks(Ty)
71-
# Create time vector, with half-cycle addition
72-
tvec <- timestep*(0:floor(Tw/timestep)) + timestep/2
71+
Tw <- ceiling(convert_yrs2wks(Ty)/timestep)
72+
# Create dataset, starting with time vector, with half-cycle addition, and unconstrained probs
73+
ds <- tibble::tibble(
74+
tzero = timestep*(0:Tw),
75+
tmid = .data$tzero + timestep/2,
76+
pfprob = prob_pf_psm(.data$tzero, dpam),
77+
osprob = prob_os_psm(.data$tzero, dpam),
78+
u_pf = .data$pfprob,
79+
u_pd = .data$osprob - .data$pfprob
80+
)
7381
# 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
76-
pfprob <- prob_pf_psm(tvec, dpam)
77-
osprob <- prob_os_psm(tvec, dpam)
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)
84-
# Discount factor
85-
vn <- (1+discrate)^(-convert_wks2yrs(tvec+timestep/2))
82+
allh <- calc_haz_psm(timevar=ds$tmid, ptdata=ptdata, dpam=dpam, psmtype=psmtype)$adj
83+
# Derive the unconstrained PPD mortality probability
84+
ds$q_ppd <- 1-exp(-allh$ppd)
85+
# Derive the constrained life table
86+
ds$clx <- calc_ltsurv(convert_wks2yrs(ds$tzero), lifetable)
87+
# Other calculations on the dataset
88+
ds <- ds |>
89+
dplyr::mutate(
90+
# Derive the background mortality for this timepoint
91+
cqx = 1 - dplyr::lead(.data$clx)/.data$clx,
92+
# Derive the TTP probability (balancing item)
93+
q_pfs = 1 - dplyr::lead(.data$u_pf)/.data$u_pf,
94+
q_ttp = .data$q_pfs - .data$q_ppd,
95+
d_pf = .data$u_pf * .data$q_ppd,
96+
c_qpfs = .data$q_ttp + pmax(.data$q_ppd, .data$cqx),
97+
# Derive the PPS mortality probability
98+
d_pfpd = .data$u_pf + .data$u_pd - dplyr::lead(.data$u_pf) - dplyr::lead(.data$u_pd),
99+
d_pps = .data$d_pfpd - .data$d_pf,
100+
q_pps = dplyr::if_else(.data$u_pd==0, 0, .data$d_pps / .data$u_pd),
101+
# Constrained probabilities
102+
cqpfs = .data$q_ttp + pmax(.data$q_ppd, .data$cqx),
103+
cqpps = pmax(.data$q_pps, .data$cqx),
104+
# Derive the constrained PF and PD memberships
105+
c_pf = .data$u_pf,
106+
c_pd = .data$u_pd,
107+
)
108+
# Derive the constrained PF and PD memberships
109+
for (t in 2:(Tw)) {
110+
ds$c_pf[t] = ds$c_pf[t-1] * (1-ds$cqpfs[t-1])
111+
ds$c_pd[t] = ds$c_pf[t-1] * ds$q_ttp[t-1] + ds$c_pd[t-1] * (1 - ds$cqpps[t-1])
112+
}
113+
# The final membership probabilities are zero
114+
ds$c_pf[Tw+1] <- ds$c_pd[Tw+1] <- 0
115+
# Final calculations on the dataset
116+
ds <- ds |>
117+
dplyr::mutate(
118+
# Discount factor
119+
vn = (1+discrate)^(-convert_wks2yrs(.data$tmid)),
120+
# RMD components in each timestep
121+
rmd_pf = (.data$c_pf + dplyr::lead(.data$c_pf))/2 * .data$vn * timestep,
122+
rmd_pd = (.data$c_pd + dplyr::lead(.data$c_pd))/2 * .data$vn * timestep
123+
)
124+
ds$rmd_pf[Tw+1] <- ds$rmd_pd[Tw+1] <- 0
86125
# Calculate RMDs
87-
pf <- sum(conpfs*vn) * timestep
88-
os <- sum(conos*vn) * timestep
126+
pf <- sum(ds$rmd_pf)
127+
pd <- sum(ds$rmd_pd)
89128
# Return values
90-
return(list(pf=pf, pd=os-pf, os=os))
129+
return(list(pf=pf, pd=pd, os=pf+pd, calc=ds))
91130
}
92131

132+
93133
#' Discretized Restricted Mean Duration calculation for State Transition Model Clock Forward structure
94134
#' Calculate restricted mean duration (RMD) in PF, PD and OS states under a State Transition Model Clock Forward structure.
95135
#' @inherit drmd_psm params return
136+
#' @importFrom rlang .data
96137
#' @seealso [drmd_psm()] [drmd_stm_cr()]
97138
#' @noRd
98139
# Examples
@@ -117,35 +158,63 @@ drmd_stm_cf <- function(dpam, Ty=10, discrate=0, lifetable=NA, timestep=1) {
117158
Tw <- tvec <- sppd <- sttp <- sos <- NULL
118159
adjsppd <- adjos <- vn <- pf <- os <- NULL
119160
# Time horizon in weeks (ceiling)
120-
Tw <- convert_yrs2wks(Ty)
121-
# Create time vector, with half-cycle addition
122-
tvec <- timestep*(0:floor(Tw/timestep)) + timestep/2
123-
# Obtain hazard and survival functions
124-
hppd <- tvec |> purrr::map_dbl(~calc_haz(.x, survobj=dpam$ppd))
125-
http <- tvec |> purrr::map_dbl(~calc_haz(.x, survobj=dpam$ttp))
126-
hpps <- tvec |> purrr::map_dbl(~calc_haz(.x, survobj=dpam$pps_cf))
127-
sppd <- tvec |> purrr::map_dbl(~calc_surv(.x, survobj=dpam$ppd))
128-
sttp <- tvec |> purrr::map_dbl(~calc_surv(.x, survobj=dpam$ttp))
129-
spps <- tvec |> purrr::map_dbl(~calc_surv(.x, survobj=dpam$pps_cf))
130-
# Derive the constrained S_PPD and S_PFS
131-
con.sppd <- constrain_survprob(sppd, lifetable=lifetable, timevec=tvec)
132-
con.spps <- constrain_survprob(spps, lifetable=lifetable, timevec=tvec)
133-
# Partial prob PD
134-
con.partprobpd <- sttp*con.sppd*http/con.spps
135-
con.partprobpd[con.spps==0] <- 0
136-
con.probpd <- con.spps * cumsum(con.partprobpd)
137-
# Discount factor
138-
vn <- (1+discrate)^(-convert_wks2yrs(tvec+timestep/2))
161+
Tw <- ceiling(convert_yrs2wks(Ty)/timestep)
162+
# Create dataset, starting with time vector, with half-cycle addition, and unconstrained probs
163+
ds <- tibble::tibble(
164+
tzero = timestep*(0:Tw),
165+
tmid = .data$tzero + timestep/2,
166+
u_pf = prob_pf_stm(.data$tzero, dpam),
167+
u_pd = prob_pd_stm_cf(.data$tzero, dpam),
168+
# Calculate PPD hazard and probability
169+
h_ppd = calc_haz(.data$tmid, survobj=dpam$ppd),
170+
q_ppd = 1-exp(-.data$h_ppd),
171+
# Derive the constrained life table
172+
clx = calc_ltsurv(convert_wks2yrs(.data$tzero), lifetable),
173+
# Derive the background mortality for this timepoint
174+
cqx = 1 - dplyr::lead(.data$clx)/.data$clx,
175+
# Derive the TTP probability (balancing item for PFS)
176+
q_pfs = 1 - dplyr::lead(.data$u_pf)/.data$u_pf,
177+
q_ttp = .data$q_pfs-.data$q_ppd,
178+
d_pf = .data$u_pf * .data$q_ppd,
179+
# Derive the PPS mortality probability
180+
d_pfpd = .data$u_pf + .data$u_pd - dplyr::lead(.data$u_pf) - dplyr::lead(.data$u_pd),
181+
d_pps = .data$d_pfpd - .data$d_pf,
182+
q_pps = dplyr::if_else(.data$u_pd==0, 0, .data$d_pps / .data$u_pd),
183+
# Constrained probabilities
184+
cqpfs = .data$q_ttp + pmax(.data$q_ppd, .data$cqx),
185+
cqpps = pmax(.data$q_pps, .data$cqx),
186+
# Initial constrained PF and PD
187+
c_pf = .data$u_pf,
188+
c_pd = .data$u_pd
189+
)
190+
# Derive the constrained PF and PD memberships
191+
for (t in 2:(Tw)) {
192+
ds$c_pf[t] = ds$c_pf[t-1] * (1-ds$cqpfs[t-1])
193+
ds$c_pd[t] = ds$c_pf[t-1] * ds$q_ttp[t-1] + ds$c_pd[t-1] * (1 - ds$cqpps[t-1])
194+
}
195+
# The final membership probabilities are zero
196+
ds$c_pf[Tw+1] <- ds$c_pd[Tw+1] <- 0
197+
# Final calculations on the dataset
198+
ds <- ds |>
199+
dplyr::mutate(
200+
# Discount factor
201+
vn = (1+discrate)^(-convert_wks2yrs(tmid)),
202+
# RMD components in each timestep
203+
rmd_pf = (.data$c_pf + dplyr::lead(.data$c_pf))/2 * .data$vn * timestep,
204+
rmd_pd = (.data$c_pd + dplyr::lead(.data$c_pd))/2 * .data$vn * timestep
205+
)
206+
ds$rmd_pf[Tw+1] <- ds$rmd_pd[Tw+1] <- 0
139207
# Calculate RMDs
140-
pf <- sum(sttp*con.sppd*vn) * timestep
141-
pd <- sum(con.probpd*vn) * timestep
208+
pf <- sum(ds$rmd_pf)
209+
pd <- sum(ds$rmd_pd)
142210
# Return values
143-
return(list(pf=pf, pd=pd, os=pf+pd))
211+
return(list(pf=pf, pd=pd, os=pf+pd, calc=ds))
144212
}
145213

146214
#' Discretized Restricted Mean Duration calculation for State Transition Model Clock Reset structure
147215
#' Calculate restricted mean duration (RMD) in PF, PD and OS states under a State Transition Model Clock Reset structure.
148216
#' @inherit drmd_psm params return
217+
#' @importFrom rlang .data
149218
#' @seealso [drmd_stm_cf()] [drmd_psm()]
150219
#' @noRd
151220
# Examples
@@ -170,42 +239,58 @@ drmd_stm_cr <- function(dpam, Ty=10, discrate=0, lifetable=NA, timestep=1) {
170239
Tw <- tvec <- sppd <- sttp <- sos <- NULL
171240
adjsppd <- adjos <- vn <- pf <- os <- NULL
172241
# Time horizon in weeks (ceiling)
173-
Tw <- convert_yrs2wks(Ty)
174-
# Create time vector, with half-cycle addition
175-
tvec <- timestep*(0:floor(Tw/timestep)) + timestep/2
176-
# Obtain unconstrained survival functions
177-
sppd <- tvec |> purrr::map_dbl(~calc_surv(.x, survobj=dpam$ppd))
178-
sttp <- tvec |> purrr::map_dbl(~calc_surv(.x, survobj=dpam$ttp))
179-
# Derive the constrained S_PPD
180-
c.sppd <- constrain_survprob(sppd, lifetable=lifetable, timevec=tvec)
181-
# Integrand with constraints on S_PPD and S_PPS
182-
integrand <- function(u, t) {
183-
i.http <- calc_haz(u, survobj=dpam$ttp)
184-
i.sttp <- calc_surv(u, survobj=dpam$ttp)
185-
i.u.sppd <- calc_surv(u, survobj=dpam$ppd)
186-
i.u.spps <- calc_surv(t-u, survobj=dpam$pps_cr)
187-
i.slxu <- calc_ltsurv(convert_wks2yrs(u), lifetable=lifetable)
188-
i.slxt <- calc_ltsurv(convert_wks2yrs(t), lifetable=lifetable)
189-
i.c.sppd <- pmin(i.u.sppd, i.slxu)
190-
i.c.spps <- pmin(i.u.spps, i.slxt/i.slxu)
191-
i.c.spps[i.slxu==0] <- 0
192-
i.c.sppd * i.sttp * i.http * i.c.spps
242+
Tw <- ceiling(convert_yrs2wks(Ty)/timestep)
243+
# Create dataset, starting with time vector, with half-cycle addition, and unconstrained probs
244+
ds <- tibble::tibble(
245+
tzero = timestep*(0:Tw),
246+
tmid = .data$tzero + timestep/2,
247+
u_pf = prob_pf_stm(.data$tzero, dpam),
248+
u_pd = prob_pd_stm_cf(.data$tzero, dpam)
249+
)
250+
# Keep calculating membership probabilities
251+
ds <- ds |>
252+
dplyr::mutate(
253+
# Calculate PPD hazard and probability
254+
h_ppd = calc_haz(.data$tmid, survobj=dpam$ppd),
255+
q_ppd = 1-exp(-h_ppd),
256+
# Derive the constrained life table
257+
clx = calc_ltsurv(convert_wks2yrs(.data$tzero), lifetable),
258+
cqx = 1 - dplyr::lead(.data$clx)/.data$clx,
259+
# Derive the TTP probability (balancing item for PF)
260+
q_pfs = 1 - dplyr::lead(.data$u_pf)/.data$u_pf,
261+
q_ttp = .data$q_pfs - .data$q_ppd,
262+
d_pf = .data$u_pf * .data$q_ppd,
263+
# Derive the PPS mortality probability
264+
d_pfpd = .data$u_pf + .data$u_pd - dplyr::lead(.data$u_pf) - dplyr::lead(.data$u_pd),
265+
d_pps = .data$d_pfpd - .data$d_pf,
266+
q_pps = dplyr::if_else(.data$u_pd==0, 0, .data$d_pps / .data$u_pd),
267+
# Constrained probabilities
268+
cqpfs = .data$q_ttp + pmax(.data$q_ppd, .data$cqx),
269+
cqpps = pmax(.data$q_pps, .data$cqx),
270+
# Initial constrained PF and PD
271+
c_pf = .data$u_pf,
272+
c_pd = .data$u_pd
273+
)
274+
# Derive the constrained PF and PD memberships
275+
for (t in 2:(Tw)) {
276+
ds$c_pf[t] = ds$c_pf[t-1] * (1-ds$cqpfs[t-1])
277+
ds$c_pd[t] = ds$c_pf[t-1] * ds$q_ttp[t-1] + ds$c_pd[t-1] * (1 - ds$cqpps[t-1])
193278
}
194-
integrand <- Vectorize(integrand, "u")
195-
# PD membership probability is the integral
196-
probpd <- function(t) {
197-
stats::integrate(integrand, lower=0, upper=t, t=t)$value
198-
}
199-
probpd <- Vectorize(probpd, "t")
200-
# Calculate the PD membership probability for each time
201-
c.probpd <- probpd(tvec)
202-
# Discount factor
203-
vn <- (1+discrate)^(-convert_wks2yrs(tvec+timestep/2))
279+
# The final membership probabilities are zero
280+
ds$c_pf[Tw+1] <- ds$c_pd[Tw+1] <- 0
281+
# Final calculations on the dataset
282+
ds <- ds |>
283+
dplyr::mutate(
284+
# Discount factor
285+
vn = (1+discrate)^(-convert_wks2yrs(.data$tmid)),
286+
# RMD components in each timestep
287+
rmd_pf = (.data$c_pf + dplyr::lead(.data$c_pf))/2 * .data$vn * timestep,
288+
rmd_pd = (.data$c_pd + dplyr::lead(.data$c_pd))/2 * .data$vn * timestep
289+
)
290+
ds$rmd_pf[Tw+1] <- ds$rmd_pd[Tw+1] <- 0
204291
# Calculate RMDs
205-
pf <- sum(sttp*c.sppd*vn) * timestep
206-
pd <- sum(c.probpd*vn) * timestep
292+
pf <- sum(ds$rmd_pf)
293+
pd <- sum(ds$rmd_pd)
207294
# Return values
208-
return(list(pf=pf, pd=pd, os=pf+pd))
295+
return(list(pf=pf, pd=pd, os=pf+pd, calc=ds))
209296
}
210-
211-

0 commit comments

Comments
 (0)