Function CVpooled (lengthy answer) [🇷 for BE/BA]
please RTFM: After attaching the package either in the R-console or in R-Studio type
help(CVpooled)
or ?CVpooled
. Alternatively read the man-page online.❝ CVpooled(CVsCmax, alpha=0.2, logscale=TRUE)
Calculates the CV (pooled from your six studies – assuming that individual CVs were obtained from log-transformed data). Furthermore, CVs are weighed by the studies’ degrees of freedom. The dfs depend on how the studies were evaluated; here the default
robust=FALSE
is applied (more about that later). Hence, we get:0.1677 with 181 degrees of freedom
In order to get the 1–α upper confidence limit of the pooled CV, you would have to use the
print
method and the additional argument verbose=TRUE
:print(CVpooled(CVsCmax, alpha=0.2), verbose=TRUE)
Pooled CV = 0.1677 with 181 degrees of freedom
Upper 80% confidence limit of CV = 0.1758
❝ print(CVpooled(CVsCmax, alpha=0.2, robust=TRUE), digits=6, verbose=TRUE)
❝ Pooled CV = 0.175054 with 129 degrees of freedom (robust df's)
❝ Upper 80% confidence limit of CV = 0.185309
❝
❝ Why are degrees of freedom so huge?
You are pooling from six studies which gives you a lot of information. In each of them the degrees of freedom are n–2 for the 2×2 studies and 2n–4 for the 3×3 studies. Type
known.designs()
to get an idea. We can hack the code of the function to look behind the curtains:CVs.1 <- CVs.2 <- CVsCmax # new data frames
CVs.1$obs <- CVs.2$obs <- 0 # column for observations
CVs.1$df <- CVs.2$df <- 0 # column for the dfs
CVs.1$robust <- FALSE # column for the model: fixed
CVs.2$robust <- TRUE # mixed
CVs.1$w.var <- CVs.2$w.var <- 0 # column for weighted variances
for (j in seq_along(CVsCmax$n)) {
dno <- PowerTOST:::.design.no(CVsCmax$design[j])
if (!is.na(dno)) {
dprop <- PowerTOST:::.design.props(dno)
n <- CVsCmax$n[j]
per <- as.integer(substr(CVsCmax$design[j],
(nchar(CVsCmax$design[j])+1)-1,
nchar(CVsCmax$design[j])))
CVs.2$obs[j] <- CVs.1$obs[j] <- per*n
CVs.1$df[j] <- eval(parse(text=dprop$df, srcfile=NULL))
CVs.2$df[j] <- eval(parse(text=dprop$df2, srcfile=NULL))
CVs.1$w.var[j] <- CV2mse(CVs.1$CV[j])*CVs.1$df[j]
CVs.2$w.var[j] <- CV2mse(CVs.2$CV[j])*CVs.2$df[j]
}
}
print(CVs.1, row.names=FALSE)
print(CVs.2, row.names=FALSE)
PKmetric CV n design source obs df robust w.var
Cmax 0.2617 23 2x2 study 1 46 21 FALSE 1.3911140
Cmax 0.1216 24 2x2 study 2 48 22 FALSE 0.3229227
Cmax 0.1426 24 2x2 study 3 48 22 FALSE 0.4428769
Cmax 0.1480 27 3x3 study 4 81 50 FALSE 1.0833777
Cmax 0.1476 27 3x3 study 4a 81 50 FALSE 1.0775921
Cmax 0.2114 18 2x2 study 5 36 16 FALSE 0.6995224
PKmetric CV n design source obs df robust w.var
Cmax 0.2617 23 2x2 study 1 46 21 TRUE 1.3911140
Cmax 0.1216 24 2x2 study 2 48 22 TRUE 0.3229227
Cmax 0.1426 24 2x2 study 3 48 22 TRUE 0.4428769
Cmax 0.1480 27 3x3 study 4 81 24 TRUE 0.5200213
Cmax 0.1476 27 3x3 study 4a 81 24 TRUE 0.5172442
Cmax 0.2114 18 2x2 study 5 36 16 TRUE 0.6995224
Check the dfs which are used to calculate the confidence limit based on the χ2 distribution):
cat("Sum of dfs (fixed effects model):", sum(CVs.1$df),
"\nSum of dfs (mixed effects model):", sum(CVs.2$df), "\n")
Sum of dfs (fixed effects model): 181
Sum of dfs (mixed effects model): 129
The hard way: Calculate pooled weighted variances and back-transform to pooled CVs:
cat("Pooled CVs:",
"\nfixed effects model (robust=FALSE):",
mse2CV(sum(CVs.1$w.var)/sum(CVs.1$df)),
"\nmixed effects model (robust=TRUE) :",
mse2CV(sum(CVs.2$w.var)/sum(CVs.2$df)), "\n")
Pooled CVs:
fixed effects model (robust=FALSE): 0.1676552
mixed effects model (robust=TRUE) : 0.1750539
For the background of the calculation see this presentation (slides 32–35).
❝ And why did I get two different results 0.1677 and 0.1750?
In your second call
❝ print(CVpooled(CVsCmax, alpha=0.2, robust=TRUE), digits=6, verbose=TRUE)
you used the argument
robust=TRUE
(where in the first one implicitly the default robust=FALSE
is used). When the CVs in the studies were calculated by ANOVA (or by SAS’ Proc GLM
) the dfs given in column df
of known.designs()
are used. Only if the CVs were calculated by a mixed effects model (for the FDA e.g., by SAS Proc Mixed
) values of column df2
are used. Note that for 2×2 designs the dfs are identical but for higher-order crossovers the dfs are different (e.g., in your 3×3 the robust dfs are n–3). Since in the latter case the dfs are lower (129 vs. 181), less weight is given to the 3×3 studies with comparably low CVs (pooled CV 0.1751 vs. 0.1677), and the confidence interval is wider (upper CL 0.1853 vs. 0.1758).❝ Is it acceptable and can I use any result to calculate sample size in PowerTOST?
No (see below).
❝ What result must I choose (0.1677, 0.1750, 0.1853) for calculating sample size?
The pooled ones (0.1677 or 0.1751) assume that it is “carved in stone” (i.e., is the “true” value). I don’t recommend that (if the actual CV will be larger than assumed the study it will be underpowered). If it will be smaller you might have lost money but gained power and the chance to demonstrate BE increases. Which
alpha
you use, is up to you. 0.2 is my suggestion (and the function’s default).1,2,3,4 Explore increasing levels of alpha
:alpha <- c(0.05, 0.2, 0.25, 0.5)
digits <- 4
res <- data.frame(alpha=alpha, rob.1=FALSE, CV.1=NA, CL.1=NA,
rob.2=TRUE, CV.2=NA, CL.2=NA)
for (j in seq_along(alpha)) {
CVp <- CVpooled(CVsCmax, alpha=alpha[j], robust=FALSE)
res$CV.1[j] <- signif(CVp$CV, digits)
res$CL.1[j] <- signif(CVp$CVupper, digits)
CVp <- CVpooled(CVsCmax, alpha=alpha[j], robust=TRUE)
res$CV.2[j] <- signif(CVp$CV, digits)
res$CL.2[j] <- signif(CVp$CVupper, digits)
}
print(res, row.names=FALSE)
alpha rob.1 CV.1 CL.1 rob.2 CV.2 CL.2
0.05 FALSE 0.1677 0.1839 TRUE 0.1751 0.1955
0.20 FALSE 0.1677 0.1758 TRUE 0.1751 0.1853
0.25 FALSE 0.1677 0.1742 TRUE 0.1751 0.1833
0.50 FALSE 0.1677 0.1680 TRUE 0.1751 0.1755
You have to know beforehand how the studies were evaluated in order to use the correct setting for the argument
robust
to get the correct pooled CV and its CL (0.1677 → 0.1758 for studies evaluated by ANOVA and 0.1751 → 0.1853 for ones evaluated by a mixed effects model).❝ Is argument "digits" from function CVpooled simply a number of observations for a variable?
No. You get the total number of subjects by …
cat("Sum of n (N):", sum(CVsCmax$n), "\n")
Sum of n (N): 143
… and the total number of observations from one of the new data frames by:
cat("Sum of observations:", sum(CVs.1$obs), "\n")
orcat("Sum of observations:", sum(CVs.2$obs), "\n")
Sum of observations: 340
The argument
digits
tells the print
argument which precision to use in the output, where 4 digits is the default:print(CVpooled(CVsCmax), verbose=TRUE)
Pooled CV = 0.1677 with 181 degrees of freedom
Upper 80% confidence limit of CV = 0.1758
print(CVpooled(CVsCmax), verbose=TRUE, digits=7)
Pooled CV = 0.1676552 with 181 degrees of freedom
Upper 80% confidence limit of CV = 0.1758098
Note that it is a little bit tricky to combine studies evaluated by fixed and mixed effect models. In such a case you have to provide the dfs yourself. Let’s assume that
study 4
was evaluated by a fixed effects model (df = 2n–4) and study 4a
by a mixed effects model (df = n–3). Provide the dfs in the additional column df
(which takes precedence over the combination n
and design
and the argument robust
is not observed any more). I recommend to give an additional column model
(which is not used but for clarity):CVs <- ("
CV | n |design| source | model | df
0.2617 | 23 | 2x2 | study 1 | fixed | 21
0.1216 | 24 | 2x2 | study 2 | fixed | 22
0.1426 | 24 | 2x2 | study 3 | fixed | 22
0.1480 | 27 | 3x3 | study 4 | fixed | 50
0.1476 | 27 | 3x3 | study 4a | mixed | 24
0.2114 | 18 | 2x2 | study 5 | fixed | 16 ")
txtcon <- textConnection(CVs)
CVdata <- read.table(txtcon, header=TRUE, sep="|", strip.white=TRUE, as.is=TRUE)
close(txtcon)
print(CVpooled(CVdata), verbose=TRUE)
Pooled CV = 0.1708 with 155 degrees of freedom
Upper 80% confidence limit of CV = 0.1798
Repeating it the hard way for comparison:
alpha <- 0.2
digits <- 4
CVdata$w.var <- CV2mse(CVdata$CV)*CVdata$df
dftot <- sum(CVdata$df)
chi <- qchisq(alpha, dftot)
pooledse2 <- sum(CVdata$w.var)/dftot
CV <- mse2CV(pooledse2)
CLCV <- se2CV(sqrt(pooledse2*dftot/chi))
cat("Pooled CV =", signif(CV, digits), "with", dftot, "degrees of freedom",
"\nUpper", sprintf("%2i%%", 100*(1-alpha)), "CL of CV =",
signif(CLCV, digits), "\n")
Pooled CV = 0.1708 with 155 degrees of freedom
Upper 80% CL of CV = 0.1798
Hope that helps.
- The idea behind α 0.2 is to have a producer’s risk of 20%.
- Gould recommended a slightly less conservative α 0.25:
Gould AL. Group Sequential Extensions of a Standard Bioequivalence Testing Procedure. J Pharmacokinet Biopharm. 1995;23(1):57–86. PMID 8576845.
- Julious’ α 0.05 is very conservative and useful in a sensitivity analysis.
Julious S. Sample Sizes for Clinical Trials. Boca Raton: Chapman & Hall / CRC Press; 2010. p. 113–114.
- Patterson & Jones recommend a rather liberal α 0.5:
Patterson S, Jones B. Bioequivalence and Statistics in Clinical Pharmacology. Boca Raton: Chapman & Hall / CRC Press; 2nd ed. 2017. p. 134–136.
Dif-tor heh smusma 🖖🏼 Довге життя Україна!
Helmut Schütz
The quality of responses received is directly proportional to the quality of the question asked. 🚮
Science Quotes
Complete thread:
- Function CVpooled (package PowerTOST) Elena777 2018-01-21 08:53 [🇷 for BE/BA]
- Function CVpooled (lengthy answer)Helmut 2018-01-21 17:21
- Function CVpooled (lengthy answer) Elena777 2018-01-27 10:11
- Function CVpooled (package PowerTOST) ElMaestro 2018-01-27 22:28
- Common sense Helmut 2018-01-28 00:27
- Common sense ElMaestro 2018-01-28 08:43
- Alzheimer’s Helmut 2018-01-28 11:38
- Common sense ElMaestro 2018-01-28 08:43
- To pool or not to pool d_labes 2018-01-28 13:52
- Common sense Helmut 2018-01-28 00:27
- Function CVpooled (package PowerTOST) ElMaestro 2018-01-29 00:07
- Pooling is fooling? Helmut 2018-01-29 17:53
- Pooling is fooling? nobody 2018-01-29 19:21
- Life is good ElMaestro 2018-01-30 18:23
- Life is good nobody 2018-01-30 18:31
- Life is good ElMaestro 2018-01-30 18:23
- Pooling is fooling? nobody 2018-01-29 19:21
- Pooling is fooling? Helmut 2018-01-29 17:53
- Function CVpooled (lengthy answer)Helmut 2018-01-21 17:21