Source code for ratingmodels.loading
r"""Retention and expense loading: turning a claims cost into a charged rate.
The charged (gross) rate is built from the **fundamental insurance equation**.
With loss & LAE per exposure unit :math:`L(1+\text{lae})`, a flat fixed
expense per unit :math:`F`, a variable load :math:`V` (expenses that are a percentage of
premium -- commission, premium tax, percent-of-premium fees and admin), and a
profit / contingency provision :math:`Q` (also a percentage of premium):
.. math::
P = L(1+\text{lae}) + F + V P + Q P
\;\Longrightarrow\;
P = \frac{L(1+\text{lae}) + F}{1 - V - Q}.
The variable load sits in the denominator because premium tax (and commission)
are levied on the premium that already contains them. The target / permissible
loss ratio is then an **output**, not an input:
.. math::
\text{PLR} = \frac{L}{P} = \frac{L\,(1 - V - Q)}{L(1+\text{lae}) + F}.
Because the fixed expense :math:`F` is added per exposure unit (not scaled by a risk's
relativity), grossing ``base_loss_cost * relativities`` with this formula keeps fixed
expense flat across all rate cells, which is the correct treatment.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Mapping
from ._utils import require_nonnegative
[docs]
@dataclass
class RetentionLoad:
"""Expense and profit loads used to gross claims up to a charged rate.
Parameters
----------
fixed_expense : float
Flat operating expense per exposure unit (a dollar amount, not
a percentage of premium). Default 0.
variable_expense_ratio : float
Sum of percent-of-premium loads: commission, premium tax, exchange /
regulatory fees, and any admin expressed as a percentage of premium.
Default 0.
profit_margin : float
Target underwriting profit / contribution to surplus, as a percentage
of premium. Default 0.
lae_ratio : float
Loss adjustment expense as a percentage of claims. Default 0.
"""
fixed_expense: float = 0.0
variable_expense_ratio: float = 0.0
profit_margin: float = 0.0
lae_ratio: float = 0.0
def __post_init__(self) -> None:
require_nonnegative(self.fixed_expense, "fixed_expense")
require_nonnegative(self.variable_expense_ratio, "variable_expense_ratio")
require_nonnegative(self.profit_margin, "profit_margin")
require_nonnegative(self.lae_ratio, "lae_ratio")
if self.variable_and_profit >= 1.0:
raise ValueError(
"variable_expense_ratio + profit_margin must be < 1; "
"the rate would be undefined or negative"
)
[docs]
@classmethod
def from_items(
cls,
fixed_expense: float = 0.0,
variable_items: Mapping[str, float] | None = None,
profit_margin: float = 0.0,
lae_ratio: float = 0.0,
) -> "RetentionLoad":
"""Construct from an itemized mapping of percent-of-premium loads.
``variable_items`` (e.g. ``{"commission": 0.04, "premium_tax": 0.023,
"aca_fees": 0.005, "admin_pct": 0.06}``) is summed into the variable
expense ratio.
"""
total_variable = float(sum((variable_items or {}).values()))
return cls(
fixed_expense=fixed_expense,
variable_expense_ratio=total_variable,
profit_margin=profit_margin,
lae_ratio=lae_ratio,
)
@property
def variable_and_profit(self) -> float:
"""Combined percent-of-premium load :math:`V + Q`."""
return self.variable_expense_ratio + self.profit_margin
[docs]
def gross_rate(self, loss_cost: float) -> float:
r"""Gross a loss cost up to a charged rate via :math:`(L(1+\text{lae})+F)/(1-V-Q)`."""
require_nonnegative(loss_cost, "loss_cost")
numerator = loss_cost * (1.0 + self.lae_ratio) + self.fixed_expense
return numerator / (1.0 - self.variable_and_profit)
[docs]
def implied_loss_ratio(self, loss_cost: float) -> float:
"""Loss ratio implied at a given claims level (claims / gross rate).
With a non-zero fixed expense this varies with the claims level; with
only percentage loads it equals ``1 - variable_expense_ratio -
profit_margin``.
"""
require_nonnegative(loss_cost, "loss_cost")
if loss_cost == 0:
return 0.0
return loss_cost / self.gross_rate(loss_cost)
[docs]
def expense_and_profit_ratio(self, loss_cost: float) -> float:
"""Share of the gross rate going to expense and profit (1 - loss ratio)."""
return 1.0 - self.implied_loss_ratio(loss_cost)
[docs]
def gross_rate(loss_cost: float, retention: RetentionLoad) -> float:
"""Functional form of :meth:`RetentionLoad.gross_rate`."""
return retention.gross_rate(loss_cost)
[docs]
def permissible_loss_ratio(retention: RetentionLoad, loss_cost: float) -> float:
"""Functional form of :meth:`RetentionLoad.implied_loss_ratio`."""
return retention.implied_loss_ratio(loss_cost)