← All methods

Method

Hierarchical priors: model card

What the population priors are, how they are fitted and how far to trust them. Every gate, threshold and sampler setting on this page is read from the trainers; every number about the fit is read from the served artifact.

Constants read from libs/shared/ml/src/priors/priors-constants.generated.ts.

Which version is served

Served: v2-sota-2026-05-15, generated 2026-10-26, run as pymc-nuts-2chain-1000draws-500tune-sampler=nutpie-likelihood=student_t.

versionstatusread by
v2servedThe priors block of /api/ml/predict, the MCP server and the chat assistant; the knowledge-graph drawer; the lab trust badges; the Korth collaboration page.
v1fitted, still readParameter detail pages, the population-priors card on paper pages, the lab wastewater overlay and the resource-recovery demo. Normal likelihood; converged means R-hat < 1.05 and ESS > 200.
v3, v3.1not servedv3 and v3.1 were experiments (adaptive partial pooling) that fitted worse than v2. The trainers remain for the record; no page reads their output.
v0archivedMoment matching on log-distributed values, which is wrong for them. Archived; nothing reads it.

What is fitted

One fit per canonical parameter slug, then one per stratum. Every stratum of a slug uses the scale chosen on that slug’s pooled values.

stratumgrouped byminimum papers
pooledevery row of the slug5
by system typeprimarySystemType, else systemType8
by application domaineach entry of application_domains (a row counts once per domain)8; 5 for resource_recovery, wastewater_treatment
by system type and domainsystem|domain5

A fit is skipped with fewer than 5 observations, or fewer usable observations than papers. A slug whose rows carry more than one SI unit keeps only its dominant unit, and the dropped units are written into the fit note. Temporal and regional strata are stubs and stay empty.

Scale and link

The fit runs on log₁₀(x) when all three conditions hold on the pooled values, and on x itself (identity link) otherwise:

  • at least 0.95 of the finite values are positive;
  • among the positive values, p95 / p5 exceeds 100;
  • the slug is not fraction-like: not (median in (0.05, 1], maximum ≤ 1.5 and minimum ≥ -0.05).

On the log scale, values ≤ 0 are dropped before fitting, so positive quantities that span orders of magnitude (power density, resistance) get a log-normal-type model. Bounded quantities (coulombic efficiency, COD removal, pH) are fitted on the raw value. That model is not constrained to the bounds, so its tails can extend past 0 or 1.

Likelihood, hierarchy and hyperpriors (v2)

y is log₁₀(x) or x per the scale above; i indexes observations, p papers. The likelihood served is student_t (environment variable PRIORS_LIKELIHOOD; default student_t).

Observation
y_pi ~ StudentT(ν, θ_p, σ_w)
Paper level, non-centred
θ_p = μ + τ·z_p, z_p ~ Normal(0, 1)
Population location
μ ~ Normal(m₀, s_μ)
Between-paper spread
τ ~ HalfNormal(s_τ)
Within-paper spread
σ_w ~ HalfNormal(s_σ)
Student-t degrees of freedom
ν ~ Gamma(α = 2, β = 0.1) · mean α/β = 20 (PyMC shape-rate form)

Hyperprior scales

casem₀s_μs_τs_σ
log₁₀ scale, no literature entry0522
raw scale, no literature entrymean(y)5·SD(y)2·SD(y)2·SD(y)
entry in literature-priors.jsonits log10_meanits log10_sdits log10_half_widthas without an entry

The raw-scale scales are floored at 10⁻⁶. A literature entry that lacks a field falls back to the trainer default for that field. Mixture, temporal, regional and joint models exist only as stubs and do not produce the served fit.

Data pool

Rows come from ExtractedParameterData through the one modelling-pool predicate (libs/data-access/database/src/modelable-pool.ts, Python twin services/ml-engine/training/modelable_pool.py). A row enters the fit only if all of these hold:

  • the normaliser judged it modelable (applicable_to_modeling), and it has a canonical slug and a numeric SI value;
  • the physical-bounds gate passed it (boundsPassed is true), and it carries no bounds:, dedupe:, physics: or contradiction: flag;
  • the snippet verifier did not fail it (verifierPassed is not false; unscored rows pass);
  • it is not archived and no reviewer rejected it;
  • its paper is visible, not archived or retracted, not a review or meta-analysis, not marked not_BES, and is a paper rather than a patent;
  • its paper is on the fit side of the paper-disjoint split: fit_validation_role is fit or unset. Validation and holdout papers never reach the fit; validation papers are used only to score coverage.

Sampling and convergence gates

NUTS via nutpie (fallback order nutpie → numpyro → pymc), 2 chains × 1000 draws after 500 tuning steps, target_accept 0.95, seed 42. PRIORS_DRAWS, PRIORS_TUNE, PRIORS_CHAINS override draws, tuning and chains; the served artifact records what it ran: pymc-nuts-2chain-1000draws-500tune-sampler=nutpie-likelihood=student_t.

