Skip to content

Commit

Permalink
Merge pull request #59 from JackKCWong/main
Browse files Browse the repository at this point in the history
add percentiles to summary output
  • Loading branch information
juan-leon authored Feb 10, 2024
2 parents a951273 + efddcc9 commit f78c799
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 23 deletions.
60 changes: 57 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ tempfile = "3"
assert_cmd = "^2"
predicates = "^3"
serial_test = "2"
rand = "0.8.5"
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! ```rust,no_run
//! use lowcharts::plot;
//!
//! let vec = &[-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99];
//! let vec = &mut [-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99];
//! // Plot a histogram of the above vector, with 4 buckets and a precision
//! // chosen by library
//! let options = plot::HistogramOptions { intervals: 4, ..Default::default() };
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fn histogram(matches: &ArgMatches) -> i32 {
Ok(r) => r,
_ => return 2,
};
let vec = reader.read(matches.value_of("input").unwrap());
let mut vec = reader.read(matches.value_of("input").unwrap());
if !assert_data(&vec, 1) {
return 1;
}
Expand All @@ -117,7 +117,7 @@ fn histogram(matches: &ArgMatches) -> i32 {
options.log_scale = matches.is_present("log-scale");
options.intervals = matches.value_of_t("intervals").unwrap();
let width = matches.value_of_t("width").unwrap();
let histogram = plot::Histogram::new(&vec, options);
let histogram = plot::Histogram::new(&mut vec, options);
print!("{histogram:width$}");
0
}
Expand Down
18 changes: 9 additions & 9 deletions src/plot/histogram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl Histogram {
///
/// `options` is a `HistogramOptions` struct with the preferences to create
/// histogram.
pub fn new(vec: &[f64], mut options: HistogramOptions) -> Self {
pub fn new(vec: &mut [f64], mut options: HistogramOptions) -> Self {
let mut stats = Stats::new(vec, options.precision);
if options.log_scale {
stats.min = 0.0; // We will silently discard negative values
Expand Down Expand Up @@ -222,7 +222,7 @@ mod tests {

#[test]
fn test_buckets() {
let stats = Stats::new(&[-2.0, 14.0], None);
let stats = Stats::new(&mut [-2.0, 14.0], None);
let options = HistogramOptions {
intervals: 8,
..Default::default()
Expand All @@ -247,14 +247,14 @@ mod tests {
intervals: 6,
..Default::default()
};
let mut hist = Histogram::new_with_stats(Stats::new(&[-2.0, 4.0], None), &options);
let mut hist = Histogram::new_with_stats(Stats::new(&mut [-2.0, 4.0], None), &options);
hist.load(&[-1.0, 2.0, -1.0, 2.0, 10.0, 10.0, 10.0, -10.0]);
assert_eq!(hist.top, 2);
}

#[test]
fn display_test() {
let stats = Stats::new(&[-2.0, 14.0], None);
let stats = Stats::new(&mut [-2.0, 14.0], None);
let options = HistogramOptions {
intervals: 8,
precision: Some(3),
Expand All @@ -280,7 +280,7 @@ mod tests {
precision: Some(3),
..Default::default()
};
let mut hist = Histogram::new_with_stats(Stats::new(&[-2.0, 14.0], None), &options);
let mut hist = Histogram::new_with_stats(Stats::new(&mut [-2.0, 14.0], None), &options);
hist.load(&[
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96,
]);
Expand All @@ -291,7 +291,7 @@ mod tests {

#[test]
fn display_test_human_units() {
let vector = &[
let vector = &mut [
-1.0,
-12000000.0,
-12000001.0,
Expand Down Expand Up @@ -320,7 +320,7 @@ mod tests {
#[test]
fn display_test_log_scale() {
let hist = Histogram::new(
&[0.4, 0.4, 0.4, 0.4, 255.0, 0.2, 1.2, 128.0, 126.0, -7.0],
&mut [0.4, 0.4, 0.4, 0.4, 255.0, 0.2, 1.2, 128.0, 126.0, -7.0],
HistogramOptions {
intervals: 8,
log_scale: true,
Expand Down Expand Up @@ -402,7 +402,7 @@ mod tests {
intervals: 8,
..Default::default()
};
let hist = Histogram::new_with_stats(Stats::new(&[-12.0, 4.0], None), &options);
let hist = Histogram::new_with_stats(Stats::new(&mut [-12.0, 4.0], None), &options);
assert!(hist.find_slot(-13.0) == None);
assert!(hist.find_slot(13.0) == None);
assert!(hist.find_slot(-12.0) == Some(0));
Expand All @@ -416,7 +416,7 @@ mod tests {
fn find_slot_logarithmic() {
let hist = Histogram::new(
// More than 8 values to avoid interval truncation
&[255.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -2000.0],
&mut [255.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -2000.0],
HistogramOptions {
intervals: 8,
log_scale: true,
Expand Down
14 changes: 10 additions & 4 deletions src/plot/xy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::BorrowMut;
use std::fmt;
use std::ops::Range;

Expand Down Expand Up @@ -32,7 +33,12 @@ impl XyPlot {
/// "None" is used, human units will be used, with an heuristic based on the
/// input data for deciding the units and the decimal places.
pub fn new(vec: &[f64], width: usize, height: usize, precision: Option<usize>) -> Self {
let mut plot = Self::new_with_stats(width, height, Stats::new(vec, precision), precision);
let mut plot = Self::new_with_stats(
width,
height,
Stats::new(vec.to_vec().borrow_mut(), precision),
precision,
);
plot.load(vec);
plot
}
Expand Down Expand Up @@ -132,7 +138,7 @@ mod tests {

#[test]
fn basic_test() {
let stats = Stats::new(&[-1.0, 4.0], None);
let stats = Stats::new(&mut [-1.0, 4.0], None);
let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3));
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]);
assert_float_eq!(plot.x_axis[0], -0.5, rmax <= f64::EPSILON);
Expand All @@ -146,7 +152,7 @@ mod tests {

#[test]
fn display_test() {
let stats = Stats::new(&[-1.0, 4.0], None);
let stats = Stats::new(&mut [-1.0, 4.0], None);
let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3));
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]);
Paint::disable();
Expand All @@ -159,7 +165,7 @@ mod tests {

#[test]
fn display_test_human_units() {
let vector = &[1000000.0, -1000000.0, -2000000.0, -4000000.0];
let vector = &mut [1000000.0, -1000000.0, -2000000.0, -4000000.0];
let plot = XyPlot::new(vector, 3, 5, None);
Paint::disable();
let display = format!("{plot}");
Expand Down
57 changes: 53 additions & 4 deletions src/stats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ pub struct Stats {
/// Number of samples of the input values.
pub samples: usize,
precision: Option<usize>, // If None, then human friendly display will be used

/// 50 percentile
pub p50: f64,
/// 90 percentile
pub p90: f64,
/// 95 percentile
pub p95: f64,
/// 99 percentile
pub p99: f64,
}

fn percentiles(vec: &mut [f64]) -> (f64, f64, f64, f64) {
vec.sort_by(|a, b| a.partial_cmp(b).unwrap());

let len = vec.len();
let p50 = vec[len / 2];
let p90 = vec[(len * 9) / 10];
let p95 = vec[(len * 95) / 100];
let p99 = vec[(len * 99) / 100];

(p50, p90, p95, p99)
}

impl Stats {
Expand All @@ -29,7 +50,7 @@ impl Stats {
/// `precision` is an Option with the number of decimals to display. If
/// "None" is used, human units will be used, with an heuristic based on the
/// input data for deciding the units and the decimal places.
pub fn new(vec: &[f64], precision: Option<usize>) -> Self {
pub fn new(vec: &mut [f64], precision: Option<usize>) -> Self {
let mut max = vec[0];
let mut min = max;
let mut temp: f64 = 0.0;
Expand All @@ -42,6 +63,7 @@ impl Stats {
}
let var = temp / vec.len() as f64;
let std = var.sqrt();
let (p50, p90, p95, p99) = percentiles(vec);
Self {
min,
max,
Expand All @@ -50,6 +72,10 @@ impl Stats {
var,
samples: vec.len(),
precision,
p50,
p90,
p95,
p99,
}
}
}
Expand All @@ -73,6 +99,14 @@ impl fmt::Display for Stats {
avg = Blue.paint(formatter.format(self.avg)),
var = Blue.paint(format!("{:.3}", self.var)),
std = Blue.paint(format!("{:.3}", self.std)),
)?;
writeln!(
f,
"p50 = {p50}; p90 = {p90}; p95 = {p95}; p99 = {p99}",
p50 = Blue.paint(formatter.format(self.p50)),
p90 = Blue.paint(formatter.format(self.p90)),
p95 = Blue.paint(formatter.format(self.p95)),
p99 = Blue.paint(formatter.format(self.p99)),
)
}
}
Expand All @@ -81,11 +115,12 @@ impl fmt::Display for Stats {
mod tests {
use super::*;
use float_eq::assert_float_eq;
use rand::{seq::SliceRandom, thread_rng};
use yansi::Paint;

#[test]
fn basic_test() {
let stats = Stats::new(&[1.1, 3.3, 2.2], Some(3));
let stats = Stats::new(&mut [1.1, 3.3, 2.2], Some(3));
assert_eq!(3_usize, stats.samples);
assert_float_eq!(stats.avg, 2.2, rmax <= f64::EPSILON);
assert_float_eq!(stats.min, 1.1, rmax <= f64::EPSILON);
Expand All @@ -96,7 +131,7 @@ mod tests {

#[test]
fn test_display() {
let stats = Stats::new(&[1.1, 3.3, 2.2], Some(3));
let stats = Stats::new(&mut [1.1, 3.3, 2.2], Some(3));
Paint::disable();
let display = format!("{stats}");
assert!(display.contains("Samples = 3"));
Expand All @@ -107,11 +142,25 @@ mod tests {

#[test]
fn test_big_num() {
let stats = Stats::new(&[123456789.1234, 123456788.1234], None);
let stats = Stats::new(&mut [123456789.1234, 123456788.1234], None);
Paint::disable();
let display = format!("{stats}");
assert!(display.contains("Samples = 2"));
assert!(display.contains("Min = 123456788.123"));
assert!(display.contains("Max = 123456789.123"));
}

#[test]
fn test_percentile() {
let mut vec: Vec<f64> = (0..100).map(|i| i as f64).collect();
vec.shuffle(&mut thread_rng());
let stats = Stats::new(&mut vec, Some(1));
Paint::disable();
let display = format!("{stats}");
println!("{}", display);
assert!(display.contains("p50 = 50.0"));
assert!(display.contains("p90 = 90.0"));
assert!(display.contains("p95 = 95.0"));
assert!(display.contains("p99 = 99.0"));
}
}

0 comments on commit f78c799

Please sign in to comment.