use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
use super::common::{invalid, qi, qu};
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::base::interval::Interval;
pub use crate::base::numeric::Q;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Ddof {
Population,
Sample,
}
impl Ddof {
fn divisor(self, op: &'static str, n: usize) -> Result<Q, SymplexError> {
match self {
Ddof::Population => {
if n == 0 {
return Err(invalid(op, "variance of an empty sample"));
}
Ok(qu(n))
}
Ddof::Sample => {
if n < 2 {
return Err(invalid(
op,
"sample variance needs at least two observations",
));
}
Ok(qu(n - 1))
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum QuantileMethod {
Exclusive,
Inclusive,
}
pub fn sum(data: &[Q]) -> Q {
data.iter().fold(Q::zero(), |acc, x| acc + x)
}
pub fn mean(data: &[Q]) -> Result<Q, SymplexError> {
if data.is_empty() {
return Err(invalid("mean", "mean of an empty sample"));
}
Ok(sum(data) / qu(data.len()))
}
pub fn sum_of_squares(data: &[Q]) -> Result<Q, SymplexError> {
let m = mean(data)?;
Ok(data
.iter()
.map(|x| {
let d = x - &m;
&d * &d
})
.fold(Q::zero(), |acc, x| acc + x))
}
pub fn variance(data: &[Q], ddof: Ddof) -> Result<Q, SymplexError> {
let div = ddof.divisor("variance", data.len())?;
Ok(sum_of_squares(data)? / div)
}
pub fn std(ctx: &Context, data: &[Q], ddof: Ddof) -> Result<Ex, SymplexError> {
Ok(ctx.from_ratio(variance(data, ddof)?).sqrt())
}
pub fn covariance(x: &[Q], y: &[Q], ddof: Ddof) -> Result<Q, SymplexError> {
const OP: &str = "covariance";
if x.len() != y.len() {
return Err(invalid(
OP,
format!(
"covariance of samples of different sizes ({} and {})",
x.len(),
y.len()
),
));
}
let div = ddof.divisor(OP, x.len())?;
let (mx, my) = (mean(x)?, mean(y)?);
let s = x
.iter()
.zip(y)
.map(|(a, b)| (a - &mx) * (b - &my))
.fold(Q::zero(), |acc, v| acc + v);
Ok(s / div)
}
pub fn pearson(ctx: &Context, x: &[Q], y: &[Q]) -> Result<Ex, SymplexError> {
let sxy = covariance(x, y, Ddof::Population)?;
let sxx = variance(x, Ddof::Population)?;
let syy = variance(y, Ddof::Population)?;
if sxx.is_zero() || syy.is_zero() {
return Err(invalid(
"pearson",
"correlation with a constant sample is undefined",
));
}
Ok((ctx.from_ratio(sxy) / (ctx.from_ratio(sxx) * ctx.from_ratio(syy)).sqrt()).simplify())
}
pub fn sorted(data: &[Q]) -> Vec<Q> {
let mut v = data.to_vec();
v.sort();
v
}
pub fn median(data: &[Q]) -> Result<Q, SymplexError> {
if data.is_empty() {
return Err(invalid("median", "median of an empty sample"));
}
let s = sorted(data);
let n = s.len();
Ok(if n % 2 == 1 {
s[n / 2].clone()
} else {
(&s[n / 2 - 1] + &s[n / 2]) / qi(2)
})
}
pub fn quantile(data: &[Q], p: &Q, method: QuantileMethod) -> Result<Q, SymplexError> {
const OP: &str = "quantile";
if data.is_empty() {
return Err(invalid(OP, "quantile of an empty sample"));
}
if p.is_negative() || *p > Q::one() {
return Err(invalid(
OP,
format!("the quantile level must lie in [0, 1], got {p}"),
));
}
let s = sorted(data);
let n = s.len();
let pos = match method {
QuantileMethod::Exclusive => p * qu(n + 1),
QuantileMethod::Inclusive => Q::one() + p * qu(n - 1),
};
let floor = pos.floor();
let frac = &pos - &floor;
let k = floor.to_integer().to_i64().unwrap_or(0);
if k < 1 {
return Ok(s[0].clone());
}
let k = k as usize;
if k >= n {
return Ok(s[n - 1].clone());
}
let lo = &s[k - 1];
let hi = &s[k];
Ok(lo + (hi - lo) * frac)
}
pub fn quantiles(data: &[Q], n: usize, method: QuantileMethod) -> Result<Vec<Q>, SymplexError> {
if n < 2 {
return Err(invalid("quantiles", "quantiles need n ≥ 2 groups"));
}
(1..n)
.map(|i| quantile(data, &(qu(i) / qu(n)), method))
.collect()
}
pub fn iqr(data: &[Q], method: QuantileMethod) -> Result<Q, SymplexError> {
let q1 = quantile(data, &Q::new(BigInt::from(1), BigInt::from(4)), method)?;
let q3 = quantile(data, &Q::new(BigInt::from(3), BigInt::from(4)), method)?;
Ok(q3 - q1)
}
pub fn min_max(data: &[Q]) -> Result<Interval<Q>, SymplexError> {
let first = data
.first()
.ok_or_else(|| invalid("min_max", "min/max of an empty sample"))?;
let mut lo = first.clone();
let mut hi = first.clone();
for x in data {
if *x < lo {
lo = x.clone();
}
if *x > hi {
hi = x.clone();
}
}
Ok(Interval::closed(lo, hi))
}
pub fn modes(data: &[Q]) -> Vec<Q> {
let s = sorted(data);
let mut best = 0usize;
let mut out: Vec<Q> = Vec::new();
let mut i = 0;
while i < s.len() {
let mut j = i;
while j < s.len() && s[j] == s[i] {
j += 1;
}
let count = j - i;
if count > best {
best = count;
out.clear();
out.push(s[i].clone());
} else if count == best {
out.push(s[i].clone());
}
i = j;
}
out
}
pub fn frequencies(data: &[Q]) -> Vec<(Q, usize)> {
let s = sorted(data);
let mut out: Vec<(Q, usize)> = Vec::new();
for x in s {
match out.last_mut() {
Some((v, c)) if *v == x => *c += 1,
_ => out.push((x, 1)),
}
}
out
}
pub fn ranks(data: &[Q]) -> Vec<Q> {
let mut idx: Vec<usize> = (0..data.len()).collect();
idx.sort_by(|&a, &b| data[a].cmp(&data[b]));
let mut out = vec![Q::zero(); data.len()];
let mut i = 0;
while i < idx.len() {
let mut j = i;
while j < idx.len() && data[idx[j]] == data[idx[i]] {
j += 1;
}
let avg = (qu(i + 1) + qu(j)) / qi(2);
for &k in &idx[i..j] {
out[k] = avg.clone();
}
i = j;
}
out
}
pub fn tie_sizes(data: &[Q]) -> Vec<usize> {
frequencies(data)
.into_iter()
.filter_map(|(_, c)| (c >= 2).then_some(c))
.collect()
}
pub fn spearman(ctx: &Context, x: &[Q], y: &[Q]) -> Result<Ex, SymplexError> {
pearson(ctx, &ranks(x), &ranks(y))
}
pub fn kendall_tau(ctx: &Context, x: &[Q], y: &[Q]) -> Result<Ex, SymplexError> {
const OP: &str = "kendall_tau";
if x.len() != y.len() {
return Err(invalid(OP, "Kendall's τ of samples of different sizes"));
}
if x.len() < 2 {
return Err(invalid(OP, "Kendall's τ needs at least two pairs"));
}
let c = count_pairs(x, y);
let untied = BigInt::from(c.concordant) + BigInt::from(c.discordant);
let not_tied_x = &untied + BigInt::from(c.ties_y);
let not_tied_y = &untied + BigInt::from(c.ties_x);
if not_tied_x.is_zero() || not_tied_y.is_zero() {
return Err(invalid(
OP,
"Kendall's τ with a constant sample is undefined",
));
}
let diff = BigInt::from(c.concordant) - BigInt::from(c.discordant);
if not_tied_x == not_tied_y {
return Ok(ctx.from_ratio(Q::new(diff, not_tied_x)));
}
let denom = ctx.from_bigint(not_tied_x * not_tied_y).sqrt();
Ok((ctx.from_bigint(diff) / denom).simplify())
}
pub fn skewness(ctx: &Context, data: &[Q]) -> Result<Ex, SymplexError> {
let (m2, m3) = (central_moment(data, 2)?, central_moment(data, 3)?);
if m2.is_zero() {
return Err(invalid(
"skewness",
"skewness of a constant sample is undefined",
));
}
Ok((ctx.from_ratio(m3) / ctx.from_ratio(m2).pow(&ctx.rational(3, 2))).simplify())
}
pub fn kurtosis(data: &[Q]) -> Result<Q, SymplexError> {
let (m2, m4) = (central_moment(data, 2)?, central_moment(data, 4)?);
if m2.is_zero() {
return Err(invalid(
"kurtosis",
"kurtosis of a constant sample is undefined",
));
}
Ok(m4 / (&m2 * &m2) - qi(3))
}
pub fn central_moment(data: &[Q], k: u32) -> Result<Q, SymplexError> {
let m = mean(data)?;
let s = data
.iter()
.map(|x| pow_q(&(x - &m), k))
.fold(Q::zero(), |acc, v| acc + v);
Ok(s / qu(data.len()))
}
fn pow_q(x: &Q, k: u32) -> Q {
let mut acc = Q::one();
for _ in 0..k {
acc *= x;
}
acc
}
pub fn median_abs_deviation(data: &[Q]) -> Result<Q, SymplexError> {
let m = median(data)?;
let devs: Vec<Q> = data.iter().map(|x| (x - &m).abs()).collect();
median(&devs)
}
pub fn zscores(ctx: &Context, data: &[Q], ddof: Ddof) -> Result<Vec<Ex>, SymplexError> {
let m = mean(data)?;
let s = std(ctx, data, ddof)?;
if variance(data, ddof)?.is_zero() {
return Err(invalid(
"zscores",
"z-scores of a constant sample are undefined",
));
}
Ok(data
.iter()
.map(|x| (ctx.from_ratio(x - &m) / &s).simplify())
.collect())
}
pub fn geometric_mean(ctx: &Context, data: &[Q]) -> Result<Ex, SymplexError> {
const OP: &str = "geometric_mean";
if data.is_empty() {
return Err(invalid(OP, "geometric mean of an empty sample"));
}
if data.iter().any(|x| !x.is_positive()) {
return Err(invalid(OP, "geometric mean needs positive observations"));
}
let prod = data.iter().fold(Q::one(), |acc, x| acc * x);
Ok(ctx
.from_ratio(prod)
.pow(&ctx.rational(1, data.len() as i64))
.simplify())
}
pub fn harmonic_mean(data: &[Q]) -> Result<Q, SymplexError> {
const OP: &str = "harmonic_mean";
if data.is_empty() {
return Err(invalid(OP, "harmonic mean of an empty sample"));
}
if data.iter().any(|x| !x.is_positive()) {
return Err(invalid(OP, "harmonic mean needs positive observations"));
}
let s = data.iter().fold(Q::zero(), |acc, x| acc + x.recip());
Ok(qu(data.len()) / s)
}
pub fn trimmed_mean(data: &[Q], p: &Q) -> Result<Q, SymplexError> {
const OP: &str = "trimmed_mean";
if p.is_negative() || *p >= Q::new(BigInt::from(1), BigInt::from(2)) {
return Err(invalid(OP, "the trimming proportion must lie in [0, 1/2)"));
}
let s = sorted(data);
let cut = (p * qu(s.len()))
.floor()
.to_integer()
.to_usize()
.unwrap_or(0);
if 2 * cut >= s.len() {
return Err(invalid(OP, "trimming removes every observation"));
}
mean(&s[cut..s.len() - cut])
}
pub fn iqr_outliers(data: &[Q], k: &Q, method: QuantileMethod) -> Result<Vec<usize>, SymplexError> {
let q1 = quantile(data, &Q::new(BigInt::from(1), BigInt::from(4)), method)?;
let q3 = quantile(data, &Q::new(BigInt::from(3), BigInt::from(4)), method)?;
let spread = &q3 - &q1;
let lo = &q1 - k * &spread;
let hi = &q3 + k * &spread;
Ok(data
.iter()
.enumerate()
.filter_map(|(i, x)| (*x < lo || *x > hi).then_some(i))
.collect())
}
pub fn mad_outliers(data: &[Q], threshold: &Q) -> Result<Vec<usize>, SymplexError> {
let m = median(data)?;
let mad = median_abs_deviation(data)?;
if mad.is_zero() {
return Err(invalid(
"mad_outliers",
"the median absolute deviation is zero",
));
}
let c = Q::new(BigInt::from(6745), BigInt::from(10_000));
Ok(data
.iter()
.enumerate()
.filter_map(|(i, x)| {
let z = (&c * (x - &m) / &mad).abs();
(z > *threshold).then_some(i)
})
.collect())
}
pub fn from_i64(data: &[i64]) -> Vec<Q> {
data.iter().map(|&x| qi(x)).collect()
}
pub fn from_f64(data: &[f64]) -> Result<Vec<Q>, SymplexError> {
data.iter()
.map(|&x| {
crate::base::numeric::f64_to_ratio_exact(x)
.ok_or_else(|| invalid("from_f64", format!("{x} is not a finite number")))
})
.collect()
}
pub fn to_f64(data: &[Q]) -> Vec<f64> {
data.iter()
.map(|x| x.to_f64().unwrap_or(f64::NAN))
.collect()
}
pub fn binomial_q(n: usize, k: usize) -> Q {
Q::from_integer(crate::base::combinatorics::binomial(n as u64, k as u64))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ConcordanceCounts {
pub concordant: usize,
pub discordant: usize,
pub ties_x: usize,
pub ties_y: usize,
pub ties_both: usize,
}
impl ConcordanceCounts {
#[must_use]
pub fn pairs(&self) -> usize {
self.concordant + self.discordant + self.ties_x + self.ties_y + self.ties_both
}
}
pub fn concordance_counts(x: &[Q], y: &[Q]) -> Result<ConcordanceCounts, SymplexError> {
const OP: &str = "concordance_counts";
if x.len() != y.len() {
return Err(invalid(
OP,
format!(
"the two variables must have the same length ({} and {})",
x.len(),
y.len()
),
));
}
if x.len() < 2 {
return Err(invalid(OP, "needs at least two observations"));
}
Ok(count_pairs(x, y))
}
fn pairs_within(t: usize) -> usize {
if t.is_multiple_of(2) {
(t / 2) * t.saturating_sub(1)
} else {
t * ((t - 1) / 2)
}
}
fn tied_pairs<T: PartialEq>(sorted: impl Iterator<Item = T>) -> usize {
let mut total = 0;
let mut run = 0;
let mut prev: Option<T> = None;
for key in sorted {
if prev.as_ref() == Some(&key) {
run += 1;
} else {
total += pairs_within(run);
run = 1;
prev = Some(key);
}
}
total + pairs_within(run)
}
fn sort_counting_inversions(v: &mut Vec<&Q>) -> usize {
let n = v.len();
let mut out: Vec<&Q> = Vec::with_capacity(n);
let mut inversions = 0;
let mut width = 1;
while width < n {
out.clear();
let mut start = 0;
while start < n {
let mid = n.min(start + width);
let end = n.min(mid + width);
let (mut i, mut j) = (start, mid);
while i < mid && j < end {
if v[j] < v[i] {
inversions += mid - i;
out.push(v[j]);
j += 1;
} else {
out.push(v[i]);
i += 1;
}
}
out.extend_from_slice(&v[i..mid]);
out.extend_from_slice(&v[j..end]);
start = end;
}
std::mem::swap(v, &mut out);
width *= 2;
}
inversions
}
fn count_pairs(x: &[Q], y: &[Q]) -> ConcordanceCounts {
let mut order: Vec<usize> = (0..x.len()).collect();
order.sort_by(|&a, &b| x[a].cmp(&x[b]).then_with(|| y[a].cmp(&y[b])));
let tied_x = tied_pairs(order.iter().map(|&i| &x[i]));
let tied_both = tied_pairs(order.iter().map(|&i| (&x[i], &y[i])));
let mut ys: Vec<&Q> = order.iter().map(|&i| &y[i]).collect();
let discordant = sort_counting_inversions(&mut ys);
let tied_y = tied_pairs(ys.into_iter());
let (ties_x, ties_y) = (tied_x - tied_both, tied_y - tied_both);
ConcordanceCounts {
concordant: pairs_within(x.len()) - ties_x - tied_y - discordant,
discordant,
ties_x,
ties_y,
ties_both: tied_both,
}
}
pub fn goodman_kruskal_gamma(x: &[Q], y: &[Q]) -> Result<Q, SymplexError> {
let c = concordance_counts(x, y)?;
let untied = c.concordant + c.discordant;
if untied == 0 {
return Err(invalid(
"goodman_kruskal_gamma",
"every pair is tied on one of the variables, γ is undefined",
));
}
Ok((qu(c.concordant) - qu(c.discordant)) / qu(untied))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Dependent {
Y,
X,
Symmetric,
}
pub fn somers_d(x: &[Q], y: &[Q], dependent: Dependent) -> Result<Q, SymplexError> {
let c = concordance_counts(x, y)?;
let diff = qu(c.concordant) - qu(c.discordant);
let untied = c.concordant + c.discordant;
let denom = match dependent {
Dependent::Y => qu(untied) + qu(c.ties_y),
Dependent::X => qu(untied) + qu(c.ties_x),
Dependent::Symmetric => qu(untied) + (qu(c.ties_x) + qu(c.ties_y)) / qi(2),
};
if denom.is_zero() {
return Err(invalid(
"somers_d",
"the independent variable is constant, Somers' D is undefined",
));
}
Ok(diff / denom)
}
pub fn kendall_tau_c(x: &[Q], y: &[Q]) -> Result<Q, SymplexError> {
let c = concordance_counts(x, y)?;
let m = frequencies(x).len().min(frequencies(y).len());
if m < 2 {
return Err(invalid(
"kendall_tau_c",
"a constant variable has no rank correlation",
));
}
let n = qu(x.len());
let diff = qu(c.concordant) - qu(c.discordant);
Ok(qi(2) * qu(m) * diff / (&n * &n * qu(m - 1)))
}