Skip to content

Bioequivalence Analysis

Bioequivalence (BE) assessment following FDA, EMA, and Health Canada guidance.


Overview

from neopkpd.nca import bioequivalence_90ci, tost_analysis, be_conclusion

# Calculate 90% CI for GMR
ci = bioequivalence_90ci(test_values, reference_values)
print(f"90% CI: ({ci.lower:.3f}, {ci.upper:.3f})")
print(f"BE: {ci.be_met}")

Quick Start

from neopkpd.nca import bioequivalence_90ci

# AUC values from crossover study
test_auc = [45.2, 52.1, 38.7, 61.3, 49.8, 55.4, 42.1, 58.9]
ref_auc = [48.1, 55.3, 41.2, 58.9, 52.4, 53.2, 44.8, 60.1]

# Calculate 90% confidence interval
ci = bioequivalence_90ci(test_auc, ref_auc)

print(f"GMR: {ci.gmr * 100:.2f}%")
print(f"90% CI: [{ci.lower * 100:.2f}%, {ci.upper * 100:.2f}%]")
print(f"Within-subject CV: {ci.cv_within * 100:.1f}%")
print(f"BE criteria met: {ci.be_met}")

90% Confidence Interval

Basic Calculation

from neopkpd.nca import bioequivalence_90ci

ci = bioequivalence_90ci(test_values, ref_values)

# Results
print(f"Geometric Mean Ratio: {ci.gmr * 100:.2f}%")
print(f"90% CI Lower: {ci.lower * 100:.2f}%")
print(f"90% CI Upper: {ci.upper * 100:.2f}%")
print(f"BE Met (80-125%): {ci.be_met}")

Result Attributes

class BioequivalenceResult:
    gmr: float           # Geometric mean ratio (Test/Reference)
    lower: float         # 90% CI lower bound
    upper: float         # 90% CI upper bound
    be_met: bool         # True if within 80-125%
    cv_within: float     # Within-subject CV
    n_subjects: int      # Number of subjects
    se: float            # Standard error of log GMR

Custom Acceptance Limits

# Custom limits for narrow therapeutic index drugs
ci = bioequivalence_90ci(
    test_values, ref_values,
    lower_limit=0.90,  # 90%
    upper_limit=1.11   # 111%
)

TOST Analysis

Two One-Sided Tests for equivalence:

from neopkpd.nca import tost_analysis

result = tost_analysis(
    test_values,
    ref_values,
    theta_lower=0.80,
    theta_upper=1.25,
    alpha=0.05
)

print(f"Lower test p-value: {result.p_lower:.4f}")
print(f"Upper test p-value: {result.p_upper:.4f}")
print(f"BE concluded: {result.be_concluded}")

TOST Result

class TOSTResult:
    t_lower: float       # t-statistic for lower bound
    t_upper: float       # t-statistic for upper bound
    p_lower: float       # p-value for lower test
    p_upper: float       # p-value for upper test
    be_concluded: bool   # True if both p < alpha

Geometric Mean Ratio

from neopkpd.nca import geometric_mean_ratio

gmr = geometric_mean_ratio(test_values, ref_values)
print(f"GMR: {gmr * 100:.2f}%")

# Point estimate only (no CI)

Within-Subject CV

from neopkpd.nca import within_subject_cv

# From crossover data
cv = within_subject_cv(
    data,
    value_col='auc',
    subject_col='subject_id',
    period_col='period'
)

print(f"Within-subject CV: {cv * 100:.1f}%")

# Classification
if cv < 0.30:
    print("Standard variability drug")
elif cv < 0.40:
    print("Moderately variable drug")
else:
    print("Highly variable drug (HVD)")

Study Designs

2x2 Crossover (Standard)

from neopkpd.nca import analyze_crossover_be

result = analyze_crossover_be(
    data,
    treatment_col='formulation',
    subject_col='subject_id',
    period_col='period',
    sequence_col='sequence',
    response_col='auc',
    design='2x2'
)

