Skip to content

stats.bioequivalence

Average bioequivalence: the two one-sided tests on the geometric mean ratio.

Test and reference are bioequivalent when the 90 % confidence interval of the geometric mean ratio of the exposure lies within 80-125 % (FDA 2026, EMA 2010, ICH M13A 2024), which is the two one-sided tests procedure of Schuirmann (1987) at \(\alpha = 0.05\). The interval comes from the design of the study: a 2x2 crossover (each subject receives both formulations in two periods, in one of two sequences) is analysed with the period differences of Chow & Liu (2009, ch. 3), which is the analysis of variance with sequence, period and subject-within-sequence effects on the log scale; a paired design uses the within-subject differences; parallel groups use the Welch interval.

A replicate design, in which at least one formulation is given twice (Design.REPLICATE, the sequences TRTR/RTRT, TRT/RTR and TRRT/RTTR), is analysed with the fixed effects analysis of variance of the log values with sequence, subject within sequence, period and formulation, which is Method A of the EMA; it separates the within-subject variability of the reference (cv_intra_r) from the one of the test (cv_intra_t) and is what the reference-scaled acceptance criteria need. bioequivalence(..., scaling=...) applies them: the average bioequivalence with expanding limits of the EMA (ABEL), the reference-scaled average bioequivalence of the FDA (RSABE) and the two narrow therapeutic index rules. The mixed model (Method B of the EMA, the FDA model) is out of scope; it needs a restricted maximum likelihood fit which the dependencies of the package do not carry.

Design

Bases: StrEnum

Design of a bioequivalence study.

BEParameter dataclass

BEParameter(
    name,
    unit,
    gmr,
    ci_low,
    ci_high,
    ci_level,
    limits,
    bioequivalent,
    p_lower,
    p_upper,
    p_value,
    log_ratio,
    se_log,
    df,
    design,
    cv_intra,
    p_period,
    p_sequence,
    n_test,
    n_reference,
    carryover=(),
    cv_intra_r=float("nan"),
    cv_intra_t=float("nan"),
    scaled=False,
    limits_scaled=None,
    criterion=None,
    sd_ratio_upper=float("nan"),
    anova=None,
)

Bioequivalence of one parameter.

Attributes:

Name Type Description
name str

name of the parameter

unit str

unit of the parameter

gmr float

geometric mean ratio test / reference

ci_low float

lower bound of the interval of the ratio

ci_high float

upper bound of the interval

ci_level float

level of the interval

limits tuple[float, float]

acceptance limits of the ratio

bioequivalent bool

whether the interval lies within the limits

p_lower float

p value of the test against the lower limit

p_upper float

p value of the test against the upper limit

p_value float

the larger of the two, the p value of the TOST procedure

log_ratio float

\(\ln \mathrm{GMR}\)

se_log float

standard error of log_ratio

df float

degrees of freedom

design Design

the design of the analysis

cv_intra float

within-subject coefficient of variation \(\sqrt{e^{\sigma_e^2} - 1}\), NaN for a parallel design

p_period float

p value of the period effect (crossover), NaN otherwise

p_sequence float

p value of the sequence (carryover) effect (crossover), NaN otherwise

n_test int

number of test values

n_reference int

number of reference values

carryover tuple[str, ...]

the subjects whose pre-dose concentration exceeds the carryover threshold (carryover_table), either flagged here or, with carryover="exclude", already dropped from the analysis

cv_intra_r float

within-subject CV of the reference formulation alone, from its replicates; NaN unless the design is REPLICATE

cv_intra_t float

within-subject CV of the test formulation alone; NaN unless the test is replicated too

scaled bool

whether the acceptance rule was derived from the variability of the reference or replaced by a narrow therapeutic index rule (scaling), so that limits is no longer the requested one

limits_scaled tuple[float, float] | None

the derived limits, None for an unscaled analysis and for the criterion of the FDA, which has no limits; limits always carries the limits the verdict was taken against

criterion float | None

the upper confidence bound of the FDA scaled criterion, \(\le 0\) for a bioequivalent formulation; None for every other analysis

sd_ratio_upper float

the upper 90 % bound of \(s_{wT}/s_{wR}\) of the narrow therapeutic index criterion of the FDA, NaN otherwise

anova DataFrame | None

the analysis of variance table of a replicate design (source, df, sum_sq, mean_sq, f, p_value), None otherwise

to_dict

to_dict()

