plotters_statistical/series/
residual_plot.rs1use plotters::element::{Drawable, PointCollection};
10use plotters::style::RGBColor;
11use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
12
13use crate::stats::StatsError;
14use crate::style::{stroke_style, translucent_fill};
15
16const DEFAULT_POINT_COLOR: RGBColor = RGBColor(0, 114, 178); const ZERO_LINE_GRAY: RGBColor = RGBColor(120, 120, 120);
18const TREND_COLOR: RGBColor = RGBColor(213, 94, 0); const DEFAULT_BINS: usize = 12;
20
21#[derive(Debug, Clone)]
24pub struct ResidualPlot {
25 points: Vec<(f64, f64)>,
28 n_points: usize,
29 n_trend: usize,
30 show_trend: bool,
31 marker_radius: u32,
32 point_color: RGBColor,
33 trend_width: u32,
34}
35
36impl ResidualPlot {
37 pub fn from_residuals(fitted: &[f64], residuals: &[f64]) -> Result<Self, StatsError> {
39 if fitted.len() != residuals.len() {
40 return Err(StatsError::LengthMismatch {
41 scores: fitted.len(),
42 labels: residuals.len(),
43 });
44 }
45 let pairs: Vec<(f64, f64)> = fitted
46 .iter()
47 .zip(residuals)
48 .map(|(&x, &r)| (x, r))
49 .filter(|(x, r)| x.is_finite() && r.is_finite())
50 .collect();
51 Self::from_pairs(pairs)
52 }
53
54 pub fn from_predictions(predicted: &[f64], actual: &[f64]) -> Result<Self, StatsError> {
57 if predicted.len() != actual.len() {
58 return Err(StatsError::LengthMismatch {
59 scores: predicted.len(),
60 labels: actual.len(),
61 });
62 }
63 let residuals: Vec<f64> = predicted.iter().zip(actual).map(|(&p, &a)| a - p).collect();
64 Self::from_residuals(predicted, &residuals)
65 }
66
67 fn from_pairs(pairs: Vec<(f64, f64)>) -> Result<Self, StatsError> {
68 if pairs.is_empty() {
69 return Err(StatsError::EmptyInput);
70 }
71 let n_points = pairs.len();
72 let xmin = pairs.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
73 let xmax = pairs.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
74
75 let trend = binned_trend(&pairs, DEFAULT_BINS);
76
77 let mut points = pairs;
78 points.push((xmin, 0.0));
79 points.push((xmax, 0.0));
80 let n_trend = trend.len();
81 points.extend(trend);
82
83 Ok(Self {
84 points,
85 n_points,
86 n_trend,
87 show_trend: false,
88 marker_radius: 3,
89 point_color: DEFAULT_POINT_COLOR,
90 trend_width: 2,
91 })
92 }
93
94 pub fn trend(mut self, show: bool) -> Self {
96 self.show_trend = show;
97 self
98 }
99
100 pub fn marker_radius(mut self, radius: u32) -> Self {
102 self.marker_radius = radius;
103 self
104 }
105
106 pub fn color(mut self, color: RGBColor) -> Self {
108 self.point_color = color;
109 self
110 }
111}
112
113impl<'a> PointCollection<'a, (f64, f64)> for &'a ResidualPlot {
114 type Point = &'a (f64, f64);
115 type IntoIter = &'a [(f64, f64)];
116 fn point_iter(self) -> &'a [(f64, f64)] {
117 &self.points
118 }
119}
120
121impl<DB: DrawingBackend> Drawable<DB> for ResidualPlot {
122 fn draw<I: Iterator<Item = BackendCoord>>(
123 &self,
124 points: I,
125 backend: &mut DB,
126 _parent_dim: (u32, u32),
127 ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
128 let pix: Vec<BackendCoord> = points.collect();
129 if pix.len() < self.n_points + 2 {
130 return Ok(());
131 }
132 let anchor_l = pix[self.n_points];
134 let anchor_r = pix[self.n_points + 1];
135 backend.draw_line(anchor_l, anchor_r, &stroke_style(&ZERO_LINE_GRAY, 1))?;
136
137 let marker = translucent_fill(&self.point_color, 0.7);
139 for p in &pix[..self.n_points] {
140 backend.draw_circle(*p, self.marker_radius, &marker, true)?;
141 }
142
143 if self.show_trend && self.n_trend >= 2 {
145 let start = self.n_points + 2;
146 let trend = &pix[start..start + self.n_trend];
147 backend.draw_path(
148 trend.iter().copied(),
149 &stroke_style(&TREND_COLOR, self.trend_width),
150 )?;
151 }
152 Ok(())
153 }
154}
155
156fn binned_trend(pairs: &[(f64, f64)], bins: usize) -> Vec<(f64, f64)> {
159 let bins = bins.max(1);
160 let xmin = pairs.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
161 let xmax = pairs.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
162 if !(xmin.is_finite() && xmax.is_finite()) || xmax <= xmin {
163 return Vec::new();
164 }
165 let width = (xmax - xmin) / bins as f64;
166 let mut sums = vec![0.0_f64; bins];
167 let mut counts = vec![0usize; bins];
168 for &(x, r) in pairs {
169 let mut idx = ((x - xmin) / width).floor() as usize;
170 if idx >= bins {
171 idx = bins - 1; }
173 sums[idx] += r;
174 counts[idx] += 1;
175 }
176 (0..bins)
177 .filter(|&b| counts[b] > 0)
178 .map(|b| {
179 let center = xmin + width * (b as f64 + 0.5);
180 (center, sums[b] / counts[b] as f64)
181 })
182 .collect()
183}