bossanova 0.1.0.dev21__tar.gz → 0.1.0.dev23__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.dev23}/PKG-INFO +1 -1
  2. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/bootstrap.py +14 -0
  3. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/permutation.py +57 -10
  4. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample/__init__.py +2 -0
  5. bossanova-0.1.0.dev23/bossanova/internal/infer/resample/lm_operators.py +280 -0
  6. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/emm.py +72 -61
  7. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/inference.py +27 -38
  8. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/joint_tests.py +142 -18
  9. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/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.dev23}/.gitignore +0 -0
  12. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/LICENSE +0 -0
  13. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/README.md +0 -0
  14. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/__init__.py +0 -0
  15. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/README.md +0 -0
  16. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/__init__.py +0 -0
  17. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/advertising.csv +0 -0
  18. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/cake.csv +0 -0
  19. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/chickweight.csv +0 -0
  20. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/credit.csv +0 -0
  21. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/gammas.csv +0 -0
  22. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/mtcars.csv +0 -0
  23. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/penguins.csv +0 -0
  24. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/poker.csv +0 -0
  25. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/sleep.csv +0 -0
  26. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/titanic.csv +0 -0
  27. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/titanic_test.csv +0 -0
  28. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/data/titanic_train.csv +0 -0
  29. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/distributions/__init__.py +0 -0
  30. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/distributions/continuous.py +0 -0
  31. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/distributions/discrete.py +0 -0
  32. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/distributions/varying.py +0 -0
  33. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/expressions.py +0 -0
  34. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/__init__.py +0 -0
  35. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/__init__.py +0 -0
  36. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/compare.py +0 -0
  37. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/cv.py +0 -0
  38. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/deviance.py +0 -0
  39. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/f_test.py +0 -0
  40. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/helpers.py +0 -0
  41. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/ic.py +0 -0
  42. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/lrt.py +0 -0
  43. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/lrt_compare.py +0 -0
  44. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/compare/refit.py +0 -0
  45. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/__init__.py +0 -0
  46. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/builders/__init__.py +0 -0
  47. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/builders/dataframes.py +0 -0
  48. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/builders/resamples.py +0 -0
  49. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/builders/results.py +0 -0
  50. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/builders/specs.py +0 -0
  51. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/builders/state.py +0 -0
  52. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/schemas.py +0 -0
  53. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/structs/__init__.py +0 -0
  54. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/structs/data.py +0 -0
  55. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/structs/display.py +0 -0
  56. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/structs/explore.py +0 -0
  57. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/structs/formula.py +0 -0
  58. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/structs/specs.py +0 -0
  59. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/structs/state.py +0 -0
  60. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/containers/validators.py +0 -0
  61. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/design/__init__.py +0 -0
  62. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/design/coding.py +0 -0
  63. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/design/names.py +0 -0
  64. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/design/reference.py +0 -0
  65. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/design/z_matrix.py +0 -0
  66. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/__init__.py +0 -0
  67. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/convergence.py +0 -0
  68. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/diagnostics.py +0 -0
  69. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/dispatch.py +0 -0
  70. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/glm.py +0 -0
  71. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/glmer.py +0 -0
  72. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/lmer.py +0 -0
  73. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/ols.py +0 -0
  74. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/predict.py +0 -0
  75. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/fit/varying.py +0 -0
  76. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/__init__.py +0 -0
  77. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/bundle.py +0 -0
  78. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/contrast_registry.py +0 -0
  79. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/contrast_specs.py +0 -0
  80. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/design.py +0 -0
  81. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/encoding.py +0 -0
  82. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/evaluate.py +0 -0
  83. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/evaluate_contrast.py +0 -0
  84. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/evaluate_newdata.py +0 -0
  85. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/evaluate_transforms.py +0 -0
  86. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/helpers.py +0 -0
  87. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/parse.py +0 -0
  88. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/parser/__init__.py +0 -0
  89. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/parser/expr.py +0 -0
  90. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/parser/parser.py +0 -0
  91. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/parser/scanner.py +0 -0
  92. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/parser/token.py +0 -0
  93. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/formula/random_effects.py +0 -0
  94. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/__init__.py +0 -0
  95. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/asymptotic.py +0 -0
  96. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/cv.py +0 -0
  97. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/dispatch.py +0 -0
  98. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/formula_utils.py +0 -0
  99. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/mee.py +0 -0
  100. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/params.py +0 -0
  101. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/prediction.py +0 -0
  102. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/profile.py +0 -0
  103. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample/common.py +0 -0
  104. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample/core.py +0 -0
  105. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample/glmer.py +0 -0
  106. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample/lmer.py +0 -0
  107. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample/results.py +0 -0
  108. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample/simulate.py +0 -0
  109. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/resample_bundle.py +0 -0
  110. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/satterthwaite_emm.py +0 -0
  111. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/infer/simulation.py +0 -0
  112. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/__init__.py +0 -0
  113. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/bracket_contrasts.py +0 -0
  114. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/compute.py +0 -0
  115. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/conditions.py +0 -0
  116. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/contrasts.py +0 -0
  117. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/explore.py +0 -0
  118. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/explore_parser.py +0 -0
  119. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/explore_scanner.py +0 -0
  120. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/factors.py +0 -0
  121. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/grid.py +0 -0
  122. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/matrices.py +0 -0
  123. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/resolve.py +0 -0
  124. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/slopes.py +0 -0
  125. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/transforms.py +0 -0
  126. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/marginal/validation.py +0 -0
  127. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/__init__.py +0 -0
  128. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/backend/__init__.py +0 -0
  129. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/backend/dispatch.py +0 -0
  130. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/backend/jax.py +0 -0
  131. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/backend/numpy.py +0 -0
  132. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/backend/protocol.py +0 -0
  133. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/batching.py +0 -0
  134. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/config.py +0 -0
  135. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/convergence.py +0 -0
  136. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/differentiation.py +0 -0
  137. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/__init__.py +0 -0
  138. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/algebra.py +0 -0
  139. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/base.py +0 -0
  140. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/core.py +0 -0
  141. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/derived.py +0 -0
  142. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/factories.py +0 -0
  143. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/plotting.py +0 -0
  144. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/distributions/probability.py +0 -0
  145. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/__init__.py +0 -0
  146. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/binomial.py +0 -0
  147. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/create.py +0 -0
  148. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/gamma.py +0 -0
  149. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/gaussian.py +0 -0
  150. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/links.py +0 -0
  151. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/poisson.py +0 -0
  152. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/response.py +0 -0
  153. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/schema.py +0 -0
  154. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/family/tdist.py +0 -0
  155. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/__init__.py +0 -0
  156. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/diagnostics.py +0 -0
  157. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/estimation.py +0 -0
  158. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/hypothesis.py +0 -0
  159. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/information_criteria.py +0 -0
  160. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/multiplicity.py +0 -0
  161. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/profile.py +0 -0
  162. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/sandwich.py +0 -0
  163. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/satterthwaite.py +0 -0
  164. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/wald_variance.py +0 -0
  165. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/inference/welch.py +0 -0
  166. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/linalg/__init__.py +0 -0
  167. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/linalg/qr.py +0 -0
  168. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/linalg/rank.py +0 -0
  169. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/linalg/schur.py +0 -0
  170. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/linalg/sparse.py +0 -0
  171. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/linalg/svd.py +0 -0
  172. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/predict.py +0 -0
  173. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/rng.py +0 -0
  174. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/rounding.py +0 -0
  175. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/__init__.py +0 -0
  176. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/glm.py +0 -0
  177. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/glmer.py +0 -0
  178. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/heuristics.py +0 -0
  179. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/initialization.py +0 -0
  180. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/lambda_builder.py +0 -0
  181. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/lambda_sparse.py +0 -0
  182. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/lambda_template.py +0 -0
  183. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/lmer.py +0 -0
  184. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/optimize.py +0 -0
  185. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/pirls_sparse.py +0 -0
  186. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/solvers/quadrature.py +0 -0
  187. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/tolerances.py +0 -0
  188. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/transforms.py +0 -0
  189. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/variance.py +0 -0
  190. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/maths/weights.py +0 -0
  191. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/rendering/__init__.py +0 -0
  192. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/rendering/latex.py +0 -0
  193. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/rendering/markdown.py +0 -0
  194. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/__init__.py +0 -0
  195. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/dgp/__init__.py +0 -0
  196. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/dgp/generate.py +0 -0
  197. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/dgp/glm.py +0 -0
  198. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/dgp/glmer.py +0 -0
  199. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/dgp/lm.py +0 -0
  200. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/dgp/lmer.py +0 -0
  201. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/harness.py +0 -0
  202. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/metrics.py +0 -0
  203. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/model_sim.py +0 -0
  204. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/simulation/power.py +0 -0
  205. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/README.md +0 -0
  206. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/__init__.py +0 -0
  207. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/cognition.py +0 -0
  208. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/compare.py +0 -0
  209. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/core.py +0 -0
  210. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/core_data.py +0 -0
  211. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/core_protocols.py +0 -0
  212. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/core_sizing.py +0 -0
  213. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/core_viz.py +0 -0
  214. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/dag.py +0 -0
  215. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/design.py +0 -0
  216. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/fit.py +0 -0
  217. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/fit_builders.py +0 -0
  218. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/fit_layers.py +0 -0
  219. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/helpers.py +0 -0
  220. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/lattice.py +0 -0
  221. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/layout.py +0 -0
  222. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/mem.py +0 -0
  223. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/mem_forest.py +0 -0
  224. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/params.py +0 -0
  225. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/predict.py +0 -0
  226. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/profile.py +0 -0
  227. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/ranef.py +0 -0
  228. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/relationships.py +0 -0
  229. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/resamples.py +0 -0
  230. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/resid.py +0 -0
  231. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/internal/viz/vif.py +0 -0
  232. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/model/__init__.py +0 -0
  233. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/model/core.py +0 -0
  234. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/model/guards.py +0 -0
  235. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/model/result.py +0 -0
  236. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/model/summary.py +0 -0
  237. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/bossanova/py.typed +0 -0
  238. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/tests/bossanova_benchmarks/bootstrap/data/README.md +0 -0
  239. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/tests/bossanova_benchmarks/insteval/data/README.md +0 -0
  240. {bossanova-0.1.0.dev21 → bossanova-0.1.0.dev23}/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.dev23
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
@@ -664,8 +653,24 @@ def _compute_emm_crossed_mem(
664
653
  L_matrix = d[:, np.newaxis] * X_ref
665
654
 
666
655
  grid = pl.DataFrame(grid_data)
667
- if grid_vars:
668
- grid = grid.select(grid_vars + [c for c in grid.columns if c not in grid_vars])
656
+
657
+ # Add scalar-pinned conditions as constant columns (set_categoricals
658
+ # and at_overrides are applied to the design matrix but must also
659
+ # appear in the output grid so users can see the pinned values).
660
+ if resolved.set_categoricals:
661
+ for cond_var, cond_level in resolved.set_categoricals.items():
662
+ if cond_var not in grid.columns:
663
+ grid = grid.with_columns(pl.lit(cond_level).alias(cond_var))
664
+ if resolved.at_overrides:
665
+ for cond_var, cond_val in resolved.at_overrides.items():
666
+ if cond_var not in grid.columns:
667
+ grid = grid.with_columns(pl.lit(cond_val).alias(cond_var))
668
+
669
+ # Reorder: grid_vars + pinned conditions first, then focal
670
+ pin_cols = list(resolved.set_categoricals) + list(resolved.at_overrides)
671
+ leading = grid_vars + [c for c in pin_cols if c not in grid_vars]
672
+ if leading:
673
+ grid = grid.select(leading + [c for c in grid.columns if c not in leading])
669
674
 
670
675
  cond_str = " ~ " + ", ".join(grid_vars) if grid_vars else ""
671
676
  return build_mee_state(
@@ -720,7 +725,6 @@ def _compute_emm_crossed_gcomp(
720
725
 
721
726
  X = bundle.X.copy()
722
727
  X_names_list = list(bundle.X_names)
723
- p = X.shape[1]
724
728
 
725
729
  # Apply scalar at_overrides to X
726
730
  for var_name, value in resolved.at_overrides.items():
@@ -746,50 +750,57 @@ def _compute_emm_crossed_gcomp(
746
750
  levels=focal_levels,
747
751
  )
748
752
 
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
-
753
+ # Pre-build all C*K patched counterfactual matrices, then batch matmul
754
+ all_matrices: list[np.ndarray] = []
753
755
  grid_data: dict[str, list[object]] = {focal_var: []}
754
756
  for gv in grid_vars:
755
757
  grid_data[gv] = []
756
758
 
757
- row_idx = 0
758
759
  for combo in grid_combos:
759
- # Apply combo-specific overrides on top of the counterfactual matrices
760
760
  for k, X_cf_k in enumerate(cf_matrices):
761
761
  X_combo = X_cf_k.copy()
762
762
 
763
- # Apply grid combo conditions
763
+ # Apply grid combo conditions (O(p) column patching, not O(N*p))
764
764
  for i, gv in enumerate(grid_vars):
765
765
  val = combo[i]
766
766
  if isinstance(val, str):
767
- # Categorical: set dummy columns for this condition var
768
767
  for j, name in enumerate(bundle.X_names):
769
768
  info = parse_design_column_name(name)
770
769
  if info.column_type == "categorical" and info.base_term == gv:
771
770
  X_combo[:, j] = 1.0 if info.level == val else 0.0
772
771
  elif gv in X_names_list:
773
- # Numeric: override column
774
772
  X_combo[:, X_names_list.index(gv)] = float(val)
775
773
 
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
-
774
+ all_matrices.append(X_combo)
785
775
  grid_data[focal_var].append(focal_levels[k])
786
776
  for i, gv in enumerate(grid_vars):
787
777
  grid_data[gv].append(combo[i])
788
- row_idx += 1
778
+
779
+ # Batched matmul + link computation over all C*K matrices
780
+ X_all = np.stack(all_matrices) # (C*K, N, p)
781
+ eta_all = np.einsum("mnp,p->mn", X_all, fit.coef) # (C*K, N)
782
+ pred_all = np.asarray(apply_link_inverse(spec.link, eta_all)) # (C*K, N)
783
+ estimates = pred_all.mean(axis=1) # (C*K,)
784
+
785
+ d_all = np.asarray(apply_link_inverse_deriv(spec.link, eta_all)) # (C*K, N)
786
+ L_matrix = np.einsum("mnp,mn->mp", X_all, d_all) / X_all.shape[1] # (C*K, p)
789
787
 
790
788
  grid = pl.DataFrame(grid_data)
791
- if grid_vars:
792
- grid = grid.select(grid_vars + [c for c in grid.columns if c not in grid_vars])
789
+
790
+ # Add scalar-pinned conditions as constant columns (mirrors MEM path)
791
+ if resolved.set_categoricals:
792
+ for cond_var, cond_level in resolved.set_categoricals.items():
793
+ if cond_var not in grid.columns:
794
+ grid = grid.with_columns(pl.lit(cond_level).alias(cond_var))
795
+ if resolved.at_overrides:
796
+ for cond_var, cond_val in resolved.at_overrides.items():
797
+ if cond_var not in grid.columns:
798
+ grid = grid.with_columns(pl.lit(cond_val).alias(cond_var))
799
+
800
+ pin_cols = list(resolved.set_categoricals) + list(resolved.at_overrides)
801
+ leading = grid_vars + [c for c in pin_cols if c not in grid_vars]
802
+ if leading:
803
+ grid = grid.select(leading + [c for c in grid.columns if c not in leading])
793
804
 
794
805
  cond_str = " ~ " + ", ".join(grid_vars) if grid_vars else ""
795
806
  return build_mee_state(