The fields as a dictionary with the enumerations as strings.

Returns:

Type Description
dict[str, Any]

Field name to value.

BEResult dataclass

BEResult(parameters, bioequivalent, limits, ci_level)

Bioequivalence of several parameters.

Attributes:

Name Type Description
parameters dict[str, BEParameter]

parameter name to its result

bioequivalent bool

whether every parameter is bioequivalent

limits tuple[float, float]

the acceptance limits which were requested, the limits argument of bioequivalence; a reference-scaled or tightened rule derives its own limits per parameter, which are in BEParameter.limits and BEParameter.limits_scaled

ci_level float

the level of the intervals

to_dict

to_dict()

The result of every parameter as a dictionary, keyed by its name.

Returns:

Type Description
dict[str, dict[str, Any]]

Parameter name to the fields of its BEParameter.

to_dataframe

to_dataframe()

One row per parameter.

Returns:

Type Description
DataFrame

The dataframe.

abel_limits

abel_limits(cv_intra_r)

The expanding limits of the EMA for a within-subject CV of the reference.

\[\theta_{U} = e^{k\,s_{wR}}, \qquad \theta_{L} = 1/\theta_{U}, \qquad s_{wR} = \sqrt{\ln(1 + \mathrm{CV}_{wR}^2)},\]

with \(k = 0.760\); the CV is capped at 50 % before it is used, so that the limits never leave 69.84-143.19 % (EMA 2010, 4.1.10). At or below a CV of 30 % the EMA does not widen at all and the limits stay 80.00-125.00 %, which is the switching condition PowerTOST::scABEL uses as well: the formula would give 80.003-124.995 % just above 30 % and so return limits slightly narrower than the unscaled ones.

Parameters:

Name Type Description Default
cv_intra_r float

the within-subject coefficient of variation of the reference formulation, as a fraction.

required

Returns:

Type Description
tuple[float, float]

The lower and the upper acceptance limit.

Raises:

Type Description
ValueError

if cv_intra_r is not a finite, non-negative number.

rsabe_criterion

rsabe_criterion(
    difference,
    se_difference,
    df_difference,
    s2_wr,
    df_wr,
    *,
    sigma_w0=FDA_SIGMA_W0,
    delta=FDA_DELTA,
    alpha=0.05,
)

The upper confidence bound of the scaled criterion of the FDA.

The linearized criterion is

\[(\mu_T - \mu_R)^2 - \theta\,\sigma_{wR}^2 \le 0, \qquad \theta = \left(\frac{\ln \Delta}{\sigma_{w0}}\right)^2,\]

and its upper \(1-\alpha\) confidence bound is Howe's approximation, the point estimate plus the root of the squared distances of the one-sided bounds of its two parts (FDA progesterone guidance 2011):

\[U = \hat E + \sqrt{\left(\left(|\hat d| + t_{1-\alpha,\nu_d}\, \mathrm{se}_d\right)^2 - \hat d^2\right)^2 + \left(\theta s_{wR}^2 - \theta s_{wR}^2 \frac{\nu_R}{\chi^2_{1-\alpha,\nu_R}}\right)^2}, \qquad \hat E = \hat d^2 - \theta s_{wR}^2.\]

The formulations pass the criterion when \(U \le 0\).

Parameters:

Name Type Description Default
difference float

the estimate \(\hat d\) of \(\ln \mathrm{GMR}\).

required
se_difference float

its standard error.

required
df_difference float

its degrees of freedom.

required
s2_wr float

the within-subject variance of the reference.

required
df_wr float

its degrees of freedom.

required

Other Parameters:

Name Type Description
sigma_w0 float

the regulatory standard deviation of the criterion, 0.25 for a highly variable drug and 0.10 for a narrow therapeutic index drug.

delta float

the bioequivalence limit the criterion is built from.

alpha float

the level of the one-sided bound.

Returns:

Type Description
float

The upper bound U, NaN when the study carries no estimate of the

float

difference or of the reference variance.

tost

tost(
    test,
    reference,
    *,
    limits=(0.8, 1.25),
    ci_level=0.9,
    design=None,
    scaling="none",
    scaled_parameters=DEFAULT_SCALED_PARAMETERS,
)

Two one-sided tests of the geometric mean ratio against the acceptance limits.

