Helmut
★★★
avatar
Homepage
Vienna, Austria,
2025-08-22 12:28
(304 d 07:40 ago)

Posting: # 24434
Views: 4,198
 

 Bug in function power.TOST() [🇷 for BE/BA]

Dear all,

[image]THX to ElMaestro for detecting a bug in power.TOST(). Say, you dosed N subjects and are interested in post hoc power of a fraction f of this sample size (e.g., of eligible subjects). Calling power.TOST(..., n = f * N) might give a lower than the correct power.
Example for CV = 0.3, N = 90, f = 0.7 (using all the defaults of a 2×2 design):

power.TOST(CV = 0.3, n = 63)              # eligible subjects
Unbalanced design. n(i)=32/31 assumed.
[1] 0.9453907
                            # correct (given the assumption)

power.TOST(CV = 0.3, n = c(32, 31))       # explicit subjects / sequence
[1] 0.9453907                             # correct

power.TOST(CV = 0.3, n = 0.7 * 90)        # eligible subjects calculated
[1] 0.9424189                             # wrong (too low)

power.TOST(CV = 0.3, n = round(0.7 * 90)) # quick fix
Unbalanced design. n(i)=32/31 assumed.
[1] 0.9453907
                            # correct (given the assumption)


Why is that so? in the vectorization of the total sample size (to get subjects / sequence), trunc(n) is used. That is correct for n = 63 but not for n = 0.7 * 90.

trunc(63); trunc(0.7 * 90)
[1] 63
[1] 62

Therefore, the degrees of freedom are only 60 instead of 61 and, consequently, the post hoc power is lower. The third example should rise an eyebrow because we don’t get a message about an unbalanced design. The quick fix works:

63; round(0.7 * 90)
[1] 63
[1] 63

We can look behind the scenes of the internal function nvec(n, grps) of PowerTOST and the planned new function:

new.fun <- function(n, grps) {
  n.int     <- as.integer(round(n))
  quotient  <- n.int %/% grps
  remainder <- n.int %% grps
  nv        <- c(rep.int(quotient + 1L, remainder),
                 rep.int(quotient, grps - remainder))
  return(as.integer(nv))
}
res <- data.frame(Approach =c("Explicit sequences", "Total sample size",
                              "f * N", "Workaround", "New function"),
                  N = c(NA, 63, 0.7 * 90, round(0.7 * 90), 0.7 * 90),
                  n1 = c(32L, rep(NA_integer_, 4)),
                  n2 = c(31L, rep(NA_integer_, 4)),
                  n = c(32L + 31L, rep(NA_integer_, 4)), correct = TRUE,
                  df = c(63L - 2L, rep(NA_integer_, 4)))
for (j in 2:nrow(res)) {
  if (j < nrow(res)) {
    res[j, 3:4] <- PowerTOST:::nvec(res$N[j], 2)
  } else {
    res[j, 3:4] <- new.fun(res$N[j], 2)
  }
  res[j, 5]   <- sum(res[j, 3:4])
  res$df[j]   <- res[j, 5] - 2
  if (!res$df[j] == res$df[1]) res$correct[j] <- FALSE
}
print(res, row.names = FALSE, right = FALSE)

 Approach           N  n1 n2 n  correct df
 Explicit sequences NA 32 31 63  TRUE   61
 Total sample size  63 32 31 63  TRUE   61
 f * N              63 31 31 62 FALSE   60
 Workaround         63 32 31 63  TRUE   61
 New function       63 32 31 63  TRUE   61


Not for the first time we are a victim of floating-point arithmetic.

print(63); print(0.7 * 90)               # by default 7 significant digits are shown
[1] 63
[1] 63                                   # they look the same
63 == 0.7 * 90; identical(63, 0.7 * 90)  # are they the same?
[1] FALSE
[1] FALSE
                                # obviously not
print(63, digits = 16); print(0.7 * 90, digits = 16); print(round(0.7 * 90), digits = 16)
[1] 63
[1] 62.99999999999999
                    # gotcha!
[1] 63                                   # the workaround
63 == round(0.7 * 90); identical(63, round(0.7 * 90))
[1] TRUE
[1] TRUE


For details see the issue at GitHub. Until it is resolved and the library updated, please use power.TOST(n = round(f * N)).
[image]However, best is to give the subjects / sequence explicitly. The functions of PowerTOST don’t have a crystal ball and can only guess while keeping the sequences as balanced as possible. Only you know the actual values. If the sample size is divisible by the number of sequence, power.TOST() assumes balanced sequences. Quoting the note in the man-page of power.TOST():

If n is given as scalar (total sample size) and this number is not divisible by the number of (sequence) groups of the design an unbalanced design with small imbalance is assumed. A corresponding message is thrown showing the assumed numbers of subjects in (sequence) groups.

This may, of course, be incorrect.