A fit is marked converged only if R-hat_max < 1.01 and ESS_bulk_min > 400 and there are 0 divergences, with R-hat and ESS taken over mu, tau, sigma_within (not the paper-level offsets). The test uses full-precision diagnostics. The artifact serves R-hat at 4 decimals, always moved up, and ESS as a whole draw count, always moved down, so a gate applied to the served numbers can never pass a fit the trainer rejected. v1 used R-hat < 1.05 and ESS > 200.

In the served artifact, 11 of 175 pooled fits and 60 of 760 strata pass all three gates. A fit that fails is still written to the artifact with converged = false; the serving gate below decides whether its interval is shown.

LOO-PSIS is computed per fit; an observation with Pareto k > 0.7 is high-influence, and LOO is treated as unreliable when more than 0.05 of observations exceed it.

What the served interval is

ci95_low and ci95_high are the 0.95 highest-density interval of the posterior of mu, the population location, back-transformed with 10ˣ on the log scale. They say where the population centre sits. They are not a predictive interval: they leave out τ and σ_w, so most new papers report values outside them.

mu is the back-transformed posterior mean of the location. On the log scale that is 10^E[μ], a geometric centre, not the arithmetic mean of x.

Coverage is checked on a different band: the predictive interval μ̂ ± z·√(τ̂² + σ̂_w²) on the fit scale, back-transformed, with z = 1.959963984540054 and posterior means for μ, τ and σ_w. It is a Normal approximation. It ignores posterior uncertainty in μ, τ and σ_w and the heavier Student-t tails, so when ν is small the true predictive tails are wider than this band.

Before an interval leaves /api/ml/predict, the serving gate (libs/shared/ml/src/priors-serving-gate.ts) withholds it when the fit is degenerate or fails diagnostics, or when it under-covers on held-out papers. It then serves the raw-corpus 10th to 90th percentile range, or no interval when that range is not available.

On the current artifacts: 6 served, 169 withheld (0 replaced by the empirical range, 169 with no interval).

How coverage is validated

validate_priors.py scores 81 held-out papers (fit_validation_role = validation) drawn through the same pool predicate. Each held-out value is scored against the system-type stratum when that stratum has at least 8 papers, else against the pooled fit, at nominal coverage 95.0%.

Overall: 93.6% of 2780 held-out values fall inside the band (nominal 95.0%); 77 fall below and 101 above. Verdict: calibrated.

This validation scored the served artifact (same generation stamp).

Lowest-coverage strata (n ≥ 5)

parametersystemncoveragebelowaboveverdict
conversion_efficiencyOTHER728.6%50under_covering_bias:prior_too_high
tafel_slopeMFC1145.5%06under_covering_bias:prior_too_low
cod_removalMERC850.0%40under_covering_bias:prior_too_high
phosphorus_removalMEC850.0%40under_covering_bias:prior_too_high
total_dissolved_solidsMFC955.6%40under_covering_bias:prior_too_high
biofilm_thicknessMFC560.0%11under_covering_spread
relative_abundanceMBES560.0%02under_covering_bias:prior_too_low
currentMFC1163.6%04under_covering_bias:prior_too_low

Verdicts: calibrated at coverage ≥ 90.0%; over_covering at ≥ 99.0%. Below 90.0%, the miss pattern decides: bias when at least 85.0% of misses fall on one side, spread when at most 65.0% do. A one-sided miss means the band is centred in the wrong place, and widening it cannot fix that.

Worked row: power_density_areal

The pooled v2 fit for power_density_areal: 239 papers, 741 observations, unit W/m², log scale. Stored values as served:

fieldstored value
scalelog
mu_log-1.1570828075387771
tau_log1.1361776046009193
sigma_within_log0.1845801573292763
mu0.0696493700017325
ci95_low0.05092907018983528
ci95_high0.09894634071028374
likelihood_familystudent_t
nu_mean1.2850516136106038
rhat_max1.0448
ess_bulk_min51
n_divergences0
convergedfalse
prior_sourceweakly_informative

How the numbers follow from the stored ones

Point
mu = 10^mu_log = 10^(-1.15708) = 0.06965 W/m²
Served band (HDI of μ)
10^HDI 0.95 (mu) = [0.05093, 0.09895] W/m²
Predictive SD, fit scale
√(τ² + σ_w²) = √(1.136² + 0.1846²) = 1.151
Predictive band the validator scores
10^(-1.157 ± 1.95996 × 1.151) = [0.000386, 12.6] W/m²
Width ratio
the predictive band is 15.6× wider than the served band on the fit scale

Gates: fails at least one, so converged = false.

Serving gate at /api/ml/predict: withheld, basis suppressed.

Reasons given:

  • sampler did not converge (converged=false, rhat_max=1.0448, ess_bulk_min=51)
  • posterior CI withheld; no empirical range available

services/ml-engine/training/hierarchical_priors_v1.py · services/ml-engine/training/hierarchical_priors_v2.py · services/ml-engine/training/validate_priors.py · services/ml-engine/training/mcmc_diagnostics.py