\(t_L = (\ln \mathrm{GMR} - \ln \theta_L) / \mathrm{se}\), \(t_U = (\ln \theta_U - \ln \mathrm{GMR}) / \mathrm{se}\), each tested one-sided with the degrees of freedom of the design at \(\alpha = (1 - \mathrm{ci\_level}) / 2\); rejecting both is the same as the interval at ci_level lying within the limits (Schuirmann 1987). Without a standard error (a single subject, or two samples without a within-subject difference) the two tests are undefined: the p values and the interval are NaN and the parameter is not bioequivalent, as in compare.

A REPLICATE design is analysed with the fixed effects analysis of variance of _replicate (EMA Method A) and reports cv_intra_r, cv_intra_t and the anova table with the ratio; n_test and n_reference count the administrations there, not the subjects, since a subject carries several of each. A subject who misses a period is kept and the unbalanced design is fitted as it is, which is what the least squares fit is for; the subjects with fewer administrations than their sequence asks for are named in a log line at info level.

scaling replaces the acceptance rule by one of the reference-scaled rules of the guidances:

value rule
"none" the 90 % interval within limits, the default
"ema" average bioequivalence with expanding limits (ABEL): for cmax alone, and only above a cv_intra_r of 30 %, the limits widen to \(e^{\pm 0.760 s_{wR}}\) (capped at 69.84-143.19 %) and the point estimate must lie within 80.00-125.00 %
"fda" reference-scaled average bioequivalence (RSABE): above \(s_{wR} = 0.294\) the upper 95 % bound of \((\mu_T-\mu_R)^2 - \theta s_{wR}^2\) must not be positive and the point estimate must lie within 80.00-125.00 %; below it the unscaled analysis decides
"fda_nti" the same criterion with \(\sigma_{w0} = 0.10\) and always scaled, plus the unscaled 90 % interval within 80.00-125.00 % and the upper 90 % bound of \(s_{wT}/s_{wR}\) at most 2.500
"ema_nti" the limits tightened to 90.00-111.11 %

Every rule but "ema_nti" needs the within-subject variability of the reference and therefore a replicate design. limits carries the limits the verdict was taken against, limits_scaled the derived ones and criterion the bound of the FDA rule, which has no limits at all; BEResult.limits stays the limits which were requested.

The point estimate of the two rules of the FDA is \(e^{\hat d}\) of the subject-level mean of the within-subject differences, the estimate the criterion itself is built on, and not the formulation effect gmr of the analysis of variance; the two agree on a balanced design and differ on an unbalanced one, and the guidance takes both conditions on the same number. gmr keeps reporting the effect of the analysis of variance either way. The point estimate of the EMA rule is gmr, which is the estimate its interval is built on.

scaled_parameters names the parameters whose limits the EMA widens, ("cmax",) by default; a steady state study whose peak is called cmax_ss passes scaled_parameters=("cmax_ss",). It has no effect on the rules of the FDA, which scale every parameter, or on "ema_nti", which tightens every parameter it is asked for.

Parameters:

Name Type Description Default
test ParameterSample

the test sample.

required
reference ParameterSample

the reference sample.

required
limits tuple[float, float]

acceptance limits of the ratio.

(0.8, 1.25)
ci_level float

level of the interval, 0.90 for the usual \(\alpha = 0.05\).

0.9
design Design | str | None

the design, as the member or as its string, detected from the samples by default.

None
scaling Scaling | str

the acceptance rule, see the table above.

'none'
scaled_parameters Sequence[str]

the parameters whose limits "ema" widens.

DEFAULT_SCALED_PARAMETERS

Returns:

Type Description
BEParameter

The result of the parameter.

Raises:

Type Description
ValueError

for reversed limits, an unknown design or scaling, a design the samples do not support, or a scaling the design cannot carry.

carryover_table

carryover_table(batch, result, *, threshold=0.05)

The pre-dose concentration of every subject against its own maximum.

ICH M13A (2024, 2.2.3.3), the FDA ANDA bioequivalence guidance and the EMA bioequivalence guideline all draw the same line: a period whose pre-dose concentration is more than 5 % of that subject's \(C_\mathrm{max}\) of the same period carries drug from the previous period, and the subject "should be dropped from the study evaluation of that period"; M13A adds that a statistical test for carryover "is not considered relevant", so this comparison replaces it.

