| 1 | use super::*; |
| 2 | |
| 3 | use std::path::Path; |
| 4 | |
| 5 | use crate::estimate::{ConfidenceInterval, Estimate}; |
| 6 | use crate::stats::bivariate::regression::Slope; |
| 7 | use crate::stats::bivariate::Data; |
| 8 | |
| 9 | pub(crate) fn regression_figure( |
| 10 | title: Option<&str>, |
| 11 | path: &Path, |
| 12 | formatter: &dyn ValueFormatter, |
| 13 | measurements: &MeasurementData<'_>, |
| 14 | size: Option<(u32, u32)>, |
| 15 | ) { |
| 16 | let slope_estimate = measurements.absolute_estimates.slope.as_ref().unwrap(); |
| 17 | let slope_dist = measurements.distributions.slope.as_ref().unwrap(); |
| 18 | let (lb, ub) = |
| 19 | slope_dist.confidence_interval(slope_estimate.confidence_interval.confidence_level); |
| 20 | |
| 21 | let data = &measurements.data; |
| 22 | let (max_iters, typical) = (data.x().max(), data.y().max()); |
| 23 | let mut scaled_y: Vec<f64> = data.y().iter().cloned().collect(); |
| 24 | let unit = formatter.scale_values(typical, &mut scaled_y); |
| 25 | let scaled_y = Sample::new(&scaled_y); |
| 26 | |
| 27 | let point_estimate = Slope::fit(&measurements.data).0; |
| 28 | let mut scaled_points = [point_estimate * max_iters, lb * max_iters, ub * max_iters]; |
| 29 | let _ = formatter.scale_values(typical, &mut scaled_points); |
| 30 | let [point, lb, ub] = scaled_points; |
| 31 | |
| 32 | let exponent = (max_iters.log10() / 3.).floor() as i32 * 3; |
| 33 | |
| 34 | let x_scale = 10f64.powi(-exponent); |
| 35 | let x_label = if exponent == 0 { |
| 36 | "Iterations" .to_owned() |
| 37 | } else { |
| 38 | format!("Iterations (x 10^{})" , exponent) |
| 39 | }; |
| 40 | |
| 41 | let size = size.unwrap_or(SIZE); |
| 42 | let root_area = SVGBackend::new(path, size).into_drawing_area(); |
| 43 | |
| 44 | let mut cb = ChartBuilder::on(&root_area); |
| 45 | if let Some(title) = title { |
| 46 | cb.caption(title, (DEFAULT_FONT, 20)); |
| 47 | } |
| 48 | |
| 49 | let x_range = plotters::data::fitting_range(data.x().iter()); |
| 50 | let y_range = plotters::data::fitting_range(scaled_y.iter()); |
| 51 | |
| 52 | let mut chart = cb |
| 53 | .margin((5).percent()) |
| 54 | .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60)) |
| 55 | .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40)) |
| 56 | .build_cartesian_2d(x_range, y_range) |
| 57 | .unwrap(); |
| 58 | |
| 59 | chart |
| 60 | .configure_mesh() |
| 61 | .x_desc(x_label) |
| 62 | .y_desc(format!("Total sample time ({})" , unit)) |
| 63 | .x_label_formatter(&|x| pretty_print_float(x * x_scale, true)) |
| 64 | .light_line_style(TRANSPARENT) |
| 65 | .draw() |
| 66 | .unwrap(); |
| 67 | |
| 68 | chart |
| 69 | .draw_series( |
| 70 | data.x() |
| 71 | .iter() |
| 72 | .zip(scaled_y.iter()) |
| 73 | .map(|(x, y)| Circle::new((*x, *y), POINT_SIZE, DARK_BLUE.filled())), |
| 74 | ) |
| 75 | .unwrap() |
| 76 | .label("Sample" ) |
| 77 | .legend(|(x, y)| Circle::new((x + 10, y), POINT_SIZE, DARK_BLUE.filled())); |
| 78 | |
| 79 | chart |
| 80 | .draw_series(std::iter::once(PathElement::new( |
| 81 | vec![(0.0, 0.0), (max_iters, point)], |
| 82 | DARK_BLUE, |
| 83 | ))) |
| 84 | .unwrap() |
| 85 | .label("Linear regression" ) |
| 86 | .legend(|(x, y)| { |
| 87 | PathElement::new( |
| 88 | vec![(x, y), (x + 20, y)], |
| 89 | DARK_BLUE.filled().stroke_width(2), |
| 90 | ) |
| 91 | }); |
| 92 | |
| 93 | chart |
| 94 | .draw_series(std::iter::once(Polygon::new( |
| 95 | vec![(0.0, 0.0), (max_iters, lb), (max_iters, ub)], |
| 96 | DARK_BLUE.mix(0.25).filled(), |
| 97 | ))) |
| 98 | .unwrap() |
| 99 | .label("Confidence interval" ) |
| 100 | .legend(|(x, y)| { |
| 101 | Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.25).filled()) |
| 102 | }); |
| 103 | |
| 104 | if title.is_some() { |
| 105 | chart |
| 106 | .configure_series_labels() |
| 107 | .position(SeriesLabelPosition::UpperLeft) |
| 108 | .draw() |
| 109 | .unwrap(); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | pub(crate) fn regression_comparison_figure( |
| 114 | title: Option<&str>, |
| 115 | path: &Path, |
| 116 | formatter: &dyn ValueFormatter, |
| 117 | measurements: &MeasurementData<'_>, |
| 118 | comparison: &ComparisonData, |
| 119 | base_data: &Data<'_, f64, f64>, |
| 120 | size: Option<(u32, u32)>, |
| 121 | ) { |
| 122 | let data = &measurements.data; |
| 123 | let max_iters = base_data.x().max().max(data.x().max()); |
| 124 | let typical = base_data.y().max().max(data.y().max()); |
| 125 | |
| 126 | let exponent = (max_iters.log10() / 3.).floor() as i32 * 3; |
| 127 | let x_scale = 10f64.powi(-exponent); |
| 128 | |
| 129 | let x_label = if exponent == 0 { |
| 130 | "Iterations" .to_owned() |
| 131 | } else { |
| 132 | format!("Iterations (x 10^{})" , exponent) |
| 133 | }; |
| 134 | |
| 135 | let Estimate { |
| 136 | confidence_interval: |
| 137 | ConfidenceInterval { |
| 138 | lower_bound: base_lb, |
| 139 | upper_bound: base_ub, |
| 140 | .. |
| 141 | }, |
| 142 | point_estimate: base_point, |
| 143 | .. |
| 144 | } = comparison.base_estimates.slope.as_ref().unwrap(); |
| 145 | |
| 146 | let Estimate { |
| 147 | confidence_interval: |
| 148 | ConfidenceInterval { |
| 149 | lower_bound: lb, |
| 150 | upper_bound: ub, |
| 151 | .. |
| 152 | }, |
| 153 | point_estimate: point, |
| 154 | .. |
| 155 | } = measurements.absolute_estimates.slope.as_ref().unwrap(); |
| 156 | |
| 157 | let mut points = [ |
| 158 | base_lb * max_iters, |
| 159 | base_point * max_iters, |
| 160 | base_ub * max_iters, |
| 161 | lb * max_iters, |
| 162 | point * max_iters, |
| 163 | ub * max_iters, |
| 164 | ]; |
| 165 | let unit = formatter.scale_values(typical, &mut points); |
| 166 | let [base_lb, base_point, base_ub, lb, point, ub] = points; |
| 167 | |
| 168 | let y_max = point.max(base_point); |
| 169 | |
| 170 | let size = size.unwrap_or(SIZE); |
| 171 | let root_area = SVGBackend::new(path, size).into_drawing_area(); |
| 172 | |
| 173 | let mut cb = ChartBuilder::on(&root_area); |
| 174 | if let Some(title) = title { |
| 175 | cb.caption(title, (DEFAULT_FONT, 20)); |
| 176 | } |
| 177 | |
| 178 | let mut chart = cb |
| 179 | .margin((5).percent()) |
| 180 | .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60)) |
| 181 | .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40)) |
| 182 | .build_cartesian_2d(0.0..max_iters, 0.0..y_max) |
| 183 | .unwrap(); |
| 184 | |
| 185 | chart |
| 186 | .configure_mesh() |
| 187 | .x_desc(x_label) |
| 188 | .y_desc(format!("Total sample time ({})" , unit)) |
| 189 | .x_label_formatter(&|x| pretty_print_float(x * x_scale, true)) |
| 190 | .light_line_style(TRANSPARENT) |
| 191 | .draw() |
| 192 | .unwrap(); |
| 193 | |
| 194 | chart |
| 195 | .draw_series(vec![ |
| 196 | PathElement::new(vec![(0.0, 0.0), (max_iters, base_point)], DARK_RED).into_dyn(), |
| 197 | Polygon::new( |
| 198 | vec![(0.0, 0.0), (max_iters, base_lb), (max_iters, base_ub)], |
| 199 | DARK_RED.mix(0.25).filled(), |
| 200 | ) |
| 201 | .into_dyn(), |
| 202 | ]) |
| 203 | .unwrap() |
| 204 | .label("Base Sample" ) |
| 205 | .legend(|(x, y)| { |
| 206 | PathElement::new(vec![(x, y), (x + 20, y)], DARK_RED.filled().stroke_width(2)) |
| 207 | }); |
| 208 | |
| 209 | chart |
| 210 | .draw_series(vec![ |
| 211 | PathElement::new(vec![(0.0, 0.0), (max_iters, point)], DARK_BLUE).into_dyn(), |
| 212 | Polygon::new( |
| 213 | vec![(0.0, 0.0), (max_iters, lb), (max_iters, ub)], |
| 214 | DARK_BLUE.mix(0.25).filled(), |
| 215 | ) |
| 216 | .into_dyn(), |
| 217 | ]) |
| 218 | .unwrap() |
| 219 | .label("New Sample" ) |
| 220 | .legend(|(x, y)| { |
| 221 | PathElement::new( |
| 222 | vec![(x, y), (x + 20, y)], |
| 223 | DARK_BLUE.filled().stroke_width(2), |
| 224 | ) |
| 225 | }); |
| 226 | |
| 227 | if title.is_some() { |
| 228 | chart |
| 229 | .configure_series_labels() |
| 230 | .position(SeriesLabelPosition::UpperLeft) |
| 231 | .draw() |
| 232 | .unwrap(); |
| 233 | } |
| 234 | } |
| 235 | |