"""
Group projection -- one group's forward roll, premium and claims together.
==========================================================================
Composes the premium roll-forward and the credibility-blended claims projection on
the given monthly membership, then weights both by the renewal probability. This is
the unit you loop over the in-force book (see :mod:`projectionmodels.book`).
Renewal probability weights premium and claims equally (a lapsed group books
neither), so the projected loss ratio is unaffected by it.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
import pandas as pd
from .pmpm import PMPMProjection, PMPMResult
from .premium import PremiumResult, PremiumRollforward
@dataclass(frozen=True)
class GroupProjectionResult:
monthly: pd.DataFrame # month, member_months, premium, claims (conditional on renewal)
premium: float # conditional on renewal
claims: float
loss_ratio: float
renewal_prob: float
expected_premium: float # renewal-weighted
expected_claims: float
pmpm: PMPMResult
premium_detail: PremiumResult
[docs]
class GroupProjection:
"""One group's forward roll: premium and claims projected together on one membership.
Composes :class:`PremiumRollforward` (the stored premium rolled
forward by rate action and plan change) and :class:`PMPMProjection`
(the credibility-blended, trended, pooled claims PMPM) on the supplied
monthly membership, then weights both by the renewal probability -- a
lapsed group books neither, so ``loss_ratio`` is unaffected by
``renewal_prob`` while ``expected_premium`` / ``expected_claims``
carry it. This is the unit :class:`BookProjection` loops over the
in-force book.
Exposes ``monthly`` (month, member-months, premium, claims --
conditional on renewal), the conditional totals ``premium`` /
``claims`` / ``loss_ratio``, and the renewal-weighted
``expected_premium`` / ``expected_claims``; the full detail,
including the claims and premium build-ups, sits on ``result``
(:class:`GroupProjectionResult`).
Parameters
----------
prospective_membership : array-like
Projected member-months by prospective month; the horizon
everything else is evaluated on.
seasonal_factors : array-like, optional
Monthly claim seasonality (averaging one); redistributes claims
across months without changing the annual total.
current_premium, current_member_months : float
The stored premium and its member-months -- rolled forward, never
rebuilt from experience.
rate_action, plan_change : float, optional
Renewal rate action and plan-value change, as decimals.
book_pmpm, claim_trend, exp_midpoint, prosp_midpoint : float
Book claims PMPM and annual trend, applied
midpoint-to-midpoint between the experience and projection
periods.
group_pmpm, group_claims, group_member_months : optional
The group's own experience -- the PMPM directly, or claims and
member-months to derive it.
group_claim_count, credibility, full_credibility_claims : optional
Credibility, or the claim count to derive it by limited
fluctuation (default full-credibility standard 1082 claims).
pooling_pmpm : float, optional
Large-claim pooling charge per member-month, trended alongside
claims.
plan_affects_claims : bool, optional
Whether ``plan_change`` also scales claims (default True).
renewal_prob : float, optional
Probability the group renews -- supplied (e.g. from
underwriting), not modelled here; weights the expected figures.
"""
def __init__(self, *, prospective_membership, seasonal_factors=None,
# premium (from DB)
current_premium, current_member_months, rate_action=0.0, plan_change=0.0,
# claims (group from DB + book)
book_pmpm, claim_trend, exp_midpoint, prosp_midpoint,
group_pmpm=None, group_claims=None, group_member_months=None,
group_claim_count=None, credibility=None, full_credibility_claims=1082.0,
pooling_pmpm=0.0, plan_affects_claims=True,
# renewal likelihood -- supplied (e.g. from underwriting), not modelled here
renewal_prob=1.0):
E = np.asarray(prospective_membership, float)
s = np.ones_like(E) if seasonal_factors is None else np.asarray(seasonal_factors, float)
plan_factor = (1.0 + plan_change) if plan_affects_claims else 1.0
prem = PremiumRollforward(current_premium=current_premium,
current_member_months=current_member_months,
rate_action=rate_action, plan_change=plan_change)
pmpm = PMPMProjection(book_pmpm=book_pmpm, claim_trend=claim_trend,
exp_midpoint=exp_midpoint, prosp_midpoint=prosp_midpoint,
group_pmpm=group_pmpm, group_claims=group_claims,
group_member_months=group_member_months,
group_claim_count=group_claim_count, credibility=credibility,
full_credibility_claims=full_credibility_claims,
plan_factor=plan_factor, pooling_pmpm=pooling_pmpm)
premium_m = prem.premium(E)
claims_m = pmpm.claims(E, s)
monthly = pd.DataFrame({"month": np.arange(1, len(E) + 1),
"member_months": E.round().astype(int),
"premium": premium_m, "claims": claims_m})
P, C = float(premium_m.sum()), float(claims_m.sum())
self.result = GroupProjectionResult(
monthly=monthly, premium=P, claims=C, loss_ratio=C / P, renewal_prob=renewal_prob,
expected_premium=renewal_prob * P, expected_claims=renewal_prob * C,
pmpm=pmpm.result, premium_detail=prem.result)
# convenient pass-through to the common result fields
@property
def monthly(self): return self.result.monthly
@property
def premium(self): return self.result.premium
@property
def claims(self): return self.result.claims
@property
def loss_ratio(self): return self.result.loss_ratio
@property
def expected_premium(self): return self.result.expected_premium
@property
def expected_claims(self): return self.result.expected_claims
@property
def renewal_prob(self): return self.result.renewal_prob
def project_group(**kwargs) -> GroupProjectionResult:
"""Functional form: returns the :class:`GroupProjectionResult`."""
return GroupProjection(**kwargs).result
[docs]
def new_business(*, book_pmpm, claim_trend, exp_midpoint, prosp_midpoint,
prospective_membership, manual_premium_pmpm, seasonal_factors=None,
close_ratio=1.0, plan_change=0.0, pooling_pmpm=0.0) -> GroupProjectionResult:
"""A sold-but-new case: no experience, so claims are fully manual (credibility 0)
and premium is the manual/target rate on projected membership; `close_ratio`
plays the role of the renewal probability."""
E = np.asarray(prospective_membership, float)
return GroupProjection(
prospective_membership=E, seasonal_factors=seasonal_factors,
current_premium=manual_premium_pmpm * E.sum(), current_member_months=E.sum(),
rate_action=0.0, plan_change=plan_change,
book_pmpm=book_pmpm, claim_trend=claim_trend, exp_midpoint=exp_midpoint,
prosp_midpoint=prosp_midpoint, group_pmpm=book_pmpm, credibility=0.0,
pooling_pmpm=pooling_pmpm, renewal_prob=close_ratio).result