---
title: "Censoring and Realistic Assessment Schedules"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Censoring and Realistic Assessment Schedules}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(simtte)
```

Every simulation in `vignette("pkpd-time-to-event")` is administratively
censored at `end` only. This vignette covers the rest of the assessment
toolkit, layered on top of any already-simulated `$events`-shaped data
frame (`sim_tte_ode()`, `sim_tte()`, or `sim_tte_df()` alike): independent
right censoring, interval censoring from a visit schedule, and realistic
(jittered, missed-visit, outcome-reactive) visit-schedule generators.

```{r baseline-sim}
sim_uncensored <- sim_tte_ode(model = "exponential",
  param = list(H0 = 0.05), n = 200, end = 30, delta = 2, seed = 1)
table(sim_uncensored$events$sim_reason)
```

## 1. Right censoring

`add_censoring()` layers a second, independent censoring process on top
of any `$events`-shaped output, drawing a per-subject censoring time and
taking `min(event time, censoring time, end)`:

```{r censoring-basic}
censored <- add_censoring(sim_uncensored$events,
  censoring = list(dist = "exponential", rate = 0.02), end = 30, seed = 2)
table(censored$sim_reason)
```

`sim_status`/`sim_time` are updated in place (so any existing code
reading those two columns keeps working); `sim_reason` records why --
`"event"`, `"censored"` (pulled earlier by the independent draw), or
`"administrative"` (hit `end`). `sim_tte_ode()` also accepts a
`censoring = ` argument directly, drawing inside the same seeded call
as `U`:

```{r censoring-arg}
sim_censored <- sim_tte_ode(model = "exponential", param = list(H0 = 0.09),
  n = 200, end = 30, delta = 2,
  censoring = list(dist = "exponential", rate = 0.02), seed = 1)
table(sim_censored$events$sim_reason)
```

`"exponential"`, `"weibull"` (fixed `shape`, solves for `scale`),
`"uniform"` (a fixed interval -- the common case for uniform-accrual
administrative censoring), `"lognormal"` (fixed `sdlog`), `"gamma"`
(fixed `shape`, `rate` only), and `"user"` (any function `n ->
numeric(n)`) are the six `dist` options; see `?add_censoring`.
`draw_censoring_times()` draws directly from any of these specs without
needing an `$events` data frame at all, e.g. to check a distribution's
own shape before applying it. Censoring that depends on a subject's own
simulated PK/PD trajectory (informative dropout, e.g. driven by
toxicity) is out of scope.

### Choosing the censoring rate for a target censoring fraction

**`censoring_rate_for()` solves for a `dist` parameter (rate, scale,
meanlog, or gamma rate) that would give a target independent-censoring
fraction, estimated from the event times of an already-run,
uncensored simulation.** It is an approximation, not an exact
calculation -- the estimate depends on the particular uncensored run
it is given, so always redraw on a larger cohort afterward and check
the realized fraction landed close to the target, the way this example
does:

```{r censoring-rate-for}
# 1. Solve: what rate gives ~20% independently censored, given this
#    model's own event-time scale?
event_times <- sim_uncensored$events$sim_time[
  sim_uncensored$events$sim_status == 1]
rate_20pct <- censoring_rate_for(event_times, target = 0.2)
rate_20pct

# 2. Redraw: apply that rate to a larger cohort.
check <- sim_tte_ode(model = "exponential", param = list(H0 = 0.09),
  n = 2000, end = 30, delta = 2,
  censoring = list(dist = "exponential", rate = rate_20pct), seed = 1)

# 3. Check: did it land close to 20%?
mean(check$events$sim_reason == "censored")
```

If the realized fraction in step 3 is not close enough, re-solve with a
larger or different uncensored sample rather than trusting the first
answer -- the check is the point, not an optional extra.

## 2. Interval censoring

Right censoring still gives an *exact* event time when an event
happens. In a real trial, many endpoints (progression, response) are
only checked at scheduled visits -- an event is known to have happened
somewhere between the last visit where the subject was event-free and
the first visit where it was detected. `add_interval_censoring()` maps
an already-simulated (and, if used, already right-censored) outcome
onto that interval:

```{r interval-basic}
visits <- c(0, 5, 10, 15, 20, 25, 30)
interval <- add_interval_censoring(sim_censored$events, visits = visits)
head(interval[, c("ID", "sim_time", "sim_status", "sim_reason",
  "sim_time_left", "sim_time_right")])
