---
title: "Sample size planning"
output: rmarkdown::pdf_document
vignette: >
  %\VignetteIndexEntry{Sample size planning}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

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

## Purpose

`testflow` provides sample-size planning functions for the trial-design
problems that come up most often in practice: continuous, binary, survival,
and ordinal endpoints, each under parallel, paired, or repeated-measures
designs, and each for superiority, non-inferiority, or equivalence
objectives.

Every planning function returns a `sample_size` object with a raw sample
size, a dropout-adjusted sample size, the assumptions used, a report-ready
sentence, and (where available) a plot. The underlying formulas and their
literature references live in the function documentation
(`?sample_size_continuous`, `?sample_size_binary`, `?sample_size_survival`,
`?sample_size_ordinal`) rather than in the returned object, so this vignette
walks through the same formulas alongside runnable examples.

The dispatcher `sample_size()` routes to the endpoint-specific helper based
on `endpoint`, `design`, and `objective`:

```{r, eval = FALSE}
sample_size(
  endpoint = c("continuous", "binary", "survival", "ordinal"),
  design = c("parallel", "paired", "repeated"),
  objective = c("superiority", "noninferiority", "equivalence"),
  ...
)
```

All dropout adjustments use \(n_{recruit} = \lceil n_{eval} / (1 - d) \rceil\),
never \(n_{eval}(1+d)\), since the latter under-adjusts except for very small
dropout proportions.

## Continuous endpoints

`sample_size_continuous()` covers parallel two-arm, paired, and
repeated-measures designs.

### Parallel superiority

With allocation ratio \(r = n_B/n_A\), common variance \(\sigma^2\), and
target difference \(\Delta\):

\[
n_A = \frac{(r+1)\sigma^2(z_{1-\beta}+z_{1-\alpha/2})^2}{r\Delta^2}
\]

```{r}
parallel_ss <- sample_size_continuous(
  design = "parallel",
  objective = "superiority",
  delta = 5,
  sd = 10,
  alpha = 0.05,
  power = 0.90
)
parallel_ss
```

### Paired superiority

Paired designs replace \(\sigma\) with the standard deviation of the paired
differences and drop the allocation ratio, since there is a single sample of
differences:

\[
n_{pairs} = \frac{(z_{1-\alpha/2}+z_{1-\beta})^2\sigma_{diff}^2}{\Delta^2}
\]

```{r}
paired_ss <- sample_size_continuous(
  design = "paired",
  objective = "superiority",
  delta = 5,
  sd_diff = 10,
  alpha = 0.05,
  power = 0.90
)
paired_ss
plot(paired_ss, type = "curve")
```

The curve confirms that the planned sample size reaches the target power:
the dashed target-power line crosses the power curve at the vertical
target-\(n\) marker.

### Repeated measures (3+ time points)

For `design = "repeated"` with `n_time > 2`, the paired-difference standard
deviation is replaced by an effective standard deviation derived from a
compound-symmetry working correlation across the repeated measurements:

\[
\sigma_{eff} = \sigma_{diff}\sqrt{\frac{1+(m-1)\rho}{m}}
\]

where \(m\) is `n_time` and \(\rho\) is the within-subject `correlation`.
This design-effect approach is standard for planning an average
treatment-effect test across repeated, exchangeably correlated measurements;
it is a `testflow` extension beyond the base paired/parallel formulas and is
not itself drawn from a specific closed-form textbook example.

```{r}
repeated_ss <- sample_size_continuous(
  design = "repeated",
  n_time = 4,
  correlation = 0.5,
  objective = "superiority",
  delta = 5,
  sd_diff = 10,
  alpha = 0.05,
  power = 0.90
)
repeated_ss
```

### Non-inferiority and equivalence

Non-inferiority uses a one-sided \(\alpha\) and the distance to the null
boundary \(d_{NI} = \Delta + \delta\) (with \(\delta\) the non-inferiority
margin, passed as `delta`, and \(\Delta\) the expected difference, passed as
`expected_difference`):

