bossanova 0.1.0.dev21__tar.gz → 0.1.0.dev22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/PKG-INFO +1 -1
  2. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/bootstrap.py +14 -0
  3. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/permutation.py +57 -10
  4. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample/__init__.py +2 -0
  5. bossanova-0.1.0.dev22/bossanova/internal/infer/resample/lm_operators.py +280 -0
  6. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/emm.py +39 -57
  7. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/inference.py +27 -38
  8. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/joint_tests.py +142 -18
  9. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/pyproject.toml +1 -1
  10. bossanova-0.1.0.dev21/bossanova/internal/infer/resample/lm_operators.py +0 -151
  11. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/.gitignore +0 -0
  12. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/LICENSE +0 -0
  13. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/README.md +0 -0
  14. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/__init__.py +0 -0
  15. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/README.md +0 -0
  16. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/__init__.py +0 -0
  17. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/advertising.csv +0 -0
  18. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/cake.csv +0 -0
  19. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/chickweight.csv +0 -0
  20. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/credit.csv +0 -0
  21. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/gammas.csv +0 -0
  22. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/mtcars.csv +0 -0
  23. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/penguins.csv +0 -0
  24. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/poker.csv +0 -0
  25. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/sleep.csv +0 -0
  26. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/titanic.csv +0 -0
  27. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/titanic_test.csv +0 -0
  28. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/data/titanic_train.csv +0 -0
  29. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/distributions/__init__.py +0 -0
  30. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/distributions/continuous.py +0 -0
  31. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/distributions/discrete.py +0 -0
  32. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/distributions/varying.py +0 -0
  33. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/expressions.py +0 -0
  34. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/__init__.py +0 -0
  35. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/__init__.py +0 -0
  36. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/compare.py +0 -0
  37. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/cv.py +0 -0
  38. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/deviance.py +0 -0
  39. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/f_test.py +0 -0
  40. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/helpers.py +0 -0
  41. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/ic.py +0 -0
  42. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/lrt.py +0 -0
  43. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/lrt_compare.py +0 -0
  44. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/compare/refit.py +0 -0
  45. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/__init__.py +0 -0
  46. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/builders/__init__.py +0 -0
  47. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/builders/dataframes.py +0 -0
  48. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/builders/resamples.py +0 -0
  49. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/builders/results.py +0 -0
  50. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/builders/specs.py +0 -0
  51. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/builders/state.py +0 -0
  52. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/schemas.py +0 -0
  53. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/structs/__init__.py +0 -0
  54. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/structs/data.py +0 -0
  55. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/structs/display.py +0 -0
  56. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/structs/explore.py +0 -0
  57. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/structs/formula.py +0 -0
  58. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/structs/specs.py +0 -0
  59. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/structs/state.py +0 -0
  60. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/containers/validators.py +0 -0
  61. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/design/__init__.py +0 -0
  62. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/design/coding.py +0 -0
  63. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/design/names.py +0 -0
  64. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/design/reference.py +0 -0
  65. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/design/z_matrix.py +0 -0
  66. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/__init__.py +0 -0
  67. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/convergence.py +0 -0
  68. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/diagnostics.py +0 -0
  69. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/dispatch.py +0 -0
  70. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/glm.py +0 -0
  71. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/glmer.py +0 -0
  72. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/lmer.py +0 -0
  73. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/ols.py +0 -0
  74. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/predict.py +0 -0
  75. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/fit/varying.py +0 -0
  76. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/__init__.py +0 -0
  77. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/bundle.py +0 -0
  78. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/contrast_registry.py +0 -0
  79. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/contrast_specs.py +0 -0
  80. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/design.py +0 -0
  81. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/encoding.py +0 -0
  82. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/evaluate.py +0 -0
  83. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/evaluate_contrast.py +0 -0
  84. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/evaluate_newdata.py +0 -0
  85. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/evaluate_transforms.py +0 -0
  86. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/helpers.py +0 -0
  87. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/parse.py +0 -0
  88. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/parser/__init__.py +0 -0
  89. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/parser/expr.py +0 -0
  90. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/parser/parser.py +0 -0
  91. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/parser/scanner.py +0 -0
  92. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/parser/token.py +0 -0
  93. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/formula/random_effects.py +0 -0
  94. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/__init__.py +0 -0
  95. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/asymptotic.py +0 -0
  96. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/cv.py +0 -0
  97. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/dispatch.py +0 -0
  98. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/formula_utils.py +0 -0
  99. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/mee.py +0 -0
  100. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/params.py +0 -0
  101. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/prediction.py +0 -0
  102. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/profile.py +0 -0
  103. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample/common.py +0 -0
  104. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample/core.py +0 -0
  105. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample/glmer.py +0 -0
  106. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample/lmer.py +0 -0
  107. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample/results.py +0 -0
  108. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample/simulate.py +0 -0
  109. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/resample_bundle.py +0 -0
  110. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/satterthwaite_emm.py +0 -0
  111. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/infer/simulation.py +0 -0
  112. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/__init__.py +0 -0
  113. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/bracket_contrasts.py +0 -0
  114. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/compute.py +0 -0
  115. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/conditions.py +0 -0
  116. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/contrasts.py +0 -0
  117. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/explore.py +0 -0
  118. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/explore_parser.py +0 -0
  119. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/explore_scanner.py +0 -0
  120. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/factors.py +0 -0
  121. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/grid.py +0 -0
  122. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/matrices.py +0 -0
  123. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/resolve.py +0 -0
  124. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/slopes.py +0 -0
  125. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/transforms.py +0 -0
  126. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/marginal/validation.py +0 -0
  127. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/__init__.py +0 -0
  128. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/backend/__init__.py +0 -0
  129. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/backend/dispatch.py +0 -0
  130. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/backend/jax.py +0 -0
  131. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/backend/numpy.py +0 -0
  132. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/backend/protocol.py +0 -0
  133. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/batching.py +0 -0
  134. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/config.py +0 -0
  135. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/convergence.py +0 -0
  136. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/differentiation.py +0 -0
  137. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/__init__.py +0 -0
  138. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/algebra.py +0 -0
  139. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/base.py +0 -0
  140. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/core.py +0 -0
  141. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/derived.py +0 -0
  142. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/factories.py +0 -0
  143. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/plotting.py +0 -0
  144. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/distributions/probability.py +0 -0
  145. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/__init__.py +0 -0
  146. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/binomial.py +0 -0
  147. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/create.py +0 -0
  148. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/gamma.py +0 -0
  149. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/gaussian.py +0 -0
  150. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/links.py +0 -0
  151. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/poisson.py +0 -0
  152. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/response.py +0 -0
  153. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/schema.py +0 -0
  154. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/family/tdist.py +0 -0
  155. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/__init__.py +0 -0
  156. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/diagnostics.py +0 -0
  157. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/estimation.py +0 -0
  158. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/hypothesis.py +0 -0
  159. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/information_criteria.py +0 -0
  160. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/multiplicity.py +0 -0
  161. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/profile.py +0 -0
  162. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/sandwich.py +0 -0
  163. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/satterthwaite.py +0 -0
  164. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/wald_variance.py +0 -0
  165. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/inference/welch.py +0 -0
  166. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/linalg/__init__.py +0 -0
  167. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/linalg/qr.py +0 -0
  168. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/linalg/rank.py +0 -0
  169. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/linalg/schur.py +0 -0
  170. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/linalg/sparse.py +0 -0
  171. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/linalg/svd.py +0 -0
  172. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/predict.py +0 -0
  173. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/rng.py +0 -0
  174. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/rounding.py +0 -0
  175. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/__init__.py +0 -0
  176. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/glm.py +0 -0
  177. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/glmer.py +0 -0
  178. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/heuristics.py +0 -0
  179. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/initialization.py +0 -0
  180. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/lambda_builder.py +0 -0
  181. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/lambda_sparse.py +0 -0
  182. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/lambda_template.py +0 -0
  183. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/lmer.py +0 -0
  184. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/optimize.py +0 -0
  185. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/pirls_sparse.py +0 -0
  186. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/solvers/quadrature.py +0 -0
  187. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/tolerances.py +0 -0
  188. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/transforms.py +0 -0
  189. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/variance.py +0 -0
  190. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/maths/weights.py +0 -0
  191. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/rendering/__init__.py +0 -0
  192. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/rendering/latex.py +0 -0
  193. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/rendering/markdown.py +0 -0
  194. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/__init__.py +0 -0
  195. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/dgp/__init__.py +0 -0
  196. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/dgp/generate.py +0 -0
  197. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/dgp/glm.py +0 -0
  198. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/dgp/glmer.py +0 -0
  199. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/dgp/lm.py +0 -0
  200. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/dgp/lmer.py +0 -0
  201. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/harness.py +0 -0
  202. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/metrics.py +0 -0
  203. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/model_sim.py +0 -0
  204. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/simulation/power.py +0 -0
  205. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/README.md +0 -0
  206. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/__init__.py +0 -0
  207. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/cognition.py +0 -0
  208. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/compare.py +0 -0
  209. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/core.py +0 -0
  210. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/core_data.py +0 -0
  211. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/core_protocols.py +0 -0
  212. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/core_sizing.py +0 -0
  213. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/core_viz.py +0 -0
  214. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/dag.py +0 -0
  215. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/design.py +0 -0
  216. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/fit.py +0 -0
  217. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/fit_builders.py +0 -0
  218. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/fit_layers.py +0 -0
  219. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/helpers.py +0 -0
  220. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/lattice.py +0 -0
  221. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/layout.py +0 -0
  222. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/mem.py +0 -0
  223. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/mem_forest.py +0 -0
  224. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/params.py +0 -0
  225. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/predict.py +0 -0
  226. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/profile.py +0 -0
  227. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/ranef.py +0 -0
  228. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/relationships.py +0 -0
  229. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/resamples.py +0 -0
  230. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/resid.py +0 -0
  231. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/internal/viz/vif.py +0 -0
  232. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/model/__init__.py +0 -0
  233. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/model/core.py +0 -0
  234. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/model/guards.py +0 -0
  235. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/model/result.py +0 -0
  236. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/model/summary.py +0 -0
  237. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/bossanova/py.typed +0 -0
  238. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/tests/bossanova_benchmarks/bootstrap/data/README.md +0 -0
  239. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/tests/bossanova_benchmarks/insteval/data/README.md +0 -0
  240. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev22}/tests/bossanova_tests/hypothesis/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bossanova
