Working with the pipe

Designs built step by step

The native pipe |> of R (version 4.1.0 or later) passes the result of one function to the first argument of the next. In BayesianQDM, qdm_endpoint() starts a design specification for an endpoint, and each of the other steps sets some arguments of the design_*() function of the endpoint:

Step Arguments
qdm_sample_size() n_t, n_c
qdm_prior() prior_t, prior_c
qdm_criterion() criterion
qdm_external() external_t, external_c
qdm_control() hypothetical_c, fixed_c
qdm_tte() milestone, effect, conduct, conduct_future (time-to-event endpoint)
qdm_computation() method, nMC, enumerate

A specification can be printed at any step. Until the required arguments are set, it lists those still needed:

spec <- qdm_endpoint("binary") |>
  qdm_sample_size(n_t = 12, n_c = 12)
spec
#> Design specification: one binary endpoint (incomplete)
#>   Arguments set: n_t, n_c
#>   Still needed: the prior distributions (qdm_prior()), the decision criterion (qdm_criterion())

Once the sample sizes, the prior distribution of the treatment group and the decision criterion are set, it describes the design:

spec <- spec |>
  qdm_prior(prior_t = prior_beta(0.5, 0.5),
            prior_c = prior_beta(0.5, 0.5)) |>
  qdm_criterion(criterion_posterior(theta_tv = 0.2, theta_mav = 0.1,
                                    gamma_go = 0.5, gamma_nogo = 0.8))
spec
#> Design specification (complete):
#> Design: one binary endpoint
#>   Treatment: n = 12, prior Beta(a = 0.5, b = 0.5)
#>   Control: concurrent, n = 12, prior Beta(a = 0.5, b = 0.5)
#> Posterior probability criterion
#>   Go:   P(theta > 0.2 | data) >= 0.5
#>   NoGo: P(theta <= 0.1 | data) >= 0.8

The steps only store their arguments. qdm_build() creates the design by calling design_binary(), which checks the arguments, so the design is the same as when design_binary() is called directly:

des <- design_binary(
  n_t = 12, n_c = 12,
  prior_t = prior_beta(0.5, 0.5), prior_c = prior_beta(0.5, 0.5),
  criterion = criterion_posterior(theta_tv = 0.2, theta_mav = 0.1,
                                  gamma_go = 0.5, gamma_nogo = 0.8)
)
identical(qdm_build(spec), des)
#> [1] TRUE

An invalid argument gives the error of design_binary() when the design is built:

try(spec |> qdm_sample_size(n_t = 0, n_c = 12) |> qdm_build())
#> Error : 'n_t' must be at least 1.

From the design to the results

qdm_decide(), qdm_oc(), qdm_calibrate() and qdm_boundary() accept a specification in place of a design and build it, and their results can be passed on to plot(). The decision for 9 responders of 12 subjects in the treatment group and 4 of 12 in the control group is

spec |>
  qdm_decide(data_t = data_binary(12, 9), data_c = data_binary(12, 4))
#> Decision: Go
#>   Go:   P(theta > 0.2 | data) = 0.8517 (threshold 0.5, met)
#>   NoGo: P(theta <= 0.1 | data) = 0.05935 (threshold 0.8, not met)
#> Posterior distributions
#>   Treatment: Beta(a = 9.5, b = 3.5)
#>   Control:   Beta(a = 4.5, b = 8.5)

The operating characteristics for true response rates of the treatment group from 0.3 to 0.7, with a control response rate of 0.3, are

scen <- data.frame(pi_t = seq(0.3, 0.7, by = 0.05), pi_c = 0.3)
spec |>
  qdm_oc(scen) |>
  plot()

The thresholds can be calibrated so that the probability of meeting the Go criterion is at most 0.05 when the treatment has no effect, and that of meeting the NoGo criterion is at most 0.2 when the effect is 0.3:

cal <- spec |>
  qdm_calibrate(scenario_go = data.frame(pi_t = 0.3, pi_c = 0.3),
                scenario_nogo = data.frame(pi_t = 0.6, pi_c = 0.3),
                target_go = 0.05, target_nogo = 0.2)
cal
#> Calibration of the decision thresholds (exact enumeration of the outcomes)
#>   gamma_go   = 0.74: P(Go criterion met | scenario_go) = 0.03587 (target 0.05)
#>   gamma_nogo = 0.39: P(NoGo criterion met | scenario_nogo) = 0.182 (target 0.2)
#> Decision probabilities with the chosen thresholds
#>       scenario pi_t pi_c      Go   NoGo   Gray Miss
#>    scenario_go  0.3  0.3 0.03587 0.7486 0.2155    0
#>  scenario_nogo  0.6  0.3 0.36483 0.1820 0.4531    0

