Skip to content

Commit be10062

Browse files
74: closed form calculations for PWCsurvOS (#97)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5941ad3 commit be10062

17 files changed

+347
-53
lines changed

NAMESPACE

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export(ExpSurvPFS)
2424
export(PCWInversionMethod)
2525
export(PWCsurvOS)
2626
export(PWCsurvPFS)
27-
export(PwcOSInt)
2827
export(WeibOSInteg)
2928
export(WeibSurvOS)
3029
export(WeibSurvPFS)
@@ -50,7 +49,7 @@ export(getNumberEvents)
5049
export(getOneClinicalTrial)
5150
export(getOneToTwoRows)
5251
export(getPCWDistr)
53-
export(getPCWHazard)
52+
export(getPWCHazard)
5453
export(getResults)
5554
export(getSimulatedData)
5655
export(getSumPCW)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
- `ExpSurvOS` now returns 0 instead of NaN for large values of t.
1111
- `WeibSurvOS` now does not return an error for large values of t.
1212
- `PWCSurvOS` now does not return an error for large values of t.
13+
- `PWCSurvOS` no longer returns values larger than 1, and is significantly faster, based on a closed form calculation instead of numerical integration.
1314
- `getSimulatedData` now also works when there are no transitions from progression to death, similarly for `getOneClinicalTrial` (which now warns if there are no such transitions at all).
1415

16+
### Miscellaneous
17+
18+
- Renamed piecewise constant hazards function to `getPWCHazard` (previously `getPCWHazard`).
19+
- `PwcOSInt` is no longer exported, and only used for internal tests.
20+
1521
# simIDM 0.0.5
1622

1723
- First CRAN version of the package.

R/corPFSOS.R

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ survPFS <- function(transition, t) {
1616
UseMethod("survPFS")
1717
}
1818

19-
#' @describeIn PFS Survival Function for an exponential transition model.
19+
#' @describeIn survPFS Survival Function for an exponential transition model.
2020
#' @export
2121
#'
2222
#' @examples
@@ -26,7 +26,7 @@ survPFS.ExponentialTransition <- function(transition, t) {
2626
ExpSurvPFS(t = t, h01 = transition$hazards$h01, h02 = transition$hazards$h02)
2727
}
2828

29-
#' @describeIn PFS Survival Function for a Weibull transition model.
29+
#' @describeIn survPFS Survival Function for a Weibull transition model.
3030
#' @export
3131
#'
3232
#' @examples
@@ -39,7 +39,7 @@ survPFS.WeibullTransition <- function(transition, t) {
3939
)
4040
}
4141

42-
#' @describeIn PFS Survival Function for a piecewise constant transition model.
42+
#' @describeIn survPFS Survival Function for a piecewise constant transition model.
4343
#' @export
4444
#'
4545
#' @examples
@@ -73,7 +73,7 @@ survOS <- function(transition, t) {
7373
UseMethod("survOS")
7474
}
7575

76-
#' @describeIn OS Survival Function for an exponential transition model.
76+
#' @describeIn survOS Survival Function for an exponential transition model.
7777
#' @export
7878
#'
7979
#' @examples
@@ -86,7 +86,7 @@ survOS.ExponentialTransition <- function(transition, t) {
8686
)
8787
}
8888

89-
#' @describeIn OS Survival Function for a Weibull transition model.
89+
#' @describeIn survOS Survival Function for a Weibull transition model.
9090
#' @export
9191
#'
9292
#' @examples
@@ -100,7 +100,7 @@ survOS.WeibullTransition <- function(transition, t) {
100100
)
101101
}
102102

103-
#' @describeIn OS Survival Function for a piecewise constant transition model.
103+
#' @describeIn survOS Survival Function for a piecewise constant transition model.
104104
#' @export
105105
#'
106106
#' @examples

