plotters_statistical/series/
roc_curve.rs1use plotters::element::{Drawable, PointCollection};
9use plotters::style::{RGBColor, ShapeStyle};
10use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
11
12use crate::stats::roc::auc_trapezoid;
13use crate::stats::{roc_curve as compute_roc, StatsError};
14use crate::style::{stroke_style, translucent_fill};
15
16const DEFAULT_COLOR: RGBColor = RGBColor(0, 114, 178); const BASELINE_GRAY: RGBColor = RGBColor(150, 150, 150);
18
19#[derive(Debug, Clone)]
22pub struct RocCurve {
23 points: Vec<(f64, f64)>,
26 n_curve: usize,
27 auc: f64,
28 color: RGBColor,
29 stroke_width: u32,
30 shade: bool,
31 shade_alpha: f64,
32 baseline: bool,
33}
34
35impl RocCurve {
36 pub fn from_scores(scores: &[f64], labels: &[bool]) -> Result<Self, StatsError> {
38 let data = compute_roc(scores, labels)?;
39 let pts: Vec<(f64, f64)> = data.points.iter().map(|p| (p.fpr, p.tpr)).collect();
40 Ok(Self::new(pts, data.auc))
41 }
42
43 pub fn from_points(points: Vec<(f64, f64)>) -> Self {
46 let auc = auc_trapezoid(&points);
47 Self::new(points, auc)
48 }
49
50 fn new(mut points: Vec<(f64, f64)>, auc: f64) -> Self {
51 let n_curve = points.len();
52 points.push((1.0, 0.0)); Self {
54 points,
55 n_curve,
56 auc,
57 color: DEFAULT_COLOR,
58 stroke_width: 2,
59 shade: false,
60 shade_alpha: 0.15,
61 baseline: false,
62 }
63 }
64
65 pub fn auc(&self) -> f64 {
67 self.auc
68 }
69
70 pub fn legend_label(&self, name: &str) -> String {
73 format!("{name} (AUC = {:.2})", self.auc)
74 }
75
76 pub fn color(mut self, color: RGBColor) -> Self {
78 self.color = color;
79 self
80 }
81
82 pub fn stroke_width(mut self, width: u32) -> Self {
84 self.stroke_width = width;
85 self
86 }
87
88 pub fn shade_area(mut self, shade: bool) -> Self {
90 self.shade = shade;
91 self
92 }
93
94 pub fn with_baseline(mut self) -> Self {
97 self.baseline = true;
98 self
99 }
100}
101
102impl<'a> PointCollection<'a, (f64, f64)> for &'a RocCurve {
103 type Point = &'a (f64, f64);
104 type IntoIter = &'a [(f64, f64)];
105 fn point_iter(self) -> &'a [(f64, f64)] {
106 &self.points
107 }
108}
109
110impl<DB: DrawingBackend> Drawable<DB> for RocCurve {
111 fn draw<I: Iterator<Item = BackendCoord>>(
112 &self,
113 points: I,
114 backend: &mut DB,
115 _parent_dim: (u32, u32),
116 ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
117 let pix: Vec<BackendCoord> = points.collect();
118 if pix.len() < self.n_curve + 1 || self.n_curve < 2 {
119 return Ok(());
120 }
121 let curve = &pix[..self.n_curve];
122 let anchor = pix[self.n_curve]; if self.shade {
126 let mut poly = curve.to_vec();
127 poly.push(anchor);
128 poly.push(curve[0]); backend.fill_polygon(poly, &translucent_fill(&self.color, self.shade_alpha))?;
130 }
131
132 if self.baseline {
134 let dashed: ShapeStyle = stroke_style(&BASELINE_GRAY, 1);
135 backend.draw_line(curve[0], curve[self.n_curve - 1], &dashed)?;
136 }
137
138 backend.draw_path(
139 curve.iter().copied(),
140 &stroke_style(&self.color, self.stroke_width),
141 )?;
142 Ok(())
143 }
144}