1use prospicio_core::Result;
12use prospicio_math::roots::{bisect, bisect_log, illinois};
13
14use crate::evt::Gpd;
15use crate::pareto::invalid;
16use crate::piecewise_pareto::Truncation;
17use crate::{Pareto, PiecewisePareto};
18
19#[derive(Debug, Clone, PartialEq)]
34pub struct LargeLosses {
35 values: Vec<f64>,
36 reporting: Vec<f64>,
37 censored: Vec<bool>,
38 weights: Vec<f64>,
39}
40
41impl LargeLosses {
42 pub fn new(values: Vec<f64>) -> Result<Self> {
45 if values.is_empty() {
46 return Err(invalid("losses", 0.0, "need at least one loss"));
47 }
48 for &x in &values {
49 if !x.is_finite() || x <= 0.0 {
50 return Err(invalid("losses", x, "must be finite and positive"));
51 }
52 }
53 let n = values.len();
54 Ok(Self {
55 values,
56 reporting: vec![0.0; n],
57 censored: vec![false; n],
58 weights: vec![1.0; n],
59 })
60 }
61
62 pub fn reporting_thresholds(self, reporting: Vec<f64>) -> Result<Self> {
64 self.check_len(reporting.len())?;
65 for (&r, &x) in reporting.iter().zip(&self.values) {
66 if !r.is_finite() || r < 0.0 || r > x {
67 return Err(invalid(
68 "reporting_thresholds",
69 r,
70 "must be finite, non-negative and at most the loss",
71 ));
72 }
73 }
74 Ok(Self { reporting, ..self })
75 }
76
77 pub fn censored(self, censored: Vec<bool>) -> Result<Self> {
80 self.check_len(censored.len())?;
81 Ok(Self { censored, ..self })
82 }
83
84 pub fn weights(self, weights: Vec<f64>) -> Result<Self> {
86 self.check_len(weights.len())?;
87 for &w in &weights {
88 if !w.is_finite() || w <= 0.0 {
89 return Err(invalid("weights", w, "must be finite and positive"));
90 }
91 }
92 Ok(Self { weights, ..self })
93 }
94
95 pub fn values(&self) -> &[f64] {
96 &self.values
97 }
98
99 pub fn len(&self) -> usize {
100 self.values.len()
101 }
102
103 pub fn is_empty(&self) -> bool {
104 self.values.is_empty()
105 }
106
107 fn check_len(&self, n: usize) -> Result<()> {
108 if n != self.values.len() {
109 return Err(invalid("length", n as f64, "must have one entry per loss"));
110 }
111 Ok(())
112 }
113
114 fn above(&self, t: f64) -> Result<Vec<(f64, f64, bool, f64)>> {
117 (0..self.len())
118 .map(|i| {
119 let y = self.values[i];
120 if y < t {
121 return Err(invalid(
122 "losses",
123 y,
124 "must be at least the lowest threshold of the fitted distribution",
125 ));
126 }
127 Ok((
128 y,
129 self.reporting[i].max(t),
130 self.censored[i],
131 self.weights[i],
132 ))
133 })
134 .collect()
135 }
136}
137
138const ALPHA_MIN: f64 = 1e-3;
140const ALPHA_MAX: f64 = 1e3;
141
142impl Pareto {
143 pub fn fit(t: f64, data: &LargeLosses, truncation: Option<f64>) -> Result<Self> {
153 let base = Pareto::new(t, 1.0)?;
154 let losses = data.above(t)?;
155 let alpha = match truncation {
156 None => closed_form_alpha(&losses)?,
157 Some(tr) => {
158 base.truncated(tr)?;
159 truncated_alpha(&losses, tr)?
160 }
161 };
162 let p = Pareto::new(t, alpha)?;
163 match truncation {
164 None => Ok(p),
165 Some(tr) => p.truncated(tr),
166 }
167 }
168}
169
170impl PiecewisePareto {
171 pub fn fit(
201 t: Vec<f64>,
202 data: &LargeLosses,
203 truncation: Option<(f64, Truncation)>,
204 ) -> Result<Self> {
205 PiecewisePareto::new(t.clone(), vec![1.0; t.len()])?;
207 if let Some((tr, Truncation::WholeDistribution)) = truncation {
208 PiecewisePareto::new(t.clone(), vec![1.0; t.len()])?
209 .truncated(tr, Truncation::WholeDistribution)?;
210 let start = PiecewisePareto::fit(t.clone(), data, None)?;
211 let losses = data.above(t[0])?;
212 let alphas = whole_truncated_alphas(&t, &losses, tr, start.alphas())?;
213 return PiecewisePareto::new(t, alphas)?.truncated(tr, Truncation::WholeDistribution);
214 }
215 let losses = data.above(t[0])?;
216 let n = t.len();
217 let mut alphas = Vec::with_capacity(n);
218 for k in 0..n {
219 let (lo, hi) = (t[k], t.get(k + 1).copied().unwrap_or(f64::INFINITY));
220 let last = k + 1 == n;
221 if let (true, Some((tr, _))) = (last, truncation) {
222 let reaching: Vec<_> = losses
225 .iter()
226 .filter(|&&(y, ..)| y > lo)
227 .map(|&(y, r, c, w)| (y, r.max(lo), c, w))
228 .collect();
229 PiecewisePareto::new(t.clone(), vec![1.0; n])?
230 .truncated(tr, Truncation::LastPiece)?;
231 alphas.push(truncated_alpha(&reaching, tr)?);
232 continue;
233 }
234 let (mut count, mut exposure) = (0.0, 0.0);
235 for &(y, r, censored, w) in &losses {
236 let (a, b) = (r.max(lo), y.min(hi));
237 if b > a {
238 exposure += w * (b / a).ln();
239 }
240 if !censored && y >= lo && y < hi {
241 count += w;
242 }
243 }
244 if exposure <= 0.0 {
245 return Err(invalid(
246 "t",
247 lo,
248 "no loss reaches into this piece above its reporting threshold",
249 ));
250 }
251 alphas.push(count / exposure);
252 }
253 let fit = PiecewisePareto::new(t, alphas)?;
254 match truncation {
255 None => Ok(fit),
256 Some((tr, kind)) => fit.truncated(tr, kind),
257 }
258 }
259}
260
261impl Gpd {
262 pub fn fit_riegel(t: f64, data: &LargeLosses) -> Result<Self> {
285 Pareto::new(t, 1.0)?;
286 let losses = data.above(t)?;
287 let count: f64 = losses.iter().filter(|l| !l.2).map(|l| l.3).sum();
288 if count == 0.0 {
289 return Err(invalid("losses", 0.0, "need at least one uncensored loss"));
290 }
291 let z = |x: f64| x / t - 1.0;
292 let exposure = |k: f64| -> (f64, f64) {
294 losses.iter().fold((0.0, 0.0), |(e, de), &(y, r, _, w)| {
295 let (zy, zr) = (z(y), z(r));
296 (
297 e + w * ((k * zy).ln_1p() - (k * zr).ln_1p()),
298 de + w * (zy / (1.0 + k * zy) - zr / (1.0 + k * zr)),
299 )
300 })
301 };
302 let score = |k: f64| -> Option<f64> {
306 let (e, de) = exposure(k);
307 if e <= 0.0 {
308 return None;
309 }
310 let own: f64 = losses
311 .iter()
312 .filter(|l| !l.2)
313 .map(|&(y, _, _, w)| w * z(y) / (1.0 + k * z(y)))
314 .sum();
315 Some(count / k - count * de / e - own)
316 };
317 let profile = |k: f64| -> Option<f64> {
318 let (e, _) = exposure(k);
319 if e <= 0.0 {
320 return None;
321 }
322 let alpha_tail = count / e;
323 let own: f64 = losses
324 .iter()
325 .filter(|l| !l.2)
326 .map(|&(y, _, _, w)| w * (k * z(y)).ln_1p())
327 .sum();
328 Some(count * (k * alpha_tail).ln() - own)
330 };
331 let steps = 480;
332 let (lo_k, hi_k) = (1e-6f64, 1e6f64);
333 let at = |j: usize| (lo_k.ln() + (hi_k / lo_k).ln() * j as f64 / steps as f64).exp();
334 let best = (0..=steps)
335 .filter_map(|j| profile(at(j)).map(|v| (j, v)))
336 .max_by(|a, b| a.1.total_cmp(&b.1))
337 .map(|(j, _)| j)
338 .ok_or_else(|| {
339 invalid(
340 "losses",
341 f64::NAN,
342 "need a loss above its reporting threshold",
343 )
344 })?;
345 if best == 0 || best == steps {
346 return Err(invalid(
347 "losses",
348 at(best),
349 "the likelihood has no maximum with alpha_ini / alpha_tail in [1e-6, 1e6]",
350 ));
351 }
352 let k = bisect_log(
354 at(best - 1),
355 at(best + 1),
356 |k| matches!(score(k), Some(s) if s > 0.0),
357 );
358 let alpha_tail = count / exposure(k).0;
359 Gpd::riegel(t, k * alpha_tail, alpha_tail)
360 }
361}
362
363fn whole_truncated_alphas(
373 t: &[f64],
374 losses: &[(f64, f64, bool, f64)],
375 tr: f64,
376 start: &[f64],
377) -> Result<Vec<f64>> {
378 let n = t.len();
379 let exposures = |a: f64| -> Vec<f64> {
380 (0..n)
381 .map(|k| {
382 let hi = t.get(k + 1).copied().unwrap_or(f64::INFINITY);
383 if a > t[k] {
384 (a.min(hi) / t[k]).ln()
385 } else {
386 0.0
387 }
388 })
389 .collect()
390 };
391 let e_tr = exposures(tr);
392 struct Row {
393 w: f64,
394 censored: bool,
395 piece: usize,
396 e_y: Vec<f64>,
397 d_y: Vec<f64>,
398 e_r: Vec<f64>,
399 d_r: Vec<f64>,
400 }
401 let mut rows = Vec::with_capacity(losses.len());
402 for &(y, r, censored, w) in losses {
403 if y >= tr {
404 return Err(invalid("losses", y, "must lie below the truncation point"));
405 }
406 let (e_y, e_r) = (exposures(y), exposures(r));
407 let diff = |e: &[f64]| e_tr.iter().zip(e).map(|(a, b)| a - b).collect::<Vec<_>>();
408 rows.push(Row {
409 w,
410 censored,
411 piece: t.partition_point(|&x| x <= y) - 1,
412 d_y: diff(&e_y),
413 d_r: diff(&e_r),
414 e_y,
415 e_r,
416 });
417 }
418 if rows.iter().all(|r| r.censored) {
419 return Err(invalid("losses", 0.0, "need at least one uncensored loss"));
420 }
421 let partial = |alphas: &[f64], k: usize| -> f64 {
422 let term = |e: &[f64], d: &[f64]| -> f64 {
424 let minus_d: f64 = alphas.iter().zip(d).map(|(a, x)| a * x).sum();
425 -e[k] + d[k] / minus_d.exp_m1()
426 };
427 rows.iter()
428 .map(|r| {
429 let own = if r.censored {
430 term(&r.e_y, &r.d_y)
431 } else {
432 let count = if r.piece == k { 1.0 / alphas[k] } else { 0.0 };
433 count - r.e_y[k]
434 };
435 r.w * (own - term(&r.e_r, &r.d_r))
436 })
437 .sum()
438 };
439 let mut alphas: Vec<f64> = start
440 .iter()
441 .map(|&a| a.clamp(ALPHA_MIN, ALPHA_MAX))
442 .collect();
443 for _ in 0..5000 {
444 let mut change = 0.0f64;
445 for k in 0..n {
446 let old = alphas[k];
447 let at = |a: f64, alphas: &mut Vec<f64>| {
448 alphas[k] = a;
449 partial(alphas, k)
450 };
451 let new = coordinate_root(old, |a| at(a, &mut alphas));
452 alphas[k] = new;
453 change = change.max((new / old).ln().abs());
454 }
455 if change < 1e-13 {
456 break;
457 }
458 }
459 Ok(alphas)
460}
461
462fn coordinate_root(start: f64, mut f: impl FnMut(f64) -> f64) -> f64 {
467 let (mut lo, mut hi) = ((start / 1.5).max(ALPHA_MIN), (start * 1.5).min(ALPHA_MAX));
468 let (mut f_lo, mut f_hi) = (f(lo), f(hi));
469 while f_lo <= 0.0 && lo > ALPHA_MIN {
470 hi = lo;
471 f_hi = f_lo;
472 lo = (lo / 4.0).max(ALPHA_MIN);
473 f_lo = f(lo);
474 }
475 if f_lo <= 0.0 {
476 return ALPHA_MIN;
477 }
478 while f_hi > 0.0 && hi < ALPHA_MAX {
479 lo = hi;
480 f_lo = f_hi;
481 hi = (hi * 4.0).min(ALPHA_MAX);
482 f_hi = f(hi);
483 }
484 if f_hi > 0.0 {
485 return ALPHA_MAX;
486 }
487 illinois(lo.ln(), hi.ln(), f_lo, f_hi, |x| f(x.exp())).exp()
489}
490
491fn closed_form_alpha(losses: &[(f64, f64, bool, f64)]) -> Result<f64> {
493 let count: f64 = losses.iter().filter(|l| !l.2).map(|l| l.3).sum();
494 let exposure: f64 = losses.iter().map(|&(y, r, _, w)| w * (y / r).ln()).sum();
495 if count == 0.0 {
496 return Err(invalid("losses", 0.0, "need at least one uncensored loss"));
497 }
498 if exposure <= 0.0 {
499 return Err(invalid(
500 "losses",
501 exposure,
502 "need a loss above its reporting threshold",
503 ));
504 }
505 Ok(count / exposure)
506}
507
508fn truncated_alpha(losses: &[(f64, f64, bool, f64)], tr: f64) -> Result<f64> {
518 if losses.iter().all(|l| l.2) {
519 return Err(invalid("losses", 0.0, "need at least one uncensored loss"));
520 }
521 for &(y, ..) in losses {
522 if y >= tr {
523 return Err(invalid("losses", y, "must lie below the truncation point"));
524 }
525 }
526 let g = |x: f64, alpha: f64| -> f64 {
527 let log_q = alpha * (x / tr).ln();
528 -x.ln() + (tr / x).ln() / (-log_q).exp_m1()
530 };
531 let score = |alpha: f64| -> f64 {
532 losses
533 .iter()
534 .map(|&(y, r, censored, w)| {
535 let own = if censored {
536 g(y, alpha)
537 } else {
538 1.0 / alpha - y.ln()
539 };
540 w * (own - g(r, alpha))
541 })
542 .sum()
543 };
544 let steps = 240;
547 let at =
548 |j: usize| (ALPHA_MIN.ln() + (ALPHA_MAX / ALPHA_MIN).ln() * j as f64 / steps as f64).exp();
549 if score(at(0)) <= 0.0 {
550 return Ok(ALPHA_MIN);
551 }
552 let Some(j) = (1..=steps).find(|&j| score(at(j)) <= 0.0) else {
553 return Ok(ALPHA_MAX);
554 };
555 Ok(bisect(at(j - 1), at(j), |a| score(a) > 0.0))
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561 use crate::{Distribution, Severity};
562 use prospicio_core::StreamRng;
563
564 #[test]
565 fn closed_form_pareto_with_thresholds_and_censoring() {
566 let data = LargeLosses::new(vec![1500.0, 3000.0, 2500.0, 8000.0])
567 .unwrap()
568 .reporting_thresholds(vec![0.0, 2000.0, 1000.0, 0.0])
569 .unwrap()
570 .censored(vec![false, false, false, true])
571 .unwrap()
572 .weights(vec![1.0, 2.0, 1.0, 1.0])
573 .unwrap();
574 let fit = Pareto::fit(1000.0, &data, None).unwrap();
575 let exposure = 1.5f64.ln() + 2.0 * 1.5f64.ln() + 2.5f64.ln() + 8f64.ln();
576 assert!((fit.alpha() - 4.0 / exposure).abs() < 1e-14);
577 assert!(Pareto::fit(2000.0, &data, None).is_err());
578 let all_censored = LargeLosses::new(vec![2000.0])
579 .unwrap()
580 .censored(vec![true])
581 .unwrap();
582 assert!(Pareto::fit(1000.0, &all_censored, None).is_err());
583 }
584
585 #[test]
586 fn truncated_fit_maximizes_the_likelihood() {
587 let data = LargeLosses::new(vec![1100.0, 1300.0, 2000.0, 3500.0, 9000.0, 4000.0])
588 .unwrap()
589 .reporting_thresholds(vec![0.0, 1200.0, 0.0, 0.0, 0.0, 0.0])
590 .unwrap()
591 .censored(vec![false, false, false, false, false, true])
592 .unwrap();
593 let tr = 20_000.0;
594 let fit = Pareto::fit(1000.0, &data, Some(tr)).unwrap();
595 let ll = |alpha: f64| -> f64 {
596 let p = Pareto::new(1000.0, alpha).unwrap().truncated(tr).unwrap();
597 let mut ll = 0.0;
598 for (i, &y) in data.values().iter().enumerate() {
599 let r = data.reporting[i].max(1000.0);
600 ll -= p.survival(r).ln();
601 ll += if data.censored[i] {
602 p.survival(y).ln()
603 } else {
604 let q = (1000.0 / tr).powf(alpha);
606 (alpha * 1000f64.powf(alpha) * y.powf(-alpha - 1.0) / (1.0 - q)).ln()
607 };
608 }
609 ll
610 };
611 let a = fit.alpha();
612 assert!(ll(a) >= ll(a * 1.001) && ll(a) >= ll(a * 0.999), "{a}");
613 let far = Pareto::fit(1000.0, &data, Some(1e30)).unwrap();
615 let open = Pareto::fit(1000.0, &data, None).unwrap();
616 assert!((far.alpha() / open.alpha() - 1.0).abs() < 1e-9);
617 assert!(Pareto::fit(1000.0, &data, Some(5000.0)).is_err());
618 }
619
620 #[test]
621 fn fits_recover_simulated_parameters() {
622 let truth =
623 PiecewisePareto::new(vec![1000.0, 3000.0, 10_000.0], vec![1.2, 2.0, 1.5]).unwrap();
624 let mut rng = StreamRng::new(11, 0);
625 let draws = truth.sample(&mut rng, 200_000);
626 let (mut values, mut reporting, mut censored) = (vec![], vec![], vec![]);
629 for (i, &x) in draws.iter().enumerate() {
630 let r = if i % 2 == 0 { 2000.0 } else { 0.0 };
631 if x <= r {
632 continue;
633 }
634 values.push(x.min(50_000.0));
635 reporting.push(r);
636 censored.push(x >= 50_000.0);
637 }
638 let data = LargeLosses::new(values)
639 .unwrap()
640 .reporting_thresholds(reporting)
641 .unwrap()
642 .censored(censored)
643 .unwrap();
644 let fit = PiecewisePareto::fit(vec![1000.0, 3000.0, 10_000.0], &data, None).unwrap();
645 for (got, want) in fit.alphas().iter().zip(truth.alphas()) {
646 assert!((got / want - 1.0).abs() < 0.03, "{got} {want}");
647 }
648 let one = PiecewisePareto::fit(vec![1000.0], &data, None).unwrap();
650 let p = Pareto::fit(1000.0, &data, None).unwrap();
651 assert!((one.alphas()[0] / p.alpha() - 1.0).abs() < 1e-14);
652 let t = PiecewisePareto::new(vec![1000.0, 3000.0], vec![1.2, 0.8])
654 .unwrap()
655 .truncated(40_000.0, Truncation::LastPiece)
656 .unwrap();
657 let draws = t.sample(&mut StreamRng::new(12, 0), 100_000);
658 let data = LargeLosses::new(draws).unwrap();
659 let fit = PiecewisePareto::fit(
660 vec![1000.0, 3000.0],
661 &data,
662 Some((40_000.0, Truncation::LastPiece)),
663 )
664 .unwrap();
665 assert!(
666 (fit.alphas()[1] / 0.8 - 1.0).abs() < 0.03,
667 "{:?}",
668 fit.alphas()
669 );
670 assert!(fit.layer(1e4, 1e4) > 0.0);
671 assert!(
674 PiecewisePareto::fit(
675 vec![1000.0, 3000.0],
676 &data,
677 Some((40_000.0, Truncation::WholeDistribution)),
678 )
679 .is_ok()
680 );
681 }
682
683 #[test]
684 fn generalized_pareto_fit_maximizes_the_likelihood() {
685 let data = LargeLosses::new(vec![
686 1100.0, 1300.0, 1750.0, 2000.0, 2600.0, 3500.0, 4100.0, 9000.0, 25_000.0, 40_000.0,
687 ])
688 .unwrap()
689 .reporting_thresholds(vec![
690 0.0, 1200.0, 0.0, 1500.0, 0.0, 0.0, 3000.0, 5000.0, 0.0, 0.0,
691 ])
692 .unwrap()
693 .censored(vec![
694 false, false, false, false, false, true, false, false, false, true,
695 ])
696 .unwrap();
697 let t = 1000.0;
698 let ll = |alpha_ini: f64, alpha_tail: f64| -> f64 {
699 let g = Gpd::riegel(t, alpha_ini, alpha_tail).unwrap();
700 let k = alpha_ini / alpha_tail;
701 (0..data.len())
702 .map(|i| {
703 let (y, r) = (data.values[i], data.reporting[i].max(t));
704 let own = if data.censored[i] {
705 g.survival(y).ln()
706 } else {
707 (alpha_ini / t).ln() - (alpha_tail + 1.0) * (k * (y / t - 1.0)).ln_1p()
708 };
709 own - g.survival(r).ln()
710 })
711 .sum()
712 };
713 let fit = Gpd::fit_riegel(t, &data).unwrap();
714 let (ai, at) = (t / fit.beta(), 1.0 / fit.xi());
715 let best = ll(ai, at);
716 for (da, dt) in [
717 (1.001, 1.0),
718 (0.999, 1.0),
719 (1.0, 1.001),
720 (1.0, 0.999),
721 (1.001, 1.001),
722 ] {
723 assert!(ll(ai * da, at * dt) < best, "{da} {dt}");
724 }
725 let p = Pareto::new(t, 2.0).unwrap();
728 let draws = p.sample(&mut StreamRng::new(9, 0), 100_000);
729 let g = Gpd::fit_riegel(t, &LargeLosses::new(draws).unwrap()).unwrap();
730 assert!(((t / g.beta()) * g.xi() - 1.0).abs() < 0.05);
731 }
732
733 #[test]
734 fn whole_truncated_piecewise_fit_maximizes_the_likelihood() {
735 let t = vec![1000.0, 2500.0, 8000.0];
736 let tr = 60_000.0;
737 let data = LargeLosses::new(vec![
738 1100.0, 1300.0, 1750.0, 2000.0, 2600.0, 3500.0, 4100.0, 5200.0, 7000.0, 9000.0,
739 12_000.0, 18_000.0, 25_000.0, 40_000.0,
740 ])
741 .unwrap()
742 .reporting_thresholds(vec![
743 0.0, 1200.0, 0.0, 1500.0, 0.0, 0.0, 3000.0, 0.0, 0.0, 5000.0, 0.0, 0.0, 0.0, 0.0,
744 ])
745 .unwrap()
746 .censored(vec![
747 false, false, false, false, false, true, false, false, false, false, true, false,
748 false, true,
749 ])
750 .unwrap();
751 let ll = |alphas: &[f64]| -> f64 {
752 let open = PiecewisePareto::new(t.clone(), alphas.to_vec()).unwrap();
753 let pp = open
754 .clone()
755 .truncated(tr, Truncation::WholeDistribution)
756 .unwrap();
757 let mass = 1.0 - open.survival(tr);
758 (0..data.len())
759 .map(|i| {
760 let (y, r) = (data.values[i], data.reporting[i].max(t[0]));
761 let own = if data.censored[i] {
762 pp.survival(y).ln()
763 } else {
764 let k = t.partition_point(|&x| x <= y) - 1;
766 (alphas[k] * open.survival(y) / (y * mass)).ln()
767 };
768 own - pp.survival(r).ln()
769 })
770 .sum()
771 };
772 let fit = PiecewisePareto::fit(t.clone(), &data, Some((tr, Truncation::WholeDistribution)))
773 .unwrap();
774 let a = fit.alphas().to_vec();
775 let best = ll(&a);
776 for k in 0..3 {
777 for f in [1.001, 0.999] {
778 let mut b = a.clone();
779 b[k] *= f;
780 assert!(ll(&b) < best, "{k} {f}");
781 }
782 }
783 let truth = PiecewisePareto::new(t.clone(), vec![1.2, 0.8, 1.5])
785 .unwrap()
786 .truncated(tr, Truncation::WholeDistribution)
787 .unwrap();
788 let draws = truth.sample(&mut StreamRng::new(13, 0), 100_000);
789 let fit = PiecewisePareto::fit(
790 t,
791 &LargeLosses::new(draws).unwrap(),
792 Some((tr, Truncation::WholeDistribution)),
793 )
794 .unwrap();
795 for (got, want) in fit.alphas().iter().zip(truth.alphas()) {
796 assert!((got / want - 1.0).abs() < 0.05, "{got} {want}");
797 }
798 }
799
800 #[test]
801 fn rejects_bad_data() {
802 assert!(LargeLosses::new(vec![]).is_err());
803 assert!(LargeLosses::new(vec![-1.0]).is_err());
804 let d = LargeLosses::new(vec![10.0, 20.0]).unwrap();
805 assert!(d.clone().reporting_thresholds(vec![15.0, 0.0]).is_err());
806 assert!(d.clone().weights(vec![1.0]).is_err());
807 assert!(d.clone().weights(vec![1.0, 0.0]).is_err());
808 assert!(PiecewisePareto::fit(vec![5.0, 50.0], &d, None).is_err());
810 }
811}