Skip to content

Parallel Design

Comprehensive guide for parallel group clinical trial simulation with multiple treatment arms.


Overview

Parallel designs assign subjects to independent treatment groups, enabling comparison of interventions without within-subject correlation.

using NeoPKPD

design = ParallelDesign(
    n_arms = 3,
    arm_names = ["Placebo", "Low Dose", "High Dose"],
    randomization_ratio = [1, 1, 1]
)

ParallelDesign Structure

struct ParallelDesign <: TrialDesign
    n_arms::Int                           # Number of treatment arms
    arm_names::Vector{String}             # Names for each arm
    randomization_ratio::Vector{Int}      # Allocation ratio
    stratification_factors::Vector{Symbol} # Stratification variables
    block_size::Int                       # Block randomization size
end

Creating Designs

# Two-arm parallel (1:1)
design = ParallelDesign(
    n_arms = 2,
    arm_names = ["Placebo", "Treatment"],
    randomization_ratio = [1, 1]
)

# Three-arm with 2:1:1 randomization
design = ParallelDesign(
    n_arms = 3,
    arm_names = ["Placebo", "Low", "High"],
    randomization_ratio = [2, 1, 1]
)

# Stratified by sex and age group
design = ParallelDesign(
    n_arms = 2,
    arm_names = ["Control", "Active"],
    randomization_ratio = [1, 1],
    stratification_factors = [:sex, :age_group],
    block_size = 4
)

Dosing Regimens

Standard Frequencies

# Once daily (QD)
qd_regimen = dosing_qd(
    dose = 100.0,
    days = 28,
    loading_dose = nothing
)

# Twice daily (BID)
bid_regimen = dosing_bid(
    dose = 50.0,
    days = 28,
    loading_dose = 100.0  # Double first dose
)

# Three times daily (TID)
tid_regimen = dosing_tid(
    dose = 25.0,
    days = 14
)

# Four times daily (QID)
qid_regimen = dosing_qid(
    dose = 20.0,
    days = 7
)

Custom Regimens

# Custom dose times (hours after midnight)
custom_regimen = dosing_custom(
    dose = 100.0,
    days = 28,
    dose_times = [8.0, 20.0]  # 8 AM and 8 PM
)

# Variable doses
variable_regimen = DosingRegimen(
    doses = [100.0, 50.0, 50.0],
    times = [0.0, 12.0, 24.0],
    repeat_days = 2
)

Titration Regimens

# Gradual dose escalation
titration = TitrationRegimen(
    start_dose = 25.0,
    target_dose = 100.0,
    n_steps = 4,
    days_per_step = 7,
    frequency = :qd,
    loading_dose = nothing,
    maintenance_days = 14
)

# Example schedule:
# Week 1: 25 mg QD
# Week 2: 50 mg QD
# Week 3: 75 mg QD
# Week 4+: 100 mg QD (maintenance)

Virtual Population

Demographic Specification

# Healthy volunteer population
spec = DemographicSpec(
    age_mean = 35.0,
    age_sd = 10.0,
    age_min = 18.0,
    age_max = 55.0,
    weight_mean = 75.0,
    weight_sd = 12.0,
    weight_min = 50.0,
    weight_max = 100.0,
    female_fraction = 0.5,
    race_distribution = Dict(
        "Caucasian" => 0.70,
        "Asian" => 0.15,
        "Black" => 0.10,
        "Other" => 0.05
    )
)

population = generate_virtual_population(spec, 150)

Pre-Built Specifications

# Healthy volunteers (Phase I typical)
spec = healthy_volunteer_spec()
# Age: 30 ± 8 years (18-45)
# Weight: 72 ± 10 kg (55-90)
# 50% female

# Patient population
spec = patient_population_spec(:diabetes)
# Age: 55 ± 12 years
# Weight: 90 ± 18 kg
# Includes HbA1c baseline

# Renal impairment
spec = patient_population_spec(:renal_impairment)
# Includes eGFR distribution

# Hepatic impairment
spec = patient_population_spec(:hepatic_impairment)
# Includes Child-Pugh score

Population Summary

population = generate_virtual_population(spec, 100)
summary = summarize_population(population)

println("Age: $(summary.age_mean) ± $(summary.age_sd) years")
println("Weight: $(summary.weight_mean) ± $(summary.weight_sd) kg")
println("Female: $(summary.female_fraction * 100)%")
println("Race distribution: $(summary.race_distribution)")

Trial Specification

Complete Trial Setup

using NeoPKPD

# Design
design = ParallelDesign(
    n_arms = 3,
    arm_names = ["Placebo", "50 mg", "100 mg"],
    randomization_ratio = [1, 1, 1]
)

# Dosing regimens per arm
regimens = [
    dosing_qd(dose=0.0, days=28),     # Placebo
    dosing_qd(dose=50.0, days=28),    # Low dose
    dosing_qd(dose=100.0, days=28)    # High dose
]

