Skip to content

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 (the default) applies validate_spec strictly; False skips validation; "lenient" or any other truthy non-True value validates with strict=False.

True

Returns:

Type Description
dict

The parsed specification, ready for simulate or power.

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 False to load a specification carrying private annotations, accepting that a misspelled field will then be ignored in silence.

True

Returns:

Type Description
dict

The specification, unchanged.

Raises:

Type Description
ValueError

If the specification is invalid. All problems found are reported together.

pilotr.validate.SPEC_VERSION module-attribute

SPEC_VERSION = '0.3'

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 .json extension, for example "between_2group_gaussian". When None (the default), the available example names are returned in place of a path.

None

Returns:

Type Description
list of str, or str

When name is None, the available example names. Otherwise, the path to that example's JSON file, ready to pass to :func:pilotr.simulate.load_spec.

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 dict or a path to a JSON spec file. The format is documented at https://github.com/pablobernabeu/pilotr/blob/main/spec/SPEC.md.

required
validate bool

Whether to validate the specification first. The default True catches the errors that would otherwise pass silently, such as a mistyped coefficient key, which resolves to no column and so sets that effect to zero. Validation costs a few milliseconds, so replicate loops validate once and then pass False.

True

Returns:

Type Description
Dataset

A table with one row per observation: a subject column, an optional item column, any grouping, factor, and continuous-predictor columns, and the response column named by spec["response"]["name"].

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.

column

column(name)

Return the values in column name as a list.

head

head(n=6)

Return the first n rows (default 6) as a list of dicts.

to_csv

to_csv(path)

Write the data set to path as CSV: a header row then one row per observation.

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: n_sims, alpha, power, n_significant, true_effect, mean_estimate, type_s, type_m. Both design-analysis quantities are nan when no replicate reached significance and when the true effect is zero, as in a null condition: neither is defined without a true value to compare against, and Type M divides by it.

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 n_subject, power, type_m, n_sims and n_significant. The two counts are what solve_curve weights the curve by, since a power estimate over 1000 replicates should count for more than one over 50.

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 power's 1000 because each replicate fits a mixed model).

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: backend (the estimator used), n_sims, n_converged (how many replicates the model fit), alpha, power, n_significant, true_effect, mean_estimate, type_s, type_m. power is the proportion of significant results among the n_converged converged replicates, not among n_sims. type_s and type_m are nan when no replicate reached significance and when the true effect is zero.

Raises:

Type Description
ValueError

If the spec has no item unit (power_mixed requires a crossed design).

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 power_curve: one record per swept value, holding the swept value, a decision rate, and the number of replicates behind it.

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, the default, takes the leading column.

None
y str

Name of the column holding the decision rate. None, the default, takes power or p_meaningful, whichever is present.

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, the default, takes n_returned, n_converged or n_sims, whichever is present.

None
effect str

Which focal effect to solve for, when the curve holds more than one. Matched against the effect or param column. None, the default, uses every record, which is correct only when the curve holds one effect.

None
transform str

The scale the swept value is fitted on: "sqrt" (the default, for a sample size), "identity" or "log".

'sqrt'
level float

Confidence level for the reported interval (default 0.95).

0.95

Returns:

Type Description
dict

Keys: value (the solved swept value), lo and hi (its confidence bounds), level, target, se (the delta-method standard error on the fitted scale, the scale on which the interval is symmetric), dispersion (the heterogeneity factor applied, 1 where the model fits), x and y (the columns used), transform, intercept and slope (the fitted coefficients), n_points (the number of curve points the fit used), and x_min and x_max (the swept range). A bound is allowed to fall outside that range. When one does, the sweep was too narrow to pin the value down and should be widened. A dispersion well above 1 says the curve is not the shape the model assumes, so the solve deserves a wider grid or more replicates before it deserves any trust.

Raises:

Type Description
ValueError

If target or level is not strictly between 0 and 1, if the curve carries fewer than three usable points, if its rate does not vary, if its rates do not straddle target within the swept range, if the fitted slope cannot be told from zero, or if the solve lands outside the swept range.

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 power_curve.

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 solve_curve, such as effect to pick one focal effect out of a curve holding several, or level for the interval.

{}

Returns:

Type Description
dict

The dict solve_curve returns, with n, n_lo and n_hi added: value, lo and hi rounded up to whole numbers.

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

uniform

uniform() -> float

Return one draw from the standard uniform distribution on (0, 1).

normal

normal() -> float

Return one draw from the standard normal distribution.

normals

normals(k: int) -> list[float]

Return a list of k standard-normal draws.

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

n distinct seeds.

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 p.

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).