```

`sim_time`/`sim_status`/`sim_reason` are untouched -- `sim_time_left`/
`sim_time_right` are added on top, so anything already reading the
first three keeps working. `sim_time_right` is `Inf` whenever there is
no known upper bound: a censored subject (never detected as an event
under this schedule), or -- more subtly -- an event that genuinely
happened but only *after* the subject's last scheduled visit, so the
schedule itself never confirmed it. `sim_status` still reads `1` for
that second case; the exact columns are the simulation's ground truth,
independent of what the visit schedule happened to observe.

**Order matters**: apply `add_interval_censoring()` *after*
`add_censoring()`, never before -- a dropout changes `sim_time`/
`sim_status`, and the interval mapping needs to see the final value to
know there was no assessment after the dropout time. `sim_tte_ode()`'s
own `censoring =`/`visits =` arguments apply them in this order
automatically.

For a nonparametric interval-censored survival fit, convert
`sim_time_right`'s `Inf` to `NA` (the `survival` package's own
convention for an open upper bound) and hand both bounds to
`survival::Surv(..., type = "interval2")` -- this and the "Informative
assessment schedules" chunk below are the only places in this vignette
that need the optional `survival` package (`Suggests`, not `Imports`);
both are skipped if it is not installed:

```{r interval-survfit, eval = requireNamespace("survival", quietly = TRUE)}
right_for_surv <- ifelse(is.infinite(interval$sim_time_right), NA,
  interval$sim_time_right)
fit <- survival::survfit(survival::Surv(time = sim_time_left,
  time2 = right_for_surv, type = "interval2") ~ 1,
  data = data.frame(sim_time_left = interval$sim_time_left,
    right_for_surv = right_for_surv))
summary(fit, times = visits)
```

### Left censoring

An event with `sim_time_left == 0` -- one that happened before the
first post-baseline visit -- is a **left-censored observation**: the
event is known to have occurred before `sim_time_right`, with the exact
time unknown. `add_interval_censoring()` already produces this with no
separate mechanism:

```{r left-censoring}
ev <- data.frame(ID = 1:3, sim_time = c(5, 15, 25),
  sim_status = c(1L, 1L, 0L))
add_interval_censoring(ev, visits = c(0, 10))
```

Subject 1's row (`sim_time_left = 0`, `sim_time_right = 10`) *is* left
censoring -- "no lower bound" is exactly what `sim_time_left == 0`
means. There is no dedicated `left_censor_at = ` option: it would only
ever rebuild, less completely, what a plain `add_interval_censoring()`
call already gives for the whole cohort at once.

### Delayed entry (not implemented)

A genuinely different concept, easy to confuse with left censoring:
**delayed entry / left truncation**, where a subject is only observed
from some entry time `entry > 0` onward (e.g. enrollment after
treatment already started), and a subject whose event already happened
before `entry` is excluded from the risk set entirely rather than
bounded by an interval. This changes the analysis dataset's *shape* --
a counting-process `survival::Surv(start, stop, event)` rather than a
single time column -- not just how one event is bounded, and nothing in
this package currently represents it.

## 3. Realistic visit schedules

A fixed schedule assumes every visit lands exactly on the nominal day
and every subject attends every one. `visit_schedule()` builds a
per-subject schedule with fixed spacing and, optionally, random jitter
(uniform or truncated-normal); `thin_visits()` and
`visit_schedule_informative()` layer more realistic visit-process
randomness on top.

```{r interval-jitter}
sched <- visit_schedule(n = 200, every = 4, end = 24, jitter = 1, seed = 1)
head(sched)

sim_jittered <- sim_tte_ode(model = "exponential", param = list(H0 = 0.05),
  n = 200, end = 24, delta = 2, visits = list(every = 4, jitter = 1),
  seed = 1)
