bossanova 0.1.0.dev23__tar.gz → 0.1.0.dev25__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 (249) hide show
  1. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/.gitignore +2 -0
  2. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/PKG-INFO +1 -1
  3. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/compare.py +25 -3
  4. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/cv.py +89 -39
  5. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/helpers.py +59 -11
  6. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/lrt_compare.py +18 -12
  7. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/__init__.py +2 -0
  8. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/builders/dataframes.py +12 -8
  9. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/builders/resamples.py +3 -5
  10. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/builders/state.py +19 -5
  11. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/schemas.py +3 -1
  12. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/structs/__init__.py +12 -6
  13. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/structs/data.py +38 -9
  14. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/structs/display.py +8 -0
  15. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/structs/explore.py +70 -11
  16. bossanova-0.1.0.dev25/bossanova/internal/containers/structs/fit.py +106 -0
  17. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/structs/formula.py +10 -7
  18. bossanova-0.1.0.dev25/bossanova/internal/containers/structs/inference.py +170 -0
  19. bossanova-0.1.0.dev25/bossanova/internal/containers/structs/marginal.py +99 -0
  20. bossanova-0.1.0.dev25/bossanova/internal/containers/structs/mixed.py +128 -0
  21. bossanova-0.1.0.dev25/bossanova/internal/containers/structs/prediction.py +89 -0
  22. bossanova-0.1.0.dev25/bossanova/internal/containers/structs/simulation.py +50 -0
  23. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/structs/specs.py +17 -2
  24. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/validators.py +155 -0
  25. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/design/z_matrix.py +1 -0
  26. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/__init__.py +13 -0
  27. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/convergence.py +1 -1
  28. bossanova-0.1.0.dev25/bossanova/internal/fit/grid.py +339 -0
  29. bossanova-0.1.0.dev25/bossanova/internal/fit/lifecycle.py +127 -0
  30. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/predict.py +16 -0
  31. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/varying.py +19 -6
  32. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/bundle.py +1 -0
  33. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/random_effects.py +9 -2
  34. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/asymptotic.py +1 -1
  35. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/bootstrap.py +59 -85
  36. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/cv.py +134 -11
  37. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/dispatch.py +10 -2
  38. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/permutation.py +10 -24
  39. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/prediction.py +10 -1
  40. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/profile.py +2 -2
  41. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/__init__.py +4 -0
  42. bossanova-0.1.0.dev25/bossanova/internal/infer/resample/glm_operators.py +226 -0
  43. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/glmer.py +7 -2
  44. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/lm_operators.py +21 -14
  45. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/lmer.py +7 -2
  46. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/simulation.py +3 -1
  47. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/bracket_contrasts.py +2 -1
  48. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/compute.py +3 -5
  49. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/contrasts.py +3 -2
  50. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/emm.py +3 -3
  51. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/inference.py +1 -1
  52. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/joint_tests.py +2 -2
  53. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/resolve.py +1 -1
  54. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/slopes.py +3 -3
  55. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/backend/jax.py +10 -2
  56. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/backend/numpy.py +6 -1
  57. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/backend/protocol.py +9 -1
  58. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/glm.py +3 -1
  59. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/heuristics.py +23 -11
  60. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/initialization.py +11 -8
  61. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/lambda_sparse.py +33 -8
  62. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/lambda_template.py +33 -8
  63. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/__init__.py +3 -0
  64. bossanova-0.1.0.dev25/bossanova/internal/simulation/lifecycle.py +156 -0
  65. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/model_sim.py +22 -6
  66. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/README.md +1 -4
  67. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/__init__.py +2 -14
  68. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/helpers.py +1 -43
  69. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/predict.py +192 -12
  70. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/profile.py +1 -1
  71. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/ranef.py +2 -2
  72. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/model/core.py +151 -266
  73. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/model/summary.py +315 -46
  74. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/pyproject.toml +4 -3
  75. bossanova-0.1.0.dev23/bossanova/internal/containers/structs/state.py +0 -886
  76. bossanova-0.1.0.dev23/bossanova/internal/viz/cognition.py +0 -276
  77. bossanova-0.1.0.dev23/bossanova/internal/viz/dag.py +0 -488
  78. bossanova-0.1.0.dev23/bossanova/internal/viz/fit.py +0 -571
  79. bossanova-0.1.0.dev23/bossanova/internal/viz/fit_builders.py +0 -324
  80. bossanova-0.1.0.dev23/bossanova/internal/viz/fit_layers.py +0 -453
  81. bossanova-0.1.0.dev23/bossanova/internal/viz/lattice.py +0 -467
  82. bossanova-0.1.0.dev23/bossanova/internal/viz/layout.py +0 -565
  83. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/LICENSE +0 -0
  84. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/README.md +0 -0
  85. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/__init__.py +0 -0
  86. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/README.md +0 -0
  87. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/__init__.py +0 -0
  88. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/advertising.csv +0 -0
  89. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/cake.csv +0 -0
  90. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/chickweight.csv +0 -0
  91. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/credit.csv +0 -0
  92. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/gammas.csv +0 -0
  93. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/mtcars.csv +0 -0
  94. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/penguins.csv +0 -0
  95. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/poker.csv +0 -0
  96. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/sleep.csv +0 -0
  97. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/titanic.csv +0 -0
  98. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/titanic_test.csv +0 -0
  99. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/data/titanic_train.csv +0 -0
  100. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/distributions/__init__.py +0 -0
  101. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/distributions/continuous.py +0 -0
  102. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/distributions/discrete.py +0 -0
  103. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/distributions/varying.py +0 -0
  104. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/expressions.py +0 -0
  105. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/__init__.py +0 -0
  106. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/__init__.py +0 -0
  107. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/deviance.py +0 -0
  108. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/f_test.py +0 -0
  109. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/ic.py +0 -0
  110. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/lrt.py +0 -0
  111. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/compare/refit.py +0 -0
  112. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/builders/__init__.py +0 -0
  113. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/builders/results.py +0 -0
  114. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/containers/builders/specs.py +0 -0
  115. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/design/__init__.py +0 -0
  116. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/design/coding.py +0 -0
  117. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/design/names.py +0 -0
  118. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/design/reference.py +0 -0
  119. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/diagnostics.py +0 -0
  120. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/dispatch.py +0 -0
  121. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/glm.py +0 -0
  122. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/glmer.py +0 -0
  123. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/lmer.py +0 -0
  124. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/fit/ols.py +0 -0
  125. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/__init__.py +0 -0
  126. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/contrast_registry.py +0 -0
  127. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/contrast_specs.py +0 -0
  128. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/design.py +0 -0
  129. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/encoding.py +0 -0
  130. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/evaluate.py +0 -0
  131. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/evaluate_contrast.py +0 -0
  132. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/evaluate_newdata.py +0 -0
  133. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/evaluate_transforms.py +0 -0
  134. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/helpers.py +0 -0
  135. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/parse.py +0 -0
  136. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/parser/__init__.py +0 -0
  137. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/parser/expr.py +0 -0
  138. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/parser/parser.py +0 -0
  139. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/parser/scanner.py +0 -0
  140. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/formula/parser/token.py +0 -0
  141. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/__init__.py +0 -0
  142. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/formula_utils.py +0 -0
  143. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/mee.py +0 -0
  144. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/params.py +0 -0
  145. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/common.py +0 -0
  146. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/core.py +0 -0
  147. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/results.py +0 -0
  148. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample/simulate.py +0 -0
  149. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/resample_bundle.py +0 -0
  150. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/infer/satterthwaite_emm.py +0 -0
  151. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/__init__.py +0 -0
  152. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/conditions.py +0 -0
  153. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/explore.py +0 -0
  154. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/explore_parser.py +0 -0
  155. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/explore_scanner.py +0 -0
  156. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/factors.py +0 -0
  157. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/grid.py +0 -0
  158. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/matrices.py +0 -0
  159. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/transforms.py +0 -0
  160. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/marginal/validation.py +0 -0
  161. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/__init__.py +0 -0
  162. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/backend/__init__.py +0 -0
  163. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/backend/dispatch.py +0 -0
  164. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/batching.py +0 -0
  165. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/config.py +0 -0
  166. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/convergence.py +0 -0
  167. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/differentiation.py +0 -0
  168. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/__init__.py +0 -0
  169. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/algebra.py +0 -0
  170. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/base.py +0 -0
  171. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/core.py +0 -0
  172. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/derived.py +0 -0
  173. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/factories.py +0 -0
  174. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/plotting.py +0 -0
  175. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/distributions/probability.py +0 -0
  176. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/__init__.py +0 -0
  177. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/binomial.py +0 -0
  178. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/create.py +0 -0
  179. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/gamma.py +0 -0
  180. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/gaussian.py +0 -0
  181. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/links.py +0 -0
  182. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/poisson.py +0 -0
  183. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/response.py +0 -0
  184. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/schema.py +0 -0
  185. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/family/tdist.py +0 -0
  186. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/__init__.py +0 -0
  187. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/diagnostics.py +0 -0
  188. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/estimation.py +0 -0
  189. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/hypothesis.py +0 -0
  190. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/information_criteria.py +0 -0
  191. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/multiplicity.py +0 -0
  192. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/profile.py +0 -0
  193. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/sandwich.py +0 -0
  194. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/satterthwaite.py +0 -0
  195. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/wald_variance.py +0 -0
  196. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/inference/welch.py +0 -0
  197. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/linalg/__init__.py +0 -0
  198. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/linalg/qr.py +0 -0
  199. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/linalg/rank.py +0 -0
  200. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/linalg/schur.py +0 -0
  201. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/linalg/sparse.py +0 -0
  202. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/linalg/svd.py +0 -0
  203. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/predict.py +0 -0
  204. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/rng.py +0 -0
  205. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/rounding.py +0 -0
  206. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/__init__.py +0 -0
  207. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/glmer.py +0 -0
  208. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/lambda_builder.py +0 -0
  209. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/lmer.py +0 -0
  210. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/optimize.py +0 -0
  211. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/pirls_sparse.py +0 -0
  212. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/solvers/quadrature.py +0 -0
  213. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/tolerances.py +0 -0
  214. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/transforms.py +0 -0
  215. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/variance.py +0 -0
  216. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/maths/weights.py +0 -0
  217. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/rendering/__init__.py +0 -0
  218. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/rendering/latex.py +0 -0
  219. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/rendering/markdown.py +0 -0
  220. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/dgp/__init__.py +0 -0
  221. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/dgp/generate.py +0 -0
  222. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/dgp/glm.py +0 -0
  223. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/dgp/glmer.py +0 -0
  224. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/dgp/lm.py +0 -0
  225. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/dgp/lmer.py +0 -0
  226. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/harness.py +0 -0
  227. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/metrics.py +0 -0
  228. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/simulation/power.py +0 -0
  229. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/compare.py +0 -0
  230. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/core.py +0 -0
  231. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/core_data.py +0 -0
  232. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/core_protocols.py +0 -0
  233. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/core_sizing.py +0 -0
  234. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/core_viz.py +0 -0
  235. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/design.py +0 -0
  236. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/mem.py +0 -0
  237. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/mem_forest.py +0 -0
  238. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/params.py +0 -0
  239. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/relationships.py +0 -0
  240. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/resamples.py +0 -0
  241. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/resid.py +0 -0
  242. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/internal/viz/vif.py +0 -0
  243. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/model/__init__.py +0 -0
  244. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/model/guards.py +0 -0
  245. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/model/result.py +0 -0
  246. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/bossanova/py.typed +0 -0
  247. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/tests/bossanova_benchmarks/bootstrap/data/README.md +0 -0
  248. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/tests/bossanova_benchmarks/insteval/data/README.md +0 -0
  249. {bossanova-0.1.0.dev23 → bossanova-0.1.0.dev25}/tests/bossanova_tests/hypothesis/README.md +0 -0
