bayesian-pricing 0.2.3__tar.gz → 0.2.4__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.
Files changed (22) hide show
  1. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/PKG-INFO +10 -9
  2. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/README.md +9 -8
  3. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/notebooks/01_hierarchical_frequency_demo.py +3 -3
  4. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/pyproject.toml +1 -1
  5. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/src/bayesian_pricing/__init__.py +9 -3
  6. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/src/bayesian_pricing/frequency.py +29 -15
  7. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/src/bayesian_pricing/relativities.py +32 -23
  8. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/src/bayesian_pricing/severity.py +15 -3
  9. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/tests/test_relativities.py +6 -6
  10. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/uv.lock +40 -110
  11. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/.github/workflows/tests.yml +0 -0
  12. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/.gitignore +0 -0
  13. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/CITATION.cff +0 -0
  14. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/LICENSE +0 -0
  15. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/benchmarks/benchmark.py +0 -0
  16. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/notebooks/bayesian_pricing_demo.py +0 -0
  17. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/notebooks/benchmark.py +0 -0
  18. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/src/bayesian_pricing/_utils.py +0 -0
  19. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/src/bayesian_pricing/diagnostics.py +0 -0
  20. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/tests/conftest.py +0 -0
  21. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/tests/test_frequency.py +0 -0
  22. {bayesian_pricing-0.2.3 → bayesian_pricing-0.2.4}/tests/test_severity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bayesian-pricing
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Hierarchical Bayesian models for insurance pricing thin-data segments
5
5
  Project-URL: Homepage, https://github.com/burning-cost/bayesian-pricing