n    <- 40
nseq <- 2
n %% nseq     # modulo
[1] 0         # remainder zero, power.TOST() assumes balanced sequences
power.TOST(CV = 0.3, n = n)
[1] 0.8158453 # in good faith
power.TOST(CV = 0.3, n = c(24, 16))
[1] 0.8002523 # actually quite imbalanced, lower power



The internal function nvec(n, grps) of PowerTOST is called from its following functions:

CVfromCI(), power.HVNTID(), power.NTID(), power.RatioF(), power.RSABE(), power.scABEL(), power.TOST(), power.TOST.sds(), power.TOST.sim(), pvalues.TOST(), scABEL.ad().

Until the package is updated, please give the total samples size n (assuming almost balanced sequences), the observed sequences as a vector (preferred), or use the workaround with rounding. Sorry for the inconvenience.

Dif-tor heh smusma 🖖🏼 Довге життя Україна! [image]
Helmut Schütz
[image]

The quality of responses received is directly proportional to the quality of the question asked. 🚮
Science Quotes
ElMaestro
★★★

Denmark,
2025-08-23 11:46
(303 d 08:22 ago)

@ Helmut
Posting: # 24435
Views: 3,478
 

 Bug in function power.TOST()

Hi Hötzi,

❝ Say, you dosed N subjects and are interested in post hoc power of a fraction f of this sample size (e.g., of eligible subjects).


:-D
Accusing me of being a member of the post-hoc power fan club? :-P I think my membership was discontinued around 2007.

For me, pre-study power considerations led me to look into this. Like checking a range of prospective attrition rates to evaluate the actual power.
Before, not after.

I have to say the bug is very minor. I have no relevant real-life (non-simulation) cases where this would make an much meaningful difference - extreme cases with unfavourable combinations of small sample sizes and high variabilities perhaps but most of what I do is not that extreme. A difference of e.g. 0.2% or whatever in a power estimation is to me not of particular relevance since in BE there are various assumption which can be violated and which may have unknown consequences for the magnitude of power.
Dasatinib comes to mind. :crying:

Add to that, that an unfortunate habit in the industry is to adjust assumptions to make the sample size / power meet a target, not the other way around. So, I would not invest too much too much time on fixing this one. PowerTOST is a great tool and the matter here does not in any way detract from its value. You and Detlew and possibly other people involved are doing a great job. Muchas gracias.

Pass or fail!
ElMaestro
Helmut
★★★
avatar
Homepage
Vienna, Austria,
2025-08-23 13:01
(303 d 07:07 ago)

@ ElMaestro
Posting: # 24436
Views: 3,519
 

 Bug in function power.TOST()

Hi ElMaestro,


❝ ❝ Say, you dosed N subjects and are interested in post hoc power of a fraction f of this sample size (e.g., of eligible subjects).

:-D

❝ Accusing me of being a member of the post-hoc power fan club? :-P

No accusation, of course. Amazingly it is still popular (in some countries…)

❝ I think my membership was discontinued around 2007.

:thumb up:

❝ For me, pre-study power considerations led me to look into this. Like checking a range of prospective attrition rates to evaluate the actual power.

❝ Before, not after.

Kudos, good practice. ICH E9 Section 3.5 suggests a sensitivity analysis a priori to assess the impact on power depending on deviations from assumptions. See also this article for examples.

❝ […] an unfortunate habit in the industry is to adjust assumptions to make the sample size / power meet a target, not the other way around.

Well, the infamous Boss Button in FARTSSIE

❝ So, I would not invest too much too much time on fixing this one. PowerTOST is a great tool and the matter here does not in any way detract from its value.

It’s an easy fix. We will do it.

❝ You and Detlew and possibly other people involved are doing a great job. Muchas gracias.

¡De nada!

Dif-tor heh smusma 🖖🏼 Довге життя Україна! [image]
Helmut Schütz
[image]

The quality of responses received is directly proportional to the quality of the question asked. 🚮
Science Quotes
d_labes
★★★

Berlin, Germany,
2025-10-06 11:59
(259 d 08:09 ago)

(edited on 2025-10-06 12:13)
@ Helmut
Posting: # 24448
Views: 1,725
 

 Bug in function power.TOST() fixed

Dear All,

the Bug is fixed in PowerTOST version 1.5-7.
On CRAN since 23.09.2025.
Sorry for being late in announcing this.

Regards,

Detlew
UA Flag
Activity
 Admin contact
23,655 posts in 4,993 threads, 1,571 registered users;
143 visitors (0 registered, 143 guests [including 30 identified bots]).
Forum time: 20:09 CEST (Europe/Vienna)

Scientists cannot simply hang their subjectivities
up on a hook outside the laboratory door.    Ruth Bleier

The Bioequivalence and Bioavailability Forum is hosted by
BEBAC Ing. Helmut Schütz
HTML5