Skip to content

Population NCA

Comprehensive documentation for multi-subject non-compartmental analysis including summary statistics and stratification.


Overview

Population NCA applies non-compartmental analysis to multiple subjects simultaneously, generating individual results and summary statistics across the population.


Quick Start

using NeoPKPD, DataFrames

# Multi-subject PK data
data = DataFrame(
    subject_id = repeat(1:12, inner=8),
    time = repeat([0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 12.0, 24.0], 12),
    conc = [...],  # Concentration values
    dose = repeat([100.0], 96)
)

# Run population NCA
pop_result = run_population_nca(data; dose_col=:dose)

# View summary
summary = summarize_population_nca(pop_result)
println(summary)

Data Format

Required Columns

data = DataFrame(
    subject_id = [1, 1, 1, 2, 2, 2, ...],  # Subject identifier
    time = [0.0, 1.0, 4.0, 0.0, 1.0, 4.0, ...],  # Time points
    conc = [0.0, 5.2, 2.1, 0.0, 4.8, 1.9, ...],  # Concentrations
    dose = [100.0, 100.0, 100.0, 150.0, ...]      # Dose per subject
)

Optional Columns

# With covariates and grouping
data = DataFrame(
    subject_id = [...],
    time = [...],
    conc = [...],
    dose = [...],
    weight = [70.0, 85.0, ...],        # Body weight
    sex = ["M", "F", ...],              # Sex
    formulation = ["Test", "Ref", ...], # Treatment group
    period = [1, 1, 2, 2, ...]          # Study period
)

Running Population NCA

Basic Usage

pop_result = run_population_nca(
    data;
    subject_col = :subject_id,
    time_col = :time,
    conc_col = :conc,
    dose_col = :dose
)

# Access individual results
for (subj, result) in pop_result.individual_results
    println("Subject $subj: AUC=$(result.auc_0_inf), Cmax=$(result.cmax)")
end

With Configuration

config = NCAConfig(
    method = LinLogMixedMethod(),
    lambda_z_min_points = 3,
    lambda_z_r2_threshold = 0.9,
    lloq = 0.05,
    blq_handling = BLQMissing()
)

pop_result = run_population_nca(
    data;
    config = config,
    route = :extravascular
)

Multiple Dose Analysis

pop_result = run_population_nca(
    data;
    dosing_type = :multiple,
    tau = 24.0,  # Dosing interval
    dose_col = :dose
)

Summary Statistics

Generate Summary

summary = summarize_population_nca(pop_result)

# Access statistics
println("=== AUC0-inf Summary ===")
println("N: $(summary.auc_0_inf.n)")
println("Mean: $(summary.auc_0_inf.mean)")
println("SD: $(summary.auc_0_inf.sd)")
println("CV%: $(summary.auc_0_inf.cv_pct)")
println("Median: $(summary.auc_0_inf.median)")
println("Min: $(summary.auc_0_inf.min)")
println("Max: $(summary.auc_0_inf.max)")
println("Geometric Mean: $(summary.auc_0_inf.geomean)")
println("Geometric CV%: $(summary.auc_0_inf.geocv_pct)")

Summary Table Format

# Get formatted summary table
table = population_nca_summary_table(pop_result)

# Output format:
# Parameter     N    Mean    SD     CV%    Median   Min    Max    GeoMean  GeoCV%
# ---------------------------------------------------------------------------
# Cmax          12   5.23    1.12   21.4   5.15     3.42   7.81   5.12     22.1
# Tmax          12   1.25    0.35   28.0   1.00     0.50   2.00   -        -
# AUC0-t        12   45.2    8.7    19.3   44.1     31.2   62.5   44.5     19.8
# AUC0-inf      12   48.1    9.2    19.1   47.2     33.5   68.2   47.4     19.5
# t1/2          12   4.52    0.85   18.8   4.35     3.12   6.21   4.45     19.2
# CL/F          12   2.12    0.42   19.8   2.08     1.47   3.01   2.08     20.1

Custom Statistics

# Select specific parameters
summary = summarize_population_nca(
    pop_result;
    parameters = [:cmax, :auc_0_inf, :t_half, :cl_f]
)

# Include percentiles
summary = summarize_population_nca(
    pop_result;
    percentiles = [5, 25, 50, 75, 95]
)

Stratified Analysis

By Single Variable

# Stratify by formulation
stratified = stratified_population_nca(
    data,
    strat_col = :formulation
)