The pre-dose value of a sample is the last value strictly before its dose time. A sample recorded at the dose time counts as a pre-dose sample for an extravascular route only, where it is drawn before the dose is swallowed; after an intravenous bolus or during an infusion the value at the dose time is the post-dose value of this period and says nothing about the previous one (reading it would flag every subject with a fraction of 1). A sample whose schedule carries no value before the dose has no pre-dose value: predose is NaN and the sample is not flagged.

Parameters:

Name Type Description Default
batch Timecourses

the timecourses of the period, one sample per subject

required
result ParameterResult

the analysis of that batch, for \(C_\mathrm{max}\)

required

Other Parameters:

Name Type Description
threshold float

the share of \(C_\mathrm{max}\) above which the sample is flagged, 0.05 of the three guidances

Returns:

Type Description
DataFrame

One row per sample with the sample dimensions, predose, cmax,

DataFrame

fraction and flagged.

Raises:

Type Description
ValueError

if the result carries no cmax, or if the batch and the result do not have the same samples.

carryover_labels

carryover_labels(batch, result, *, dim, threshold=0.05)

The labels of the subjects carryover_table flags.

Parameters:

Name Type Description Default
batch Timecourses

the timecourses of the period

required
result ParameterResult

the analysis of that batch

required

Other Parameters:

Name Type Description
dim str

the sample dimension whose labels name the subjects

threshold float

the share of Cmax above which a sample is flagged

Returns:

Type Description
list[str]

The labels, as strings, in the order of the samples.

Raises:

Type Description
ValueError

if dim is no sample dimension of the batch, or as carryover_table.

bioequivalence

bioequivalence(
    test,
    reference,
    parameters=("auc_inf_obs", "cmax"),
    *,
    dim="individual",
    limits=(0.8, 1.25),
    ci_level=0.9,
    design=None,
    scaling="none",
    scaled_parameters=DEFAULT_SCALED_PARAMETERS,
    include_excluded=False,
    carryover="ignore",
    carryover_threshold=0.05,
    test_batch=None,
    reference_batch=None,
    **indexers,
)

Average bioequivalence of the parameters of two results.

Every parameter is taken with ParameterResult.sample(name, dim, **indexers) from both results and tested with tost; the study is bioequivalent when every parameter is. A subject which a result marks excluded (pkpdutils.nca.NCAResult.exclude) is left out unless include_excluded asks for it.

With the timecourses of the two periods (test_batch, reference_batch) the pre-dose concentrations are read as well (carryover_table): carryover="flag" names the subjects above the threshold in BEParameter.carryover and carryover="exclude" drops them from the analysis of every parameter, which is what ICH M13A (2024) and the FDA ANDA guidance ask for.

Parameters:

Name Type Description Default
test ParameterResult

the result of the test formulation.

required
reference ParameterResult

the result of the reference formulation.

required
parameters Sequence[str]

the parameters to test.

('auc_inf_obs', 'cmax')
dim str

the sample dimension of the individuals.

'individual'
limits tuple[float, float]

acceptance limits of the ratio.

(0.8, 1.25)
ci_level float

level of the intervals.

0.9
design Design | str | None

the design, as the member or as its string, detected from the samples by default.

None
scaling Scaling | str

the acceptance rule, "none" for the fixed limits and one of "ema", "fda", "fda_nti", "ema_nti" for the reference-scaled and the narrow therapeutic index rules of the guidances, see tost. "ema_nti" tightens the limits of every parameter it is asked for, which is why the EMA rule for \(C_\mathrm{max}\) ("when it is of particular importance") is expressed by naming cmax in parameters or leaving it out.

'none'
scaled_parameters Sequence[str]

the parameters whose limits scaling="ema" widens, ("cmax",) by default; a steady state study passes ("cmax_ss",).

DEFAULT_SCALED_PARAMETERS
include_excluded bool

analyse the excluded subjects as well.

False
carryover Literal['ignore', 'flag', 'exclude']

what to do with a subject whose pre-dose concentration exceeds carryover_threshold of its own Cmax: "ignore" nothing, "flag" name it in BEParameter.carryover, "exclude" drop it from the analysis and name it there as well.

'ignore'
carryover_threshold float

the share of Cmax which counts as carryover.

0.05
test_batch Timecourses | None

the timecourses the test result was computed from, needed for the carryover check.

None
reference_batch Timecourses | None

the timecourses of the reference result.

None
**indexers Any

coordinate label per remaining sample dimension.

{}

Returns:

Type Description
BEResult

The result.

Raises:

Type Description
ValueError

for an unknown design, for a carryover check without the batches, or as tost.