\[
n_{per\ group} = \frac{2\sigma^2(z_{1-\beta}+z_{1-\alpha})^2}{d_{NI}^2}
\]

Equivalence uses the distance to the nearer of the two boundaries,
\(d_{EQ} = \delta - |\Delta|\), with the same one-sided form; when the
expected difference is exactly zero, `testflow` switches to the standard
closed-form correction that uses \(z_{1-\beta/2}\) instead of \(z_{1-\beta}\):

```{r}
sample_size_continuous(
  design = "parallel",
  objective = "noninferiority",
  delta = 2,
  expected_difference = 0.5,
  sd = 10,
  alpha = 0.025,
  power = 0.90
)

sample_size_continuous(
  design = "parallel",
  objective = "equivalence",
  delta = 4,
  expected_difference = 0,
  sd = 10,
  alpha = 0.05,
  power = 0.90
)
```

## Binary endpoints

`sample_size_binary()` covers parallel risk-difference planning and paired
(discordant-pairs) planning.

### Parallel superiority

Two variance estimators are available via `method`: `pooled` uses the
null (pooled-proportion) variance, and `anticipated` uses the
alternative-hypothesis variance.

\[
n_A = \frac{\left[z_{1-\alpha/2}\sqrt{(1+1/r)\bar p(1-\bar p)}
  + z_{1-\beta}\sqrt{p_A(1-p_A)+p_B(1-p_B)/r}\right]^2}{(p_A-p_B)^2},
\qquad \bar p = \frac{p_A+rp_B}{1+r}
\]

```{r}
sample_size_binary(
  design = "parallel",
  objective = "superiority",
  p1 = 0.4,
  p2 = 0.25,
  method = "pooled",
  alpha = 0.05,
  power = 0.90
)
```

### Paired (discordant-pairs) superiority

Paired binary designs plan on the discordant cells directly. With discordant
odds ratio \(OR_D = p_{10}/p_{01}\) and discordance rate
\(\lambda_D = p_{10}+p_{01}\):

\[
n_{total} = \left\lceil
\frac{(z_{1-\alpha/2}+z_{1-\beta})^2(OR_D+1)^2}{(OR_D-1)^2\lambda_D}
\right\rceil
\]

```{r}
paired_binary_ss <- sample_size_binary(
  design = "paired",
  objective = "superiority",
  p10 = 0.2,
  p01 = 0.1,
  alpha = 0.05,
  power = 0.90
)
paired_binary_ss
```

### Non-inferiority and equivalence

Both use the same asymmetric variance form as the anticipated-response
superiority estimator, evaluated at the non-inferiority or equivalence
distance. When `p1 == p2` for equivalence planning, the allocation ratio
still enters through an `(r+1)/r` factor, matching the general two-arm
special case rather than assuming equal allocation:

```{r}
sample_size_binary(
  design = "parallel",
  objective = "equivalence",
  p1 = 0.3,
  p2 = 0.3,
  margin = 0.15,
  allocation = 2,
  alpha = 0.05,
  power = 0.90
)
```

## Survival endpoints

`sample_size_survival()` plans on the number of events required, then
converts to a total sample size using the planned survival probabilities.

### Required events

For superiority under an assumed exponential hazard:

\[
D_{total} = \frac{4(z_{1-\alpha/2}+z_{1-\beta})^2}{[\log(HR)]^2}
\]

or, using the proportional-hazards-only approximation (`method = "ph_only"`):

\[
D_{total} = \frac{2(HR+1)^2(z_{1-\alpha/2}+z_{1-\beta})^2}{(HR-1)^2}
\]

### Converting events to a total sample size

Given planned survival probabilities \(S_A(t)\) and \(S_B(t)\) at the
analysis horizon, and equal allocation, the total sample size satisfying an
expected \(D_{total}\) events is:

