Source code for extremeloss.estimation.metrics

from __future__ import annotations

import numpy as np

from ..utils.validation import as_1d_float_array, validate_q


def _var_rank(n: int, q: np.ndarray) -> np.ndarray:
    """Rank k of the VaR order statistic: k = ceil(n*q), guarded against
    floating-point error when n*q is an exact integer."""
    nq = n * q
    k = np.where(np.abs(nq - np.round(nq)) < 1e-8, np.round(nq), np.ceil(nq))
    return np.clip(k.astype(np.intp), 1, n)


[docs] def empirical_var(losses, q): """Empirical VaR: the order statistic ``x_(ceil(n*q))``. Implements ``VaR_q = inf{x : F(x) >= q}`` on the empirical distribution (identical to ``np.quantile(..., method="inverted_cdf")``). This is the ecosystem-standard estimator shared with ``risksim`` and ``lossmodels``; it serves as the crude-Monte-Carlo baseline that the variance-reduced estimators in this subpackage are compared against. ``q`` may be scalar (returns ``float``) or array-like (returns array). """ validate_q(q) arr = np.sort(as_1d_float_array(losses, name="losses")) q_arr = np.asarray(q, dtype=float) k = _var_rank(arr.size, q_arr) out = arr[k - 1] return float(out) if q_arr.ndim == 0 else np.asarray(out, dtype=float)
[docs] def empirical_tvar(losses, q): """Empirical TVaR via the average-quantile (Acerbi-Tasche) plug-in. Implements ``TVaR_q = (1/(1-q)) * integral_q^1 VaR_u du`` exactly on the empirical distribution: with sorted losses and ``k = ceil(n*q)``, TVaR_q = [ sum_{i>k} x_(i) + x_(k) * (k - n*q) ] / (n * (1 - q)). Correct in the presence of ties/atoms and always ``>= empirical_var``. Ecosystem-standard estimator shared with ``risksim`` and ``lossmodels``. ``q`` may be scalar (returns ``float``) or array-like (returns array). """ validate_q(q) arr = np.sort(as_1d_float_array(losses, name="losses")) n = arr.size q_arr = np.asarray(q, dtype=float) k = _var_rank(n, q_arr) csum = np.concatenate(([0.0], np.cumsum(arr))) tail_sum = csum[n] - csum[k] var_vals = arr[k - 1] nq = n * q_arr weight = np.where(np.abs(nq - np.round(nq)) < 1e-8, 0.0, k - nq) out = (tail_sum + var_vals * weight) / (n * (1.0 - q_arr)) # TVaR >= VaR holds as a theorem in exact arithmetic; enforce it so # floating-point noise (e.g. a constant tail at a layer limit) can never # produce tvar infinitesimally below var. out = np.maximum(out, var_vals) return float(out) if q_arr.ndim == 0 else np.asarray(out, dtype=float)
def exceedance_probability(losses, threshold: float) -> float: arr = as_1d_float_array(losses, name="losses") return float(np.mean(arr > threshold)) def exceedance_curve(losses, thresholds) -> dict[str, np.ndarray]: arr = as_1d_float_array(losses, name="losses") grid = as_1d_float_array(thresholds, name="thresholds") probs = np.array([np.mean(arr > u) for u in grid], dtype=float) return {"thresholds": grid, "probabilities": probs} def survival_function(losses, grid) -> dict[str, np.ndarray]: return exceedance_curve(losses, grid)