Source code for lossmodels.severity.base
import numpy as np
from ..utils.random import RNGLike
from abc import ABC, abstractmethod
from scipy.integrate import quad
from scipy.optimize import brentq
[docs]
class SeverityModel(ABC):
"""
Base class for severity (loss size) distributions.
All severity models must implement:
- sample
- mean
- variance
"""
[docs]
@abstractmethod
def sample(self, size: int = 1, rng: RNGLike = None) -> np.ndarray:
"""Generate random loss samples"""
pass
[docs]
@abstractmethod
def mean(self) -> float:
"""Expected loss"""
pass
[docs]
@abstractmethod
def variance(self) -> float:
"""Variance of loss"""
pass
def std(self) -> float:
return np.sqrt(self.variance())
# --- Quantile / inverse CDF ---
[docs]
def quantile(self, p):
"""Inverse CDF (quantile / Value-at-Risk).
Numerically inverts ``cdf``. Subclasses with a closed-form inverse
override this for speed and accuracy. Returns a Python ``float`` for a
scalar ``p`` and a ``numpy.ndarray`` for array-like ``p``.
"""
arr = np.asarray(p, dtype=float)
out = np.array([self._invert_cdf(float(pi)) for pi in np.atleast_1d(arr)], dtype=float)
return float(out[0]) if arr.ndim == 0 else out.reshape(arr.shape)
[docs]
def ppf(self, p):
"""Alias for :meth:`quantile` (inverse CDF)."""
return self.quantile(p)
def _invert_cdf(self, p: float) -> float:
if not 0.0 <= p <= 1.0:
raise ValueError("p must be in [0, 1].")
if p == 0.0:
return 0.0
if p == 1.0:
return float("inf")
if not hasattr(self, "cdf"):
raise TypeError("Severity model must implement cdf(x) to invert.")
hi = 1.0
while self.cdf(hi) < p:
hi *= 2.0
if hi > 1e15:
break
return float(brentq(lambda x: self.cdf(x) - p, 0.0, hi, maxiter=200))
# --- Actuarial-specific methods ---
[docs]
def limited_expected_value(self, d: float, n_sim: int = 100_000) -> float:
"""
E[min(X, d)] computed deterministically from the survival function.
For a nonnegative loss random variable X,
E[min(X, d)] = integral_0^d S_X(x) dx
where S_X(x) = 1 - F_X(x).
The ``n_sim`` argument is retained for backward compatibility but is no
longer used.
"""
del n_sim # backward-compatibility placeholder
if d < 0:
raise ValueError("d must be nonnegative.")
if d == 0:
return 0.0
if not hasattr(self, "cdf"):
raise TypeError("Severity model must implement cdf(x).")
value, _ = quad(lambda x: 1.0 - self.cdf(x), 0.0, d, limit=200)
return float(value)
[docs]
def excess_loss(self, d: float, n_sim: int = 100_000) -> float:
"""
E[(X - d)+] computed deterministically from the survival function.
For a nonnegative loss random variable X,
E[(X - d)+] = integral_d^infinity S_X(x) dx
where S_X(x) = 1 - F_X(x).
This remains well defined even when E[X] does not exist, provided the
tail integral from d to infinity converges. The ``n_sim`` argument is
retained for backward compatibility but is no longer used.
"""
del n_sim # backward-compatibility placeholder
if d < 0:
raise ValueError("d must be nonnegative.")
if not hasattr(self, "cdf"):
raise TypeError("Severity model must implement cdf(x).")
value, _ = quad(lambda x: 1.0 - self.cdf(x), d, np.inf, limit=200)
return float(value)
def __repr__(self):
return f"{self.__class__.__name__}()"