Uncertainty & Error#
Every number a model reports carries uncertainty from two distinct sources, and prophys reports them separately because they are reduced by different things:
Source |
Reduced by |
Reported by |
|---|---|---|
Monte-Carlo error — the statistic was estimated from a finite draw |
More samples |
|
Parameter uncertainty — the parameters were estimated from finite data |
More data |
A model can have negligible Monte-Carlo error and large parameter uncertainty, or the reverse. Adding samples never reduces the second; adding data never reduces the first.
Monte-Carlo error#
CompiledModel.assess returns
the estimate together with the sampling error of the same draw:
import prophys as prp
compiled = model.compile(n_samples=20_000)
compiled.assess("loss") # the mean
compiled.assess("loss", "prob", level=2.0) # P(loss <= 2)
compiled.assess("loss", "quantile", level=0.99) # the 99th percentile
compiled.assess("loss", "cvar", level=0.95) # tail risk
Each returns an EstimateWithError (or a
TailRiskEstimate for CVaR):
EstimateWithError:
quantile[loss] at 0.99 = 10.3263 +/- 0.7063
95% interval = [8.94206, 11.7106]
relative error = 6.840%
n_samples = 20000, ess = 20000.0 (iact 1.00)
Three estimators sit behind these, one per shape of statistic:
A mean (and an exceedance probability, which is the mean of an indicator) uses non-overlapping batch means plus an initial-positive-sequence autocorrelation time, so correlated draws — a
Processpath, an MCMC chain — report a larger error and a lower effective sample size than independent ones.A quantile uses the Maritz-Jarrett order-statistic estimator, which needs no density estimate at the quantile. The classical \(\sqrt{q(1-q)/n}/f(x_q)\) form requires \(f\) precisely where the sample is thinnest.
A CVaR uses the influence function of the Rockafellar-Uryasev functional, which includes the uncertainty of the VaR threshold. The standard error of the mean of the tail draws omits that term and is correspondingly optimistic.
Sample-size planning#
required_samples() converts an achieved error into the
draws a target precision would take, on the \(1/\sqrt{n}\) scaling:
estimate = compiled.assess("loss", "cvar", level=0.99)
prp.required_samples(estimate, target_relative_error=0.01)
Parameter uncertainty#
After a fit, parameter_uncertainty() inverts the
observed information — the Hessian of the same negative log-likelihood the
fit minimized — at the optimum:
result = prp.calibrate(compiled, "y", data, n_steps=3000)
u = prp.parameter_uncertainty(compiled, {"y": data}, result=result)
print(u)
ParameterUncertainty:
fitted on 500 observations, log-likelihood -1054.32
mu = 2.92676 +/- 0.09951 [2.73172, 3.1218]
sigma = 1.99021 +/- 0.07036 [1.8523, 2.12811]
Standard errors are reported in constrained space — the values the model graph uses and the reader interprets — obtained from the unconstrained-space Hessian by the delta method, so a positive scale parameter’s interval never straddles zero.
The Hessian is central-differenced from the exact analytic gradient. The native kernels provide exact first derivatives but no second derivatives of their own, so a model graph is differentiable exactly once; differencing an exact gradient leaves a relative accuracy of about \(\varepsilon^{2/3}\), far below the sampling uncertainty being reported.
Unidentified parameters#
At a genuine minimum the Hessian is positive definite. When it is not,
parameter_uncertainty refuses rather than returning numbers:
ValueError: The Hessian at the reported optimum is not positive definite,
so no covariance exists there. The optimizer has not reached a minimum, or
some parameter (or combination) is not identified by the data.
For a model that is identified but only weakly,
collinear_pairs() lists the
parameter pairs the data constrain only in combination:
for a, b, rho in u.collinear_pairs():
print(f"{a} and {b} are correlated at {rho:+.3f}")
Propagating into a derived quantity#
Parameter uncertainty rarely matters on its own; what matters is its effect
on the quantity being reported. propagate() carries the
covariance through the Jacobian of any function of the parameters:
design_value = prp.propagate(u, lambda p: p["mu"] + 1.645 * p["sigma"])
print(design_value.interval(0.95))
This is a first-order propagation: exact for a linear function, and an
approximation whose quality falls with the curvature of the function over
the parameter uncertainty. For a strongly non-linear derived quantity,
sample the parameters with
sample_parameters() and push the draws through
instead.
Profile likelihood#
The Laplace interval assumes the log-likelihood is quadratic near its
maximum. profile_likelihood() makes no such assumption:
it fixes one parameter across a grid, re-optimizes every other parameter at
each point, and reads the interval off the likelihood ratio.
import numpy as np
profile = prp.profile_likelihood(
compiled, {"y": data}, "sigma", np.linspace(1.5, 2.5, 21), result=result
)
print(profile.interval)
For a location parameter, where the likelihood is exactly quadratic, the two intervals coincide; for a bounded or skewed parameter the profile interval is asymmetric and the Laplace one is not. It costs one optimization per grid point.
Beyond the Gaussian approximation#
Both routes above summarize the posterior by a Gaussian around its mode. For the posterior itself — skewed, bounded, or multi-modal as it may be — see Posterior Inference.
- prophys.engine.assess(compiled, attribute_name, statistic='mean', *, level=None, n_samples=None, params=None, key=None, n_batches=0)[source]#
Estimate a statistic of a model attribute together with its Monte Carlo error.
The statistic is estimated from a single sample of the attribute, so the Monte Carlo error reported is that of the same draw the estimate came from. Use
required_samples()on the result to size a run for a target precision.- Parameters:
compiled – A
CompiledModel.attribute_name (str) – Which attribute to sample. It must be scalar valued. For a path valued attribute, sample it and pass one step’s draws to
mean_error(),quantile_error()orcvar_error().statistic (str) –
"mean","prob","quantile"or"cvar"."prob"needs level as the threshold and estimatesP(attribute <= level)as the mean of an indicator."quantile"and"cvar"need level as the probability level.level (float | None) – Threshold or probability level, as above.
n_samples (int | None) – Draws to use. Defaults to the model’s compiled n_samples.
params (Mapping[str, Any] | None) – Unconstrained parameter values by name, such as a fit’s raw_params.
key (Any) – PRNG key. Defaults to the model’s compiled seed, which makes the assessment reproducible but ties repeated calls to one sample. Pass distinct keys to see the variation between runs.
n_batches (int) – Batch count for the mean and probability error (see
mean_error()).
- Returns:
An
EstimateWithError, or aTailRiskEstimatefor"cvar".- Raises:
ValueError – If statistic is unknown, level is missing where required, or the attribute is not scalar valued.
- Return type:
Example
>>> est = assess(compiled, "load", "quantile", level=0.99, n_samples=20000) >>> est.value, est.interval() >>> required_samples(est, target_relative_error=0.01)
- class prophys.engine.EstimateWithError(statistic, value, std_error, n_samples, effective_sample_size, autocorrelation_time, attribute=None, level=None)[source]#
A Monte Carlo estimate of a mean, probability or quantile with its sampling error.
Returned by
assess(),mean_error()andquantile_error().str()prints the estimate, its 95% interval, relative error, sample size, effective sample size and autocorrelation time.- Parameters:
- statistic: str#
Which statistic this is, one of
STATISTICSor the name given tomean_error().
- effective_sample_size: float#
Draws’ worth of independent information,
n / iact. Equal to n_samples for independent draws, lower for a correlated series. Always n_samples for a quantile, whose draws are taken as independent.
- autocorrelation_time: float#
Integrated autocorrelation time (Geyer’s initial positive sequence), 1.0 for independent draws and always 1.0 for a quantile.
- level: float | None = None#
The q of a quantile estimate or the threshold of a prob estimate.
Nonefor a mean.
- property relative_error: float#
Standard error as a fraction of the estimate. nan for an estimate that is zero or not finite, where no relative statement is meaningful.
- class prophys.engine.TailRiskEstimate(alpha, var, cvar, std_error, n_tail, n_samples, attribute=None)[source]#
Value at Risk and Conditional Value at Risk estimates with the CVaR’s standard error.
Returned by
assess()withstatistic="cvar"and bycvar_error().- Parameters:
- cvar: float#
Conditional Value at Risk,
var + mean((x - var)^+) / (1 - alpha), about the mean of the draws at or above the VaR.
- std_error: float#
Influence function standard error of cvar, which includes the uncertainty of the VaR threshold itself.
- property relative_error: float#
Standard error as a fraction of the CVaR. nan for a CVaR that is zero or not finite.
- prophys.engine.mean_error(draws, *, n_batches=0, name='mean')[source]#
Sample mean of draws with its batch means standard error.
Batch means make the error valid for correlated draws (an MCMC chain, a time series) as well as independent ones. Correlation up to a block length raises the reported error, and the effective sample size comes from the integrated autocorrelation time. Non finite draws are dropped, and fewer than four finite draws fall back to
sd / sqrt(n).- Parameters:
- Returns:
- Raises:
ValueError – If draws has more than one axis.
RuntimeError – A jax.errors.JaxRuntimeError from the native kernel if no draw is finite.
- Return type:
- prophys.engine.quantile_error(draws, q)[source]#
Sample q quantile of draws with its Maritz and Jarrett standard error.
The error is an order statistic estimate, so it stays defined in the far tail where a density plug in estimate would need the density precisely where the sample is thinnest. The draws are treated as independent and non finite draws are dropped. The quantile interpolates linearly, as numpy does.
- Parameters:
- Returns:
An
EstimateWithErrorwithlevel=q.- Raises:
ValueError – If draws has more than one axis.
RuntimeError – A jax.errors.JaxRuntimeError from the native kernel if q is outside
[0, 1]or fewer than two draws are finite.
- Return type:
- prophys.engine.cvar_error(draws, alpha)[source]#
Upper tail CVaR of draws at level alpha, with its influence function error.
The CVaR takes the Rockafellar and Uryasev form
VaR + mean((x - VaR)^+) / (1 - alpha). Non finite draws are dropped.- Parameters:
- Returns:
A
TailRiskEstimatewith the VaR, the CVaR and its standard error.- Raises:
ValueError – If draws has more than one axis.
RuntimeError – A jax.errors.JaxRuntimeError from the native kernel if alpha is outside
[0, 1)or fewer than two draws are finite.
- Return type:
- prophys.engine.required_samples(estimate, target_relative_error)[source]#
Number of draws needed to reach a target relative error, extrapolated from an estimate.
Monte Carlo error falls as
1/sqrt(n), so the requirement scales as the square of the ratio of achieved to target error. The result assumes the estimator’s variance does not itself change with n, which holds for a mean and is approximate for a tail quantile, where the far tail is better resolved at larger n than the extrapolation credits.- Parameters:
estimate (EstimateWithError | TailRiskEstimate) – An achieved
EstimateWithErrororTailRiskEstimate.target_relative_error (float) – Desired standard error as a fraction of the estimate, such as
0.01.
- Returns:
The required sample size, which is below n_samples when the target is already met. The estimate’s own n_samples when its relative error is undefined or zero.
- Raises:
ValueError – If target_relative_error is not positive.
- Return type:
- prophys.engine.parameter_uncertainty(compiled, observations, *, result=None, raw_params=None, weights=None, prior_neg_log_prob=None, trainable=None, frozen=None)[source]#
Compute standard errors and correlations of a fit’s parameters (Laplace approximation).
The Hessian of the negative log likelihood the fit minimized is taken at the fitted point in unconstrained space. Its inverse, the covariance, is mapped into constrained space by the delta method (see
laplace_covariance()). The weights, prior_neg_log_prob, trainable and frozen arguments must match the ones the fit used, since they define the objective whose curvature is measured.Parameters excluded by trainable or frozen are held fixed at their fitted values. They were not estimated, so they carry no uncertainty and do not enter the Hessian.
- 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) – Alternatively, the fitted unconstrained parameters. Takes precedence over result.
prior_neg_log_prob (Callable[[Mapping[str, Any]], Any] | None) – As used in the fit.
- Returns:
A
ParameterUncertaintyreport. Array valued parameters contribute one entry per element, named"name[i,j]".- Raises:
ValueError – If neither result nor raw_params is given, no parameter is selected, or the Hessian is not positive definite (the fit has not reached a minimum, or a parameter is not identified by the data).
- Return type:
Example
>>> fit = finetune(compiled, {"conc": measured}) >>> unc = parameter_uncertainty(compiled, {"conc": measured}, result=fit) >>> unc.interval("emission_rate"), unc.collinear_pairs()
- class prophys.engine.ParameterUncertainty(names, estimates, std_errors, covariance, correlation, n_observations, log_likelihood)[source]#
Standard errors, covariance and correlations of fitted parameters (Laplace approximation).
Returned by
parameter_uncertainty()andlaplace_covariance(). All quantities are in constrained space, the values the model graph uses and the user reads, obtained from the Hessian in unconstrained space by the delta method.- Parameters:
- names: list[str]#
Flattened parameter names, in the order the matrices index them. An array valued Param contributes one entry per element, named
"route[2,0]".
- n_observations: int#
Informative observations the fit used, for reference when comparing fits. Whatever the caller passed to
laplace_covariance().
- log_likelihood: float#
the negated loss, prior term included when one was supplied.
- Type:
Log likelihood at the optimum
- interval(name, level=0.95)[source]#
Normal theory confidence interval
(lower, upper)for one parameter.
- correlation_between(a, b)[source]#
Correlation coefficient between the estimates of parameters a and b.
- Raises:
ValueError – If a or b is not one of names.
- Parameters:
- Return type:
- collinear_pairs(threshold=0.95)[source]#
Parameter pairs whose absolute correlation is at least threshold.
Returns
(a, b, rho)tuples, strongest first. Any result means the data determine a combination of those parameters much better than either alone. The individual standard errors remain correct, but reading them as independent uncertainties overstates what the fit resolved.
- prophys.engine.laplace_covariance(loss_fn, raw_params, param_specs, *, n_observations=0)[source]#
Laplace (observed information) covariance of parameters at the minimum of a loss.
The lower level form of
parameter_uncertainty(), for a loss built by hand. loss_fn is a negative log likelihood over unconstrained parameter values, asnegative_log_likelihood()builds, and raw_params must be at its minimum. The Hessian there (fromobserved_information()) is the observed Fisher information. Its inverse is the parameter covariance, which is then mapped into constrained space by the delta method.A Hessian that is not positive definite is a statement about the fit. At a genuine minimum it is positive definite, so a failed factorization means the optimizer stopped elsewhere (a saddle, a flat direction or an unconverged run), and any covariance derived there would be meaningless.
- Parameters:
loss_fn (Callable[[Mapping[str, Any]], Any]) – Negative log likelihood of a dict of unconstrained values.
raw_params (Mapping[str, Any]) – The minimizing unconstrained values, by name. Every entry is treated as estimated.
param_specs (Mapping[str, Any]) – The matching Param objects, by name, such as
compiled.param_specs.n_observations (int) – Observation count recorded in the result.
- Returns:
A
ParameterUncertaintyin constrained space.- Raises:
ValueError – If raw_params is empty, or the Hessian has non finite entries or is not positive definite.
- Return type:
- prophys.engine.propagate(uncertainty, fn)[source]#
Propagate parameter uncertainty into a derived quantity by the delta method.
fn is evaluated at the fitted parameters, and its Jacobian there carries the parameter covariance into the derived quantity:
Cov(g) = J Cov(theta) J^T.This is a first order propagation. It is exact for a linear fn and an approximation whose quality falls with the curvature of fn over the parameter uncertainty. For a strongly nonlinear derived quantity, sample the parameters from the covariance (
sample_parameters()) and push the samples through fn instead.- Parameters:
uncertainty (ParameterUncertainty) – A
ParameterUncertaintyfromparameter_uncertainty().fn (Callable[[Mapping[str, float]], Any]) – Differentiable function taking a mapping of (flattened) parameter name to constrained value and returning a scalar or 1D array. It may evaluate the model.
- Returns:
A
DerivedUncertaintywith the value, standard error and covariance of every component.- Return type:
Example
>>> derived = propagate(unc, lambda p: p["a"] * p["b"]) >>> derived.values[0], derived.std_errors[0], derived.interval()
- class prophys.engine.DerivedUncertainty(values, std_errors, covariance)[source]#
Delta method standard errors of a quantity derived from the fitted parameters.
Returned by
propagate(). A scalar quantity is stored as a single component, index 0.
- prophys.engine.profile_likelihood(compiled, observations, name, grid, *, result=None, raw_params=None, weights=None, prior_neg_log_prob=None, level=0.95, n_steps=200, learning_rate=0.01)[source]#
Profile the log likelihood in one parameter and read off its likelihood ratio interval.
At each grid value the named parameter is frozen at that constrained value and
finetune()optimizes every other parameter again, starting from the fitted point. The resulting curve is the profile log likelihood. The confidence interval is the range where the profile stays within half the chi square (1 degree of freedom) critical value of the log likelihood at the fit.Unlike the Laplace interval this makes no quadratic assumption, so it reports the asymmetric interval a bounded or skewed parameter actually has. It costs one optimization per grid point. Every other parameter is optimized, including any the original fit held frozen.
name must be a scalar Param. Profiling one element of an array valued parameter would need the array split into separate leaves.
- Parameters:
compiled – The fitted model.
observations (Mapping[str, Any]) – Attribute name to observations, as passed to the fit.
name (str) – The scalar parameter to profile.
grid (Sequence[float]) – Constrained values to fix it at. Should bracket the expected interval on both sides. For a bounded Param, values at or beyond a bound are moved just inside it.
result – The fit’s
CalibrationResult.raw_params (Mapping[str, Any] | None) – Alternatively, the fitted unconstrained parameters. Takes precedence over result.
prior_neg_log_prob (Callable[[Mapping[str, Any]], Any] | None) – As used in the fit.
level (float) – Confidence level: 0.68, 0.90, 0.95 or 0.99.
n_steps (int) – Optimizer steps per grid point.
learning_rate (float) – Adam learning rate per grid point.
- Returns:
A
ProfileLikelihood. Ananinterval bound means the grid did not extend far enough on that side.- Raises:
ValueError – If level is unsupported, the parameter is not scalar, neither result nor raw_params is given, or a grid value is not positive for a softplus parameter.
KeyError – If the model has no parameter name.
- Return type:
- class prophys.engine.ProfileLikelihood(name, grid, log_likelihood, estimate, max_log_likelihood, level, interval)[source]#
Profile log likelihood of one parameter with its likelihood ratio interval.
Returned by
profile_likelihood().- Parameters:
- log_likelihood: list[float]#
Log likelihood at each grid point, maximized over all other parameters.
- interval: tuple[float, float]#
where the profile, followed outward from its highest grid point, falls below max_log_likelihood by half the chi square (1 degree of freedom) critical value, linearly interpolated. A nan bound means the grid was too narrow to bracket the interval on that side.
- Type:
Likelihood ratio confidence interval
- prophys.engine.sample_parameters(uncertainty, n_draws, seed=0)[source]#
Draw parameter vectors from the Laplace (Gaussian) approximation of the fit.
Draws from
N(estimate, covariance)of aParameterUncertainty. These are the draws a predictive score (waic(),loo()) integrates over when no MCMC posterior is available.The draws inherit the Laplace approximation’s assumptions. The posterior is taken to be Gaussian in the reported (constrained) parameters, so a draw can fall outside a parameter’s admissible range when the estimate sits within a standard error or two of a bound. For a posterior that respects the constraints exactly, sample with
prophys.inferenceinstead and pass those draws.- Parameters:
uncertainty (ParameterUncertainty) – The Laplace approximation.
n_draws (int) – Number of draws.
seed (int) – NumPy random seed.
- Returns:
Flattened parameter name to an array of n_draws constrained values.
- Raises:
ValueError – If n_draws is not positive.
- Return type:
- class prophys.engine.Report[source]#
Base class of every diagnostic result in
prophys.engine.Subclasses are frozen dataclasses. On construction every field goes through
to_plain(), so a report never holds a live JAX array.str(report)gives a readable console summary,to_dict()a record ready for JSON, andfields()the(name, value)pairs.