6
6
  Project-URL: Repository, https://github.com/burning-cost/bayesian-pricing
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
  [![PyPI](https://img.shields.io/pypi/v/bayesian-pricing)](https://pypi.org/project/bayesian-pricing/)
43
43
  [![Python](https://img.shields.io/pypi/pyversions/bayesian-pricing)](https://pypi.org/project/bayesian-pricing/)
44
44
  [![Tests](https://img.shields.io/badge/tests-passing-brightgreen)]()
45
- [![License](https://img.shields.io/badge/license-BSD--3-blue)]()
45
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
46
46
 
47
47
  Hierarchical Bayesian models for insurance pricing thin-data segments — when your rating grid has more cells than your book has claims, partial pooling gives you credible estimates where every other method gives you noise or nothing.
48
48
 
@@ -138,6 +138,7 @@ model.fit(df, claim_count_col="claims", exposure_col="exposure", sampler_config=
138
138
  # Posterior predictive means for each segment
139
139
  preds = model.predict()
140
140
  print(preds)
141
+ # Output is illustrative — exact values depend on your data and sampler seed:
141
142
  # veh_group age_band mean p5 p50 p95 credibility_factor
142
143
  # Supermini 17-21 0.1234 0.0812 0.1201 0.1731 0.38
143
144
  # Sports 17-21 0.1891 0.1102 0.1845 0.2881 0.21 <- thin
@@ -158,19 +159,19 @@ tables = rel.relativities()
158
159
  # Single factor in rate-table format
159
160
  veh_table = rel.relativities(factor="veh_group")
160
161
  print(veh_table.table)
161
- # level relativity lower_90pct upper_90pct credibility_factor interval_width
162
- # Sports 1.524 1.234 1.891 0.71 0.657
163
- # Saloon 1.000 0.921 1.082 0.94 0.161
164
- # Supermini 0.819 0.764 0.881 0.89 0.117
162
+ # level relativity lower_90pct upper_90pct uncertainty_reduction interval_width
163
+ # Sports 1.524 1.234 1.891 0.71 0.657
164
+ # Saloon 1.000 0.921 1.082 0.94 0.161
165
+ # Supermini 0.819 0.764 0.881 0.89 0.117
165
166
 
166
167
  # Identify thin segments that need manual review
167
168
  thin = rel.thin_segments(credibility_threshold=0.3)
168
169
  print(thin)
169
- # factor level credibility_factor relativity
170
- # veh_group Sports-17-21 0.18 1.84 <- sparse cell, wide CI
170
+ # factor level uncertainty_reduction relativity
171
+ # veh_group Sports-17-21 0.18 1.84 <- sparse cell, wide CI
171
172
 
172
173
  # Export for Excel / rate system import
173
- summary_df = rel.summary() # long format: factor, level, relativity, CI, credibility
174
+ summary_df = rel.summary() # long format: factor, level, relativity, CI, uncertainty_reduction
174
175
  summary_df.write_csv("bayesian_relativities.csv")
175
176
  ```
176
177
 
@@ -3,7 +3,7 @@
3
3
  [![PyPI](https://img.shields.io/pypi/v/bayesian-pricing)](https://pypi.org/project/bayesian-pricing/)
4
4
  [![Python](https://img.shields.io/pypi/pyversions/bayesian-pricing)](https://pypi.org/project/bayesian-pricing/)
5
5
  [![Tests](https://img.shields.io/badge/tests-passing-brightgreen)]()
6
- [![License](https://img.shields.io/badge/license-BSD--3-blue)]()
6
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
7
7
 
8
8
  Hierarchical Bayesian models for insurance pricing thin-data segments — when your rating grid has more cells than your book has claims, partial pooling gives you credible estimates where every other method gives you noise or nothing.
9
9
 
@@ -99,6 +99,7 @@ model.fit(df, claim_count_col="claims", exposure_col="exposure", sampler_config=
99
99
  # Posterior predictive means for each segment
100
100
  preds = model.predict()
101
101
  print(preds)
102
+ # Output is illustrative — exact values depend on your data and sampler seed:
102
103
  # veh_group age_band mean p5 p50 p95 credibility_factor
103
104
  # Supermini 17-21 0.1234 0.0812 0.1201 0.1731 0.38
104
105
  # Sports 17-21 0.1891 0.1102 0.1845 0.2881 0.21 <- thin
@@ -119,19 +120,19 @@ tables = rel.relativities()
119
120
  # Single factor in rate-table format
120
121
  veh_table = rel.relativities(factor="veh_group")
121
122
  print(veh_table.table)
122
- # level relativity lower_90pct upper_90pct credibility_factor interval_width
123
- # Sports 1.524 1.234 1.891 0.71 0.657
124
- # Saloon 1.000 0.921 1.082 0.94 0.161
125
- # Supermini 0.819 0.764 0.881 0.89 0.117
123
+ # level relativity lower_90pct upper_90pct uncertainty_reduction interval_width
124
+ # Sports 1.524 1.234 1.891 0.71 0.657
125
+ # Saloon 1.000 0.921 1.082 0.94 0.161
126
+ # Supermini 0.819 0.764 0.881 0.89 0.117
126
127
 
127
128
  # Identify thin segments that need manual review
128
129
  thin = rel.thin_segments(credibility_threshold=0.3)
129
130
  print(thin)
130
- # factor level credibility_factor relativity
131
- # veh_group Sports-17-21 0.18 1.84 <- sparse cell, wide CI
131
+ # factor level uncertainty_reduction relativity
132
+ # veh_group Sports-17-21 0.18 1.84 <- sparse cell, wide CI
132
133
 
133
134
  # Export for Excel / rate system import
134
- summary_df = rel.summary() # long format: factor, level, relativity, CI, credibility
135
+ summary_df = rel.summary() # long format: factor, level, relativity, CI, uncertainty_reduction
135
136
  summary_df.write_csv("bayesian_relativities.csv")
136
137
  ```
137
138
 
@@ -352,7 +352,7 @@ veh_table["recovery_error_pct"] = (
352
352
  )
353
353
  print("Vehicle group relativity recovery:")
354
354
  display(veh_table[["level", "true_relativity", "relativity", "lower_90pct", "upper_90pct",
355
- "recovery_error_pct", "credibility_factor"]].round(3))
355
+ "recovery_error_pct", "uncertainty_reduction"]].round(3))
356
356
 
357
357
  # COMMAND ----------
358
358
 
@@ -554,8 +554,8 @@ display(sev_rt.table)
554
554
  freq_rt = rel.relativities(factor="veh_group")
555
555
 
556
556
  # Merge frequency and severity relativities
557
- pp_table = freq_rt.table[["level", "relativity", "credibility_factor"]].merge(
558
- sev_rt.table[["level", "relativity", "credibility_factor"]],
557
+ pp_table = freq_rt.table[["level", "relativity", "uncertainty_reduction"]].rename({"uncertainty_reduction": "uncertainty_reduction_freq"}).merge(
558
+ sev_rt.table[["level", "relativity", "uncertainty_reduction"]].rename({"uncertainty_reduction": "uncertainty_reduction_sev"}),
559
559
  on="level",
560
560
  suffixes=("_freq", "_sev"),
561
561
  )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bayesian-pricing"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "Hierarchical Bayesian models for insurance pricing thin-data segments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -37,19 +37,25 @@ Usage::
37
37
 
38
38
  rel = BayesianRelativities(freq_model)
39
39
  print(rel.relativities()) # Polars DataFrame with posterior median + credible interval
40
- print(rel.credibility_factors()) # How much weight each segment puts on own data
40
+ print(rel.credibility_factors()) # Uncertainty reduction per segment (how data-dominated each level is)
41
41
  """
42
42
 
43
- from bayesian_pricing.frequency import HierarchicalFrequency
43
+ from bayesian_pricing.frequency import HierarchicalFrequency, SamplerConfig
44
44
  from bayesian_pricing.severity import HierarchicalSeverity
45
45
  from bayesian_pricing.relativities import BayesianRelativities
46
46
  from bayesian_pricing.diagnostics import convergence_summary, posterior_predictive_check
47
47
 
48
- __version__ = "0.2.1"
48
+ from importlib.metadata import version, PackageNotFoundError
49
+
50
+ try:
51
+ __version__ = version("bayesian-pricing")
52
+ except PackageNotFoundError:
53
+ __version__ = "0.0.0" # not installed
49
54
  __all__ = [
50
55
  "HierarchicalFrequency",
51
56
  "HierarchicalSeverity",
52
57
  "BayesianRelativities",
58
+ "SamplerConfig",
53
59
  "convergence_summary",
54
60
  "posterior_predictive_check",
55
61
  ]
@@ -236,6 +236,15 @@ class HierarchicalFrequency:
236
236
  _validate_columns_present(df, self.group_cols + [claim_count_col, exposure_col])
237
237
  _validate_positive(df[exposure_col], exposure_col)
238
238
 
239
+ # Validate interaction pairs before _check_pymc() so users get
240
+ # ValueError rather than ImportError for bad pair specifications.
241
+ for col_a, col_b in self.interaction_pairs:
242
+ if col_a not in self.group_cols or col_b not in self.group_cols:
243
+ raise ValueError(
244
+ f"Interaction pair ({col_a!r}, {col_b!r}) references a column "
245
+ f"not in group_cols: {self.group_cols}"
246
+ )
247
+
239
248
  _check_pymc()
240
249
  import pymc as pm
241
250
 
@@ -259,15 +268,6 @@ class HierarchicalFrequency:
259
268
  group_indices[col] = idx
260
269
  group_levels[col] = levels
261
270
 
262
- # Build interaction indices
263
- interaction_indices: dict[tuple[str, str], np.ndarray] = {}
264
- for col_a, col_b in self.interaction_pairs:
265
- if col_a not in self.group_cols or col_b not in self.group_cols:
266
- raise ValueError(
267
- f"Interaction pair ({col_a!r}, {col_b!r}) references a column "
268
- f"not in group_cols: {self.group_cols}"
269
- )
270
-
271
271
  # Coords for PyMC named dimensions
272
272
  coords = {}
273
273
  for col in self.group_cols:
@@ -421,8 +421,10 @@ class HierarchicalFrequency:
421
421
  - The group columns from your training data
422
422
  - mean: posterior mean claim rate (use this as your point estimate)
423
423
  - The quantiles you specified (default: p5, p50, p95)
424
- - credibility_factor: how much this segment was pulled toward the portfolio
425
- mean (0 = fully pooled to mean, 1 = trusts own data completely)
424
+ - posterior_shrinkage_ratio: measures how far the posterior mean sits between
425
+ the portfolio mean (0) and the raw observed rate (1). This is NOT the
426
+ Bühlmann-Straub credibility factor Z; it is a post-hoc ratio. Use
427
+ variance_components() for the B-S-analogous variance decomposition.
426
428
 
427
429
  Args:
428
430
  quantiles: Posterior quantiles to return. Default gives 90% credible
@@ -468,15 +470,23 @@ class HierarchicalFrequency:
468
470
  for q in quantiles:
469
471
  result_dict[f"p{int(q * 100)}"] = np.quantile(rate_samples, q, axis=0).tolist()
470
472
 
471
- # Credibility factor: Z = (posterior mean - portfolio mean) / (observed rate - portfolio mean)
472
- # Undefined when observed rate == portfolio mean; clamp to [0, 1].
473
+ # posterior_shrinkage_ratio: how far the posterior mean sits between the
474
+ # portfolio mean (0) and the raw observed rate (1).
475
+ #
476
+ # This is NOT the Bühlmann-Straub credibility factor Z. The B-S Z is derived
477
+ # analytically as n_i / (n_i + k) where k = sigma^2 / tau^2 (ratio of
478
+ # within-segment variance to between-segment variance). This column instead
479
+ # measures posterior shrinkage post-hoc:
480
+ # (posterior_mean - portfolio_mean) / (observed_rate - portfolio_mean)
481
+ # It is clipped to [0, 1] and set to 0.5 when the denominator is near zero.
482
+ # Use variance_components() for the B-S-analogous tau^2 estimates.
473
483
  portfolio_mean = float(np.exp(alpha_samples.mean()))
474
484
  obs = self._get_observed_rates()
475
485
  denom = obs - portfolio_mean
476
486
  num = np.array(result_dict["mean"]) - portfolio_mean
477
487
  with np.errstate(divide="ignore", invalid="ignore"):
478
488
  z = np.where(np.abs(denom) < 1e-10, 0.5, np.clip(num / denom, 0, 1))
479
- result_dict["credibility_factor"] = z.tolist()
489
+ result_dict["posterior_shrinkage_ratio"] = z.tolist()
480
490
 
481
491
  return pl.DataFrame(result_dict)
482
492
 
@@ -510,7 +520,11 @@ class HierarchicalFrequency:
510
520
  hdi_prob=0.94,
511
521
  )
512
522
 
513
- # Add human interpretation column
523
+ # Add human interpretation column.
524
+ # typical_relativity_spread = exp(sigma) - 1 is the approximate coefficient
525
+ # of variation of the lognormal random effect: for a segment at mu + sigma,
526
+ # the relativity is exp(sigma), so exp(sigma) - 1 is the fractional spread
527
+ # above 1.0. Provides an intuitive %-spread for reviewers.
514
528
  summary_pd["typical_relativity_spread"] = np.exp(summary_pd["mean"]) - 1
515
529
 
516
530
  # Convert to Polars, bringing the index (parameter names) in as a column
@@ -44,14 +44,16 @@ class RelativityTable:
44
44
  factor: The grouping column name (e.g., "veh_group").
45
45
  levels: List of levels in the factor.
46
46
  table: Polars DataFrame with columns: level, relativity, lower, upper,
47
- credibility_factor, interval_width.
47
+ uncertainty_reduction, interval_width.
48
48
 
49
- The credibility_factor tells you how data-dominated each level is:
50
- 0.0 = fully pooled to portfolio mean (estimate is 1.0 relativity)
51
- 1.0 = trusts own experience completely (like a fixed-effects GLM)
52
- 0.6 = weighted blend: 60% own data, 40% portfolio mean
49
+ The uncertainty_reduction column measures how much the posterior has reduced
50
+ uncertainty relative to the prior: 1 - posterior_std / prior_rel_std. A value
51
+ near 1.0 means the data has largely eliminated prior uncertainty (thick segment).
52
+ A value near 0.0 means posterior uncertainty is still close to prior uncertainty
53
+ (thin segment, minimal data influence). This is distinct from the B-S credibility
54
+ factor Z -- it measures uncertainty reduction, not the mixing weight on own data.
53
55
 
54
- Levels with credibility_factor < 0.3 have wide credible intervals and
56
+ Levels with uncertainty_reduction < 0.3 have wide credible intervals and
55
57
  should be treated with caution. They should not drive large rate changes.
56
58
  """
57
59
 
@@ -162,8 +164,11 @@ class BayesianRelativities:
162
164
  point_estimate = np.median(rel_samples, axis=0)
163
165
  hdi_lower, hdi_upper = self._compute_hdi(rel_samples)
164
166
 
165
- # Credibility factor heuristic: compare posterior uncertainty to prior uncertainty.
166
- # Low posterior std relative to prior std -> high credibility (less uncertain).
167
+ # Uncertainty reduction: compare posterior uncertainty to prior uncertainty.
168
+ # 1 - posterior_std / prior_rel_std. Near 1 = data has resolved uncertainty (thick
169
+ # segment). Near 0 = posterior still similar to prior (thin, heavily pooled segment).
170
+ # Note: this is NOT the same as the B-S credibility Z from predict(), which measures
171
+ # the shrinkage fraction (posterior_mean - portfolio_mean) / (observed - portfolio_mean).
167
172
  prior_std = np.ones(len(levels)) * self._model.variance_prior_sigma
168
173
  posterior_std = rel_samples.std(axis=0)
169
174
  prior_rel_std = np.exp(prior_std) - 1
@@ -177,21 +182,26 @@ class BayesianRelativities:
177
182
  "relativity": point_estimate.tolist(),
178
183
  lower_col: hdi_lower.tolist(),
179
184
  upper_col: hdi_upper.tolist(),
180
- "credibility_factor": credibility.tolist(),
185
+ "uncertainty_reduction": credibility.tolist(),
181
186
  "interval_width": (hdi_upper - hdi_lower).tolist(),
182
187
  }).sort("relativity", descending=True)
183
188
 
184
189
  return RelativityTable(factor=col, levels=levels, table=table)
185
190
 
186
191
  def credibility_factors(self) -> "pl.DataFrame":
187
- """Return credibility factors for all segments across all factors.
192
+ """Return uncertainty_reduction for all segments across all factors.
188
193
 
189
- The credibility factor answers: "what fraction of this segment's estimate
190
- comes from its own experience vs the portfolio mean?" This is the Bayesian
191
- equivalent of Z_j = w_j / (w_j + K) in Bühlmann-Straub.
194
+ The uncertainty_reduction column measures how much the posterior has reduced
195
+ uncertainty relative to the prior: 1 - posterior_std / prior_rel_std.
196
+ A value near 1.0 means the data has largely eliminated prior uncertainty.
197
+ A value near 0.0 means the segment is thin and heavily pooled.
198
+
199
+ Note: this is distinct from the B-S credibility factor Z returned by
200
+ predict(). Use predict() credibility_factor to understand shrinkage;
201
+ use this method to understand how data-dominated each segment is.
192
202
 
193
203
  Returns:
194
- Polars DataFrame with factor, level, credibility_factor, and relativity columns.
204
+ Polars DataFrame with factor, level, uncertainty_reduction, and relativity columns.
195
205
  """
196
206
  import polars as pl
197
207
 
@@ -202,7 +212,7 @@ class BayesianRelativities:
202
212
  rows.append({
203
213
  "factor": col,
204
214
  "level": row["level"],
205
- "credibility_factor": row["credibility_factor"],
215
+ "uncertainty_reduction": row["uncertainty_reduction"],
206
216
  "relativity": row["relativity"],
207
217
  })
208
218
  return pl.DataFrame(rows)
@@ -215,22 +225,21 @@ class BayesianRelativities:
215
225
  The credible intervals for these segments will be wide.
216
226
 
217
227
  Practical use: flag thin segments in the rate table for underwriter review.
218
- A segment with credibility_factor = 0.1 should not drive a large rate change
219
- even if the posterior median relativity differs from 1.0 significantly.
228
+ A segment with uncertainty_reduction = 0.1 should not drive a large rate
229
+ change even if the posterior median relativity differs from 1.0 significantly.
220
230
 
221
231
  Args:
222
- credibility_threshold: Segments with credibility_factor below this
223
- value are returned. Default 0.3 (meaning less than 30% of the
224
- estimate comes from own experience).
232
+ credibility_threshold: Segments with uncertainty_reduction below this
233
+ value are returned. Default 0.3.
225
234
 
226
235
  Returns:
227
- Polars DataFrame with factor, level, credibility_factor, and relativity.
236
+ Polars DataFrame with factor, level, uncertainty_reduction, and relativity.
228
237
  """
229
238
  creds = self.credibility_factors()
230
239
  return (
231
240
  creds
232
- .filter(creds["credibility_factor"] < credibility_threshold)
233
- .sort("credibility_factor")
241
+ .filter(creds["uncertainty_reduction"] < credibility_threshold)
242
+ .sort("uncertainty_reduction")
234
243
  )
235
244
 
236
245
  def summary(self) -> "pl.DataFrame":
@@ -179,6 +179,16 @@ class HierarchicalSeverity:
179
179
  _validate_columns_present(df, required_cols)
180
180
  _validate_positive(df[severity_col], severity_col)
181
181
 
182
+ if weight_col and weight_col in df.columns:
183
+ weights_check = df[weight_col].to_numpy(dtype=float)
184
+ if np.any(weights_check <= 0):
185
+ n_bad = int(np.sum(weights_check <= 0))
186
+ raise ValueError(
187
+ f"weight_col '{weight_col}' contains {n_bad} zero or negative "
188
+ f"value(s). Weights must be strictly positive claim counts. "
189
+ f"Remove or exclude zero-claim segments before fitting."
190
+ )
191
+
182
192
  _check_pymc()
183
193
  import pymc as pm
184
194
 
@@ -274,9 +284,11 @@ class HierarchicalSeverity:
274
284
  mu_severity = pm.math.exp(mu_log)
275
285
 
276
286
  # Gamma shape: controls within-segment CV.
277
- # HalfNormal(2) allows shape from near-0 (high dispersion) to ~6 (low CV).
278
- # UK attritional motor claims typically have shape ~0.25-0.7.
279
- gamma_shape = pm.HalfNormal("gamma_shape", sigma=2.0)
287
+ # UK attritional motor severity has CV ~1.5-2.5, implying shape ~0.16-0.44.
288
+ # HalfNormal(sigma=0.5) has median ~0.34, placing most prior mass in the
289
+ # attritional motor range. The previous default of sigma=2 put ~83% of
290
+ # mass above shape=0.44, systematically underestimating dispersion.
291
+ gamma_shape = pm.HalfNormal("gamma_shape", sigma=0.5)
280
292
 
281
293
  # Gamma parameterisation: mean = mu, concentration = gamma_shape
282
294
  # Each segment observation is a sample average over `weights` claims.
@@ -106,7 +106,7 @@ class TestBayesianRelativitiesOutput:
106
106
  def test_relativity_table_has_required_columns(self, fitted_rel):
107
107
  rt = fitted_rel.relativities(factor="veh_group")
108
108
  required_cols = {"level", "relativity", "lower_90pct", "upper_90pct",
109
- "credibility_factor", "interval_width"}
109
+ "uncertainty_reduction", "interval_width"}
110
110
  assert required_cols.issubset(set(rt.table.columns))
111
111
 
112
112
  def test_relativity_table_is_polars(self, fitted_rel):
@@ -128,8 +128,8 @@ class TestBayesianRelativitiesOutput:
128
128
  def test_credibility_factors_bounded(self, fitted_rel):
129
129
  cred = fitted_rel.credibility_factors()
130
130
  assert isinstance(cred, pl.DataFrame)
131
- assert (cred["credibility_factor"] >= 0).all()
132
- assert (cred["credibility_factor"] <= 1).all()
131
+ assert (cred["uncertainty_reduction"] >= 0).all()
132
+ assert (cred["uncertainty_reduction"] <= 1).all()
133
133
 
134
134
  def test_base_level_normalisation(self, freq_segment_data):
135
135
  """With base_level={"veh_group": "A"}, group A should have relativity 1.0."""
@@ -178,13 +178,13 @@ class TestBayesianRelativitiesOutput:
178
178
  thin = fitted_rel.thin_segments(credibility_threshold=0.5)
179
179
  assert isinstance(thin, pl.DataFrame)
180
180
  if len(thin) > 0:
181
- assert (thin["credibility_factor"] < 0.5).all()
181
+ assert (thin["uncertainty_reduction"] < 0.5).all()
182
182
 
183
183
  def test_thin_segments_sorted_ascending(self, fitted_rel):
184
184
  thin = fitted_rel.thin_segments(credibility_threshold=1.0) # all segments
185
185
  if len(thin) > 1:
186
- cred = thin["credibility_factor"].to_numpy()
187
- assert (cred[1:] >= cred[:-1]).all(), "thin_segments should be sorted by credibility"
186
+ cred = thin["uncertainty_reduction"].to_numpy()
187
+ assert (cred[1:] >= cred[:-1]).all(), "thin_segments should be sorted by uncertainty_reduction"
188
188
 
189
189
  def test_single_factor_returns_relativity_table_not_dict(self, fitted_rel):
190
190
  result = fitted_rel.relativities(factor="veh_group")
@@ -21,115 +21,37 @@ resolution-markers = [
21
21
  name = "arviz"
22
22
  version = "0.23.4"
23
23
  source = { registry = "https://pypi.org/simple" }
24
- resolution-markers = [
25
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
26
- "python_full_version == '3.11.*' and sys_platform == 'emscripten'",
27
- "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
28
- "python_full_version < '3.11'",
29
- ]
30
24
  dependencies = [
31
- { name = "h5netcdf", marker = "python_full_version < '3.12'" },
32
- { name = "h5py", marker = "python_full_version < '3.12'" },
33
- { name = "matplotlib", marker = "python_full_version < '3.12'" },
25
+ { name = "h5netcdf" },
26
+ { name = "h5py" },
27
+ { name = "matplotlib" },
34
28
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
35
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
36
- { name = "packaging", marker = "python_full_version < '3.12'" },
29
+ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
30
+ { name = "packaging" },
37
31
  { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
38
- { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
39
- { name = "platformdirs", marker = "python_full_version < '3.12'" },
32
+ { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
33
+ { name = "platformdirs" },
40
34
  { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
41
- { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
42
- { name = "setuptools", marker = "python_full_version < '3.12'" },
43
- { name = "typing-extensions", marker = "python_full_version < '3.12'" },
35
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
36
+ { name = "setuptools" },
37
+ { name = "typing-extensions" },
44
38
  { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
45
- { name = "xarray", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
39
+ { name = "xarray", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
46
40
  { name = "xarray-einstats", version = "0.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
47
41
  { name = "xarray-einstats", version = "0.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
42
+ { name = "xarray-einstats", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
48
43
  ]
49
44
  sdist = { url = "https://files.pythonhosted.org/packages/f3/c9/9c853633715f972eecc20995763c6e3005a3afcdcf47e39d20cd1c2889cd/arviz-0.23.4.tar.gz", hash = "sha256:611be826995066036c9443ea98d11486c279ef3da3b6cdc5c0816fab434115b9", size = 1592968, upload-time = "2026-02-04T17:57:53.664Z" }
50
45
  wheels = [
51
46
  { url = "https://files.pythonhosted.org/packages/44/1f/227f9cb7edcd3e14ab05928f3db00e9d595c0f269c87bf35f565ce44941b/arviz-0.23.4-py3-none-any.whl", hash = "sha256:c46c7faf8a06abadc9b5b64000584062ecbc20c2298e2bd6dfba04bb01a684ca", size = 1673773, upload-time = "2026-02-04T17:57:51.778Z" },
52
47
  ]
53
48
 
54
- [[package]]
55
- name = "arviz"
56
- version = "1.0.0"
57
- source = { registry = "https://pypi.org/simple" }
58
- resolution-markers = [
59
- "python_full_version >= '3.14' and sys_platform == 'win32'",
60
- "python_full_version >= '3.14' and sys_platform == 'emscripten'",
61
- "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
62
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
63
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
64
- "python_full_version == '3.13.*' and sys_platform == 'emscripten'",
65
- "python_full_version == '3.12.*' and sys_platform == 'emscripten'",
66
- "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
67
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
68
- ]
69
- dependencies = [
70
- { name = "arviz-base", marker = "python_full_version >= '3.12'" },
71
- { name = "arviz-plots", marker = "python_full_version >= '3.12'" },
72
- { name = "arviz-stats", extra = ["xarray"], marker = "python_full_version >= '3.12'" },
73
- ]
74
- sdist = { url = "https://files.pythonhosted.org/packages/5b/e1/398fcbc5b043245fe7d704346b42385b886956a99fcb09964b41aada4267/arviz-1.0.0.tar.gz", hash = "sha256:9bc978e8b590d5ee88fe024c4fa10e214dc42c31329e645b2ad998419cbe7ff6", size = 8272, upload-time = "2026-03-02T14:58:48.996Z" }
75
- wheels = [
76
- { url = "https://files.pythonhosted.org/packages/30/dc/b1dcb62745f58f800d4073c9ad914a5cf882fe1d8f20793eae0a1d54bbac/arviz-1.0.0-py3-none-any.whl", hash = "sha256:8caf30516bb7e13c237d266e3d37c59531898a93db9e89c1f1bb9320699e97dd", size = 8445, upload-time = "2026-03-02T14:58:47.7Z" },
77
- ]
78
-
79
- [[package]]
80
- name = "arviz-base"
81
- version = "1.0.0"
82
- source = { registry = "https://pypi.org/simple" }
83
- dependencies = [
84
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
85
- { name = "typing-extensions", marker = "python_full_version >= '3.12'" },
86
- { name = "xarray", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
87
- ]
88
- sdist = { url = "https://files.pythonhosted.org/packages/17/9b/84b06b529b1b397c50f8ff2e5af5af7a09eef24c6ee5334e08e2cf12ef96/arviz_base-1.0.0.tar.gz", hash = "sha256:6b9796043b4e394056fd8a1ab51c8d4d3e88dcf8f2b698a46b660ff4346c8841", size = 1403207, upload-time = "2026-03-02T07:36:17.604Z" }
89
- wheels = [
90
- { url = "https://files.pythonhosted.org/packages/35/69/6624d40ec780a30d187e9224e1c2bf51eebe0e61ebd2c7dee06dd4464a24/arviz_base-1.0.0-py3-none-any.whl", hash = "sha256:77c93e3503166517da8155d580ec9615e9b7b89ab7890f8d9ab72c78a9b6c9e6", size = 1420656, upload-time = "2026-03-02T07:36:15.588Z" },
91
- ]
92
-
93
- [[package]]
94
- name = "arviz-plots"
95
- version = "1.0.0"
96
- source = { registry = "https://pypi.org/simple" }
97
- dependencies = [
98
- { name = "arviz-base", marker = "python_full_version >= '3.12'" },
99
- { name = "arviz-stats", extra = ["xarray"], marker = "python_full_version >= '3.12'" },
100
- ]
101
- sdist = { url = "https://files.pythonhosted.org/packages/12/af/113e16c1c7990badc433d694008ffb6d235265d9da15c22bff1ca485d4b3/arviz_plots-1.0.0.tar.gz", hash = "sha256:e8bb70824b50f09f7c527dac4519aef1b72cb0a9eadcd4da25b80c22a4bcef71", size = 143729, upload-time = "2026-03-02T10:03:36.015Z" }
102
- wheels = [
103
- { url = "https://files.pythonhosted.org/packages/aa/ff/9e1914853bd6df5f7ddf7a1cc261baeae87992bd46c9b40d0300b499d18d/arviz_plots-1.0.0-py3-none-any.whl", hash = "sha256:b086a603be6d880f6b446beae4d2111c7c3dc2ce2e65481cd88097f474694dde", size = 225351, upload-time = "2026-03-02T10:03:34.437Z" },
104
- ]
105
-
106
- [[package]]
107
- name = "arviz-stats"
108
- version = "1.0.0"
109
- source = { registry = "https://pypi.org/simple" }
110
- dependencies = [
111
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
112
- { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
113
- ]
114
- sdist = { url = "https://files.pythonhosted.org/packages/5b/09/df9f50fd79ca7a990564bfb45902ad79b5d87ed7deb6ae0f90d3844c5408/arviz_stats-1.0.0.tar.gz", hash = "sha256:056228574bbc1e9045fd41da168d62fe4167097fd886056b8085a40799142e53", size = 144731, upload-time = "2026-03-02T08:06:52.294Z" }
115
- wheels = [
116
- { url = "https://files.pythonhosted.org/packages/b2/3f/7f0df19fc24f375b2e28e594893a1869314b868a6791046f1a56b5ca78a1/arviz_stats-1.0.0-py3-none-any.whl", hash = "sha256:3b2e5e8c8714827497e82eb1ab8064a16a7fb1a8f733edf325bff2c81ec4a400", size = 171588, upload-time = "2026-03-02T08:06:50.685Z" },
117
- ]
118
-
119
- [package.optional-dependencies]
120
- xarray = [
121
- { name = "arviz-base", marker = "python_full_version >= '3.12'" },
122
- { name = "xarray", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
123
- { name = "xarray-einstats", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
124
- ]
125
-
126
49
  [[package]]
127
50
  name = "bayesian-pricing"
128
- version = "0.2.0"
51
+ version = "0.2.1"
129
52
  source = { editable = "." }
130
53
  dependencies = [
131
- { name = "arviz", version = "0.23.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
132
- { name = "arviz", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
54
+ { name = "arviz" },
133
55
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
134
56
  { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
135
57
  { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -161,12 +83,12 @@ dev = [
161
83
 
162
84
  [package.metadata]
163
85
  requires-dist = [
164
- { name = "arviz", specifier = ">=0.17" },
86
+ { name = "arviz", specifier = ">=0.17,<1.0" },
165
87
  { name = "jax", marker = "extra == 'numpyro'", specifier = ">=0.4" },
166
88
  { name = "numpy", specifier = ">=1.24" },
167
89
  { name = "numpyro", marker = "extra == 'numpyro'", specifier = ">=0.13" },
168
90
  { name = "pandas", specifier = ">=2.0" },
169
- { name = "polars", specifier = ">=0.20" },
91
+ { name = "polars", specifier = ">=1.0" },
170
92
  { name = "pyarrow", specifier = ">=23.0.1" },
171
93
  { name = "pymc", marker = "extra == 'pymc'", specifier = ">=5.0" },
172
94
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" },
@@ -291,12 +213,21 @@ name = "contourpy"
291
213
  version = "1.3.3"
292
214
  source = { registry = "https://pypi.org/simple" }
293
215
  resolution-markers = [
216
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
217
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
218
+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
219
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
220
+ "python_full_version == '3.12.*' and sys_platform == 'win32'",
221
+ "python_full_version == '3.13.*' and sys_platform == 'emscripten'",
222
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten'",
223
+ "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
224
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
294
225
  "python_full_version == '3.11.*' and sys_platform == 'win32'",
295
226
  "python_full_version == '3.11.*' and sys_platform == 'emscripten'",
296
227
  "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
297
228
  ]
298
229
  dependencies = [
299
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
230
+ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
300
231
  ]
301
232
  sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
302
233
  wheels = [
@@ -597,8 +528,8 @@ version = "1.8.1"
597
528
  source = { registry = "https://pypi.org/simple" }
598
529
  dependencies = [
599
530
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
600
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
601
- { name = "packaging", marker = "python_full_version < '3.12'" },
531
+ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
532
+ { name = "packaging" },
602
533
  ]
603
534
  sdist = { url = "https://files.pythonhosted.org/packages/ef/03/92d6cc02c0055158167255980461155d6e17f1c4143c03f8bcc18d3e3f3a/h5netcdf-1.8.1.tar.gz", hash = "sha256:9b396a4cc346050fc1a4df8523bc1853681ec3544e0449027ae397cb953c7a16", size = 78679, upload-time = "2026-01-23T07:35:31.233Z" }
604
535
  wheels = [
@@ -611,7 +542,7 @@ version = "3.16.0"
611
542
  source = { registry = "https://pypi.org/simple" }
612
543
  dependencies = [
613
544
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
614
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
545
+ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
615
546
  ]
616
547
  sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" }
617
548
  wheels = [
@@ -970,16 +901,16 @@ version = "3.10.8"
970
901
  source = { registry = "https://pypi.org/simple" }
971
902
  dependencies = [
972
903
  { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
973
- { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
974
- { name = "cycler", marker = "python_full_version < '3.12'" },
975
- { name = "fonttools", marker = "python_full_version < '3.12'" },
976
- { name = "kiwisolver", marker = "python_full_version < '3.12'" },
904
+ { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
905
+ { name = "cycler" },
906
+ { name = "fonttools" },
907
+ { name = "kiwisolver" },
977
908
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
978
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
979
- { name = "packaging", marker = "python_full_version < '3.12'" },
980
- { name = "pillow", marker = "python_full_version < '3.12'" },
981
- { name = "pyparsing", marker = "python_full_version < '3.12'" },
982
- { name = "python-dateutil", marker = "python_full_version < '3.12'" },
909
+ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
910
+ { name = "packaging" },
911
+ { name = "pillow" },
912
+ { name = "pyparsing" },
913
+ { name = "python-dateutil" },
983
914
  ]
984
915
  sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" }
985
916
  wheels = [
@@ -1733,7 +1664,7 @@ resolution-markers = [
1733
1664
  "python_full_version < '3.11'",
1734
1665
  ]
1735
1666
  dependencies = [
1736
- { name = "arviz", version = "0.23.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
1667
+ { name = "arviz", marker = "python_full_version < '3.11'" },
1737
1668
  { name = "cachetools", marker = "python_full_version < '3.11'" },
1738
1669
  { name = "cloudpickle", marker = "python_full_version < '3.11'" },
1739
1670
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -1768,8 +1699,7 @@ resolution-markers = [
1768
1699
  "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
1769
1700
  ]
1770
1701
  dependencies = [
1771
- { name = "arviz", version = "0.23.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
1772
- { name = "arviz", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
1702
+ { name = "arviz", marker = "python_full_version >= '3.11'" },
1773
1703
  { name = "cachetools", marker = "python_full_version >= '3.11'" },
1774
1704
  { name = "cloudpickle", marker = "python_full_version >= '3.11'" },
1775
1705
  { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },