---
title: "End-of-Follow-up Outcomes"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{End-of-Follow-up Outcomes}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  eval = identical(Sys.getenv("IN_PKGDOWN"), "true") && identical(Sys.getenv("GITHUB_ACTIONS"), "true")
)
data.table::setDTthreads(2)
```

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

## What this is for

The rest of the `SEQTaRget` package estimates survival outcomes: a binary event that may
occur at any point during follow-up, summarised through risks, survival curves or
a hazard ratio.

An end-of-follow-up outcome is different. It is measured once, at a single
follow-up time chosen by the user, e.g., a biomarker at 12 months, disease status at two
years, a questionnaire score at the end of the trial. There is no time-to-event
to model, and the quantity of interest is simply the average outcome in each
treatment arm.

The `SEQopts()` option `end_of_fup = TRUE` switches to that estimand. For each trial-period the outcome
is read at the requested follow-up time and averaged within each baseline
treatment arm, weighted by the period-trial-specific weight at the time the
measurement was taken. For a binary outcome this is the weighted proportion in
each arm; for a continuous outcome, the weighted mean.

Because there is no outcome model, `end_of_fup` cannot be combined with
`km.curves`, `hazard`, `compevent`, or the dose-response method.

## A minimal example

`end_of_fup.time` is the follow-up time `k` at which the outcome is evaluated,
counted in follow-up periods since trial enrollment (not calendar time).

```{r}
options <- SEQopts(end_of_fup = TRUE,
                   # evaluate the outcome 12 follow-up periods after enrollment
                   end_of_fup.time = 12,
                   # "binary" reports a proportion, "continuous" a mean
                   end_of_fup.type = "binary",
                   bootstrap = TRUE,
                   # fixes the bootstrap resamples, so the intervals below are
                   # reproducible; without it each run draws a fresh seed
                   seed = 1636,
                   bootstrap.nboot = 20)

model <- SEQuential(SEQdata, id.col = "ID",
                             time.col = "time",
                             eligible.col = "eligible",
                             treatment.col = "tx_init",
                             outcome.col = "outcome",
                             time_varying.cols = c("N", "L", "P"),
                             fixed.cols = "sex",
                             method = "ITT",
                             options = options)

end_of_fup(model)
```

`end_of_fup()` returns, per subgroup, two tables: `estimates` gives the weighted
proportion (or mean) in each arm with its bootstrap confidence interval, and an
account of how much of the arm it rests on. `Trial-periods (Eligible)` is every
trial-period that reached the follow-up time, and the next three partition it:
`(Analysed)` contribute to the estimate, `(Censored)` were measured at some point
but not within the window, and `(No measurement)` were never measured at all - so
the three sum back to the eligible total. `% Censored` gives the censored share of
that total. `Subjects` counts the distinct people behind the analysed
trial-periods; one subject contributes several trial-periods and can be analysed
in some and censored in others. `comparison` gives the pairwise between-arm contrast: the difference in
proportions here, or the difference in means for a continuous outcome,
with its standard error and confidence interval. For a binary outcome the ratio
of proportions is reported alongside it, with an interval computed on the log
scale and a `log(Ratio) SE` for inverse-variance pooling. Contrasts are paired by
bootstrap iteration, so their intervals account for the correlation between arms.

## Missing measurements and the time window

Outcomes measured at particular visits are rarely available for everyone at
exactly time `k`. Encode "not measured at this time" as `NA` in the outcome
column - `end_of_fup` is the one mode that accepts missing outcomes, precisely
because missingness is meaningful here. Every other column must still be
complete.

`end_of_fup.window` sets the half-width of a window used when a trial-period has
no measurement at exactly `k`:

```{r}
options <- SEQopts(end_of_fup = TRUE,
                   end_of_fup.time = 12,
                   # accept a measurement anywhere in [9, 15] when there is none at 12
                   end_of_fup.window = 3,
                   bootstrap = TRUE,
                   seed = 1636,
                   bootstrap.nboot = 20)

windowed <- SEQuential(SEQdata, id.col = "ID",
                                time.col = "time",
                                eligible.col = "eligible",
                                treatment.col = "tx_init",
                                outcome.col = "outcome",
                                time_varying.cols = c("N", "L", "P"),
                                fixed.cols = "sex",
                                method = "ITT",
                                options = options)

