Skip to content

Add possibility for left-truncation to ppc_km_overlay() and ppc_km_overlay_grouped() #347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# bayesplot (development version)

* Add possibility for left-truncation to `ppc_km_overlay()` and `ppc_km_overlay_grouped()` by @Sakuski
* Added `ppc_loo_pit_ecdf()` by @TeemuSailynoja

# bayesplot 1.12.0
Expand Down
26 changes: 25 additions & 1 deletion R/ppc-censoring.R
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,29 @@
#' \donttest{
#' ppc_km_overlay_grouped(y, yrep[1:25, ], group = group, status_y = status_y)
#' }
#' # With left-truncation (delayed entry) times:
#' left_truncation_y <- runif(length(y), min = 0, max = 0.6) * y
#' \donttest{
#' ppc_km_overlay(y, yrep[1:25, ], status_y = status_y,
#' left_truncation_y = left_truncation_y)
#' }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get a warning when I try this example:

Warning message:
In survival::Surv(time = left_truncation_y[data$y_id], time2 = data$value,  :
  Stop time must be > start time, NA created

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning was to be expected in case of that example, but I made a cleaner example that does not produce warnings. In addition, I think the new example visualizes the effect of left-truncation a bit better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In real life, the warning is an indication of an impossible observation or posterior predictive draw, so the warning itself is good. When creating examples, we just have to make sure that each value in left_truncation_y is lower than the corresponding value in y or corresponding values in yrep so we don't get the warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok great, thanks, that makes sense.

NULL

#' @export
#' @rdname PPC-censoring
#' @param status_y The status indicator for the observations from `y`. This must
#' be a numeric vector of the same length as `y` with values in \{0, 1\} (0 =
#' right censored, 1 = event).
#' @param left_truncation_y Optional parameter that specifies left-truncation
#' (delayed entry) times for the observations from `y`. This must
#' be a numeric vector of the same length as `y`. If `NULL` (default),
#' no left-truncation is assumed.
ppc_km_overlay <- function(
y,
yrep,
...,
status_y,
left_truncation_y = NULL,
size = 0.25,
alpha = 0.7
) {
Expand All @@ -82,6 +93,12 @@ ppc_km_overlay <- function(
stopifnot(is.numeric(status_y))
stopifnot(all(status_y %in% c(0, 1)))

if (!is.null(left_truncation_y)) {
if (!is.numeric(left_truncation_y) || length(left_truncation_y) != length(y)) {
stop("`left_truncation_y` must be a numeric vector of the same length as `y`.")
}
}

data <- ppc_data(y, yrep, group = status_y)

# Modify the status indicator:
Expand All @@ -96,7 +113,12 @@ ppc_km_overlay <- function(
as.numeric(as.character(.data$group)),
1))

sf_form <- survival::Surv(value, group) ~ rep_label
if (is.null(left_truncation_y)) {
sf_form <- survival::Surv(time = data$value, event = data$group) ~ rep_label
} else {
sf_form <- survival::Surv(time = left_truncation_y[data$y_id], time2 = data$value, event = data$group) ~ rep_label
}

if (!is.null(add_group)) {
data <- dplyr::inner_join(data,
tibble::tibble(y_id = seq_along(y),
Expand Down Expand Up @@ -164,6 +186,7 @@ ppc_km_overlay_grouped <- function(
group,
...,
status_y,
left_truncation_y = NULL,
size = 0.25,
alpha = 0.7
) {
Expand All @@ -175,6 +198,7 @@ ppc_km_overlay_grouped <- function(
add_group = group,
...,
status_y = status_y,
left_truncation_y = left_truncation_y,
size = size,
alpha = alpha
)
Expand Down
32 changes: 30 additions & 2 deletions man/PPC-censoring.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions tests/testthat/_snaps/ppc-censoring/ppc-km-overlay-default-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions tests/testthat/data-for-ppc-tests.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ y <- rnorm(100)
yrep <- matrix(rnorm(2500), ncol = 100)
group <- gl(4, 25, labels = LETTERS[1:4])
status_y <- rep_len(0:1, length.out = length(y))
left_truncation_y <- y - 10

y2 <- rpois(30, 1)
yrep2 <- matrix(rpois(30, 1), ncol = 30)
Expand All @@ -26,4 +27,22 @@ vdiff_loo_y <- rnorm(100, 30, 5)
vdiff_loo_yrep <- matrix(rnorm(100 * 400, 30, 5), nrow = 400)
vdiff_loo_lw <- vdiff_loo_yrep
vdiff_loo_lw[] <- rnorm(100 * 400, -8, 2)


vdiff_y3 <- rexp(50, rate = 0.2)
vdiff_status_y3 <- rep_len(0:1, length.out = length(vdiff_y3))
vdiff_group3 <- rep_len(c(1,2), length.out = 50)
vdiff_left_truncation_y3 <- runif(length(vdiff_y3), min = 0, max = 0.6) * vdiff_y3

simulate_truncated_exp <- function(n, rate, trunc_point) {
u <- runif(n)
return(trunc_point - log(u) / rate)
}

rate <- 0.2
vdiff_yrep3 <- matrix(NA, nrow = 10, ncol = 50)
for (i in 1:50) {
vdiff_yrep3[, i] <- simulate_truncated_exp(10, rate, vdiff_left_truncation_y3[i])
}

set.seed(seed = NULL)
42 changes: 38 additions & 4 deletions tests/testthat/test-ppc-censoring.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ source(test_path("data-for-ppc-tests.R"))

test_that("ppc_km_overlay returns a ggplot object", {
skip_if_not_installed("ggfortify")
expect_gg(ppc_km_overlay(y, yrep, status_y = status_y, size = 0.5, alpha = 0.2))
expect_gg(ppc_km_overlay(y, yrep, status_y = status_y, left_truncation_y = left_truncation_y, size = 0.5, alpha = 0.2))
expect_gg(ppc_km_overlay(y2, yrep2, status_y = status_y2))
})

test_that("ppc_km_overlay_grouped returns a ggplot object", {
skip_if_not_installed("ggfortify")
expect_gg(ppc_km_overlay_grouped(y, yrep, group,
status_y = status_y))
status_y = status_y,
left_truncation_y = left_truncation_y,
size = 0.5, alpha = 0.2))
expect_gg(ppc_km_overlay_grouped(y, yrep, as.numeric(group),
status_y = status_y))
status_y = status_y,
left_truncation_y = left_truncation_y,
size = 0.5, alpha = 0.2))
expect_gg(ppc_km_overlay_grouped(y, yrep, as.integer(group),
status_y = status_y))
status_y = status_y,
left_truncation_y = left_truncation_y,
size = 0.5, alpha = 0.2))

expect_gg(ppc_km_overlay_grouped(y2, yrep2, group2,
status_y = status_y2))
Expand Down Expand Up @@ -44,6 +50,17 @@ test_that("ppc_km_overlay renders correctly", {
size = 2,
alpha = .2)
vdiffr::expect_doppelganger("ppc_km_overlay (size, alpha)", p_custom)

p_base2 <- ppc_km_overlay(vdiff_y3, vdiff_yrep3, status_y = vdiff_status_y3)
vdiffr::expect_doppelganger("ppc_km_overlay (default 2)", p_base2)

p_custom2 <- ppc_km_overlay(
vdiff_y3,
vdiff_yrep3,
status_y = vdiff_status_y3,
left_truncation_y = vdiff_left_truncation_y3)
vdiffr::expect_doppelganger("ppc_km_overlay (left_truncation_y)",
p_custom2)
})

test_that("ppc_km_overlay_grouped renders correctly", {
Expand All @@ -69,4 +86,21 @@ test_that("ppc_km_overlay_grouped renders correctly", {
"ppc_km_overlay_grouped (size, alpha)",
p_custom
)

p_base2 <- ppc_km_overlay_grouped(vdiff_y3, vdiff_yrep3, vdiff_group3,
status_y = vdiff_status_y3)
vdiffr::expect_doppelganger("ppc_km_overlay_grouped (default 2)", p_base2)

p_custom2 <- ppc_km_overlay_grouped(
vdiff_y3,
vdiff_yrep3,
vdiff_group3,
status_y = vdiff_status_y3,
left_truncation_y = vdiff_left_truncation_y3
)

vdiffr::expect_doppelganger(
"ppc_km_overlay_grouped (left_truncation_y)",
p_custom2
)
})