The calibrated thresholds are \(\gamma_{go} =\) 0.74 and \(\gamma_{nogo} =\) 0.39, and cal$design is the design with these thresholds, which can start another pipe:

cal$design |>
  qdm_decide(data_t = data_binary(12, 9), data_c = data_binary(12, 4))
#> Decision: Go
#>   Go:   P(theta > 0.2 | data) = 0.8517 (threshold 0.74, met)
#>   NoGo: P(theta <= 0.1 | data) = 0.05935 (threshold 0.39, not met)
#> Posterior distributions
#>   Treatment: Beta(a = 9.5, b = 3.5)
#>   Control:   Beta(a = 4.5, b = 8.5)

The observed data that lead to each decision are

spec |>
  qdm_boundary() |>
  plot()

Changing one step

A step given again replaces the arguments that it set before, so a specification can be the start of several pipes that change one of its parts. For example, the probabilities of a Go decision with no effect (\(\pi_t = \pi_c = 0.2\)) and with an effect of 0.3 (\(\pi_t = 0.5\)), for sample sizes from 12 to 30 per group, are

scen2 <- data.frame(pi_t = c(0.2, 0.5), pi_c = 0.2)
n <- c(12, 18, 24, 30)
go <- vapply(n, function(k) {
  oc <- spec |>
    qdm_sample_size(n_t = k, n_c = k) |>
    qdm_oc(scen2)
  oc$Go
}, numeric(2))
data.frame(n = n, no_effect = go[1, ], effect = go[2, ])
#>    n  no_effect    effect
#> 1 12 0.09840322 0.6947283
#> 2 18 0.07088178 0.7602799
#> 3 24 0.03782211 0.8044900
#> 4 30 0.01759145 0.7638057

With 30 subjects per group, the probability of a Go decision is 0.76 with the effect of 0.3 and 0.018 with no effect, against 0.69 and 0.098 with 12 subjects per group.

External control data borrowed through a power prior:

spec |>
  qdm_external(external_c = borrow(data_binary(n = 40, y = 8), a0 = 0.5)) |>
  qdm_oc(scen2)
#> Operating characteristics (exact enumeration of the outcomes)
#>  pi_t pi_c      Go    NoGo   Gray Miss
#>   0.2  0.2 0.05791 0.49764 0.4444    0
#>   0.5  0.2 0.72467 0.01883 0.2565    0

A criterion based on the posterior predictive probability of the effect that will be observed in a future study with 50 subjects per group:

spec |>
  qdm_criterion(criterion_predictive(theta_null = 0.1, m_t = 50, m_c = 50,
                                     gamma_go = 0.8, gamma_nogo = 0.8)) |>
  qdm_decide(data_t = data_binary(12, 9), data_c = data_binary(12, 4))
#> Decision: Go
#>   Go:   P(future theta > 0.1 | data) = 0.9123 (threshold 0.8, met)
#>   NoGo: P(future theta <= 0.1 | data) = 0.08771 (threshold 0.8, not met)
#> Posterior distributions
#>   Treatment: Beta(a = 9.5, b = 3.5)
#>   Control:   Beta(a = 4.5, b = 8.5)

A single-arm study of 20 subjects compared with a fixed control response rate of 0.2, for which the control group has no prior distribution:

spec |>
  qdm_sample_size(n_t = 20, n_c = 0) |>
  qdm_prior(prior_t = prior_beta(0.5, 0.5)) |>
  qdm_control(fixed_c = 0.2) |>
  qdm_decide(data_t = data_binary(20, 9))
#> Decision: Go
#>   Go:   P(theta > 0.2 | data) = 0.6798 (threshold 0.5, met)
#>   NoGo: P(theta <= 0.1 | data) = 0.07527 (threshold 0.8, not met)
#> Posterior distributions
#>   Treatment: Beta(a = 9.5, b = 11.5)
#>   Control:   fixed value 0.2

Other endpoints

The steps are the same for every endpoint. qdm_computation() sets the method of computation of the probabilities, for example the moment-matching approximation for one continuous endpoint:

qdm_endpoint("continuous") |>
  qdm_sample_size(n_t = 15, n_c = 15) |>
  qdm_prior(prior_t = prior_vague(), prior_c = prior_vague()) |>
  qdm_criterion(criterion_posterior(theta_tv = 30, theta_mav = 15,
                                    gamma_go = 0.6, gamma_nogo = 0.7)) |>
  qdm_computation(method = "MM") |>
  qdm_oc(data.frame(mu_t = c(0, 20, 40), sd_t = 25, mu_c = 0, sd_c = 25),
         nsim = 2000, seed = 1)
