Sample Size, Minimum Detectable Effect, and Power › Worked Example: Clinical Trial Design

Michael BrenndoerferJanuary 7, 20265 min read
Machine Learning from ScratchAll chapters
Part 2 of 9Hypothesis Testing
Chapter 19Sample Size, Minimum Detectable Effect, and Power › Worked Example: Clinical Trial Design
Reading section 7 of 12 · Worked Example: Clinical Trial Design · 5 minSection 7 of 12 · 5 min

Book contents

Section 7 of 12 · 5 min read

Sample Size, Minimum Detectable Effect, and Power › Worked Example: Clinical Trial DesignOne section · Sections in this chapter7 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's work through a complete power analysis for a realistic scenario.

Scenario: A pharmaceutical company is designing a clinical trial to test whether a new blood pressure medication reduces systolic blood pressure more than the current standard treatment. Based on pilot data:

  • Current treatment reduces systolic BP by 10 mmHg on average
  • Standard deviation of BP reduction is approximately 15 mmHg
  • The company wants to detect a 5 mmHg additional reduction (clinically meaningful)
  • They want 90% power to ensure reliable results
  • Using standard α = 0.05 (two-tailed)
In[13]:
Code
import numpy as np
from scipy import stats

# Study parameters
current_treatment_effect = 10  # mmHg reduction
new_treatment_effect = 15  # Expected mmHg reduction
effect_size = new_treatment_effect - current_treatment_effect  # 5 mmHg
sigma = 15  # Standard deviation
alpha = 0.05
target_power = 0.90

print("=== Clinical Trial Power Analysis ===\n")
print("Parameters:")
print(f"  Expected effect: {effect_size} mmHg improvement over standard")
print(f"  Standard deviation: {sigma} mmHg")
print(f"  Significance level: α = {alpha}")
print(f"  Target power: {target_power * 100}%")

# Calculate Cohen's d
d = effect_size / sigma
print(f"\nStandardized effect size (Cohen's d): {d:.3f}")

# Sample size calculation
z_alpha = stats.norm.ppf(1 - alpha / 2)
z_beta = stats.norm.ppf(target_power)

n_per_group = 2 * ((z_alpha + z_beta) / d) ** 2
n_per_group = int(np.ceil(n_per_group))

print("\n=== Required Sample Size ===")
print(f"  z_α/2 = {z_alpha:.4f}")
print(f"  z_(1-β) = {z_beta:.4f}")
print(f"  n per group = 2 × (({z_alpha:.3f} + {z_beta:.3f}) / {d:.3f})²")
print(f"  n per group = {n_per_group}")
print(f"  Total participants = {2 * n_per_group}")
Out[13]:
Console
=== Clinical Trial Power Analysis ===

Parameters:
  Expected effect: 5 mmHg improvement over standard
  Standard deviation: 15 mmHg
  Significance level: α = 0.05
  Target power: 90.0%

