bossanova 0.1.0.dev13__tar.gz → 0.1.0.dev14__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 (236) hide show
  1. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/.gitignore +1 -0
  2. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/PKG-INFO +1 -1
  3. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/builders/specs.py +12 -52
  4. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/schemas.py +36 -0
  5. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/structs/data.py +3 -0
  6. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/structs/explore.py +29 -2
  7. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/bundle.py +77 -0
  8. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/common/formula_utils.py +43 -0
  9. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/parse.py +127 -0
  10. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/compute.py +74 -11
  11. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/conditions.py +4 -3
  12. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/contrasts.py +4 -11
  13. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/explore.py +218 -78
  14. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/__init__.py +10 -0
  15. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/harness.py +211 -118
  16. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/metrics.py +29 -0
  17. bossanova-0.1.0.dev14/bossanova/internal/operations/simulation/power.py +244 -0
  18. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/model/core.py +224 -6
  19. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/model/summary.py +23 -9
  20. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/pyproject.toml +1 -1
  21. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/LICENSE +0 -0
  22. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/README.md +0 -0
  23. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/__init__.py +0 -0
  24. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/README.md +0 -0
  25. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/__init__.py +0 -0
  26. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/advertising.csv +0 -0
  27. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/cake.csv +0 -0
  28. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/chickweight.csv +0 -0
  29. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/credit.csv +0 -0
  30. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/gammas.csv +0 -0
  31. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/mtcars.csv +0 -0
  32. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/penguins.csv +0 -0
  33. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/poker.csv +0 -0
  34. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/sleep.csv +0 -0
  35. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/titanic.csv +0 -0
  36. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/titanic_test.csv +0 -0
  37. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/data/titanic_train.csv +0 -0
  38. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/distributions/__init__.py +0 -0
  39. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/distributions/continuous.py +0 -0
  40. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/distributions/discrete.py +0 -0
  41. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/distributions/varying.py +0 -0
  42. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/expressions.py +0 -0
  43. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/__init__.py +0 -0
  44. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/__init__.py +0 -0
  45. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/builders/__init__.py +0 -0
  46. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/builders/data.py +0 -0
  47. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/builders/dataframes.py +0 -0
  48. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/builders/resamples.py +0 -0
  49. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/builders/results.py +0 -0
  50. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/builders/state.py +0 -0
  51. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/structs/__init__.py +0 -0
  52. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/structs/display.py +0 -0
  53. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/structs/formula.py +0 -0
  54. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/structs/specs.py +0 -0
  55. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/structs/state.py +0 -0
  56. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/containers/validators.py +0 -0
  57. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/__init__.py +0 -0
  58. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/backend/__init__.py +0 -0
  59. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/backend/dispatch.py +0 -0
  60. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/backend/jax.py +0 -0
  61. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/backend/numpy.py +0 -0
  62. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/backend/protocol.py +0 -0
  63. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/batching.py +0 -0
  64. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/config.py +0 -0
  65. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/convergence.py +0 -0
  66. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/design/__init__.py +0 -0
  67. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/design/coding.py +0 -0
  68. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/design/names.py +0 -0
  69. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/design/reference.py +0 -0
  70. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/design/z_matrix.py +0 -0
  71. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/differentiation.py +0 -0
  72. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/__init__.py +0 -0
  73. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/algebra.py +0 -0
  74. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/base.py +0 -0
  75. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/core.py +0 -0
  76. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/derived.py +0 -0
  77. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/factories.py +0 -0
  78. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/plotting.py +0 -0
  79. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/distributions/probability.py +0 -0
  80. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/__init__.py +0 -0
  81. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/binomial.py +0 -0
  82. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/create.py +0 -0
  83. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/gamma.py +0 -0
  84. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/gaussian.py +0 -0
  85. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/links.py +0 -0
  86. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/poisson.py +0 -0
  87. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/response.py +0 -0
  88. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/schema.py +0 -0
  89. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/family/tdist.py +0 -0
  90. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/__init__.py +0 -0
  91. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/contrasts.py +0 -0
  92. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/diagnostics.py +0 -0
  93. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/estimation.py +0 -0
  94. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/hypothesis.py +0 -0
  95. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/information_criteria.py +0 -0
  96. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/multiplicity.py +0 -0
  97. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/profile.py +0 -0
  98. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/sandwich.py +0 -0
  99. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/satterthwaite.py +0 -0
  100. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/wald_variance.py +0 -0
  101. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/inference/welch.py +0 -0
  102. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/linalg/__init__.py +0 -0
  103. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/linalg/qr.py +0 -0
  104. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/linalg/schur.py +0 -0
  105. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/linalg/sparse.py +0 -0
  106. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/linalg/svd.py +0 -0
  107. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/predict.py +0 -0
  108. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/rng.py +0 -0
  109. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/__init__.py +0 -0
  110. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/glm.py +0 -0
  111. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/glmer.py +0 -0
  112. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/heuristics.py +0 -0
  113. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/initialization.py +0 -0
  114. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/lambda_builder.py +0 -0
  115. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/lambda_sparse.py +0 -0
  116. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/lambda_template.py +0 -0
  117. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/lmer.py +0 -0
  118. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/optimize.py +0 -0
  119. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/pirls_sparse.py +0 -0
  120. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/solvers/quadrature.py +0 -0
  121. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/tolerances.py +0 -0
  122. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/transforms.py +0 -0
  123. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/variance.py +0 -0
  124. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/maths/weights.py +0 -0
  125. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/__init__.py +0 -0
  126. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/common/__init__.py +0 -0
  127. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/common/data_utils.py +0 -0
  128. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/common/factors.py +0 -0
  129. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/__init__.py +0 -0
  130. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/compare.py +0 -0
  131. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/cv.py +0 -0
  132. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/deviance.py +0 -0
  133. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/f_test.py +0 -0
  134. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/helpers.py +0 -0
  135. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/lrt.py +0 -0
  136. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/lrt_compare.py +0 -0
  137. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/compare/refit.py +0 -0
  138. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/contrasts.py +0 -0
  139. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/convergence.py +0 -0
  140. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/diagnostics.py +0 -0
  141. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/fit/__init__.py +0 -0
  142. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/fit/dispatch.py +0 -0
  143. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/fit/glm.py +0 -0
  144. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/fit/glmer.py +0 -0
  145. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/fit/lmer.py +0 -0
  146. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/fit/ols.py +0 -0
  147. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/fit/rank.py +0 -0
  148. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/__init__.py +0 -0
  149. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/design.py +0 -0
  150. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/encoding.py +0 -0
  151. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/evaluate.py +0 -0
  152. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/evaluate_newdata.py +0 -0
  153. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/evaluate_transforms.py +0 -0
  154. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/helpers.py +0 -0
  155. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/parser/__init__.py +0 -0
  156. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/parser/expr.py +0 -0
  157. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/parser/parser.py +0 -0
  158. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/parser/scanner.py +0 -0
  159. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/parser/token.py +0 -0
  160. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/formula/random_effects.py +0 -0
  161. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/__init__.py +0 -0
  162. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/asymptotic.py +0 -0
  163. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/bootstrap.py +0 -0
  164. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/cv.py +0 -0
  165. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/mee.py +0 -0
  166. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/params.py +0 -0
  167. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/permutation.py +0 -0
  168. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/prediction.py +0 -0
  169. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/profile.py +0 -0
  170. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/resample_bundle.py +0 -0
  171. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/satterthwaite_emm.py +0 -0
  172. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/infer/simulation.py +0 -0
  173. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/__init__.py +0 -0
  174. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/emm.py +0 -0
  175. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/grid.py +0 -0
  176. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/inference.py +0 -0
  177. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/joint_tests.py +0 -0
  178. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/slopes.py +0 -0
  179. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/marginal/validation.py +0 -0
  180. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/predict.py +0 -0
  181. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/profile.py +0 -0
  182. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/rendering/__init__.py +0 -0
  183. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/rendering/latex.py +0 -0
  184. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/__init__.py +0 -0
  185. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/common.py +0 -0
  186. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/core.py +0 -0
  187. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/glm.py +0 -0
  188. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/glmer.py +0 -0
  189. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/lm.py +0 -0
  190. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/lm_bca.py +0 -0
  191. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/lm_operators.py +0 -0
  192. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/lmer.py +0 -0
  193. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/mixed.py +0 -0
  194. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/results.py +0 -0
  195. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/resample/utils.py +0 -0
  196. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/dgp/__init__.py +0 -0
  197. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/dgp/generate.py +0 -0
  198. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/dgp/glm.py +0 -0
  199. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/dgp/glmer.py +0 -0
  200. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/dgp/lm.py +0 -0
  201. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/dgp/lmer.py +0 -0
  202. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/simulation/model_sim.py +0 -0
  203. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/transforms.py +0 -0
  204. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/operations/varying.py +0 -0
  205. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/README.md +0 -0
  206. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/__init__.py +0 -0
  207. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/cognition.py +0 -0
  208. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/compare.py +0 -0
  209. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/core.py +0 -0
  210. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/core_data.py +0 -0
  211. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/core_protocols.py +0 -0
  212. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/core_sizing.py +0 -0
  213. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/core_viz.py +0 -0
  214. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/dag.py +0 -0
  215. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/design.py +0 -0
  216. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/fit.py +0 -0
  217. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/fit_builders.py +0 -0
  218. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/fit_layers.py +0 -0
  219. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/helpers.py +0 -0
  220. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/lattice.py +0 -0
  221. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/layout.py +0 -0
  222. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/mem.py +0 -0
  223. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/params.py +0 -0
  224. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/predict.py +0 -0
  225. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/profile.py +0 -0
  226. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/ranef.py +0 -0
  227. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/relationships.py +0 -0
  228. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/resamples.py +0 -0
  229. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/resid.py +0 -0
  230. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/internal/viz/vif.py +0 -0
  231. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/model/__init__.py +0 -0
  232. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/model/guards.py +0 -0
  233. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/bossanova/py.typed +0 -0
  234. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/tests/bossanova_benchmarks/bootstrap/data/README.md +0 -0
  235. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/tests/bossanova_benchmarks/insteval/data/README.md +0 -0
  236. {bossanova-0.1.0.dev13 → bossanova-0.1.0.dev14}/tests/bossanova_tests/hypothesis/README.md +0 -0