head(sim_jittered$events[, c("sim_time", "sim_status", "sim_time_left",
  "sim_time_right")])
```

### Missed visits and assessment dropout

`thin_visits()` reads no simulated outcome at all -- it only thins a
schedule, so it can never bias an interval-censored analysis (the
visit process stays independent of the outcome, the assumption most
such analyses make). Each non-baseline visit is independently missed
with probability `p_miss`; independently of that, a subject may also
start dropping out at a random visit and never return, with probability
`p_dropout`:

```{r thin-visits}
realized <- thin_visits(sched, p_miss = 0.1, p_dropout = 0.05, seed = 2)
nrow(sched) - nrow(realized) # visits lost to missed/dropout
out <- add_interval_censoring(sim_censored$events, visits = realized)
head(out[, c("sim_time", "sim_status", "sim_time_left", "sim_time_right")])
```

`thin_visits()` also accepts a plain vector schedule directly, given
the subject IDs to expand it across (`ids = `), and composes with
`add_censoring()` in either relative order -- it only ever touches the
visit schedule, never `events`.

### Informative assessment schedules

`visit_schedule_informative()` is different in kind: it reads
`$events` and lets the visit process **react to the simulated
outcome** -- a visit shortly before a subject's event may be missed at
an elevated rate (`miss_near_event`, e.g. "too unwell to attend"), and
an unscheduled extra visit may be added shortly after it
(`extra_visit_after_event`, e.g. "brought in for follow-up after a
reported symptom"). This makes the resulting schedule **informative by
construction** -- whether/when a subject is assessed now depends on
whether/when they had the event -- which every function in this
package says loudly, on every call, because it breaks the assumption
most interval-censored analyses (including the Turnbull fit above) make
about assessment timing. Use it to *study* that bias, not as a general
visit generator:

```{r informative-bias, message = FALSE, eval = requireNamespace("survival", quietly = TRUE)}
rate <- 0.08
sim_small <- sim_tte_ode(model = "exponential", param = list(H0 = rate),
  n = 800, end = 24, delta = 2, seed = 1)
base_sched <- visit_schedule(n = 800, every = 3, end = 24, seed = 1)
base_sched$ID <- sim_small$events$ID[base_sched$ID]
visit_times <- seq(0, 24, by = 3)
analytical_surv <- exp(-rate * visit_times)

fit_surv <- function(schedule) {
  out <- add_interval_censoring(sim_small$events, visits = schedule)
  right <- ifelse(is.infinite(out$sim_time_right), NA, out$sim_time_right)
  fit <- survival::survfit(survival::Surv(time = sim_time_left, time2 = right,
    type = "interval2") ~ 1,
    data = data.frame(sim_time_left = out$sim_time_left, right = right))
  summary(fit, times = visit_times, extend = TRUE)$surv
}

c1_sched <- thin_visits(base_sched, p_miss = 0.3, seed = 2)
c2_sched <- visit_schedule_informative(base_sched, sim_small$events,
  end = 24, p_miss_base = 0.1, miss_near_event = list(window = 3, p = 0.9),
  seed = 2)

data.frame(time = visit_times,
  truth = round(analytical_surv, 3),
  c1_non_informative = round(fit_surv(c1_sched), 3),
  c2_informative = round(fit_surv(c2_sched), 3))
```

At the same *nominal* miss rate (0.3 for C1's uniform miss; 0.1
background + 0.9 near an event for C2 -- additive, not overriding: a
near-window visit for an event subject is missed with probability
`1 - (1 - p_miss_base) * (1 - p)`, so the two mechanisms compete rather
than one replacing the other), C2's estimate strays further from the
analytical curve than C1's -- missing visits specifically *because* a
subject is about to have the event pulls the estimated survival away
from the truth in a way a uniform miss rate does not. A visit-process
generator reacting to a subject's own PK/PD trajectory, rather than the
exact event time, is out of scope for this release -- it would need
`keep_trajectory = TRUE` and interpolation between reported times, the
same complexity class this package has consistently declined to add.
