Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 42 additions & 57 deletions src/statistics/slice_statistics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ impl<D: AsMut<[f64]> + AsRef<[f64]>> Data<D> {
self.0.as_ref().iter()
}

// Selection algorithm from Numerical Recipes
// See: https://en.wikipedia.org/wiki/Selection_algorithm
// Selection via `<[T]>::select_nth_unstable_by`, which is an introselect
// with an O(n) worst case. The previous hand-rolled Numerical Recipes
// quickselect was ~4x slower on large uniform data and quadratic on
// adversarial inputs; `total_cmp` also gives NaN a defined ordering
// (positive NaNs sort past +inf) instead of the arbitrary result the NR
// partition produced.
fn select_inplace(&mut self, rank: usize) -> f64 {
if rank == 0 {
return self.min();
Expand All @@ -74,61 +78,11 @@ impl<D: AsMut<[f64]> + AsRef<[f64]>> Data<D> {
return self.max();
}

let mut low = 0;
let mut high = self.len() - 1;
loop {
if high <= low + 1 {
if high == low + 1 && self[high] < self[low] {
self.swap(low, high)
}
return self[rank];
}

let middle = (low + high) / 2;
self.swap(middle, low + 1);

if self[low] > self[high] {
self.swap(low, high);
}
if self[low + 1] > self[high] {
self.swap(low + 1, high);
}
if self[low] > self[low + 1] {
self.swap(low, low + 1);
}

let mut begin = low + 1;
let mut end = high;
let pivot = self[begin];
loop {
loop {
begin += 1;
if self[begin] >= pivot {
break;
}
}
loop {
end -= 1;
if self[end] <= pivot {
break;
}
}
if end < begin {
break;
}
self.swap(begin, end);
}

self[low + 1] = self[end];
self[end] = pivot;

if end >= rank {
high = end - 1;
}
if end <= rank {
low = begin;
}
}
*self
.0
.as_mut()
.select_nth_unstable_by(rank, |a, b| a.total_cmp(b))
.1
}
}

Expand Down Expand Up @@ -550,6 +504,37 @@ mod tests {

// TODO: test codeplex issue 5667 (Math.NET)

/// `select_inplace` used to implement the Numerical Recipes quickselect by
/// hand, whose partition loop walks off the end of the array when the slice
/// contains NaN (statrs-dev/statrs#163). `total_cmp` gives NaN a defined
/// place in a total order, so the search stays in bounds.
#[test]
fn test_order_statistics_with_nan_do_not_panic() {
// Arrays rather than `Vec`, so the test also builds under `no_std`.
let mut v: [f64; 1000] = core::array::from_fn(|i| (i as f64 * 0.7).sin());
for i in (0..1000).step_by(7) {
v[i] = f64::NAN;
}
let mut d = Data::new(v);
// the values themselves are unspecified once NaN is present; what is
// being pinned is that these terminate in bounds
let _ = d.quantile(0.5);
let _ = d.quantile(0.0);
let _ = d.quantile(1.0);
let _ = d.order_statistic(500);
let _ = OrderStatistics::median(&mut d);
let _ = d.interquartile_range();

// all-NaN, and NaN at the exact positions the old partition tripped on
let mut all_nan = Data::new([f64::NAN; 64]);
let _ = all_nan.quantile(0.5);
for pos in [0usize, 1, 31, 62, 63] {
let mut v = [0.0f64; 64];
v[pos] = f64::NAN;
let _ = Data::new(v).quantile(0.5);
}
}

#[test]
fn test_median_robust_on_infinities() {
let data3 = [2.0, f64::NEG_INFINITY, f64::INFINITY];
Expand Down