Experimental Design and Inference
Overview
Implements a comprehensive suite of experimental designs, both fixed (e.g., block, stratified, matched-pair, cluster, factorial, and mixed-integer-programming-based optimal designs) and sequential (including matching-on-the-fly designs, biased coin designs, and covariate-adaptive urn designs that assign treatment one subject at a time while maintaining covariate balance), for continuous, incidence, count, proportion, survival, and ordinal response types. For each design and response type combination, provides the corresponding inference procedures, including exact, asymptotic, distribution-free, and resampling-based (bootstrap, jackknife, and randomization) methods, so that estimation and testing are always matched to how the data were generated. An 'InferenceSuite' facility runs all applicable inference procedures for a given design and response type at once and reports a single Cauchy-combined p-value summarizing their evidence. A built-in simulation framework supports power analysis and operating-characteristic studies across designs, response types, and inference procedures, with optional parallelization via 'mirai'. Missing covariate data is handled automatically via built-in imputation. Core numerical routines are implemented in C++ via 'Rcpp' for speed on large designs and simulation studies. Machine-specific tuning for optimization is included.
This is the first version submitted to CRAN. Versions 1.0.0 and 1.0.1 were
GitHub-only releases; their entries below record what changed in each. Most of
1.0.2 is correctness work found by the extended comprehensive test suite and
by writing unit tests: resampling and randomization results that were silently
wrong in specific classes, a few kernel defects (including one memory-safety
bug), and standard-error and confidence-interval fixes.
Breaking changes
toggle_asserts(),set_num_cores(), and the internal thread-count bookkeeping it drives no longer set globaloptions()as a side effect (CRAN policy: a package must not modify and leave changed the user's session options).toggle_asserts()now stores its flag in the package's own internal state; calling it no longer changes whatgetOption("edi.run_asserts")returns (a user's ownoptions(edi.run_asserts = ...)is still read and honored). Likewise,set_num_cores()/set_package_threads()no longer setoptions(mc.cores = ...)(nothing in EDI read it back; downstream code relying on this package to set it forparallel/pbmcapplyneeds to set it itself now) and no longer callfixest::setFixest_nthreads()(which setsoptions(fixest_nthreads), the user's own option, not EDI's to change). No change to which thread counts are actually used internally — only to these previously-observableoptions()side effects.
Show all changes in 1.0.2Hide the rest of the changes in 1.0.2
New features
InferenceCountQuasiPoissonnow composes the marginal-estimand component (MarginalEstimand), likeInferenceCountPoisson, so marginal (standardized) effect estimands are available for the quasi-Poisson class as well.
Bug fixes
- Reused-worker resampling reused a stale cached fit across draws for classes whose fit is cached under a custom guard key (first found in
InferenceOrdinalGCompMeanDiff, whosecached_values$mdguard survived the loader's narrow reset list). Every draw then returned the same number, the resampling distribution collapsed to a point mass, andcompute_rand_two_sided_pval()sat at its floor regardless of the true effect (reject rate 1.0 at a true null). The worker loaders for randomization, non-parametric bootstrap, m-out-of-n bootstrap and randomization-bootstrap draws now reset the cached fit state between draws, and a structural regression test asserts that every class exposing a reusable-worker method returns a non-degenerate distribution. InferencePropGCompMeanDiffandInferenceSurvivalGLMMWeibullFrailtyLoggammaIVWCreturnedNA(or a stale value) in randomization inference: the first from a sample-usability gate applied to the wrong resampling context, the second from its estimate-onlyw_starpooling. Both are fixed and no longer listed as known-broken.- The KK survival compound classes
InferenceSurvivalKKLWACoxPHOneLik,InferenceSurvivalGLMMWeibullFrailtyLoggammaOneLikandInferenceSurvivalGLMMWeibullFrailtyLoggammaIVWCfed realNAs into the fitter for every censored subject during randomization inference:compute_treatment_estimate_during_randomization_inference()re-readyfrom the design and deriveddead = as.numeric(!is.na(y)), but since they/y_L/y_Rmigration the design'syusesNAto mark a censored observation. On simulated data with a strong effect the randomizationp-value was 0.02 with no censoring and 0.97 with about 20% censoring.y/deadare now derived asDesign$get_effective_time()/$get_effective_dead()do. A sweep of the other 16 survival classes found no further instance. InferenceOrdinalCloglogRegr's parametric-bootstrapp-value was badly over-rejecting (60% at a true null). One cause is fixed: the delta-constrained null refit started from a single cold start and could stall; it is now multi-start (the same fixInferenceOrdinalStereotypeLogitRegrreceived earlier). *A second cause, a sign mismatch in the shared bootstrap-data simulator, is root-caused but not fixed here.*- The Efron biased-coin randomization null draws (
generate_permutations) compared weighted arm counts (n_T * prob_Tvsn_C * (1 - prob_T)) instead of the raw arm countsDesignSeqOneByOneEfronactually compares, so for anyprob_T != 0.5the randomization null did not follow the design's own assignment rule. It now uses the raw counts. fast_probit_regression's L-BFGS objective held a danglingEigen::Refto a temporary, giving non-deterministic fits andNaNnegative log-likelihoods. The objective now owns its design matrix.fast_log_binomial_regression's weighted log-likelihood let zero-weight rows constrain the[0, 1]mean support although they are absent from the likelihood; zero-weight rows are now skipped, so the identity- and log-link binomial fits no longer reject valid data because of them.- The robust-regression bootstrap kernel accepted any
methodstring and would throw inside an OpenMP region, terminating R;methodis now validated ("M"or"MM") before the parallel region. stable_signature()(used to key cached randomization state) hashed a strided sample of the serialized object, which collided on 0/1 integer permutation matrices; it now hashes the full serialization.InferenceCountPoisson's covariance falls back to the Fisher information when the kernel returns noX'WX;InferenceIncidLogRegrgained an estimand-aware standard error and degrees of freedom.- Weibull and Cox fits with fixed (held) coefficients now compute the covariance from the free-parameter information block only and expand it back, so the free coefficients' variances are finite and the fixed parameters' rows and columns are
NaNby convention; the Cox cluster-robust sandwich does the same. - OpenMP's primary thread no longer polls R's interrupt machinery while worker threads are active in the Wilcoxon-Hodges-Lehmann kernels.
fast_gaussian_lmm_gls_cpp()(the fixed-variance-component GLS solve used by the randomization and non-studentised bootstrap fast paths ofInferenceContinKKGLMM) mixedX'Xwith a cross-term scaled by an extra1/sigma_e^2, so it returned the wrong coefficient whenever the residual standard deviation was not exactly 1 (on a paired-plus-singleton fixture withsigma_e = 0.4the slope was -0.53 instead of the correct 0.63; atsigma_e = 1the two agreed). It now matches an explicit GLS to machine precision at everysigma_e.- The KK21 stepwise weight selection for ordinal responses read a nonexistent field (
ssq_b_2) fromfast_ordinal_regression_with_var_cpp(); it now reads the returnedssq_b_j. fast_zero_one_inflated_beta_cpp()did not check the length of a suppliedwarm_start_params(orwarm_start_fisher_info, or the row counts ofX,X_zero_oneandy), so a mis-sized start let the optimizer read and write past its parameter buffer (heap corruption, found under valgrind; it aborted R withfree(): invalid next size). It now raises an ordinary error.InferencePropZeroOneInflatedBetaRegralways passed a correctly sized start and was not affected.fast_ordinal_clmmnow errors on mismatchedX/y/group_idlengths instead of reading out of bounds.InferenceIncidRiskDiff,InferenceCountRobustPoisson, and the zero-inflated/hurdle Poisson classes reported a confident (often zero-width) confidence interval andp-value on a perfectly or near-perfectly fit response (e.g.yfully separated byw, or constant), because the Huber-White/model-based standard error collapses to a numerically-zero-but-positive value there. Found via a rawcomprehensive_testsresults audit's new adversarial-data/fault-injection sweep.robust_sandwich_variance()and the affected classes' standard-error paths now treat a variance below.Machine$double.epsas non-estimable (NA), not as precision.InferenceCountHurdlePoisson/InferenceCountZeroInflatedPoissonreported a confident estimate andp-value on a hurdle fit that has no MLE by construction (every positive count equal to 1, so the zero-truncated Poisson likelihood only improves aslambda -> 0). Non-estimability is now decided from the data alone (not from what the optimizer/glmmTMB happens to return), so the result isNAon every platform.InferenceRandCustom$compute_rand_confidence_interval()'s fast path ignored the trialdeltashift during the randomization search, so the returned interval was in the custom statistic's own scale (e.g. centred on a Welch-t value of ~2-4) rather than the response's scale. Thecomprehensive_testsharness's own custom-statistic randomization-CI check also fed it a non-translation-equivariant statistic (Welch t), which cannot give a meaningful CI under this or any correct implementation; it now uses a difference-in-means statistic for that check.InferenceAllSimpleWilcox$compute_asymp_confidence_interval(alpha)cached and returned the 95% Wilcoxon interval for everyalpha, so a 90% or 99% interval silently came back at the wrong nominal level.InferenceOrdinalAdjCatLogitRegr's parametric-bootstrap confidence interval could be wildly wrong (e.g.[-30, -11]around an estimate of-0.09) because each simulated replicate's treatment coefficient was read from the wrong field of the refit object (fit$b, slopes only) when the anchor fit's index into its own coefficient vector (fit$params, thresholds + slopes) pointed somewhere else entirely onfit$b. Every parametric-bootstrap replicate now reads the same field the anchor fit used.InferenceOrdinalContRatioRegr(and, via the same hardened QR-column-dropping helper, 8 other ordinal threshold-model classes) could silently fit from a garbage cold start and "converge" after one iteration when the design matrix contained columns collinear with the model's *implicit* per-stage intercept (e.g. a stratified design's two complementary factor dummies) —qr()alone, with no intercept column to check against, reported the matrix as full rank. The column-dropping helper now optionally includes that implicit intercept in its rank check.- A likelihood-ratio confidence interval whose Newton/bisection inversion converges to within a small tolerance of the point estimate — without landing on it exactly — was reported as a zero-width interval instead of falling back to the Wald interval (or
NA), because the "failed inversion" sentinel check required bit-identical bounds.InferenceOrdinalStereotypeLogitRegr'scompute_lik_ratio_confidence_interval()hit this on ~6% of one audited sample. Itscompute_lik_ratio_bootstrap_confidence_interval()had a related bug: a bootstrapp-value belowalpha*at the point estimate itself* (which should be ~1, since the LR statistic there is 0) was silently treated as a legitimate zero-width interval rather than a failed constrained-fit refit; it now reportsNA. compute_rand_bootstrap_confidence_interval(type = "smoothed")andcompute_rand_bootstrap_two_sided_pval(type = "smoothed")added raw-scale Gaussian kernel noise to count responses, so a resampled zero could become a slightly negative non-integer. Under the CI inversion's multiplicative count shift that became a large negative integer, the Rcpp Poisson-GLMM fit gave up, and the glmmTMB fallback rejected every such draw (GLMM FIT ERROR: negative values not allowed for the 'Poisson' family), corrupting the null distribution and yielding a degenerate conservative bound (e.g.InferenceCountKKGLMM). Withuse_rcpp = FALSEevery draw failed and the p-value wasNA. Kernel noise on count responses is now rounded and floored at zero so the resampled draw stays on the non-negative integer support (the same convention the count shift already uses); other response types are unchanged.InferenceCountHurdlePoisson/InferenceCountHurdleNegBin/InferenceCountZeroInflatedPoisson/InferenceCountZeroInflatedNegBin's standard error could fall through to the generic information-matrix-inverse fallback on a hurdle fit with no MLE by construction (every positive count equal to 1), and whether that generic inversion happened to return a spurious finite value or correctly fail depended on the LAPACK/BLAS backend (confirmed via CI to differ between this environment, where it correctly returned non-estimable, and Windows). The data-driven non-estimability check that already gated the estimate andp-value now also runs before this generic fallback, so the standard error isNAon every platform.SimulationFramework's internal design/inference-combination builder unconditionally disabled package assertions and never restored them; a caller running withturn_off_asserts_for_speed = FALSE(assertions intentionally kept on) had assertions silently disabled for the rest of the R session after this method ran once, which could mask unrelated argument-validation bugs in later, unrelated code in the same session. It now restores whatever assert state was in effect on entry.
From NEWS.md in the release
Install
Health
- OK13 OKNow
- OK3 OK2026-10-07
Documentation
- Help pages
- 371 (190 internal)includes a package overview page
- Examples that run
- 54%86 run in a plain check (1 of them under a condition) · 73 only when donttest examples are enabled (R CMD check --as-cran)
- Exported functions with examples
- 57%
- Documented parameters
- 99%
- Return-value docs
- 65%
- Help pages citing references
- 30%
- Backend Contracts: fast_* and C++ Utilities HTML
- Cookbook: Continuous Outcome, End to End HTML
- Cookbook: Count Outcome, End to End HTML
- Cookbook: Incidence (Binary) Outcome, End to End HTML
- Cookbook: Ordinal Outcome, End to End HTML
- Cookbook: Proportion Outcome, End to End HTML
- Cookbook: Survival Outcome with Censoring, End to End HTML
- Extending EDI with Your Own Inference and Design Classes HTML
- Notation Glossary HTML
- Reproducibility: RNG and Seed Conventions HTML
Show 1 moreHide 1 vignette
- Validation Evidence HTML
Downloads
Repository
- PRs merged (all time)
- 0%
- Median days to close, 50 most recently updated closed issues
- 21
- Median days to close, 50 most recently updated closed PRs
- 0
Issues over time
Commits over time
Commit identities over time
14 development-tooling and community-health practices detected across 6 families in the upstream repository
Checks run against github.com/kapelner/edi on 2026-10-18.
The R CMD check workflow runs on Linux, macOS and Windows, and on R-devel
GitHub Pages site last deployed 2026-10-18 · The repository has a site deploy workflow
Show all practicesHide 2 practices
This repository shows signs of AI coding tools.
- Claude Codefirst seen 2026-02-03Seen in the repository: CLAUDE.mdNamed in .gitignore: .claude, .mcp.jsonPull request it wrote: opened by a project memberCommits: at least 71 crediting it in the message
- Codexfirst seen 2026-04-05Named in .gitignore: .codex
- Aiderfirst seen 2026-05-26Named in .gitignore: .aider*
- Gemini CLIfirst seen 2026-08-05Seen in the repository: GEMINI.mdNamed in .gitignore: .gemini
- GitHub Copilotfirst seen 2026-09-15Seen in the repository: .github/copilot-instructions.md, .github/workflows/copilot-setup-steps.yml
- Cursorfirst seen 2026-09-16Seen in the repository: .cursor
- Claude Sonnet 541 commits · Jul to Sep 2026
- Claude Fable 511 commits · Aug to Sep 2026
- Claude Fable 5.19 commits · Sep 2026
- Claude Sonnet 4.66 commits · May to Jul 2026
- Claude Opus 53 commits · Sep 2026
1 more model not listed.
Dependencies
Installing EDI adds 23 packages (13 with compiled code, 13 MB of source tarballs). Heaviest: data.table 5.7 MB, Rcpp 1.8 MB, RcppEigen 1.7 MB.
Code & Tests
People & History
- 1.0.2Latest2026-10-07 · current release ·
3 days in CRAN's queue
Listed in CRAN's incoming queue from 2026-10-03 21:57 to 2026-10-07 06:45 UTC, in 16 readings. - RR 4.6.0 released · 2026-04-24
Package metadata
- First published
- 2026-10-07
- Total releases
- 1 / 1 yrs
- License
- GPL-3 OSI
- Minimum R
- ≥ 3.5.0
- Download size
- 3.4 MB
Cite
Cite this package
Run in R for the authors' preferred citation:
citation("EDI")The package provides a citation file that builds its entry when R runs it. Run citation() to see it.
The repository also keeps a CITATION.cff.
Cite the R Observatory
For a number measured here: a download total, a coverage figure, an archival date.
From data release v2026-10-21, which the citation names so these numbers can be found later. More on citing and the projects behind them.