BinaryPowerBSSR() answers a design question. Given a
re-estimation rule, what is the power and the expected sample size
across the scenarios that might occur? It averages over every possible
interim outcome, and it is used before the trial starts.
BinaryBSSR() answers an operational question. The
interim analysis has produced one specific number, so how many more
patients must be enrolled? It is used once, while the trial is
running.
This vignette follows a single trial through both.
A trial compares an experimental treatment against control with a binary response endpoint. The clinical team assumes a response probability of 0.45 under treatment and 0.20 under control, so a risk difference of 0.25. Randomization is one to one, the one-sided level is 0.025 and the target power is 0.80. The Z-pooled exact unconditional test is chosen.
Delta.A <- 0.25
plan <- BinarySampleSize(p1 = 0.45, p2 = 0.20, r = 1, alpha = 0.025,
tar.power = 0.8, Test = 'Z-pool')
plan
#> Sample size for a two-arm trial with a binary endpoint
#>
#> Test : Z-pool
#> Alternative : greater
#> Response rates : p1 = 0.45, p2 = 0.2
#> Allocation ratio : 1 to 1
#> Alpha : 0.025
#> Target power : 0.8
#>
#> Required sample size: N1 = 54, N2 = 54, total N = 108
#> Attained power : 0.8038The assumed pooled response probability is 0.325. Its value drives the required sample size much more strongly than the risk difference alone, which is what makes it worth re-estimating.
pooled <- seq(0.20, 0.45, by = 0.05)
sens <- data.frame(
pooled = pooled,
p1 = pooled + Delta.A / 2,
p2 = pooled - Delta.A / 2
)
sens$N <- vapply(seq_len(nrow(sens)), function(i) {
BinarySampleSize(sens$p1[i], sens$p2[i], 1, 0.025, 0.8, 'Chisq')$N
}, numeric(1))
sens
#> pooled p1 p2 N
#> 1 0.20 0.325 0.075 74
#> 2 0.25 0.375 0.125 88
#> 3 0.30 0.425 0.175 102
#> 4 0.35 0.475 0.225 110
#> 5 0.40 0.525 0.275 120
#> 6 0.45 0.575 0.325 120The required total sample size rises by about 60 percent across this range, while the risk difference stays fixed at 0.25. The reason is that the variance of a binary endpoint is largest at one half, so a pooled probability closer to that value costs patients. An error in the assumed pooled probability is therefore expensive, and it is exactly the quantity that blinded interim data can estimate.
Before committing to the design, check what the re-estimation buys. The interim analysis is placed at half of the planned sample size.
design <- BinaryPowerBSSR(
p = seq(0.20, 0.45, by = 0.05),
Delta.A = Delta.A, Delta.T = Delta.A,
N1 = plan$N1, N2 = plan$N2, omega = 0.5, r = 1,
alpha = 0.025, tar.power = 0.8, Test = 'Chisq',
restricted = TRUE
)
design
#> Blinded sample size re-estimation for a binary endpoint
#>
#> Test : Chisq
#> Alternative : greater
#> Design rule : restricted
#> Initial size : N1 = 54, N2 = 54
#> Interim fraction: 0.5, giving n1 = 27 and n2 = 27
#> Treatment effect: assumed 0.25, true 0.25
#> Alpha : 0.025, target power 0.8
#>
#> p p1 p2 power.BSSR power.TRAD E.N
#> 0.20 0.325 0.075 0.9284 0.9283 108.0
#> 0.25 0.375 0.125 0.8710 0.8703 108.3
#> 0.30 0.425 0.175 0.8263 0.8222 109.6
#> 0.35 0.475 0.225 0.8058 0.7937 112.5
#> 0.40 0.525 0.275 0.7975 0.7630 116.0
#> 0.45 0.575 0.325 0.7971 0.7361 118.5The fixed-sample design was sized at a pooled probability of 0.325.
It keeps its power at and below that value and loses it above, falling
further behind as the pooled probability approaches one half. Under the
restricted rule the re-estimation never goes below the planned sample
size, so it leaves the power alone in the over-powered scenarios and
adds patients in the under-powered ones. The E.N column
shows what that costs.
Enrolment reaches the interim milestone. The unblinded statistician is not involved. The only number extracted from the database is the total count of responders across both groups.
n1 <- ceiling(plan$N1 / 2)
n2 <- ceiling(plan$N2 / 2)
S <- round(0.40 * (n1 + n2))
data.frame(n1 = n1, n2 = n2, S = S, pooled.rate = round(S / (n1 + n2), 3))
#> n1 n2 S pooled.rate
#> 1 27 27 22 0.407The observed pooled rate is above the assumed 0.325. By the argument of the previous section a higher pooled rate at the same risk difference calls for more patients, so the trial has to grow.
re <- BinaryBSSR(
n1 = n1, n2 = n2, S = S,
Delta.A = Delta.A, r = 1,
alpha = 0.025, tar.power = 0.8, Test = 'Z-pool',
restricted = TRUE, N1 = plan$N1, N2 = plan$N2
)
re
#> Blinded sample size re-estimation from interim data
#>
#> Test : Z-pool
#> Alternative : greater
#> Design rule : restricted
#> Assumed effect : 0.25
#> Alpha : 0.025, target power 0.8
#>
#> Interim data
#> Patients : n1 = 27, n2 = 27, total n = 54
#> Responders : S = 22, blinded pooled rate = 0.4074
#> Recovered rates : hat.p1 = 0.5324, hat.p2 = 0.2824
#>
#> Re-estimation
#> Required total : N1 = 60, N2 = 60, total N = 120
#> Still to enrol : group 1 = 33, group 2 = 33, total = 66
#> Final size : N1 = 60, N2 = 60, total N = 120
#> Power at final N : 0.8029The output separates three quantities that are easy to confuse.
N1.re and N2.re are the total sample sizes the
re-estimation calls for. n1.stage2 and
n2.stage2 are the additional patients still to enrol, which
is what the enrolment plan needs. N1.final and
N2.final are what the trial will end up with, and they can
exceed the re-estimated totals when the restricted rule applies or when
the interim sample size already exceeds the requirement.
Running the same call over the possible values of \(S\) shows the shape of the rule.
S.grid <- round(seq(0.15, 0.55, by = 0.05) * (n1 + n2))
decision <- data.frame(S = S.grid, pooled = round(S.grid / (n1 + n2), 3))
decision$N.restricted <- vapply(S.grid, function(s) {
BinaryBSSR(n1, n2, s, Delta.A, 1, 0.025, 0.8, 'Chisq',
restricted = TRUE, N1 = plan$N1, N2 = plan$N2)$N.final
}, numeric(1))
decision$N.unrestricted <- vapply(S.grid, function(s) {
BinaryBSSR(n1, n2, s, Delta.A, 1, 0.025, 0.8, 'Chisq')$N.final
}, numeric(1))
decision
#> S pooled N.restricted N.unrestricted
#> 1 8 0.148 108 54
#> 2 11 0.204 108 76
#> 3 14 0.259 108 94
#> 4 16 0.296 108 102
#> 5 19 0.352 110 110
#> 6 22 0.407 120 120
#> 7 24 0.444 120 120
#> 8 27 0.500 120 120
#> 9 30 0.556 120 120The restricted curve is flat at the planned total until the interim data are unfavourable enough to trigger an increase. The unrestricted curve dips below the plan when the interim data are favourable. The two curves meet once the required sample size exceeds the plan.
When the blinded pooled rate is extreme, the recovered group probabilities can fall outside the unit interval and are truncated.
edge <- BinaryBSSR(n1 = 20, n2 = 20, S = 2, Delta.A = 0.4, r = 1,
alpha = 0.025, tar.power = 0.8, Test = 'Chisq')
data.frame(hat.p = edge$hat.p, hat.p1 = edge$hat.p1,
hat.p2 = edge$hat.p2, N.re = edge$N.re)
#> hat.p hat.p1 hat.p2 N.re
#> 1 0.05 0.25 0 42With a pooled rate of 0.05 and an assumed effect of 0.4, the control probability would be negative, so it is set to zero while the treatment probability stays at \(\hat{p} + \Delta_{A} / 2\). The recovered risk difference is then only 0.25 rather than 0.4, and the re-estimated sample size is correspondingly larger. An assumed effect that the observed pooled rate cannot accommodate therefore produces conservative re-estimation, which is worth checking when the response rate is low.
With an allocation ratio of \(r\) to 1, the recovered probabilities are weighted accordingly and the second-stage enrolment follows the same ratio.
BinaryBSSR(n1 = 40, n2 = 20, S = 18, Delta.A = 0.25, r = 2,
alpha = 0.025, tar.power = 0.8, Test = 'Chisq')
#> Blinded sample size re-estimation from interim data
#>
#> Test : Chisq
#> Alternative : greater
#> Design rule : unrestricted
#> Assumed effect : 0.25
#> Alpha : 0.025, target power 0.8
#>
#> Interim data
#> Patients : n1 = 40, n2 = 20, total n = 60
#> Responders : S = 18, blinded pooled rate = 0.3
#> Recovered rates : hat.p1 = 0.3833, hat.p2 = 0.1333
#>
#> Re-estimation
#> Required total : N1 = 70, N2 = 35, total N = 105
#> Still to enrol : group 1 = 30, group 2 = 15, total = 45
#> Final size : N1 = 70, N2 = 35, total N = 105
#> Power at final N : 0.802The interim total of responders is the only quantity that leaves the database, so the analysis stays blinded and no alpha is spent. The re-estimated sample size still depends on \(\Delta_{A}\), which is fixed at the design stage and must not be revised in the light of the interim data.
Fix the timing of the interim analysis, the value of \(\Delta_{A}\), the test and the choice
between the restricted and unrestricted rules in the protocol before the
trial starts. Report the operating characteristics from
BinaryPowerBSSR() alongside them, since the type I error
rate of the procedure is a property of the whole rule rather than of the
final test alone.
Setting Delta.T to zero makes the two groups identical,
so power.BSSR and power.TRAD become rejection
probabilities under the null hypothesis. The function evaluates them
only at the values passed through p and does not search
over the unit interval, so the largest type I error rate has to be read
off a grid.
null <- BinaryPowerBSSR(
p = seq(0.02, 0.98, by = 0.02),
Delta.A = Delta.A, Delta.T = 0,
N1 = plan$N1, N2 = plan$N2, omega = 0.5, r = 1,
alpha = 0.025, tar.power = 0.8, Test = 'Chisq', restricted = TRUE
)
data.frame(
largest.type1.BSSR = round(max(null$power.BSSR), 5),
attained.at.p = null$p[which.max(null$power.BSSR)],
largest.type1.fixed = round(max(null$power.TRAD), 5)
)
#> largest.type1.BSSR attained.at.p largest.type1.fixed
#> 1 0.02713 0.5 0.02592The rejection region and the re-estimated sample size are shared
across the elements of p, so a grid of this size costs
little more than a handful of scenarios.
Both figures sit above the nominal 0.025, for two separate reasons.
The chi-squared test is not exact, which is why the fixed-sample design
alone already overshoots, and the re-estimation then pushes the maximum
higher still. Substituting an exact test such as Boschloo
removes the first source but not the second, because the type I error
rate of an adaptive rule is not inherited from the test it applies at
the end. Running this check is therefore worthwhile whichever test is
chosen.