3
- Version: 0.1.0.dev21
3
+ Version: 0.1.0.dev22
4
4
  Summary: Bridging statistical cultures with some jazz
5
5
  Author: Eshin Jolly
6
6
  License-Expression: MIT
@@ -104,6 +104,20 @@ def compute_bootstrap_params(
104
104
  # Generate all bootstrap indices at once
105
105
  boot_indices = generate_bootstrap_indices(rng, n, n_boot)
106
106
 
107
+ # LM fast path: batched OLS via chunked vmap
108
+ is_lm = (
109
+ spec.family == "gaussian"
110
+ and spec.link == "identity"
111
+ and not spec.has_random_effects
112
+ )
113
+
114
+ if is_lm:
115
+ from bossanova.internal.infer.resample.lm_operators import (
116
+ compute_batched_ols_bootstrap,
117
+ )
118
+
119
+ return compute_batched_ols_bootstrap(boot_indices, bundle.X, bundle.y)
120
+
107
121
  if n_jobs == 1:
108
122
  # Sequential execution
109
123
  boot_coefs = np.zeros((n_boot, p))
@@ -184,6 +184,42 @@ def explore_with_fit(
184
184
  )
185
185
 
186
186
 
187
+ def _compute_permutation_lm_fast(
188
+ X: np.ndarray,
189
+ y: np.ndarray,
190
+ n_perm: int,
191
+ rng: np.random.Generator,
192
+ ) -> np.ndarray:
193
+ """Compute all permutation coefficients in a single matmul.
194
+
195
+ For LM models, coefficients are a linear map of y when X is fixed:
196
+ ``coef = (X'X)^{-1} X' y``. Pre-computing the factorization once
197
+ and applying it to all permuted y vectors simultaneously avoids
198
+ the per-permutation solver overhead.
199
+
200
+ Args:
201
+ X: Design matrix, shape ``(n, p)``.
202
+ y: Original response vector, shape ``(n,)``.
203
+ n_perm: Number of permutations.
204
+ rng: NumPy random generator for reproducibility.
205
+
206
+ Returns:
207
+ Array of null coefficients, shape ``(n_perm, p)``.
208
+ """
209
+ from bossanova.internal.infer.resample.lm_operators import (
210
+ build_lm_coefficient_operator,
211
+ )
212
+
213
+ n = len(y)
214
+ perm_indices = np.stack([rng.permutation(n) for _ in range(n_perm)])
215
+ Y_perm = y[perm_indices] # (n_perm, n)
216
+
217
+ operator = build_lm_coefficient_operator(X)
218
+ # operator accepts [n, m] → [p, m], so transpose in and out
219
+ null_coef = operator(Y_perm.T).T # (n_perm, p)
220
+ return np.asarray(null_coef)
221
+
222
+
187
223
  # =============================================================================
188
224
  # Public API
189
225
  # =============================================================================
@@ -233,22 +269,33 @@ def compute_params_permutation(
233
269
 
234
270
  n_coef = len(coef)
235
271
 
236
- # Store null distribution of coefficients and t-statistics
237
- null_coef = np.zeros((n_perm, n_coef))
238
- null_t = np.zeros((n_perm, n_coef))
239
-
240
272
  # Original y for permutation
241
273
  y_orig = bundle.y.copy()
242
274
  n_obs = len(y_orig)
243
275
 
244
- for i in range(n_perm):
245
- perm_idx = rng.permutation(n_obs)
246
- y_perm = y_orig[perm_idx]
276
+ # LM fast path: single matmul via hat-matrix operator
277
+ is_lm = (
278
+ spec.family == "gaussian"
279
+ and spec.link == "identity"
280
+ and not spec.has_random_effects
281
+ )
247
282
 
248
- fit_perm = fit_with_permuted_y(spec, bundle, y_perm)
283
+ if is_lm:
284
+ null_coef = _compute_permutation_lm_fast(bundle.X, y_orig, n_perm, rng)
285
+ null_t = (null_coef - null) / se_obs
286
+ else:
287
+ # Generic path: refit model on each permutation
288
+ null_coef = np.zeros((n_perm, n_coef))
289
+ null_t = np.zeros((n_perm, n_coef))
290
+
291
+ for i in range(n_perm):
292
+ perm_idx = rng.permutation(n_obs)
293
+ y_perm = y_orig[perm_idx]
294
+
295
+ fit_perm = fit_with_permuted_y(spec, bundle, y_perm)
249
296
 
250
- null_coef[i, :] = fit_perm.coef
251
- null_t[i, :] = (fit_perm.coef - null) / se_obs
297
+ null_coef[i, :] = fit_perm.coef
298
+ null_t[i, :] = (fit_perm.coef - null) / se_obs
252
299
 
253
300
  # Permutation p-values based on alternative
254
301
  if alternative == "two-sided":
@@ -26,6 +26,7 @@ from bossanova.internal.infer.resample.core import (
26
26
  from bossanova.internal.infer.resample.glmer import glmer_bootstrap
27
27
  from bossanova.internal.infer.resample.lm_operators import (
28
28
  build_lm_coefficient_operator,
29
+ compute_batched_ols_bootstrap,
29
30
  )
30
31
  from bossanova.internal.infer.resample.lmer import lmer_bootstrap
31
32
  from bossanova.internal.infer.resample.results import BootstrapResult
@@ -35,6 +36,7 @@ __all__ = [
35
36
  "BCa_MIN_NBOOT",
36
37
  "BootstrapResult",
37
38
  "build_lm_coefficient_operator",
39
+ "compute_batched_ols_bootstrap",
38
40
  "compute_bootstrap_ci_basic",
39
41
  "compute_bootstrap_ci_bca",
40
42
  "compute_bootstrap_ci_percentile",
@@ -0,0 +1,280 @@
1
+ """Linear model coefficient operator (hat-matrix trick).
2
+
3
+ Provides ``build_lm_coefficient_operator`` which pre-computes the expensive
4
+ factorization of (X'X) once and returns a lightweight function that maps
5
+ any response vector Y to OLS coefficients. Used by hypothesis property
6
+ tests (BS.4) and the generic bootstrap engine.
7
+ """
8
+
9
+ from typing import Any, Callable, Literal
10
+
11
+ import numpy as np
12
+ import scipy.linalg as la
13
+ from bossanova.internal.maths.backend import get_backend
14
+
15
+ # Type alias for arrays (works with both JAX and NumPy)
16
+ Array = Any
17
+
18
+ __all__ = [
19
+ "Array",
20
+ "build_lm_coefficient_operator",
21
+ "compute_batched_ols_bootstrap",
22
+ ]
23
+
24
+
25
+ # =============================================================================
26
+ # Helpers
27
+ # =============================================================================
28
+
29
+
30
+ def _ensure_2d(xp: Any, Y: Array) -> tuple[Array, bool]:
31
+ """Convert Y to 2d, returning (Y_2d, was_1d)."""
32
+ is_1d = Y.ndim == 1
33
+ if is_1d:
34
+ Y = Y[:, xp.newaxis]
35
+ return Y, is_1d
36
+
37
+
38
+ def _squeeze_if_1d(xp: Any, coef: Array, is_1d: bool) -> Array:
39
+ """Squeeze trailing dim if input was 1d."""
40
+ if is_1d:
41
+ return xp.squeeze(coef, axis=-1)
42
+ return coef
43
+
44
+
45
+ def _make_cholesky_operator(
46
+ xp: Any, X: Array, cho_solve: Callable[..., Array]
47
+ ) -> Callable[[Array], Array]:
48
+ """Build a Cholesky-based coefficient operator.
49
+
50
+ Args:
51
+ xp: Array module (numpy or jax.numpy).
52
+ X: Design matrix, shape (n, p).
53
+ cho_solve: Cholesky solve function (scipy or jax.scipy).
54
+
55
+ Returns:
56
+ Function mapping Y -> coefficients.
57
+ """
58
+ XtX = X.T @ X
59
+ L = xp.linalg.cholesky(XtX)
60
+
61
+ def apply(Y: Array) -> Array:
62
+ Y = xp.asarray(Y)
63
+ Y, is_1d = _ensure_2d(xp, Y)
64
+ coef = cho_solve((L, True), X.T @ Y)
65
+ return _squeeze_if_1d(xp, coef, is_1d)
66
+
67
+ return apply
68
+
69
+
70
+ def _make_qr_operator(
71
+ xp: Any,
72
+ X: Array,
73
+ qr_fn: Callable[..., tuple[Array, Array]],
74
+ solve_tri: Callable[..., Array],
75
+ ) -> Callable[[Array], Array]:
76
+ """Build a QR-based coefficient operator.
77
+
78
+ Args:
79
+ xp: Array module (numpy or jax.numpy).
80
+ X: Design matrix, shape (n, p).
81
+ qr_fn: QR decomposition function.
82
+ solve_tri: Triangular solve function.
83
+
84
+ Returns:
85
+ Function mapping Y -> coefficients.
86
+ """
87
+ Q, R = qr_fn(X)
88
+
89
+ def apply(Y: Array) -> Array:
90
+ Y = xp.asarray(Y)
91
+ Y, is_1d = _ensure_2d(xp, Y)
92
+ coef = solve_tri(R, Q.T @ Y, lower=False)
93
+ return _squeeze_if_1d(xp, coef, is_1d)
94
+
95
+ return apply
96
+
97
+
98
+ # =============================================================================
99
+ # Coefficient Operator (Hat-Matrix Trick)
100
+ # =============================================================================
101
+
102
+
103
+ def build_lm_coefficient_operator(
104
+ X: Array,
105
+ method: Literal["cholesky", "qr"] = "cholesky",
106
+ ) -> Callable[[Array], Array]:
107
+ """Create reusable linear operator Y -> coefficients.
108
+
109
+ Precomputes and caches the expensive factorization. Returns a function
110
+ that maps Y -> coefficients.
111
+
112
+ This implements the "hat-matrix trick" for efficient permutation testing:
113
+ For fixed design matrix X, coefficients are a linear map from Y -> beta_hat:
114
+ beta_hat = (X'X)^-1 X'y. We precompute the factorization of (X'X) once
115
+ and reuse it for all permuted/bootstrapped Y values.
116
+
117
+ Args:
118
+ X: Design matrix, shape (n, p).
119
+ method: Factorization method.
120
+ - 'cholesky': Uses Cholesky decomposition of X'X (faster, more stable).
121
+ - 'qr': Uses QR decomposition of X (handles rank deficiency better).
122
+
123
+ Returns:
124
+ Function that maps Y (shape [n] or [n, m]) to coefficients.
125
+
126
+ Examples:
127
+ ```python
128
+ X = np.array([[1, 2], [1, 3], [1, 4]])
129
+ operator = build_lm_coefficient_operator(X)
130
+ y = np.array([5, 8, 11])
131
+ coef = operator(y)
132
+ ```
133
+ """
134
+ backend = get_backend()
135
+
136
+ if backend == "jax":
137
+ import jax.numpy as jnp
138
+ import jax.scipy.linalg as jsp
139
+
140
+ X_arr = jnp.asarray(X)
141
+ xp = jnp
142
+
143
+ if method == "cholesky":
144
+ return _make_cholesky_operator(xp, X_arr, jsp.cho_solve)
145
+ elif method == "qr":
146
+ return _make_qr_operator(xp, X_arr, jnp.linalg.qr, jsp.solve_triangular)
147
+ else:
148
+ raise ValueError(f"method must be 'cholesky' or 'qr', got {method}")
149
+
150
+ else:
151
+ X_arr = np.asarray(X)
152
+ xp = np
153
+
154
+ if method == "cholesky":
155
+ return _make_cholesky_operator(xp, X_arr, la.cho_solve)
156
+ elif method == "qr":
157
+ return _make_qr_operator(
158
+ xp,
159
+ X_arr,
160
+ lambda x: la.qr(x, mode="economic"),
161
+ la.solve_triangular,
162
+ )
163
+ else:
164
+ raise ValueError(f"method must be 'cholesky' or 'qr', got {method}")
165
+
166
+
167
+ # =============================================================================
168
+ # JIT-Cached OLS from Indices (Bootstrap Fast Path)
169
+ # =============================================================================
170
+
171
+ _ols_from_indices_cache: dict[str, Any] = {}
172
+
173
+
174
+ def _make_ols_from_indices_fn(ops: Any) -> Callable:
175
+ """Create JIT-compiled OLS solver that indexes into X and y.
176
+
177
+ The returned function takes ``(indices, X, y)`` and computes OLS
178
+ coefficients via Cholesky factorization of ``X[indices]``. This is
179
+ the inner kernel for ``compute_batched_ols_bootstrap`` — it gets
180
+ vmap'd over the bootstrap index array.
181
+
182
+ On JAX, Cholesky returns NaN (not an exception) for singular input,
183
+ so rank-deficient bootstrap samples naturally produce NaN coefficients.
184
+
185
+ Args:
186
+ ops: Backend ArrayOps (numpy or JAX).
187
+
188
+ Returns:
189
+ JIT-compiled function ``(indices, X, y) -> coef``.
190
+ """
191
+ xp = ops.np
192
+
193
+ def _core(indices: Array, X: Array, y: Array) -> Array:
194
+ X_b = X[indices]
195
+ y_b = y[indices]
196
+ XtX = X_b.T @ X_b
197
+ Xty = X_b.T @ y_b
198
+ L = xp.linalg.cholesky(XtX)
199
+ # Solve L @ z = Xty, then L.T @ coef = z
200
+ z = ops.solve_triangular(L, Xty, lower=True)
201
+ coef = ops.solve_triangular(L.T, z, lower=False)
202
+ return coef
203
+
204
+ return ops.jit(_core)
205
+
206
+
207
+ def _get_ols_from_indices_fn() -> Callable:
208
+ """Get JIT-compiled OLS-from-indices function for current backend.
209
+
210
+ Returns:
211
+ Cached JIT-compiled function ``(indices, X, y) -> coef``.
212
+ """
213
+ from bossanova.internal.maths.backend import get_ops
214
+
215
+ backend = get_backend()
216
+ if backend not in _ols_from_indices_cache:
217
+ ops = get_ops()
218
+ _ols_from_indices_cache[backend] = _make_ols_from_indices_fn(ops)
219
+ return _ols_from_indices_cache[backend]
220
+
221
+
222
+ def compute_batched_ols_bootstrap(
223
+ boot_indices: np.ndarray,
224
+ X: np.ndarray,
225
+ y: np.ndarray,
226
+ chunk_size: int | None = None,
227
+ ) -> np.ndarray:
228
+ """Compute bootstrap OLS coefficients using chunked vmap.
229
+
230
+ For LM models (gaussian family, identity link, no random effects),
231
+ each bootstrap replicate is an independent OLS solve. This function
232
+ batches those solves via ``ops.vmap`` — on JAX, this compiles to
233
+ a single XLA program; on NumPy, it degrades to a loop+stack
234
+ (equivalent to sequential but without Python overhead from
235
+ ``fit_model``).
236
+
237
+ Memory safety is ensured by chunking: ``compute_batch_size``
238
+ determines how many solves to batch at once based on available
239
+ memory.
240
+
241
+ Args:
242
+ boot_indices: Bootstrap index array, shape ``(n_boot, n)``.
243
+ X: Design matrix, shape ``(n, p)``.
244
+ y: Response vector, shape ``(n,)``.
245
+ chunk_size: Number of bootstrap samples per chunk. If ``None``,
246
+ computed automatically from available memory.
247
+
248
+ Returns:
249
+ Coefficient array, shape ``(n_boot, p)``.
250
+ """
251
+ from bossanova.internal.maths.backend import get_ops
252
+ from bossanova.internal.maths.batching import compute_batch_size
253
+
254
+ ops = get_ops()
255
+ n_boot = boot_indices.shape[0]
256
+ n, p = X.shape
257
+
258
+ # Compute chunk size if not provided
259
+ if chunk_size is None:
260
+ # Memory per bootstrap sample: X_b(n*p) + y_b(n) + XtX(p*p) + L(p*p) + Xty(p) + coef(p)
261
+ bytes_per_item = (n * p + n + 2 * p * p + 2 * p) * 8
262
+ chunk_size = compute_batch_size(n_items=n_boot, bytes_per_item=bytes_per_item)
263
+
264
+ # Get JIT-cached OLS function and vmap it
265
+ ols_fn = _get_ols_from_indices_fn()
266
+ vmapped_ols = ops.vmap(ols_fn, in_axes=(0, None, None))
267
+
268
+ # Convert to backend arrays
269
+ X_arr = ops.asarray(X)
270
+ y_arr = ops.asarray(y)
271
+
272
+ # Process in chunks
273
+ results = []
274
+ for start in range(0, n_boot, chunk_size):
275
+ end = min(start + chunk_size, n_boot)
276
+ idx_chunk = ops.asarray(boot_indices[start:end])
277
+ chunk_coefs = vmapped_ols(idx_chunk, X_arr, y_arr)
278
+ results.append(np.asarray(chunk_coefs))
279
+
280
+ return np.concatenate(results, axis=0)
@@ -272,20 +272,15 @@ def _compute_emm_gcomp(
272
272
  levels=levels,
273
273
  )
274
274
 
275
- # Compute g-computation estimates and L_matrix for each level
276
- n_levels = len(levels)
277
- p = X.shape[1]
278
- estimates = np.empty(n_levels, dtype=np.float64)
279
- L_matrix = np.empty((n_levels, p), dtype=np.float64)
275
+ # Vectorized g-computation: batch all K counterfactual matmuls
276
+ X_all = np.stack(cf_matrices) # (K, N, p)
277
+ eta_all = np.einsum("knp,p->kn", X_all, fit.coef) # (K, N)
278
+ pred_all = np.asarray(apply_link_inverse(spec.link, eta_all)) # (K, N)
279
+ estimates = pred_all.mean(axis=1) # (K,)
280
280
 
281
- for k, X_cf in enumerate(cf_matrices):
282
- eta_k = X_cf @ fit.coef # (N,)
283
- pred_k = np.asarray(apply_link_inverse(spec.link, eta_k)) # (N,)
284
- estimates[k] = pred_k.mean()
285
-
286
- # Delta method: L_k = mean(dμ/dη(η_k)[:, None] * X_cf_k, axis=0)
287
- d_k = np.asarray(apply_link_inverse_deriv(spec.link, eta_k)) # (N,)
288
- L_matrix[k] = (d_k[:, np.newaxis] * X_cf).mean(axis=0)
281
+ # Delta method: L_k = mean(dμ/dη(η_k)[:, None] * X_cf_k, axis=0)
282
+ d_all = np.asarray(apply_link_inverse_deriv(spec.link, eta_all)) # (K, N)
283
+ L_matrix = np.einsum("knp,kn->kp", X_all, d_all) / X_all.shape[1] # (K, p)
289
284
 
290
285
  # Build grid DataFrame
291
286
  grid = _build_emm_grid(focal_var, levels, at_overrides, set_categoricals)
@@ -404,32 +399,26 @@ def compute_conditional_emm(
404
399
 
405
400
  assert isinstance(varying_offsets, VaryingState)
406
401
  n_groups = varying_offsets.n_groups
407
- n_levels = len(levels)
408
402
 
409
- # Compute per-group intercept BLUPs
403
+ # Compute per-group BLUP offsets: vectorized over all groups at once
410
404
  b_intercept = varying_offsets.effects.get("Intercept", np.zeros(n_groups))
411
-
412
- # Build estimates: one row per (level, group) combination
413
- # Shape: (n_levels * n_groups,)
414
- estimates_link = np.empty(n_levels * n_groups)
415
- L_matrix = np.empty((n_levels * n_groups, X_ref.shape[1]))
416
-
417
- for g in range(n_groups):
418
- # Total BLUP offset for this group
419
- blup_offset = b_intercept[g]
420
- for effect_name, blups in varying_offsets.effects.items():
421
- if effect_name == "Intercept":
422
- continue
423
- # Add slope BLUP contributions (evaluated at X_means)
424
- X_names_list = list(bundle.X_names)
425
- if effect_name in X_names_list:
426
- cov_idx = X_names_list.index(effect_name)
427
- blup_offset += blups[g] * X_means[cov_idx]
428
-
429
- for lev in range(n_levels):
430
- row_idx = lev * n_groups + g
431
- estimates_link[row_idx] = X_ref[lev] @ fit.coef + blup_offset
432
- L_matrix[row_idx] = X_ref[lev]
405
+ blup_offsets = b_intercept.copy() # (G,)
406
+ X_names_list = list(bundle.X_names)
407
+ for effect_name, blups in varying_offsets.effects.items():
408
+ if effect_name == "Intercept":
409
+ continue
410
+ if effect_name in X_names_list:
411
+ cov_idx = X_names_list.index(effect_name)
412
+ blup_offsets += blups * X_means[cov_idx] # (G,) broadcast
413
+
414
+ # Vectorized per-level, per-group linear predictor
415
+ base_link = X_ref @ fit.coef # (K,)
416
+ # estimates_link[lev, g] = base_link[lev] + blup_offsets[g]
417
+ estimates_link_2d = base_link[:, np.newaxis] + blup_offsets[np.newaxis, :] # (K, G)
418
+ estimates_link = estimates_link_2d.ravel() # matches lev*n_groups+g indexing
419
+
420
+ # L_matrix: each X_ref[lev] repeated n_groups times
421
+ L_matrix = np.repeat(X_ref, n_groups, axis=0) # (K*G, p)
433
422
 
434
423
  estimates = estimates_link
435
424
  link_name: str | None = None
@@ -720,7 +709,6 @@ def _compute_emm_crossed_gcomp(
720
709
 
721
710
  X = bundle.X.copy()
722
711
  X_names_list = list(bundle.X_names)
723
- p = X.shape[1]
724
712
 
725
713
  # Apply scalar at_overrides to X
726
714
  for var_name, value in resolved.at_overrides.items():
@@ -746,46 +734,40 @@ def _compute_emm_crossed_gcomp(
746
734
  levels=focal_levels,
747
735
  )
748
736
 
749
- n_total = len(focal_levels) * len(grid_combos)
750
- estimates = np.empty(n_total, dtype=np.float64)
751
- L_matrix = np.empty((n_total, p), dtype=np.float64)
752
-
737
+ # Pre-build all C*K patched counterfactual matrices, then batch matmul
738
+ all_matrices: list[np.ndarray] = []
753
739
  grid_data: dict[str, list[object]] = {focal_var: []}
754
740
  for gv in grid_vars:
755
741
  grid_data[gv] = []
756
742
 
757
- row_idx = 0
758
743
  for combo in grid_combos:
759
- # Apply combo-specific overrides on top of the counterfactual matrices
760
744
  for k, X_cf_k in enumerate(cf_matrices):
761
745
  X_combo = X_cf_k.copy()
762
746
 
763
- # Apply grid combo conditions
747
+ # Apply grid combo conditions (O(p) column patching, not O(N*p))
764
748
  for i, gv in enumerate(grid_vars):
765
749
  val = combo[i]
766
750
  if isinstance(val, str):
767
- # Categorical: set dummy columns for this condition var
768
751
  for j, name in enumerate(bundle.X_names):
769
752
  info = parse_design_column_name(name)
770
753
  if info.column_type == "categorical" and info.base_term == gv:
771
754
  X_combo[:, j] = 1.0 if info.level == val else 0.0
772
755
  elif gv in X_names_list:
773
- # Numeric: override column
774
756
  X_combo[:, X_names_list.index(gv)] = float(val)
775
757
 
776
- # G-computation: mean response-scale prediction
777
- eta_k = X_combo @ fit.coef
778
- pred_k = np.asarray(apply_link_inverse(spec.link, eta_k))
779
- estimates[row_idx] = pred_k.mean()
780
-
781
- # Delta method: L_row = mean(dmu/deta[:, None] * X_combo, axis=0)
782
- d_k = np.asarray(apply_link_inverse_deriv(spec.link, eta_k))
783
- L_matrix[row_idx] = (d_k[:, np.newaxis] * X_combo).mean(axis=0)
784
-
758
+ all_matrices.append(X_combo)
785
759
  grid_data[focal_var].append(focal_levels[k])
786
760
  for i, gv in enumerate(grid_vars):
787
761
  grid_data[gv].append(combo[i])
788
- row_idx += 1
762
+
763
+ # Batched matmul + link computation over all C*K matrices
764
+ X_all = np.stack(all_matrices) # (C*K, N, p)
765
+ eta_all = np.einsum("mnp,p->mn", X_all, fit.coef) # (C*K, N)
766
+ pred_all = np.asarray(apply_link_inverse(spec.link, eta_all)) # (C*K, N)
767
+ estimates = pred_all.mean(axis=1) # (C*K,)
768
+
769
+ d_all = np.asarray(apply_link_inverse_deriv(spec.link, eta_all)) # (C*K, N)
770
+ L_matrix = np.einsum("mnp,mn->mp", X_all, d_all) / X_all.shape[1] # (C*K, p)
789
771
 
790
772
  grid = pl.DataFrame(grid_data)
791
773
  if grid_vars:
@@ -277,22 +277,19 @@ def _pvalue_per_df(
277
277
  """
278
278
  from scipy import stats
279
279
 
280
- n = len(statistic)
281
- p_values = np.empty(n, dtype=np.float64)
282
280
  p_min = np.finfo(np.float64).eps
283
- for i in range(n):
284
- dist = stats.t(df=df[i])
285
- if alternative == "two-sided":
286
- p_values[i] = 2 * dist.sf(np.abs(statistic[i]))
287
- elif alternative == "greater":
288
- p_values[i] = dist.sf(statistic[i])
289
- elif alternative == "less":
290
- p_values[i] = dist.cdf(statistic[i])
291
- else:
292
- raise ValueError(
293
- f"alternative must be 'two-sided', 'greater', or 'less', "
294
- f"got {alternative!r}"
295
- )
281
+
282
+ if alternative == "two-sided":
283
+ p_values = 2 * stats.t.sf(np.abs(statistic), df=df)
284
+ elif alternative == "greater":
285
+ p_values = stats.t.sf(statistic, df=df)
286
+ elif alternative == "less":
287
+ p_values = stats.t.cdf(statistic, df=df)
288
+ else:
289
+ raise ValueError(
290
+ f"alternative must be 'two-sided', 'greater', or 'less', "
291
+ f"got {alternative!r}"
292
+ )
296
293
  return np.clip(p_values, p_min, 1.0)
297
294
 
298
295
 
@@ -324,28 +321,23 @@ def _ci_per_df(
324
321
  Returns:
325
322
  Tuple of (ci_lower, ci_upper) arrays, each shape (n,).
326
323
  """
327
- n = len(estimate)
328
- ci_lower = np.empty(n, dtype=np.float64)
329
- ci_upper = np.empty(n, dtype=np.float64)
330
-
331
324
  # Determine per-estimate critical values
332
325
  crits = _per_estimate_critical_values(df, conf_level, mee, vcov)
333
326
 
334
- for i in range(n):
335
- if alternative == "two-sided":
336
- ci_lower[i] = estimate[i] - crits[i] * se[i]
337
- ci_upper[i] = estimate[i] + crits[i] * se[i]
338
- elif alternative == "greater":
339
- ci_lower[i] = estimate[i] - crits[i] * se[i]
340
- ci_upper[i] = float("inf")
341
- elif alternative == "less":
342
- ci_lower[i] = float("-inf")
343
- ci_upper[i] = estimate[i] + crits[i] * se[i]
344
- else:
345
- raise ValueError(
346
- f"alternative must be 'two-sided', 'greater', or 'less', "
347
- f"got {alternative!r}"
348
- )
327
+ if alternative == "two-sided":
328
+ ci_lower = estimate - crits * se
329
+ ci_upper = estimate + crits * se
330
+ elif alternative == "greater":
331
+ ci_lower = estimate - crits * se
332
+ ci_upper = np.full_like(estimate, float("inf"))
333
+ elif alternative == "less":
334
+ ci_lower = np.full_like(estimate, float("-inf"))
335
+ ci_upper = estimate + crits * se
336
+ else:
337
+ raise ValueError(
338
+ f"alternative must be 'two-sided', 'greater', or 'less', "
339
+ f"got {alternative!r}"
340
+ )
349
341
  return ci_lower, ci_upper
350
342
 
351
343
 
@@ -401,10 +393,7 @@ def _per_estimate_critical_values(
401
393
  return np.full(n, crit)
402
394
 
403
395
  # No adjustment — standard per-df t critical values
404
- crits = np.empty(n, dtype=np.float64)
405
- for i in range(n):
406
- crits[i] = stats.t.ppf(1 - alpha / 2, df=df[i])
407
- return crits
396
+ return stats.t.ppf(1 - alpha / 2, df=df)
408
397
 
409
398
 
410
399
  def compute_mee_se(