# Population
spec = healthy_volunteer_spec()
population = generate_virtual_population(spec, 150)

# PK model
model = TwoCompOral()
params = TwoCompOralParams(
    Ka = 1.5,
    CL = 10.0,
    V1 = 50.0,
    Q = 5.0,
    V2 = 100.0
)

# Inter-individual variability
omega = OmegaMatrix([
    0.09 0.03 0.0;    # IIV on CL (30% CV)
    0.03 0.04 0.0;    # IIV on V1 (20% CV), correlated with CL
    0.0  0.0  0.16    # IIV on Ka (40% CV)
])

# Residual error (proportional)
sigma = 0.1  # 10% CV

# Create trial specification
trial = TrialSpec(
    name = "Phase 2 Dose Finding Study",
    design = design,
    regimens = regimens,
    population = population,
    pk_model = model,
    pk_params = params,
    omega = omega,
    sigma = sigma,
    observation_times = [0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 12.0, 24.0],
    endpoints = [:cmax, :auc_0_24, :auc_0_inf]
)

Trial Simulation

Running Simulation

# Simulate trial
result = simulate_trial(trial, seed=12345)

# Access results
println("Study: $(result.name)")
println("Enrolled: $(result.n_enrolled)")
println("Completed: $(result.n_completed)")
println("Dropout rate: $(result.dropout_rate * 100)%")

Arm Results

for (arm_name, arm) in result.arms
    println("\n$(arm_name):")
    println("  N completed: $(arm.n_completed)")
    println("  AUC: $(arm.mean_auc) ± $(arm.sd_auc)")
    println("  Cmax: $(arm.mean_cmax) ± $(arm.sd_cmax)")
    println("  CV%: $(arm.cv_auc * 100)%")
end

Individual Subject Data

# Access individual-level data
for subject in result.subjects
    println("Subject $(subject.id):")
    println("  Arm: $(subject.arm)")
    println("  AUC: $(subject.auc)")
    println("  Cmax: $(subject.cmax)")
    println("  Completed: $(subject.completed)")
end

Compliance Modeling

Compliance Patterns

# Random compliance (probabilistic missed doses)
compliance = ComplianceSpec(
    pattern = :random,
    rate = 0.85  # 85% compliance
)

# Weekend-miss pattern
compliance = ComplianceSpec(
    pattern = :weekend_miss,
    weekday_rate = 0.95,
    weekend_rate = 0.70
)

# Decay pattern (decreasing over time)
compliance = ComplianceSpec(
    pattern = :decay,
    initial_rate = 0.95,
    final_rate = 0.75,
    half_time_days = 14
)

# Early-good pattern
compliance = ComplianceSpec(
    pattern = :early_good,
    initial_rate = 0.95,
    later_rate = 0.80,
    transition_day = 14
)

Adding Compliance to Trial

trial = TrialSpec(
    name = "Phase 2 with Compliance",
    design = design,
    regimens = regimens,
    population = population,
    pk_model = model,
    pk_params = params,
    omega = omega,
    sigma = sigma,
    compliance = ComplianceSpec(pattern=:random, rate=0.85)
)

Dropout Modeling

Dropout Specification

# Constant dropout rate
dropout = DropoutSpec(
    rate = 0.15,  # 15% dropout
    pattern = :constant
)

# Time-dependent dropout
dropout = DropoutSpec(
    pattern = :increasing,
    initial_rate = 0.05,
    final_rate = 0.20,
    shape = :linear
)

# Exposure-dependent dropout (adverse events)
dropout = DropoutSpec(
    pattern = :exposure_dependent,
    threshold = 100.0,  # If Cmax > threshold
    rate_below = 0.10,
    rate_above = 0.30
)

Adding Dropout to Trial

trial = TrialSpec(
    ...,
    dropout = DropoutSpec(rate=0.15, pattern=:constant)
)

Statistical Analysis

Arm Comparisons

# Compare active to placebo
comparison = compare_arms(
    result.arms["100 mg"].auc_values,
    result.arms["Placebo"].auc_values,
    arm1_name = "100 mg",
    arm2_name = "Placebo",
    alpha = 0.05
)

println("Mean difference: $(comparison.difference)")
println("95% CI: [$(comparison.ci_lower), $(comparison.ci_upper)]")
println("p-value: $(comparison.pvalue)")
println("Significant: $(comparison.significant)")

Dose-Response Analysis

# Analyze dose-response
doses = [0.0, 50.0, 100.0]
means = [
    result.arms["Placebo"].mean_auc,
    result.arms["50 mg"].mean_auc,
    result.arms["100 mg"].mean_auc
]

# Linear trend test
trend = dose_response_trend(doses, means)
println("Slope: $(trend.slope)")
println("p-value (trend): $(trend.pvalue)")