\[
N_{total} = \frac{2D_{total}}{(1-S_A(t))+(1-S_B(t))}
\]

which follows directly from setting expected events
\(\frac{N}{2}(1-S_A(t)) + \frac{N}{2}(1-S_B(t))\) equal to \(D_{total}\) under
equal-sized arms. If `survival_a`/`survival_b` are omitted, the function
reports required events only and leaves `n` as the event count.

```{r}
survival_ss <- sample_size_survival(
  hr = 0.7,
  survival_a = 0.8,
  survival_b = 0.7,
  alpha = 0.05,
  power = 0.90
)
survival_ss
```

Non-inferiority and equivalence work on the log-hazard-ratio scale, with
distances \(d_{NI} = \log(HR_M) - \log(HR)\) and
\(d_{EQ} = \min(\theta-\theta_L,\ \theta_U-\theta)\) respectively, both used
in \(D_{total} \approx 4(z_{1-\alpha}+z_{1-\beta})^2/d^2\) and converted to a
total sample size the same way.

### Uniform accrual

The flat conversion above assumes every subject is followed for the full
study duration. When enrollment is staggered, pass `accrual_duration`
(enrollment window \(R\)) and `follow_up` (additional follow-up after
accrual ends) together with `survival_a`/`survival_b`; the event
probability for each arm becomes the accrual-averaged

\[
\bar q_g = 1 - \frac{e^{-\lambda_g(T-R)} - e^{-\lambda_g T}}{\lambda_g R},
\qquad T = R + \text{follow\_up},
\]

with \(\lambda_g\) implied by \(S_g(T)\) under an exponential-survival
assumption, in place of the flat \(1-S_g(t)\):

```{r}
accrual_ss <- sample_size_survival(
  hr = 0.7,
  survival_a = 0.8,
  survival_b = 0.7,
  alpha = 0.05,
  power = 0.90,
  accrual_duration = 12,
  follow_up = 24
)
accrual_ss
```

Staggered accrual always requires *more* subjects than instantaneous
accrual for the same number of events (later-enrolled subjects have less
follow-up time), and the accrual-adjusted result reduces to the flat
conversion as `accrual_duration` shrinks toward 0.

## Ordinal endpoints

`sample_size_ordinal()` implements Noether's method for a
Mann-Whitney/Wilcoxon-type comparison of two independent groups. With
\(P = P(A>B) + \tfrac12 P(A=B)\) and equal allocation:

\[
n_{per\ group} = \frac{(z_{1-\alpha/2}+z_{1-\beta})^2}{6(P-0.5)^2}
\]

```{r}
ordinal_ss <- sample_size_ordinal(
  p_superiority = 0.6,
  alpha = 0.05,
  power = 0.90
)
ordinal_ss
```

## Bioequivalence

`sample_size_bioequivalence()` plans a two one-sided test (TOST)
bioequivalence study on the log scale, for a `crossover` or `parallel`
design. With anticipated geometric mean ratio \(GMR\),
\(\theta_0=\log(GMR)\), bounds \(\theta_L=\log(L)\), \(\theta_U=\log(U)\)
(usually 0.80 and 1.25), and \(d_{BE}=\min(\theta_0-\theta_L,\
\theta_U-\theta_0)\):

```{r}
be_ss <- sample_size_bioequivalence(
  design = "crossover",
  gmr = 0.95,
  cv_within = 0.30,
  alpha = 0.05,
  power = 0.90
)
be_ss
```

The default `method = "iterative_tost"` searches for the smallest sample
size achieving the *exact* TOST power, computed via the noncentral t
distribution (verified against `PowerTOST::power.TOST()`, the
regulatory-standard calculation, in package tests) rather than a normal
approximation, since the estimated standard error carries its own sampling
variability. See `?sample_size_bioequivalence` for the full formula. This is
more accurate than the closed-form approximation
\(n_{total} \approx 2\sigma_w^2(z_{1-\beta}+z_{1-\alpha})^2/d_{BE}^2\)
available via `method = "normal_approx"`; the two agree closely near
\(GMR=1\) but can diverge further away from it. `design = "parallel"` uses
`cv_between` in place of `cv_within` and respects the `allocation` ratio.

