Cross-language reproducibility¶
The point of the shared specification is that one design produces the same data in R and in Python. The two implementations never call each other; they share a numerical contract.
The shared generator¶
Native random-number generators differ across ecosystems (R uses Mersenne-Twister with
inversion, NumPy uses PCG64), so a naive port would diverge. pilotr instead ships one
generator implemented identically in both languages.
Uniforms come from L'Ecuyer's (1988) combined linear congruential generator. Every
intermediate product stays below 2**53, so the arithmetic is exact in IEEE-754 doubles
and in Python integers alike. Normals use Wichura's (1988) Algorithm AS 241 inverse-CDF, the
same routine R's qnorm uses, so deviates agree to full double precision. Everything else
(Cholesky-correlated random effects, inverse-CDF Poisson and ordinal draws, Marsaglia and
Tsang gamma draws for the Beta family) derives from those two through a documented, identical
draw order.
The draw order is specified on the Specification page.
Determinism in Python¶
The same specification and seed always give the same data.
from pilotr import simulate
spec = {
"name": "demo", "seed": 2024,
"units": {"subject": {"n": 200}},
"factors": [{"name": "group", "levels": ["a", "b"],
"contrasts": {"effect": [-0.5, 0.5]}, "between": "subject"}],
"fixed": {"intercept": 100, "coefficients": {"effect": 5}},
"response": {"family": "gaussian", "name": "score", "sigma": 10},
}
print(simulate(spec).rows == simulate(spec).rows)
True
Changing the seed changes the draws, while the structure and the ground-truth parameters stay fixed.
spec2 = dict(spec, seed=spec["seed"] + 1)
print(simulate(spec).column("score") == simulate(spec2).column("score"))
False
Verifying parity¶
Simulate the same spec and seed in both languages and compare. In Python:
from pilotr import simulate, load_spec
simulate(load_spec("design.json")).to_csv("py.csv")
In R:
library(pilotr)
write.csv(simulate_design(load_spec("design.json")), "r.csv", row.names = FALSE)
The two CSVs compare exactly, up to CSV number formatting. The repository's python/examples/parity_check.py
runs this comparison across the worked example designs and reports the maximum difference:
zero for every design with rounded responses, and below 1e-14 for the unrounded continuous
design, where R's CSV writer prints 15 significant digits. The script's small tolerance
(1e-6) absorbs that number formatting only, never a divergence in the generators.
A simulated crossed design has one row per subject-by-item observation. The first rows of the worked crossed reaction-time example look like this (the same rows the R package produces):
from pilotr import simulate, load_spec, pilotr_example
data = simulate(load_spec(pilotr_example("crossed_mixed_rt")))
print(table(data.head(6)))
| subject | item | condition | RT |
|---|---|---|---|
| 1 | 1 | related | 669 |
| 1 | 1 | unrelated | 727 |
| 1 | 2 | related | 699 |
| 1 | 2 | unrelated | 503 |
| 1 | 3 | related | 661 |
| 1 | 3 | unrelated | 517 |