Skip to main content

plotters_statistical/series/
calibration_curve.rs

1//! Calibration curve (reliability diagram): binned predicted probability vs
2//! observed frequency, with the perfect-calibration `y = x` diagonal.
3
4use plotters::element::{Drawable, PointCollection};
5use plotters::style::RGBColor;
6use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
7
8use crate::stats::{calibration_curve, StatsError};
9use crate::style::{stroke_style, translucent_fill};
10
11const DEFAULT_COLOR: RGBColor = RGBColor(0, 158, 115); // Okabe–Ito green
12const DIAGONAL_GRAY: RGBColor = RGBColor(150, 150, 150);
13
14/// A reliability diagram as a drawable series (coordinate space `(f64, f64)` =
15/// `(mean predicted probability, observed frequency)`).
16#[derive(Debug, Clone)]
17pub struct CalibrationCurve {
18    // [bin points (n_bins_used)] then [2 diagonal endpoints (0,0),(1,1)].
19    points: Vec<(f64, f64)>,
20    n_pts: usize,
21    color: RGBColor,
22    stroke_width: u32,
23    marker_radius: u32,
24    show_markers: bool,
25    show_diagonal: bool,
26}
27
28impl CalibrationCurve {
29    /// Build from predicted `scores` (probabilities) and true binary `labels`,
30    /// using `n_bins` equal-width bins over `[0, 1]`.
31    pub fn from_scores(scores: &[f64], labels: &[bool], n_bins: usize) -> Result<Self, StatsError> {
32        let bins = calibration_curve(scores, labels, n_bins)?;
33        let mut points: Vec<(f64, f64)> = bins
34            .iter()
35            .map(|b| (b.mean_predicted, b.observed_freq))
36            .collect();
37        let n_pts = points.len();
38        points.push((0.0, 0.0));
39        points.push((1.0, 1.0));
40        Ok(Self {
41            points,
42            n_pts,
43            color: DEFAULT_COLOR,
44            stroke_width: 2,
45            marker_radius: 4,
46            show_markers: true,
47            show_diagonal: true,
48        })
49    }
50
51    /// Show/hide the `y = x` perfect-calibration reference line.
52    pub fn diagonal(mut self, show: bool) -> Self {
53        self.show_diagonal = show;
54        self
55    }
56
57    /// Show/hide the per-bin markers.
58    pub fn markers(mut self, show: bool) -> Self {
59        self.show_markers = show;
60        self
61    }
62
63    /// Set the line/marker color.
64    pub fn color(mut self, color: RGBColor) -> Self {
65        self.color = color;
66        self
67    }
68
69    /// Set the line stroke width in pixels.
70    pub fn stroke_width(mut self, width: u32) -> Self {
71        self.stroke_width = width;
72        self
73    }
74}
75
76impl<'a> PointCollection<'a, (f64, f64)> for &'a CalibrationCurve {
77    type Point = &'a (f64, f64);
78    type IntoIter = &'a [(f64, f64)];
79    fn point_iter(self) -> &'a [(f64, f64)] {
80        &self.points
81    }
82}
83
84impl<DB: DrawingBackend> Drawable<DB> for CalibrationCurve {
85    fn draw<I: Iterator<Item = BackendCoord>>(
86        &self,
87        points: I,
88        backend: &mut DB,
89        _parent_dim: (u32, u32),
90    ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
91        let pix: Vec<BackendCoord> = points.collect();
92        if pix.len() < self.n_pts + 2 {
93            return Ok(());
94        }
95        if self.show_diagonal {
96            backend.draw_line(
97                pix[self.n_pts],
98                pix[self.n_pts + 1],
99                &stroke_style(&DIAGONAL_GRAY, 1),
100            )?;
101        }
102        let curve = &pix[..self.n_pts];
103        if curve.len() >= 2 {
104            backend.draw_path(
105                curve.iter().copied(),
106                &stroke_style(&self.color, self.stroke_width),
107            )?;
108        }
109        if self.show_markers {
110            let fill = translucent_fill(&self.color, 0.9);
111            for p in curve {
112                backend.draw_circle(*p, self.marker_radius, &fill, true)?;
113            }
114        }
115        Ok(())
116    }
117}