use num_traits::ToPrimitive;
use radiate_utils::Primitive;
pub struct DistShape {
pub unique: usize,
pub evenness: f32,
pub gini: f32,
}
#[inline]
pub fn shape<P: Primitive>(sorted: &[P]) -> DistShape {
let m = sorted.len();
if m == 0 {
return DistShape {
unique: 0,
evenness: 0.0,
gini: 0.0,
};
}
let shift = if sorted[0] < P::ZERO {
P::ZERO.safe_sub(sorted[0])
} else {
P::ZERO
};
let total = m as f32;
let mut unique = 0_f32;
let mut entropy = 0_f32; let mut run_len = 0_f32; let mut last: Option<P> = None;
let mut gini_sum = 0.0_f32; let mut gini_weighted = 0.0_f32;
for (i, &val) in sorted.iter().enumerate() {
match last {
Some(l) if l.is_equal(val) => run_len += 1.0,
_ => {
if run_len > 0.0 {
let p = run_len / total;
entropy -= p * p.ln();
}
unique += 1.0;
run_len = 1.0;
last = Some(val);
}
}
let x = val.safe_add(shift).extract::<f32>().unwrap_or(0.0);
gini_sum += x;
gini_weighted += (i as f32 + 1.0) * x;
}
if run_len > 0.0 {
let p = run_len / total;
entropy -= p * p.ln();
}
let evenness = if unique > 1.0 {
(entropy / unique.ln()).min(1.0)
} else {
0.0
};
let gini = if gini_sum > 0.0 {
((2.0 * gini_weighted) / (total * gini_sum) - (total + 1.0) / total).max(0.0)
} else {
0.0
};
DistShape {
unique: unique as usize,
evenness,
gini,
}
}
#[inline]
pub fn evenness<T: ToPrimitive>(weights: &[T]) -> f32 {
let mut total = 0.0_f32;
let mut weighted_log = 0.0_f32; let mut nonzero = 0_f32;
for w in weights {
let w = w.to_f32().unwrap_or(0.0);
if w > 0.0 {
total += w;
weighted_log += w * w.ln();
nonzero += 1.0;
}
}
if nonzero <= 1.0 {
return 0.0;
}
let entropy = total.ln() - weighted_log / total;
(entropy / nonzero.ln()).min(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-4, "expected {b}, got {a}");
}
#[test]
fn evenness_empty_or_single_category_is_zero() {
approx(evenness::<usize>(&[]), 0.0);
approx(evenness(&[7]), 0.0);
approx(evenness(&[0, 5, 0]), 0.0);
}
#[test]
fn evenness_equal_counts_is_maximal() {
approx(evenness(&[4, 4, 4]), 1.0);
approx(evenness(&[1, 1]), 1.0);
}
#[test]
fn evenness_accepts_fractional_weights() {
approx(evenness(&[1.5_f32, 1.0, 0.5]), 0.9206);
}
#[test]
fn evenness_skewed_counts_drops_below_one() {
approx(evenness(&[15, 10, 5]), 0.9206);
}
#[test]
fn evenness_ignores_empty_categories_in_the_denominator() {
approx(evenness(&[5, 0, 5, 0]), 1.0);
}
}