@@ -83,3 +83,4 @@ bossanova-docs/wip/
83
83
  .hypothesis/
84
84
  research.md
85
85
  201b-ghct-05-models/
86
+ emmeans/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bossanova
3
- Version: 0.1.0.dev13
3
+ Version: 0.1.0.dev14
4
4
  Summary: Bridging statistical cultures with some jazz
5
5
  Author: Eshin Jolly
6
6
  License-Expression: MIT
@@ -145,9 +145,9 @@ def build_model_spec_from_formula(
145
145
  ) -> ModelSpec:
146
146
  """Build ModelSpec by parsing a formula string and resolving defaults.
147
147
 
148
- Lightweight formula parser that extracts response variable, fixed terms,
148
+ Parses the formula into an AST to extract response variable, fixed terms,
149
149
  and random effect terms without requiring data. Validates formula syntax
150
- via Scanner/Parser and resolves estimation method from family and RE.
150
+ and resolves estimation method from family and RE presence.
151
151
 
152
152
  Args:
153
153
  formula: R-style model formula (e.g., ``"y ~ x + (1|group)"``).
@@ -173,67 +173,27 @@ def build_model_spec_from_formula(
173
173
  if "~" not in formula:
174
174
  raise ValueError(f"Invalid formula: {formula!r}. Formula must contain '~'.")
175
175
 
176
- lhs, rhs = formula.split("~", 1)
177
- response = strip_backticks(lhs.strip())
178
-
179
- # Handle LHS transforms like rank(y), log(y), zscore(rank(y)) — unwrap all layers
180
- while True:
181
- lhs_func_match = re.match(r"^([a-zA-Z_]\w*)\((.+)\)$", response)
182
- if not lhs_func_match:
183
- break
184
- response = strip_backticks(lhs_func_match.group(2).strip())
185
-
186
- if not response:
187
- raise ValueError(
188
- f"Invalid formula: {formula!r}. No response variable specified."
189
- )
190
-
191
- # Expand || and / syntax before validation (these are valid lme4 syntax
192
- # that the Scanner/Parser doesn't handle directly)
193
- from bossanova.internal.operations.formula.parse import (
194
- expand_double_verts,
195
- expand_nested_syntax,
196
- )
197
-
198
- expanded, _ = expand_double_verts(formula)
199
- expanded, _ = expand_nested_syntax(expanded)
200
-
201
- # Validate expanded formula syntax via the real parser
202
- from bossanova.internal.operations.formula.parser import Parser, Scanner
176
+ from bossanova.internal.operations.formula.parse import extract_formula_structure
203
177
 
204
178
  try:
205
- tokens = Scanner(expanded).scan(add_intercept=False)
206
- Parser(tokens).parse()
179
+ structure = extract_formula_structure(formula)
207
180
  except Exception as exc:
208
181
  raise ValueError(f"Invalid formula: {formula!r}. {exc}") from exc
209
182
 
210
- # Use expanded RHS for RE detection and fixed term extraction
211
- _, expanded_rhs = expanded.split("~", 1)
212
-
213
- # Extract random effect terms: patterns like (1|group), (1+x|group), etc.
214
- re_pattern = r"\([^)]*\|[^)]*\)"
215
- random_terms = tuple(re.findall(re_pattern, expanded_rhs))
216
- has_re = len(random_terms) > 0
217
-
218
- # Extract fixed terms by removing random effects from RHS
219
- fixed_rhs = re.sub(re_pattern, "", expanded_rhs)
220
- raw_terms = [strip_backticks(t.strip()) for t in fixed_rhs.split("+") if t.strip()]
221
-
222
- # Build fixed terms list (add Intercept unless suppressed with 0 or -1)
223
- has_intercept = not any(t in ("0", "-1") for t in raw_terms)
224
- fixed_terms: list[str] = ["Intercept"] if has_intercept else []
225
- fixed_terms.extend(t for t in raw_terms if t not in ("0", "-1", "1"))
226
- fixed_terms_tuple = tuple(fixed_terms) if fixed_terms else ("Intercept",)
183
+ if structure.response_var is None:
184
+ raise ValueError(
185
+ f"Invalid formula: {formula!r}. No response variable specified."
186
+ )
227
187
 
228
188
  return build_model_spec(
229
189
  formula=formula,
230
190
  family=family,
231
191
  link=link,
232
192
  method=method,
233
- has_random_effects=has_re,
234
- response_var=response,
235
- fixed_terms=fixed_terms_tuple,
236
- random_terms=random_terms,
193
+ has_random_effects=structure.has_random_effects,
194
+ response_var=strip_backticks(structure.response_var),
195
+ fixed_terms=structure.fixed_term_names,
196
+ random_terms=structure.random_terms_raw,
237
197
  )
238
198
 
239
199
 
@@ -34,6 +34,7 @@ __all__ = [
34
34
  "ParamsBoot",
35
35
  "ParamsCv",
36
36
  "ParamsPerm",
37
+ "PowerSummaryCols",
37
38
  "PredictionsAsymp",
38
39
  "PredictionsBase",
39
40
  "PredictionsCv",
@@ -164,6 +165,20 @@ class Col:
164
165
  SIM_MEAN: str = "sim_mean"
165
166
  SIM_SD: str = "sim_sd"
166
167
 
168
+ # -- Power analysis --
169
+ POWER: str = "power"
170
+ POWER_CI_LOWER: str = "power_ci_lower"
171
+ POWER_CI_UPPER: str = "power_ci_upper"
172
+ TRUE_VALUE: str = "true_value"
173
+ COVERAGE: str = "coverage"
174
+ BIAS: str = "bias"
175
+ RMSE: str = "rmse"
176
+ MEAN_SE: str = "mean_se"
177
+ EMPIRICAL_SE: str = "empirical_se"
178
+ N_SIMS: str = "n_sims"
179
+ N_FAILED: str = "n_failed"
180
+ N: str = "n"
181
+
167
182
  # -- CV diagnostics --
168
183
  CV_RMSE: str = "cv_rmse"
169
184
  CV_MAE: str = "cv_mae"
@@ -277,6 +292,27 @@ AugmentedDataCols = (Col.FITTED, Col.RESID, Col.HAT, Col.STD_RESID, Col.COOKSD)
277
292
  SimulationsInferCols = (Col.SIM_MEAN, Col.SIM_SD, "sim_q025", "sim_q975")
278
293
 
279
294
 
295
+ # =============================================================================
296
+ # Power analysis columns (rows = grid points × terms)
297
+ # =============================================================================
298
+
299
+ # Note: Grid columns (n, sigma, coef values) are dynamic — only metric columns are fixed
300
+ PowerSummaryCols = (
301
+ Col.TERM,
302
+ Col.TRUE_VALUE,
303
+ Col.POWER,
304
+ Col.POWER_CI_LOWER,
305
+ Col.POWER_CI_UPPER,
306
+ Col.COVERAGE,
307
+ Col.BIAS,
308
+ Col.RMSE,
309
+ Col.MEAN_SE,
310
+ Col.EMPIRICAL_SE,
311
+ Col.N_SIMS,
312
+ Col.N_FAILED,
313
+ )
314
+
315
+
280
316
  # =============================================================================
281
317
  # .effects Schemas (rows = grid points for EMM/marginal effects)
282
318
  # =============================================================================
@@ -196,6 +196,9 @@ class DataBundle:
196
196
  # Random effects metadata
197
197
  re_metadata: REMetadata | None = field(default=None)
198
198
 
199
+ # Binary response level mapping (e.g. ("No", "Yes") means No→0, Yes→1)
200
+ response_levels: tuple[str, ...] | None = field(default=None)
201
+
199
202
  # Rank deficiency info (None means full rank or not yet checked)
200
203
  rank_info: RankInfo | None = field(default=None)
201
204
 
@@ -44,14 +44,41 @@ class ExploreFormula:
44
44
  means use n_levels - 1, i.e., maximum degree).
45
45
  conditions: Tuple of Condition objects specifying conditioning variables.
46
46
  focal_at_values: Specific values to evaluate the focal variable at
47
- (e.g., from ``Days[0, 3, 6, 9]`` syntax). None means use all levels.
47
+ (e.g., from ``Days@[0, 3, 6, 9]`` syntax). None means use all levels.
48
+ focal_at_range: Number of evenly-spaced values across the focal variable's
49
+ range (e.g., from ``Days@range(5)`` syntax). None means not set.
50
+ focal_at_quantile: Number of quantile values for the focal variable
51
+ (e.g., from ``Days@quantile(3)`` syntax). None means not set.
48
52
  """
