--- title: "Designing two-stage trials with ordered categorical outcomes" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Designing two-stage trials with ordered categorical outcomes} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(OptOTrials) ``` ## Overview `OptOTrials` constructs optimal one- and two-stage randomised trial designs for ordered categorical outcomes. The whole workflow runs through two functions: * `rule()` builds a decision rule; * `op()` estimates its operating characteristics by simulation. The test statistic and the monitoring scheme are chosen by argument, so you do not need to remember a different function name for each combination. ## Step 1: check the proportional odds assumption The score test (`test = "S"`) is only appropriate when the proportional odds assumption holds. `Proportional_odds_assumption()` reports whether it does and returns the implied common log odds ratio. ```{r} p1 <- c(0.075, 0.182, 0.319, 0.243, 0.015, 0.166) p2 <- p2_fun(p1, log(3.06)) Proportional_odds_assumption(p1, p2) ``` When the assumption fails the function says so and warns, so the condition cannot pass unnoticed in a script: ```{r} q1 <- c(1/3, 1/3, 1/3) q2 <- c(1/2, 1/3, 1/6) res <- tryCatch(Proportional_odds_assumption(q1, q2), warning = function(w) NA) res ``` For that scenario use the Mann-Whitney-Wilcoxon test (`"M"`) or the win odds test (`"W"`) instead. ## Step 2: build a design `p2_fun()` is a helper in this package that constructs the experimental-group category probabilities from the control probabilities and a specified log odds ratio under the proportional odds model, as used above. ```{r} d <- rule(alpha = 0.05, beta = 0.2, p1 = p1, p2 = p2, test = "M", stopping = "F", criterion = 1) d ``` The printed output names every quantity and additionally translates each boundary onto an effect-size scale, so the design can be communicated to non-statistician collaborators: for the score test the implied odds ratio, and for the rank-based tests the implied win odds. Individual components are available by name, so no positional indexing is needed: ```{r} d$n1 d$t1f d$effect ``` ## Step 3: operating characteristics ```{r} o <- op(d, nsim = 2000, seed = 1234) o ``` Both hypotheses are evaluated in one call. Futility and superiority stopping probabilities are reported separately, and Monte Carlo standard errors are attached so that a deviation of the estimated type I error rate from the nominal level can be judged against simulation noise. Supplying `seed` re-seeds immediately before each scenario, so the result does not depend on how much randomness has already been consumed. ## Step 4: comparing criteria `design_table()` sweeps a set of tests and criteria and returns a data frame, re-seeding for every row so each row reproduces on its own. ```{r} design_table(0.05, 0.2, p1, p2, tests = c("S", "M"), criteria = c(1, 3), stopping = "F", nsim = 1000) ``` ## Degenerate optima Criterion 2 minimises the expected sample size under the alternative. This can drive the optimum to a stage-1 size of 1 with an extreme interim boundary: the criterion rewards designs under which continuation to the final analysis is almost certain when the treatment works, so the optimiser commits essentially all information to the final analysis and the design becomes two-stage in name only. `rule()` detects this. By default it warns and substitutes the single-stage design: ```{r} d2 <- rule(0.05, 0.2, p1, p2, test = "M", stopping = "F", criterion = 2) d2$substituted ``` Use `on_degenerate = "none"` to inspect the raw optimum, or `"error"` to make it fail loudly in an automated pipeline. ## Choosing between asymptotic and exact approaches The designs rely on the asymptotic normality of the test statistics. This is adequate at the stage-1 sizes arising in typical applications, but the approximation degrades when the stage-1 size is small or when some outcome categories are rare. In those situations an exact approach is preferable; see the package reference manual for the relevant references. ## Moving from the earlier interface Versions up to 1.0.2 provided a separate function for each combination of test and stopping rule (`ruleF()`, `ruleFS()`, `op.F()`, `op.FS()`, `op.1stage()` and the `Decision_rule_*` family). These are deprecated in this version: each still works and still returns exactly what it always returned, but warns and names its replacement. They will be removed in version 1.1.0. The correspondence is tabulated in `help("OptOTrials-deprecated")`: | Deprecated | Replacement | |---|---| | `ruleF()`, `ruleFS()` | `rule(..., stopping = "F"/"FS")` | | `Decision_rule_{S,M,W}.F` / `.FS` / `_1stage` | `rule(..., test = ..., stopping = ...)` | | `op.F()`, `op.FS()`, `op.1stage()` | `op(design, nsim, seed)` | Two habits change. `rule()` returns a named object, so `res[3]` becomes `res$n1`, and the object is passed straight to `op()` instead of its elements being transcribed. `op()` evaluates both hypotheses in one call, so the old pattern of calling the simulator twice, once with `(p1, p2)` and once with `(p1, p1)`, is no longer needed.