@@ -8,6 +8,7 @@ sdist/
8
8
  dev
9
9
  *.DS*
10
10
  research/
11
+ scratch/
11
12
  plans/
12
13
  archive/
13
14
  .private-journal
@@ -31,6 +32,7 @@ lmer-refactor-notes/
31
32
  *.txt
32
33
  *.json
33
34
  !tests/pyodide/package.json
35
+ !context7.json
34
36
  *.png
35
37
 
36
38
  # Node.js (for Pyodide tests)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bossanova
3
- Version: 0.1.0.dev23
3
+ Version: 0.1.0.dev25
4
4
  Summary: Bridging statistical cultures with some jazz
5
5
  Author: Eshin Jolly
6
6
  License-Expression: MIT
@@ -60,6 +60,7 @@ def compare(
60
60
  seed: int | None = None,
61
61
  metric: str = "mse",
62
62
  digits: int | None = None,
63
+ holdout_group: str | None = None,
63
64
  ) -> pl.DataFrame:
64
65
  """Compare nested statistical models.
65
66
 
@@ -71,6 +72,20 @@ def compare(
71
72
  - lmer/glmer: Likelihood ratio test
72
73
  - cv: Cross-validation with Nadeau-Bengio corrected t-test
73
74
 
75
+ Cross-type comparisons (e.g., glm vs glmer) are supported when models
76
+ share the same family. The fixed-only model is treated as nested within
77
+ the mixed model via zero variance components.
78
+
79
+ | Model Pair | Hypothesis Test | AIC/BIC | CV |
80
+ |------------|-----------------|---------|-----|
81
+ | lm vs lm | F-test | Yes | Yes |
82
+ | glm vs glm | Deviance | Yes | Yes |
83
+ | lmer vs lmer | LRT | Yes | Yes |
84
+ | glmer vs glmer | LRT | Yes | Yes |
85
+ | lm vs lmer | LRT | Yes | Yes |
86
+ | glm vs glmer | LRT | Yes | Yes |
87
+ | Different families | — | Yes | Yes |
88
+
74
89
  Args:
75
90
  *models: Two or more fitted model objects.
76
91
  method: Comparison method. Options:
@@ -78,7 +93,7 @@ def compare(
78
93
  - "f": F-test (for lm)
79
94
  - "lrt": Likelihood ratio test (for mixed models)
80
95
  - "deviance": Deviance test (for glm)
81
- - "cv": Cross-validation comparison (for lm/glm)
96
+ - "cv": Cross-validation comparison (any model type)
82
97
  - "aic": AIC comparison with delta-AIC and Akaike weights
83
98
  - "bic": BIC comparison with delta-BIC and Schwarz weights
84
99
  sort: If True, sort models by complexity before comparing.
@@ -234,7 +249,8 @@ def compare(
234
249
 
235
250
  # Infer method if auto
236
251
  if method == "auto":
237
- method = resolve_method(model_list[0])
252
+ type_set = {m._model_type for m in model_list}
253
+ method = resolve_method(model_list[0], model_types=type_set)
238
254
 
239
255
  # For LRT with refit=True, refit REML models with ML
240
256
  if method == "lrt" and refit:
@@ -248,7 +264,13 @@ def compare(
248
264
  elif method == "deviance":
249
265
  result = compare_deviance(model_list, test=test)
250
266
  elif method == "cv":
251
- result = compare_cv(model_list, cv=cv, seed=seed, metric=metric)
267
+ result = compare_cv(
268
+ model_list,
269
+ cv=cv,
270
+ seed=seed,
271
+ metric=metric,
272
+ holdout_group=holdout_group,
273
+ )
252
274
  elif method == "aic":
253
275
  result = compare_aic(model_list)
254
276
  elif method == "bic":
@@ -3,6 +3,9 @@
3
3
  Compares models using k-fold (or LOO) cross-validation with the corrected
4
4
  variance estimator from Nadeau & Bengio (2003) to account for overlapping
5
5
  training sets.
6
+
7
+ Supports group-aware splitting via ``holdout_group`` to prevent data leakage
8
+ from random-effects structure.
6
9
  """
