1use prospicio_core::{Error, Result};
5use prospicio_math::roots::bisect;
6use prospicio_math::special::ln_gamma;
7
8use crate::distribution::{Distribution, check_probability};
9use crate::gamma::Gamma;
10use crate::severity::Severity;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct Tweedie {
48 mean: f64,
49 dispersion: f64,
50 power: f64,
51 lambda: f64,
52 severity: Gamma,
53}
54
55impl Tweedie {
56 pub fn new(mean: f64, dispersion: f64, power: f64) -> Result<Self> {
59 positive("mean", mean)?;
60 positive("dispersion", dispersion)?;
61 if !(power > 1.0 && power < 2.0) {
62 return Err(Error::InvalidParameter {
63 name: "power",
64 value: power,
65 reason: "must be in (1, 2)",
66 });
67 }
68 let lambda = mean.powf(2.0 - power) / (dispersion * (2.0 - power));
69 let shape = (2.0 - power) / (power - 1.0);
70 let scale = dispersion * (power - 1.0) * mean.powf(power - 1.0);
71 Ok(Self {
72 mean,
73 dispersion,
74 power,
75 lambda,
76 severity: Gamma::new(shape, scale)?,
77 })
78 }
79
80 pub fn from_poisson_gamma(lambda: f64, shape: f64, scale: f64) -> Result<Self> {
92 positive("lambda", lambda)?;
93 Gamma::new(shape, scale)?;
94 let power = (shape + 2.0) / (shape + 1.0);
95 let mean = lambda * shape * scale;
96 let dispersion = scale / ((power - 1.0) * mean.powf(power - 1.0));
97 Self::new(mean, dispersion, power)
98 }
99
100 pub fn mean_param(&self) -> f64 {
102 self.mean
103 }
104
105 pub fn dispersion(&self) -> f64 {
107 self.dispersion
108 }
109
110 pub fn power(&self) -> f64 {
112 self.power
113 }
114
115 pub fn lambda(&self) -> f64 {
117 self.lambda
118 }
119
120 pub fn severity(&self) -> Gamma {
122 self.severity
123 }
124
125 pub fn ln_pdf(&self, y: f64) -> f64 {
142 if y < 0.0 || y.is_nan() {
143 return f64::NEG_INFINITY;
144 }
145 if y == 0.0 {
146 return -self.lambda;
147 }
148 let (alpha, theta) = (self.severity.shape(), self.severity.scale());
151 let ln_term = |n: f64| {
152 n * self.lambda.ln() - self.lambda - ln_gamma(n + 1.0) + (n * alpha - 1.0) * y.ln()
153 - y / theta
154 - ln_gamma(n * alpha)
155 - n * alpha * theta.ln()
156 };
157 let peak = {
159 let mut best = (1.0, ln_term(1.0));
160 let guess = (y / (alpha * theta)).max(self.lambda).max(1.0);
161 for n in [guess.floor(), guess.ceil(), self.lambda.floor().max(1.0)] {
162 let v = ln_term(n.max(1.0));
163 if v > best.1 {
164 best = (n.max(1.0), v);
165 }
166 }
167 let (mut n, mut v) = best;
169 loop {
170 let up = ln_term(n + 1.0);
171 if up > v {
172 (n, v) = (n + 1.0, up);
173 continue;
174 }
175 if n > 1.0 {
176 let down = ln_term(n - 1.0);
177 if down > v {
178 (n, v) = (n - 1.0, down);
179 continue;
180 }
181 }
182 break (n, v);
183 }
184 };
185 let (n0, v0) = peak;
186 let mut sum = 1.0;
187 let mut n = n0 + 1.0;
188 loop {
189 let r = (ln_term(n) - v0).exp();
190 sum += r;
191 if r <= 1e-17 * sum {
192 break;
193 }
194 n += 1.0;
195 }
196 let mut n = n0 - 1.0;
197 while n >= 1.0 {
198 let r = (ln_term(n) - v0).exp();
199 sum += r;
200 if r <= 1e-17 * sum {
201 break;
202 }
203 n -= 1.0;
204 }
205 v0 + sum.ln()
206 }
207
208 fn poisson_sum(&self, beyond: f64, mut g: impl FnMut(&Gamma) -> f64) -> f64 {
213 let (alpha, theta) = (self.severity.shape(), self.severity.scale());
214 let ln_lambda = self.lambda.ln();
215 let floor = self.lambda.max(beyond);
216 let mut sum = 0.0;
217 for n in 1..10_000_000u32 {
218 let nf = f64::from(n);
219 let w = (nf * ln_lambda - self.lambda - ln_gamma(nf + 1.0)).exp();
220 let term = if w == 0.0 {
221 0.0
222 } else {
223 w * g(&Gamma::new(nf * alpha, theta).expect("valid shape and scale"))
224 };
225 sum += term;
226 if nf > floor && (term <= 1e-17 * sum || w == 0.0) {
227 break;
228 }
229 }
230 sum
231 }
232
233 fn claims_to_reach(&self, y: f64) -> f64 {
235 let k = y / (self.severity.shape() * self.severity.scale());
236 k + 10.0 * k.sqrt() + 10.0
237 }
238}
239
240impl Distribution for Tweedie {
241 fn mean(&self) -> f64 {
242 self.mean
243 }
244
245 fn variance(&self) -> f64 {
246 self.dispersion * self.mean.powf(self.power)
247 }
248
249 fn cdf(&self, y: f64) -> f64 {
250 if y < 0.0 {
251 return 0.0;
252 }
253 let zero = (-self.lambda).exp();
254 if y == 0.0 {
255 return zero;
256 }
257 (zero + self.poisson_sum(0.0, |g| g.cdf(y))).min(1.0)
258 }
259
260 fn survival(&self, y: f64) -> f64 {
261 if y < 0.0 {
262 return 1.0;
263 }
264 if y == 0.0 {
265 return -(-self.lambda).exp_m1();
266 }
267 self.poisson_sum(self.claims_to_reach(y), |g| g.survival(y))
268 .min(1.0)
269 }
270
271 fn quantile(&self, p: f64) -> Result<f64> {
274 check_probability(p)?;
275 if p <= (-self.lambda).exp() {
276 return Ok(0.0);
277 }
278 if p == 1.0 {
279 return Ok(f64::INFINITY);
280 }
281 let below = |y: f64| {
282 if p <= 0.5 {
283 self.cdf(y) < p
284 } else {
285 self.survival(y) > 1.0 - p
286 }
287 };
288 let mut hi = self.mean + self.variance().sqrt();
289 while below(hi) {
290 hi *= 2.0;
291 }
292 Ok(bisect(0.0, hi, below))
293 }
294}
295
296impl Severity for Tweedie {
297 fn lev(&self, limit: f64) -> f64 {
298 if limit <= 0.0 {
299 return limit;
300 }
301 if limit == f64::INFINITY {
302 return self.mean;
303 }
304 self.poisson_sum(0.0, |g| g.lev(limit))
305 }
306
307 fn stop_loss(&self, retention: f64) -> f64 {
308 if retention <= 0.0 {
309 return self.mean - retention;
310 }
311 if retention == f64::INFINITY {
312 return 0.0;
313 }
314 self.poisson_sum(self.claims_to_reach(retention), |g| g.stop_loss(retention))
315 }
316
317 fn layer(&self, limit: f64, attachment: f64) -> f64 {
318 let a = attachment.max(0.0);
319 self.poisson_sum(self.claims_to_reach(a + limit.min(1e300)), |g| {
320 g.layer(limit, a)
321 })
322 }
323
324 fn layer_second_moment(&self, limit: f64, attachment: f64) -> f64 {
325 let a = attachment.max(0.0);
326 self.poisson_sum(self.claims_to_reach(a + limit.min(1e300)), |g| {
327 g.layer_second_moment(limit, a)
328 })
329 }
330}
331
332fn positive(name: &'static str, value: f64) -> Result<()> {
333 if value.is_finite() && value > 0.0 {
334 Ok(())
335 } else {
336 Err(Error::InvalidParameter {
337 name,
338 value,
339 reason: "must be finite and positive",
340 })
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn parameterizations_agree() {
350 let y = Tweedie::new(500.0, 40.0, 1.6).unwrap();
351 let z = Tweedie::from_poisson_gamma(y.lambda(), y.severity().shape(), y.severity().scale())
352 .unwrap();
353 assert!((z.mean() / 500.0 - 1.0).abs() < 1e-14);
354 assert!((z.dispersion() / 40.0 - 1.0).abs() < 1e-12);
355 assert!((z.power() - 1.6).abs() < 1e-14);
356 assert!(Tweedie::new(1.0, 1.0, 2.0).is_err());
357 assert!(Tweedie::new(1.0, 1.0, 1.0).is_err());
358 }
359
360 #[test]
361 fn moments_and_layers_from_the_series() {
362 let y = Tweedie::new(500.0, 40.0, 1.6).unwrap();
363 assert!((y.layer(f64::INFINITY, 0.0) / y.mean() - 1.0).abs() < 1e-12);
365 let m2 = y.layer_second_moment(f64::INFINITY, 0.0);
366 let want = y.variance() + y.mean() * y.mean();
367 assert!((m2 / want - 1.0).abs() < 1e-12, "{m2} {want}");
368 for d in [10.0, 500.0, 5_000.0] {
369 assert!((y.lev(d) + y.stop_loss(d) - 500.0).abs() < 1e-9);
370 assert!((y.cdf(d) + y.survival(d) - 1.0).abs() < 1e-14);
371 }
372 }
373
374 #[test]
375 fn density_integrates_to_the_distribution_function() {
376 let y = Tweedie::new(10.0, 2.0, 1.4).unwrap();
377 let (a, b) = (0.5, 30.0);
379 let panels = 400;
380 let h = (b - a) / f64::from(panels);
381 let integral: f64 = (0..panels)
382 .map(|i| {
383 let lo = a + h * f64::from(i);
384 prospicio_math::integrate::gauss_legendre(
385 |x| Ok::<_, ()>(y.ln_pdf(x).exp()),
386 lo,
387 lo + h,
388 )
389 .unwrap()
390 })
391 .sum();
392 assert!(
393 (integral - (y.cdf(b) - y.cdf(a))).abs() < 1e-12,
394 "{integral}"
395 );
396 }
397
398 #[test]
399 fn quantiles_invert_and_respect_the_atom() {
400 let y = Tweedie::new(10.0, 2.0, 1.4).unwrap();
401 let p0 = y.cdf(0.0);
402 assert_eq!(y.quantile(p0 * 0.5), Ok(0.0));
403 for p in [0.5, 0.9, 0.999999] {
404 let x = y.quantile(p).unwrap();
405 assert!((y.cdf(x) - p).abs() < 1e-12, "{p}");
406 }
407 }
408}