for (stratum, result) in stratified
    println("=== $stratum ===")
    summary = summarize_population_nca(result)
    println("AUC0-inf mean: $(summary.auc_0_inf.mean)")
end

By Multiple Variables

# Stratify by formulation and fed/fasted
stratified = stratified_population_nca(
    data,
    strat_cols = [:formulation, :fed_state]
)

# Results grouped by combination
# e.g., ("Test", "Fed"), ("Test", "Fasted"), ("Ref", "Fed"), ("Ref", "Fasted")

Comparison Between Strata

# Compare Test vs Reference
test_summary = summarize_population_nca(stratified["Test"])
ref_summary = summarize_population_nca(stratified["Ref"])

# Geometric mean ratio
gmr_auc = test_summary.auc_0_inf.geomean / ref_summary.auc_0_inf.geomean
gmr_cmax = test_summary.cmax.geomean / ref_summary.cmax.geomean

println("AUC GMR: $(round(gmr_auc * 100, digits=2))%")
println("Cmax GMR: $(round(gmr_cmax * 100, digits=2))%")

Individual Results Access

Accessing All Results

pop_result = run_population_nca(data)

# Iterate through individuals
for (subject_id, result) in pop_result.individual_results
    println("Subject $subject_id:")
    println("  Cmax: $(result.cmax) at Tmax: $(result.tmax)")
    println("  AUC0-t: $(result.auc_0_t)")
    println("  AUC0-inf: $(result.auc_0_inf)")
    println("  t1/2: $(result.t_half)")
    println("  Lambda_z R²: $(result.lambda_z_result.r_squared)")
    println("")
end

Export to DataFrame

# Convert results to DataFrame
results_df = population_nca_to_dataframe(pop_result)

# Columns: subject_id, cmax, tmax, auc_0_t, auc_0_inf, t_half, cl_f, vz_f, ...

Filter by Quality

# Only include subjects with good lambda_z fit
quality_results = filter(pop_result.individual_results) do (_, result)
    !isnothing(result.lambda_z_result) &&
    result.lambda_z_result.r_squared >= 0.9 &&
    result.auc_extra_pct <= 20.0
end

println("Subjects meeting quality criteria: $(length(quality_results))/$(length(pop_result.individual_results))")

Dose Normalization

Normalize by Dose

# Dose-normalized population NCA
pop_result = run_population_nca(
    data;
    dose_normalize = true,
    dose_col = :dose
)

# Access normalized values
for (subj, result) in pop_result.individual_results
    println("Subject $subj: AUC/D = $(result.auc_dn), Cmax/D = $(result.cmax_dn)")
end

Weight-Normalized Dose

# Dose per kg body weight
data.dose_per_kg = data.dose ./ data.weight

pop_result = run_population_nca(
    data;
    dose_col = :dose_per_kg,
    dose_normalize = true
)

Handling Missing Data

Subjects with Insufficient Data

pop_result = run_population_nca(data; config=config)

# Check for failed subjects
if !isempty(pop_result.failed_subjects)
    println("Subjects with NCA failures:")
    for (subj, reason) in pop_result.failed_subjects
        println("  Subject $subj: $reason")
    end
end

Partial Results

# Some parameters may be missing for individual subjects
for (subj, result) in pop_result.individual_results
    if isnan(result.auc_0_inf)
        println("Subject $subj: AUC0-inf not calculable (lambda_z failed)")
    end
end

Covariate Analysis

Summary by Covariate

# Summarize by weight category
data.wt_cat = ifelse.(data.weight .< 70, "Low", "High")

stratified = stratified_population_nca(data, strat_col=:wt_cat)

for (cat, result) in stratified
    summary = summarize_population_nca(result)
    println("$cat weight: CL/F = $(summary.cl_f.mean) ± $(summary.cl_f.sd)")
end

Correlation Analysis

# Extract PK parameters and covariates
results_df = population_nca_to_dataframe(pop_result)

# Merge with covariate data
merged = leftjoin(results_df, unique(data[:, [:subject_id, :weight, :age]]), on=:subject_id)

# Calculate correlations
using Statistics
r_cl_wt = cor(merged.cl_f, merged.weight)
r_vz_wt = cor(merged.vz_f, merged.weight)

println("CL/F vs Weight correlation: $(round(r_cl_wt, digits=3))")
println("Vz/F vs Weight correlation: $(round(r_vz_wt, digits=3))")

Example: Complete Population NCA

using NeoPKPD, DataFrames

