Posterior Inference ===================== :func:`~prophys.engine.calibrate` returns a point estimate; :doc:`uncertainty` puts a Gaussian around it. :mod:`prophys.inference` gives the posterior itself, so a skewed, bounded or multi-modal posterior is reported as it is rather than as the best-fitting ellipse around its mode. Requires the ``inference`` extra:: pip install "prophys[inference]" The bridge is deliberately thin. A prophys model already supplies everything a gradient-based sampler needs — a differentiable log-likelihood over an unconstrained parameter vector — so the work is exposing that as a NumPyro potential function, declaring priors, and packaging the draws in the shape the rest of the engine consumes. Sampling --------- .. code-block:: python import prophys as prp from prophys.inference import sample_posterior fit = prp.calibrate(compiled, "y", data, n_steps=2000) posterior = sample_posterior( compiled, {"y": data}, init_params=fit.raw_params, # start at the mode: shorter warmup n_warmup=500, n_samples=1000, n_chains=2, ) print(posterior.summary()) :: PosteriorSummary: 2000 draws over 2 chain(s), 0 divergence(s) mu = 2.02293 +/- 0.1022 [1.83093, 2.21944] r_hat 1.001, ess 1000 sigma = 1.45369 +/- 0.07293 [1.31567, 1.60417] r_hat 0.999, ess 688 Sampling happens in **unconstrained** space, where the geometry is smooth and unbounded and NUTS performs well; the draws are mapped back through ``Param.forward`` before being reported, so a positive scale parameter's posterior never contains a negative value. Both forms are available: :attr:`~prophys.inference.Posterior.draws` (constrained) and :attr:`~prophys.inference.Posterior.raw_draws`. Convergence ------------ :meth:`~prophys.inference.Posterior.summary` reports the three things that decide whether the draws describe the posterior at all: * **r_hat** — split Gelman-Rubin, comparing chains *and* each chain's two halves, so a single chain that drifted over its run is detected. Above about 1.01 the chains have not mixed. * **ess** — effective sample size per parameter. A few hundred suffices for a posterior mean; a tail quantile needs more. * **divergences** — NUTS transitions that could not follow the posterior's geometry. Any at all mean the draws may be biased in a way more sampling does not fix. :attr:`~prophys.inference.PosteriorSummary.converged` requires all three to be healthy. A single-chain run reports ``False`` whatever the draws look like: with one chain there is no between-chain comparison to make. Chains started from ``init_params`` are jittered apart from each other. Identical starting points can agree for many draws without having explored anything, which is precisely the failure ``r_hat`` exists to detect. Priors ------- Priors are declared per parameter, on the **unconstrained** value — the scale the sampler works on. For a ``Param`` with bounds or a softplus transform, that scale is the pre-image of its constrained range. .. code-block:: python posterior = sample_posterior( compiled, {"y": data}, priors={ "mu": ("normal", {"loc": 0.0, "scale": 5.0}), "sigma": ("cauchy", {"loc": 0.0, "scale": 2.5}), }, ) Named priors are ``normal``, ``cauchy``, ``student_t``, ``uniform`` and ``laplace``; anything else is passed as a NumPyro distribution instance. Parameters without an entry get a wide ``Normal(0, 10)``: sampling needs a proper prior to have a normalizable target, and a flat improper one leaves an unbounded parameter free to wander where the likelihood is flat. Feeding the rest of the engine ------------------------------- :attr:`~prophys.inference.Posterior.draws` is the same mapping :func:`~prophys.engine.sample_parameters` produces, so every predictive score and posterior-predictive function accepts it directly: .. code-block:: python prp.waic(compiled, "y", data, posterior.thin(400)) prp.loo(compiled, "y", data, posterior.thin(400)) prp.posterior_predictive_check( compiled, "y", data, statistic="max", param_draws=posterior.thin(400) ) :meth:`~prophys.inference.Posterior.thin` takes a random subset rather than every k-th draw: with a residual periodicity in the chain, systematic thinning can land repeatedly on the same phase of it. Embedding in a larger NumPyro program -------------------------------------- :func:`~prophys.inference.potential_function` exposes the model's negative log-likelihood on its own, for use as one factor among several — inside a hierarchical model whose upper levels are declared in NumPyro directly, or alongside another likelihood term: .. code-block:: python from prophys.inference import potential_function import numpyro potential = potential_function(compiled, {"y": data}) def model(): params = {"mu": numpyro.sample("mu", dist.Normal(0.0, 5.0)), ...} numpyro.factor("prophys", -potential(params)) It is the same objective :func:`~prophys.engine.finetune` minimizes, so a posterior sampled from it and a point fit describe one model rather than two. .. autofunction:: prophys.inference.sample_posterior .. autoclass:: prophys.inference.Posterior :members: .. autoclass:: prophys.inference.PosteriorSummary :members: .. autofunction:: prophys.inference.potential_function