
Power Analysis for Between-Subject Discounting Designs
Brent Kaplan
Source:vignettes/power-analysis.Rmd
power-analysis.RmdThe Monte Carlo runs below are precomputed (the full set of
power_discounting() / find_n_discounting()
calls in this vignette takes several minutes, well past CRAN’s
vignette-build budget), so the outputs shown come from real runs; copy
the chunks into your session to reproduce them.
Why simulation-based power?
Before running a study, a researcher planning a delay-discounting
experiment needs an answer to: “How many subjects do I need to
detect a group difference in discounting of this size at 80%
power?” Closed-form power formulas do not exist for mixed-effects
discounting models with scale-location-truncated beta (SLT-beta)
observation models, and Wald asymptotics can be unreliable at realistic
study sizes. power_discounting() therefore estimates power
by Monte Carlo simulation:
- Simulate a two-group indifference-point dataset with
[simulate_dd_ip()] under assumed population parameters, a hypothesized
delta_k(the condition shift on natural-log k), and the design. - Refit it with the package’s TMB mixed-effects fitter, [fit_dd_tmb()]. The simulation and refit always use the same discounting function and observation family, so the tested contrast is the simulated effect.
- Extract the Wald standard error of the log-k condition contrast from
the TMB
sdreport. - Record both a p-value verdict and a confidence-interval verdict
(does the Wald CI exclude 0). Both are referred to a t distribution with
n_subjects - 2degrees of freedom (the design’s two-sample df) rather than the asymptotic normal, which is anticonservative at study-relevant N. This df is an empirical calibration device, not a df derived from the mixed model (the TMB fit has no exact t sampling theory);df = Infrecovers the z-test. The two verdicts use the same standard error and reference distribution, so they always agree; both report formats are returned. - Repeat
n_simtimes. Power is the proportion of usable fits (converged, positive-definite Hessian, finite SE) that reject, reported with a Wilson confidence interval because it is itself an estimate from finitely many replicates.
The design is between-subject: n_subjects is
the total N, split across the two conditions round-robin (use even
numbers for equal groups). This mirrors
beezdemand::power_demand(), which models a
within-subject condition. The asymmetry matches what each
package’s simulator and fitter already represent.
Estimating power at a fixed sample size
Suppose pilot data suggest the comparison group discounts about twice
as steeply (delta_k = log(2) ≈ 0.69) and subjects vary with
sigma_u = 0.6 around their group’s log k:
res <- power_discounting(
n_subjects = 60,
effect = list(delta_k = log(2)),
design = list(
delays = c(7, 30, 180, 365, 730, 1460, 2920),
sigma_u = 0.6, # subject SD on log k
phi = 10 # SLT-beta precision
),
n_sim = 40, # small for a fast vignette; see guidance below
seed = 1234,
verbose = FALSE
)
res
#> Monte Carlo power analysis (beezdiscounting)
#> Target: k:conditionC2 (delta_k = 0.6931), two-sided alpha = 0.05, t reference (df = 58)
#> n_subjects = 60 (between-subject, 2 conditions), n_sim = 40 (converged 40, usable 40)
#> Power (CI-exclusion): 0.925 [95% MC CI 0.801, 0.974]
#> p-value hit rate: 0.925Convergence diagnostics are part of the output. Replicates whose
refit fails are excluded from the power denominator and counted in
n_converged / n_used rather than treated as
“no effect detected”.
res$n_converged
#> [1] 40
res$n_used
#> [1] 40
table(res$replicates$status)
#>
#> ok
#> 40Searching for a sample size
find_n_discounting() wraps the engine in a bisection
search over total N for the smallest sample whose estimated power
reaches the target. The search accounts for Monte Carlo noise: at each
candidate N it adds replicates until the Wilson interval falls clearly
above or below the target (up to n_sim_max), and it
re-evaluates the selected N and its lower neighbor (or the lower bound
itself, when the target is already met there) before reporting.
Bisection assumes power is monotone in N; because each N is judged from
independent replicates, a fluctuation at a lower N can hide a crossing
the search never revisits. Evaluated N that contradict monotonicity
demote the status to uncertain, but never-visited N cannot
be checked, so treat the result as an estimate and confirm it with a
large n_sim at the chosen N.
search <- find_n_discounting(
target_power = 0.8,
effect = list(delta_k = log(3)),
n_range = c(6, 60),
n_sim = 30, # small for a fast vignette
seed = 5678,
verbose = FALSE
)
search
#> Sample-size search (Monte Carlo power)
#> Target power 0.80 for delta_k = 1.099 at alpha = 0.05
#> Estimated minimum n_subjects = 13 (status: uncertain)
#> This is an estimated minimum under Monte Carlo uncertainty;
#> at least one decision fell back to a point estimate, so the true
#> power at this N may be below the target.
#> Validate it with an independent power_discounting() run at this N
#> (large n_sim, new seed) before reporting.
#>
#> Evaluations:
#> n_subjects n_sim_total n_used usable_fraction power ci_lower ci_upper
#> 60 30 30 1 1.0000000 0.8864866 1.0000000
#> 6 30 30 1 0.5666667 0.3919731 0.7262251
#> 33 30 30 1 1.0000000 0.8864866 1.0000000
#> 19 60 60 1 0.9666667 0.8863623 0.9908107
#> 12 120 120 1 0.7750000 0.6924310 0.8405085
#> 15 60 60 1 0.9500000 0.8629948 0.9828505
#> 13 120 120 1 0.8333333 0.7565472 0.8894400
#> 13 120 120 1 0.8833333 0.8136665 0.9292188
#> 12 120 120 1 0.8333333 0.7565472 0.8894400
#> decision
#> above
#> below
#> above
#> above
#> ambiguous_below
#> above
#> ambiguous_above
#> above
#> ambiguous_aboveThe result is an estimated minimum under Monte Carlo
uncertainty (the status field says whether the
confirmation pass was conclusive). For a defensible grant number, rerun
power_discounting() at the returned N with a large
n_sim and report that estimate with its Monte Carlo
interval.
Sensitivity to the assumed effect size
Power claims are conditional on the assumed effect and heterogeneity. A small sweep makes that dependence visible:
deltas <- c(log(1.5), log(2), log(3))
sweep <- vapply(deltas, function(d) {
power_discounting(
n_subjects = 60, effect = list(delta_k = d),
n_sim = 30, seed = 42, verbose = FALSE
)$power
}, numeric(1))
data.frame(k_ratio = exp(deltas), power = sweep)
#> k_ratio power
#> 1 1.5 0.6333333
#> 2 2.0 1.0000000
#> 3 3.0 1.0000000How many replicates do you need?
The Monte Carlo standard error of a power estimate near
p is sqrt(p (1 - p) / n_sim):
n_sim |
MC SE near power = 0.8 | Use |
|---|---|---|
| 100 | 0.040 | quick interactive exploration |
| 500 | 0.018 | default; serious exploration |
| 2000 | 0.009 | grant applications, preregistrations |
Always report the Wilson interval (power_mc_ci)
alongside the point estimate.
Validity and Limitations
Validity checks. The package test suite
(tests/testthat/test-dd-power.R) verifies, with
preregistered seeds and tolerance bands fixed before the tests were
first run:
-
Type I error calibration (the primary check): with
delta_k = 0, the empirical false-positive rate at nominalalpha = .05over 1,200 replicates must fall in [0.03, 0.07] (a band of 3.18 binomial standard errors that excludes both half and 1.5 times the nominal rate).n_sim = 1200was computed from that tolerance (9 * .05 * .95 / .02^2 ≈ 1069). A second null check runs at N = 60. The t(n - 2) reference distribution these checks calibrate was adopted after the sibling beezdemand calibration battery showed the asymptotic z-test is anticonservative at study-relevant sample sizes. -
Convergence handling: a configuration that reliably
produces non-convergence confirms that failed replicates are excluded
from the power denominator and reported in
n_converged/n_usedrather than counted as misses. -
Closed-form benchmark: with
family = "gaussian", tiny residual error, and many delays, each subject’s log k is recovered nearly exactly and the design reduces to a two-sample comparison of log k; the Monte Carlo estimate must matchpwr::pwr.t.test()within a preregistered tolerance of 0.10. -
Monotonicity: power increases with
n_subjectsanddelta_kand decreases withsigma_u, within Monte Carlo slack. -
Reproducibility: identical
seedgives identical results, asserted exactly.
These checks validate the default configuration
(equation = "mazur", family = "sltb",
random_effects = k ~ 1), where the refit model exactly
matches the data-generating process.
The find_n_* search statuses ("confirmed",
"uncertain", "unresolved") describe a
heuristic Monte Carlo decision rule (repeated looks at ordinary Wilson
intervals across candidate N) rather than a formal sequential testing
procedure with a guaranteed error rate. When the confirmation pass
contradicts the search, the function returns n = NA rather
than an unsupported number.
Explicitly out of scope in v1 (deferred to future work; none of these is approximated):
- Effects on the curvature exponent
s(Green-Myerson / Rachlin) or the SLT-beta precisionphi; the two-parameter equations are excluded entirely. - Power for derived measures (ED50, AUC, indifference points at a delay).
- Random effects beyond the single log-k intercept; correlated
k/phiork/srandom effects. (random_effectsaccepts such formulas, but the simulator still generates only the log-k intercept, so the refit is over-specified relative to the data-generating process; treat such a run as a robustness probe rather than a power estimate under those random effects.) - Arbitrary user-supplied designs beyond the package’s between-subject simulator (two conditions, round-robin allocation, all subjects at all delays).
- Any graphical or interactive interface.
Interpreting a power estimate. A reported power
estimate is (a) conditional on the assumed population parameters and
heterogeneity, so vary them and check the sensitivity of the answer; (b)
conditional on usable fits, so take the n_used warning
seriously if it fires; and (c) a Monte Carlo estimate, so cite it with
its interval at an n_sim sized for the decision it
supports.