Guides / Choose a prior
Choose a prior¶
The prior on the population matters most when the data say least: many bins, few objects, or measurement errors as wide as the bins. Start with the default. Change it when the checks at the end of this page say the prior, not the data, sets the answer. What each prior means is in the theory.
Your situation |
Prior to try |
|---|---|
no strong reason to choose |
|
about as many bins as objects |
|
a jagged fit, with errors as wide as the bins |
|
a reference sample of the same population |
|
unequal bins, with a Dirichlet |
|
expected numbers of objects, with prior information on them |
|
binference’s priors fall into three families. The Dirichlet prior family is conjugate, so every engine takes it. The logistic priors add smoothness across neighbouring bins; Gibbs and VB do not accept them. The Gamma rates prior models expected counts rather than fractions.
import numpy as np
import binference
from binference import priors
mock = binference.simulate.mock_1d(1000, binference.Bins.linear(0, 3, 30), noise=(0.1, 0.3), rng=4)
lk = mock.likelihoods
Pass a prior as the second argument of infer(), or as prior=
to PopulationModel and PopulationEstimator.
The Dirichlet family¶
flat = priors.Dirichlet(1.0) # the default
sparse = priors.Dirichlet.reference() # concentration 1/K per bin
by_volume = priors.Dirichlet.from_base_measure(5.0) # concentration in proportion to bin width
ref_counts = np.bincount(mock.truth.labels[:50], minlength=30)
informed = priors.Dirichlet.from_counts(ref_counts, strength=0.5)
for p in (flat, sparse, informed):
print(p, binference.infer(lk, p, method="map").rho[:3].round(4))
Dirichlet(1.0): use it unless you have a reason not to.Dirichlet.reference(): use it when the number of bins \(K\) is comparable to the number of objects \(N\). There the default biases the fit toward equal fractions.Dirichlet.from_base_measure(a): use it on unequal bins, so the prior means the same density however you bin.Dirichlet.from_counts(counts, strength): use it when you have the counts \(n^{\rm ref}_k\) of a reference sample.strengthis \(\kappa\): 1 trusts the sample fully, and smaller values trust it less.
GeneralizedDirichlet, DirichletTree and PolyaTree stay conjugate, with more
freedom in how bins co-vary:
gd = priors.GeneralizedDirichlet(a=2.0, b=6.0) # stick-breaking Betas
tree = priors.DirichletTree([[0, 1, 2], [3, [4, 5]]], alpha=2.0)
polya = priors.PolyaTree(c=2.0) # dyadic, multi-scale
print(binference.infer(lk, polya, method="map").rho[:3].round(4))
DirichletTree: use it when groups of bins belong together, such as subclasses of a class. Pass the groups as nested lists of bin indices, or as dicts of named groups; on categorical bins a bin can be named by its label. A dictalphasets the concentration of the branch into each group, or bin as the tree writes it, that it names; the other branches keep 1.PolyaTree: use it on 1-D ordered bins for a conjugate prior with structure at several scales.
describe(bins) draws the tree of any prior in this family: each branch with its
concentration, the share of its parent it takes on average, and the mean fraction of the
population below it. Pass counts to see the posterior given them.
classes = binference.Bins.categories(["Sa", "Sb", "E0", "E3", "S0"])
early = {"elliptical": ["E0", "E3"], "lenticular": ["S0"]}
named = priors.DirichletTree({"spiral": ["Sa", "Sb"], "early": early}, alpha={"spiral": 3.0})
print(named.describe(classes))
DirichletTree: prior, on 5 bins
root: Sa, Sb, E0, E3, S0
├── α=3 share 0.75 mass 0.750 spiral: Sa, Sb
│ ├── α=1 share 0.50 mass 0.375 Sa
│ └── α=1 share 0.50 mass 0.375 Sb
└── α=1 share 0.25 mass 0.250 early: E0, E3, S0
├── α=1 share 0.50 mass 0.125 elliptical: E0, E3
│ ├── α=1 share 0.50 mass 0.062 E0
│ └── α=1 share 0.50 mass 0.062 E3
└── α=1 share 0.50 mass 0.125 lenticular: S0
Smoothness: the logistic family¶
If the Dirichlet fit is jagged and the errors are as wide as the bins, use a smoothness prior. It infers how smooth the population is:
rw = priors.LogisticRandomWalk(order=2)
gp = priors.LogisticGP(kernel="matern32")
print(binference.infer(lk, gp, method="map").rho[:3].round(4))
prior |
use it when |
hyperparameters (inferred by default) |
|---|---|---|
|
neighbouring bins should be close |
|
|
the population follows smooth trends |
|
|
you want a stationary Gaussian process: |
|
|
you have a covariance, or want independent bins |
|
Grids. All four work on 1-D bins and on grids. Only
LogisticNormalaccepts masked grids and categories.Rough populations. If a
LogisticGPfit looks too smooth, raisen_basis(defaults).Hyperpriors. Give a hyperparameter a hyperprior, or a number to fix it:
gp_fixed = priors.LogisticGP(amplitude=1.5, length_scale=priors.LogNormal(np.log(0.5), 0.5)) rw_tight = priors.LogisticRandomWalk(order=2, scale=priors.HalfNormal(0.3))
A reference population. With
referenceits fraction in each bin,mean=np.log(reference)centres the prior on it.
Rates instead of fractions¶
GammaRates(shape, rate) puts independent Gamma priors on the expected
number of objects per bin, \(\lambda_k\). The data then form a Poisson
process. Every fit of fractions already gives the total and the rates, under a
scale-free prior on the total (Selection and rates). Use GammaRates when the rates need
their own prior, for example bin-specific shapes or an informative total. Combine it
with an exposure or a selection function. Occurrence rates per star, or counts per unit
time or area, are estimated this way.
est = binference.infer(lk, priors.GammaRates(shape=1.0, rate=1e-3), method="map")
print(est.rates.sum().round(1), est.rho.sum())
Sampled results hold both rates, the draws of \(\lambda\), and samples, the fractions
they imply. NUTS and Gibbs both sample rates.
Check a prior¶
Draw from the prior to see which populations it allows before any data:
model = binference.PopulationModel(lk, gp)
draws = model.sample_prior(5, rng=0) # five populations from the prior
print(draws.shape, np.allclose(draws.sum(axis=1), 1.0))
After a fit, two numbers say how much the prior sets the answer. A bin with little
data support takes its fraction from the prior (Choose bins).
result.prior_information_ratio() compares the prior’s information with the data’s.
It is small when the data dominate, and near one or above when the prior does.
fit = binference.infer(lk, rw, method="map")
print(round(fit.prior_information_ratio(), 3))
To choose between priors, fit each and compare the
log predictive density of held-out objects
(Read a result). binference.simulate.prior_predictive() simulates whole
catalogues from the prior, which is what simulation-based calibration needs
(Check a result).