R/estimateParams.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ haz.WeibullTransition <- function(transition, t, trans) {
148148
#' )
149149
#' haz(transition, 6, 2)
150150
haz.PWCTransition <- function(transition, t, trans) {
151-
getPCWHazard(unlist(transition$hazards[trans], use.names = FALSE),
151+
getPWCHazard(unlist(transition$hazards[trans], use.names = FALSE),
152152
unlist(transition$intervals[trans], use.names = FALSE),
153153
x = t
154154
)

R/getSimulatedData.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,9 @@ getSimulatedData <- function(N,
204204
Sum_PCW <- getSumPCW(h$h01, h$h02, pw$pw01, pw$pw02)
205205
wait_time <- getPCWDistr(U, Sum_PCW$hazards, Sum_PCW$intervals, entry)
206206
# A binomial experiment decides on death or progression.
207-
numerator <- getPCWHazard(h$h01, pw$pw01, wait_time)
208-
denumerator <- getPCWHazard(h$h01, pw$pw01, wait_time) +
209-
getPCWHazard(h$h02, pw$pw02, wait_time)
207+
numerator <- getPWCHazard(h$h01, pw$pw01, wait_time)
208+
denumerator <- getPWCHazard(h$h01, pw$pw01, wait_time) +
209+
getPWCHazard(h$h02, pw$pw02, wait_time)
210210
}
211211

212212
to_prob <- stats::rbinom(N, 1, numerator / denumerator)

R/piecewiseHazards.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
#' @export
1111
#'
1212
#' @examples
13-
#' getPCWHazard(c(1, 1.2, 1.4), c(0, 2, 3), c(1, 4, 6))
14-
getPCWHazard <- function(haz, pw, x) { # nolint
13+
#' getPWCHazard(c(1, 1.2, 1.4), c(0, 2, 3), c(1, 4, 6))
14+
getPWCHazard <- function(haz, pw, x) { # nolint
1515
sapply(x, function(jj) {
1616
y <- NULL
1717
# Find interval and corresponding hazard value for time x[jj].
@@ -44,8 +44,8 @@ getSumPCW <- function(haz1, haz2, pw1, pw2) {
4444
haz_sum <- NULL
4545
# Get sum of hazards for all intervals.
4646
for (i in seq_along(cuts_sum)) {
47-
haz_sum[i] <- getPCWHazard(haz1, pw1, cuts_sum[i]) +
48-
getPCWHazard(haz2, pw2, cuts_sum[i])
47+
haz_sum[i] <- getPWCHazard(haz1, pw1, cuts_sum[i]) +
48+
getPWCHazard(haz2, pw2, cuts_sum[i])
4949
}
5050
list(
5151
hazards = haz_sum,

R/survivalFunctions.R

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ pwA <- function(t, haz, pw) {
152152
# Time intervals: time-points + cut points.
153153
pw_new <- c(pw[pw < max(t)])
154154
pw_time <- sort(unique(c(pw_new, t)))
155-
haz_all <- getPCWHazard(haz, pw, pw_time)
155+
haz_all <- getPWCHazard(haz, pw, pw_time)
156156

157157
# Cumulative hazard.
158158
i <- 1:(length(pw_time) - 1)
@@ -193,13 +193,11 @@ PWCsurvPFS <- function(t, h01, h02, pw01, pw02) {
193193
#'
194194
#' @return Numeric results of the integrand used to calculate
195195
#' the OS survival function for piecewise constant transition hazards, see `PWCsurvOS`.
196-
#' @export
197196
#'
198-
#' @examples
199-
#' PwcOSInt(1:5, 6, c(0.3, 0.5), c(0.5, 0.8), c(0.7, 1), c(0, 4), c(0, 8), c(0, 3))
197+
#' @keywords internal
200198
PwcOSInt <- function(x, t, h01, h02, h12, pw01, pw02, pw12) {
201199
PWCsurvPFS(x, h01, h02, pw01, pw02) *
202-
getPCWHazard(h01, pw01, x) *
200+
getPWCHazard(h01, pw01, x) *
203201
exp(pwA(x, h12, pw12) - pwA(t, h12, pw12))
204202
}
205203

@@ -215,7 +213,6 @@ PwcOSInt <- function(x, t, h01, h02, h12, pw01, pw02, pw12) {
215213
#'
216214
#' @return This returns the value of OS survival function at time t.
217215
#' @export
218-
219216
#'
220217
#' @examples
221218
#' PWCsurvOS(1:5, c(0.3, 0.5), c(0.5, 0.8), c(0.7, 1), c(0, 4), c(0, 8), c(0, 3))
@@ -228,19 +225,75 @@ PWCsurvOS <- function(t, h01, h02, h12, pw01, pw02, pw12) {
228225
assert_intervals(pw02, length(h02))
229226
assert_intervals(pw12, length(h12))
230227

231-
PWCsurvPFS(t, h01, h02, pw01, pw02) +
232-
sapply(t, function(t) {
233-
integrateVector(PwcOSInt,
234-
upper = t,
235-
t = t,
236-
h01 = h01,
237-
h02 = h02,
238-
h12 = h12,
239-
pw01 = pw01,
240-
pw02 = pw02,
241-
pw12 = pw12
228+
# Assemble unique time points and corresponding hazard values once.
229+
unique_pw_times <- sort(unique(c(pw01, pw02, pw12)))
230+
h01_at_times <- getPWCHazard(h01, pw01, unique_pw_times)
231+
h02_at_times <- getPWCHazard(h02, pw02, unique_pw_times)
232+
h12_at_times <- getPWCHazard(h12, pw12, unique_pw_times)
233+
234+
# We work for the integral parts with sorted and unique time points.
235+
t_sorted <- sort(unique(t))
236+
t_sorted_zero <- c(0, t_sorted)
237+
n_t <- length(t_sorted)
238+
int_sums <- numeric(n_t)
239+
cum_haz_12 <- pwA(t_sorted, h12, pw12)
240+
for (i in seq_len(n_t)) {
241+
t_start <- t_sorted_zero[i]
242+
t_end <- t_sorted_zero[i + 1]
243+
# Determine the indices of the time intervals we are working with here.
244+
start_index <- findInterval(t_start, unique_pw_times)
245+
# We use here left.open = TRUE, so that when t_end is identical to a change point,
246+
# then we don't need an additional part.
247+
end_index <- max(findInterval(t_end, unique_pw_times, left.open = TRUE), 1L)
248+
index_bounds <- start_index:end_index
249+
# Determine corresponding time intervals.
250+
time_bounds <- if (length(index_bounds) == 1L) {
251+
rbind(c(t_start, t_end))
252+
} else if (length(index_bounds) == 2L) {
253+
rbind(
254+
c(t_start, unique_pw_times[end_index]),
255+
c(unique_pw_times[end_index], t_end)
242256
)
243-
})
257+
} else {
258+
n_ind_bounds <- length(index_bounds)
259+
rbind(
260+
c(t_start, unique_pw_times[start_index + 1L]),
261+
cbind(
262+
unique_pw_times[index_bounds[-c(1, n_ind_bounds)]],
263+
unique_pw_times[index_bounds[-c(1, 2)]]
264+
),
265+
c(unique_pw_times[end_index], t_end)
266+
)
267+
}
268+
for (j in seq_along(index_bounds)) {
269+
time_j <- time_bounds[j, 1]
270+
time_jp1 <- time_bounds[j, 2]
271+
intercept_a <- pwA(time_j, h12, pw12) - pwA(time_j, h01, pw01) - pwA(time_j, h02, pw02)
272+
slope_b <- h12_at_times[index_bounds[j]] - h01_at_times[index_bounds[j]] - h02_at_times[index_bounds[j]]
273+
h01_j <- h01_at_times[index_bounds[j]]
274+
int_js <- if (slope_b != 0) {
275+
h01_j / slope_b * exp(intercept_a) *
276+
(exp(slope_b * (time_jp1 - time_j) - cum_haz_12[i:n_t]) - exp(-cum_haz_12[i:n_t]))
277+
} else {
278+
h01_j * exp(intercept_a - cum_haz_12[i:n_t]) * (time_jp1 - time_j)
279+
}
280+
int_sums[i:n_t] <- int_sums[i:n_t] + int_js
281+
}
282+
}
283+
284+
# Match back to all time points,
285+
# which might not be unique or ordered.
286+
int_sums <- int_sums[match(t, t_sorted)]
287+
result <- PWCsurvPFS(t, h01, h02, pw01, pw02) + int_sums
288+
289+
# Cap at 1 in a safe way - i.e. first check.
290+
assert_numeric(result, finite = TRUE, any.missing = FALSE)
291+
above_one <- result > 1
292+
if (any(above_one)) {
293+
assert_true(all((result[above_one] - 1) < sqrt(.Machine$double.eps)))
294+
result[above_one] <- 1
295+
}
296+
result
244297
}
245298

246299
#' Helper Function for Single Quantile for OS Survival Function

_pkgdown.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ reference:
3838
- weibull_transition
3939
- title: Helper functions for piecewise or Weibull Hazards
4040
contents:
41-
- getPCWHazard
41+
- getPWCHazard
4242
- getSumPCW
4343
- getPCWDistr
4444
- PCWInversionMethod
@@ -72,7 +72,6 @@ reference:
7272
- WeibSurvOS
7373
- pwA
7474
- PWCsurvPFS
75-
- PwcOSInt
7675
- PWCsurvOS
7776
- title: Hazard functions
7877
contents:

design/design_getSimulatedData.Rmd

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,9 @@ getSimulatedData <- function(N,
261261
unique(sort(c(pw$pw01, pw$pw02))), entry
262262
)
263263
### binomial experiment decides on death or progression
264-
numerator <- getPCWHazard(h$h01, pw$pw01, wait_time)
265-
denumerator <- getPCWHazard(h$h01, pw$pw01, wait_time) +
266-
getPCWHazard(h$h02, pw$pw02, wait_time)
264+
numerator <- getPWCHazard(h$h01, pw$pw01, wait_time)
265+
denumerator <- getPWCHazard(h$h01, pw$pw01, wait_time) +
266+
getPWCHazard(h$h02, pw$pw02, wait_time)
267267
}
268268
269269
to_prob <- rbinom(N, 1, numerator / denumerator)
@@ -300,7 +300,7 @@ getSimulatedData <- function(N,
300300
`getsimulateData` uses the following helper functions:
301301

302302
- `getOneTwoTransitionRows`: generates rows with transition times for patients in the transient state progression, i.e. entry and exit times for 1-> 2 or 1-> cens transitions
303-
- `getPCWHazard`: for piecewise-constant hazards: function to get hazard value at time x
303+
- `getPWCHazard`: for piecewise-constant hazards: function to get hazard value at time x
304304
- `getSumPCW`: calculates the sum of two piece-wise constant hazards
305305
- `getPCWDistr`: generates event times with a distribution resulting
306306
from specified piece-wise constant hazards
@@ -378,7 +378,7 @@ addStaggeredEntry <- function(simData, N, accrualParam, accrualValue) {
378378
### or Weibull distributed event times
379379
380380
## for piecewise-constant hazards: function to get hazard value at time x
381-
getPCWHazard <- function(haz, pw, x) {
381+
getPWCHazard <- function(haz, pw, x) {
382382
hazVal <- sapply(x, function(jj) {
383383
y <- NULL
384384
# find interval and correponding hazard value for time x[jj]
@@ -401,8 +401,8 @@ getSumPCW <- function(haz1, haz2, pw1, pw2) {
401401
haz_sum <- NULL
402402
## get sum of hazards for all intervals
403403
for (i in 1:(length(cuts_sum))) {
404-
haz_sum[i] <- getPCWHazard(haz1, pw1, cuts_sum[i]) +
405-
getPCWHazard(haz2, pw2, cuts_sum[i])
404+
haz_sum[i] <- getPWCHazard(haz1, pw1, cuts_sum[i]) +
405+
getPWCHazard(haz2, pw2, cuts_sum[i])
406406
}
407407
return(haz_sum)
408408
}

inst/WORDLIST

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,22 @@ calender
2525
cdot
2626
censAct
2727
doi
28+
dotsb
29+
dotsc
30+
du
2831
du
2932
entryAct
3033
exitAct
3134
frac
3235
funder
36+
geq
3337
ik
3438
infty
3539
integrand
3640
lapply
3741
mathbb
3842
multistate
43+
neq
3944
pre
4045
recruitTime
4146
rightarrow

0 commit comments

Comments
 (0)