mixle.inference.risk module

Risk / tail metrics over a Monte-Carlo outcome distribution (e.g. J2’s NPV samples).

A distribution’s mean says nothing about how bad the bad outcomes are. This module turns a plain sample array – typically NPVDistribution’s samples, but any array of scalar outcomes works – into the two standard tail-risk summaries plus a scenario ranking:

  • value_at_risk() – the loss not exceeded with probability alpha (a quantile).

  • conditional_value_at_risk() – the expected loss given that the VaR threshold is breached (expected shortfall); always at least as large as VaR. A Generalized Pareto tail extrapolation is available by OPT-IN only (gpd_tail=True): measured on sparse tails it was strictly noisier than the raw tail mean it replaced (audit R-1), so sparse tails default to the honest sample statistic.

  • stress_rank() – named stress scenarios (low-grade, price-crash, carbon-spike, …) ranked from worst to least-bad loss.

Throughout, loss is -outcome – a positive loss means a bad (low/negative NPV) draw, so VaR and CVaR come back as positive numbers when the distribution has meaningful downside.

value_at_risk(samples, alpha=0.95)[source]

Value-at-Risk at confidence alpha.

VaR_alpha = -quantile(samples, 1 - alpha): losses are -outcome, so the (1-alpha) lower tail of the outcome distribution (rare, bad draws) becomes the loss exceeded only (1-alpha) of the time.

Parameters:
  • samples (Any) – array-like of scalar outcomes (e.g. an NPV Monte-Carlo sample array).

  • alpha (float) – confidence level in (0, 1); 0.95 means “loss exceeded 5% of the time”.

Returns:

The VaR as a loss (positive when the distribution has meaningful downside).

Return type:

float

conditional_value_at_risk(samples, alpha=0.95, *, min_tail=20, gpd_tail=False)[source]

Conditional Value-at-Risk (expected shortfall) at confidence alpha.

The empirical estimator is the mass-weighted mean over the worst 1 - alpha probability mass (NOT the docstring formula -mean(samples <= -VaR), which overweights observations tied at VaR – audit R-3 pinned the mismatch between the stated and computed estimator). Because the tail mean is at least as extreme as the threshold bounding it, CVaR >= VaR.

gpd_tail=True opts in to a Generalized Pareto refinement of a sparse tail (fewer than min_tail exceedances). It is OFF by default because it is measurably WORSE exactly where it fires (audit R-1): fitting a two-parameter heavy-tail MLE to the same 5-19 points it is meant to rescue, then plugging the estimates into VaR + scale/(1 - shape), multiplied the estimator’s spread instead of shrinking it – the estimand of that measurement is the estimator’s own sampling uncertainty: on lognormal losses at n = 100 (true ES 8.54), the raw tail mean’s sampling sd was 2.36 while the GPD path’s was 46.4 with a worst case of 663. Use the refinement only with an exogenous reason to trust a GPD tail (e.g. a threshold chosen from much more data), and treat its output as a model extrapolation, not a sample statistic; its parameters are plug-in MLEs with no interval attached (audit R-2).

Parameters:
  • samples (Any) – array-like of scalar outcomes (same array passed to value_at_risk()).

  • alpha (float) – confidence level in (0, 1).

  • min_tail (int) – tail sample count below which the (opt-in) GPD refinement is attempted.

  • gpd_tail (bool) – opt in to the GPD tail extrapolation described above.

Returns:

The CVaR as a loss; always >= value_at_risk(samples, alpha).

Return type:

float

stress_rank(scenarios)[source]

Rank named stress scenarios (e.g. low-grade, price-crash, carbon-spike) by loss.

Each scenario’s value may be a scalar outcome (e.g. a single stressed NPV) or an array of outcomes (e.g. NPV samples drawn under that stress); the ranking key is -mean(value) either way, so a bare float and a 1-sample array behave identically.

Parameters:

scenarios (dict[str, Any]) – mapping of scenario name -> scalar outcome or array of outcomes.

Returns:

[(name, loss), ...] sorted from worst (largest loss) to least-bad, ties broken by the input mapping’s iteration order.

Return type:

list[tuple[str, float]]