---
title: "Using your own data generator"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Using your own data generator}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

## Overview

`simdata_fast()` generates piecewise-exponential survival and dropout times,
subgroups, multi-arm trials, and correlated endpoints from an illness-death
model. Other data-generating models, such as Weibull or cure-model survival,
copula-based dependence, or resampling from historical data, are easy to write
in a few lines of R. The analysis side of FastSurvival does not depend on how
the data were generated: `cutoff_fast()`, `analysis_fast()`, `switch_fast()`,
and `simsummary_fast()` read a data frame with a fixed set of columns. This
vignette generates trials from a Weibull model with a cure fraction in the
treatment group, a model that `simdata_fast()` does not provide, and runs the
same workflow as for data from `simdata_fast()`.

The columns that the functions read are

| Function | Required columns |
|----------|------------------|
| `cutoff_fast()` | `sim`, `accrual_time`, and the time and event columns named by `tte.col` and `event.col` (`tte` and `event` by default) |
| `analysis_fast()`, `pairwise_fast()` | `sim`, `group`, `accrual_time`, `tte`, `event` |
| `switch_fast()` | `sim`, `group`, `accrual_time`, and the latent times `surv_time` and `dropout_time` |

where `tte` is the observed time from accrual, `event` is 1 for an event and 0
for censoring, and `accrual_time` is the calendar time of enrollment. The
latent columns are needed only by `switch_fast()`, which changes the survival
time after the switch and recomputes the observed columns.

```{r load}
library(FastSurvival)
```

## A Weibull model with a cure fraction

The control group has Weibull survival with shape 1.3 and scale 16 months. In
the treatment group 15% of the subjects are cured and the others have Weibull
survival with the same shape and scale 18 months, so the survival curves
separate slowly and the treatment curve levels off at 0.15. Subjects are
enrolled uniformly over 12 months and drop out at a constant rate of 0.01 per
month. The generator below writes the columns listed above for all simulated
trials at once.

```{r generator}
gen_cure_weibull <- function(nsim, n_per, accrual, shape, scale, cure,
                             drop_rate, seed) {
  set.seed(seed)
  n_tot <- 2L * n_per
  N     <- nsim * n_tot
  sim   <- rep(seq_len(nsim), each = n_tot)
  group <- rep(rep(1:2, each = n_per), times = nsim)
  accrual_time <- stats::runif(N, 0, accrual)
  surv_time    <- stats::rweibull(N, shape = shape, scale = scale[group])
  surv_time[stats::runif(N) < cure[group]] <- Inf   # cured subjects
  dropout_time <- stats::rexp(N, rate = drop_rate)
  tte <- pmin(surv_time, dropout_time)
  data.frame(sim, group, accrual_time, surv_time, dropout_time, tte,
             event = as.integer(surv_time <= dropout_time),
             calendar_time = accrual_time + tte)
}

shape <- 1.3
scale <- c(16, 18)
cure  <- c(0, 0.15)

dat <- gen_cure_weibull(nsim = 2000, n_per = 200, accrual = 12,
                        shape = shape, scale = scale, cure = cure,
                        drop_rate = 0.01, seed = 2026)
head(dat)
```

A quick check of the generator compares the proportion of latent survival
times beyond a few time points with the survival function of the model,
`cure + (1 - cure) * exp(-(t / scale)^shape)`.

```{r check-generator}
surv_model <- function(t, g) cure[g] + (1 - cure[g]) * exp(-(t / scale[g])^shape)
t_chk <- c(6, 12, 24, 36)
data.frame(
  t               = t_chk,
  control_sim     = sapply(t_chk, function(t) mean(dat$surv_time[dat$group == 1] > t)),
  control_model   = surv_model(t_chk, 1),
  treatment_sim   = sapply(t_chk, function(t) mean(dat$surv_time[dat$group == 2] > t)),
  treatment_model = surv_model(t_chk, 2)
)
```

## Analysis times

The interim analysis takes place at 150 events or at month 24, whichever comes
first, and the final analysis at 280 events, but not earlier than 9 months
after the interim and not later than month 48. `cutoff_fast()` returns the
calendar time of both looks for every simulated trial.

```{r cutoffs}
cut <- cutoff_fast(dat, event.looks = c(150, 280), max.time = c(24, 48),
                   min.gap = c(NA, 9))
head(cut)
colMeans(cut)
```

## Log-rank and max-combo tests

Because the treatment effect builds up late and ends in a plateau, the
max-combo test of Fleming-Harrington weights is compared with the log-rank
test. The nominal one-sided levels 0.002 and 0.024 are close to those of an
O'Brien-Fleming-type spending function at these looks and are used for
illustration. With `mc.alpha` set to these levels, the max-combo p-value is
integrated only when the Bonferroni bounds do not decide the comparison with
the level, which leaves the decisions unchanged.

```{r analysis}
alpha_look <- c(0.002, 0.024)
set.seed(1)
res <- analysis_fast(dat, control = 1, cutoff.looks = cut,
                     stat = c("logrank", "maxcombo"), side = 1,
                     mc.alpha = alpha_look)

oc_lr <- simsummary_fast(res, p.col = "logrank.p",  alpha = alpha_look)
oc_mc <- simsummary_fast(res, p.col = "maxcombo.p", alpha = alpha_look)
data.frame(
  test             = c("Log-rank", "Max-combo"),
  reject_interim   = c(oc_lr[oc_lr$look == "1", "prob.stop.efficacy"],
                       oc_mc[oc_mc$look == "1", "prob.stop.efficacy"]),
  power            = c(oc_lr[oc_lr$look == "overall", "cum.reject"],
                       oc_mc[oc_mc$look == "overall", "cum.reject"])
)
mean(res$maxcombo.p.exact, na.rm = TRUE)
```

The last value is the share of the max-combo p-values that required the
multivariate normal integral.

## Crossover after the interim analysis

`switch_fast()` works on the same data. Here every control subject who is still
on study at the interim analysis switches to the experimental treatment, which
multiplies the remaining survival time by 1.5. The final analysis is then
triggered by the events of the modified data. The outcomes observed by the
interim analysis are unchanged, so the interim cutoffs and statistics are the
same as before.

```{r switching}
sw <- switch_fast(dat, group = 1, when = "cutoff",
                  cutoff = cut[, 1, drop = FALSE], aft.factor = 1.5)
cut_sw <- cutoff_fast(sw, event.looks = c(150, 280), max.time = c(24, 48),
                      min.gap = c(NA, 9))
all.equal(cut_sw[, 1], cut[, 1])

res_sw <- analysis_fast(sw, control = 1, cutoff.looks = cut_sw,
                        stat = "logrank", side = 1)
all.equal(res_sw$logrank.z[res_sw$look == 1], res$logrank.z[res$look == 1])

oc_sw <- simsummary_fast(res_sw, p.col = "logrank.p", alpha = alpha_look)
c(log_rank_power_without_switching = oc_lr[oc_lr$look == "overall", "cum.reject"],
  log_rank_power_with_crossover    = oc_sw[oc_sw$look == "overall", "cum.reject"])
```

## Remarks

Any generator can be used in the same way, provided that it writes one row per
subject with the columns above and that the simulation identifiers group the
rows of each trial. The data do not need to be sorted, but generating them
grouped by `sim`, as `simdata_fast()` does, avoids a sorting step in the
analysis functions. For an illness-death model, `switch_fast()` reads the
columns `e1_surv_time`, `e2_surv_time`, `dropout_time`, and `intermediate`
instead, as described in its help page.
