bayesian-pricing 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.
- bayesian_pricing-0.1.0/.github/workflows/tests.yml +43 -0
- bayesian_pricing-0.1.0/.gitignore +75 -0
- bayesian_pricing-0.1.0/LICENSE +21 -0
- bayesian_pricing-0.1.0/PKG-INFO +221 -0
- bayesian_pricing-0.1.0/README.md +186 -0
- bayesian_pricing-0.1.0/notebooks/01_hierarchical_frequency_demo.py +611 -0
- bayesian_pricing-0.1.0/pyproject.toml +66 -0
- bayesian_pricing-0.1.0/src/bayesian_pricing/__init__.py +47 -0
- bayesian_pricing-0.1.0/src/bayesian_pricing/_utils.py +89 -0
- bayesian_pricing-0.1.0/src/bayesian_pricing/diagnostics.py +299 -0
- bayesian_pricing-0.1.0/src/bayesian_pricing/frequency.py +534 -0
- bayesian_pricing-0.1.0/src/bayesian_pricing/relativities.py +284 -0
- bayesian_pricing-0.1.0/src/bayesian_pricing/severity.py +395 -0
- bayesian_pricing-0.1.0/tests/conftest.py +120 -0
- bayesian_pricing-0.1.0/tests/test_frequency.py +266 -0
- bayesian_pricing-0.1.0/tests/test_relativities.py +211 -0
- bayesian_pricing-0.1.0/tests/test_severity.py +189 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install package and test dependencies
|
|
25
|
+
run: |
|
|
26
|
+
pip install --upgrade pip
|
|
27
|
+
# Install with PyMC extras for the full test suite
|
|
28
|
+
pip install -e ".[pymc,dev]"
|
|
29
|
+
|
|
30
|
+
- name: Run API tests (no PyMC required)
|
|
31
|
+
run: |
|
|
32
|
+
pytest tests/ -v -m "not pymc" --tb=short
|
|
33
|
+
|
|
34
|
+
- name: Run full test suite with PyMC
|
|
35
|
+
run: |
|
|
36
|
+
pytest tests/ -v --tb=short -x
|
|
37
|
+
# -x stops on first failure. PyMC tests use Pathfinder so should be fast
|
|
38
|
+
# but still take a few minutes per Python version.
|
|
39
|
+
timeout-minutes: 30
|
|
40
|
+
|
|
41
|
+
- name: Check import and version
|
|
42
|
+
run: |
|
|
43
|
+
python -c "import bayesian_pricing; print(bayesian_pricing.__version__)"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.pyd
|
|
7
|
+
|
|
8
|
+
# Distribution / packaging
|
|
9
|
+
.Python
|
|
10
|
+
build/
|
|
11
|
+
develop-eggs/
|
|
12
|
+
dist/
|
|
13
|
+
downloads/
|
|
14
|
+
eggs/
|
|
15
|
+
.eggs/
|
|
16
|
+
lib/
|
|
17
|
+
lib64/
|
|
18
|
+
parts/
|
|
19
|
+
sdist/
|
|
20
|
+
var/
|
|
21
|
+
wheels/
|
|
22
|
+
share/python-wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
MANIFEST
|
|
27
|
+
|
|
28
|
+
# PyInstaller
|
|
29
|
+
*.manifest
|
|
30
|
+
*.spec
|
|
31
|
+
|
|
32
|
+
# Unit test / coverage
|
|
33
|
+
htmlcov/
|
|
34
|
+
.tox/
|
|
35
|
+
.nox/
|
|
36
|
+
.coverage
|
|
37
|
+
.coverage.*
|
|
38
|
+
.cache
|
|
39
|
+
nosetests.xml
|
|
40
|
+
coverage.xml
|
|
41
|
+
*.cover
|
|
42
|
+
*.py,cover
|
|
43
|
+
.hypothesis/
|
|
44
|
+
.pytest_cache/
|
|
45
|
+
cover/
|
|
46
|
+
|
|
47
|
+
# Virtual environments
|
|
48
|
+
.env
|
|
49
|
+
.venv
|
|
50
|
+
env/
|
|
51
|
+
venv/
|
|
52
|
+
ENV/
|
|
53
|
+
env.bak/
|
|
54
|
+
venv.bak/
|
|
55
|
+
|
|
56
|
+
# IDE
|
|
57
|
+
.idea/
|
|
58
|
+
.vscode/
|
|
59
|
+
*.swp
|
|
60
|
+
*.swo
|
|
61
|
+
|
|
62
|
+
# Jupyter
|
|
63
|
+
.ipynb_checkpoints/
|
|
64
|
+
|
|
65
|
+
# uv
|
|
66
|
+
.uv/
|
|
67
|
+
|
|
68
|
+
# mypy
|
|
69
|
+
.mypy_cache/
|
|
70
|
+
.dmypy.json
|
|
71
|
+
dmypy.json
|
|
72
|
+
|
|
73
|
+
# OS
|
|
74
|
+
.DS_Store
|
|
75
|
+
Thumbs.db
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Burning Cost
|
|
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,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bayesian-pricing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hierarchical Bayesian models for insurance pricing thin-data segments
|
|
5
|
+
Project-URL: Homepage, https://github.com/burning-cost/bayesian-pricing
|
|
6
|
+
Project-URL: Repository, https://github.com/burning-cost/bayesian-pricing
|
|
7
|
+
Author-email: Burning Cost <pricing.frontier@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: actuarial,bayesian,credibility,hierarchical,insurance,pricing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: arviz>=0.17
|
|
22
|
+
Requires-Dist: numpy>=1.24
|
|
23
|
+
Requires-Dist: pandas>=2.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: arviz>=0.17; extra == 'dev'
|
|
26
|
+
Requires-Dist: pymc>=5.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Provides-Extra: numpyro
|
|
30
|
+
Requires-Dist: jax>=0.4; extra == 'numpyro'
|
|
31
|
+
Requires-Dist: numpyro>=0.13; extra == 'numpyro'
|
|
32
|
+
Provides-Extra: pymc
|
|
33
|
+
Requires-Dist: pymc>=5.0; extra == 'pymc'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# bayesian-pricing
|
|
37
|
+
|
|
38
|
+
Hierarchical Bayesian models for insurance pricing thin-data segments.
|
|
39
|
+
|
|
40
|
+
## The problem
|
|
41
|
+
|
|
42
|
+
UK personal lines rating operates on multi-dimensional grids. A typical motor model has driver age × NCD × vehicle group × postcode area × occupation. That is potentially 4.5 million rating cells. With 1 million policies, most cells are either empty or contain fewer than 30 observations.
|
|
43
|
+
|
|
44
|
+
Standard approaches all fail at thin cells:
|
|
45
|
+
|
|
46
|
+
- **Saturated GLM**: one coefficient per cell. Overfits noise. A cell with 3 claims gets a relativity of 3/expected, which is meaningless.
|
|
47
|
+
- **Main-effects GLM**: forces multiplicativity. A young driver in a sports car has a rate exactly equal to young-driver-relativity × sports-car-relativity. Reality is super-multiplicative and the model cannot detect it.
|
|
48
|
+
- **Ridge/LASSO GLM**: uniform regularisation regardless of exposure. A cell with 5,000 policy-years gets the same shrinkage as one with 20 policy-years. Wrong.
|
|
49
|
+
- **GBM with min_data_in_leaf**: refuses to split on thin cells. Cannot borrow strength from related cells. No calibrated uncertainty.
|
|
50
|
+
|
|
51
|
+
The correct answer is **partial pooling**: thin segments borrow strength from related segments via a shared population distribution. The degree of borrowing is data-driven - determined by the ratio of within-segment sampling noise to between-segment signal variance. This is the Bayesian posterior.
|
|
52
|
+
|
|
53
|
+
Under Normal-Normal conjugacy, partial pooling is exactly Bühlmann-Straub credibility. This library generalises it to Poisson (frequency) and Gamma (severity) likelihoods, with multiple crossed random effects.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv add "bayesian-pricing[pymc]"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
PyMC 5.x is an optional dependency - it is not pulled in by default because it has C++ compiler requirements on some platforms. The `[pymc]` extra handles this. For GPU-accelerated inference on large portfolios:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv add "bayesian-pricing[numpyro]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
Input is segment-level sufficient statistics - one row per rating cell, with exposure and claim count. This is the practical production design: aggregate your book to rating cells first, then run the model. A book with 500k policies typically has 5,000–20,000 non-empty rating cells. The model operates on those cells, making NUTS feasible on a standard machine.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import polars as pl
|
|
73
|
+
from bayesian_pricing import HierarchicalFrequency, BayesianRelativities
|
|
74
|
+
from bayesian_pricing.frequency import SamplerConfig
|
|
75
|
+
|
|
76
|
+
# One row per rating cell
|
|
77
|
+
df = pl.DataFrame({
|
|
78
|
+
"veh_group": ["Supermini", "Supermini", "Sports", "Sports", "Saloon"],
|
|
79
|
+
"age_band": ["17-21", "31-40", "17-21", "31-40", "31-40"],
|
|
80
|
+
"claims": [8, 120, 3, 45, 200],
|
|
81
|
+
"exposure": [60.0, 900.0, 25.0, 350.0, 2000.0],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
# Fit hierarchical Poisson model
|
|
85
|
+
model = HierarchicalFrequency(
|
|
86
|
+
group_cols=["veh_group", "age_band"],
|
|
87
|
+
prior_mean_rate=0.09, # portfolio mean claim rate
|
|
88
|
+
variance_prior_sigma=0.3, # prior belief on between-segment variation
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
config = SamplerConfig(
|
|
92
|
+
method="nuts", # use "pathfinder" for fast iteration during model development
|
|
93
|
+
draws=1000,
|
|
94
|
+
tune=1000,
|
|
95
|
+
chains=4,
|
|
96
|
+
random_seed=42,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
model.fit(df, claim_count_col="claims", exposure_col="exposure", sampler_config=config)
|
|
100
|
+
|
|
101
|
+
# Posterior predictive means for each segment
|
|
102
|
+
preds = model.predict()
|
|
103
|
+
print(preds)
|
|
104
|
+
# veh_group age_band mean p5 p50 p95 credibility_factor
|
|
105
|
+
# Supermini 17-21 0.1234 0.0812 0.1201 0.1731 0.38
|
|
106
|
+
# Sports 17-21 0.1891 0.1102 0.1845 0.2881 0.21 ← thin
|
|
107
|
+
# ...
|
|
108
|
+
|
|
109
|
+
# Variance components: how much does each factor drive frequency?
|
|
110
|
+
print(model.variance_components())
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Relativities
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
rel = BayesianRelativities(model, hdi_prob=0.9)
|
|
117
|
+
|
|
118
|
+
# Full table for all factors
|
|
119
|
+
tables = rel.relativities()
|
|
120
|
+
|
|
121
|
+
# Single factor in rate-table format
|
|
122
|
+
veh_table = rel.relativities(factor="veh_group")
|
|
123
|
+
print(veh_table.table)
|
|
124
|
+
# level relativity lower_90pct upper_90pct credibility_factor interval_width
|
|
125
|
+
# Sports 1.524 1.234 1.891 0.71 0.657
|
|
126
|
+
# Saloon 1.000 0.921 1.082 0.94 0.161
|
|
127
|
+
# Supermini 0.819 0.764 0.881 0.89 0.117
|
|
128
|
+
|
|
129
|
+
# Identify thin segments that need manual review
|
|
130
|
+
thin = rel.thin_segments(credibility_threshold=0.3)
|
|
131
|
+
print(thin)
|
|
132
|
+
# factor level credibility_factor relativity
|
|
133
|
+
# veh_group Sports-17-21 0.18 1.84 ← sparse cell, wide CI
|
|
134
|
+
|
|
135
|
+
# Export for Excel / rate system import
|
|
136
|
+
summary_df = rel.summary() # long format: factor, level, relativity, CI, credibility
|
|
137
|
+
summary_df.write_csv("bayesian_relativities.csv")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Severity model
|
|
141
|
+
|
|
142
|
+
The severity model has the same API but uses a Gamma likelihood:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from bayesian_pricing import HierarchicalSeverity
|
|
146
|
+
|
|
147
|
+
sev_model = HierarchicalSeverity(
|
|
148
|
+
group_cols=["veh_group"], # severity varies by vehicle, not driver age
|
|
149
|
+
prior_mean_severity=1800.0, # portfolio mean attritional claim cost
|
|
150
|
+
variance_prior_sigma=0.2, # severity has less between-segment variation than frequency
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
sev_model.fit(
|
|
154
|
+
sev_df,
|
|
155
|
+
severity_col="avg_claim_cost",
|
|
156
|
+
weight_col="claim_count", # segments with more claims get more influence
|
|
157
|
+
sampler_config=SamplerConfig(method="nuts", draws=1000, tune=1000, chains=4),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
sev_preds = sev_model.predict()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Convergence diagnostics
|
|
164
|
+
|
|
165
|
+
MCMC results are only valid if the sampler converged. Check before using output:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from bayesian_pricing.diagnostics import convergence_summary, posterior_predictive_check
|
|
169
|
+
|
|
170
|
+
# R-hat, ESS, divergence counts
|
|
171
|
+
diag = convergence_summary(model)
|
|
172
|
+
# Prints warnings if R-hat > 1.01 or ESS < 400
|
|
173
|
+
|
|
174
|
+
# Check model describes the data
|
|
175
|
+
ppc = posterior_predictive_check(model, claim_count_col="claims")
|
|
176
|
+
# Returns: mean, variance, p90, p95 checks
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Inference options
|
|
180
|
+
|
|
181
|
+
| Method | When to use | Speed | Accuracy |
|
|
182
|
+
|---|---|---|---|
|
|
183
|
+
| `SamplerConfig(method="pathfinder")` | Model development, prior sensitivity | Minutes | Good approximation |
|
|
184
|
+
| `SamplerConfig(method="nuts")` | Final production estimates | 20–60 min | Exact (asymptotically) |
|
|
185
|
+
| `SamplerConfig(nuts_sampler="numpyro")` | Large portfolios, GPU available | Fast on GPU | Exact |
|
|
186
|
+
|
|
187
|
+
For portfolios with more than 50k rating cells, consider the two-stage approach: fit a GBM on the full book, extract segment-level residuals, then run the Bayesian model on the residuals. The GBM captures dense-cell signal; the Bayesian model handles thin-cell pooling.
|
|
188
|
+
|
|
189
|
+
## Relationship to Bühlmann-Straub credibility
|
|
190
|
+
|
|
191
|
+
The Bühlmann-Straub credibility premium is the exact posterior mean of a hierarchical model under Normal-Normal conjugacy. This library generalises that result:
|
|
192
|
+
|
|
193
|
+
| Feature | Bühlmann-Straub | bayesian-pricing |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| Likelihood | Normal (symmetric loss) | Poisson, Gamma, NB |
|
|
196
|
+
| Number of grouping factors | One | Multiple crossed |
|
|
197
|
+
| Credible intervals | No (point estimates) | Yes (full posterior) |
|
|
198
|
+
| Hyperparameter uncertainty | Plugged in | Integrated out |
|
|
199
|
+
| Groups | > 20 needed for stable K | Works with 5+ |
|
|
200
|
+
|
|
201
|
+
For single-factor pricing with many groups (e.g., scheme pricing), Bühlmann-Straub is computationally trivial and entirely adequate. Use this library when you need multiple crossed random effects, non-Normal likelihoods, or full posterior uncertainty.
|
|
202
|
+
|
|
203
|
+
## Design decisions
|
|
204
|
+
|
|
205
|
+
**Non-centered parameterization throughout.** The centered version (`u_i ~ Normal(0, sigma)`) creates funnel geometry in the posterior when sigma is small - which is exactly the case for well-regularised insurance models. HMC cannot traverse the funnel efficiently. The non-centered version decouples the raw offsets from the scale and eliminates this problem. See Twiecki (2017) for the clearest exposition.
|
|
206
|
+
|
|
207
|
+
**Segment-level input, not policy-level.** This is the practical production design. NUTS does not scale linearly with observation count. A model with 10,000 rating cells runs in minutes; a model with 1 million policy rows takes hours. Aggregate first.
|
|
208
|
+
|
|
209
|
+
**HalfNormal variance hyperpriors, not HalfCauchy.** HalfCauchy has heavy tails that allow unrealistically large random effects for thin cells - the opposite of the regularisation we want. HalfNormal (Gelman et al., 2013) produces appropriate shrinkage for insurance factors.
|
|
210
|
+
|
|
211
|
+
**Frequency-severity split, not Tweedie.** The split allows different pooling structures for frequency and severity. Young drivers have high frequency but similar severity to older drivers. A Tweedie cannot capture this. The Gamma likelihood handles attritional severity; model large claims separately with Pareto or log-normal.
|
|
212
|
+
|
|
213
|
+
**PyMC optional.** The library parses and validates data without PyMC. Tests for the data layer run in CI without it. This makes the library usable in environments where PyMC is hard to install.
|
|
214
|
+
|
|
215
|
+
## References
|
|
216
|
+
|
|
217
|
+
1. Bühlmann, H. (1967). Experience rating and credibility. *ASTIN Bulletin*, 4(3), 199–207.
|
|
218
|
+
2. Gelman et al. (2013). *Bayesian Data Analysis*, 3rd ed. Chapter 5.
|
|
219
|
+
3. Ohlsson, E. (2008). Combining generalised linear models and credibility models. *Scandinavian Actuarial Journal*.
|
|
220
|
+
4. Krapu et al. (2023). Flexible hierarchical risk modeling for large insurance data via NumPyro. *arXiv:2312.07432*.
|
|
221
|
+
5. Twiecki, T. (2017). Why hierarchical models are awesome, tricky, and Bayesian. twiecki.io.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# bayesian-pricing
|
|
2
|
+
|
|
3
|
+
Hierarchical Bayesian models for insurance pricing thin-data segments.
|
|
4
|
+
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
UK personal lines rating operates on multi-dimensional grids. A typical motor model has driver age × NCD × vehicle group × postcode area × occupation. That is potentially 4.5 million rating cells. With 1 million policies, most cells are either empty or contain fewer than 30 observations.
|
|
8
|
+
|
|
9
|
+
Standard approaches all fail at thin cells:
|
|
10
|
+
|
|
11
|
+
- **Saturated GLM**: one coefficient per cell. Overfits noise. A cell with 3 claims gets a relativity of 3/expected, which is meaningless.
|
|
12
|
+
- **Main-effects GLM**: forces multiplicativity. A young driver in a sports car has a rate exactly equal to young-driver-relativity × sports-car-relativity. Reality is super-multiplicative and the model cannot detect it.
|
|
13
|
+
- **Ridge/LASSO GLM**: uniform regularisation regardless of exposure. A cell with 5,000 policy-years gets the same shrinkage as one with 20 policy-years. Wrong.
|
|
14
|
+
- **GBM with min_data_in_leaf**: refuses to split on thin cells. Cannot borrow strength from related cells. No calibrated uncertainty.
|
|
15
|
+
|
|
16
|
+
The correct answer is **partial pooling**: thin segments borrow strength from related segments via a shared population distribution. The degree of borrowing is data-driven - determined by the ratio of within-segment sampling noise to between-segment signal variance. This is the Bayesian posterior.
|
|
17
|
+
|
|
18
|
+
Under Normal-Normal conjugacy, partial pooling is exactly Bühlmann-Straub credibility. This library generalises it to Poisson (frequency) and Gamma (severity) likelihoods, with multiple crossed random effects.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv add "bayesian-pricing[pymc]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
PyMC 5.x is an optional dependency - it is not pulled in by default because it has C++ compiler requirements on some platforms. The `[pymc]` extra handles this. For GPU-accelerated inference on large portfolios:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv add "bayesian-pricing[numpyro]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Input is segment-level sufficient statistics - one row per rating cell, with exposure and claim count. This is the practical production design: aggregate your book to rating cells first, then run the model. A book with 500k policies typically has 5,000–20,000 non-empty rating cells. The model operates on those cells, making NUTS feasible on a standard machine.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import polars as pl
|
|
38
|
+
from bayesian_pricing import HierarchicalFrequency, BayesianRelativities
|
|
39
|
+
from bayesian_pricing.frequency import SamplerConfig
|
|
40
|
+
|
|
41
|
+
# One row per rating cell
|
|
42
|
+
df = pl.DataFrame({
|
|
43
|
+
"veh_group": ["Supermini", "Supermini", "Sports", "Sports", "Saloon"],
|
|
44
|
+
"age_band": ["17-21", "31-40", "17-21", "31-40", "31-40"],
|
|
45
|
+
"claims": [8, 120, 3, 45, 200],
|
|
46
|
+
"exposure": [60.0, 900.0, 25.0, 350.0, 2000.0],
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
# Fit hierarchical Poisson model
|
|
50
|
+
model = HierarchicalFrequency(
|
|
51
|
+
group_cols=["veh_group", "age_band"],
|
|
52
|
+
prior_mean_rate=0.09, # portfolio mean claim rate
|
|
53
|
+
variance_prior_sigma=0.3, # prior belief on between-segment variation
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
config = SamplerConfig(
|
|
57
|
+
method="nuts", # use "pathfinder" for fast iteration during model development
|
|
58
|
+
draws=1000,
|
|
59
|
+
tune=1000,
|
|
60
|
+
chains=4,
|
|
61
|
+
random_seed=42,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
model.fit(df, claim_count_col="claims", exposure_col="exposure", sampler_config=config)
|
|
65
|
+
|
|
66
|
+
# Posterior predictive means for each segment
|
|
67
|
+
preds = model.predict()
|
|
68
|
+
print(preds)
|
|
69
|
+
# veh_group age_band mean p5 p50 p95 credibility_factor
|
|
70
|
+
# Supermini 17-21 0.1234 0.0812 0.1201 0.1731 0.38
|
|
71
|
+
# Sports 17-21 0.1891 0.1102 0.1845 0.2881 0.21 ← thin
|
|
72
|
+
# ...
|
|
73
|
+
|
|
74
|
+
# Variance components: how much does each factor drive frequency?
|
|
75
|
+
print(model.variance_components())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Relativities
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
rel = BayesianRelativities(model, hdi_prob=0.9)
|
|
82
|
+
|
|
83
|
+
# Full table for all factors
|
|
84
|
+
tables = rel.relativities()
|
|
85
|
+
|
|
86
|
+
# Single factor in rate-table format
|
|
87
|
+
veh_table = rel.relativities(factor="veh_group")
|
|
88
|
+
print(veh_table.table)
|
|
89
|
+
# level relativity lower_90pct upper_90pct credibility_factor interval_width
|
|
90
|
+
# Sports 1.524 1.234 1.891 0.71 0.657
|
|
91
|
+
# Saloon 1.000 0.921 1.082 0.94 0.161
|
|
92
|
+
# Supermini 0.819 0.764 0.881 0.89 0.117
|
|
93
|
+
|
|
94
|
+
# Identify thin segments that need manual review
|
|
95
|
+
thin = rel.thin_segments(credibility_threshold=0.3)
|
|
96
|
+
print(thin)
|
|
97
|
+
# factor level credibility_factor relativity
|
|
98
|
+
# veh_group Sports-17-21 0.18 1.84 ← sparse cell, wide CI
|
|
99
|
+
|
|
100
|
+
# Export for Excel / rate system import
|
|
101
|
+
summary_df = rel.summary() # long format: factor, level, relativity, CI, credibility
|
|
102
|
+
summary_df.write_csv("bayesian_relativities.csv")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Severity model
|
|
106
|
+
|
|
107
|
+
The severity model has the same API but uses a Gamma likelihood:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from bayesian_pricing import HierarchicalSeverity
|
|
111
|
+
|
|
112
|
+
sev_model = HierarchicalSeverity(
|
|
113
|
+
group_cols=["veh_group"], # severity varies by vehicle, not driver age
|
|
114
|
+
prior_mean_severity=1800.0, # portfolio mean attritional claim cost
|
|
115
|
+
variance_prior_sigma=0.2, # severity has less between-segment variation than frequency
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
sev_model.fit(
|
|
119
|
+
sev_df,
|
|
120
|
+
severity_col="avg_claim_cost",
|
|
121
|
+
weight_col="claim_count", # segments with more claims get more influence
|
|
122
|
+
sampler_config=SamplerConfig(method="nuts", draws=1000, tune=1000, chains=4),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
sev_preds = sev_model.predict()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Convergence diagnostics
|
|
129
|
+
|
|
130
|
+
MCMC results are only valid if the sampler converged. Check before using output:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from bayesian_pricing.diagnostics import convergence_summary, posterior_predictive_check
|
|
134
|
+
|
|
135
|
+
# R-hat, ESS, divergence counts
|
|
136
|
+
diag = convergence_summary(model)
|
|
137
|
+
# Prints warnings if R-hat > 1.01 or ESS < 400
|
|
138
|
+
|
|
139
|
+
# Check model describes the data
|
|
140
|
+
ppc = posterior_predictive_check(model, claim_count_col="claims")
|
|
141
|
+
# Returns: mean, variance, p90, p95 checks
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Inference options
|
|
145
|
+
|
|
146
|
+
| Method | When to use | Speed | Accuracy |
|
|
147
|
+
|---|---|---|---|
|
|
148
|
+
| `SamplerConfig(method="pathfinder")` | Model development, prior sensitivity | Minutes | Good approximation |
|
|
149
|
+
| `SamplerConfig(method="nuts")` | Final production estimates | 20–60 min | Exact (asymptotically) |
|
|
150
|
+
| `SamplerConfig(nuts_sampler="numpyro")` | Large portfolios, GPU available | Fast on GPU | Exact |
|
|
151
|
+
|
|
152
|
+
For portfolios with more than 50k rating cells, consider the two-stage approach: fit a GBM on the full book, extract segment-level residuals, then run the Bayesian model on the residuals. The GBM captures dense-cell signal; the Bayesian model handles thin-cell pooling.
|
|
153
|
+
|
|
154
|
+
## Relationship to Bühlmann-Straub credibility
|
|
155
|
+
|
|
156
|
+
The Bühlmann-Straub credibility premium is the exact posterior mean of a hierarchical model under Normal-Normal conjugacy. This library generalises that result:
|
|
157
|
+
|
|
158
|
+
| Feature | Bühlmann-Straub | bayesian-pricing |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| Likelihood | Normal (symmetric loss) | Poisson, Gamma, NB |
|
|
161
|
+
| Number of grouping factors | One | Multiple crossed |
|
|
162
|
+
| Credible intervals | No (point estimates) | Yes (full posterior) |
|
|
163
|
+
| Hyperparameter uncertainty | Plugged in | Integrated out |
|
|
164
|
+
| Groups | > 20 needed for stable K | Works with 5+ |
|
|
165
|
+
|
|
166
|
+
For single-factor pricing with many groups (e.g., scheme pricing), Bühlmann-Straub is computationally trivial and entirely adequate. Use this library when you need multiple crossed random effects, non-Normal likelihoods, or full posterior uncertainty.
|
|
167
|
+
|
|
168
|
+
## Design decisions
|
|
169
|
+
|
|
170
|
+
**Non-centered parameterization throughout.** The centered version (`u_i ~ Normal(0, sigma)`) creates funnel geometry in the posterior when sigma is small - which is exactly the case for well-regularised insurance models. HMC cannot traverse the funnel efficiently. The non-centered version decouples the raw offsets from the scale and eliminates this problem. See Twiecki (2017) for the clearest exposition.
|
|
171
|
+
|
|
172
|
+
**Segment-level input, not policy-level.** This is the practical production design. NUTS does not scale linearly with observation count. A model with 10,000 rating cells runs in minutes; a model with 1 million policy rows takes hours. Aggregate first.
|
|
173
|
+
|
|
174
|
+
**HalfNormal variance hyperpriors, not HalfCauchy.** HalfCauchy has heavy tails that allow unrealistically large random effects for thin cells - the opposite of the regularisation we want. HalfNormal (Gelman et al., 2013) produces appropriate shrinkage for insurance factors.
|
|
175
|
+
|
|
176
|
+
**Frequency-severity split, not Tweedie.** The split allows different pooling structures for frequency and severity. Young drivers have high frequency but similar severity to older drivers. A Tweedie cannot capture this. The Gamma likelihood handles attritional severity; model large claims separately with Pareto or log-normal.
|
|
177
|
+
|
|
178
|
+
**PyMC optional.** The library parses and validates data without PyMC. Tests for the data layer run in CI without it. This makes the library usable in environments where PyMC is hard to install.
|
|
179
|
+
|
|
180
|
+
## References
|
|
181
|
+
|
|
182
|
+
1. Bühlmann, H. (1967). Experience rating and credibility. *ASTIN Bulletin*, 4(3), 199–207.
|
|
183
|
+
2. Gelman et al. (2013). *Bayesian Data Analysis*, 3rd ed. Chapter 5.
|
|
184
|
+
3. Ohlsson, E. (2008). Combining generalised linear models and credibility models. *Scandinavian Actuarial Journal*.
|
|
185
|
+
4. Krapu et al. (2023). Flexible hierarchical risk modeling for large insurance data via NumPyro. *arXiv:2312.07432*.
|
|
186
|
+
5. Twiecki, T. (2017). Why hierarchical models are awesome, tricky, and Bayesian. twiecki.io.
|