print(f"GMR: {result.gmr * 100:.2f}%")
print(f"90% CI: [{result.lower * 100:.2f}%, {result.upper * 100:.2f}%]")
print(f"Period effect p-value: {result.period_pvalue:.4f}")
print(f"Sequence effect p-value: {result.sequence_pvalue:.4f}")

Replicate Designs

# 2x4 Replicate (for HVD)
result = analyze_crossover_be(
    data,
    design='2x4_replicate',
    response_col='auc'
)

# Partial replicate 3x3
result = analyze_crossover_be(
    data,
    design='3x3_partial',
    response_col='auc'
)

Parallel Group

result = analyze_parallel_be(
    data,
    treatment_col='formulation',
    subject_col='subject_id',
    response_col='auc'
)

Reference-Scaled BE (HVD)

RSABE (FDA)

For highly variable drugs (CV > 30%):

from neopkpd.nca import rsabe_analysis

result = rsabe_analysis(
    test_values,
    ref_values,
    design='2x4_replicate'
)

print(f"Reference CV: {result.cv_reference * 100:.1f}%")
print(f"Scaling applied: {result.scaling_applied}")
print(f"Scaled criterion met: {result.scaled_criterion_met}")
print(f"GMR constraint (80-125%): {result.gmr_constraint_met}")
print(f"RSABE conclusion: {result.be_met}")

ABEL (EMA)

Average Bioequivalence with Expanding Limits:

from neopkpd.nca import abel_analysis

result = abel_analysis(
    test_values,
    ref_values,
    design='2x4_replicate'
)

print(f"Reference CV: {result.cv_reference * 100:.1f}%")
print(f"Widened limits: [{result.lower_limit * 100:.2f}%, {result.upper_limit * 100:.2f}%]")
print(f"GMR within limits: {result.within_widened}")
print(f"GMR constraint (80-125%): {result.gmr_constraint_met}")
print(f"ABEL conclusion: {result.be_met}")

Sample Size Calculation

For BE Study

from neopkpd.nca import be_sample_size

n = be_sample_size(
    cv=0.25,           # Expected within-subject CV (25%)
    gmr=0.95,          # Expected GMR
    power=0.80,        # Target power
    alpha=0.05,        # Significance level
    design='2x2'       # Study design
)

print(f"Required subjects per sequence: {n}")
print(f"Total subjects: {2 * n}")

Power Calculation

from neopkpd.nca import be_power

power = be_power(
    n=24,
    cv=0.25,
    gmr=0.95,
    design='2x2'
)

print(f"Expected power: {power * 100:.1f}%")

Sample Size Table

CV% GMR=0.95 GMR=0.90 GMR=1.00
15 10 14 8
20 16 24 12
25 24 36 18
30 34 50 26
35 46 68 36

Complete BE Analysis

import pandas as pd
from neopkpd.nca import (
    run_population_nca, bioequivalence_90ci,
    tost_analysis, within_subject_cv, NCAConfig
)

# Load crossover study data
data = pd.read_csv('be_study_data.csv')

# Run NCA for all subjects
config = NCAConfig(
    method="lin_log_mixed",
    lambda_z_min_points=3,
    lambda_z_r2_threshold=0.9
)

pop_result = run_population_nca(
    data,
    subject_col='SUBJID',
    time_col='TIME',
    conc_col='CONC',
    dose_col='DOSE',
    config=config
)

# Get NCA results by formulation
results_df = pop_result.to_dataframe()
results_df = results_df.merge(
    data[['SUBJID', 'FORMULATION']].drop_duplicates(),
    on='SUBJID'
)

test_auc = results_df[results_df['FORMULATION'] == 'Test']['auc_0_inf'].values
ref_auc = results_df[results_df['FORMULATION'] == 'Reference']['auc_0_inf'].values
test_cmax = results_df[results_df['FORMULATION'] == 'Test']['cmax'].values
ref_cmax = results_df[results_df['FORMULATION'] == 'Reference']['cmax'].values

# BE Analysis for AUC
print("=" * 60)
print("BIOEQUIVALENCE ANALYSIS")
print("=" * 60)

