Model Validation#
Four questions about a fitted model, four groups of functions in
prophys.engine.scoring.
Question |
Function |
Needs |
|---|---|---|
How well does it fit, penalized for complexity? |
A point estimate |
|
How well will it predict new data? |
Parameter draws |
|
Is its uncertainty the right size? |
Observations |
|
Does it reproduce the features of the data? |
Observations |
report_card() runs every applicable one in a single
call and is the natural thing to attach to a
ModelPackage.
Information criteria#
ic = prp.information_criteria(compiled, {"y": data}, result=result)
print(ic)
InformationCriteria:
log-likelihood = -552.62 (2 parameters, 300 observations)
AIC = 1109.24
AICc = 1109.28
BIC = 1116.65
The parameter count is the number actually estimated, so trainable and
frozen must match the fit. The observation count excludes records marked
missing, which contribute no likelihood term.
Prefer AICc to AIC whenever \(n/k\) is below about 40; it converges to AIC as \(n\) grows, so it is never the worse choice. BIC penalizes parameters more heavily at any realistic sample size and targets the model that generated the data rather than the one that predicts best.
No prior term is included: an information criterion is defined on the likelihood, and adding the regularizer to the value it is computed from would double-count it. Score a regularized or hierarchical fit with WAIC or PSIS-LOO instead.
Predictive scores#
WAIC and PSIS-LOO estimate the expected log predictive density on new data by integrating over a distribution of parameters, penalizing the effective flexibility the data actually saw rather than the parameter count. Both take parameter draws — from the Laplace approximation, or from a posterior:
u = prp.parameter_uncertainty(compiled, {"y": data}, result=result)
draws = prp.sample_parameters(u, 400)
print(prp.waic(compiled, "y", data, draws))
print(prp.loo(compiled, "y", data, draws))
PSIS-LOO additionally reports a per-observation Pareto shape \(k\) from its importance-weight tail fit. Values above 0.7 mark observations whose leave-one-out estimate is not reliable and should be obtained by actually refitting without them:
score = prp.loo(compiled, "y", data, draws)
print(score.unreliable_points)
Comparing models#
compare() ranks models and pairs each difference with
the standard error of that difference:
ranking = prp.compare({"simple": score_a, "richer": score_b})
print(ranking)
ranking.is_distinguishable("richer")
Pairing matters. Two models scored on the same observations have correlated
errors, so the standard error of their difference is smaller — often much
smaller — than the two scores’ standard errors combined. All scores must
therefore be over the same observations in the same order, which
compare checks.
Calibration#
A model can have an excellent mean and still be useless for risk work by being systematically over- or under-confident. The check is the probability integral transform: if the predictive distribution \(F\) is correct, \(u = F(y)\) for a genuine observation is uniform on \((0, 1)\).
print(prp.calibration(compiled, "y", data, params=result.raw_params))
CalibrationReport:
y: 300 observations, calibrated, unbiased
PIT mean = 0.4976 (0.5 when calibrated), variance = 0.0843 (0.0833)
KS = 0.0351, CvM = 0.0473, AD = 0.3339
reliability index = 0.1200
The report names the failure mode directly:
dispersion—"under-dispersed"(too confident: the truth lands outside the predicted range too often),"over-dispersed"(too vague), or"calibrated", from the PIT variance against its calibrated value of \(1/12\).bias— whether predictions sit systematically above or below the data, from the PIT mean against 0.5.
Three test statistics accompany them. Anderson-Darling weights the tails, where the Kolmogorov-Smirnov and Cramér-von Mises statistics are least sensitive; for a model that exists to answer tail questions it is the one to read. The reliability index is the rank histogram’s total absolute deviation from flat — an effect size with no null distribution attached, so it stays informative at sample sizes where any test rejects.
CRPS#
The continuous ranked probability score is a proper scoring rule for the whole predictive distribution, expressed in the observation’s own units:
It stays finite where the observation lands in a region the model gave zero density (unlike a log-likelihood), rewards sharpness and calibration jointly, and reduces to absolute error for a deterministic forecast — so a probabilistic model and a point prediction can be compared on one scale.
print(prp.crps(compiled, "y", data, params=result.raw_params))
The default fair=True applies the ensemble-size bias correction; without
it, scores computed at different n_ensemble are not comparable, because
the uncorrected estimator measures the spread of the ensemble members rather
than of the distribution behind them.
Posterior predictive checks#
Simulate replicate datasets from the fitted model and compare any summary statistic of the real data against the replicate distribution:
check = prp.posterior_predictive_check(
compiled, "y", data, statistic="skew", params=result.raw_params
)
print(check.p_value, check.reproduces_the_data)
The p-value is two-sided and small when the model fails to reproduce the statistic. A large value is not evidence the model is right — only that this statistic does not distinguish it from the data.
Choose a statistic the likelihood does not fit directly. Checking the mean
of a model whose mean parameter is free will pass by construction and says
nothing; checking the skew of a Gaussian fitted to lognormal data fails
immediately, which is the point. Built-in names are in
TEST_STATISTICS; any callable on a 1-D array
works too.
Passing param_draws makes each replicate use a different parameter draw,
so the check propagates parameter uncertainty as well as predictive noise.
Report card#
card = prp.report_card(
compiled, "y", data, result=result, param_draws=draws
)
for name, report in card.items():
print(name)
print(report)
The dict it returns is serializable and is what
export_model() records under diagnostics, so the
diagnostics travel with the exported distributions.
- prophys.engine.information_criteria(compiled, observations, *, result=None, raw_params=None, weights=None, trainable=None, frozen=None)[source]#
Compute AIC, AICc and BIC for a fit of a compiled model to observations.
The parameter count is the number of scalar values actually estimated, so trainable and frozen must match the fit. The observation count excludes records marked missing, which contribute no likelihood term and so should not inflate a sample size penalty.
No prior term is included. An information criterion is defined on the likelihood, and adding a penalty to the value it is computed from would count the regularization twice. Use
waic()orloo()to score a regularized or hierarchical fit.- Parameters:
compiled – The fitted model.
observations (Mapping[str, Any]) – Attribute name to observations, as passed to the fit.
result – The fit’s
CalibrationResult.raw_params (Mapping[str, Any] | None) – The fitted unconstrained parameters, used instead of result when given.
weights (Mapping[str, float] | None) – Likelihood weight per attribute, as used in the fit.
- Returns:
An
InformationCriteriareport.- Raises:
ValueError – If neither result nor raw_params is given.
- Return type:
- class prophys.engine.InformationCriteria(log_likelihood, n_parameters, n_observations, aic, aicc, bic)[source]#
Information criteria (AIC, AICc and BIC) of a fitted model, from
information_criteria().- Parameters:
- prophys.engine.waic(compiled, attribute_name, observations, param_draws)[source]#
Score predictive accuracy with the Widely Applicable Information Criterion (WAIC).
Estimates the expected log predictive density from the pointwise log likelihood across parameter draws, penalized by its variance. Cheaper than
loo()and usually close to it. Where the two disagree materially, the Pareto diagnostic ofloo()says which observations are responsible.- Parameters:
- Returns:
A
PredictiveScorewithcriterion="waic".- Return type:
- prophys.engine.loo(compiled, attribute_name, observations, param_draws)[source]#
Estimate leave one out predictive accuracy by Pareto smoothed importance sampling (PSIS LOO).
Reports the expected log predictive density like
waic(), plus a Pareto shape per observation that flags where the estimate itself is unreliable (seePredictiveScore.unreliable_points).- Parameters:
- Returns:
A
PredictiveScorewithcriterion="loo"and pareto_k.- Return type:
- class prophys.engine.PredictiveScore(criterion, elpd, elpd_se, p_eff, deviance, n_observations, pointwise, pareto_k=None)[source]#
Expected log predictive density estimated by WAIC or PSIS LOO, from
waic()orloo().- Parameters:
- pointwise: list[float]#
Elpd contribution of each observation, from which
compare()computes differences.
- prophys.engine.compare(scores)[source]#
Rank models by predictive score, with the standard error of each difference.
All scores must be over the same observations in the same order. The contributions of each observation are differenced pairwise, which is what makes the standard error of the difference meaningful. Scores computed on different data would give a number with no interpretation, so the observation counts are checked (only the counts, not the values).
- Parameters:
scores (Mapping[str, PredictiveScore]) – Model name to its
PredictiveScore. At least two, all from the same criterion.- Returns:
A
ModelComparison, best model first.- Raises:
ValueError – If fewer than two scores are given, or they differ in criterion or observation count.
- Return type:
Example
>>> ranking = compare({"linear": loo(m1, "y", obs, d1), "quadratic": loo(m2, "y", obs, d2)}) >>> print(ranking)
- class prophys.engine.ModelComparison(criterion, names, elpd, delta, delta_se)[source]#
Ranking of models by a predictive score, with each model’s difference from the best.
Returned by
compare().- Parameters:
- delta: dict[str, float]#
Difference from the best model’s elpd (0 for the best, negative for the rest).
- delta_se: dict[str, float]#
Standard error of that difference, from the paired contributions of each observation. Pairing matters. Two models scored on the same observations have correlated errors, so the standard error of the difference is smaller, often much smaller, than the standard errors of the two scores combined.
- prophys.engine.calibration(compiled, attribute_name, observations, *, params=None, n_bins=10, n_ensemble=4096, key=None, seed=0)[source]#
Test whether the predictive distribution is calibrated against observations (PIT).
Computes the probability integral transform (PIT) of every exactly observed value and tests it for uniformity. Where the attribute’s distribution has a CDF, the PIT is computed exactly from it. Otherwise it falls back to ensemble ranks against n_ensemble predictive draws, with ties broken by uniform random draws so a discrete predictive distribution does not register as miscalibrated. Censored and missing records are skipped.
- Parameters:
compiled – The model.
attribute_name (str) – The observed attribute.
observations (Any) – Its observations.
params (Mapping[str, Any] | None) – Unconstrained parameter values, such as a fit’s raw_params.
n_bins (int) – Bins of the rank histogram.
n_ensemble (int) – Predictive draws for the rank fallback.
key (Any) – PRNG key. Defaults to
PRNGKey(seed).seed (int) – Seed used when key is not given.
- Returns:
- Raises:
ValueError – If fewer than two values are exactly observed.
- Return type:
- class prophys.engine.CalibrationReport(attribute, n_observations, ks, cvm, ad, pit_mean, pit_variance, histogram, reliability_index)[source]#
Probability integral transform (PIT) calibration of a predictive distribution.
Returned by
calibration().dispersionandbiasgive a plain language verdict and the test statistics quantify it.- Parameters:
- property dispersion: str#
"calibrated","under-dispersed"(too confident) or"over-dispersed"(too vague), from the PIT variance against its calibrated value of 1/12.The 10% band is a reporting convention, not a test. It keeps a well calibrated model from being labelled by ordinary sampling noise. Read ks and ad for the formal statistics.
- prophys.engine.crps(compiled, attribute_name, observations, *, params=None, n_ensemble=4096, fair=True, key=None, seed=0)[source]#
Score the predictive distribution with the continuous ranked probability score (CRPS).
Scores the whole predictive distribution against each exactly observed value, in the observation’s units. Lower is better. The predictive distribution is represented by n_ensemble draws, and every observation is scored against the same draws. Censored and missing records are skipped.
- Parameters:
compiled – The model.
attribute_name (str) – The observed attribute.
observations (Any) – Its observations.
params (Mapping[str, Any] | None) – Unconstrained parameter values, such as a fit’s raw_params.
n_ensemble (int) – Number of predictive draws.
fair (bool) – Apply the bias correction for ensemble size (default). Without it, scores computed at different n_ensemble are not comparable.
key (Any) – PRNG key. Defaults to
PRNGKey(seed).seed (int) – Seed used when key is not given.
- Returns:
A
CrpsScore.- Raises:
ValueError – If no value is exactly observed.
- Return type:
- class prophys.engine.CrpsScore(attribute, mean, std_error, n_observations, n_ensemble, pointwise)[source]#
Continuous ranked probability score (CRPS) of a predictive distribution, from
crps().- Parameters:
- prophys.engine.posterior_predictive_check(compiled, attribute_name, observations, *, statistic='std', params=None, param_draws=None, n_replicates=500, key=None, seed=0)[source]#
Compare a summary statistic of the data against replicate datasets simulated from the model.
Each replicate is a dataset of the same size as observations. When param_draws is supplied, the replicates cycle through the parameter draws, so the check propagates parameter uncertainty as well as predictive noise. With only params, replicates vary by predictive noise alone and the check is correspondingly stricter.
statistic is a name from
TEST_STATISTICSor any callable on a one dimensional array. Choose one the likelihood does not fit directly. A check on the mean of a model fitted by maximizing a likelihood whose mean parameter is free passes by construction and says nothing.The returned p value is two sided and small when the model fails to reproduce the statistic (see
PosteriorPredictiveCheck.p_value).- Parameters:
compiled – The model.
attribute_name (str) – The observed attribute.
observations (Any) – Its observations. Only exactly observed values are used.
statistic (str | Callable[[ndarray], float]) –
"mean","std","min","max","median","skew","range", or a callable.params (Mapping[str, Any] | None) – Unconstrained parameter values, used only when param_draws is not given.
param_draws (Mapping[str, Any] | None) – Constrained parameter draws (as for
waic()), cycled over the replicates.n_replicates (int) – Number of replicate datasets.
key (Any) – PRNG key. Defaults to
PRNGKey(seed).seed (int) – Seed used when key is not given.
- Returns:
- Raises:
ValueError – If fewer than two values are exactly observed.
KeyError – If statistic is an unknown name.
- Return type:
- class prophys.engine.PosteriorPredictiveCheck(attribute, statistic, observed, replicate_mean, replicate_std, p_value, n_replicates)[source]#
Posterior predictive check of one summary statistic, from
posterior_predictive_check().- Parameters:
- p_value: float#
twice the smaller tail probability of the observed statistic under the replicate distribution.
Small values mean the model does not reproduce this feature of the data. 1.0 means the observed value sits at the replicate median. A large value is not evidence that the model is right, only that this particular statistic does not distinguish it from the data.
- Type:
Two sided posterior predictive p value
- prophys.engine.report_card(compiled, attribute_name, observations, *, result=None, raw_params=None, param_draws=None, statistics=('std', 'max'), n_ensemble=4096, seed=0)[source]#
Run every applicable goodness of fit diagnostic for one fitted attribute.
Intended as the default thing to run after a fit and to attach to a ModelPackage (pass it as diagnostics to
export_model()). The individual functions give finer control. The information criteria count every model parameter as estimated, so callinformation_criteria()directly for a fit that froze some.- Parameters:
compiled – The fitted model.
attribute_name (str) – The observed attribute.
observations (Any) – Its observations.
result – The fit’s
CalibrationResult.raw_params (Mapping[str, Any] | None) – The fitted unconstrained parameters, used instead of result when given.
param_draws (Mapping[str, Any] | None) – Optional constrained parameter draws. They enable WAIC and PSIS LOO and make the predictive checks integrate over them.
statistics (Sequence[str]) – Statistics for the posterior predictive checks.
n_ensemble (int) – Predictive draws for calibration and CRPS.
seed (int) – PRNG seed for every Monte Carlo step.
- Returns:
"information_criteria"(when fitted parameters are given),"calibration","crps", one"ppc_<statistic>"per entry of statistics, and"waic"and"loo"when param_draws is given.- Return type:
Report name to report
- prophys.engine.pointwise_log_likelihood(compiled, attribute_name, observations, param_draws)[source]#
Log likelihood of every observation under every parameter draw.
This is the
[n_draws, n_obs]matrix thatwaic()andloo()are computed from.- Parameters:
compiled – The model.
attribute_name (str) – The observed attribute.
observations (Any) – Its observations (an array or Observations record).
param_draws (Mapping[str, Any]) – Parameter name to an array of n_draws constrained scalar values, as returned by
sample_parameters()or found inPosterior.draws. Draws outside a bounded parameter’s range are clamped just inside it, and draws of a softplus parameter are clamped to at least1e-6.
- Returns:
NumPy array of shape
(n_draws, n_obs).- Raises:
ValueError – If param_draws is empty or its arrays differ in length.
- Return type:
Kernel correctness#
The native kernels themselves are validated independently of any model:
Against independent references, for values and gradients: scipy, OpenTURNS, shapely, scikit-learn, networkx, arviz, SALib, scoringrules and the
rainflowpackage, closed forms evaluated in mpmath at 40–50 significant digits, complex-step differentiation of complex-capable references, and automatic differentiation of independently written JAX formulas.CPU against GPU, bit for bit. Every GPU kernel runs on an emulated device (several threads, in a shuffled order) and must reproduce the CPU kernel’s output exactly, at both precisions, across batch and broadcast layouts; a parity suite repeats the comparison on CUDA hardware.