pub fn monotonicity(sequence: &[f64]) -> f64 {
let n = sequence.len();
if n < 2 {
return f64::NAN;
}
if sequence.iter().any(|x| x.is_nan()) {
return f64::NAN;
}
let ranks = average_ranks(sequence);
pearson_with_naturals(&ranks)
}
fn average_ranks(values: &[f64]) -> Vec<f64> {
let n = values.len();
let mut indexed: Vec<(usize, f64)> = values.iter().copied().enumerate().collect();
indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut ranks = vec![0.0; n];
let mut i = 0;
while i < n {
let mut j = i;
while j + 1 < n && indexed[j + 1].1 == indexed[i].1 {
j += 1;
}
let avg_rank = (i + 1 + j + 1) as f64 / 2.0;
for k in i..=j {
ranks[indexed[k].0] = avg_rank;
}
i = j + 1;
}
ranks
}
fn pearson_with_naturals(ranks: &[f64]) -> f64 {
let n = ranks.len();
let n_f = n as f64;
let sum_y: f64 = ranks.iter().sum();
let sum_y2: f64 = ranks.iter().map(|v| v * v).sum();
let sum_xy: f64 = ranks
.iter()
.enumerate()
.map(|(i, &y)| (i + 1) as f64 * y)
.sum();
let sum_x = n_f * (n_f + 1.0) / 2.0;
let sum_x2 = n_f * (n_f + 1.0) * (2.0 * n_f + 1.0) / 6.0;
let numerator = n_f * sum_xy - sum_x * sum_y;
let var_x = n_f * sum_x2 - sum_x * sum_x;
let var_y = n_f * sum_y2 - sum_y * sum_y;
let denominator = (var_x * var_y).sqrt();
if denominator == 0.0 {
return f64::NAN;
}
numerator / denominator
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a.is_nan() && b.is_nan()) || (a - b).abs() <= tol
}
#[test]
fn strictly_increasing_returns_one() {
let seq: Vec<f64> = (0..50).map(|i| i as f64).collect();
assert!(approx_eq(monotonicity(&seq), 1.0, 1e-12));
}
#[test]
fn strictly_decreasing_returns_minus_one() {
let seq: Vec<f64> = (0..50).map(|i| -(i as f64)).collect();
assert!(approx_eq(monotonicity(&seq), -1.0, 1e-12));
}
#[test]
fn empty_or_single_returns_nan() {
assert!(monotonicity(&[]).is_nan());
assert!(monotonicity(&[2.71]).is_nan());
}
#[test]
fn constant_returns_nan() {
assert!(monotonicity(&[1.0, 1.0, 1.0, 1.0]).is_nan());
}
#[test]
fn nan_propagates() {
assert!(monotonicity(&[1.0, f64::NAN, 2.0]).is_nan());
}
#[test]
fn duplicates_use_average_rank() {
let r = monotonicity(&[10.0, 20.0, 20.0, 30.0]);
assert!(approx_eq(r, 0.948_683_298_050_513_8, 1e-12));
}
#[test]
fn random_sample_matches_known_value() {
let seq = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0, 5.0, 3.0, 5.0];
let r = monotonicity(&seq);
assert!(approx_eq(r, 0.437_829_861_082_743_97, 1e-12));
}
#[test]
fn two_element_distinct() {
assert!(approx_eq(monotonicity(&[1.0, 2.0]), 1.0, 1e-12));
assert!(approx_eq(monotonicity(&[2.0, 1.0]), -1.0, 1e-12));
}
}