use crate::{Error, Result};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CorrelationPreservation {
pearson: f32,
spearman: f32,
count: usize,
}
impl CorrelationPreservation {
pub fn measure(source: &[f32], output: &[f32]) -> Result<Self> {
if source.len() != output.len() {
return Err(Error::validation(format!(
"paired series must have equal length; source {} vs output {}",
source.len(),
output.len()
)));
}
if source.len() < 2 {
return Err(Error::validation(
"correlation needs at least two paired observations",
));
}
ensure_finite(source, "source")?;
ensure_finite(output, "output")?;
let linear = pearson(source, output)?;
let rank_order = pearson(&ranks(source), &ranks(output))?;
Ok(Self {
pearson: linear as f32,
spearman: rank_order as f32,
count: source.len(),
})
}
pub fn pearson(&self) -> f32 {
self.pearson
}
pub fn spearman(&self) -> f32 {
self.spearman
}
pub fn count(&self) -> usize {
self.count
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NeighborhoodPreservation {
overlap: f32,
k: usize,
count: usize,
}
impl NeighborhoodPreservation {
pub fn measure(source: &[Vec<f32>], output: &[Vec<f32>], k: usize) -> Result<Self> {
let n = source.len();
if n != output.len() {
return Err(Error::validation(format!(
"paired embeddings must have equal length; source {n} vs output {}",
output.len()
)));
}
if k == 0 {
return Err(Error::validation("neighborhood size k must be positive"));
}
if k >= n {
return Err(Error::validation(format!(
"neighborhood size k = {k} needs at least {} items, got {n}",
k + 1
)));
}
ensure_embeddings(source, "source")?;
ensure_embeddings(output, "output")?;
let mut total = 0.0f64;
for i in 0..n {
let src = nearest(source, i, k);
let out = nearest(output, i, k);
let shared = src.iter().filter(|j| out.contains(j)).count();
total += shared as f64 / k as f64;
}
Ok(Self {
overlap: (total / n as f64) as f32,
k,
count: n,
})
}
pub fn overlap(&self) -> f32 {
self.overlap
}
pub fn k(&self) -> usize {
self.k
}
pub fn count(&self) -> usize {
self.count
}
}
fn pearson(x: &[f32], y: &[f32]) -> Result<f64> {
let n = x.len() as f64;
let mx = x.iter().map(|&v| v as f64).sum::<f64>() / n;
let my = y.iter().map(|&v| v as f64).sum::<f64>() / n;
let (mut sxy, mut sxx, mut syy) = (0.0f64, 0.0f64, 0.0f64);
for (&xi, &yi) in x.iter().zip(y) {
let (dx, dy) = (xi as f64 - mx, yi as f64 - my);
sxy += dx * dy;
sxx += dx * dx;
syy += dy * dy;
}
if sxx <= 0.0 || syy <= 0.0 {
return Err(Error::validation(
"a series is constant; correlation is undefined",
));
}
Ok((sxy / (sxx * syy).sqrt()).clamp(-1.0, 1.0))
}
fn ranks(xs: &[f32]) -> Vec<f32> {
let mut order: Vec<usize> = (0..xs.len()).collect();
order.sort_by(|&a, &b| xs[a].partial_cmp(&xs[b]).expect("finite values compare"));
let mut ranks = vec![0.0f32; xs.len()];
let mut i = 0;
while i < order.len() {
let mut j = i;
while j + 1 < order.len() && xs[order[j + 1]] == xs[order[i]] {
j += 1;
}
let avg = (i + j) as f32 / 2.0 + 1.0;
for &idx in &order[i..=j] {
ranks[idx] = avg;
}
i = j + 1;
}
ranks
}
fn nearest(set: &[Vec<f32>], i: usize, k: usize) -> Vec<usize> {
let mut others: Vec<(f64, usize)> = (0..set.len())
.filter(|&j| j != i)
.map(|j| (sq_distance(&set[i], &set[j]), j))
.collect();
others.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.expect("finite distances compare")
.then(a.1.cmp(&b.1))
});
others.into_iter().take(k).map(|(_, j)| j).collect()
}
fn sq_distance(a: &[f32], b: &[f32]) -> f64 {
a.iter()
.zip(b)
.map(|(&x, &y)| {
let d = x as f64 - y as f64;
d * d
})
.sum()
}
fn ensure_finite(xs: &[f32], label: &str) -> Result<()> {
if let Some(bad) = xs.iter().find(|v| !v.is_finite()) {
return Err(Error::validation(format!(
"{label} series must be finite; found {bad}"
)));
}
Ok(())
}
fn ensure_embeddings(set: &[Vec<f32>], label: &str) -> Result<()> {
let dim = set[0].len();
if dim == 0 {
return Err(Error::validation(format!(
"{label} embeddings must be non-empty"
)));
}
for (i, v) in set.iter().enumerate() {
if v.len() != dim {
return Err(Error::validation(format!(
"{label} embeddings must share a dimension; item 0 has {dim}, item {i} has {}",
v.len()
)));
}
ensure_finite(v, label)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn correlation_perfect_and_anti() {
let c = CorrelationPreservation::measure(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0]).unwrap();
assert!((c.pearson() - 1.0).abs() < 1e-6);
assert!((c.spearman() - 1.0).abs() < 1e-6);
assert_eq!(c.count(), 3);
let anti = CorrelationPreservation::measure(&[1.0, 2.0, 3.0], &[6.0, 4.0, 2.0]).unwrap();
assert!((anti.pearson() + 1.0).abs() < 1e-6);
assert!((anti.spearman() + 1.0).abs() < 1e-6);
}
#[test]
fn spearman_captures_monotone_nonlinear() {
let c = CorrelationPreservation::measure(&[1.0, 2.0, 3.0, 4.0], &[1.0, 4.0, 9.0, 16.0])
.unwrap();
assert!((c.spearman() - 1.0).abs() < 1e-6);
assert!(c.pearson() < 1.0 - 1e-4);
}
#[test]
fn correlation_rejects_degenerate_inputs() {
assert!(CorrelationPreservation::measure(&[1.0, 2.0], &[1.0]).is_err());
assert!(CorrelationPreservation::measure(&[1.0], &[1.0]).is_err());
assert!(CorrelationPreservation::measure(&[2.0, 2.0, 2.0], &[1.0, 2.0, 3.0]).is_err());
assert!(CorrelationPreservation::measure(&[1.0, 2.0], &[f32::NAN, 1.0]).is_err());
}
#[test]
fn neighborhood_perfect_when_identical() {
let pts = vec![vec![0.0], vec![1.0], vec![2.0], vec![5.0]];
let n = NeighborhoodPreservation::measure(&pts, &pts, 1).unwrap();
assert_eq!(n.overlap(), 1.0);
assert_eq!(n.k(), 1);
assert_eq!(n.count(), 4);
}
#[test]
fn neighborhood_partial_overlap() {
let source = vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0]];
let output = vec![vec![0.0], vec![1.0], vec![3.0], vec![2.0]];
let n = NeighborhoodPreservation::measure(&source, &output, 1).unwrap();
assert!((n.overlap() - 0.5).abs() < 1e-6);
}
#[test]
fn neighborhood_rejects_bad_shapes_and_k() {
let pts = vec![vec![0.0], vec![1.0], vec![2.0]];
assert!(NeighborhoodPreservation::measure(&pts, &pts, 0).is_err());
assert!(NeighborhoodPreservation::measure(&pts, &pts, 3).is_err());
assert!(NeighborhoodPreservation::measure(&pts, &pts[..2], 1).is_err());
let ragged = vec![vec![0.0, 1.0], vec![1.0], vec![2.0, 3.0]];
assert!(NeighborhoodPreservation::measure(&ragged, &pts, 1).is_err());
let nonfinite = vec![vec![0.0], vec![f32::INFINITY], vec![2.0]];
assert!(NeighborhoodPreservation::measure(&nonfinite, &pts, 1).is_err());
}
proptest! {
#[test]
fn correlation_identity_is_one(xs in proptest::collection::vec(-100.0f32..100.0, 2..32)) {
prop_assume!(xs.iter().any(|&v| v != xs[0]));
let c = CorrelationPreservation::measure(&xs, &xs).unwrap();
prop_assert!((c.pearson() - 1.0).abs() < 1e-3);
prop_assert!((c.spearman() - 1.0).abs() < 1e-3);
}
#[test]
fn correlation_sign_follows_affine_slope(
xs in proptest::collection::vec(-50.0f32..50.0, 2..32),
a in prop_oneof![-10.0f32..-0.1, 0.1f32..10.0],
b in -5.0f32..5.0,
) {
prop_assume!(xs.iter().any(|&v| v != xs[0]));
let ys: Vec<f32> = xs.iter().map(|&x| a * x + b).collect();
let c = CorrelationPreservation::measure(&xs, &ys).unwrap();
prop_assert!((c.pearson() - a.signum()).abs() < 1e-3, "pearson {}", c.pearson());
prop_assert!((c.spearman() - a.signum()).abs() < 1e-3, "spearman {}", c.spearman());
}
#[test]
fn correlation_within_bounds(
xs in proptest::collection::vec(-50.0f32..50.0, 2..32),
ys in proptest::collection::vec(-50.0f32..50.0, 2..32),
) {
let n = xs.len().min(ys.len());
prop_assume!(xs[..n].iter().any(|&v| v != xs[0]));
prop_assume!(ys[..n].iter().any(|&v| v != ys[0]));
let c = CorrelationPreservation::measure(&xs[..n], &ys[..n]).unwrap();
prop_assert!((-1.0..=1.0).contains(&c.pearson()));
prop_assert!((-1.0..=1.0).contains(&c.spearman()));
}
#[test]
fn neighborhood_identity_is_one(
set in proptest::collection::vec(
proptest::collection::vec(-20.0f32..20.0, 1..6),
3..12,
),
k in 1usize..3,
) {
let dim = set[0].len();
let pts: Vec<Vec<f32>> = set.iter().filter(|v| v.len() == dim).cloned().collect();
prop_assume!(pts.len() > k);
let n = NeighborhoodPreservation::measure(&pts, &pts, k).unwrap();
prop_assert_eq!(n.overlap(), 1.0);
}
#[test]
fn neighborhood_within_bounds(
base in proptest::collection::vec(
proptest::collection::vec(-20.0f32..20.0, 2..3),
4..12,
),
shuffle in proptest::collection::vec(-20.0f32..20.0, 8..24),
k in 1usize..3,
) {
let source = base.clone();
let output: Vec<Vec<f32>> = source
.iter()
.enumerate()
.map(|(i, _)| vec![shuffle[(2 * i) % shuffle.len()], shuffle[(2 * i + 1) % shuffle.len()]])
.collect();
prop_assume!(source.len() > k);
let n = NeighborhoodPreservation::measure(&source, &output, k).unwrap();
prop_assert!((0.0..=1.0).contains(&n.overlap()));
}
}
}