Back to Claude Scientific Skills

Simulation-Based (Monte Carlo) Power

skills/statistical-power/references/simulation_based_power.md

2.52.05.5 KB
Original Source

Simulation-Based (Monte Carlo) Power

Closed-form power exists for a handful of standard tests. For everything else, simulate. This is not a second-best approximation — for complex designs it is the correct method, and it has one big advantage: the power estimate uses the exact analysis you will run on the real data, so there is no mismatch between the planning model and the analysis model.

The recipe (always the same)

  1. Simulate a dataset of size n from your assumed truth: the effect you want to detect, plus realistic structure (baseline rates, residual SD, cluster random effects, dropout, covariate distributions).
  2. Analyze it with the exact model/test planned for the real study.
  3. Repeat R times. Power = fraction of replicates where the test is significant. Use R ≥ 1,000; use 5,000–10,000 for a stable estimate near the 80% decision point.

Always report the Monte Carlo confidence interval on the estimate (the scripts/simulate_power.py harness returns a Wilson interval). With R = 1,000 the ±2 SE width near p = 0.8 is roughly ±0.025, so don't over-interpret 0.81 vs 0.79.

Using the harness

scripts/simulate_power.py gives you simulate_power() and find_sample_size(). You supply a function gen_and_test(n, rng) -> bool that builds one dataset, runs the analysis, and returns whether it was significant. The rng is a seeded numpy.random.Generator so runs are reproducible and replicates are independent.

python
from simulate_power import simulate_power, find_sample_size

def gen_and_test(n, rng):
    # ... simulate n observations under the assumed effect ...
    # ... fit the planned model ...
    return pvalue < 0.05

# power at a fixed n
print(simulate_power(gen_and_test, n=200, n_sims=2000))

# search for the n that hits 80% power
n, est = find_sample_size(gen_and_test, target_power=0.80, n_sims=2000)

The file ships four adaptable examples: two-group difference (a sanity check against the closed-form t-test), logistic regression, a cluster-randomized trial with an ICC, and a repeated-measures linear mixed model. Copy the closest one and edit the data-generating block.

When you must simulate

Design / analysisWhy no formulaWhat to simulate
Logistic / Poisson regressionPower depends on the full covariate distributionGenerate predictors, compute the linear predictor, draw the outcome, fit the GLM
Mixed-effects / repeated measuresRandom effects + within-subject correlationDraw subject/cluster random effects, then observations; fit mixedlm
Cluster-randomized trialICC inflates variance; clusters are the unitCluster random intercepts via ICC; fit a mixed model or use the design effect
Survival (Cox / log-rank)Censoring and event-time distributionDraw event and censoring times; fit lifelines CoxPH or run a log-rank test
Interaction termsPower for an interaction ≪ power for main effectsGenerate the factorial structure and the interaction effect; test that coefficient
MediationProduct-of-coefficients null is non-normalSimulate the path model; bootstrap or test the indirect effect
Non-standard / custom testNo theory at allWhatever your analysis script does

Key correctness points

  • Analyze exactly as planned. If the real analysis adjusts for covariates, include them in the simulation. If it uses a robust SE or a specific correction, apply it in gen_and_test. The whole value of simulation is fidelity to the plan.
  • Handle estimation failures. GLMs and mixed models can fail to converge or hit perfect separation. Wrap the fit in try/except and count a failure as not significant (conservative). If failures are common, that itself is a warning about the design or sample size.
  • Watch Type I error too. As a check, simulate under the null (effect = 0) and confirm the rejection rate ≈ α. If it's inflated (common with small-cluster mixed models or naive cluster SEs), your planned analysis is anticonservative and the power number is meaningless until you fix the analysis.
  • Seed it. A fixed seed makes the search reproducible and stops find_sample_size from chasing simulation noise around the boundary.

Modeling realistic complications

  • Dropout. Either simulate missingness directly (drop rows / occasions under the assumed mechanism, then analyze the reduced data — captures the real power loss including any bias), or compute the analyzed n and inflate the enrolled n by 1/(1−dropout).
  • Clustering / ICC. Split total variance into between-cluster (τ²) and residual (σ²) with ICC = τ²/(τ²+σ²), draw a cluster random effect ~ N(0, τ), add it to every member of the cluster. See example_cluster_randomized.
  • Unequal allocation / stratification. Generate the exact group sizes and strata the design will produce; don't assume balance the design won't deliver.
  • Repeated measures. Subject random intercept (and slope, if relevant) plus a within-subject residual; the within-subject correlation is τ²/(τ²+σ²).

Reporting a simulation-based power analysis

State enough that someone could rerun it:

Power was estimated by simulation (5,000 replicates per sample size). Data were
generated assuming a baseline event rate of 20%, a treatment log-odds of 0.8, and
analyzed with logistic regression adjusting for age and site, matching the planned
analysis. A sample of n = 150 per arm yielded 82% power (95% Monte Carlo CI
80.5-83.5%) at α = .05 (two-sided). Code is available at [link].