diff options
Diffstat (limited to 'src/plot/plotters_backend/distributions.rs')
-rwxr-xr-x | src/plot/plotters_backend/distributions.rs | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/src/plot/plotters_backend/distributions.rs b/src/plot/plotters_backend/distributions.rs new file mode 100755 index 0000000..ed4d61a --- /dev/null +++ b/src/plot/plotters_backend/distributions.rs @@ -0,0 +1,309 @@ +use super::*; +use crate::estimate::Estimate; +use crate::estimate::Statistic; +use crate::measurement::ValueFormatter; +use crate::report::{BenchmarkId, MeasurementData, ReportContext}; +use crate::stats::Distribution; + +fn abs_distribution( + id: &BenchmarkId, + context: &ReportContext, + formatter: &dyn ValueFormatter, + statistic: Statistic, + distribution: &Distribution<f64>, + estimate: &Estimate, + size: Option<(u32, u32)>, +) { + let ci = &estimate.confidence_interval; + let typical = ci.upper_bound; + let mut ci_values = [ci.lower_bound, ci.upper_bound, estimate.point_estimate]; + let unit = formatter.scale_values(typical, &mut ci_values); + let (lb, ub, point) = (ci_values[0], ci_values[1], ci_values[2]); + + let start = lb - (ub - lb) / 9.; + let end = ub + (ub - lb) / 9.; + let mut scaled_xs: Vec<f64> = distribution.iter().cloned().collect(); + let _ = formatter.scale_values(typical, &mut scaled_xs); + let scaled_xs_sample = Sample::new(&scaled_xs); + let (kde_xs, ys) = kde::sweep(scaled_xs_sample, KDE_POINTS, Some((start, end))); + + // interpolate between two points of the KDE sweep to find the Y position at the point estimate. + let n_point = kde_xs + .iter() + .position(|&x| x >= point) + .unwrap_or(kde_xs.len() - 1) + .max(1); // Must be at least the second element or this will panic + let slope = (ys[n_point] - ys[n_point - 1]) / (kde_xs[n_point] - kde_xs[n_point - 1]); + let y_point = ys[n_point - 1] + (slope * (point - kde_xs[n_point - 1])); + + let start = kde_xs + .iter() + .enumerate() + .find(|&(_, &x)| x >= lb) + .unwrap() + .0; + let end = kde_xs + .iter() + .enumerate() + .rev() + .find(|&(_, &x)| x <= ub) + .unwrap() + .0; + let len = end - start; + + let kde_xs_sample = Sample::new(&kde_xs); + + let path = context.report_path(id, &format!("{}.svg", statistic)); + let root_area = SVGBackend::new(&path, size.unwrap_or(SIZE)).into_drawing_area(); + + let x_range = plotters::data::fitting_range(kde_xs_sample.iter()); + let mut y_range = plotters::data::fitting_range(ys.iter()); + + y_range.end *= 1.1; + + let mut chart = ChartBuilder::on(&root_area) + .margin((5).percent()) + .caption( + format!("{}:{}", id.as_title(), statistic), + (DEFAULT_FONT, 20), + ) + .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60)) + .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40)) + .build_ranged(x_range, y_range) + .unwrap(); + + chart + .configure_mesh() + .disable_mesh() + .x_desc(format!("Average time ({})", unit)) + .y_desc("Density (a.u.)") + .x_label_formatter(&|&v| pretty_print_float(v, true)) + .y_label_formatter(&|&v| pretty_print_float(v, true)) + .draw() + .unwrap(); + + chart + .draw_series(LineSeries::new( + kde_xs.iter().zip(ys.iter()).map(|(&x, &y)| (x, y)), + &DARK_BLUE, + )) + .unwrap() + .label("Bootstrap distribution") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &DARK_BLUE)); + + chart + .draw_series(AreaSeries::new( + kde_xs + .iter() + .zip(ys.iter()) + .skip(start) + .take(len) + .map(|(&x, &y)| (x, y)), + 0.0, + DARK_BLUE.mix(0.25).filled().stroke_width(3), + )) + .unwrap() + .label("Confidence interval") + .legend(|(x, y)| { + Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.25).filled()) + }); + + chart + .draw_series(std::iter::once(PathElement::new( + vec![(point, 0.0), (point, y_point)], + DARK_BLUE.filled().stroke_width(3), + ))) + .unwrap() + .label("Point estimate") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &DARK_BLUE)); + + chart + .configure_series_labels() + .position(SeriesLabelPosition::UpperRight) + .draw() + .unwrap(); +} + +pub(crate) fn abs_distributions( + id: &BenchmarkId, + context: &ReportContext, + formatter: &dyn ValueFormatter, + measurements: &MeasurementData<'_>, + size: Option<(u32, u32)>, +) { + crate::plot::REPORT_STATS + .iter() + .filter_map(|stat| { + measurements.distributions.get(*stat).and_then(|dist| { + measurements + .absolute_estimates + .get(*stat) + .map(|est| (*stat, dist, est)) + }) + }) + .for_each(|(statistic, distribution, estimate)| { + abs_distribution( + id, + context, + formatter, + statistic, + distribution, + estimate, + size, + ) + }) +} + +fn rel_distribution( + id: &BenchmarkId, + context: &ReportContext, + statistic: Statistic, + distribution: &Distribution<f64>, + estimate: &Estimate, + noise_threshold: f64, + size: Option<(u32, u32)>, +) { + let ci = &estimate.confidence_interval; + let (lb, ub) = (ci.lower_bound, ci.upper_bound); + + let start = lb - (ub - lb) / 9.; + let end = ub + (ub - lb) / 9.; + let (xs, ys) = kde::sweep(distribution, KDE_POINTS, Some((start, end))); + let xs_ = Sample::new(&xs); + + // interpolate between two points of the KDE sweep to find the Y position at the point estimate. + let point = estimate.point_estimate; + let n_point = xs + .iter() + .position(|&x| x >= point) + .unwrap_or(ys.len() - 1) + .max(1); + let slope = (ys[n_point] - ys[n_point - 1]) / (xs[n_point] - xs[n_point - 1]); + let y_point = ys[n_point - 1] + (slope * (point - xs[n_point - 1])); + + let start = xs.iter().enumerate().find(|&(_, &x)| x >= lb).unwrap().0; + let end = xs + .iter() + .enumerate() + .rev() + .find(|&(_, &x)| x <= ub) + .unwrap() + .0; + let len = end - start; + + let x_min = xs_.min(); + let x_max = xs_.max(); + + let (fc_start, fc_end) = if noise_threshold < x_min || -noise_threshold > x_max { + let middle = (x_min + x_max) / 2.; + + (middle, middle) + } else { + ( + if -noise_threshold < x_min { + x_min + } else { + -noise_threshold + }, + if noise_threshold > x_max { + x_max + } else { + noise_threshold + }, + ) + }; + let y_range = plotters::data::fitting_range(ys.iter()); + let path = context.report_path(id, &format!("change/{}.svg", statistic)); + let root_area = SVGBackend::new(&path, size.unwrap_or(SIZE)).into_drawing_area(); + + let mut chart = ChartBuilder::on(&root_area) + .margin((5).percent()) + .caption( + format!("{}:{}", id.as_title(), statistic), + (DEFAULT_FONT, 20), + ) + .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60)) + .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40)) + .build_ranged(x_min..x_max, y_range.clone()) + .unwrap(); + + chart + .configure_mesh() + .disable_mesh() + .x_desc("Relative change (%)") + .y_desc("Density (a.u.)") + .x_label_formatter(&|&v| pretty_print_float(v, true)) + .y_label_formatter(&|&v| pretty_print_float(v, true)) + .draw() + .unwrap(); + + chart + .draw_series(LineSeries::new( + xs.iter().zip(ys.iter()).map(|(x, y)| (*x, *y)), + &DARK_BLUE, + )) + .unwrap() + .label("Bootstrap distribution") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &DARK_BLUE)); + + chart + .draw_series(AreaSeries::new( + xs.iter() + .zip(ys.iter()) + .skip(start) + .take(len) + .map(|(x, y)| (*x, *y)), + 0.0, + DARK_BLUE.mix(0.25).filled().stroke_width(3), + )) + .unwrap() + .label("Confidence interval") + .legend(|(x, y)| { + Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.25).filled()) + }); + + chart + .draw_series(std::iter::once(PathElement::new( + vec![(point, 0.0), (point, y_point)], + DARK_BLUE.filled().stroke_width(3), + ))) + .unwrap() + .label("Point estimate") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &DARK_BLUE)); + + chart + .draw_series(std::iter::once(Rectangle::new( + [(fc_start, y_range.start), (fc_end, y_range.end)], + DARK_RED.mix(0.1).filled(), + ))) + .unwrap() + .label("Noise threshold") + .legend(|(x, y)| { + Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_RED.mix(0.25).filled()) + }); + chart + .configure_series_labels() + .position(SeriesLabelPosition::UpperRight) + .draw() + .unwrap(); +} + +pub(crate) fn rel_distributions( + id: &BenchmarkId, + context: &ReportContext, + _measurements: &MeasurementData<'_>, + comparison: &ComparisonData, + size: Option<(u32, u32)>, +) { + crate::plot::CHANGE_STATS.iter().for_each(|&statistic| { + rel_distribution( + id, + context, + statistic, + comparison.relative_distributions.get(statistic), + comparison.relative_estimates.get(statistic), + comparison.noise_threshold, + size, + ) + }); +} |