Files
Ely-GPUI-Components/src/charts/stats.rs
T

212 lines
6.4 KiB
Rust

use super::scale::nice_step;
/// Bins of a round width near a `count`th of the data's range, each with its start, its end, and how many values fall in it; the top edge counts in the last.
pub(crate) fn bins(values: &[f64], count: usize) -> Vec<(f64, f64, usize)> {
assert!(count > 0, "a histogram needs a bin");
if values.is_empty() {
return Vec::new();
}
let (low, high) = values
.iter()
.fold((f64::MAX, f64::MIN), |(low, high), value| {
(low.min(*value), high.max(*value))
});
let width = nice_step(high - low, count);
let start = (low / width).floor() * width;
let many = (((high - start) / width).ceil() as usize).max(1);
let mut bins: Vec<(f64, f64, usize)> = (0..many)
.map(|ix| {
(
start + width * ix as f64,
start + width * (ix + 1) as f64,
0,
)
})
.collect();
for value in values {
let ix = (((value - start) / width) as usize).min(many - 1);
bins[ix].2 += 1;
}
bins
}
/// A spread in five numbers: the whiskers' ends within 1.5 spreads of the box, the quartiles, the median; and the values past the whiskers.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Five {
pub low: f64,
pub q1: f64,
pub median: f64,
pub q3: f64,
pub high: f64,
pub outliers: Vec<f64>,
}
/// The value `share` of the way through sorted `values`, between neighbors.
fn quantile(sorted: &[f64], share: f64) -> f64 {
let at = share * (sorted.len() - 1) as f64;
let (below, above) = (at.floor() as usize, at.ceil() as usize);
sorted[below] + (sorted[above] - sorted[below]) * (at - below as f64)
}
pub(crate) fn five(values: &[f64]) -> Five {
assert!(!values.is_empty(), "a box needs values");
let mut sorted = values.to_vec();
sorted.sort_by(f64::total_cmp);
let (q1, median, q3) = (
quantile(&sorted, 0.25),
quantile(&sorted, 0.5),
quantile(&sorted, 0.75),
);
let reach = (q3 - q1) * 1.5;
let inside = |value: &&f64| **value >= q1 - reach && **value <= q3 + reach;
let low = *sorted
.iter()
.find(inside)
.expect("the quartiles lie inside");
let high = *sorted
.iter()
.rev()
.find(inside)
.expect("the quartiles lie inside");
let outliers = sorted
.iter()
.copied()
.filter(|value| *value < low || *value > high)
.collect();
Five {
low,
q1,
median,
q3,
high,
outliers,
}
}
/// A smooth estimate of where values gather, sampled at `count` points across a little past their range: a Gaussian kernel with Silverman's bandwidth.
pub(crate) fn density(values: &[f64], count: usize) -> Vec<(f64, f64)> {
assert!(
values.len() >= 2 && count >= 2,
"a density needs two values and two samples"
);
let n = values.len() as f64;
let mean = values.iter().sum::<f64>() / n;
let spread = (values
.iter()
.map(|value| (value - mean).powi(2))
.sum::<f64>()
/ (n - 1.0))
.sqrt();
let bandwidth = (1.06 * spread * n.powf(-0.2)).max(f64::EPSILON);
let (low, high) = values
.iter()
.fold((f64::MAX, f64::MIN), |(low, high), value| {
(low.min(*value), high.max(*value))
});
let (from, to) = (low - bandwidth, high + bandwidth);
let norm = 1.0 / (n * bandwidth * (2.0 * std::f64::consts::PI).sqrt());
(0..count)
.map(|ix| {
let at = from + (to - from) * ix as f64 / (count - 1) as f64;
let sum: f64 = values
.iter()
.map(|value| (-0.5 * ((at - value) / bandwidth).powi(2)).exp())
.sum();
(at, sum * norm)
})
.collect()
}
/// A waterfall bar's kind: a rise, a fall, or a total that stands on zero.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Step {
Up,
Down,
Total,
}
/// Each change floating from the running total, bottom and top; a total stands on zero, reaching up or down to the sum so far.
pub(crate) fn steps(changes: &[(f64, bool)]) -> Vec<(f64, f64, Step)> {
let mut running = 0.0f64;
changes
.iter()
.map(|(value, total)| {
if *total {
return (running.min(0.0), running.max(0.0), Step::Total);
}
let from = running;
running += value;
(
from.min(running),
from.max(running),
if *value >= 0.0 { Step::Up } else { Step::Down },
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn values_fall_into_equal_bins() {
let bins = bins(&[1.0, 2.0, 2.5, 9.0, 10.0], 5);
assert_eq!(bins.len(), 5);
assert_eq!((bins[0].0, bins[4].1), (0.0, 10.0));
let counts: Vec<usize> = bins.iter().map(|bin| bin.2).collect();
assert_eq!(
counts,
[1, 2, 0, 0, 2],
"the top edge counts in the last bin"
);
}
#[test]
fn five_numbers_and_outliers() {
let spread = five(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 100.0]);
assert_eq!((spread.q1, spread.median, spread.q3), (3.0, 5.0, 7.0));
assert_eq!((spread.low, spread.high), (1.0, 8.0));
assert_eq!(spread.outliers, [100.0]);
}
#[test]
fn density_peaks_where_values_gather() {
let samples = density(&[0.0, 0.1, 0.2, 5.0], 50);
let peak = samples
.iter()
.max_by(|a, b| a.1.total_cmp(&b.1))
.expect("samples");
assert!(
peak.0 < 1.0,
"the peak sits near the cluster, at {}",
peak.0
);
}
#[test]
fn a_total_below_zero_hangs_down_from_it() {
let bars = steps(&[(-10.0, false), (0.0, true)]);
let (bottom, top, step) = bars[1];
assert_eq!(
(bottom, top, step),
(-10.0, 0.0, Step::Total),
"the bar runs from the total up to zero"
);
}
#[test]
fn a_waterfall_floats_changes_and_stands_totals_on_zero() {
let bars = steps(&[(100.0, false), (-30.0, false), (0.0, true), (20.0, false)]);
assert_eq!(
bars,
[
(0.0, 100.0, Step::Up),
(70.0, 100.0, Step::Down),
(0.0, 70.0, Step::Total),
(70.0, 90.0, Step::Up)
]
);
}
}