API reference¶
The public API, in the groups the R package's reference
index uses and in the same
order, so a name is found in the same place on either site. Import every name
here from the top-level package, for example from pilotr import simulate,
power.
The R package covers more ground. Calibration, sweeps, the generated-analysis emitters and the no-code app have no Python counterpart yet, so the groups they occupy on that side do not appear below.
Design specifications¶
Load and validate the portable JSON specification that everything else reads.
The specification is the same file in either language, and SPEC_VERSION is the
version of that format this release understands.
pilotr.simulate.load_spec ¶
load_spec(path, validate=True)
Load a JSON design specification from a file.
The specification is validated by default, because several ways of getting one wrong produce
plausible data and no error at all: a mistyped coefficient key resolves to no column and so
silently sets that effect to zero, and a response parameter left over from another family is
ignored. Validation also refuses a specification declaring a spec_version newer than this
implementation understands, and never reads such a file in part.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to a JSON design-specification file. |
required |
validate
|
bool
|
Whether to validate the specification after reading it. |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
The parsed specification, ready for |
pilotr.validate.validate_spec ¶
validate_spec(spec, strict: bool = True)
Validate a design specification, returning it so the call can be chained.
Checks the specification against the portable schema and against the cross-field rules the
schema cannot express, and checks that its declared spec_version is one this
implementation understands. Called by load_spec by default.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
dict
|
A parsed design specification. |
required |
strict
|
bool
|
Whether an unrecognised field is an error (the default) or a warning. Pass |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
The specification, unchanged. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the specification is invalid. All problems found are reported together. |
pilotr.examples.pilotr_example ¶
pilotr_example(name: str | None = None) -> list[str] | str
List the bundled example specifications, or return the path to one.
pilotr ships one ready-to-run specification per design family, as JSON. These are the same files that drive the R twin and the no-code app.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The base name of an example, with or without the |
None
|
Returns:
| Type | Description |
|---|---|
list of str, or str
|
When |
Simulation¶
Draw a data set from a specification, returned together with the design information an analysis needs.
pilotr.simulate.simulate ¶
simulate(spec, validate=True) -> Dataset
Simulate a data set from a design specification.
Build a linear predictor from the fixed effect sizes (categorical contrasts, continuous predictors and their interactions) plus the crossed by-subject and by-item random intercepts and slopes, then map it through the chosen response family.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
dict or str
|
A design specification, either an already-parsed |
required |
validate
|
bool
|
Whether to validate the specification first. The default |
True
|
Returns:
| Type | Description |
|---|---|
Dataset
|
A table with one row per observation: a |
pilotr.simulate.Dataset ¶
A simulated data set: named columns and a list of row dicts.
Returned by simulate. Lightweight and dependency-free; build a pandas DataFrame
from dataset.rows when you need one.
Attributes:
| Name | Type | Description |
|---|---|---|
columns |
list of str
|
Column names, in order. |
rows |
list of dict
|
One dict per observation, keyed by column name. |
Power and design analysis¶
Estimate power by simulation, at one sample size or across a range of them, and solve the resulting curve for the value that meets a target.
pilotr.power.power ¶
power(spec, n_sims=1000, alpha=0.05, workers=1)
Simulation-based power and design analysis for a two-group Gaussian design.
Repeatedly simulate from the specification, apply a two-sample t-test, and report power together with the Type S (sign) and Type M (magnitude) errors of Gelman and Carlin (2014), computed over the significant replicates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
dict or str
|
A two-group Gaussian design specification (dict or path to a JSON file). |
required |
n_sims
|
int
|
Number of Monte Carlo replicates (default 1000). |
1000
|
alpha
|
float
|
Two-sided significance level (default 0.05). |
0.05
|
workers
|
int
|
Number of local worker processes over which to spread the replicates (default 1, serial). The replicate seeds are derived once from the specification's seed, so any worker count returns results identical to a serial run. |
1
|
Returns:
| Type | Description |
|---|---|
dict
|
Keys: |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If the design is not a single two-level between-subjects Gaussian factor. |
Notes
Requires scipy (imported lazily, in each worker process when parallel); install the
power or dev extra.
pilotr.power.power_curve ¶
power_curve(spec, subject_ns, n_sims=1000, alpha=0.05, workers=1)
Power curve over sample size for a two-group Gaussian design.
Sweep the number of subjects and compute power at each grid point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
dict or str
|
A two-group Gaussian design specification. |
required |
subject_ns
|
iterable of int
|
Subject counts to evaluate. |
required |
n_sims
|
int
|
Replicates per grid point (default 1000). |
1000
|
alpha
|
float
|
Significance level (default 0.05). |
0.05
|
workers
|
int
|
Number of local worker processes over which to spread the replicates at each grid point (default 1, serial). One process pool is started and reused across the whole sweep, and any worker count returns results identical to a serial run. |
1
|
Returns:
| Type | Description |
|---|---|
list of dict
|
One dict per grid point, with keys |
See Also
solve_curve, target_n : solve this curve for the sample size that meets a target power.
pilotr.power.power_mixed ¶
power_mixed(spec, n_sims=50, alpha=0.05, workers=1)
Crossed mixed-effects simulation-based power in Python, via statsmodels MixedLM with by-subject and by-item random intercepts and slopes as (independent) variance components.
This is pilotr's own simulation loop over the portable design specification, not a
wrapper around an existing power package. It covers territory pioneered by simr (Green
and MacLeod, 2016, doi:10.1111/2041-210x.12504) and mixedpower (Kumle, Vo and Draschkow,
2021, doi:10.3758/s13428-021-01546-0); pilotr differs in being driven by the portable
cross-language spec, in reporting Type S and Type M errors and in built-in
parallelisation via workers (default 1, serial), which returns results identical to a
serial run for any worker count.
Accuracy caveat (verified behaviour, not a bug). statsmodels fits crossed random effects
as independent variance components and, in our tests, substantially overstates random-slope
variance (for example, a by-subject slope SD of about 0.12 estimated against 0.04 true).
This inflates the fixed-effect standard error. For designs with by-subject or by-item random
slopes, the backend is therefore markedly conservative. On the crossed RT design it
reports power around 0.48, against the R/lme4 reference of about 0.73. It still recovers the
fixed effect (mean estimate about 0.048 against 0.05 true) and the Type S and Type M
quantities correctly, and it is reliable for random-intercept designs. We recommend treating
its output as a conservative lower bound and using the R/lme4 power_mixed as the reference
whenever random slopes or random-effect correlations matter. Data generation is identical
across R and Python. This discrepancy arises solely in the Python LMM estimator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
dict or str
|
A design specification (dict or path to a JSON file) with exactly one within-unit factor and a crossed design with an item unit. |
required |
n_sims
|
int
|
Number of Monte Carlo replicates (default 50, smaller than |
50
|
alpha
|
float
|
Two-sided significance level (default 0.05). |
0.05
|
workers
|
int
|
Number of local worker processes over which to spread the replicates (default 1, serial). The replicate seeds are derived once from the specification's seed, so any worker count returns results identical to a serial run. |
1
|
Returns:
| Type | Description |
|---|---|
dict
|
Keys: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the spec has no item unit ( |
NotImplementedError
|
If the design does not have exactly one within-unit factor. |
Notes
Requires the mixed extra (statsmodels and pandas, imported lazily in each worker
process when parallel).
pilotr.solve.solve_curve ¶
solve_curve(curve, target, x=None, y=None, n=None, effect=None, transform='sqrt', level=0.95)
Solve a simulated design curve for the value that meets a target.
Take the curve a sweep has already produced, fit the decision rate against the swept value, and solve for the value at which the rate meets a target. The solved value comes with a confidence interval, because a point read off a simulated curve without one repeats the overconfidence that design analysis exists to expose.
The input is the list of records power_curve returns, used as it stands. The swept
value is taken from the leading column, which is where the curve functions put it, and
the rate from power or p_meaningful, whichever the curve carries. Each rate is a
proportion over a known number of replicates, and that count, read from n_returned,
n_converged or n_sims, weights the fit: a rate over 200 replicates should count for
more than a rate over 20.
The fit is a binomial regression with a probit link, and the solved value is the swept
value at which the fitted rate equals target. The probit is chosen because power is a
normal tail probability: under the normal approximation to a two-group comparison, the
probit of power is linear in the square root of the sample size, so the model has the
shape a design analysis already implies. Measured against R's stats::power.t.test across
twelve combinations of effect size and target power on each of three grid shapes, at 400
replicates a point, the solved sample size fell within 2.9% of the analytic answer on
average, against 3.4% for a logit fitted the same way.
The interval is the delta-method interval of R's MASS::dose.p, computed on the scale
named by transform and mapped back, so it is symmetric on that scale and asymmetric on
the natural one. That asymmetry is the honest shape: at the top of a power curve a given
change in rate costs far more sample size than the same change lower down. Where the
two-parameter model does not describe the curve, the interval is widened by the
heterogeneity factor of probit analysis, Pearson's chi-square over its degrees of freedom
(Finney, 1971), reported as dispersion. It is floored at 1, so a well-fitting curve is
left alone and a badly-fitting one cannot report a narrower interval than its own
residuals justify.
The default transform of "sqrt" suits a sample-size axis, where a rate rises with the
square root of the sample size. Sweep something else, an effect size or a random-effect
standard deviation, and "identity" is usually right.
Nothing here extrapolates. A curve whose rates do not straddle the target is refused, with the range it did cover reported, and so is a fit that solves outside the swept range. A curve whose fitted slope cannot be told from zero is refused too: the crossing is then compatible with any value at all, and an interval that said otherwise would be false.
What the interval covers is the Monte Carlo uncertainty of the fit, not the gap between
the fitted shape and the true curve. Across 36 checks against R's stats::power.t.test at
400 replicates a point, the solved size sat within 2.9% of the analytic answer on average
and within 6.9% at worst, and the nominal 95% interval covered the analytic value 35 times
out of 36. Nearly all of that error is the Monte Carlo noise the interval is describing,
and it falls with the square root of the replicate count. Raise the count far enough and
the interval narrows onto a fitted shape that is still slightly the wrong shape, so
replicates alone do not make a solved size arbitrarily accurate. The remedies are a finer
grid, more replicates, or a design with a closed form to check against.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
curve
|
sequence of dict
|
A curve, as returned by |
required |
target
|
float
|
The decision rate to solve for, strictly between 0 and 1. |
required |
x
|
str
|
Name of the column holding the swept value. |
None
|
y
|
str
|
Name of the column holding the decision rate. |
None
|
n
|
str or float or sequence
|
The number of replicates behind each rate, either the name of a column or a numeric
value. |
None
|
effect
|
str
|
Which focal effect to solve for, when the curve holds more than one. Matched against
the |
None
|
transform
|
str
|
The scale the swept value is fitted on: |
'sqrt'
|
level
|
float
|
Confidence level for the reported interval (default 0.95). |
0.95
|
Returns:
| Type | Description |
|---|---|
dict
|
Keys: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
References
Fieller, E. C. (1954). Some problems in interval estimation. Journal of the Royal Statistical Society: Series B, 16(2), 175-185. doi:10.1111/j.2517-6161.1954.tb00159.x
Finney, D. J. (1971). Probit analysis (3rd ed.). Cambridge University Press.
Examples:
>>> curve = [{"n_subject": 40, "power": 0.38, "n_sims": 100},
... {"n_subject": 100, "power": 0.74, "n_sims": 100},
... {"n_subject": 160, "power": 0.89, "n_sims": 100}]
>>> round(solve_curve(curve, target=0.8)["value"])
120
pilotr.solve.target_n ¶
target_n(curve, target=0.8, **kwargs)
Solve a power curve for the sample size that reaches a target power.
The sample-size case of solve_curve, and the number a power analysis is usually run to
obtain. Takes the curve a sweep over sample size has produced and returns the size at
which power reaches target, rounded up to a whole number of units alongside the exact
solution.
Everything solve_curve does applies here, including its refusals: a curve that never
reaches the target within the sizes it swept is refused outright, and the
reported interval can extend past the largest size simulated, which means the sweep was
too narrow to settle the question.
The whole-number fields round up rather than to nearest, because a design cannot recruit a fraction of a subject and rounding down would leave the study short of the target it was sized for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
curve
|
sequence of dict
|
A power curve, as returned by |
required |
target
|
float
|
The power to reach (default 0.8, the convention this package's plots draw a line at). |
0.8
|
**kwargs
|
Further arguments passed to |
{}
|
Returns:
| Type | Description |
|---|---|
dict
|
The dict |
Examples:
>>> curve = [{"n_subject": 40, "power": 0.38, "n_sims": 100},
... {"n_subject": 100, "power": 0.74, "n_sims": 100},
... {"n_subject": 160, "power": 0.89, "n_sims": 100}]
>>> target_n(curve)["n"]
120
Reproducibility¶
The shared random-number stream and the numerical primitives that make a Python run and an R run agree bit for bit.
pilotr.core.RNG ¶
Shared cross-language random-number generator.
The combined linear congruential generator of L'Ecuyer (1988), implemented identically
in R and Python so that a given seed yields the same stream in both. All intermediate
products stay below 2**53, so the arithmetic is exact in IEEE-754 doubles. The
draw-order contract is documented in the specification at
https://github.com/pablobernabeu/pilotr/blob/main/spec/SPEC.md.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seed
|
int
|
Seed for the generator (coerced to a non-negative integer). |
required |
pilotr.core.replicate_seeds ¶
replicate_seeds(base, n: int) -> list[int]
The seeds pilotr's replicate loops give to their replicates, from a specification's seed.
Until 0.3 the rule was base + i. Consecutive seeds are not independent streams in this
generator: seeding sets s1 to 1 + (seed mod 2147483562) and s2 from s1, and only
ten warm-up draws are discarded, so replicate i and replicate i + 1 begin a few steps
apart in the same sequence rather than in unrelated parts of it. Measured over 2,000
replicates, the first draw of replicate i correlated 0.95 with the first draw of i + 1.
An arithmetic scramble does not fix that, because the seeding rule is itself linear in the seed: adding a Weyl increment and applying a Lehmer step left the first draws correlated at -0.27. Drawing the seeds from the shared generator does work, since successive outputs of the combined generator are what it exists to make look independent. Duplicates are skipped, so no two replicates are handed the same seed and silently produce identical data, and the skipping is deterministic, so this matches the R implementation exactly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
int
|
The specification's seed. |
required |
n
|
int
|
How many replicate seeds to return. |
required |
Returns:
| Type | Description |
|---|---|
list of int
|
|
pilotr.core.as241 ¶
as241(p: float) -> float
Inverse standard-normal CDF (quantile function), Wichura's (1988) Algorithm AS 241.
The PPND16 routine underlying R's qnorm, accurate to full double precision.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p
|
float
|
A probability in the open interval (0, 1). |
required |
Returns:
| Type | Description |
|---|---|
float
|
The standard-normal quantile for |
pilotr.core.inv_logit ¶
inv_logit(x: float) -> float
Logistic (inverse-logit) link, 1 / (1 + exp(-x)).
The two branches keep the exponent negative on either side of zero, so a large |x|
underflows towards 0 or 1 where a single expression would overflow math.exp. The R twin,
.inv_logit, branches at the same value, so both languages evaluate the same expression
for a given x.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
A value on the logit scale. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The corresponding probability, in (0, 1). |