Tutorials / Validating an analysis

Validating an analysis

Convergence diagnostics say whether a sampler explored the posterior. They do not say whether the posterior is calibrated. This tutorial checks a whole pipeline with binference’s validation tools: an exact reference for a small problem, simulation-based calibration (SBC), and coverage. It also shows what a failure looks like.

import warnings

import numpy as np

import binference
from binference import priors
from binference.validation import coverage, exact_posterior, sbc

Exact posteriors

With three bins, binference can integrate the posterior on a grid. Here it compares that exact posterior with NUTS and Gibbs, on data with a selection:

bins = binference.Bins([0.0, 1.0, 2.0, 3.0])
S = np.array([0.9, 0.5, 0.25])
mock = binference.simulate.mock_1d(250, bins, density=np.array([0.4, 0.35, 0.25]),
                               noise=(0.3, 0.8), selection=S, rng=777)
exact = exact_posterior(mock.likelihoods, priors.Dirichlet(1.0))
nuts = binference.infer(mock.likelihoods, method="nuts", rng=1)
gibbs = binference.infer(mock.likelihoods, method="gibbs", rng=1, n_samples=4000)
print("exact mean", exact.mean.round(4), " sd", exact.std.round(4))
print("NUTS  mean", nuts.mean.round(4), " sd", nuts.std.round(4))
print("Gibbs mean", gibbs.mean.round(4), " sd", gibbs.std.round(4))
exact mean [0.4071 0.3173 0.2756]  sd [0.0429 0.0642 0.0601]
NUTS  mean [0.4072 0.3164 0.2765]  sd [0.0416 0.0632 0.0588]
Gibbs mean [0.4077 0.3209 0.2714]  sd [0.0424 0.0651 0.062 ]

The theory page describes the test suite’s comparisons.

Simulation-based calibration

SBC (Talts et al. 2018) checks the whole loop of prior, simulator and inference (simulation-based calibration). Here it checks 150 fits of 50 objects in four bins. The measurement errors are 0.1, less than half a bin, so that every Gibbs fit converges in a short run:

prior = priors.Dirichlet(1.0)
grid = binference.Bins.linear(0.0, 1.0, 4)
simulate = binference.simulate.prior_predictive(prior, grid, n_objects=50, noise=0.1)

def fit_gibbs(lk, rng):
    return binference.infer(lk, prior, method="gibbs", rng=rng, n_chains=4,
                        n_warmup=200, n_samples=500, check=False)

result = sbc(simulate, fit_gibbs, n_simulations=150, n_draws=19, rng=0)
print("uniformity p-values:",
      {n: round(float(v), 3) for n, v in zip(result.names, result.pvalues(10))})
uniformity p-values: {'rho[0]': 0.893, 'rho[1]': 0.936, 'rho[2]': 0.057, 'rho[3]': 0.534, 'loglike': 0.35}
result.plot();
Five small histograms of ranks from 0 to 19. The bars stay within or at the edge of the grey bands, so the histograms are flat within the noise.

SBC rank histograms of the four fractions and of the log-likelihood of the true population (last panel), for Gibbs. The grey band holds 99% of histograms of uniform ranks.

The rank histograms sit inside the band expected for uniform ranks. The last panel ranks the log-likelihood of the true population. It is sensitive to errors in the correlations between bins, which per-bin ranks can miss.

What a failure looks like

Mean-field VB is over-confident, so the true value falls outside the bulk of its draws too often. It is most over-confident when the likelihoods are broad, so this check uses errors of 0.25, as wide as a bin. The rank histograms of the fractions become U-shaped:

def fit_vb(lk, rng):
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", binference.ApproximationWarning)
        return binference.infer(lk, prior, method="vb", rng=rng, n_samples=1000)

simulate_broad = binference.simulate.prior_predictive(prior, grid, n_objects=50, noise=0.25)
bad = sbc(simulate_broad, fit_vb, n_simulations=150, n_draws=19, rng=0)
print("uniformity p-values:",
      {n: round(float(v), 4) for n, v in zip(bad.names, bad.pvalues(10))})
uniformity p-values: {'rho[0]': 0.0, 'rho[1]': 0.0, 'rho[2]': 0.0, 'rho[3]': 0.0, 'loglike': 0.0}
bad.plot();
Five small histograms of ranks from 0 to 19. The four for the fractions are shaped like a U, with the lowest and highest ranks far above the grey band. The last, for the log-likelihood, piles up at the lowest ranks.

SBC rank histograms for mean-field VB. The grey band holds 99% of histograms of uniform ranks.

Coverage

Coverage asks how often central credible intervals contain the truth. With data simulated from the prior, Bayesian intervals have their nominal coverage on average. coverage checks any derived quantity, here the population mean:

centers = grid.centers
cov = coverage(simulate, fit_gibbs, n_simulations=150, levels=(0.5, 0.9),
               functionals={"mean": lambda rho: rho @ centers}, rng=1)
rates = cov.coverage_rates()                   # (levels, quantities)
se = cov.stderr().ravel()                      # one standard error per level
print(f"{'level':>6s}" + "".join(f"{n:>8s}" for n in cov.names))
for level, row, e in zip(cov.levels, rates, se):
    print(f"{level:6.2f}" + "".join(f"{r:8.2f}" for r in row) + f"   (standard error {e:.3f})")
z = (rates - np.array(cov.levels)[:, None]) / se[:, None]
print(f"deviation from nominal, in standard errors: mean {z.mean():.1f}, largest {np.abs(z).max():.1f}")
 level  rho[0]  rho[1]  rho[2]  rho[3]    mean
  0.50    0.50    0.45    0.48    0.49    0.44   (standard error 0.041)
  0.90    0.90    0.85    0.89    0.92    0.88   (standard error 0.024)
deviation from nominal, in standard errors: mean -0.6, largest 1.9

Each rate lies within about two standard errors of its level, as it should for a calibrated fit.

Validating your own pipeline

To check your own analysis, simulate catalogues like yours and call sbc or coverage with the settings of the real fit (mock catalogues).