49
53
 
50
54
  focal_var: str = field(validator=validators.instance_of(str))
51
55
  contrast_type: str | None = field(default=None)
52
56
  contrast_degree: int | None = field(default=None)
53
57
  conditions: tuple[Condition, ...] = field(factory=tuple, converter=tuple)
54
- focal_at_values: tuple[float, ...] | None = field(default=None)
58
+ focal_at_values: tuple[float | str, ...] | None = field(default=None)
59
+ focal_at_range: int | None = field(default=None)
60
+ focal_at_quantile: int | None = field(default=None)
61
+
62
+ def __attrs_post_init__(self) -> None:
63
+ """Validate at most one focal at-spec is set."""
64
+ n_set = sum(
65
+ x is not None
66
+ for x in (self.focal_at_values, self.focal_at_range, self.focal_at_quantile)
67
+ )
68
+ if n_set > 1:
69
+ raise ValueError(
70
+ "At most one of focal_at_values, focal_at_range, focal_at_quantile "
71
+ "may be set."
72
+ )
73
+
74
+ @property
75
+ def has_focal_at(self) -> bool:
76
+ """Return True if any focal at-spec is set."""
77
+ return (
78
+ self.focal_at_values is not None
79
+ or self.focal_at_range is not None
80
+ or self.focal_at_quantile is not None
81
+ )
55
82
 
