ratingmodels 0.1.0__tar.gz
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.
- ratingmodels-0.1.0/.gitignore +66 -0
- ratingmodels-0.1.0/CHANGELOG.md +55 -0
- ratingmodels-0.1.0/LICENSE +21 -0
- ratingmodels-0.1.0/PKG-INFO +230 -0
- ratingmodels-0.1.0/README.md +198 -0
- ratingmodels-0.1.0/docs/api/base_loading.md +5 -0
- ratingmodels-0.1.0/docs/api/buildup.md +3 -0
- ratingmodels-0.1.0/docs/api/credibility.md +3 -0
- ratingmodels-0.1.0/docs/api/decomposition.md +3 -0
- ratingmodels-0.1.0/docs/api/indication.md +5 -0
- ratingmodels-0.1.0/docs/api/rates.md +5 -0
- ratingmodels-0.1.0/docs/api/relativity.md +3 -0
- ratingmodels-0.1.0/docs/api/renewal.md +5 -0
- ratingmodels-0.1.0/docs/api/trend.md +3 -0
- ratingmodels-0.1.0/docs/changelog.md +55 -0
- ratingmodels-0.1.0/docs/index.md +66 -0
- ratingmodels-0.1.0/docs/theory.md +227 -0
- ratingmodels-0.1.0/examples/quickstart.py +154 -0
- ratingmodels-0.1.0/mkdocs.yml +68 -0
- ratingmodels-0.1.0/pyproject.toml +58 -0
- ratingmodels-0.1.0/src/ratingmodels/__init__.py +157 -0
- ratingmodels-0.1.0/src/ratingmodels/_utils.py +53 -0
- ratingmodels-0.1.0/src/ratingmodels/base_rate.py +151 -0
- ratingmodels-0.1.0/src/ratingmodels/blend.py +20 -0
- ratingmodels-0.1.0/src/ratingmodels/buildup.py +253 -0
- ratingmodels-0.1.0/src/ratingmodels/constraints.py +58 -0
- ratingmodels-0.1.0/src/ratingmodels/credibility.py +129 -0
- ratingmodels-0.1.0/src/ratingmodels/datasets.py +78 -0
- ratingmodels-0.1.0/src/ratingmodels/decomposition.py +102 -0
- ratingmodels-0.1.0/src/ratingmodels/experience_rate.py +131 -0
- ratingmodels-0.1.0/src/ratingmodels/indication.py +156 -0
- ratingmodels-0.1.0/src/ratingmodels/loading.py +126 -0
- ratingmodels-0.1.0/src/ratingmodels/manual_rate.py +110 -0
- ratingmodels-0.1.0/src/ratingmodels/relativity.py +300 -0
- ratingmodels-0.1.0/src/ratingmodels/renewal.py +84 -0
- ratingmodels-0.1.0/src/ratingmodels/trend.py +74 -0
- ratingmodels-0.1.0/tests/test_ratingmodels.py +590 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Python build artifacts
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
*.egg
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
ENV/
|
|
18
|
+
|
|
19
|
+
# Test and coverage outputs
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
coverage.xml
|
|
23
|
+
htmlcov/
|
|
24
|
+
.tox/
|
|
25
|
+
.nox/
|
|
26
|
+
|
|
27
|
+
# Type checker / linter caches
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.pyright/
|
|
30
|
+
.ruff_cache/
|
|
31
|
+
|
|
32
|
+
# Jupyter notebooks
|
|
33
|
+
.ipynb_checkpoints/
|
|
34
|
+
|
|
35
|
+
# Local environment files
|
|
36
|
+
.env
|
|
37
|
+
.env.*
|
|
38
|
+
*.local
|
|
39
|
+
|
|
40
|
+
# IDE / editor files
|
|
41
|
+
.vscode/
|
|
42
|
+
.idea/
|
|
43
|
+
*.swp
|
|
44
|
+
*.swo
|
|
45
|
+
.DS_Store
|
|
46
|
+
|
|
47
|
+
# Logs
|
|
48
|
+
*.log
|
|
49
|
+
|
|
50
|
+
# Temporary files
|
|
51
|
+
tmp/
|
|
52
|
+
temp/
|
|
53
|
+
*.tmp
|
|
54
|
+
|
|
55
|
+
# Data files generated locally
|
|
56
|
+
*.csv
|
|
57
|
+
*.xlsx
|
|
58
|
+
*.xls
|
|
59
|
+
*.parquet
|
|
60
|
+
*.feather
|
|
61
|
+
*.pkl
|
|
62
|
+
*.pickle
|
|
63
|
+
|
|
64
|
+
# Keep example datasets only if intentionally committed
|
|
65
|
+
# !examples/*.csv
|
|
66
|
+
# !examples/*.xlsx
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Credibility consolidated into `actuarialpy`.** Trend and credibility are
|
|
13
|
+
shared ecosystem primitives; `actuarialpy` is the home for credibility. The
|
|
14
|
+
credibility math is no longer duplicated here -- `ratingmodels.credibility`
|
|
15
|
+
and `blend` are thin adapters over `actuarialpy` (`full_credibility_claims`,
|
|
16
|
+
`limited_fluctuation_z`, `credibility_weighted_estimate`, and
|
|
17
|
+
`BuhlmannStraub.from_frame`). The public `ratingmodels` API and results are
|
|
18
|
+
unchanged. This removes the risk of the two Bühlmann-Straub implementations
|
|
19
|
+
drifting apart, and `actuarialpy` now uses the general unbiased estimator
|
|
20
|
+
(handling unequal period counts). Adds a dependency on `actuarialpy>=0.32.0`
|
|
21
|
+
and drops the direct `scipy` dependency.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Credibility** (`credibility`): `full_credibility_standard`,
|
|
25
|
+
`limited_fluctuation_credibility`, `buhlmann_credibility`, and empirical
|
|
26
|
+
`buhlmann_straub` with exposure weights.
|
|
27
|
+
- **Trend** (`trend`): midpoint-to-midpoint trend factors, date helpers, and
|
|
28
|
+
utilization / unit-cost decomposition.
|
|
29
|
+
- **Manual rating** (`manual_rate`): `ManualRate`, `manual_pmpm`,
|
|
30
|
+
`aggregate_demographic_factor`.
|
|
31
|
+
- **Experience rating** (`experience_rate`): `ExperienceRate`, `pool_claims`,
|
|
32
|
+
`expected_excess_charge`.
|
|
33
|
+
- **Rate build-up** (`buildup`): typed steps (`start`, `multiply`, `add`,
|
|
34
|
+
`segment_multiply`, `checkpoint`), an `evaluate` engine and `BuildUp` fluent
|
|
35
|
+
builder producing a reconciling breakdown, and `participation_blend` /
|
|
36
|
+
`combine_streams` for combining par/non-par and medical+drug streams.
|
|
37
|
+
`ManualRate` is reimplemented as a thin layer over the engine and gains
|
|
38
|
+
`breakdown()` / `steps()`.
|
|
39
|
+
- **Base rate & off-balance** (`base_rate`): `base_rate_from_experience`,
|
|
40
|
+
`average_relativity`, `off_balance_factor`, `rebalance_base_rate`.
|
|
41
|
+
- **Retention & loading** (`loading`): `RetentionLoad` (fundamental insurance
|
|
42
|
+
equation gross-up), `gross_rate`, `permissible_loss_ratio`. `ManualRate`,
|
|
43
|
+
`ExperienceRate`, and `RateIndication` accept an optional `retention` that
|
|
44
|
+
overrides the single-loss-ratio path with the full expense/profit build-up.
|
|
45
|
+
- **Blending & indication** (`blend`, `indication`): `blend` and the
|
|
46
|
+
`RateIndication` orchestrator with build-up and loss-ratio methods.
|
|
47
|
+
- **Rate-change decomposition** (`decomposition`): `decompose_rate_change`
|
|
48
|
+
with multiplicative and percentage-point contributions and an explicit
|
|
49
|
+
residual.
|
|
50
|
+
- **GLM relativities** (`relativity`): `GLMRelativities` (Poisson / Gamma /
|
|
51
|
+
Tweedie via in-package IRLS), `FactorTable`, `one_way_relativities`.
|
|
52
|
+
- **Constraints & renewal** (`constraints`, `renewal`): caps, floors, banding,
|
|
53
|
+
rounding, corridors; `renew` and `member_level_renewal`.
|
|
54
|
+
- **Synthetic data** (`datasets`): `sample_claims`, `sample_rating_data`.
|
|
55
|
+
- Full pytest suite (54 tests) and MkDocs Material documentation.
|
|
@@ -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.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ratingmodels
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Actuarial pricing and rate-indication tools for experience-rated insurance portfolios.
|
|
5
|
+
Project-URL: Homepage, https://github.com/OpenActuarial/ratingmodels
|
|
6
|
+
Project-URL: Documentation, https://openactuarial.github.io/ratingmodels/
|
|
7
|
+
Project-URL: Repository, https://github.com/OpenActuarial/ratingmodels
|
|
8
|
+
Project-URL: Issues, https://github.com/OpenActuarial/ratingmodels/issues
|
|
9
|
+
Author: OpenActuarial
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: actuarial,credibility,glm,health-insurance,insurance,pricing,rate-indication,rating
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: actuarialpy>=0.32.0
|
|
23
|
+
Requires-Dist: numpy>=1.22
|
|
24
|
+
Requires-Dist: pandas>=1.4
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
28
|
+
Provides-Extra: docs
|
|
29
|
+
Requires-Dist: mkdocs-material>=9; extra == 'docs'
|
|
30
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# ratingmodels
|
|
34
|
+
|
|
35
|
+
**Actuarial pricing and rate-indication tools for experience-rated insurance portfolios.**
|
|
36
|
+
|
|
37
|
+
Part of the [OpenActuarial](https://github.com/OpenActuarial) ecosystem.
|
|
38
|
+
|
|
39
|
+
`ratingmodels` covers the group rating workflow — the step that turns experience
|
|
40
|
+
analysis and loss modeling into an actual rate. Where the rest of the ecosystem
|
|
41
|
+
explains *what happened* and models *loss risk*, this package answers the central
|
|
42
|
+
pricing question: **what rate should we charge, and why did it change?**
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
actuarialpy -> experience analysis (PMPM, loss ratios, trend, completion)
|
|
46
|
+
ratingmodels -> pricing and rate indications <-- this package
|
|
47
|
+
lossmodels -> loss-distribution modeling
|
|
48
|
+
risksim -> portfolio Monte Carlo simulation
|
|
49
|
+
extremeloss -> extreme-value tail estimation
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What it does
|
|
53
|
+
|
|
54
|
+
- **Credibility** — limited fluctuation (square-root rule), Bühlmann, and
|
|
55
|
+
empirical Bühlmann-Straub with exposure weights.
|
|
56
|
+
- **Trend** — midpoint-to-midpoint factors; utilization / unit-cost split.
|
|
57
|
+
- **Manual rating** — base rate × relativities, loaded to a charged rate.
|
|
58
|
+
- **Experience rating** — pooling of large claims, trend, pooling charge,
|
|
59
|
+
benefit/demographic adjustments, loading.
|
|
60
|
+
- **Rate build-up** — an ordered, auditable evaluator (multiply / add-dollar /
|
|
61
|
+
segment-conditional) with labeled subtotals and a reconciling breakdown, plus
|
|
62
|
+
par/non-par participation blending and medical+drug combining. Supplies the
|
|
63
|
+
*grammar* of a manual build-up; the factor values stay yours.
|
|
64
|
+
- **Base rate & off-balance** — indicated base loss cost from book experience
|
|
65
|
+
(base × relativities reproduces book losses); off-balance correction and
|
|
66
|
+
base rebalancing when relativities are revised.
|
|
67
|
+
- **Retention & gross-up** — charged rate from the fundamental insurance
|
|
68
|
+
equation (loss & LAE, flat fixed expense, percent-of-premium loads, profit),
|
|
69
|
+
with the target loss ratio as an *output*, not an input.
|
|
70
|
+
- **Blending & indication** — credibility-weighted blend; build-up and
|
|
71
|
+
loss-ratio indication methods.
|
|
72
|
+
- **Rate-change decomposition** — multiplicative and percentage-point
|
|
73
|
+
contribution-to-change with an explicit residual.
|
|
74
|
+
- **GLM relativities** — Poisson / Gamma / Tweedie GLMs fit by IRLS, so factors
|
|
75
|
+
are estimated *jointly* (correcting for correlation between rating variables)
|
|
76
|
+
rather than one-way. No statsmodels dependency — the IRLS is in-package.
|
|
77
|
+
- **Constraints & renewal** — rate caps/floors, banding, rounding, corridors,
|
|
78
|
+
and member-level re-rating.
|
|
79
|
+
|
|
80
|
+
Dependencies are `numpy`, `pandas`, and `actuarialpy` (which supplies the shared credibility primitives; see below).
|
|
81
|
+
|
|
82
|
+
## Install
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install ratingmodels
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
From source:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone https://github.com/OpenActuarial/ratingmodels
|
|
92
|
+
cd ratingmodels
|
|
93
|
+
pip install -e ".[dev]"
|
|
94
|
+
pytest
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Quick start
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import ratingmodels as rm
|
|
101
|
+
|
|
102
|
+
# --- experience side -------------------------------------------------------
|
|
103
|
+
capped, excess = rm.pool_claims(group_claims, pooling_point=250_000)
|
|
104
|
+
exp = rm.ExperienceRate(
|
|
105
|
+
incurred_claims=4_200_000,
|
|
106
|
+
exposure=96_000, # member-months
|
|
107
|
+
trend_annual=0.075,
|
|
108
|
+
trend_years=1.5, # experience midpoint -> rating midpoint
|
|
109
|
+
pooled_excess=excess,
|
|
110
|
+
pooling_charge_pmpm=4.00,
|
|
111
|
+
target_loss_ratio=0.85,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# --- manual side -----------------------------------------------------------
|
|
115
|
+
man = rm.ManualRate(
|
|
116
|
+
base_pmpm=480,
|
|
117
|
+
factors={"area": 1.05, "industry": 0.97, "tier": 1.10},
|
|
118
|
+
target_loss_ratio=0.85,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# --- credibility and indication -------------------------------------------
|
|
122
|
+
z = rm.limited_fluctuation_credibility(n=96_000, n_full=120_000)
|
|
123
|
+
|
|
124
|
+
ind = rm.RateIndication(
|
|
125
|
+
experience_claims_pmpm=exp.claims_pmpm(),
|
|
126
|
+
manual_claims_pmpm=man.claims_pmpm(),
|
|
127
|
+
credibility=z,
|
|
128
|
+
current_rate=560,
|
|
129
|
+
target_loss_ratio=0.85,
|
|
130
|
+
trend_total_factor=exp.trend_factor(),
|
|
131
|
+
benefit_factor=1.00,
|
|
132
|
+
demographic_factor=1.01,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
print(f"indicated rate : {ind.indicated_rate():.2f}")
|
|
136
|
+
print(f"indicated change : {ind.indicated_rate_change():+.2%}")
|
|
137
|
+
|
|
138
|
+
# why did the rate move?
|
|
139
|
+
print(ind.rate_change_decomposition().to_frame())
|
|
140
|
+
|
|
141
|
+
# apply a renewal cap
|
|
142
|
+
action = rm.renew(current_rate=560, indicated_rate=ind.indicated_rate(), cap=0.15)
|
|
143
|
+
print(f"proposed (capped): {action.proposed_rate:.2f} ({action.proposed_change:+.2%})")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Rate build-up with an audit trail
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import ratingmodels as rm
|
|
150
|
+
|
|
151
|
+
med_par = rm.evaluate([
|
|
152
|
+
rm.start("Par Base Claim Cost", 941.63),
|
|
153
|
+
rm.add("$30 specialist copay", -11.44),
|
|
154
|
+
rm.multiply("Rating Region", 1.083),
|
|
155
|
+
rm.checkpoint("Medical Par Base Claim Cost"),
|
|
156
|
+
])
|
|
157
|
+
med_par.value # final running total
|
|
158
|
+
med_par.breakdown # DataFrame: step, operation, label, operand, running_total
|
|
159
|
+
|
|
160
|
+
# blend in-/out-of-network, then add the drug stream
|
|
161
|
+
med = rm.participation_blend(med_par.value, nonpar=1478.56, participation_rate=0.90)
|
|
162
|
+
total = rm.combine_streams({"Medical": med, "Drug": 323.67})
|
|
163
|
+
total.value # feeds into trend / credibility / retention
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The package supplies the build-up *grammar*; you supply the factor values
|
|
167
|
+
(cost-sharing, age/sex, area, ...) from your filed tables. `ManualRate` is a
|
|
168
|
+
thin shortcut over this engine, so `ManualRate(...).breakdown()` returns the
|
|
169
|
+
same audit trail.
|
|
170
|
+
|
|
171
|
+
### Base rate and retention
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import ratingmodels as rm
|
|
175
|
+
import pandas as pd
|
|
176
|
+
|
|
177
|
+
# indicated base loss cost from book experience (off-balance method)
|
|
178
|
+
book = pd.DataFrame({
|
|
179
|
+
"exposure": [24_000, 18_000, 30_000, 24_000],
|
|
180
|
+
"area": [1.00, 1.20, 0.90, 1.05],
|
|
181
|
+
"tier": [1.00, 1.10, 0.95, 1.25],
|
|
182
|
+
"loss": [11_750_000, 11_400_000, 12_300_000, 15_200_000],
|
|
183
|
+
})
|
|
184
|
+
base = rm.base_rate_from_experience(book, "exposure", "loss",
|
|
185
|
+
factor_cols=["area", "tier"])
|
|
186
|
+
base.base_loss_cost # average loss cost / average relativity
|
|
187
|
+
|
|
188
|
+
# gross claims up to a charged rate; the loss ratio falls out
|
|
189
|
+
retention = rm.RetentionLoad.from_items(
|
|
190
|
+
fixed_expense_pmpm=22.0,
|
|
191
|
+
variable_items={"commission": 0.03, "premium_tax": 0.023, "aca_fees": 0.005},
|
|
192
|
+
profit_margin=0.03,
|
|
193
|
+
)
|
|
194
|
+
retention.gross_rate(540.0) # charged rate
|
|
195
|
+
retention.implied_loss_ratio(540.0) # target loss ratio (an output)
|
|
196
|
+
|
|
197
|
+
# rebalance the base when relativities are revised (hold level, then +8%)
|
|
198
|
+
rm.rebalance_base_rate(current_base=base.base_loss_cost,
|
|
199
|
+
current_avg_relativity=1.0928, new_avg_relativity=1.12,
|
|
200
|
+
overall_change=0.08)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### GLM relativities
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import ratingmodels as rm
|
|
207
|
+
from ratingmodels.datasets import sample_rating_data
|
|
208
|
+
|
|
209
|
+
df = sample_rating_data(n=20_000)
|
|
210
|
+
model = rm.GLMRelativities(family="poisson").fit(
|
|
211
|
+
df, response="claims", predictors=["area", "industry", "tier"],
|
|
212
|
+
exposure="exposure", # enters as a log offset
|
|
213
|
+
base_levels={"area": "A"}, # optional; defaults to modal level
|
|
214
|
+
)
|
|
215
|
+
print(model.base_value_) # fitted base frequency
|
|
216
|
+
print(model.relativities_["industry"]) # relativity per level, base = 1.0
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Scope and honest limitations
|
|
220
|
+
|
|
221
|
+
This is a modeling and workflow toolkit, not filed rate software. It does not
|
|
222
|
+
manage rate filings, store filed factor tables with effective dating, or enforce
|
|
223
|
+
state-specific rating rules. The pooling-charge helper is a simple group-level
|
|
224
|
+
estimate; a production charge is normally derived book-wide or from an EVT tail
|
|
225
|
+
model (see `extremeloss`). All bundled data in `ratingmodels.datasets` is
|
|
226
|
+
synthetic and carries no assumptions.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# ratingmodels
|
|
2
|
+
|
|
3
|
+
**Actuarial pricing and rate-indication tools for experience-rated insurance portfolios.**
|
|
4
|
+
|
|
5
|
+
Part of the [OpenActuarial](https://github.com/OpenActuarial) ecosystem.
|
|
6
|
+
|
|
7
|
+
`ratingmodels` covers the group rating workflow — the step that turns experience
|
|
8
|
+
analysis and loss modeling into an actual rate. Where the rest of the ecosystem
|
|
9
|
+
explains *what happened* and models *loss risk*, this package answers the central
|
|
10
|
+
pricing question: **what rate should we charge, and why did it change?**
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
actuarialpy -> experience analysis (PMPM, loss ratios, trend, completion)
|
|
14
|
+
ratingmodels -> pricing and rate indications <-- this package
|
|
15
|
+
lossmodels -> loss-distribution modeling
|
|
16
|
+
risksim -> portfolio Monte Carlo simulation
|
|
17
|
+
extremeloss -> extreme-value tail estimation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
- **Credibility** — limited fluctuation (square-root rule), Bühlmann, and
|
|
23
|
+
empirical Bühlmann-Straub with exposure weights.
|
|
24
|
+
- **Trend** — midpoint-to-midpoint factors; utilization / unit-cost split.
|
|
25
|
+
- **Manual rating** — base rate × relativities, loaded to a charged rate.
|
|
26
|
+
- **Experience rating** — pooling of large claims, trend, pooling charge,
|
|
27
|
+
benefit/demographic adjustments, loading.
|
|
28
|
+
- **Rate build-up** — an ordered, auditable evaluator (multiply / add-dollar /
|
|
29
|
+
segment-conditional) with labeled subtotals and a reconciling breakdown, plus
|
|
30
|
+
par/non-par participation blending and medical+drug combining. Supplies the
|
|
31
|
+
*grammar* of a manual build-up; the factor values stay yours.
|
|
32
|
+
- **Base rate & off-balance** — indicated base loss cost from book experience
|
|
33
|
+
(base × relativities reproduces book losses); off-balance correction and
|
|
34
|
+
base rebalancing when relativities are revised.
|
|
35
|
+
- **Retention & gross-up** — charged rate from the fundamental insurance
|
|
36
|
+
equation (loss & LAE, flat fixed expense, percent-of-premium loads, profit),
|
|
37
|
+
with the target loss ratio as an *output*, not an input.
|
|
38
|
+
- **Blending & indication** — credibility-weighted blend; build-up and
|
|
39
|
+
loss-ratio indication methods.
|
|
40
|
+
- **Rate-change decomposition** — multiplicative and percentage-point
|
|
41
|
+
contribution-to-change with an explicit residual.
|
|
42
|
+
- **GLM relativities** — Poisson / Gamma / Tweedie GLMs fit by IRLS, so factors
|
|
43
|
+
are estimated *jointly* (correcting for correlation between rating variables)
|
|
44
|
+
rather than one-way. No statsmodels dependency — the IRLS is in-package.
|
|
45
|
+
- **Constraints & renewal** — rate caps/floors, banding, rounding, corridors,
|
|
46
|
+
and member-level re-rating.
|
|
47
|
+
|
|
48
|
+
Dependencies are `numpy`, `pandas`, and `actuarialpy` (which supplies the shared credibility primitives; see below).
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install ratingmodels
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From source:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/OpenActuarial/ratingmodels
|
|
60
|
+
cd ratingmodels
|
|
61
|
+
pip install -e ".[dev]"
|
|
62
|
+
pytest
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import ratingmodels as rm
|
|
69
|
+
|
|
70
|
+
# --- experience side -------------------------------------------------------
|
|
71
|
+
capped, excess = rm.pool_claims(group_claims, pooling_point=250_000)
|
|
72
|
+
exp = rm.ExperienceRate(
|
|
73
|
+
incurred_claims=4_200_000,
|
|
74
|
+
exposure=96_000, # member-months
|
|
75
|
+
trend_annual=0.075,
|
|
76
|
+
trend_years=1.5, # experience midpoint -> rating midpoint
|
|
77
|
+
pooled_excess=excess,
|
|
78
|
+
pooling_charge_pmpm=4.00,
|
|
79
|
+
target_loss_ratio=0.85,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# --- manual side -----------------------------------------------------------
|
|
83
|
+
man = rm.ManualRate(
|
|
84
|
+
base_pmpm=480,
|
|
85
|
+
factors={"area": 1.05, "industry": 0.97, "tier": 1.10},
|
|
86
|
+
target_loss_ratio=0.85,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# --- credibility and indication -------------------------------------------
|
|
90
|
+
z = rm.limited_fluctuation_credibility(n=96_000, n_full=120_000)
|
|
91
|
+
|
|
92
|
+
ind = rm.RateIndication(
|
|
93
|
+
experience_claims_pmpm=exp.claims_pmpm(),
|
|
94
|
+
manual_claims_pmpm=man.claims_pmpm(),
|
|
95
|
+
credibility=z,
|
|
96
|
+
current_rate=560,
|
|
97
|
+
target_loss_ratio=0.85,
|
|
98
|
+
trend_total_factor=exp.trend_factor(),
|
|
99
|
+
benefit_factor=1.00,
|
|
100
|
+
demographic_factor=1.01,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
print(f"indicated rate : {ind.indicated_rate():.2f}")
|
|
104
|
+
print(f"indicated change : {ind.indicated_rate_change():+.2%}")
|
|
105
|
+
|
|
106
|
+
# why did the rate move?
|
|
107
|
+
print(ind.rate_change_decomposition().to_frame())
|
|
108
|
+
|
|
109
|
+
# apply a renewal cap
|
|
110
|
+
action = rm.renew(current_rate=560, indicated_rate=ind.indicated_rate(), cap=0.15)
|
|
111
|
+
print(f"proposed (capped): {action.proposed_rate:.2f} ({action.proposed_change:+.2%})")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Rate build-up with an audit trail
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import ratingmodels as rm
|
|
118
|
+
|
|
119
|
+
med_par = rm.evaluate([
|
|
120
|
+
rm.start("Par Base Claim Cost", 941.63),
|
|
121
|
+
rm.add("$30 specialist copay", -11.44),
|
|
122
|
+
rm.multiply("Rating Region", 1.083),
|
|
123
|
+
rm.checkpoint("Medical Par Base Claim Cost"),
|
|
124
|
+
])
|
|
125
|
+
med_par.value # final running total
|
|
126
|
+
med_par.breakdown # DataFrame: step, operation, label, operand, running_total
|
|
127
|
+
|
|
128
|
+
# blend in-/out-of-network, then add the drug stream
|
|
129
|
+
med = rm.participation_blend(med_par.value, nonpar=1478.56, participation_rate=0.90)
|
|
130
|
+
total = rm.combine_streams({"Medical": med, "Drug": 323.67})
|
|
131
|
+
total.value # feeds into trend / credibility / retention
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The package supplies the build-up *grammar*; you supply the factor values
|
|
135
|
+
(cost-sharing, age/sex, area, ...) from your filed tables. `ManualRate` is a
|
|
136
|
+
thin shortcut over this engine, so `ManualRate(...).breakdown()` returns the
|
|
137
|
+
same audit trail.
|
|
138
|
+
|
|
139
|
+
### Base rate and retention
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import ratingmodels as rm
|
|
143
|
+
import pandas as pd
|
|
144
|
+
|
|
145
|
+
# indicated base loss cost from book experience (off-balance method)
|
|
146
|
+
book = pd.DataFrame({
|
|
147
|
+
"exposure": [24_000, 18_000, 30_000, 24_000],
|
|
148
|
+
"area": [1.00, 1.20, 0.90, 1.05],
|
|
149
|
+
"tier": [1.00, 1.10, 0.95, 1.25],
|
|
150
|
+
"loss": [11_750_000, 11_400_000, 12_300_000, 15_200_000],
|
|
151
|
+
})
|
|
152
|
+
base = rm.base_rate_from_experience(book, "exposure", "loss",
|
|
153
|
+
factor_cols=["area", "tier"])
|
|
154
|
+
base.base_loss_cost # average loss cost / average relativity
|
|
155
|
+
|
|
156
|
+
# gross claims up to a charged rate; the loss ratio falls out
|
|
157
|
+
retention = rm.RetentionLoad.from_items(
|
|
158
|
+
fixed_expense_pmpm=22.0,
|
|
159
|
+
variable_items={"commission": 0.03, "premium_tax": 0.023, "aca_fees": 0.005},
|
|
160
|
+
profit_margin=0.03,
|
|
161
|
+
)
|
|
162
|
+
retention.gross_rate(540.0) # charged rate
|
|
163
|
+
retention.implied_loss_ratio(540.0) # target loss ratio (an output)
|
|
164
|
+
|
|
165
|
+
# rebalance the base when relativities are revised (hold level, then +8%)
|
|
166
|
+
rm.rebalance_base_rate(current_base=base.base_loss_cost,
|
|
167
|
+
current_avg_relativity=1.0928, new_avg_relativity=1.12,
|
|
168
|
+
overall_change=0.08)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### GLM relativities
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import ratingmodels as rm
|
|
175
|
+
from ratingmodels.datasets import sample_rating_data
|
|
176
|
+
|
|
177
|
+
df = sample_rating_data(n=20_000)
|
|
178
|
+
model = rm.GLMRelativities(family="poisson").fit(
|
|
179
|
+
df, response="claims", predictors=["area", "industry", "tier"],
|
|
180
|
+
exposure="exposure", # enters as a log offset
|
|
181
|
+
base_levels={"area": "A"}, # optional; defaults to modal level
|
|
182
|
+
)
|
|
183
|
+
print(model.base_value_) # fitted base frequency
|
|
184
|
+
print(model.relativities_["industry"]) # relativity per level, base = 1.0
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Scope and honest limitations
|
|
188
|
+
|
|
189
|
+
This is a modeling and workflow toolkit, not filed rate software. It does not
|
|
190
|
+
manage rate filings, store filed factor tables with effective dating, or enforce
|
|
191
|
+
state-specific rating rules. The pooling-charge helper is a simple group-level
|
|
192
|
+
estimate; a production charge is normally derived book-wide or from an EVT tail
|
|
193
|
+
model (see `extremeloss`). All bundled data in `ratingmodels.datasets` is
|
|
194
|
+
synthetic and carries no assumptions.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Credibility consolidated into `actuarialpy`.** Trend and credibility are
|
|
13
|
+
shared ecosystem primitives; `actuarialpy` is the home for credibility. The
|
|
14
|
+
credibility math is no longer duplicated here -- `ratingmodels.credibility`
|
|
15
|
+
and `blend` are thin adapters over `actuarialpy` (`full_credibility_claims`,
|
|
16
|
+
`limited_fluctuation_z`, `credibility_weighted_estimate`, and
|
|
17
|
+
`BuhlmannStraub.from_frame`). The public `ratingmodels` API and results are
|
|
18
|
+
unchanged. This removes the risk of the two Bühlmann-Straub implementations
|
|
19
|
+
drifting apart, and `actuarialpy` now uses the general unbiased estimator
|
|
20
|
+
(handling unequal period counts). Adds a dependency on `actuarialpy>=0.32.0`
|
|
21
|
+
and drops the direct `scipy` dependency.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Credibility** (`credibility`): `full_credibility_standard`,
|
|
25
|
+
`limited_fluctuation_credibility`, `buhlmann_credibility`, and empirical
|
|
26
|
+
`buhlmann_straub` with exposure weights.
|
|
27
|
+
- **Trend** (`trend`): midpoint-to-midpoint trend factors, date helpers, and
|
|
28
|
+
utilization / unit-cost decomposition.
|
|
29
|
+
- **Manual rating** (`manual_rate`): `ManualRate`, `manual_pmpm`,
|
|
30
|
+
`aggregate_demographic_factor`.
|
|
31
|
+
- **Experience rating** (`experience_rate`): `ExperienceRate`, `pool_claims`,
|
|
32
|
+
`expected_excess_charge`.
|
|
33
|
+
- **Rate build-up** (`buildup`): typed steps (`start`, `multiply`, `add`,
|
|
34
|
+
`segment_multiply`, `checkpoint`), an `evaluate` engine and `BuildUp` fluent
|
|
35
|
+
builder producing a reconciling breakdown, and `participation_blend` /
|
|
36
|
+
`combine_streams` for combining par/non-par and medical+drug streams.
|
|
37
|
+
`ManualRate` is reimplemented as a thin layer over the engine and gains
|
|
38
|
+
`breakdown()` / `steps()`.
|
|
39
|
+
- **Base rate & off-balance** (`base_rate`): `base_rate_from_experience`,
|
|
40
|
+
`average_relativity`, `off_balance_factor`, `rebalance_base_rate`.
|
|
41
|
+
- **Retention & loading** (`loading`): `RetentionLoad` (fundamental insurance
|
|
42
|
+
equation gross-up), `gross_rate`, `permissible_loss_ratio`. `ManualRate`,
|
|
43
|
+
`ExperienceRate`, and `RateIndication` accept an optional `retention` that
|
|
44
|
+
overrides the single-loss-ratio path with the full expense/profit build-up.
|
|
45
|
+
- **Blending & indication** (`blend`, `indication`): `blend` and the
|
|
46
|
+
`RateIndication` orchestrator with build-up and loss-ratio methods.
|
|
47
|
+
- **Rate-change decomposition** (`decomposition`): `decompose_rate_change`
|
|
48
|
+
with multiplicative and percentage-point contributions and an explicit
|
|
49
|
+
residual.
|
|
50
|
+
- **GLM relativities** (`relativity`): `GLMRelativities` (Poisson / Gamma /
|
|
51
|
+
Tweedie via in-package IRLS), `FactorTable`, `one_way_relativities`.
|
|
52
|
+
- **Constraints & renewal** (`constraints`, `renewal`): caps, floors, banding,
|
|
53
|
+
rounding, corridors; `renew` and `member_level_renewal`.
|
|
54
|
+
- **Synthetic data** (`datasets`): `sample_claims`, `sample_rating_data`.
|
|
55
|
+
- Full pytest suite (54 tests) and MkDocs Material documentation.
|