## Precision-based sample size

`sample_size_precision()` sizes a study to a target confidence-interval
half-width \(w\) instead of power against an effect size — there is no
`power` argument because there is no alternative hypothesis being tested.

```{r}
sample_size_precision(
  endpoint = "continuous",
  design = "one_sample",
  width = 2,
  sd = 10,
  alpha = 0.05
)

sample_size_precision(
  endpoint = "binary",
  design = "two_sample",
  width = 0.08,
  p1 = 0.4,
  p2 = 0.3,
  alpha = 0.05
)
```

`design` is `one_sample`, `two_sample`, or (binary only) `odds_ratio`
(precision on the log-odds-ratio scale); `endpoint = "binary"`,
`design = "one_sample"` also accepts `conservative = TRUE` to plan against
the worst-case \(p=0.5\) instead of an anticipated `p` (only with the
default `method = "wald"`).

### Rare-prevalence planning: Wilson and exact confidence intervals

For `endpoint = "binary"`, `design = "one_sample"`, `method` chooses the
confidence-interval used for planning: `"wald"` (default) is the original
closed-form normal approximation, unchanged; `"wilson"` and `"exact"`
(Clopper-Pearson) instead run a deterministic search for the minimum integer
`n` whose confidence interval has maximum one-sided half-width no greater
than `width`. This matters for rare-prevalence screening studies (e.g.
newborn screening, rare adverse events), where the Wald normal approximation
can be unreliable and where a naive doubling of the closed-form `n` is not
guaranteed to hit the target precision.

```{r}
# Common prevalence: Wald is adequate and remains the default
sample_size_precision(endpoint = "binary", design = "one_sample", width = 0.05, p = 0.30)

# Rare prevalence: Wilson score confidence interval
sample_size_precision(endpoint = "binary", design = "one_sample", width = 0.02, p = 0.01, method = "wilson")

# Rare prevalence: exact Clopper-Pearson, with the precision requirement
# extended over a prevalence-local band of event counts (criterion =
# "worst_case") rather than just the single anticipated count
sample_size_precision(
  endpoint = "binary", design = "one_sample", width = 0.005,
  p = 0.001, method = "exact", criterion = "worst_case"
)
```

`criterion = "expected"` (the default for `wilson`/`exact`) checks precision
at the single anticipated event count `round(n * p)`; `criterion =
"worst_case"` additionally requires the target to hold across nearby
plausible event counts, which is more conservative and typically requires a
somewhat larger `n`.

A confidence-interval width target is not the same planning objective as
"observe at least one case": a narrow interval can still be planned around a
study that, by chance, observes zero events. `sample_size_precision()`
reports this explicitly via `x$diagnostics` for one-sample binary precision
planning:

```{r}
x <- sample_size_precision(
  endpoint = "binary", design = "one_sample", width = 0.02,
  p = 0.05, method = "wilson", dropout = 0.10
)
x$diagnostics$complete_case_n       # n required by the precision criterion alone
x$diagnostics$adjusted_n            # complete_case_n inflated for dropout
x$diagnostics$expected_events       # anticipated events at adjusted_n
x$diagnostics$probability_zero_events
x$diagnostics$achieved_maximum_half_width
```

`complete_case_n` is the statistical answer to "how many complete
observations achieve the requested precision"; `adjusted_n` is the
recruitment target after inflating for `dropout`. Achieved-precision
diagnostics (`achieved_lower`, `achieved_upper`,
`achieved_maximum_half_width`) are evaluated at `complete_case_n`, since
dropout does not change the precision of the completed-case analysis.
`expected_events` and `probability_zero_events` are evaluated at
`adjusted_n`, the actual number of participants recruited. When either the
expected event count or expected non-event count falls below
`min_expected_events` (default `5`), or when `method = "wald"` is used,
`x$diagnostics$approximation_warning` explains why the reported precision
may be optimistic, and the same message is raised as a single `warning()`.

