use crate::rng::{Mt19937, shuffle_arange};
#[cfg(test)]
fn quantile_type7(xs: &mut [f64], p: f64) -> f64 {
let n = xs.len();
if n == 1 {
return xs[0];
}
let h = (n as f64 - 1.0) * p;
let lo = h.floor() as usize;
let frac = h - lo as f64;
if lo + 1 >= n {
return *xs.select_nth_unstable_by(n - 1, f64::total_cmp).1;
}
let (_, &mut kth, right) = xs.select_nth_unstable_by(lo, f64::total_cmp);
let next = right.iter().copied().fold(f64::INFINITY, f64::min);
kth + frac * (next - kth)
}
#[cfg(test)]
fn nanpercentile(col: &[f64], q: f64) -> f64 {
let mut v: Vec<f64> = col.iter().copied().filter(|x| !x.is_nan()).collect();
if v.is_empty() {
return f64::NAN;
}
quantile_type7(&mut v, q / 100.0)
}
fn nanpercentile_all(col: &[f64], references: &[f64]) -> Vec<f64> {
let mut v: Vec<f64> = col.iter().copied().filter(|x| !x.is_nan()).collect();
if v.is_empty() {
return vec![f64::NAN; references.len()];
}
v.sort_unstable_by(f64::total_cmp);
let n = v.len();
let nm1 = (n - 1) as f64;
references
.iter()
.map(|&r| {
let q = r * 100.0;
let h = nm1 * (q / 100.0);
let lo = h.floor() as usize;
let frac = h - lo as f64;
if lo + 1 >= n {
return v[n - 1];
}
v[lo] + frac * (v[lo + 1] - v[lo])
})
.collect()
}
fn fit_column(col: &[f64], references: &[f64]) -> Vec<f64> {
let mut q = nanpercentile_all(col, references);
let mut running_max = f64::NEG_INFINITY;
for v in &mut q {
if !v.is_nan() {
if *v < running_max {
*v = running_max;
} else {
running_max = *v;
}
}
}
q
}
pub fn fit_quantiles(
data: &[f64],
n_rows: usize,
n_cols: usize,
n_quantiles_req: usize,
subsample: Option<usize>,
seed: u64,
) -> (Vec<f64>, Vec<Vec<f64>>) {
let n_quantiles = n_quantiles_req.min(n_rows).max(1);
let step = if n_quantiles > 1 {
1.0 / (n_quantiles - 1) as f64
} else {
0.0
};
let references: Vec<f64> = (0..n_quantiles)
.map(|i| {
if i == n_quantiles - 1 {
1.0
} else {
i as f64 * step
}
})
.collect();
let shared_indices: Option<Vec<usize>> = subsample.and_then(|k| {
if n_rows > k {
let mut rng = Mt19937::seed(seed as u32);
let all = shuffle_arange(&mut rng, n_rows);
Some(all[..k].to_vec())
} else {
None
}
});
let quantiles: Vec<Vec<f64>> = (0..n_cols)
.map(|j| {
let col: Vec<f64> = match &shared_indices {
Some(idxs) => idxs.iter().map(|&i| data[i * n_cols + j]).collect(),
None => (0..n_rows).map(|i| data[i * n_cols + j]).collect(),
};
fit_column(&col, &references)
})
.collect();
(references, quantiles)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64) {
assert!((a - b).abs() < 1e-12, "{a} != {b}");
}
#[test]
fn nanpercentile_type7() {
let c = [1.0, 2.0, 3.0, 4.0, 5.0];
close(nanpercentile(&c, 25.0), 2.0);
close(nanpercentile(&c, 50.0), 3.0);
close(nanpercentile(&c, 75.0), 4.0);
close(nanpercentile(&c, 10.0), 1.4);
close(nanpercentile(&c, 0.0), 1.0);
close(nanpercentile(&c, 100.0), 5.0);
}
#[test]
fn nanpercentile_unsorted() {
close(nanpercentile(&[7.0, 3.0, 9.0, 1.0, 5.0], 10.0), 1.8);
close(nanpercentile(&[7.0, 3.0, 9.0, 1.0, 5.0], 75.0), 7.0);
}
#[test]
fn fit_column_basic() {
let refs: Vec<f64> = (0..5).map(|i| i as f64 / 4.0).collect();
let q = fit_column(&[1.0, 2.0, 3.0, 4.0, 5.0], &refs);
for (got, want) in q.iter().zip(&[1.0, 2.0, 3.0, 4.0, 5.0]) {
close(*got, *want);
}
}
#[test]
fn monotonic_accumulate() {
let refs = vec![0.0, 0.5, 1.0];
let q = fit_column(&[5.0, 5.0, 5.0], &refs);
assert!(q.windows(2).all(|w| w[0] <= w[1]));
}
}