7
10
 
8
11
  from typing import Any
@@ -12,11 +15,10 @@ import polars as pl
12
15
  from scipy import stats
13
16
 
14
17
  from bossanova.internal.containers.schemas import Col, ComparisonCv
15
- from bossanova.internal.maths.rng import RNG
16
- from bossanova.internal.compare.helpers import get_model_type
17
- from bossanova.internal.infer.resample.core import (
18
- generate_kfold_indices,
19
- generate_loo_indices,
18
+ from bossanova.internal.infer.cv import (
19
+ generate_group_kfold_splits,
20
+ generate_kfold_splits,
21
+ resolve_holdout_group_ids,
20
22
  )
21
23
 
22
24
  __all__ = [
@@ -44,8 +46,6 @@ def compute_cv_fold_score(
44
46
  Returns:
45
47
  Test set error for this fold.
46
48
  """
47
- model_type = get_model_type(model)
48
-
49
49
  # Get original data
50
50
  data = model.data
51
51
  formula = model.formula
@@ -57,12 +57,9 @@ def compute_cv_fold_score(
57
57
  # Refit on training data using unified model()
58
58
  from bossanova.model import model as model_cls
59
59
 
60
- if model_type in ("lm", "glm"):
61
- new_model = model_cls(
62
- formula, train_data, family=model.family, link=model.link
63
- ).fit()
64
- else:
65
- raise ValueError(f"CV comparison not supported for model type: {model_type}")
60
+ new_model = model_cls(
61
+ formula, train_data, family=model.family, link=model.link
62
+ ).fit()
66
63
 
67
64
  # Predict on test data
68
65
  new_model.predict(newdata=test_data)
@@ -84,58 +81,112 @@ def compute_cv_fold_score(
84
81
  raise ValueError(f"Unknown metric: {metric}. Use 'mse', 'rmse', or 'mae'.")
85
82
 
86
83
 
84
+ def _resolve_compare_splits(
85
+ models: list[Any],
86
+ cv: int | str,
87
+ seed: int | None,
88
+ holdout_group: str | None,
89
+ ) -> tuple[list[tuple[np.ndarray, np.ndarray]], int]:
90
+ """Resolve fold splits for model comparison.
91
+
92
+ Checks all models for RE structure and resolves group-aware splitting.
93
+ For cross-type comparisons (lm vs lmer), the mixed model's grouping
94
+ governs splits for all models.
95
+
96
+ Args:
97
+ models: List of fitted models.
98
+ cv: Number of folds or ``"loo"``.
99
+ seed: Random seed.
100
+ holdout_group: User-specified grouping column, or None.
101
+
102
+ Returns:
103
+ Tuple of (splits list, effective integer k).
104
+ """
105
+ import warnings
106
+
107
+ n = models[0]._bundle.n
108
+
109
+ # Find the first model with RE structure (for cross-type comparisons)
110
+ group_ids = None
111
+ for m in models:
112
+ group_ids = resolve_holdout_group_ids(holdout_group, m.data, m._spec, m._bundle)
113
+ if group_ids is not None:
114
+ break
115
+
116
+ if group_ids is not None:
117
+ n_groups = len(np.unique(group_ids))
118
+
119
+ if cv == "loo":
120
+ splits = generate_group_kfold_splits(group_ids, k=n_groups, seed=seed)
121
+ return splits, n_groups
122
+
123
+ k = int(cv)
124
+ if k > n_groups:
125
+ warnings.warn(
126
+ f"k={k} exceeds number of groups ({n_groups}); using k={n_groups}",
127
+ stacklevel=4,
128
+ )
129
+ k = n_groups
130
+ splits = generate_group_kfold_splits(group_ids, k=k, seed=seed)
131
+ return splits, k
132
+
133
+ # No group structure — naive splits
134
+ if cv == "loo":
135
+ from bossanova.internal.infer.resample.core import generate_loo_indices
136
+
137
+ return generate_loo_indices(n), n
138
+
139
+ k = int(cv)
140
+ return generate_kfold_splits(n, k, seed=seed), k
141
+
142
+
87
143
  def compare_cv(
88
144
  models: list[Any],
89
145
  cv: int | str = 5,
90
146
  seed: int | None = None,
91
147
  metric: str = "mse",
148
+ holdout_group: str | None = None,
92
149
  ) -> pl.DataFrame:
93
150
  """Compare models using cross-validation with Nadeau-Bengio corrected t-test.
94
151
 
95
152
  Uses the corrected variance estimator from Nadeau & Bengio (2003) to account
96
153
  for the non-independence of CV folds (overlapping training sets).
97
154
 
155
+ When models have random effects, uses group-aware fold splitting to prevent
156
+ data leakage. For crossed random effects, ``holdout_group`` must be specified.
157
+
98
158
  Args:
99
- models: List of fitted models (lm or glm) to compare.
159
+ models: List of fitted models to compare.
100
160
  cv: Number of folds, or "loo" for leave-one-out.
101
161
  seed: Random seed for reproducible fold splits.
102
162
  metric: Error metric ("mse", "rmse", "mae").
163
+ holdout_group: Column name to use for group-aware fold splitting.
164
+ Required for crossed random effects. Can reference any column
165
+ in the model's data, even if not in the formula.
103
166
 
104
167
  Returns:
105
- DataFrame with columns:
106
- - model: Formula string
107
- - PRE: Proportional reduction in error
108
- - t_stat: Nadeau-Bengio corrected t-statistic
109
- - cv_score: Mean CV score (error)
110
- - cv_se: Standard error of CV score
111
- - diff: Difference from reference model (first model)
112
- - diff_se: Corrected standard error of difference
113
- - p_value: Two-sided p-value
168
+ DataFrame with columns: model, PRE, t_stat, cv_score, cv_se,
169
+ diff, diff_se, p_value.
114
170
 
115
171
  Notes:
116
172
  The Nadeau-Bengio correction accounts for the fact that k-fold CV
117
173
  training sets overlap by approximately (k-2)/(k-1) fraction.
118
174
 
119
- Corrected variance: var_corrected = var(diff) * (1/k + n_test/n_train)
120
- t-statistic: t = mean(diff) / sqrt(var_corrected)
121
- p-value: 2 * (1 - t.cdf(|t|, df=k-1))
175
+ With group-aware splits, fold sizes may be unequal. The correction
176
+ uses the mean n_test/n_train ratio across folds.
122
177
 
123
178
  References:
124
179
  Nadeau & Bengio (2003) "Inference for the Generalization Error"
125
180
  """
126
181
  n_models = len(models)
127
- n = models[0]._bundle.n
128
182
 
129
183
  # Generate shared fold splits for fair comparison
130
- rng = RNG.from_seed(seed)
131
- if cv == "loo":
132
- k = n # LOO has n folds
133
- splits = generate_loo_indices(n)
134
- else:
135
- k = int(cv)
136
- splits = generate_kfold_indices(rng, n, k, shuffle=True)
137
- n_train = len(splits[0][0])
138
- n_test = len(splits[0][1])
184
+ splits, k = _resolve_compare_splits(models, cv, seed, holdout_group)
185
+
186
+ # Compute mean n_test/n_train ratio for Nadeau-Bengio correction
187
+ # (handles unequal fold sizes from group splits)
188
+ ratios = [len(test) / len(train) for train, test in splits]
189
+ mean_ratio = float(np.mean(ratios))
139
190
 
140
191
  # Collect per-fold scores for each model
141
192
  fold_scores: dict[int, list[float]] = {i: [] for i in range(n_models)}
@@ -183,9 +234,8 @@ def compare_cv(
183
234
  mean_diff = np.mean(diff_vals)
184
235
  var_diff = np.var(diff_vals, ddof=1)
185
236
 
186
- # Nadeau-Bengio correction
187
- # var_corrected = var(diff) * (1/k + n_test/n_train)
188
- var_corrected = var_diff * (1 / k + n_test / n_train)
237
+ # Nadeau-Bengio correction with mean ratio for unequal folds
238
+ var_corrected = var_diff * (1 / k + mean_ratio)
189
239
  se_corrected = np.sqrt(var_corrected)
190
240
 
191
241
  # t-statistic and p-value
@@ -10,6 +10,7 @@ from typing import Any
10
10
 
11
11
  __all__ = [
12
12
  "check_nested",
13
+ "get_model_family_group",
13
14
  "get_model_type",
14
15
  "resolve_method",
15
16
  "sort_models_by_complexity",
@@ -34,6 +35,24 @@ def get_model_type(model: Any) -> str:
34
35
  return model._model_type
35
36
 
36
37
 
38
+ def get_model_family_group(model: Any) -> str:
39
+ """Get the family group for cross-type compatibility checking.
40
+
41
+ Models in the same family group can be compared via LRT because
42
+ the fixed-only model is nested within the mixed model (zero variance
43
+ components). Models in different family groups have incompatible
44
+ likelihoods and cannot be compared via hypothesis tests.
45
+
46
+ Args:
47
+ model: A fitted model instance.
48
+
49
+ Returns:
50
+ Family group string: 'gaussian' for lm/lmer, or the family name
51
+ (e.g. 'binomial', 'poisson', 'gamma') for glm/glmer.
52
+ """
53
+ return model._spec.family
54
+
55
+
37
56
  # =============================================================================
38
57
  # Validation and Sorting Helpers
39
58
  # =============================================================================
@@ -73,12 +92,31 @@ def validate_models(
73
92
  if not getattr(m, "_is_fitted", False):
74
93
  m.fit()
75
94
 
76
- # Check same type
95
+ # Check type compatibility
77
96
  model_types = [get_model_type(m) for m in models]
78
97
  if len(set(model_types)) > 1:
79
- raise ValueError(
80
- f"All models must be the same type. Got: {', '.join(model_types)}"
98
+ # Cross-type comparisons are allowed for:
99
+ # - lm + lmer (both Gaussian) via LRT
100
+ # - glm + glmer (same family) via LRT
101
+ # - Any combination via AIC/BIC/CV (non-hypothesis methods)
102
+ #
103
+ # Disallowed cross-type pairs (different likelihoods):
104
+ # - lm + glm, lm + glmer, glm + lmer, lmer + glmer
105
+ type_set = set(model_types)
106
+ family_groups = {get_model_family_group(m) for m in models}
107
+
108
+ is_valid_cross_type = len(family_groups) == 1 and (
109
+ type_set <= {"lm", "lmer"} or type_set <= {"glm", "glmer"}
81
110
  )
111
+ is_ic_or_cv = method in ("aic", "bic", "cv")
112
+
113
+ if not is_valid_cross_type and not is_ic_or_cv:
114
+ raise ValueError(
115
+ f"Cannot compare models of types {', '.join(model_types)} "
116
+ f"with method='{method}'. Cross-type hypothesis tests require "
117
+ "compatible families (lm+lmer or glm+glmer with same family). "
118
+ "Use method='aic', 'bic', or 'cv' for non-nested comparison."
119
+ )
82
120
 
83
121
  # Check same response
84
122
  responses = [m._spec.response_var for m in models]
@@ -95,10 +133,13 @@ def validate_models(
95
133
  )
96
134
 
97
135
  # For LRT, check that all models use ML estimation (unless refit=True)
98
- model_type = model_types[0]
99
- inferred_method = method if method != "auto" else resolve_method(models[0])
136
+ type_set = set(model_types)
137
+ inferred_method = (
138
+ method if method != "auto" else resolve_method(models[0], model_types=type_set)
139
+ )
140
+ has_mixed = any(t in ("lmer", "glmer") for t in model_types)
100
141
 
101
- if inferred_method == "lrt" and model_type in ("lmer", "glmer") and not refit:
142
+ if inferred_method == "lrt" and has_mixed and not refit:
102
143
  methods = [m._spec.method for m in models]
103
144
  reml_models = [m for m, meth in zip(models, methods) if meth == "reml"]
104
145
  if reml_models:
@@ -147,23 +188,30 @@ def sort_models_by_complexity(models: tuple[Any, ...]) -> list[Any]:
147
188
  return sorted(models, key=get_total_params)
148
189
 
149
190
 
150
- def resolve_method(model: Any) -> str:
191
+ def resolve_method(model: Any, *, model_types: set[str] | None = None) -> str:
151
192
  """Infer the appropriate comparison method for a model type.
152
193
 
153
- Reads ``model._model_type`` to determine the model type and maps it
154
- to the corresponding comparison method.
194
+ For same-type comparisons, reads ``model._model_type`` to determine
195
+ the method. For cross-type comparisons (lm+lmer or glm+glmer),
196
+ returns ``"lrt"`` since the fixed-only model is nested within the
197
+ mixed model via zero variance components.
155
198
 
156
199
  Args:
157
200
  model: A fitted model object with a ``_model_type`` attribute
158
201
  (one of ``"lm"``, ``"glm"``, ``"lmer"``, ``"glmer"``).
202
+ model_types: Set of all model types being compared. When provided,
203
+ enables cross-type method resolution.
159
204
 
160
205
  Returns:
161
- Comparison method: ``"f"`` for lm, ``"lrt"`` for lmer/glmer,
162
- ``"deviance"`` for glm.
206
+ Comparison method: ``"f"`` for lm, ``"lrt"`` for lmer/glmer
207
+ (and cross-type lm+lmer or glm+glmer), ``"deviance"`` for glm.
163
208
 
164
209
  Raises:
165
210
  ValueError: If the model type cannot be mapped to a comparison method.
166
211
  """
212
+ # Cross-type: lm+lmer or glm+glmer → always LRT
213
+ if model_types is not None and len(model_types) > 1:
214
+ return "lrt"
167
215
 
168
216
  if model._model_type == "lm":
169
217
  return "f"
@@ -16,7 +16,8 @@ from bossanova.internal.containers.schemas import (
16
16
  from bossanova.internal.maths.config import is_singular
17
17
  from bossanova.internal.maths.inference import compute_aic as compute_aic
18
18
  from bossanova.internal.maths.inference import compute_bic as _compute_bic
19
- from bossanova.internal.compare.helpers import check_nested, get_model_type
19
+ from bossanova.internal.maths.family import ESTIMATED_DISPERSION_FAMILIES
20
+ from bossanova.internal.compare.helpers import check_nested
20
21
 
21
22
  __all__ = [
22
23
  "compare_lrt",
@@ -25,23 +26,28 @@ __all__ = [
25
26
 
26
27
 
27
28
  def get_n_params(model: Any) -> int:
28
- """Get total number of parameters for a mixed model.
29
+ """Get total number of estimated parameters for LRT comparison.
30
+
31
+ Handles all model types (lm, glm, lmer, glmer) for cross-type
32
+ LRT comparisons where a fixed-only model is nested within a mixed
33
+ model via zero variance components.
34
+
35
+ Parameter counting follows R's ``logLik()`` convention:
36
+
37
+ - Fixed effects (p)
38
+ - Variance parameters (len(theta), mixed models only)
39
+ - Dispersion/sigma (+1 for Gaussian and other estimated-dispersion families)
29
40
 
30
41
  Args:
31
- model: A fitted lmer or glmer model.
42
+ model: A fitted model instance.
32
43
 
33
44
  Returns:
34
- Total parameter count: fixed effects + variance parameters + sigma (lmer only).
45
+ Total parameter count.
35
46
  """
36
47
  n_fixed = model._bundle.p
37
- n_theta = len(model._fit.theta)
38
-
39
- # lmer has sigma as additional parameter, glmer does not
40
- model_type = get_model_type(model)
41
- if model_type == "lmer":
42
- return n_fixed + n_theta + 1
43
- else: # glmer
44
- return n_fixed + n_theta
48
+ n_theta = len(model._fit.theta) if model._fit.theta is not None else 0
49
+ n_disp = 1 if model._spec.family in ESTIMATED_DISPERSION_FAMILIES else 0
50
+ return n_fixed + n_theta + n_disp
45
51
 
46
52
 
47
53
  def compare_lrt(models: list[Any]) -> pl.DataFrame:
@@ -41,6 +41,7 @@ from bossanova.internal.containers.structs import (
41
41
  MathDisplay,
42
42
  MeeState,
43
43
  ModelSpec,
44
+ PredictionConfig,
44
45
  PredictionState,
45
46
  ProfileState,
46
47
  REInfo,
@@ -106,6 +107,7 @@ __all__ = [
106
107
  "MathDisplay",
107
108
  "MeeState",
108
109
  "ModelSpec",
110
+ "PredictionConfig",
109
111
  "PredictionState",
110
112
  "ProfileState",
111
113
  "REInfo",
@@ -62,7 +62,7 @@ def build_params_dataframe(
62
62
 
63
63
  - **asymp**: all inference columns (p_value last).
64
64
  - **boot**: all inference columns; df = n_resamples.
65
- - **perm**: no CIs; df = n_valid_resamples.
65
+ - **perm**: percentile CIs; df = n_valid_resamples.
66
66
  - **cv**: PRE columns only.
67
67
  - **None**: term + estimate only.
68
68
 
@@ -101,6 +101,8 @@ def build_params_dataframe(
101
101
 
102
102
  if method == "perm":
103
103
  data[Col.SE] = params_inference.se.tolist()
104
+ data[Col.CI_LOWER] = params_inference.ci_lower.tolist()
105
+ data[Col.CI_UPPER] = params_inference.ci_upper.tolist()
104
106
  data[Col.STATISTIC] = params_inference.statistic.tolist()
105
107
  data[Col.DF] = params_inference.df.tolist()
106
108
  data[Col.P_VALUE] = params_inference.p_value.tolist()
@@ -224,7 +226,7 @@ def build_varying_corr_dataframe(
224
226
 
225
227
  Args:
226
228
  varying_spread: Variance component state containing ``rho``
227
- (dict mapping ``"effect1_effect2"`` keys to correlation values).
229
+ (dict mapping ``"effect1:effect2"`` keys to correlation values).
228
230
 
229
231
  Returns:
230
232
  DataFrame with columns ``group``, ``effect1``, ``effect2``, ``corr``.
@@ -235,10 +237,8 @@ def build_varying_corr_dataframe(
235
237
  corrs: list[float] = []
236
238
 
237
239
  for key, value in varying_spread.rho.items():
238
- # rho keys follow "effect1_effect2" naming from varying.py
239
- # which uses rho_key = corr_part.replace(":", "_")
240
- # where corr_part = "Intercept:Days" -> key = "Intercept_Days"
241
- parts = key.split("_", 1)
240
+ # rho keys use ":" separator: "Intercept:Days", "x_a:x_b"
241
+ parts = key.split(":", 1)
242
242
  if len(parts) == 2:
243
243
  effect1s.append(parts[0])
244
244
  effect2s.append(parts[1])
@@ -331,8 +331,8 @@ def build_predictions_dataframe(pred: PredictionState) -> pl.DataFrame:
331
331
  inference, and CV columns.
332
332
 
333
333
  Returns:
334
- DataFrame with ``fitted``, optional ``link``, inference columns,
335
- and optional CV columns.
334
+ DataFrame with optional grid columns (formula mode), ``fitted``,
335
+ optional ``link``, inference columns, and optional CV columns.
336
336
  """
337
337
  data: dict[str, list] = {Col.FITTED: pred.fitted.tolist()}
338
338
  if pred.link is not None:
@@ -348,6 +348,10 @@ def build_predictions_dataframe(pred: PredictionState) -> pl.DataFrame:
348
348
  pl.Series(Col.CV_FOLD, pred.cv_fold),
349
349
  )
350
350
 
351
+ # Prepend grid columns when formula-mode predictions
352
+ if pred.grid is not None:
353
+ result = pl.concat([pred.grid, result], how="horizontal")
354
+
351
355
  return result
352
356
 
353
357
 
@@ -15,18 +15,16 @@ from bossanova.internal.containers.schemas import (
15
15
  Col,
16
16
  ResamplesRawSchema,
17
17
  )
18
- from bossanova.internal.containers.structs.state import (
19
- ResamplesState,
20
- )
18
+ from bossanova.internal.containers.structs.inference import ResamplesState
21
19
 
22
20
  if TYPE_CHECKING:
23
21
  import polars as pl
24
22
  from numpy.typing import NDArray
25
23
 
26
- from bossanova.internal.containers.structs.state import (
24
+ from bossanova.internal.containers.structs.inference import (
27
25
  InferenceState,
28
- MeeState,
29
26
  )
27
+ from bossanova.internal.containers.structs.marginal import MeeState
30
28
 
31
29
  __all__ = [
32
30
  "build_mee_resamples",
@@ -7,17 +7,24 @@ from typing import TYPE_CHECKING
7
7
  import numpy as np
8
8
 
9
9
 
10
- from bossanova.internal.containers.structs.state import (
10
+ from bossanova.internal.containers.structs.fit import FitState
11
+ from bossanova.internal.containers.structs.inference import (
11
12
  CVState,
12
- FitState,
13
13
  InferenceState,
14
14
  JointTestState,
15
- MeeState,
16
- PredictionState,
17
- SimulationInferenceState,
15
+ )
16
+ from bossanova.internal.containers.structs.marginal import MeeState
17
+ from bossanova.internal.containers.structs.mixed import (
18
18
  VaryingSpreadState,
19
19
  VaryingState,
20
20
  )
21
+ from bossanova.internal.containers.structs.prediction import (
22
+ PredictionConfig,
23
+ PredictionState,
24
+ )
25
+ from bossanova.internal.containers.structs.simulation import (
26
+ SimulationInferenceState,
27
+ )
21
28
 
22
29
  if TYPE_CHECKING:
23
30
  import polars as pl
@@ -384,11 +391,13 @@ def build_prediction_state(
384
391
  *,
385
392
  link: np.ndarray | None = None,
386
393
  X_pred: np.ndarray | None = None,
394
+ config: PredictionConfig | None = None,
387
395
  se: np.ndarray | None = None,
388
396
  ci_lower: np.ndarray | None = None,
389
397
  ci_upper: np.ndarray | None = None,
390
398
  interval_type: str | None = None,
391
399
  conf_level: float | None = None,
400
+ grid: "pl.DataFrame | None" = None,
392
401
  ) -> PredictionState:
393
402
  """Build a PredictionState from prediction computation.
394
403
 
@@ -397,11 +406,14 @@ def build_prediction_state(
397
406
  link: Predicted values on link scale (for GLM/GLMM).
398
407
  X_pred: Design matrix used for predictions. Stored so that
399
408
  ``.infer()`` can compute delta-method SEs on the correct X.
409
+ config: Prediction configuration for bootstrap replay.
400
410
  se: Standard errors of predictions.
401
411
  ci_lower: Lower interval bounds.
402
412
  ci_upper: Upper interval bounds.
403
413
  interval_type: Type of interval ("confidence" or "prediction").
404
414
  conf_level: Confidence level for intervals.
415
+ grid: Grid DataFrame for formula-mode predictions. When present,
416
+ ``build_predictions_dataframe()`` prepends these columns.
405
417
 
406
418
  Returns:
407
419
  Frozen PredictionState instance.
@@ -428,11 +440,13 @@ def build_prediction_state(
428
440
  fitted=fitted,
429
441
  link=link,
430
442
  X_pred=X_pred,
443
+ config=config,
431
444
  se=se,
432
445
  ci_lower=ci_lower,
433
446
  ci_upper=ci_upper,
434
447
  interval_type=interval_type,
435
448
  conf_level=conf_level,
449
+ grid=grid,
436
450
  )
437
451
 
438
452
 
@@ -256,6 +256,8 @@ ParamsPerm = Schema(
256
256
  Col.TERM: String,
257
257
  Col.ESTIMATE: Float64,
258
258
  Col.SE: Float64,
259
+ Col.CI_LOWER: Float64,
260
+ Col.CI_UPPER: Float64,
259
261
  Col.STATISTIC: Float64,
260
262
  Col.DF: Float64,
261
263
  Col.P_VALUE: Float64,
@@ -512,7 +514,7 @@ VaryingSpreadInfer = Schema(
512
514
  # Component naming conventions:
513
515
  # - "sigma2": Residual variance
514
516
  # - "tau2_<group>:<effect>": Random effect variance (e.g., "tau2_subject:Intercept")
515
- # - "rho_<effect1>_<effect2>": Correlation between random effects
517
+ # - "rho_<effect1>:<effect2>": Correlation between random effects
516
518
  # - "icc": Intraclass correlation coefficient
517
519
 
518
520