Power Analysis
Comprehensive guide for sample size calculation and power analysis in NeoPKPD Python.
Overview
Power analysis determines the sample size needed to detect effects with adequate statistical power.
from neopkpd import trial
# Calculate required sample size
result = trial.estimate_sample_size(
target_power=0.80,
effect_size=0.5,
sd=1.0,
alpha=0.05
)
print(f"Required n per arm: {result.n_per_arm}")
Analytical Power
Two-Sample t-Test
from neopkpd.trial import estimate_power_analytical
# Calculate power for given sample size
power = estimate_power_analytical(
n_per_arm=50,
effect_size=0.5, # Cohen's d
sd=1.0,
alpha=0.05,
test="two_sample_t"
)
print(f"Power: {power.power:.1%}")
print(f"Effect size: {power.effect_size}")
One-Sample t-Test
power = estimate_power_analytical(
n=30,
effect_size=0.6,
sd=1.0,
alpha=0.05,
test="one_sample_t"
)
Paired t-Test
power = estimate_power_analytical(
n_pairs=25,
effect_size=0.5,
sd=1.0, # SD of differences
alpha=0.05,
test="paired_t"
)
Sample Size Estimation
Basic Estimation
from neopkpd.trial import estimate_sample_size
# Find n for target power
result = estimate_sample_size(
target_power=0.80,
effect_size=0.5,
sd=1.0,
alpha=0.05,
test="two_sample_t"
)
print(f"Required n per arm: {result.n_per_arm}")
print(f"Total N: {result.n_total}")
print(f"Achieved power: {result.achieved_power:.1%}")
SampleSizeResult Class
@dataclass
class SampleSizeResult:
"""Result of sample size calculation."""
n_per_arm: int # Sample size per arm
n_total: int # Total sample size
achieved_power: float # Actual power achieved
effect_size: float # Effect size used
alpha: float # Alpha level
test: str # Statistical test used
With Dropout Adjustment
# Account for anticipated dropout
result = estimate_sample_size(
target_power=0.80,
effect_size=0.5,
sd=1.0,
alpha=0.05,
dropout_rate=0.15 # 15% dropout
)
print(f"Enrollment needed: {result.n_to_enroll}")
print(f"Expected completers: {result.n_per_arm}")
Bioequivalence Power
Standard BE (2×2 Crossover)
from neopkpd.trial import be_sample_size
# Calculate sample size for BE study
result = be_sample_size(
gmr=0.95, # Expected geometric mean ratio
cv_within=0.25, # Within-subject CV
theta1=0.80, # Lower BE limit
theta2=1.25, # Upper BE limit
alpha=0.05,
power=0.80
)
print(f"Required N: {result.n_total}")
print(f"Power: {result.power:.1%}")
BE Power Calculation
from neopkpd.trial import be_power
# Calculate power for given sample size
power = be_power(
n=24,
gmr=0.95,
cv_within=0.25,
theta1=0.80,
theta2=1.25,
alpha=0.05
)
print(f"Power: {power:.1%}")
Replicate Design Sample Size
# 2×4 replicate design (for HVD)
result = be_sample_size(
gmr=0.95,
cv_within=0.35, # Higher CV
theta1=0.80,
theta2=1.25,
alpha=0.05,
power=0.80,
design="replicate_2x4"
)
# Partial replicate (3×3)
result = be_sample_size(
gmr=0.95,
cv_within=0.35,
power=0.80,
design="partial_replicate"
)
Highly Variable Drug Power
RSABE Sample Size (FDA)
from neopkpd.trial import rsabe_sample_size
# Reference-scaled average bioequivalence
result = rsabe_sample_size(
gmr=0.95,
cv_reference=0.40, # Reference CV > 30%
theta_s=0.8928, # Scaling factor
sigma_w0=0.25, # Regulatory cutoff
alpha=0.05,
power=0.80
)
print(f"Required N: {result.n_total}")
print(f"Scaling applied: {result.scaling_applied}")
print(f"Effective limits: [{result.lower_limit:.2f}, {result.upper_limit:.2f}]")
ABEL Sample Size (EMA)
from neopkpd.trial import abel_sample_size
# Average bioequivalence with expanding limits
result = abel_sample_size(
gmr=0.95,
cv_reference=0.40,
cv_cutoff=0.30,
max_widening=0.50, # Maximum expansion
alpha=0.05,
power=0.80
)
print(f"Required N: {result.n_total}")
print(f"Widened limits: [{result.lower_limit:.2%}, {result.upper_limit:.2%}]")
Power Curves
Generate Power Curve
from neopkpd.trial import power_curve
# Power vs sample size
curve = power_curve(
effect_size=0.5,
sd=1.0,
alpha=0.05,
n_range=range(10, 101, 5),
test="two_sample_t"
)
# Find minimum N for 80% power
min_n = next(n for n, p in zip(curve.n, curve.power) if p >= 0.80)
print(f"Minimum N for 80% power: {min_n}")
Effect Size Sensitivity
# Power for different effect sizes at fixed N
sensitivity = trial.effect_sensitivity(
n=50,
alpha=0.05,
effect_sizes=[0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
test="two_sample_t"
)
for es, pow in zip(sensitivity.effect_sizes, sensitivity.powers):
print(f"Effect size {es:.1f}: Power = {pow:.1%}")
Minimum Detectable Effect
# What effect can we detect with given N and power?
mde = trial.minimum_detectable_effect(
n_per_arm=50,
alpha=0.05,
power=0.80,
sd=1.0
)
print(f"Minimum detectable effect: d = {mde:.3f}")
Simulation-Based Power
Monte Carlo Power Estimation
from neopkpd.trial import simulate_power
# Power via simulation
result = simulate_power(
n_per_arm=50,
effect_size=0.5,
sd=1.0,
alpha=0.05,
n_simulations=1000,
seed=42
)
print(f"Simulated power: {result.power:.1%}")
print(f"95% CI: ({result.ci_lower:.1%}, {result.ci_upper:.1%})")
BE Study Simulation
# Simulate BE study power
result = trial.simulate_be_power(
n=24,
gmr=0.95,
cv_within=0.25,
n_simulations=1000,
seed=42
)
print(f"BE power: {result.power:.1%}")
print(f"Pass rate: {result.pass_rate:.1%}")
Trial Simulation Power
# Full trial simulation
power = trial.simulate_trial_power(
trial_spec=trial_spec,
n_per_arm=50,
endpoint="auc_comparison",
success_criterion=lambda r: r.p_value < 0.05 and r.effect > 0,
n_simulations=1000,
seed=42
)
Multi-Arm Trials
Dunnett's Test
from neopkpd.trial import multiarm_sample_size
# Multiple treatment arms vs placebo
result = multiarm_sample_size(
n_arms=4, # 1 placebo + 3 active
effect_size=0.5,
sd=1.0,
alpha=0.05,
power=0.80,
comparison="dunnett" # Many-to-one
)
print(f"N per arm: {result.n_per_arm}")
print(f"Total N: {result.n_total}")
print(f"Adjusted alpha: {result.adjusted_alpha:.4f}")
ANOVA
# Overall group difference
result = multiarm_sample_size(
n_arms=4,
effect_size=0.3, # f-statistic
alpha=0.05,
power=0.80,
comparison="anova"
)
Pairwise Comparisons
# All pairwise with Bonferroni correction
result = multiarm_sample_size(
n_arms=4,
effect_size=0.5,
alpha=0.05,
power=0.80,
comparison="bonferroni"
)
Adaptive Designs
Group Sequential Power
from neopkpd.trial import group_sequential_power
# O'Brien-Fleming boundaries
result = group_sequential_power(
n_per_arm=100,
effect_size=0.4,
sd=1.0,
alpha=0.05,
n_looks=3,
information_fractions=[0.33, 0.67, 1.0],
spending_function="obrien_fleming"
)
print(f"Overall power: {result.power:.1%}")
print(f"P(stop at look 1): {result.stop_probs[0]:.1%}")
print(f"P(stop at look 2): {result.stop_probs[1]:.1%}")
Sample Size Re-estimation
from neopkpd.trial import ssr_sample_size
# Initial sample size with interim re-estimation
result = ssr_sample_size(
initial_n=50,
target_power=0.80,
effect_size=0.5,
sd=1.0,
interim_fraction=0.5,
max_n=150
)
print(f"Initial N: {result.initial_n}")
print(f"Expected final N: {result.expected_n}")
print(f"Max N: {result.max_n}")
Special Populations
Pediatric Studies
# Pediatric with higher variability
result = estimate_sample_size(
target_power=0.80,
effect_size=0.5,
sd=1.0,
alpha=0.05,
variability_inflation=1.2 # 20% higher variability
)
Rare Disease
# Small population constraints
result = trial.rare_disease_power(
available_patients=50,
effect_size=0.8, # Expect large effect
sd=1.0,
alpha=0.10 # Relaxed alpha
)
print(f"Achievable power: {result.power:.1%}")
print(f"Recommended design: {result.recommended_design}")
Alpha Spending
O'Brien-Fleming
from neopkpd.trial import alpha_spending
# O'Brien-Fleming spending
alpha = alpha_spending(
information_fraction=0.5,
total_alpha=0.05,
method="obrien_fleming"
)
print(f"Alpha spent at 50%: {alpha:.4f}")
Pocock
alpha = alpha_spending(
information_fraction=0.5,
total_alpha=0.05,
method="pocock"
)
Lan-DeMets
# Flexible spending function
alpha = alpha_spending(
information_fraction=0.5,
total_alpha=0.05,
method="lan_demets",
rho=1.0 # Parameter
)
Complete Example
from neopkpd import trial
# ==========================================
# Comprehensive Power Analysis
# ==========================================
print("=== Power Analysis for Phase III Trial ===\n")
# Study parameters
effect_size = 0.40 # Expected treatment effect
sd = 1.0 # Population SD
alpha = 0.05 # Two-sided alpha
target_power = 0.80 # Target power
dropout_rate = 0.15 # Expected dropout
# 1. Calculate base sample size
print("--- Sample Size Calculation ---")
result = trial.estimate_sample_size(
target_power=target_power,
effect_size=effect_size,
sd=sd,
alpha=alpha,
dropout_rate=dropout_rate
)
print(f"Required completers: {result.n_per_arm} per arm")
print(f"Enrollment needed: {result.n_to_enroll} per arm")
print(f"Total enrollment: {result.n_to_enroll * 2}")
print(f"Achieved power: {result.achieved_power:.1%}")
# 2. Power curve
print("\n--- Power by Sample Size ---")
curve = trial.power_curve(
effect_size=effect_size,
sd=sd,
alpha=alpha,
n_range=range(30, 101, 10)
)
print("N/arm Power")
print("-" * 20)
for n, power in zip(curve.n, curve.power):
marker = "*" if power >= 0.80 else ""
print(f"{n:5d} {power:.1%}{marker}")
# 3. Effect size sensitivity
print("\n--- Detectable Effect Sizes ---")
print(f"With N = {result.n_per_arm} per arm:")
for power_target in [0.70, 0.80, 0.90]:
mde = trial.minimum_detectable_effect(
n_per_arm=result.n_per_arm,
alpha=alpha,
power=power_target,
sd=sd
)
print(f" {int(power_target*100)}% power: d = {mde:.3f}")
# 4. Simulation verification
print("\n--- Simulation Verification ---")
sim_result = trial.simulate_power(
n_per_arm=result.n_per_arm,
effect_size=effect_size,
sd=sd,
alpha=alpha,
n_simulations=1000,
seed=42
)
print(f"Simulated power: {sim_result.power:.1%}")
print(f"95% CI: ({sim_result.ci_lower:.1%}, {sim_result.ci_upper:.1%})")
# 5. Group sequential design
print("\n--- Group Sequential Design ---")
gs_result = trial.group_sequential_power(
n_per_arm=result.n_per_arm,
effect_size=effect_size,
sd=sd,
alpha=alpha,
n_looks=2,
information_fractions=[0.5, 1.0],
spending_function="obrien_fleming"
)
print(f"Overall power: {gs_result.power:.1%}")
print(f"Efficacy boundaries: {gs_result.boundaries}")
print(f"P(stop at interim): {gs_result.stop_probs[0]:.1%}")
print(f"Expected sample size: {gs_result.expected_n:.0f}")
# 6. Summary
print("\n" + "=" * 50)
print("RECOMMENDATION")
print("=" * 50)
print(f"\nEnroll {result.n_to_enroll * 2} subjects total")
print(f"({result.n_to_enroll} per arm)")
print(f"\nWith {int(dropout_rate*100)}% dropout:")
print(f" Expected completers: {result.n_per_arm * 2}")
print(f" Power: {result.achieved_power:.1%}")
print(f"\nTo detect effect size d = {effect_size}")
print(f"At alpha = {alpha} (two-sided)")
Power Tables
Parallel Design Reference
| Effect Size |
CV |
N per Arm (80%) |
N per Arm (90%) |
| 0.3 |
30% |
176 |
235 |
| 0.4 |
30% |
99 |
132 |
| 0.5 |
30% |
64 |
85 |
| 0.6 |
30% |
44 |
59 |
| 0.7 |
30% |
33 |
44 |
BE Crossover Reference
| CV Within |
GMR |
N (80%) |
N (90%) |
| 15% |
0.95 |
10 |
14 |
| 20% |
0.95 |
16 |
22 |
| 25% |
0.95 |
24 |
32 |
| 30% |
0.95 |
36 |
48 |
| 25% |
1.00 |
18 |
24 |
Function Reference
| Function |
Description |
estimate_power_analytical |
Analytical power calculation |
estimate_sample_size |
Sample size for target power |
be_sample_size |
BE study sample size |
be_power |
BE study power |
power_curve |
Power vs sample size curve |
simulate_power |
Monte Carlo power |
multiarm_sample_size |
Multi-arm trial sample size |
alpha_spending |
Alpha spending functions |
minimum_detectable_effect |
MDE calculation |
See Also