print("\n--- AUC0-inf ---")
auc_be = bioequivalence_90ci(test_auc, ref_auc)
print(f"N: {auc_be.n_subjects}")
print(f"GMR: {auc_be.gmr * 100:.2f}%")
print(f"90% CI: [{auc_be.lower * 100:.2f}%, {auc_be.upper * 100:.2f}%]")
print(f"Within-subject CV: {auc_be.cv_within * 100:.1f}%")
print(f"BE criteria met: {'YES' if auc_be.be_met else 'NO'}")

# BE Analysis for Cmax
print("\n--- Cmax ---")
cmax_be = bioequivalence_90ci(test_cmax, ref_cmax)
print(f"N: {cmax_be.n_subjects}")
print(f"GMR: {cmax_be.gmr * 100:.2f}%")
print(f"90% CI: [{cmax_be.lower * 100:.2f}%, {cmax_be.upper * 100:.2f}%]")
print(f"Within-subject CV: {cmax_be.cv_within * 100:.1f}%")
print(f"BE criteria met: {'YES' if cmax_be.be_met else 'NO'}")

# Overall Conclusion
print("\n" + "=" * 60)
overall_be = auc_be.be_met and cmax_be.be_met
print(f"OVERALL BIOEQUIVALENCE: {'ESTABLISHED' if overall_be else 'NOT ESTABLISHED'}")
print("=" * 60)

# TOST confirmation
print("\n--- TOST Analysis ---")
auc_tost = tost_analysis(test_auc, ref_auc)
print(f"AUC: Lower p={auc_tost.p_lower:.4f}, Upper p={auc_tost.p_upper:.4f}")
print(f"     BE concluded: {auc_tost.be_concluded}")

cmax_tost = tost_analysis(test_cmax, ref_cmax)
print(f"Cmax: Lower p={cmax_tost.p_lower:.4f}, Upper p={cmax_tost.p_upper:.4f}")
print(f"      BE concluded: {cmax_tost.be_concluded}")

Regulatory Reports

FDA-Style Report

from neopkpd.nca import generate_be_report

report = generate_be_report(
    test_auc, ref_auc, test_cmax, ref_cmax,
    regulatory='FDA',
    study_design='2x2_crossover'
)

print(report)

EMA-Style Report

report = generate_be_report(
    test_auc, ref_auc, test_cmax, ref_cmax,
    regulatory='EMA',
    study_design='2x2_crossover',
    include_tmax=True  # EMA includes Tmax
)

print(report)

Highly Variable Drug Analysis

from neopkpd.nca import (
    within_subject_cv, rsabe_analysis, abel_analysis
)

# Check if HVD criteria met
cv = within_subject_cv(data, 'auc', 'subject_id', 'period')
print(f"Within-subject CV: {cv * 100:.1f}%")

if cv > 0.30:
    print("HVD criteria met - applying reference scaling")

    # FDA RSABE
    rsabe = rsabe_analysis(
        test_values, ref_values,
        design='2x4_replicate'
    )
    print(f"\nRSABE (FDA): {rsabe.be_met}")

    # EMA ABEL
    abel = abel_analysis(
        test_values, ref_values,
        design='2x4_replicate'
    )
    print(f"ABEL (EMA): {abel.be_met}")
else:
    print("Standard ABE analysis applicable")
    be = bioequivalence_90ci(test_values, ref_values)
    print(f"ABE: {be.be_met}")

Acceptance Criteria

Standard BE (80-125%)

Parameter Acceptance Range
AUC 80.00% - 125.00%
Cmax 80.00% - 125.00%

Narrow Therapeutic Index (90-111%)

# NTI drugs (warfarin, phenytoin, etc.)
be = bioequivalence_90ci(
    test_values, ref_values,
    lower_limit=0.90,
    upper_limit=1.11
)

Highly Variable Drugs (Widened)

Reference CV EMA Widened Range
30-50% Calculated per CV
>50% 69.84% - 143.19%

See Also