projectionmodels 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- projectionmodels/__init__.py +50 -0
- projectionmodels/book.py +65 -0
- projectionmodels/group.py +105 -0
- projectionmodels/pmpm.py +70 -0
- projectionmodels/premium.py +45 -0
- projectionmodels-0.1.0.dist-info/METADATA +127 -0
- projectionmodels-0.1.0.dist-info/RECORD +9 -0
- projectionmodels-0.1.0.dist-info/WHEEL +4 -0
- projectionmodels-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
projectionmodels -- forward projection (budgeting) for a book of business
|
|
3
|
+
=========================================================================
|
|
4
|
+
Part of the OpenActuarial ecosystem. Sits beside `ratingmodels` and `lossmodels`
|
|
5
|
+
on top of the `actuarialpy` primitives layer, and depends only downward on
|
|
6
|
+
`actuarialpy` -- never sideways on another workflow package.
|
|
7
|
+
|
|
8
|
+
Rating builds a price from experience; projection takes the book AS IT IS -- stored
|
|
9
|
+
premiums, historical claims, membership assumptions -- and rolls it forward for
|
|
10
|
+
planning. The dividing line: the moment something computes what you *should* charge
|
|
11
|
+
rather than what the projection *is*, it belongs in `ratingmodels`, not here.
|
|
12
|
+
|
|
13
|
+
Layers
|
|
14
|
+
------
|
|
15
|
+
PMPMProjection credibility-blended, trended, pooled claims PMPM
|
|
16
|
+
PremiumRollforward stored premium rolled forward by rate action / plan change
|
|
17
|
+
GroupProjection one group: premium + claims + renewal weighting (loop unit)
|
|
18
|
+
BookProjection aggregate in-force renewals + new business -> book budget
|
|
19
|
+
|
|
20
|
+
Each class computes on construction and exposes a frozen ``*Result`` dataclass via
|
|
21
|
+
``.result``; a lowercase functional form (``project_group`` etc.) returns the result
|
|
22
|
+
directly.
|
|
23
|
+
|
|
24
|
+
Quick start
|
|
25
|
+
-----------
|
|
26
|
+
from projectionmodels import GroupProjection, BookProjection
|
|
27
|
+
|
|
28
|
+
g = GroupProjection(prospective_membership=E, seasonal_factors=s,
|
|
29
|
+
current_premium=..., current_member_months=...,
|
|
30
|
+
rate_action=0.06, plan_change=-0.02,
|
|
31
|
+
book_pmpm=..., claim_trend=0.06,
|
|
32
|
+
exp_midpoint=..., prosp_midpoint=...,
|
|
33
|
+
group_claims=..., group_member_months=..., group_claim_count=...,
|
|
34
|
+
pooling_pmpm=..., renewal_prob=0.90) # renewal likelihood from underwriting
|
|
35
|
+
book = BookProjection([g_1, g_2, new_biz], labels=[...])
|
|
36
|
+
book.loss_ratio
|
|
37
|
+
"""
|
|
38
|
+
from .pmpm import PMPMProjection, PMPMResult, project_pmpm
|
|
39
|
+
from .premium import PremiumRollforward, PremiumResult, roll_forward_premium
|
|
40
|
+
from .group import (GroupProjection, GroupProjectionResult, project_group, new_business)
|
|
41
|
+
from .book import BookProjection, BookResult, project_book
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"PMPMProjection", "PMPMResult", "project_pmpm",
|
|
47
|
+
"PremiumRollforward", "PremiumResult", "roll_forward_premium",
|
|
48
|
+
"GroupProjection", "GroupProjectionResult", "project_group", "new_business",
|
|
49
|
+
"BookProjection", "BookResult", "project_book",
|
|
50
|
+
]
|
projectionmodels/book.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Book projection -- aggregate group projections into the book budget.
|
|
3
|
+
====================================================================
|
|
4
|
+
Rolls up in-force renewals and new business into total premium, claims, and loss
|
|
5
|
+
ratio (by group and by month). Totals use the EXPECTED (renewal-weighted) figures,
|
|
6
|
+
so a group contributes in proportion to how likely it is to renew.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from .group import GroupProjection, GroupProjectionResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class BookResult:
|
|
18
|
+
premium: float # expected (renewal-weighted) book premium
|
|
19
|
+
claims: float
|
|
20
|
+
loss_ratio: float
|
|
21
|
+
by_group: pd.DataFrame # per-group expected premium/claims/LR/renewal
|
|
22
|
+
monthly: pd.DataFrame # expected premium/claims by month, summed over the book
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BookProjection:
|
|
26
|
+
def __init__(self, projections, labels=None):
|
|
27
|
+
results = [p.result if isinstance(p, GroupProjection) else p for p in projections]
|
|
28
|
+
if not results:
|
|
29
|
+
raise ValueError("no projections supplied")
|
|
30
|
+
labels = list(labels) if labels is not None else [f"grp_{i}" for i in range(len(results))]
|
|
31
|
+
|
|
32
|
+
rows = []
|
|
33
|
+
prem_m = np.zeros(len(results[0].monthly))
|
|
34
|
+
clm_m = np.zeros_like(prem_m)
|
|
35
|
+
months = results[0].monthly["month"].to_numpy()
|
|
36
|
+
for lab, r in zip(labels, results):
|
|
37
|
+
rows.append({"group": lab, "premium": r.expected_premium, "claims": r.expected_claims,
|
|
38
|
+
"loss_ratio": (r.expected_claims / r.expected_premium
|
|
39
|
+
if r.expected_premium else np.nan),
|
|
40
|
+
"renewal_prob": r.renewal_prob})
|
|
41
|
+
prem_m += r.monthly["premium"].to_numpy() * r.renewal_prob
|
|
42
|
+
clm_m += r.monthly["claims"].to_numpy() * r.renewal_prob
|
|
43
|
+
|
|
44
|
+
by_group = pd.DataFrame(rows)
|
|
45
|
+
monthly = pd.DataFrame({"month": months, "premium": prem_m, "claims": clm_m})
|
|
46
|
+
monthly["loss_ratio"] = monthly["claims"] / monthly["premium"]
|
|
47
|
+
P, C = float(by_group["premium"].sum()), float(by_group["claims"].sum())
|
|
48
|
+
self.result = BookResult(premium=P, claims=C, loss_ratio=C / P,
|
|
49
|
+
by_group=by_group, monthly=monthly)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def premium(self): return self.result.premium
|
|
53
|
+
@property
|
|
54
|
+
def claims(self): return self.result.claims
|
|
55
|
+
@property
|
|
56
|
+
def loss_ratio(self): return self.result.loss_ratio
|
|
57
|
+
@property
|
|
58
|
+
def by_group(self): return self.result.by_group
|
|
59
|
+
@property
|
|
60
|
+
def monthly(self): return self.result.monthly
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def project_book(projections, labels=None) -> BookResult:
|
|
64
|
+
"""Functional form: returns the :class:`BookResult`."""
|
|
65
|
+
return BookProjection(projections, labels=labels).result
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Group projection -- one group's forward roll, premium and claims together.
|
|
3
|
+
==========================================================================
|
|
4
|
+
Composes the premium roll-forward and the credibility-blended claims projection on
|
|
5
|
+
the given monthly membership, then weights both by the renewal probability. This is
|
|
6
|
+
the unit you loop over the in-force book (see :mod:`projectionmodels.book`).
|
|
7
|
+
|
|
8
|
+
Renewal probability weights premium and claims equally (a lapsed group books
|
|
9
|
+
neither), so the projected loss ratio is unaffected by it.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from .pmpm import PMPMProjection, PMPMResult
|
|
17
|
+
from .premium import PremiumRollforward, PremiumResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class GroupProjectionResult:
|
|
22
|
+
monthly: pd.DataFrame # month, member_months, premium, claims (conditional on renewal)
|
|
23
|
+
premium: float # conditional on renewal
|
|
24
|
+
claims: float
|
|
25
|
+
loss_ratio: float
|
|
26
|
+
renewal_prob: float
|
|
27
|
+
expected_premium: float # renewal-weighted
|
|
28
|
+
expected_claims: float
|
|
29
|
+
pmpm: PMPMResult
|
|
30
|
+
premium_detail: PremiumResult
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GroupProjection:
|
|
34
|
+
def __init__(self, *, prospective_membership, seasonal_factors=None,
|
|
35
|
+
# premium (from DB)
|
|
36
|
+
current_premium, current_member_months, rate_action=0.0, plan_change=0.0,
|
|
37
|
+
# claims (group from DB + book)
|
|
38
|
+
book_pmpm, claim_trend, exp_midpoint, prosp_midpoint,
|
|
39
|
+
group_pmpm=None, group_claims=None, group_member_months=None,
|
|
40
|
+
group_claim_count=None, credibility=None, full_credibility_claims=1082.0,
|
|
41
|
+
pooling_pmpm=0.0, plan_affects_claims=True,
|
|
42
|
+
# renewal likelihood -- supplied (e.g. from underwriting), not modelled here
|
|
43
|
+
renewal_prob=1.0):
|
|
44
|
+
E = np.asarray(prospective_membership, float)
|
|
45
|
+
s = np.ones_like(E) if seasonal_factors is None else np.asarray(seasonal_factors, float)
|
|
46
|
+
plan_factor = (1.0 + plan_change) if plan_affects_claims else 1.0
|
|
47
|
+
|
|
48
|
+
prem = PremiumRollforward(current_premium=current_premium,
|
|
49
|
+
current_member_months=current_member_months,
|
|
50
|
+
rate_action=rate_action, plan_change=plan_change)
|
|
51
|
+
pmpm = PMPMProjection(book_pmpm=book_pmpm, claim_trend=claim_trend,
|
|
52
|
+
exp_midpoint=exp_midpoint, prosp_midpoint=prosp_midpoint,
|
|
53
|
+
group_pmpm=group_pmpm, group_claims=group_claims,
|
|
54
|
+
group_member_months=group_member_months,
|
|
55
|
+
group_claim_count=group_claim_count, credibility=credibility,
|
|
56
|
+
full_credibility_claims=full_credibility_claims,
|
|
57
|
+
plan_factor=plan_factor, pooling_pmpm=pooling_pmpm)
|
|
58
|
+
|
|
59
|
+
premium_m = prem.premium(E)
|
|
60
|
+
claims_m = pmpm.claims(E, s)
|
|
61
|
+
monthly = pd.DataFrame({"month": np.arange(1, len(E) + 1),
|
|
62
|
+
"member_months": E.round().astype(int),
|
|
63
|
+
"premium": premium_m, "claims": claims_m})
|
|
64
|
+
P, C = float(premium_m.sum()), float(claims_m.sum())
|
|
65
|
+
self.result = GroupProjectionResult(
|
|
66
|
+
monthly=monthly, premium=P, claims=C, loss_ratio=C / P, renewal_prob=renewal_prob,
|
|
67
|
+
expected_premium=renewal_prob * P, expected_claims=renewal_prob * C,
|
|
68
|
+
pmpm=pmpm.result, premium_detail=prem.result)
|
|
69
|
+
|
|
70
|
+
# convenient pass-through to the common result fields
|
|
71
|
+
@property
|
|
72
|
+
def monthly(self): return self.result.monthly
|
|
73
|
+
@property
|
|
74
|
+
def premium(self): return self.result.premium
|
|
75
|
+
@property
|
|
76
|
+
def claims(self): return self.result.claims
|
|
77
|
+
@property
|
|
78
|
+
def loss_ratio(self): return self.result.loss_ratio
|
|
79
|
+
@property
|
|
80
|
+
def expected_premium(self): return self.result.expected_premium
|
|
81
|
+
@property
|
|
82
|
+
def expected_claims(self): return self.result.expected_claims
|
|
83
|
+
@property
|
|
84
|
+
def renewal_prob(self): return self.result.renewal_prob
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def project_group(**kwargs) -> GroupProjectionResult:
|
|
88
|
+
"""Functional form: returns the :class:`GroupProjectionResult`."""
|
|
89
|
+
return GroupProjection(**kwargs).result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def new_business(*, book_pmpm, claim_trend, exp_midpoint, prosp_midpoint,
|
|
93
|
+
prospective_membership, manual_premium_pmpm, seasonal_factors=None,
|
|
94
|
+
close_ratio=1.0, plan_change=0.0, pooling_pmpm=0.0) -> GroupProjectionResult:
|
|
95
|
+
"""A sold-but-new case: no experience, so claims are fully manual (credibility 0)
|
|
96
|
+
and premium is the manual/target rate on projected membership; `close_ratio`
|
|
97
|
+
plays the role of the renewal probability."""
|
|
98
|
+
E = np.asarray(prospective_membership, float)
|
|
99
|
+
return GroupProjection(
|
|
100
|
+
prospective_membership=E, seasonal_factors=seasonal_factors,
|
|
101
|
+
current_premium=manual_premium_pmpm * E.sum(), current_member_months=E.sum(),
|
|
102
|
+
rate_action=0.0, plan_change=plan_change,
|
|
103
|
+
book_pmpm=book_pmpm, claim_trend=claim_trend, exp_midpoint=exp_midpoint,
|
|
104
|
+
prosp_midpoint=prosp_midpoint, group_pmpm=book_pmpm, credibility=0.0,
|
|
105
|
+
pooling_pmpm=pooling_pmpm, renewal_prob=close_ratio).result
|
projectionmodels/pmpm.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PMPM projection -- the claims engine.
|
|
3
|
+
=====================================
|
|
4
|
+
Credibility-blends the group's own PMPM with the book PMPM, trends and
|
|
5
|
+
plan-adjusts it, and adds a large-claim pooling load:
|
|
6
|
+
|
|
7
|
+
Z from the group's claim count (limited fluctuation) unless supplied
|
|
8
|
+
blended = Z * group_pmpm + (1 - Z) * book_pmpm
|
|
9
|
+
projected = blended * trend * plan_factor + pooling_pmpm * trend
|
|
10
|
+
|
|
11
|
+
`projected_pmpm` is a rate per member-month. Call `.claims(membership, seasonal)`
|
|
12
|
+
to turn it into projected claim dollars by month; seasonality (factors averaging 1)
|
|
13
|
+
redistributes across months without changing the annual total.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
import numpy as np
|
|
18
|
+
import actuarialpy as ap
|
|
19
|
+
from actuarialpy import credibility_weighted_estimate
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class PMPMResult:
|
|
24
|
+
group_pmpm: float
|
|
25
|
+
book_pmpm: float
|
|
26
|
+
credibility: float
|
|
27
|
+
blended_pmpm: float
|
|
28
|
+
trend_factor: float
|
|
29
|
+
plan_factor: float
|
|
30
|
+
pooling_pmpm: float
|
|
31
|
+
projected_pmpm: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PMPMProjection:
|
|
35
|
+
def __init__(self, *, book_pmpm, claim_trend, exp_midpoint, prosp_midpoint,
|
|
36
|
+
group_pmpm=None, group_claims=None, group_member_months=None,
|
|
37
|
+
group_claim_count=None, credibility=None, full_credibility_claims=1082.0,
|
|
38
|
+
plan_factor=1.0, pooling_pmpm=0.0):
|
|
39
|
+
if group_pmpm is None:
|
|
40
|
+
if group_claims is None or group_member_months is None:
|
|
41
|
+
raise ValueError("supply group_pmpm, or group_claims and group_member_months")
|
|
42
|
+
group_pmpm = ap.pure_premium(group_claims, group_member_months)
|
|
43
|
+
if credibility is None:
|
|
44
|
+
if group_claim_count is None:
|
|
45
|
+
raise ValueError("supply credibility, or group_claim_count to derive it")
|
|
46
|
+
credibility = min(float(ap.limited_fluctuation_z(group_claim_count, full_credibility_claims)), 1.0)
|
|
47
|
+
|
|
48
|
+
tf = float(ap.midpoint_trend_factor(exp_midpoint, prosp_midpoint, claim_trend))
|
|
49
|
+
blended = float(credibility_weighted_estimate(group_pmpm, book_pmpm, credibility))
|
|
50
|
+
projected = blended * tf * plan_factor + pooling_pmpm * tf
|
|
51
|
+
self.result = PMPMResult(
|
|
52
|
+
group_pmpm=float(group_pmpm), book_pmpm=float(book_pmpm),
|
|
53
|
+
credibility=float(credibility), blended_pmpm=blended, trend_factor=tf,
|
|
54
|
+
plan_factor=float(plan_factor), pooling_pmpm=float(pooling_pmpm),
|
|
55
|
+
projected_pmpm=float(projected))
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def projected_pmpm(self) -> float:
|
|
59
|
+
return self.result.projected_pmpm
|
|
60
|
+
|
|
61
|
+
def claims(self, membership, seasonal_factors=None):
|
|
62
|
+
"""Projected claim dollars by prospective month."""
|
|
63
|
+
E = np.asarray(membership, float)
|
|
64
|
+
s = np.ones_like(E) if seasonal_factors is None else np.asarray(seasonal_factors, float)
|
|
65
|
+
return self.result.projected_pmpm * E * s
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def project_pmpm(**kwargs) -> PMPMResult:
|
|
69
|
+
"""Functional form: returns the :class:`PMPMResult`."""
|
|
70
|
+
return PMPMProjection(**kwargs).result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Premium roll-forward -- the stored premium projected by known factors.
|
|
3
|
+
======================================================================
|
|
4
|
+
Premiums come from the database; they are NOT rebuilt from loss experience (that is
|
|
5
|
+
`ratingmodels`). This just rolls the stored figure forward:
|
|
6
|
+
|
|
7
|
+
projected_pmpm = (current_premium / current_member_months)
|
|
8
|
+
* (1 + rate_action) * (1 + plan_change)
|
|
9
|
+
|
|
10
|
+
Premium is level per member-month (it earns evenly), so `.premium(membership)`
|
|
11
|
+
scales by membership with no seasonal shape -- unlike claims.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class PremiumResult:
|
|
20
|
+
current_pmpm: float
|
|
21
|
+
rate_action: float
|
|
22
|
+
plan_change: float
|
|
23
|
+
projected_pmpm: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PremiumRollforward:
|
|
27
|
+
def __init__(self, *, current_premium, current_member_months,
|
|
28
|
+
rate_action=0.0, plan_change=0.0):
|
|
29
|
+
current_pmpm = current_premium / current_member_months
|
|
30
|
+
projected = current_pmpm * (1.0 + rate_action) * (1.0 + plan_change)
|
|
31
|
+
self.result = PremiumResult(float(current_pmpm), float(rate_action),
|
|
32
|
+
float(plan_change), float(projected))
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def projected_pmpm(self) -> float:
|
|
36
|
+
return self.result.projected_pmpm
|
|
37
|
+
|
|
38
|
+
def premium(self, membership):
|
|
39
|
+
"""Projected premium dollars by prospective month (level per member-month)."""
|
|
40
|
+
return self.result.projected_pmpm * np.asarray(membership, float)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def roll_forward_premium(**kwargs) -> PremiumResult:
|
|
44
|
+
"""Functional form: returns the :class:`PremiumResult`."""
|
|
45
|
+
return PremiumRollforward(**kwargs).result
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: projectionmodels
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Forward projection (budgeting) for a book of business: premium roll-forward and credibility-blended, trended, seasonalized claims. Part of the OpenActuarial ecosystem.
|
|
5
|
+
Project-URL: Homepage, https://github.com/OpenActuarial/projectionmodels
|
|
6
|
+
Project-URL: Documentation, https://openactuarial.org/projectionmodels.html
|
|
7
|
+
Project-URL: Repository, https://github.com/OpenActuarial/projectionmodels
|
|
8
|
+
Project-URL: Issues, https://github.com/OpenActuarial/projectionmodels/issues
|
|
9
|
+
Author: Michael Bryant
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: PMPM,actuarial,budgeting,forecasting,insurance,loss ratio,projection
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: actuarialpy>=0.40
|
|
28
|
+
Requires-Dist: numpy>=1.23
|
|
29
|
+
Requires-Dist: pandas>=1.5
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1; extra == 'dev'
|
|
32
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# projectionmodels
|
|
38
|
+
|
|
39
|
+
[](https://github.com/OpenActuarial/projectionmodels/actions/workflows/ci.yml) [](https://pypi.org/project/projectionmodels/)
|
|
40
|
+
|
|
41
|
+
Forward projection (budgeting) for a book of business — part of the
|
|
42
|
+
[OpenActuarial](https://openactuarial.org) ecosystem.
|
|
43
|
+
|
|
44
|
+
`ratingmodels` builds a price from experience. `projectionmodels` does the opposite
|
|
45
|
+
direction: it takes the book **as it is** — stored premiums, historical claims, and
|
|
46
|
+
membership assumptions — and rolls it forward for planning and budgeting. It sits
|
|
47
|
+
beside `ratingmodels` and `lossmodels` on top of the `actuarialpy` primitives layer,
|
|
48
|
+
and depends only **downward** on `actuarialpy` — never sideways on another workflow
|
|
49
|
+
package.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install projectionmodels
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Layers
|
|
58
|
+
|
|
59
|
+
| object | role |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `PMPMProjection` | credibility-blended, trended, plan-adjusted claims PMPM with a pooling load |
|
|
62
|
+
| `PremiumRollforward` | stored premium rolled forward by rate action and plan change |
|
|
63
|
+
| `GroupProjection` | one group: premium + claims + renewal weighting — the unit you loop over the book |
|
|
64
|
+
| `BookProjection` | aggregate in-force renewals + new business into the book budget |
|
|
65
|
+
|
|
66
|
+
Each class computes on construction and exposes a frozen `*Result` dataclass via
|
|
67
|
+
`.result`; a lowercase functional form (`project_group`, `project_book`, …) returns
|
|
68
|
+
the result directly.
|
|
69
|
+
|
|
70
|
+
## What it computes
|
|
71
|
+
|
|
72
|
+
**Premium** is a stored value rolled forward (not rebuilt from experience), level
|
|
73
|
+
per member-month:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
prem_pmpm* = (current_premium / current_member_months) · (1 + rate_action) · (1 + plan_change)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Claims** are the actuarial piece — a credibility blend of the group's own PMPM and
|
|
80
|
+
the book PMPM, trended, plan-adjusted, and seasonalised onto the given membership:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Z = limited-fluctuation credibility from the group's claim count
|
|
84
|
+
blended = Z · group_pmpm + (1 − Z) · book_pmpm
|
|
85
|
+
projected = blended · trend · plan_factor + pooling_pmpm · trend
|
|
86
|
+
claims_m = projected · membership_m · seasonal_m
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Renewal probability** is supplied per group (e.g. from underwriting) via
|
|
90
|
+
`renewal_prob` — it is an input, not something this package models. It weights
|
|
91
|
+
premium and claims equally (a lapsed group books neither), so the projected loss
|
|
92
|
+
ratio is unaffected. New business is the same `GroupProjection` with
|
|
93
|
+
`group_pmpm = book_pmpm`, `credibility = 0`, and `renewal_prob = close_ratio`.
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import numpy as np, pandas as pd
|
|
99
|
+
from projectionmodels import GroupProjection, BookProjection
|
|
100
|
+
|
|
101
|
+
g = GroupProjection(
|
|
102
|
+
prospective_membership=np.full(12, 1900.0), seasonal_factors=season_factors,
|
|
103
|
+
current_premium=4_500_000, current_member_months=21_600,
|
|
104
|
+
rate_action=0.06, plan_change=-0.02,
|
|
105
|
+
book_pmpm=180.0, claim_trend=0.06,
|
|
106
|
+
exp_midpoint=pd.Timestamp("2025-07-01"), prosp_midpoint=pd.Timestamp("2027-07-01"),
|
|
107
|
+
group_claims=3_800_000, group_member_months=21_600, group_claim_count=6_000,
|
|
108
|
+
pooling_pmpm=8.0, renewal_prob=0.90) # renewal likelihood from underwriting
|
|
109
|
+
|
|
110
|
+
book = BookProjection([g, ...], labels=["GroupA", ...])
|
|
111
|
+
book.loss_ratio # expected book loss ratio
|
|
112
|
+
book.by_group # per-group expected premium / claims / LR
|
|
113
|
+
book.monthly # book premium & claims by month
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Built on `actuarialpy`
|
|
117
|
+
|
|
118
|
+
This package adds no primitives of its own — it composes existing `actuarialpy`
|
|
119
|
+
primitives and depends only downward:
|
|
120
|
+
|
|
121
|
+
- `credibility_weighted_estimate` — the credibility blend, `Z·group + (1−Z)·book`
|
|
122
|
+
- `midpoint_trend_factor` — trend to the prospective midpoint
|
|
123
|
+
- `seasonality_factors` / `apply_seasonality` — monthly seasonality
|
|
124
|
+
- `pure_premium` — PMPM
|
|
125
|
+
|
|
126
|
+
`pool_losses` / `excess_over_threshold` in `actuarialpy` cap large claims when you
|
|
127
|
+
derive the pooling PMPM upstream.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
projectionmodels/__init__.py,sha256=ZU3bormnPaifAbIDpKc_-3XhdovhpbdLJ6a690brwYk,2485
|
|
2
|
+
projectionmodels/book.py,sha256=eUSmccCLoHnUJr0roZr4bdrQTmyup6j74kaYMIKkbUM,2825
|
|
3
|
+
projectionmodels/group.py,sha256=B0PzQCJZvYlp9iCax8vJ2Z4ez3tRVVjIXUCFglAOPBY,5246
|
|
4
|
+
projectionmodels/pmpm.py,sha256=RCMnET4E-SnO-ofMT4V--vgxiunPcwfHvQjHV6_RnqY,3024
|
|
5
|
+
projectionmodels/premium.py,sha256=Hx3lVm5XnTqSKB9MQ9ljpACy-CI7CnJQQMw-uhXWiZo,1687
|
|
6
|
+
projectionmodels-0.1.0.dist-info/METADATA,sha256=SNoy0JitwJu50mwsK8bxKngtUC8WUnnHQnov0QQ1_8g,5631
|
|
7
|
+
projectionmodels-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
projectionmodels-0.1.0.dist-info/licenses/LICENSE,sha256=2hvamn9mVK3WmUG9GyH0XLwvFoEkowbxsu4AZeCgf-g,1070
|
|
9
|
+
projectionmodels-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenActuarial
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|