use num_traits::{Float, ToPrimitive};
use rayon::prelude::*;
use crate::prelude::{BixverseFloat, BixverseNumeric};
const PEARSON_MIN_LEN: usize = 10_000;
pub fn rank_vector<T>(vec: &[T]) -> Vec<T>
where
T: Float,
{
let n = vec.len();
if n == 0 {
return Vec::new();
}
let mut indexed_values: Vec<(T, usize)> = vec
.iter()
.copied()
.enumerate()
.map(|(i, v)| (v, i))
.collect();
indexed_values
.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut ranks = vec![T::zero(); n];
let mut i = 0;
while i < n {
let current_value = indexed_values[i].0;
let start = i;
while i < n && indexed_values[i].0 == current_value {
i += 1;
}
let avg_rank = (start + i + 1) as f64 / 2.0;
let rank_value = T::from(avg_rank).unwrap();
for j in start..i {
ranks[indexed_values[j].1] = rank_value;
}
}
ranks
}
pub fn median<T>(x: &[T]) -> Option<T>
where
T: BixverseFloat,
{
if x.is_empty() {
return None;
}
let mut data = x.to_vec();
let len = data.len();
if len.is_multiple_of(2) {
let (_, median1, right) =
data.select_nth_unstable_by(len / 2 - 1, |a, b| a.partial_cmp(b).unwrap());
let median2 = right
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
Some((*median1 + *median2) / T::from_f64(2.0).unwrap())
} else {
let (_, median, _) = data.select_nth_unstable_by(len / 2, |a, b| a.partial_cmp(b).unwrap());
Some(*median)
}
}
pub fn mad<T>(x: &[T], scale: Option<T>) -> Option<T>
where
T: BixverseFloat,
{
if x.is_empty() {
return None;
}
let median_val = median(x)?;
let deviations: Vec<T> = x.iter().map(|&val| (val - median_val).abs()).collect();
let raw = median(&deviations)?;
Some(match scale {
Some(k) => raw * k,
None => raw,
})
}
pub fn standard_deviation<T>(x: &[T]) -> T
where
T: BixverseFloat + std::iter::Sum,
{
let n = T::from_usize(x.len()).unwrap();
let mean: T = x.iter().copied().sum::<T>() / n;
let variance = x.iter().map(|&val| (val - mean).powi(2)).sum::<T>() / (n - T::one());
variance.sqrt()
}
pub fn sum_sq_f64<T>(values: &[T]) -> f64
where
T: BixverseNumeric + ToPrimitive,
{
values
.par_iter()
.with_min_len(10_000)
.map(|&v| {
let x = v.to_f64().expect("numeric type does not convert to f64");
x * x
})
.sum()
}
pub fn mean_nan<T>(x: &[T]) -> T
where
T: BixverseFloat + std::iter::Sum,
{
let finite: Vec<T> = x.iter().copied().filter(|x| x.is_finite()).collect();
if finite.is_empty() {
T::nan()
} else {
finite.iter().copied().sum::<T>() / T::from_usize(finite.len()).unwrap()
}
}
pub fn quantile_sorted<T>(sorted: &[T], q: T) -> T
where
T: BixverseFloat,
{
if sorted.is_empty() {
return T::zero();
}
let pos = q.clamp(T::zero(), T::one()) * T::from_usize(sorted.len() - 1).unwrap();
let lo = pos.floor().to_usize().unwrap();
let hi = (lo + 1).min(sorted.len() - 1);
let frac = pos - T::from_usize(lo).unwrap();
sorted[lo] * (T::one() - frac) + sorted[hi] * frac
}
pub fn quantile<T>(values: &[T], q: T) -> T
where
T: BixverseFloat,
{
let mut sorted: Vec<T> = values.to_vec();
sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
quantile_sorted(&sorted, q)
}
pub fn pearson_correlation<T>(x: &[T], y: &[T]) -> Option<f64>
where
T: BixverseFloat + Sync,
{
if x.len() != y.len() || x.len() < 2 {
return None;
}
let n = x.len() as f64;
let (sum_x, sum_y) = x
.par_iter()
.zip(y.par_iter())
.with_min_len(PEARSON_MIN_LEN)
.fold(
|| (0.0f64, 0.0f64),
|acc, (&a, &b)| {
(
acc.0 + a.to_f64().unwrap_or(0.0),
acc.1 + b.to_f64().unwrap_or(0.0),
)
},
)
.reduce(|| (0.0f64, 0.0f64), |a, b| (a.0 + b.0, a.1 + b.1));
let (mean_x, mean_y) = (sum_x / n, sum_y / n);
let (cov, var_x, var_y) = x
.par_iter()
.zip(y.par_iter())
.with_min_len(PEARSON_MIN_LEN)
.fold(
|| (0.0f64, 0.0f64, 0.0f64),
|acc, (&a, &b)| {
let da = a.to_f64().unwrap_or(0.0) - mean_x;
let db = b.to_f64().unwrap_or(0.0) - mean_y;
(acc.0 + da * db, acc.1 + da * da, acc.2 + db * db)
},
)
.reduce(
|| (0.0f64, 0.0f64, 0.0f64),
|a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2),
);
let denom = (var_x * var_y).sqrt();
if denom <= 0.0 || !denom.is_finite() {
return None;
}
Some(cov / denom)
}
pub fn interp_linear_at<T>(x: &[T], y: &[T], at: T) -> T
where
T: BixverseFloat,
{
let n = x.len();
if n == 0 || y.len() < n {
return at;
}
if n == 1 {
return y[0];
}
let upper = x.partition_point(|&k| k <= at);
let j = upper.clamp(1, n - 1) - 1;
let dx = x[j + 1] - x[j];
if dx <= T::zero() {
return y[j];
}
y[j] + (y[j + 1] - y[j]) * ((at - x[j]) / dx)
}
pub fn interp_log_linear_at<T>(x: &[T], y: &[T], at: T) -> T
where
T: BixverseFloat,
{
let n = x.len();
if n == 0 || y.len() < n {
return at;
}
if n == 1 {
return y[0];
}
let upper = x.partition_point(|&k| k <= at);
let j = upper.clamp(1, n - 1) - 1;
if at <= T::zero() || x[j] <= T::zero() || x[j + 1] <= T::zero() {
return interp_linear_at(x, y, at);
}
let dx = x[j + 1].ln() - x[j].ln();
if dx <= T::zero() {
return y[j];
}
y[j] + (y[j + 1] - y[j]) * ((at.ln() - x[j].ln()) / dx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_sq_f64_survives_a_swamped_accumulator() {
let n_ones = 1_000_000usize;
let mut values = vec![1e4f32];
values.extend(std::iter::repeat_n(1.0f32, n_ones));
let exact = 1e8 + n_ones as f64;
let got = sum_sq_f64(&values);
assert!(
(got - exact).abs() <= 1e-6 * exact,
"f64 accumulation is off: got {got}, exact {exact}"
);
let naive = values
.iter()
.fold(0f32, |acc, &v| acc + v * v)
.to_f64()
.unwrap();
assert!(
(naive - exact).abs() > 0.005 * exact,
"the f32 reference was supposed to be visibly wrong, so this test proves nothing"
);
}
#[test]
fn test_sum_sq_f64_on_integer_storage() {
let values: Vec<u32> = (1..=1000).collect();
let exact: f64 = (1..=1000u64).map(|v| (v * v) as f64).sum();
assert!((sum_sq_f64(&values) - exact).abs() < 1e-6);
}
#[test]
fn test_pearson_correlation() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let perfect: Vec<f64> = x.iter().map(|v| 3.0 * v + 1.0).collect();
let inverse: Vec<f64> = x.iter().map(|v| -2.0 * v).collect();
assert!((pearson_correlation(&x, &perfect).unwrap() - 1.0).abs() < 1e-12);
assert!((pearson_correlation(&x, &inverse).unwrap() + 1.0).abs() < 1e-12);
assert_eq!(pearson_correlation(&x, &[2.0; 5]), None);
assert_eq!(pearson_correlation(&x, &[1.0, 2.0]), None);
assert_eq!(pearson_correlation(&[1.0], &[1.0]), None);
}
#[test]
fn test_pearson_correlation_survives_a_large_offset() {
let x: Vec<f64> = (0..1000).map(|i| 1e8 + i as f64 * 1e-4).collect();
let y: Vec<f64> = x.iter().map(|v| -2.0 * v + 5.0).collect();
let self_corr = pearson_correlation(&x, &x).expect("identical vectors correlate at one");
assert!(
(self_corr - 1.0).abs() < 1e-9,
"self correlation is {self_corr}"
);
let inverse = pearson_correlation(&x, &y).expect("an affine map correlates at minus one");
assert!(
(inverse + 1.0).abs() < 1e-9,
"inverse correlation is {inverse}"
);
}
#[test]
fn test_rank_vector() {
let vec = vec![3.0, 1.0, 2.0, 3.0];
let ranks = rank_vector(&vec);
assert_eq!(ranks, vec![3.5, 1.0, 2.0, 3.5]);
let empty: Vec<f64> = vec![];
assert_eq!(rank_vector(&empty), Vec::<f64>::new());
}
#[test]
fn test_median() {
let vec_odd = vec![1.0, 3.0, 2.0];
assert_eq!(median(&vec_odd), Some(2.0));
let vec_even = vec![1.0, 4.0, 2.0, 3.0];
assert_eq!(median(&vec_even), Some(2.5));
let empty: Vec<f64> = vec![];
assert_eq!(median(&empty), None);
}
#[test]
fn test_mad() {
let vec = vec![1.0, 1.0, 2.0, 2.0, 4.0, 6.0, 9.0];
assert_eq!(mad(&vec, None), Some(1.0));
}
#[test]
fn test_standard_deviation() {
let vec = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let std = standard_deviation(&vec);
assert!((std - 2.1380899352).abs() < 1e-6);
}
#[test]
fn test_interp_linear_hits_the_knots() {
let x = [0.0, 1.0, 3.0, 6.0];
let y = [10.0, 20.0, 0.0, 30.0];
for (&xi, &yi) in x.iter().zip(y.iter()) {
assert!((interp_linear_at(&x, &y, xi) - yi).abs() < 1e-12);
}
}
#[test]
fn test_interp_linear_interior() {
let x = [0.0, 1.0, 3.0];
let y = [0.0, 10.0, 30.0];
assert!((interp_linear_at(&x, &y, 0.5) - 5.0).abs() < 1e-12);
assert!((interp_linear_at(&x, &y, 2.0) - 20.0).abs() < 1e-12);
}
#[test]
fn test_interp_linear_extrapolates() {
let x = [1.0, 2.0, 3.0];
let y = [10.0, 20.0, 30.0];
assert!((interp_linear_at(&x, &y, 0.0) - 0.0).abs() < 1e-12);
assert!((interp_linear_at(&x, &y, 5.0) - 50.0).abs() < 1e-12);
}
#[test]
fn test_interp_linear_single_knot_is_constant() {
assert!((interp_linear_at(&[2.0], &[7.0], -100.0) - 7.0).abs() < 1e-12);
assert!((interp_linear_at(&[2.0], &[7.0], 100.0) - 7.0).abs() < 1e-12);
}
#[test]
fn test_interp_degenerate_knots() {
assert!((interp_linear_at::<f64>(&[], &[], 3.0) - 3.0).abs() < 1e-12);
assert!((interp_linear_at(&[1.0, 2.0, 3.0], &[1.0], 2.5) - 2.5).abs() < 1e-12);
assert!((interp_log_linear_at::<f64>(&[], &[], 3.0) - 3.0).abs() < 1e-12);
assert!((interp_log_linear_at(&[1.0, 2.0, 3.0], &[1.0], 2.5) - 2.5).abs() < 1e-12);
}
#[test]
fn test_interp_log_linear_geometric_midpoint() {
let x = [1.0, 100.0];
let y = [0.0, 1.0];
assert!((interp_log_linear_at(&x, &y, 10.0) - 0.5).abs() < 1e-12);
assert!((interp_log_linear_at(&x, &y, 1.0) - 0.0).abs() < 1e-12);
assert!((interp_log_linear_at(&x, &y, 100.0) - 1.0).abs() < 1e-12);
assert!((interp_log_linear_at(&x, &y, 10_000.0) - 2.0).abs() < 1e-12);
}
#[test]
fn test_interp_log_linear_falls_back_on_non_positive() {
let x = [1.0, 3.0];
let y = [10.0, 30.0];
let linear = interp_linear_at(&x, &y, -1.0);
assert!((interp_log_linear_at(&x, &y, -1.0) - linear).abs() < 1e-12);
}
}