Standardized effect size (Cohen's d): 0.333

=== Required Sample Size ===
  z_α/2 = 1.9600
  z_(1-β) = 1.2816
  n per group = 2 × ((1.960 + 1.282) / 0.333)²
  n per group = 190
  Total participants = 380
In[14]:
Code
# Sensitivity analysis: What if our assumptions are wrong?

print("\n=== Sensitivity Analysis ===\n")
print("What if the true effect or variability differs from our assumptions?\n")

# Vary effect size
print("1. Effect Size Sensitivity (σ = 15 mmHg, power = 90%):")
print(f"   {'Effect (mmHg)':<15} {'Cohen d':<10} {'n per group':<15}")
print("   " + "-" * 40)
for effect in [3, 4, 5, 6, 7]:
    d = effect / sigma
    n = 2 * ((z_alpha + z_beta) / d) ** 2
    print(f"   {effect:<15} {d:<10.3f} {int(np.ceil(n)):<15}")

# Vary standard deviation
print("\n2. Variance Sensitivity (effect = 5 mmHg, power = 90%):")
print(f"   {'SD (mmHg)':<15} {'Cohen d':<10} {'n per group':<15}")
print("   " + "-" * 40)
for sd in [10, 12, 15, 18, 20]:
    d = effect_size / sd
    n = 2 * ((z_alpha + z_beta) / d) ** 2
    print(f"   {sd:<15} {d:<10.3f} {int(np.ceil(n)):<15}")

# Vary power
print("\n3. Power Sensitivity (effect = 5 mmHg, σ = 15 mmHg):")
print(f"   {'Power':<15} {'n per group':<15} {'Total N':<15}")
print("   " + "-" * 45)
for power in [0.70, 0.80, 0.85, 0.90, 0.95]:
    z_b = stats.norm.ppf(power)
    n = 2 * ((z_alpha + z_b) / d) ** 2
    n = int(np.ceil(n))
    print(f"   {power * 100:.0f}%{'':<10} {n:<15} {2 * n:<15}")
Out[14]:
Console

=== Sensitivity Analysis ===

What if the true effect or variability differs from our assumptions?

1. Effect Size Sensitivity (σ = 15 mmHg, power = 90%):
   Effect (mmHg)   Cohen d    n per group    
   ----------------------------------------
   3               0.200      526            
   4               0.267      296            
   5               0.333      190            
   6               0.400      132            
   7               0.467      97             

2. Variance Sensitivity (effect = 5 mmHg, power = 90%):
   SD (mmHg)       Cohen d    n per group    
   ----------------------------------------
   10              0.500      85             
   12              0.417      122            
   15              0.333      190            
   18              0.278      273            
   20              0.250      337            

3. Power Sensitivity (effect = 5 mmHg, σ = 15 mmHg):
   Power           n per group     Total N        
   ---------------------------------------------
   70%           198             396            
   80%           252             504            
   85%           288             576            
   90%           337             674            
   95%           416             832
Out[15]:
Visualization
Power versus sample size for a five mmHg treatment effect, with 80 and 90 percent targets and the required sample size annotated.
Power as a function of sample size, showing the 90% target.
Power versus true treatment effect for 190 participants per group, with the expected five mmHg effect and 90 percent target marked.
Power as a function of true effect size, given the planned sample of 190 per group.

Decision Summary

In[16]:
Code
print("=== Clinical Trial Design Decision ===\n")

n_required = 190  # From our calculation
dropout_rate = 0.15  # Typical for clinical trials
n_enrolled = int(np.ceil(n_required / (1 - dropout_rate)))

print("Recommended Design:")
print(f"  • Enroll {n_enrolled} patients per group ({2 * n_enrolled} total)")
print(f"    (Accounts for {dropout_rate * 100:.0f}% expected dropout)")
print(f"  • Expected completers: {n_required} per group")
print("  • Power: 90% to detect 5 mmHg difference")
print("  • MDE at enrolled sample: ~4.3 mmHg")
print()
print("Study Characteristics:")
print("  • Can detect effects as small as Cohen's d = 0.29")
print("  • 10% risk of missing a true 5 mmHg effect (Type II)")
print("  • 5% risk of false positive (Type I)")
print()
print("Practical Considerations:")
print("  • Budget for ~450 total enrolled patients")
print("  • Allow for longer recruitment if dropout > expected")
print("  • Consider adaptive design if early results warrant")
Out[16]:
Console
=== Clinical Trial Design Decision ===

Recommended Design:
  • Enroll 224 patients per group (448 total)
    (Accounts for 15% expected dropout)
  • Expected completers: 190 per group
  • Power: 90% to detect 5 mmHg difference
  • MDE at enrolled sample: ~4.3 mmHg

Study Characteristics:
  • Can detect effects as small as Cohen's d = 0.29
  • 10% risk of missing a true 5 mmHg effect (Type II)
  • 5% risk of false positive (Type I)

Practical Considerations:
  • Budget for ~450 total enrolled patients
  • Allow for longer recruitment if dropout > expected
  • Consider adaptive design if early results warrant