# End-of-Follow-up Outcomes

    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).

    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`:

    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:

    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.

    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.

    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`:

    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.