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