# Load multi-subject PK data
data = DataFrame(
    subject_id = repeat(1:24, inner=10),
    time = repeat([0.0, 0.25, 0.5, 1.0, 2.0, 4.0, 6.0, 8.0, 12.0, 24.0], 24),
    conc = [...],  # 240 concentration values
    dose = repeat([500.0], 240),
    weight = repeat([65.0, 72.0, 80.0, ...], inner=10),  # 24 weights
    formulation = repeat(["Test", "Reference"], inner=120)
)

# Configure NCA
config = NCAConfig(
    method = LinLogMixedMethod(),
    lambda_z_min_points = 3,
    lambda_z_r2_threshold = 0.9,
    extrapolation_max_pct = 20.0,
    lloq = 0.1
)

# Run population NCA
pop_result = run_population_nca(
    data;
    config = config,
    route = :extravascular,
    dose_col = :dose
)

# Overall summary
println("=== Population NCA Summary ===")
println("Total subjects: $(length(pop_result.individual_results))")
println("Failed subjects: $(length(pop_result.failed_subjects))")

summary = summarize_population_nca(pop_result)
println("\nParameter    N    Mean±SD         CV%    GeoMean  GeoCV%")
println("-" ^ 60)
for param in [:cmax, :tmax, :auc_0_t, :auc_0_inf, :t_half, :cl_f, :vz_f]
    s = getfield(summary, param)
    if param == :tmax
        @printf("%-10s  %d   %.2f±%.2f    %.1f%%   -        -\n",
            string(param), s.n, s.mean, s.sd, s.cv_pct)
    else
        @printf("%-10s  %d   %.2f±%.2f    %.1f%%   %.2f     %.1f%%\n",
            string(param), s.n, s.mean, s.sd, s.cv_pct, s.geomean, s.geocv_pct)
    end
end

# Stratified by formulation
println("\n=== By Formulation ===")
stratified = stratified_population_nca(data, strat_col=:formulation)

for form in ["Test", "Reference"]
    s = summarize_population_nca(stratified[form])
    println("\n$form:")
    println("  AUC0-inf: $(round(s.auc_0_inf.geomean, digits=2)) (GeoCV $(round(s.auc_0_inf.geocv_pct, digits=1))%)")
    println("  Cmax:     $(round(s.cmax.geomean, digits=2)) (GeoCV $(round(s.cmax.geocv_pct, digits=1))%)")
end

# GMR calculation
test_s = summarize_population_nca(stratified["Test"])
ref_s = summarize_population_nca(stratified["Reference"])
gmr_auc = test_s.auc_0_inf.geomean / ref_s.auc_0_inf.geomean
gmr_cmax = test_s.cmax.geomean / ref_s.cmax.geomean

println("\n=== Geometric Mean Ratios (Test/Reference) ===")
println("AUC GMR: $(round(gmr_auc * 100, digits=2))%")
println("Cmax GMR: $(round(gmr_cmax * 100, digits=2))%")

# Quality assessment
println("\n=== Quality Assessment ===")
n_good_lz = count(r -> r.lambda_z_result.r_squared >= 0.9, values(pop_result.individual_results))
n_low_extrap = count(r -> r.auc_extra_pct <= 20.0, values(pop_result.individual_results))
total = length(pop_result.individual_results)

println("Lambda_z R² ≥ 0.9: $n_good_lz/$total ($(round(100*n_good_lz/total, digits=1))%)")
println("AUC extrap ≤ 20%: $n_low_extrap/$total ($(round(100*n_low_extrap/total, digits=1))%)")

# Export for further analysis
results_df = population_nca_to_dataframe(pop_result)
println("\nResults exported to DataFrame with $(nrow(results_df)) subjects")

Output Formats

Summary Statistics Structure

struct NCAParameterSummary
    n::Int              # Number of subjects
    mean::Float64       # Arithmetic mean
    sd::Float64         # Standard deviation
    cv_pct::Float64     # CV% (100 * SD/mean)
    median::Float64     # Median
    min::Float64        # Minimum
    max::Float64        # Maximum
    geomean::Float64    # Geometric mean
    geocv_pct::Float64  # Geometric CV%
    percentiles::Dict{Int,Float64}  # Requested percentiles
end

Export Formats

# CSV export
population_nca_to_csv(pop_result, "nca_results.csv")

# JSON export
population_nca_to_json(pop_result, "nca_results.json")

# DataFrame (in-memory)
df = population_nca_to_dataframe(pop_result)

See Also