### Unequal allocation for two proportions

Binary `design = "two_sample"` precision planning now implements unequal
allocation directly, rather than silently defaulting to the equal-allocation
formula whenever `allocation != 1`:

```{r}
sample_size_precision(
  endpoint = "binary", design = "two_sample", width = 0.08,
  p1 = 0.4, p2 = 0.3, allocation = 2
)
```

With \(r = n_B/n_A\) and \(\mathrm{Var}(\hat p_1-\hat p_2) =
p_1(1-p_1)/n_A + p_2(1-p_2)/n_B\):
\[
n_A = \frac{z_{1-\alpha/2}^2}{w^2}\left[p_1(1-p_1) + \frac{p_2(1-p_2)}{r}\right],
\qquad n_B = r\,n_A
\]
which reduces exactly to the previous equal-allocation formula when \(r=1\).

## Cluster-randomized adjustment

`sample_size_cluster_adjust()` inflates an individually randomized sample
size for cluster randomization by the design effect
\(DE = 1+(m-1)\rho\) (or, for unevenly sized clusters with coefficient of
variation \(CV_m\), \(DE \approx 1+((1+CV_m^2)m-1)\rho\)):

```{r}
parallel_ss <- sample_size_continuous(
  design = "parallel", objective = "superiority",
  delta = 5, sd = 10, alpha = 0.05, power = 0.90
)
sample_size_cluster_adjust(unname(parallel_ss$n_adjusted["A"]), m = 20, rho = 0.02)
```

Unlike the other `sample_size_*()` functions, this one returns a plain
integer rather than a `sample_size` object, since it is a post-processing
adjustment applied to an already-computed sample size rather than a full
planning calculation in its own right — the same shape as
`sample_size_adjust_dropout()`.

## Dropout adjustment and reporting

`sample_size_adjust_dropout()` exposes the dropout adjustment directly for
sample sizes computed outside `testflow`:

```{r}
sample_size_adjust_dropout(100, dropout = 0.15)
```

Every result carries a report-ready sentence and converts to a tidy
one-row summary:

```{r}
report(paired_ss)
as_tibble(paired_ss)
```

## Summary of supported settings

| Endpoint | Supported design(s) | Supported objective(s) | Formula family |
|---|---|---|---|
| Continuous | parallel, paired, repeated (2+ time points) | superiority, non-inferiority, equivalence | normal-approximation formulas for the mean difference, plus a compound-symmetry repeated-measures extension |
| Binary | parallel, paired, repeated (2 time points) | superiority, non-inferiority, equivalence | risk-difference planning and discordant-pair planning |
| Survival | parallel | superiority, non-inferiority, equivalence | event-based proportional-hazards planning, optional uniform-accrual adjustment |
| Ordinal | parallel | superiority | Noether approximation |
| Bioequivalence | crossover, parallel | equivalence (TOST) | iterative TOST or normal approximation, log scale |
| Precision | one-sample, two-sample, log-OR | CI half-width (no power/objective) | continuous and binary CI-width formulas |

`sample_size_cluster_adjust()` and `sample_size_adjust_dropout()` are
post-processing helpers that adjust an already-computed sample size rather
than planning one from scratch.

## Reference

Julious SA. *Sample Sizes for Clinical Trials*. Chapman & Hall/CRC; 2010.

Phillips KF. Power of the two one-sided tests procedure in bioequivalence.
*Journal of Pharmacokinetics and Biopharmaceutics*. 1990;18(2):137-144.

Donner A, Klar N. *Design and Analysis of Cluster Randomization Trials in
Health Research*. Arnold; 2000.
