using Pumas # available with Pumas products - also reexports Distributions
using NCA # available with Pumas products
using Bioequivalence # available with Pumas products
using BioequivalencePower # available with Pumas products
using PharmaDatasets # available with Pumas products
using GLM
using DataFramesMeta
using SummaryTables
using DataFrames
using DataFramesMeta
using AlgebraOfGraphics
using CairoMakie
using Random
using LinearAlgebra
using MixedModels

A Course on Bioequivalence: Unit 10 - Details of Linear and Mixed Effects Models
This is Unit 10 of a complete bioequivalence analysis course using Pumas. There are 15 units in the course in total.
1 Unit overview
This unit fills in some missing details of fixed effects linear models and mixed effects (random effects) linear models. In particular we continue the discussion from Unit 5 and present further details of models used for bioequivalence.
As you can recall from Unit 5:
- With the fixed effects models,
pumas_be
uses Julia’sGLM
package. This yields behavior compatible with SAS’sPROC GLM
. - With the mixed effects models,
pumas_be
uses Julia’sMixedModels
package. This yields behavior compatible with SAS’sPROC MIXED
.
As a general rule we use the model based on the type of design:
- If the design is a non-replicate cross over design, a fixed effects model is used.
- If the design is a replicate design (partial replicate or fully replicate), a mixed effects model is used.
Further, in replicate designs, an additional auxiliary estimator is used to estimate the within subject variability of either the reference product, the test product, or both products.
Some FDA examples present the computation of this auxiliary estimator using SAS’s PROC MIXED
, but it can also be done with a fixed effects model as carried out in pumas_be
. In any case, we present the details of this auxiliary estimator, which with Pumas can also be called directly via the within_subject_variability
function.
In addition to these topics, this unit presents the fixed effects models used for cases of multiple formulations with multiple test products. This continues the discussion of multiple formulations in Unit 4 where we called such cases 1RnT
cases (with n=2
or n=3
).
Hence in summary this unit presents the following topics:
- A review and further investigation of the fixed effects model formulation used for standard \(2 \times 2\) crossover designs as well as crossover designs with multiple test products (
1RnT
). - A dive into the mixed effect model formulations used for partial and fully replicate designs. This includes a brief discussion of REML (residual maximum likelihood estimation) and a brief mention of the Satterthwaite computation for such models.
- A discussion of the model used to estimate within subject variability (used with the
within_subject_variability
Pumas function, and also used implicitly withpumas_be
).
We use the following packages.
We have used all of these packages in previous units.
2 Fixed effects models
Mathematically (and loosely) a fixed effect model has the general form:
\[ \underbrace{y}_{\text{log endpoint}} = \underbrace{\beta^{\top} x}_{\text{linear combination of fixed effects}} + \qquad \underbrace{\varepsilon}_{\text{noise}} \]
Here \(\beta\) is a vector of coefficients that is estimated and \(x\) is composed of the predictors. Hence the response \(y\) (typically a log transformed endpoint) is a linear function of the predictors \(x\). The noise term \(\varepsilon\) is considered to be normally distributed with a \(0\) mean and a standard deviation \(\sigma\), which is also an unknown model parameter.
See Unit 5 for an introductory discussion of the linear model. You can also see more details in Nazarathy and Klok (2021), Liquet, Moka, and Nazarathy (2024), and references within.
A review of the basic bioequivalence linear model
Let’s return to a basic \(2 \times 2\) crossover design example:
= dataset(joinpath("bioequivalence", "RT_TR", "PJ2017_3_12.csv"))
data = pumas_be(data) be_result
Observation Counts | ||
Sequence ╲ Period | 1 | 2 |
RT | 23 | 23 |
TR | 23 | 23 |
Paradigm: Non replicated crossover design | ||||
Model: Linear model | ||||
Criteria: Standard ABE | ||||
Endpoint: AUC | ||||
Formulations: Reference(R), Test(T) | ||||
Results(AUC) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 78.95 | ||
Geometric Naive Mean | 83.08 | |||
T | Geometric Marginal Mean | 86.99 | ||
Geometric Naive Mean | 86.35 | |||
Geometric Mean T/R Ratio (%) | 110.2 | |||
Degrees of Freedom | 43 | |||
90% Confidence Interval (%) | [94.08, 129] | Fail | CI ⊆ [80, 125] | |
Variability | CV (%) | σ̂ | 46.89 | 0.4458 | ||
ANOVA | Formulation (p-value) | 0.308 | ||
Sequence (p-value) | 0.1742 | |||
Period (p-value) | 0.5947 | |||
As you may recall from Unit 5, this is the formula used:
|> formula |> println be_result.model
endpoint ~ 1 + sequence + formulation + period + sequence & id_within_sequence
All of the effects 1
(the intercept), sequence
(the sequence effect), formulation
(the formulation effect), period
(the period effect), and sequence & id_within_sequence
(the subject effect), are part of the linear combination \(\beta^{\top} x\).
The bulk of the model is thus captured by the coefficients \(\beta\) which we can retrieve via the coef
function:
round.(coef(be_result.model), digits = 3) |> println
[4.459, -0.13, 0.097, -0.05, -0.135, -1.743, 0.344, -1.123, 1.154, -1.032 … -1.63, -0.345, -1.132, -0.934, 1.498, 0.672, -0.14, 2.598, 0.013, 0.0]
Note that there are 50 coefficients here, most of which are for the `sequence & id_within_sequence
subject effects.
Fixed effects models for more than two formulations
As we saw at the end of Unit 4, we can also have a single model, even if there are more than two formulations. In particular we support the case of three formulations (one reference formulation and two test formulations), and four formulations (one reference formulation three test formulations). These cases are most popular in the 1RnT
setting where the additional formulations are considered as test products (in the case of multiple reference products the recommendation is typically to consider different models).
When we run bioequivalence analysis for such cases, even though a separate hypothesis test is considered for each formulation, Pumas still uses a single linear model for all formulations. This allows to use all of the available data to estimate the variability and to consider the test formulation. This is most suitable for what we call the `
As an example let us focus on the case with three formulations with this dataset:
= dataset("bioequivalence/RST_RTS_SRT_STR_TRS_TSR/PJ2017_4_5")
data_3_formulations detect_design(data_3_formulations)
(design = "RST|RTS|SRT|STR|TRS|TSR",
num_sequences = 6,
num_periods = 3,
num_formulations = 3,
reference_formulation = 'R',
test_formulations = ['S', 'T'],
replicated = non_replicated,
supports_rsabe = false,
crossover = true,)
As you can see, there are 6 sequences in this design, comprised of all possible permutations of the reference R
, and the test formulations S
and T
. To see the size of the dataset use:
size(data_3_formulations)
(186, 5)
or alternatively to see the number of rows use:
nrow(data_3_formulations)
186
Here are the first few rows of the dataset:
simple_table(first(data_3_formulations, 8))
id | sequence | period | AUC | Cmax |
1 | SRT | 1 | 7260 | 1633 |
1 | SRT | 2 | 6463 | 1366 |
1 | SRT | 3 | 8759 | 2141 |
2 | RTS | 1 | 3457 | 776 |
2 | RTS | 2 | 6556 | 2387 |
2 | RTS | 3 | 4081 | 1355 |
4 | TSR | 1 | 4006 | 1326 |
4 | TSR | 2 | 4879 | 1028 |
And here are the last few rows of the dataset:
simple_table(last(data_3_formulations, 8))
id | sequence | period | AUC | Cmax |
62 | SRT | 2 | 7069 | 1995 |
62 | SRT | 3 | 6530 | 1236 |
63 | TRS | 1 | 2204 | 495 |
63 | TRS | 2 | 2927 | 770 |
63 | TRS | 3 | missing | missing |
67 | RST | 1 | 4045 | 1025 |
67 | RST | 2 | 7865 | 2668 |
67 | RST | 3 | missing | missing |
As you can see, there is some missing endpoint data in the dataset. When carrying out analysis for a specific endpoint (either AUC or Cmax in this case), Pumas will throw away lines with missing values for the endpoint.
Let’s run pumas_be
on both endpoints. You can see that the observation counts differ per endpoint (click the tabs to change between AUC and Cmax):
= pumas_be(data_3_formulations, endpoint = :AUC) auc_result
Observation Counts | |||
Sequence ╲ Period | 1 | 2 | 3 |
RST | 8 | 8 | 8 |
RTS | 11 | 11 | 11 |
SRT | 11 | 11 | 11 |
STR | 10 | 10 | 10 |
TRS | 10 | 10 | 10 |
TSR | 10 | 10 | 10 |
Paradigm: 3 formulations | ||||
Model: Linear model | ||||
Criteria: Standard ABE | ||||
Endpoint: AUC | ||||
Formulations: Reference(R), Test(S and T) | ||||
Results(AUC) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 3808 | ||
Geometric Naive Mean | 3824 | |||
S | Geometric Marginal Mean | 5346 | ||
Geometric Naive Mean | 5412 | |||
Geometric Mean S/R Ratio (%) | 140.4 | |||
Degrees of Freedom | 114 | |||
90% Confidence Interval (%) | [131.7, 149.7] | Fail | CI ⊆ [80, 125] | |
T | Geometric Marginal Mean | 4426 | ||
Geometric Naive Mean | 4426 | |||
Geometric Mean T/R Ratio (%) | 116.2 | |||
Degrees of Freedom | 114 | |||
90% Confidence Interval (%) | [109, 123.9] | Pass | CI ⊆ [80, 125] | |
Variability | CV (%) | σ̂ | 21.22 | 0.2099 | ||
= pumas_be(data_3_formulations, endpoint = :Cmax) cmax_result
Observation Counts | |||
Sequence ╲ Period | 1 | 2 | 3 |
RST | 9 | 9 | 8 |
RTS | 11 | 11 | 11 |
SRT | 11 | 11 | 11 |
STR | 10 | 10 | 10 |
TRS | 11 | 11 | 10 |
TSR | 10 | 10 | 10 |
Paradigm: 3 formulations | ||||
Model: Linear model | ||||
Criteria: Standard ABE | ||||
Endpoint: Cmax | ||||
Formulations: Reference(R), Test(S and T) | ||||
Results(Cmax) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 832.4 | ||
Geometric Naive Mean | 837.5 | |||
S | Geometric Marginal Mean | 1327 | ||
Geometric Naive Mean | 1340 | |||
Geometric Mean S/R Ratio (%) | 159.4 | |||
Degrees of Freedom | 118 | |||
90% Confidence Interval (%) | [146.1, 173.9] | Fail | CI ⊆ [80, 125] | |
T | Geometric Marginal Mean | 1082 | ||
Geometric Naive Mean | 1078 | |||
Geometric Mean T/R Ratio (%) | 129.9 | |||
Degrees of Freedom | 118 | |||
90% Confidence Interval (%) | [119.1, 141.8] | Fail | CI ⊆ [80, 125] | |
Variability | CV (%) | σ̂ | 29.69 | 0.2907 | ||
Let’s focus on the AUC case. This is the model formula, as you can see it is exactly the same formula like the \(2 \times 2\) case:
|> formula |> println auc_result.model
endpoint ~ 1 + sequence + formulation + period + sequence & id_within_sequence
The difference is that there are more levels for the various factors.
Note that for brevity here we only display the first 15 rows of the coefficient table:
simple_table(first(coeftable(auc_result.model), 15), halign = :left)
Name | Coef. | Std. Error | t | Pr(>|t|) | Lower 95% | Upper 95% |
(Intercept) | 8.18 | 0.0563 | 145 | 3.02e-131 | 8.07 | 8.29 |
sequence: RTS | 0.0481 | 0.0585 | 0.823 | 0.412 | -0.0678 | 0.164 |
sequence: SRT | 0.172 | 0.0585 | 2.93 | 0.00407 | 0.0556 | 0.287 |
sequence: STR | -0.1 | 0.0596 | -1.68 | 0.0948 | -0.219 | 0.0177 |
sequence: TRS | 0.187 | 0.061 | 3.06 | 0.00273 | 0.0661 | 0.308 |
sequence: TSR | 0.0529 | 0.0596 | 0.888 | 0.377 | -0.0652 | 0.171 |
formulation: S | 0.339 | 0.0386 | 8.79 | 1.8e-14 | 0.263 | 0.416 |
formulation: T | 0.15 | 0.0386 | 3.9 | 0.000163 | 0.0741 | 0.227 |
period: 2 | 0.053 | 0.0384 | 1.38 | 0.169 | -0.0229 | 0.129 |
period: 3 | -0.0294 | 0.0388 | -0.756 | 0.451 | -0.106 | 0.0476 |
sequence: RST & id_within_sequence: 2 | -0.405 | 0.116 | -3.48 | 0.000702 | -0.635 | -0.175 |
sequence: RTS & id_within_sequence: 2 | 0.0825 | 0.116 | 0.714 | 0.477 | -0.146 | 0.311 |
sequence: SRT & id_within_sequence: 2 | -0.521 | 0.116 | -4.51 | 1.59e-5 | -0.75 | -0.292 |
sequence: STR & id_within_sequence: 2 | -0.125 | 0.115 | -1.09 | 0.279 | -0.353 | 0.103 |
sequence: TRS & id_within_sequence: 2 | 0.378 | 0.117 | 3.24 | 0.00159 | 0.147 | 0.61 |
You can see:
- The
sequence
factor (with 6 levels) has 5 coefficients. - The
period
factor (with 3 levels) has 2 coefficients. - Most importantly, the
formulation
factor (with 3 levels) has 2 coefficients.
Let’s extract the coefficients associated with formulation
:
= coef(auc_result.model)[7:8]
β_S, β_T (β_S, β_T)
(0.3392505331133662, 0.15049814185173857)
We can then obtain the geometric mean ratio estimates (GMR) results via these:
= round(100exp(β_S), digits = 2)
GMR_S = round(100exp(β_T), digits = 2)
GMR_T (GMR_S, GMR_T)
(140.39, 116.24)
You can see (that up to rounding variations) these values agree with the pumas_be
output above.
Degrees of freedom and variability estimation
The degrees of freedom (DOF) of the model’s residuals represent the number of independent pieces of information available to estimate the model’s error variance (or variability).
Intuitively, you can think of it this way: you start with a certain number of observations (n
), and each time you use the data to estimate a parameter (like an intercept or a coefficient for sequence
or period
), you “use up” one degree of freedom. What remains are the residual degrees of freedom, used to estimate the variability. This is why, in the simplest case, they are calculated as the total number of observations minus the number of estimated parameters.
This value is critical for obtaining an unbiased estimate of the residual standard deviation and for determining the specific t-distribution used in confidence interval calculations.
In the simple case with n
observations and p
estimated coefficients, are obtained via n - p
. For example in our case,
= nrow(auc_result.data)
n = length(coef(auc_result.model))
p n, p
(180, 70)
And hence we may expect n-p
degrees of freedom:
= n - p dof
110
However, more specifically, the DOF are determined by properties of the design matrix of the data. If really needed, you can access this design matrix, X
, via:
= auc_result.model.mm.m
X size(X)
(180, 70)
This matrix is internally used for least squares estimation of the model.
Now it sometimes occurs, due to missing observations that that the columns of the design matrix are not linearly independent. From a mathematical (linear algebra) point of view, this means that the column rank of the design matrix is less thank the number of columns. If really needed we can investigate it via the rank
function from the LinearAlgebra
package:
rank(X)
66
With this, the correct way to compute the degrees of freedom is via:
= n - rank(X) dof
114
Indeed this agrees with the output of dof_residual
from the GLM
package:
= dof_residual(auc_result.model) dof
114.0
Now with this, we can obtain \(\hat{\sigma}\), the estimate of the standard deviation of \(\varepsilon\) via:
= round(sqrt(deviance(auc_result.model) / dof), digits = 4) σ̂
0.2099
Note that we can also use the dispersion
function, but in this case we need to use model.model
:
round(dispersion(auc_result.model.model), digits = 4)
0.2099
In any case, we can consider the value as a CV:
round(100sigma2geocv(σ̂), digits = 2)
21.22
As you can see, this value agrees with the Variability
estimate in the above pumas_be
output.
3 Mixed (random) effects models
Mathematically (and loosely) a mixed (random) effects model has the general form:
\[ \underbrace{y}_{\text{log endpoint}} = \underbrace{\beta^{\top} x}_{\text{linear combination of fixed effects}} + \qquad \underbrace{\zeta}_{\text{random effects}} + \qquad \underbrace{\varepsilon}_{\text{noise}} \]
Here the additional term beyond fixed effects models is denoted as \(\zeta\). It is a a random noise term which can also depend on the predictors \(x\) and on other factors.
The most basic use for it in the case of clinical trials (and bioequivalence trials) is to capture subject effects.
When considering any mixed effects model, our goal is not only to estimate \(\beta\) and the variability of \(\varepsilon\), \(\sigma\), but to also estimate the variance-covariance structure of \(\zeta\).
Two variations of mixed effects models for replicated designs
In Unit 5 we saw the use of the MixedModels
package directly for a \(2 \times 2\) crossover design. This agrees with cases where one wants to use the FDA’s SAS PROC MIXED
suggestion.
When we move onto replicate (partial or complete) designs, pumas_be
also uses mixed models, directly under the hood. To see this consider this fully replicate dataset:
= dataset(joinpath("bioequivalence", "RTTR_TRRT", "PJ2017_4_3")); data
Let us run pumas_be
on this dataset, where we also consider two options for the homogeneity
argument, false
for an unequal variance assumption, and true
for an equal variance assumption (the default is false
).
Click on this tabset to alternate between the outputs. You will see slight differences between the Degrees of Freedom
, and the associated quantities such as the GMR estimate and the \(90\%\) confidence interval.
With homogeneity = true
we enforce an equal variance assumption.
= pumas_be(data, endpoint = :Cmax, homogeneity = true) result_yes_homogenous
Observation Counts | ||||
Sequence ╲ Period | 1 | 2 | 3 | 4 |
RTTR | 8 | 8 | 8 | 8 |
TRRT | 9 | 9 | 9 | 8 |
Paradigm: Replicated crossover that supports reference scaling | ||||
Model: Mixed model (equal variance) | ||||
Criteria: Standard ABE | ||||
Endpoint: Cmax | ||||
Formulations: Reference(R), Test(T) | ||||
Results(Cmax) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 1150 | ||
Geometric Naive Mean | 1150 | |||
T | Geometric Marginal Mean | 1049 | ||
Geometric Naive Mean | 1048 | |||
Geometric Mean T/R Ratio (%) | 91.21 | |||
Degrees of Freedom | 45.97 | |||
90% Confidence Interval (%) | [83.21, 99.99] | Pass | CI ⊆ [80, 125] | |
Variability | CVᵣ (%) | σ̂ᵣ | 21.17 | 0.2094 | ||
CVₜ (%) | σ̂ₜ | 26.87 | 0.264 | |||
Variability Ratio (%) | 126.2 | |||
ANOVA | Formulation (p-value) | 0.09955 | ||
Sequence (p-value) | 0.5563 | |||
Period (p-value) | 0.4375 | |||
The default for linear mixed models is homogeneity = false
. Hence we don’t need to use explicitly use this option.
= pumas_be(data, endpoint = :Cmax) result_non_homogenous
Observation Counts | ||||
Sequence ╲ Period | 1 | 2 | 3 | 4 |
RTTR | 8 | 8 | 8 | 8 |
TRRT | 9 | 9 | 9 | 8 |
Paradigm: Replicated crossover that supports reference scaling | ||||
Model: Mixed model (unequal variance) | ||||
Criteria: Standard ABE | ||||
Endpoint: Cmax | ||||
Formulations: Reference(R), Test(T) | ||||
Results(Cmax) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 1150 | ||
Geometric Naive Mean | 1150 | |||
T | Geometric Marginal Mean | 1049 | ||
Geometric Naive Mean | 1048 | |||
Geometric Mean T/R Ratio (%) | 91.23 | |||
Degrees of Freedom | 40.82 | |||
90% Confidence Interval (%) | [83.18, 100.1] | Pass | CI ⊆ [80, 125] | |
Variability | CVᵣ (%) | σ̂ᵣ | 21.17 | 0.2094 | ||
CVₜ (%) | σ̂ₜ | 26.87 | 0.264 | |||
Variability Ratio (%) | 126.2 | |||
ANOVA | Formulation (p-value) | 0.1019 | ||
Sequence (p-value) | 0.5596 | |||
Period (p-value) | 0.4916 | |||
Let us see the formulas used across the models.
println("Equal variance:")
println(result_yes_homogenous.model.formula)
println("\nUnequal variance:")
println(result_non_homogenous.model.formula)
Equal variance:
endpoint ~ 1 + formulation + sequence + period + (1 | id)
Unequal variance:
endpoint ~ 1 + formulation + sequence + period + (formulation + 0 | id) + zerocorr(formulation + 0 | row)
In both cases the fixed effects are the intercept (1
), and then the formulation
, the sequence
, and the period
. These same fixed effects were in the non-random effects (non-mixed) models analyzed previously.
The difference is with the random effects. These are terms with a |
character.
The equal variance model (homogeneity = true
) has:
- The random effect
(1 | id)
term implies a random effect perid
. This is similar to the model used in Unit 5. The| id
syntax means “grouped byid
”. Hence eachid
has its own instance of the random effect \(\zeta\), but since there are multiple occurrences of the endpoint for the subjectid
, they all have the same instance of subject effect.
The unequal variance model (homogeneity = false
) has two variance components:
- The random effect
(formulation + 0 | id)
which implies that eachid
has a different instance of the random effect like in the case of equal model variance, but this time, the instances associated with formulationsR
andT
are allowed to have different variability. I.e. there is one variability forR
and one forT
. - The random effect
zerocorr(formulation + 0 | row)
is an unusual grouping in the world of mixed models, yet it is the suitable model in this case. Thezerocorr()
wrapper specifies that the random effects for this grouping factor have zero correlations between their elements—i.e., the random effects are uncorrelated. Theformulation + 0 | row
syntax means that for each row, we consider random effects based on theformulation
. This is a way to allow the unexplained variability in the model to have different variability parameters for each formulation.
With these interpretations it is useful to consider the model
field of the pumas_be
output in both cases:
result_yes_homogenous.model
Est. | SE | z | p | σ_id | |
(Intercept) | 6.9935 | 0.0747 | 93.56 | <1e-99 | 0.0871 |
formulation: T | -0.0920 | 0.0547 | -1.68 | 0.0928 | |
sequence: TRRT | -0.0416 | 0.0692 | -0.60 | 0.5471 | |
period: 2 | 0.0832 | 0.0767 | 1.08 | 0.2783 | |
period: 3 | 0.0942 | 0.0767 | 1.23 | 0.2194 | |
period: 4 | 0.1221 | 0.0780 | 1.57 | 0.1172 | |
Residual | 0.2234 |
result_non_homogenous.model
Est. | SE | z | p | σ_row | σ_id | |
(Intercept) | 6.9988 | 0.0724 | 96.62 | <1e-99 | ||
formulation: T | -0.0918 | 0.0549 | -1.67 | 0.0942 | 0.1369 | 0.0771 |
sequence: TRRT | -0.0415 | 0.0696 | -0.60 | 0.5507 | ||
period: 2 | 0.0757 | 0.0760 | 1.00 | 0.3194 | ||
period: 3 | 0.0908 | 0.0760 | 1.19 | 0.2321 | ||
period: 4 | 0.1115 | 0.0765 | 1.46 | 0.1449 | ||
formulation: R | 0.0196 | 0.1004 | ||||
Residual | 0.2000 |
In the equal variance model we see the estimate σ_id
(0.0871
) for the random effects in addition to the residual variability estimate (0.2234
).
In the unequal variance model we see that the estimate σ_id
is split across formulation: T
(0.0771
) and formulation: R
(0.1004
). Further the estimate σ_row
which goes on each row of the model is also split across the formulations with formulation: T
(0.1369
) and formulation: R
(0.0196
). Finally, there is also the residual variability estimate (0.2000
).
This latter model can potentially capture more variability in the data and with this potentially obtain more suitable estimates for the formulation effect.
The Satterthwaite computation
You may notice the degrees of freedom appearing in the outputs of the above linear models. For the equal variance model assumption the Degrees of Freedom
term is 45.97
, and for the unequal variance model the Degrees of Freedom
term is 40.82
.
You may obtain these values (and any other value appearing in pumas_be
output) via the result_table
field. For example,
= result_yes_homogenous.result_table[:DF] dof_yes_homogenous
45.97
= result_non_homogenous.result_table[:DF] dof_non_homogenous
40.82
Now these degrees of freedom are computed via an approach called Satterthwaite’s method for t-statistics. A brief summary of the approach is Haubo and Christensen (2020), at this point, let us take these computations as given.
Importantly, we now calculate a confidence interval based on these degrees of freedom. We also use the stderror
function from the MixedModels
package. Here is the computation for each case. You can compare we obtain the same confidence intervals as those in pumas_be
.
= coef(result_yes_homogenous.model)[2] β_T
-0.09195391814186732
= stderror(result_yes_homogenous.model)[2] SE
0.05470239368161244
= quantile(TDist(dof_yes_homogenous), 0.95) t_quantile
1.6786829275296131
= [β_T - SE * t_quantile, β_T + SE * t_quantile]
ci round.(100exp.(ci), digits = 2) |> println
[83.21, 99.99]
= coef(result_non_homogenous.model)[2] β_T
-0.09181507690732098
= stderror(result_non_homogenous.model)[2] SE
0.05486184122988195
= quantile(TDist(dof_non_homogenous), 0.95) t_quantile
1.6830495451436747
= [β_T - SE * t_quantile, β_T + SE * t_quantile]
ci round.(100exp.(ci), digits = 2) |> println
[83.18, 100.05]
Restricted maximum likelihood (REML)
The parameter estimation in mixed models is typically initially positioned as maximum likelihood estimation (MLE). With an MLE approach, the estimation is based on a strong theoretical footing which also supports associated hypothesis tests. However, with small samples as often occurring in the context of maximum likelihood, and/or with complex variance structures of mixed models, an alternative popular approach is restricted maximum likelihood (REML). In a nutshell, such an approach presents better (less biased) estimates of variance parameters.
See Section 5.1 of Patterson and Jones (2017) for further discussion of REML in our context. Additional references are suggested in that reference.
From the perspective of pumas_be
, the reml
option is by default true
. If wishing to carry out estimation without REML, but rather with a direct maximum likelihood approach, you may set reml = false
. For example, here is the equal variance model from above, only this time with direct maximum likelihood estimation, as opposed to residual maximum likelihood.
= pumas_be(data, endpoint = :Cmax, reml = true) be_result
Observation Counts | ||||
Sequence ╲ Period | 1 | 2 | 3 | 4 |
RTTR | 8 | 8 | 8 | 8 |
TRRT | 9 | 9 | 9 | 8 |
Paradigm: Replicated crossover that supports reference scaling | ||||
Model: Mixed model (unequal variance) | ||||
Criteria: Standard ABE | ||||
Endpoint: Cmax | ||||
Formulations: Reference(R), Test(T) | ||||
Results(Cmax) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 1150 | ||
Geometric Naive Mean | 1150 | |||
T | Geometric Marginal Mean | 1049 | ||
Geometric Naive Mean | 1048 | |||
Geometric Mean T/R Ratio (%) | 91.23 | |||
Degrees of Freedom | 40.82 | |||
90% Confidence Interval (%) | [83.18, 100.1] | Pass | CI ⊆ [80, 125] | |
Variability | CVᵣ (%) | σ̂ᵣ | 21.17 | 0.2094 | ||
CVₜ (%) | σ̂ₜ | 26.87 | 0.264 | |||
Variability Ratio (%) | 126.2 | |||
ANOVA | Formulation (p-value) | 0.1019 | ||
Sequence (p-value) | 0.5596 | |||
Period (p-value) | 0.4916 | |||
|> println be_result.model
Linear mixed model fit by REML
endpoint ~ 1 + formulation + sequence + period + (formulation + 0 | id) + zerocorr(formulation + 0 | row)
REML criterion at convergence: 13.441805854606276
Variance components:
Column Variance Std.Dev. Corr.
row formulation: R 0.0003849 0.0196180
formulation: T 0.0187330 0.1368686 .
id formulation: R 0.0100757 0.1003778
formulation: T 0.0059463 0.0771126 +1.00
Residual 0.0400044 0.2000111
Number of obs: 67; levels of grouping factors: 67, 17
Fixed-effects parameters:
───────────────────────────────────────────────────────
Coef. Std. Error z Pr(>|z|)
───────────────────────────────────────────────────────
(Intercept) 6.99877 0.0724352 96.62 <1e-99
formulation: T -0.0918151 0.0548618 -1.67 0.0942
sequence: TRRT -0.0415016 0.0695515 -0.60 0.5507
period: 2 0.0756548 0.0759892 1.00 0.3194
period: 3 0.0908021 0.0759892 1.19 0.2321
period: 4 0.111534 0.0765 1.46 0.1449
───────────────────────────────────────────────────────
= pumas_be(data, endpoint = :Cmax, reml = false) be_result
Observation Counts | ||||
Sequence ╲ Period | 1 | 2 | 3 | 4 |
RTTR | 8 | 8 | 8 | 8 |
TRRT | 9 | 9 | 9 | 8 |
Paradigm: Replicated crossover that supports reference scaling | ||||
Model: Mixed model (unequal variance) | ||||
Criteria: Standard ABE | ||||
Endpoint: Cmax | ||||
Formulations: Reference(R), Test(T) | ||||
Results(Cmax) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 1150 | ||
Geometric Naive Mean | 1150 | |||
T | Geometric Marginal Mean | 1049 | ||
Geometric Naive Mean | 1048 | |||
Geometric Mean T/R Ratio (%) | 91.24 | |||
Degrees of Freedom | 45.06 | |||
90% Confidence Interval (%) | [83.52, 99.67] | Pass | CI ⊆ [80, 125] | |
Variability | CVᵣ (%) | σ̂ᵣ | 21.17 | 0.2094 | ||
CVₜ (%) | σ̂ₜ | 26.87 | 0.264 | |||
Variability Ratio (%) | 126.2 | |||
ANOVA | Formulation (p-value) | 0.08819 | ||
Sequence (p-value) | 0.5352 | |||
Period (p-value) | 0.4537 | |||
|> println be_result.model
Linear mixed model fit by maximum likelihood
endpoint ~ 1 + formulation + sequence + period + (formulation + 0 | id) + zerocorr(formulation + 0 | row)
logLik -2 logLik AIC AICc BIC
4.9215 -9.8430 14.1570 19.9347 40.6133
Variance components:
Column Variance Std.Dev. Corr.
row formulation: R 0.0001768 0.0132959
formulation: T 0.0171759 0.1310570 .
id formulation: R 0.0083688 0.0914809
formulation: T 0.0048674 0.0697665 +1.00
Residual 0.0369367 0.1921891
Number of obs: 67; levels of grouping factors: 67, 17
Fixed-effects parameters:
────────────────────────────────────────────────────────
Coef. Std. Error z Pr(>|z|)
────────────────────────────────────────────────────────
(Intercept) 6.9987 0.0686395 101.96 <1e-99
formulation: T -0.0916953 0.0526136 -1.74 0.0814
sequence: TRRT -0.0413819 0.0653844 -0.63 0.5268
period: 2 0.0755887 0.0728789 1.04 0.2997
period: 3 0.0907633 0.0728789 1.25 0.2130
period: 4 0.111669 0.0733674 1.52 0.1280
────────────────────────────────────────────────────────
4 Within Subject Variability estimation
While we may potentially estimate the within subject variability directly from the model, a common approach is to setup auxiliary estimators focused directly on computing this variability. This approach is also suggested in the FDA draft guidances FDA (2011) and FDA (2012), which are further discussed in the context of reference scaling in the sequel.
The pumas_be
function carries out this estimation for us, yet let us also see how to do it in steps.
Separating out R
and T
Consider this example:
= dataset(joinpath("bioequivalence", "RTTR_TRRT", "PJ2017_4_3")); data
Let us separate out the R
and the T
data, after preprocess_be
:
= preprocess_be(data; endpoint = :Cmax); data_preprocessed
= @rsubset data_preprocessed :formulation == 'R'
data_R = @rsubset data_preprocessed :formulation == 'T'; data_T
Using the auxiliary estimator to estimate within subject variability
We can now use Pumas’ within_subject_variability
which gives us an estimate as well as a degrees of freedom for the estimate.
within_subject_variability(data_R)
(σw = 0.2094036782727373,
k = 15.0,)
within_subject_variability(data_T)
(σw = 0.2640679869013234,
k = 14.0,)
Here we combine these estimates and also use sigma2geocv
:
= combine(
σw_estimates groupby(data_preprocessed, :formulation),
-> within_subject_variability(
subdf
subdf;= true,
userepeatedobsonly = true,
allownonreplicated
),
)
@rtransform! σw_estimates :CV = round(100sigma2geocv(:σw), digits = 2)
@rtransform! σw_estimates :σw = round(:σw, digits = 4)
@rselect! σw_estimates :CV :σw :k
simple_table(σw_estimates, round_mode = nothing)
CV | σw | k |
21.17 | 0.2094 | 15.0 |
26.87 | 0.2641 | 14.0 |
Observe that these same results appear with pumas_be
.
pumas_be(data, endpoint = :Cmax)
Observation Counts | ||||
Sequence ╲ Period | 1 | 2 | 3 | 4 |
RTTR | 8 | 8 | 8 | 8 |
TRRT | 9 | 9 | 9 | 8 |
Paradigm: Replicated crossover that supports reference scaling | ||||
Model: Mixed model (unequal variance) | ||||
Criteria: Standard ABE | ||||
Endpoint: Cmax | ||||
Formulations: Reference(R), Test(T) | ||||
Results(Cmax) | Assessment | Criteria | ||
R | Geometric Marginal Mean | 1150 | ||
Geometric Naive Mean | 1150 | |||
T | Geometric Marginal Mean | 1049 | ||
Geometric Naive Mean | 1048 | |||
Geometric Mean T/R Ratio (%) | 91.23 | |||
Degrees of Freedom | 40.82 | |||
90% Confidence Interval (%) | [83.18, 100.1] | Pass | CI ⊆ [80, 125] | |
Variability | CVᵣ (%) | σ̂ᵣ | 21.17 | 0.2094 | ||
CVₜ (%) | σ̂ₜ | 26.87 | 0.264 | |||
Variability Ratio (%) | 126.2 | |||
ANOVA | Formulation (p-value) | 0.1019 | ||
Sequence (p-value) | 0.5596 | |||
Period (p-value) | 0.4916 | |||
5 Conclusion
This unit provided a deeper exploration into the statistical models that power bioequivalence analysis in Pumas. We revisited the fixed-effects linear models used for standard crossover designs, extending the discussion to handle cases with multiple test formulations within a single, unified model. We then transitioned to the more complex mixed-effects models, which are essential for analyzing replicate designs. Key concepts such as the distinction between equal and unequal variance assumptions, the role of Restricted Maximum Likelihood (REML) in estimation, and the Satterthwaite method for approximating degrees of freedom were introduced to explain the machinery behind the results.
The models and methods detailed here are not just theoretical exercises; they form the critical foundation for advanced bioequivalence assessments. The ability to correctly model subject effects and estimate within-subject variability, particularly for the reference product, is a prerequisite for the scaled bioequivalence approaches required by regulatory agencies for highly variable drugs. The next unit will build directly upon this foundation, introducing Reference-Scaled Average Bioequivalence (RSABE) and demonstrating how the outputs from these mixed models, especially the within-subject variability, are used to adjust the bioequivalence acceptance criteria.
6 Unit exercises
Multiple Formulations in a Fixed-Effects Model Using the
data_3_formulations
dataset from the unit, let’s manually verify the Geometric Mean Ratio (GMR) estimates from the underlying model.data_3_formulations = dataset("bioequivalence/RST_RTS_SRT_STR_TRS_TSR/PJ2017_4_5") auc_result = pumas_be(data_3_formulations, endpoint = :AUC)
- Extract the coefficient vector from the model using
coef(auc_result.model)
. - Identify the coefficients corresponding to the formulation effects for
S
andT
(Hint: look at thecoeftable
). Remember thatR
is the reference level. - Using the extracted coefficients, manually calculate the GMR as a percentage for
S vs. R
andT vs. R
by applying100 * exp(coefficient)
. - Compare your calculated GMRs to the
GMR (%)
values reported in thepumas_be
output table. Do they match?
- Extract the coefficient vector from the model using
Degrees of Freedom and Model Rank For a fixed-effects model, the residual degrees of freedom (DOF) are crucial for inference. They are calculated as the number of observations minus the rank of the design matrix. Let’s investigate this with the
auc_result
model from Exercise 1, which has missing data.- Determine the number of observations used in the model by finding the number of rows in
auc_result.data
. - Determine the number of coefficients in the model using
length(coef(auc_result.model))
. - Calculate the “naive” degrees of freedom by subtracting the number of coefficients from the number of observations.
- Now, find the true degrees of freedom by first getting the design matrix
X = auc_result.model.mm.m
and then calculatingn - rank(X)
, wheren
is the number of observations. Compare this to the value from part (c). - Verify your result from part (d) by calling
dof_residual(auc_result.model)
. Why is using the matrix rank more robust than simply counting coefficients, especially when data is missing?
- Determine the number of observations used in the model by finding the number of rows in
Comparing Mixed Models: Equal vs. Unequal Variances This unit showed two different mixed models for replicate designs based on the
homogeneity
argument. Let’s explore the difference in their variance components. Use theRTTR_TRRT
dataset.data = dataset(joinpath("bioequivalence", "RTTR_TRRT", "PJ2017_4_3")); result_yes_homogenous = pumas_be(data, endpoint = :Cmax, homogeneity = true) result_non_homogenous = pumas_be(data, endpoint = :Cmax, homogeneity = false)
- Print the model summary for the equal variance case:
println(result_yes_homogenous.model)
. Identify the estimated standard deviations (σ
) for the random effects (id
) and the residual. - Now print the model summary for the unequal variance case:
println(result_non_homogenous.model)
. Identify all the estimated standard deviations (σ
). - Based on the model outputs, explain what the
homogeneity
assumption means in practice. How do the random effect structures differ between the two models? Which model allows for different subject variability on the Test vs. Reference product?
- Print the model summary for the equal variance case:
Manual CI Calculation with Satterthwaite’s Method The confidence interval for a mixed model in
pumas_be
depends on the coefficient estimate, its standard error, and the Satterthwaite degrees of freedom. Let’s manually reconstruct the 90% CI for the unequal variance model (result_non_homogenous
from Exercise 3).- Extract the coefficient for
formulation: T
fromresult_non_homogenous.model
. - Extract the corresponding standard error for that coefficient using
stderror(result_non_homogenous.model)
. - Extract the degrees of freedom from the
pumas_be
output table:result_non_homogenous.result_table[:DF]
. - Calculate the critical t-value using
quantile(TDist(dof), 0.95)
, wheredof
is the value from part (c). - Construct the 90% CI on the log scale:
[coefficient ± t_value * std_error]
. - Convert the interval to the original scale as a percentage and verify that it matches the
90% CI
in thepumas_be
output.
- Extract the coefficient for
Estimating Within-Subject Variability
pumas_be
uses an auxiliary estimator to estimate the within-subject variability for each formulation in a replicate design. We can replicate this using thewithin_subject_variability
function. Use the sameRTTR_TRRT
dataset.- First, preprocess the data for the Cmax endpoint:
data_preprocessed = preprocess_be(data; endpoint = :Cmax)
. - Create two new DataFrames: one containing only observations for formulation
R
(data_R
) and one for formulationT
(data_T
). - Run
within_subject_variability(data_R)
andwithin_subject_variability(data_T)
. This will return aNamedTuple
containingσw
and the degrees of freedomk
. - For both
R
andT
, take theσw
value and convert it to a CV% using thesigma2geocv
function. - Compare your calculated CV% values to the
Within-Subject Variability (CV%)
forR
andT
reported in the output ofpumas_be(data, endpoint = :Cmax)
. Do they match?
- First, preprocess the data for the Cmax endpoint: