Source code for ratingmodels.pooling
r"""Pooling charges from a fitted severity model.
`experience_rate` takes ``pooled_excess`` and ``pooling_charge`` as inputs;
this module is where those numbers come from. Given any severity model
exposing the two-method tail protocol --
- ``sf(x)``: unconditional survival :math:`P(X > x)`
- ``mean_excess(d)``: :math:`E[X - d \mid X > d]`
-- the expected cost above a pooling point per unit of exposure is
.. math::
\text{frequency} \times S(d) \times e(d)
\;=\; \text{frequency} \times E[(X - d)_+],
grossed up for expenses and risk margin. ``lossmodels`` severity
distributions and ``extremeloss`` GPD tail fits both satisfy the protocol,
but *any* object with those two methods qualifies -- the seam is
duck-typed, and neither package is a dependency of this one.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
__all__ = ["pooling_charge_from_severity"]
[docs]
def pooling_charge_from_severity(
severity,
pooling_point: float,
expected_frequency: float,
expense_ratio: float = 0.0,
risk_margin: float = 0.0,
) -> pd.Series:
r"""Expected pooling charge per exposure unit, decomposed.
Parameters
----------
severity
Any object with ``sf(x)`` and ``mean_excess(d)`` (see module
docstring). Both must accept a float and return a float.
pooling_point : float
The per-claim attachment ``d`` above which losses are pooled.
expected_frequency : float
Expected claims per exposure unit (the same exposure unit the
charge should be quoted in).
expense_ratio : float
Expense provision as a share of the *charge*: the pure charge is
divided by ``1 - expense_ratio``. Must lie in ``[0, 1)``.
risk_margin : float
Proportional loading on the pure excess cost, applied before the
expense gross-up.
Returns
-------
pandas.Series
The build-up, each step auditable: ``exceedance_probability``
(:math:`S(d)`), ``mean_excess`` (:math:`e(d)`),
``expected_excess_per_claim`` (:math:`S(d)\,e(d) = E[(X-d)_+]`),
``pure_excess_cost`` (frequency :math:`\times\; E[(X-d)_+]`), and
``pooling_charge`` (after margin and expense gross-up). The final
value is what ``experience_rate`` expects as its
``pooling_charge`` input.
Raises
------
TypeError
If ``severity`` lacks the protocol methods.
ValueError
For an infinite mean excess (a tail with :math:`\xi \ge 1` has no
finite pooling cost at any attachment) or invalid loadings.
"""
for method in ("sf", "mean_excess"):
if not callable(getattr(severity, method, None)):
raise TypeError(
f"severity must expose callable {method!r}; got "
f"{type(severity).__name__} (the protocol is sf + mean_excess)"
)
if pooling_point < 0:
raise ValueError("pooling_point must be nonnegative")
if expected_frequency < 0:
raise ValueError("expected_frequency must be nonnegative")
if not 0.0 <= expense_ratio < 1.0:
raise ValueError("expense_ratio must be in [0, 1)")
if risk_margin < 0:
raise ValueError("risk_margin must be nonnegative")
surv = float(severity.sf(float(pooling_point)))
if not 0.0 <= surv <= 1.0:
raise ValueError(f"severity.sf returned {surv!r}, outside [0, 1]")
if surv == 0.0:
excess_per_claim = 0.0
me = 0.0
else:
me = float(severity.mean_excess(float(pooling_point)))
if not np.isfinite(me):
raise ValueError(
"mean excess is not finite at this pooling point: the tail "
"has no finite expected excess (e.g. a GPD with xi >= 1), so "
"no finite pooling charge exists"
)
excess_per_claim = surv * me
pure = expected_frequency * excess_per_claim
charge = pure * (1.0 + risk_margin) / (1.0 - expense_ratio)
return pd.Series(
{
"exceedance_probability": surv,
"mean_excess": me,
"expected_excess_per_claim": excess_per_claim,
"pure_excess_cost": pure,
"pooling_charge": charge,
},
name="pooling_charge_build_up",
)