Source code for ratingmodels.indication

r"""Rate indication: the central object that turns experience and manual
inputs into an indicated rate and a rate change.

Two standard methods are exposed.

**Build-up (loss-ratio loaded).**  Blend experience and manual *claims* by
credibility, then gross up by the target loss ratio:

.. math::
    \text{indicated rate} = \frac{Z\,\text{exp claims} + (1-Z)\,\text{man claims}}
                                 {\text{target LR}}, \qquad
    \text{change} = \frac{\text{indicated}}{\text{current}} - 1.

**Loss-ratio (credibility-weighted indication).**  Weight the experience
indication against a trend-only ("no experience") indication:

.. math::
    \text{change} = Z\!\left(\frac{\text{exp LR}}{\text{target LR}} - 1\right)
                    + (1 - Z)\,\big((1+t)^{\Delta}-1\big).
"""
from __future__ import annotations

from dataclasses import dataclass

from ._utils import require_positive, require_unit_interval
from .blend import blend
from .decomposition import RateChangeDecomposition, decompose_rate_change
from .loading import RetentionLoad


[docs] @dataclass class RateIndication: r"""Develop an indicated rate from experience and manual inputs. Parameters ---------- experience_loss_cost : float Trended, pooled, adjusted experience loss cost (per exposure unit) (see :class:`ratingmodels.ExperienceRate`). manual_loss_cost : float Manual loss cost at the rating-period level (see :class:`ratingmodels.ManualRate`). credibility : float Credibility ``Z`` assigned to experience, in [0, 1]. current_rate : float Current charged rate per exposure unit. target_loss_ratio : float Claims / premium target used to load claims to a charged rate. current_premium : float, optional On-level earned premium over the experience period; required only for the loss-ratio method. exposure : float, optional Exposure units over the experience period; required for the loss-ratio method (with ``current_premium``) to form an experience loss ratio. trend_total_factor : float Total claims trend factor :math:`(1+t)^\Delta`; used by the loss-ratio method's trend-only side and by the decomposition. Default 1.0. benefit_factor, demographic_factor : float Driver factors for the rate-change decomposition. Default 1.0. """ experience_loss_cost: float manual_loss_cost: float credibility: float current_rate: float target_loss_ratio: float = 0.85 current_premium: float | None = None exposure: float | None = None trend_total_factor: float = 1.0 benefit_factor: float = 1.0 demographic_factor: float = 1.0 retention: "RetentionLoad | None" = None def __post_init__(self) -> None: require_positive(self.experience_loss_cost + 1e-12, "experience_loss_cost") require_positive(self.manual_loss_cost, "manual_loss_cost") require_unit_interval(self.credibility, "credibility") require_positive(self.current_rate, "current_rate") require_unit_interval(self.target_loss_ratio, "target_loss_ratio", closed=False) # ----- claims-level blending ----- # def blended_loss_cost(self) -> float: return blend( self.experience_loss_cost, self.manual_loss_cost, self.credibility ) # ----- charged rates ----- # def _gross(self, loss_cost: float) -> float: """Gross claims to a charged rate via retention, else target loss ratio.""" if self.retention is not None: return self.retention.gross_rate(loss_cost) return loss_cost / self.target_loss_ratio def experience_rate(self) -> float: return self._gross(self.experience_loss_cost) def manual_rate(self) -> float: return self._gross(self.manual_loss_cost) def blended_rate(self) -> float: return self._gross(self.blended_loss_cost()) # ----- indication (build-up) ----- #
[docs] def indicated_rate(self) -> float: """Indicated charged rate (build-up method).""" return self.blended_rate()
[docs] def indicated_rate_change(self) -> float: """Proportional change implied by the build-up indicated rate.""" return self.indicated_rate() / self.current_rate - 1.0
# ----- indication (loss-ratio method) ----- # def experience_loss_ratio(self) -> float: if self.current_premium is None or self.exposure is None: raise ValueError( "current_premium and exposure are required for the loss-ratio method" ) require_positive(self.current_premium, "current_premium") require_positive(self.exposure, "exposure") experience_claims = self.experience_loss_cost * self.exposure return experience_claims / self.current_premium
[docs] def loss_ratio_indication(self) -> float: r"""Credibility-weighted loss-ratio rate change. Experience side: ``exp_LR / target_LR - 1``. Trend-only side: ``trend_total_factor - 1``. """ exp_side = self.experience_loss_ratio() / self.target_loss_ratio - 1.0 trend_side = self.trend_total_factor - 1.0 z = self.credibility return z * exp_side + (1 - z) * trend_side
# ----- decomposition ----- #
[docs] def rate_change_decomposition(self) -> RateChangeDecomposition: r"""Decompose the build-up indicated change into named drivers. Drivers: * ``trend`` -- the claims trend factor, * ``experience`` -- the credibility effect, blended/manual claims (equals 1 when ``Z = 0``), * ``benefit`` and ``demographic`` -- supplied adjustment factors. Any remaining movement (rate adequacy / loading) is absorbed by an explicit ``residual`` factor so the parts reconcile to the total. """ experience_factor = self.blended_loss_cost() / self.manual_loss_cost drivers = { "trend": self.trend_total_factor, "experience": experience_factor, "benefit": self.benefit_factor, "demographic": self.demographic_factor, } total = self.indicated_rate() / self.current_rate return decompose_rate_change(drivers, total_factor=total)