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.
@@ -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.