#> Operating characteristics (simulation of 2000 studies; probabilities by moment-matching approximation)
#>  mu_t sd_t mu_c sd_c    Go   NoGo   Gray Miss
#>     0   25    0   25 0.000 0.8545 0.1455    0
#>    20   25    0   25 0.098 0.1450 0.7570    0
#>    40   25    0   25 0.789 0.0000 0.2110    0

qdm_tte() sets the arguments specific to a time-to-event endpoint, here the hazard ratio as the effect measure, for which smaller effects are favorable:

qdm_endpoint("tte") |>
  qdm_sample_size(n_t = 30, n_c = 30) |>
  qdm_prior(prior_t = prior_gamma(0.1, 1), prior_c = prior_gamma(0.1, 1)) |>
  qdm_criterion(criterion_posterior(theta_tv = 0.67, theta_mav = 0.8,
                                    gamma_go = 0.5, gamma_nogo = 0.6,
                                    direction = "less")) |>
  qdm_tte(effect = "hazard_ratio") |>
  qdm_decide(data_t = data_tte(events = 12, exposure = 300),
             data_c = data_tte(events = 20, exposure = 260))
#> Decision: Go
#>   Go:   P(theta < 0.67 | data) = 0.7633 (threshold 0.5, met)
#>   NoGo: P(theta >= 0.8 | data) = 0.113 (threshold 0.6, not met)
#> Posterior distributions
#>   Treatment: hazard ~ Gamma(shape = 12.1, rate = 301)
#>   Control:   hazard ~ Gamma(shape = 20.1, rate = 261)

Reports for a study protocol

show_report() can end a pipe. It writes the design, and the result passed to it, as a draft of the corresponding sections of a study protocol: the study design, the statistical model, the decision criteria with their formulas, the computation of the probabilities and the result. The report is an HTML file (file_type = "html", the default), whose formulas are displayed by MathJax, or a plain text file in Markdown format with the formulas in LaTeX (file_type = "text"). It is written in a temporary file unless file is given, and it is shown when R is used interactively (open). Here the text report of the operating characteristics is printed:

spec |>
  qdm_oc(scen2) |>
  show_report(file_type = "text", open = TRUE)
#> # Decision criteria and operating characteristics
#> 
#> ## Study design
#> 
#> The proof-of-concept study has one binary endpoint and will enroll 12 subjects in the treatment group and 12 subjects in the control group.
#> 
#> ## Statistical model
#> 
#> The number of responders among the $n_k$ subjects of group $k$ ($k = t, c$ for the treatment and control groups) follows a binomial distribution with response rate $\pi_k$, and the treatment effect is the difference in response rates, $\theta = \pi_t - \pi_c$.
#> 
#> The prior distribution of the treatment group is $\pi_t \sim \textrm{Beta}(0.5, 0.5)$. The prior distribution of the control group is $\pi_c \sim \textrm{Beta}(0.5, 0.5)$. The posterior distributions have closed forms.
#> 
#> ## Decision criteria
#> 
#> The decision will be based on the posterior probability of the treatment effect $\theta$ given the data $D$ of the study, with the target value $\theta_{TV} = 0.2$ and the minimum acceptable value $\theta_{MAV} = 0.1$.
#> 
#> - Go criterion: $\Pr(\theta > 0.2 \mid D) \ge 0.5$
#> - NoGo criterion: $\Pr(\theta \le 0.1 \mid D) \ge 0.8$
#> 
#> The decision will be Go if only the Go criterion is met, NoGo if only the NoGo criterion is met, and Gray if neither is met; if both are met, the decision is called Miss. Because $\gamma_{go} + \gamma_{nogo} > 1$, the two criteria cannot both be met.
#> 
#> ## Computation of the probabilities
#> 
#> The probabilities of the decision criteria are computed by numerical integration.
#> 
#> ## Operating characteristics
#> 
#> The probabilities of Go, NoGo, Gray and Miss decisions under scenarios for the true parameters (exact enumeration of the outcomes) are as follows.
#> 
#> | $\pi_t$ | $\pi_c$ | Go | NoGo | Gray | Miss |
#> |---|---|---|---|---|---|
#> | 0.2 | 0.2 | 0.098 | 0.445 | 0.456 | 0.000 |
#> | 0.5 | 0.2 | 0.695 | 0.035 | 0.271 | 0.000 |
#> 
#> $\pi_t$ and $\pi_c$ are the true response rates of the treatment and control groups.
#> 
#> Report written by BayesianQDM 1.1.0.

The HTML report is written and opened in the same way:

spec |>
  qdm_oc(scen2) |>
  show_report(file = "decision_criteria.html")

The report gives the parts that depend on the design and the results. The parts that depend on the study, such as the name of the endpoint and the reasons for the target value and the minimum acceptable value, are to be added in the protocol.