Responder Analysis

# Count responders (AUC > threshold)
threshold = 50.0  # Target exposure

for (arm_name, arm) in result.arms
    resp = responder_analysis(
        arm.auc_values,
        threshold = threshold,
        direction = :above,
        confidence = 0.95
    )

    println("$(arm_name):")
    println("  Responders: $(resp.n_responders)/$(resp.n_total)")
    println("  Rate: $(resp.rate * 100)% [$(resp.ci_lower*100), $(resp.ci_upper*100)]")
end

Complete Example

using NeoPKPD

# ======================
# Phase 2 Dose Finding Study
# ======================

# 1. Define design
design = ParallelDesign(
    n_arms = 4,
    arm_names = ["Placebo", "25 mg", "50 mg", "100 mg"],
    randomization_ratio = [1, 1, 1, 1],
    stratification_factors = [:sex],
    block_size = 4
)

# 2. Define dosing regimens
regimens = [
    dosing_qd(dose=0.0, days=28),
    dosing_qd(dose=25.0, days=28),
    dosing_qd(dose=50.0, days=28),
    dosing_qd(dose=100.0, days=28)
]

# 3. Generate population
spec = healthy_volunteer_spec()
population = generate_virtual_population(spec, 200)

# 4. Define PK model
model = OneCompOralFirstOrder()
params = OneCompOralFirstOrderParams(
    Ka = 1.2,
    CL = 8.0,
    V = 60.0
)

omega = OmegaMatrix([
    0.09 0.02;    # IIV CL
    0.02 0.04     # IIV V
])

# 5. Create trial specification
trial = TrialSpec(
    name = "Phase 2 Dose Finding",
    design = design,
    regimens = regimens,
    population = population,
    pk_model = model,
    pk_params = params,
    omega = omega,
    sigma = 0.1,
    observation_times = [0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 12.0, 24.0],
    endpoints = [:cmax, :auc_0_24],
    compliance = ComplianceSpec(pattern=:random, rate=0.90),
    dropout = DropoutSpec(rate=0.10, pattern=:constant)
)

# 6. Run simulation
println("Running trial simulation...")
result = simulate_trial(trial, seed=42)

# 7. Report results
println("\n" * "=" ^ 60)
println("PHASE 2 DOSE FINDING STUDY RESULTS")
println("=" ^ 60)

println("\n--- Enrollment Summary ---")
println("Total enrolled: $(result.n_enrolled)")
println("Total completed: $(result.n_completed)")
println("Dropout rate: $(round(result.dropout_rate * 100, digits=1))%")

println("\n--- PK Results by Arm ---")
println("Arm          N    AUC Mean±SD        Cmax Mean±SD")
println("-" ^ 60)

for arm_name in ["Placebo", "25 mg", "50 mg", "100 mg"]
    arm = result.arms[arm_name]
    auc_str = "$(round(arm.mean_auc, digits=1)) ± $(round(arm.sd_auc, digits=1))"
    cmax_str = "$(round(arm.mean_cmax, digits=2)) ± $(round(arm.sd_cmax, digits=2))"
    println("$(rpad(arm_name, 12)) $(arm.n_completed)    $(rpad(auc_str, 18)) $(cmax_str)")
end

# 8. Statistical comparisons
println("\n--- Statistical Comparisons vs Placebo ---")
placebo_auc = result.arms["Placebo"].auc_values

for dose in ["25 mg", "50 mg", "100 mg"]
    comp = compare_arms(
        result.arms[dose].auc_values,
        placebo_auc,
        arm1_name = dose,
        arm2_name = "Placebo"
    )

    sig = comp.significant ? "*" : ""
    println("$(dose) vs Placebo: Δ=$(round(comp.difference, digits=1)), " *
            "95% CI [$(round(comp.ci_lower, digits=1)), $(round(comp.ci_upper, digits=1))], " *
            "p=$(round(comp.pvalue, digits=4))$sig")
end

# 9. Dose-response
println("\n--- Dose-Response Analysis ---")
doses = [0.0, 25.0, 50.0, 100.0]
mean_aucs = [result.arms[arm].mean_auc for arm in ["Placebo", "25 mg", "50 mg", "100 mg"]]
trend = dose_response_trend(doses, mean_aucs)
println("Linear trend slope: $(round(trend.slope, digits=3)) per mg")
println("Trend p-value: $(round(trend.pvalue, digits=4))")

# 10. Responder analysis
println("\n--- Responder Analysis (AUC > 30) ---")
for arm_name in ["Placebo", "25 mg", "50 mg", "100 mg"]
    arm = result.arms[arm_name]
    resp = responder_analysis(arm.auc_values, threshold=30.0, direction=:above)
    println("$(arm_name): $(resp.n_responders)/$(resp.n_total) " *
            "($(round(resp.rate*100, digits=1))%)")
end

See Also