56
83
  @property
57
84
  def has_contrast(self) -> bool:
@@ -71,6 +71,17 @@ def build_bundle_from_data(
71
71
  if response_var not in data.columns:
72
72
  raise ValueError(f"Response variable '{response_var}' not found in data")
73
73
 
74
+ # Auto-encode string/categorical response for binomial family
75
+ response_levels: tuple[str, ...] | None = None
76
+ if spec.family == "binomial":
77
+ import polars as pl
78
+
79
+ response_dtype = data[response_var].dtype
80
+ if response_dtype in (pl.String, pl.Categorical) or isinstance(
81
+ response_dtype, pl.Enum
82
+ ):
83
+ data, response_levels = _encode_binary_response(data, response_var)
84
+
74
85
  # Functional formula API: parse -> build -> return learned spec
75
86
  formula_spec = parse_formula(
76
87
  formula,
@@ -150,6 +161,7 @@ def build_bundle_from_data(
150
161
  re_metadata=re_metadata,
151
162
  factor_levels={k: list(v) for k, v in formula_spec.factors.items()},
152
163
  contrast_types=dict(formula_spec.contrast_types),
164
+ response_levels=response_levels,
153
165
  rank_info=rank_info,
154
166
  )
155
167
 
@@ -352,6 +364,48 @@ def _raise_missing_detail(
352
364
  )
353
365
 
354
366
 
367
+ def _encode_binary_response(
368
+ data: pl.DataFrame,
369
+ response_var: str,
370
+ ) -> tuple[pl.DataFrame, tuple[str, ...]]:
371
+ """Auto-encode a string/categorical response for binomial models.
372
+
373
+ Sorts unique levels alphabetically (matching R's ``factor()`` ordering)
374
+ and maps the first level to 0 and the second to 1.
375
+
376
+ Args:
377
+ data: Input DataFrame with a string/categorical response column.
378
+ response_var: Name of the response column.
379
+
380
+ Returns:
381
+ Tuple of (modified DataFrame with numeric response, level mapping).
382
+ The level mapping is ``(level_for_0, level_for_1)``.
383
+
384
+ Raises:
385
+ ValueError: If the response column does not have exactly 2 unique levels.
386
+ """
387
+ import polars as pl
388
+
389
+ col = data[response_var]
390
+ levels = sorted(col.cast(pl.String).unique().drop_nulls().to_list())
391
+ if len(levels) != 2:
392
+ raise ValueError(
393
+ f"Binomial family with string response requires exactly 2 levels, "
394
+ f"but '{response_var}' has {len(levels)}: {levels}. "
395
+ f"Use a numeric 0/1 response or a two-level factor."
396
+ )
397
+
398
+ # Encode: first alphabetically → 0, second → 1
399
+ encoded = (
400
+ pl.when(pl.col(response_var).cast(pl.String) == levels[1])
401
+ .then(1.0)
402
+ .otherwise(0.0)
403
+ .alias(response_var)
404
+ )
405
+ data = data.with_columns(encoded)
406
+ return data, tuple(levels)
407
+
408
+
355
409
  def _validate_response(family: str, y: np.ndarray, response_var: str) -> None:
356
410
  """Family-specific response variable validation.
357
411
 
@@ -373,6 +427,15 @@ def _validate_response(family: str, y: np.ndarray, response_var: str) -> None:
373
427
  f"Check your data or use a different family (e.g., gaussian)."
374
428
  )
375
429
 
430
+ if family == "binomial":
431
+ if np.any(y < 0) or np.any(y > 1):
432
+ warnings.warn(
433
+ f"Binomial family expects response values in [0, 1], "
434
+ f"but '{response_var}' has values outside this range. "
435
+ f"This matches R's behavior (non-integer successes warning).",
436
+ stacklevel=4,
437
+ )
438
+
376
439
 
377
440
  def _reindex_groups_after_na_drop(
378
441
  *,
@@ -514,6 +577,20 @@ def _build_random_effects(
514
577
  build_random_effects_from_spec,
515
578
  )
516
579
 
580
+ # Ensure String columns known as factors are Enum before RE construction.
581
+ # build_design_matrices handles this for the FE path, but the RE path
582
+ # receives the original DataFrame. Without this, factor() calls in RE
583
+ # terms (e.g. "(factor(x)|group)") crash in encode_categorical.
584
+ import polars as pl
585
+
586
+ if formula_spec.factors:
587
+ casts = []
588
+ for col_name, levels in formula_spec.factors.items():
589
+ if col_name in data.columns and data[col_name].dtype == pl.String:
590
+ casts.append(pl.col(col_name).cast(pl.Enum(levels)))
591
+ if casts:
592
+ data = data.with_columns(casts)
593
+
517
594
  re_info = build_random_effects_from_spec(formula_spec, data)
518
595
  if re_info is None:
519
596
  return None, None
@@ -4,6 +4,7 @@ import re
4
4
 
5
5
  __all__ = [
6
6
  "build_ablated_formula",
7
+ "parse_value",
7
8
  "remove_predictor_from_formula",
8
9
  ]
9
10
 
@@ -55,6 +56,48 @@ def build_ablated_formula(
55
56
  return f"{response_var} ~ {' + '.join(rhs_parts)}"
56
57
 
57
58
 
59
+ def parse_value(s: str) -> float | str:
60
+ """Parse a single value as float or string.
61
+
62
+ Tries to parse as float first; falls back to unquoted or quoted string.
63
+
64
+ Args:
65
+ s: String to parse.
66
+
67
+ Returns:
68
+ Parsed value as float or string.
69
+
70
+ Raises:
71
+ ValueError: If the string is empty.
72
+
73
+ Examples:
74
+ >>> parse_value("3.14")
75
+ 3.14
76
+ >>> parse_value("hello")
77
+ 'hello'
78
+ >>> parse_value("'quoted'")
79
+ 'quoted'
80
+ """
81
+ s = s.strip()
82
+ if not s:
83
+ raise ValueError("Empty value")
84
+
85
+ # Try to parse as number
86
+ try:
87
+ return float(s)
88
+ except ValueError:
89
+ pass
90
+
91
+ # Remove quotes if present
92
+ if (s.startswith('"') and s.endswith('"')) or (
93
+ s.startswith("'") and s.endswith("'")
94
+ ):
95
+ return s[1:-1]
96
+
97
+ # Return as-is (unquoted string, e.g., factor level)
98
+ return s
99
+
100
+
58
101
  def remove_predictor_from_formula(formula: str, predictor: str) -> str:
59
102
  """Remove a predictor term from a formula string.
60
103
 
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import re
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from attrs import frozen
9
+
8
10
  from bossanova.internal.operations.formula.parser import Parser, Scanner
9
11
  from bossanova.internal.operations.formula.parser.expr import (
10
12
  Binary,
@@ -28,8 +30,10 @@ if TYPE_CHECKING:
28
30
 
29
31
  __all__ = [
30
32
  "FormulaError",
33
+ "FormulaStructure",
31
34
  "expand_double_verts",
32
35
  "expand_nested_syntax",
36
+ "extract_formula_structure",
33
37
  "parse_formula",
34
38
  ]
35
39
 
@@ -235,6 +239,129 @@ def extract_rhs_terms(
235
239
  return rhs_terms, re_terms, has_intercept
236
240
 
237
241
 
242
+ @frozen
243
+ class FormulaStructure:
244
+ """Data-free formula structure extracted from an AST.
245
+
246
+ Contains the same structural information as a full ``parse_formula()``
247
+ call but without requiring data for categorical detection. Used by
248
+ ``build_model_spec_from_formula()`` to replace regex-based extraction.
249
+
250
+ Attributes:
251
+ response_var: Response variable name, or None for RHS-only formulas.
252
+ response_transform: Tuple of LHS transforms (innermost-first),
253
+ or None if no transforms.
254
+ fixed_term_names: Human-readable fixed-effect term names
255
+ (e.g. ``["Intercept", "x", "group"]``).
256
+ has_intercept: Whether the formula includes an intercept.
257
+ has_random_effects: Whether the formula contains ``|`` terms.
258
+ random_terms_raw: Raw string representations of RE terms
259
+ (e.g. ``["(1|subject)"]``).
260
+ """
261
+
262
+ response_var: str | None
263
+ response_transform: tuple[str, ...] | None
264
+ fixed_term_names: tuple[str, ...]
265
+ has_intercept: bool
266
+ has_random_effects: bool
267
+ random_terms_raw: tuple[str, ...]
268
+
269
+
270
+ def extract_formula_structure(formula: str) -> FormulaStructure:
271
+ """Extract formula structure from a formula string without data.
272
+
273
+ Parses the formula into an AST and walks it to extract response
274
+ variable, fixed-effect term names, intercept presence, and
275
+ random-effect term strings. Does not require data (no categorical
276
+ detection).
277
+
278
+ Args:
279
+ formula: R-style formula string (e.g. ``"y ~ x + (1|group)"``).
280
+
281
+ Returns:
282
+ FormulaStructure with extracted information.
283
+
284
+ Raises:
285
+ FormulaError: If formula syntax is invalid.
286
+ """
287
+ # Known transforms that can appear on the LHS
288
+ _KNOWN_RESPONSE_TRANSFORMS = set(STATEFUL_TRANSFORMS) | {"log", "log10", "sqrt"}
289
+
290
+ # Pre-process: expand || and / syntax
291
+ expanded, _ = expand_double_verts(formula)
292
+ expanded, _ = expand_nested_syntax(expanded)
293
+
294
+ # Tokenize and parse
295
+ tokens = Scanner(expanded).scan(add_intercept=False)
296
+ ast = Parser(tokens, expanded).parse()
297
+
298
+ response_var: str | None = None
299
+ response_transform: tuple[str, ...] | None = None
300
+
301
+ if isinstance(ast, Binary) and ast.operator.kind == "TILDE":
302
+ # Extract response from LHS
303
+ if isinstance(ast.left, Call) and isinstance(ast.left.callee, Variable):
304
+ chain: list[str] = []
305
+ node = ast.left
306
+ while isinstance(node, Call) and isinstance(node.callee, Variable):
307
+ func_name = node.callee.name.lexeme
308
+ if func_name not in _KNOWN_RESPONSE_TRANSFORMS:
309
+ raise FormulaError(
310
+ f"Unknown response transform '{func_name}'. "
311
+ f"Supported: {sorted(_KNOWN_RESPONSE_TRANSFORMS)}",
312
+ formula=formula,
313
+ )
314
+ if not node.args:
315
+ raise FormulaError(
316
+ f"{func_name}() requires an argument",
317
+ formula=formula,
318
+ )
319
+ chain.append(func_name)
320
+ node = node.args[0]
321
+ var_name = extract_name(node)
322
+ if var_name is None:
323
+ raise FormulaError(
324
+ f"{chain[-1]}() argument must be a variable name",
325
+ formula=formula,
326
+ )
327
+ response_var = var_name
328
+ response_transform = tuple(reversed(chain))
329
+ else:
330
+ response_var = extract_name(ast.left)
331
+ rhs_terms, re_terms, has_intercept = extract_rhs_terms(ast.right)
332
+ else:
333
+ rhs_terms, re_terms, has_intercept = extract_rhs_terms(ast)
334
+
335
+ # Extract human-readable fixed term names from AST nodes
336
+ fixed_names: list[str] = []
337
+ if has_intercept:
338
+ fixed_names.append("Intercept")
339
+ for term in rhs_terms:
340
+ name = extract_name(term)
341
+ # For Call nodes like factor(x), extract the argument name
342
+ if name is None and isinstance(term, Call) and term.args:
343
+ name = extract_name(term.args[0])
344
+ if name is not None and name not in ("0", "1", "-1"):
345
+ fixed_names.append(name)
346
+
347
+ # Extract RE term strings from the original expanded formula
348
+ re_pattern = r"\([^)]*\|[^)]*\)"
349
+ _, expanded_rhs = expanded.split("~", 1) if "~" in expanded else ("", expanded)
350
+ random_terms_raw = tuple(re.findall(re_pattern, expanded_rhs))
351
+
352
+ if not fixed_names:
353
+ fixed_names = ["Intercept"]
354
+
355
+ return FormulaStructure(
356
+ response_var=response_var,
357
+ response_transform=response_transform,
358
+ fixed_term_names=tuple(fixed_names),
359
+ has_intercept=has_intercept,
360
+ has_random_effects=len(re_terms) > 0,
361
+ random_terms_raw=random_terms_raw,
362
+ )
363
+
364
+
238
365
  # =============================================================================
239
366
  # Formula pre-processing (|| and / syntax expansion)
240
367
  # =============================================================================