end_of_fup(windowed)[[1]]$estimates
end_of_fup(windowed)[[1]]$comparison
```

`comparison` is where the treatment effect lives: `Difference` is the difference
in proportions between the two arms, and `Ratio` their ratio. Both directions of
each arm pair are reported, so the row you want is the one whose `A_x` is your
reference arm.

The selection rule, applied to each trial-period independently, is:

1. If there is a measurement at exactly `k`, use it.
2. Otherwise, use the measurement *nearest* to `k` within
   `[k - window, k + window]`. Where two measurements are equally far either
   side of `k`, the *later* one is taken, so that at least `k` of follow-up has
   elapsed.
3. If there is no measurement anywhere in the window, the trial-period is
   *censored* - it contributes nothing to the average.

The weight used is always the weight at the time the chosen measurement was
taken, not the weight at `k`.

A window is not free. Widening it recovers trial-periods that would otherwise be
dropped, but the measurements it recovers are taken further from the time you
actually care about, and the trial-periods it recovers are not a random subset -
a trial-period with no measurement at `k` is often one whose follow-up ended
early. Treat the window as a trade-off between precision and how literally the
estimate answers "the outcome at time `k`", and check how much of the estimate
rests on it using the accounting table below.

## Checking what contributed

`diagnostics()` reports where every trial-period went. `eof.nonunique` counts
trial-periods and `eof.unique` counts distinct subjects:

```{r}
diagnostics(windowed)$eof.nonunique
```

The four categories are mutually exclusive, so the trial-period counts partition
`Eligible`:

- *At k* - contributed, using a measurement at exactly `k`.
- *In window* - contributed, having fallen back to the window.
- *Excluded (outside window)* - measured at some point, but not within the
  window.
- *Excluded (no measurement)* - never measured at any follow-up time. Under
  `method = "censoring"` this also picks up trial-periods artificially censored
  before any measurement was taken.

`At k` plus `In window` is exactly the number of trial-periods behind the
estimate, so the two tables always reconcile. The subject counts in
`eof.unique` need *not* sum to `Eligible`, because one subject can fall into
different categories for different trials.

If `In window` is large relative to `At k`, or `Excluded` dominates, the estimate
is resting on much less - or much more indirect - data than the arm totals alone
suggest.

## Continuous outcomes

Set `end_of_fup.type = "continuous"` for an outcome that is not 0/1. The estimate
becomes a weighted mean, reported in a `Mean` column rather than `Proportion`, and
its confidence interval is not clamped to `[0, 1]`. The between-arm contrast is
the difference in means; no ratio is reported, since a continuous outcome need not
be bounded away from zero.

```{r}
data <- data.table::copy(SEQdata)
set.seed(42)
data[, biomarker := 10 + 2 * as.numeric(as.character(tx_init)) + N + rnorm(.N)]

continuous <- SEQuential(data, id.col = "ID",
                               time.col = "time",
                               eligible.col = "eligible",
                               treatment.col = "tx_init",
                               outcome.col = "biomarker",
                               time_varying.cols = c("N", "L", "P"),
                               fixed.cols = "sex",
                               method = "ITT",
                               options = SEQopts(end_of_fup = TRUE,
                                                 end_of_fup.time = 12,
                                                 end_of_fup.type = "continuous",
                                                 end_of_fup.window = 3,
                                                 bootstrap = TRUE,
                                                 seed = 1636,
                                                 bootstrap.nboot = 20))

end_of_fup(continuous)[[1]]$estimates
end_of_fup(continuous)[[1]]$comparison
```

Here `Difference` is the difference in means, and there is no `Ratio` column.

Note that the usual outcome diagnostic tables count `outcome == 1` rows, which
has no meaning for a continuous outcome, so they are reported as `NA`. In their
place `diagnostics()` reports the N, mean and SD of the raw analysed
measurements per arm in `eof.summary`; the follow-up and end-of-follow-up
tables remain available.

```{r}
diagnostics(continuous)$eof.summary
```

## Per-protocol effects

`end_of_fup` composes with weighting in the usual way, so a per-protocol
end-of-follow-up effect is the censoring method with `weighted = TRUE`:

```{r}
perprotocol <- SEQuential(SEQdata, id.col = "ID",
                                   time.col = "time",
                                   eligible.col = "eligible",
                                   treatment.col = "tx_init",
                                   outcome.col = "outcome",
                                   time_varying.cols = c("N", "L", "P"),
                                   fixed.cols = "sex",
                                   method = "censoring",
                                   options = SEQopts(weighted = TRUE,
                                                     numerator = "sex",
                                                     denominator = "N + L + P + sex",
                                                     end_of_fup = TRUE,
                                                     end_of_fup.time = 12,
                                                     end_of_fup.window = 3,
                                                     bootstrap = TRUE,
                                                     seed = 1636,
                                                     bootstrap.nboot = 20))

end_of_fup(perprotocol)[[1]]$estimates
end_of_fup(perprotocol)[[1]]$comparison
```

Subjects who deviate from their assigned strategy are artificially censored at
the point of deviation, and their outcome is missing from then on. A
trial-period that deviates before `k` therefore has no measurement to
contribute and is excluded rather than carried forward - it appears under
`Excluded (no measurement)` or `Excluded (outside window)` in the accounting
table, depending on what it had measured earlier.
