use faer::MatRef;
use rayon::prelude::*;
use crate::prelude::*;
use bixverse_core::core::math::vector_helpers::rank_vector;
#[derive(Clone, Debug)]
pub struct LigandActivityScores<T> {
pub auroc: Vec<T>,
pub aupr: Vec<T>,
pub aupr_corrected: Vec<T>,
pub pearson: Vec<T>,
pub spearman: Vec<T>,
}
fn auroc_from_ranks<T: BixverseFloat + std::iter::Sum>(
ranks: &[T],
pos_indices: &[usize],
n_pos: usize,
n_neg: usize,
) -> T {
if n_pos == 0 || n_neg == 0 {
return T::nan();
}
let sum_pos_ranks: T = pos_indices.iter().map(|&i| ranks[i]).sum();
let n_pos_t = T::from_usize(n_pos).unwrap();
let n_neg_t = T::from_usize(n_neg).unwrap();
let half = T::from_f64(0.5).unwrap();
let u = sum_pos_ranks - n_pos_t * (n_pos_t + T::one()) * half;
u / (n_pos_t * n_neg_t)
}
fn aupr_value<T: BixverseFloat>(pred: &[T], response: &[bool], n_pos: usize) -> T {
let n = pred.len();
let n_neg = n - n_pos;
if n_pos == 0 || n_neg == 0 {
return T::nan();
}
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_unstable_by(|&a, &b| pred[b].total_cmp(&pred[a]));
let n_pos_t = T::from_usize(n_pos).unwrap();
let half = T::from_f64(0.5).unwrap();
let mut tp: usize = 0;
let mut fp: usize = 0;
let mut prev_recall = T::zero();
let mut prev_precision = T::one();
let mut aupr = T::zero();
let mut i = 0;
while i < n {
let current = pred[idx[i]];
let mut j = i;
while j < n && pred[idx[j]] == current {
if response[idx[j]] {
tp += 1;
} else {
fp += 1;
}
j += 1;
}
let tp_t = T::from_usize(tp).unwrap();
let recall = tp_t / n_pos_t;
let precision = tp_t / T::from_usize(tp + fp).unwrap();
aupr += (recall - prev_recall) * (precision + prev_precision) * half;
prev_recall = recall;
prev_precision = precision;
i = j;
}
aupr
}
fn pearson_generic<T: BixverseFloat + std::iter::Sum>(x: &[T], y: &[T]) -> T {
let n = x.len();
if n < 2 {
return T::nan();
}
let n_t = T::from_usize(n).unwrap();
let sum_x: T = x.iter().copied().sum();
let sum_y: T = y.iter().copied().sum();
let sum_xy: T = x.iter().zip(y.iter()).map(|(&a, &b)| a * b).sum();
let sum_x2: T = x.iter().map(|&a| a * a).sum();
let sum_y2: T = y.iter().map(|&a| a * a).sum();
let mean_x = sum_x / n_t;
let mean_y = sum_y / n_t;
let var_x = sum_x2 / n_t - mean_x * mean_x;
let var_y = sum_y2 / n_t - mean_y * mean_y;
if var_x <= T::zero() || var_y <= T::zero() {
return T::nan();
}
let cov = sum_xy / n_t - mean_x * mean_y;
cov / (var_x.sqrt() * var_y.sqrt())
}
pub fn ligand_activity_scores<T>(
predictions: &MatRef<T>,
response: &[bool],
) -> LigandActivityScores<T>
where
T: BixverseFloat + std::iter::Sum + Send + Sync,
{
let n_ligands = predictions.nrows();
let n = response.len();
assert_eq!(
predictions.ncols(),
n,
"prediction columns must match response length"
);
let n_pos: usize = response.iter().filter(|&&b| b).count();
let n_neg: usize = n - n_pos;
let response_t: Vec<T> = response
.iter()
.map(|&b| if b { T::one() } else { T::zero() })
.collect();
let half = T::from_f64(0.5).unwrap();
let rank_neg = (T::from_usize(n_neg).unwrap() + T::one()) * half;
let rank_pos =
T::from_usize(n_neg).unwrap() + (T::from_usize(n_pos).unwrap() + T::one()) * half;
let response_ranks: Vec<T> = response
.iter()
.map(|&b| if b { rank_pos } else { rank_neg })
.collect();
let pos_indices: Vec<usize> = response
.iter()
.enumerate()
.filter_map(|(i, &b)| if b { Some(i) } else { None })
.collect();
let aupr_random = if n > 0 {
T::from_usize(n_pos).unwrap() / T::from_usize(n).unwrap()
} else {
T::nan()
};
let results: Vec<(T, T, T, T)> = (0..n_ligands)
.into_par_iter()
.map(|i| {
let pred_row: Vec<T> = (0..n).map(|j| predictions[(i, j)]).collect();
let pred_ranks = rank_vector(&pred_row);
let auroc = auroc_from_ranks(&pred_ranks, &pos_indices, n_pos, n_neg);
let aupr = aupr_value(&pred_row, response, n_pos);
let pearson = pearson_generic(&pred_row, &response_t);
let spearman = pearson_generic(&pred_ranks, &response_ranks);
(auroc, aupr, pearson, spearman)
})
.collect();
let mut auroc = Vec::with_capacity(n_ligands);
let mut aupr = Vec::with_capacity(n_ligands);
let mut aupr_corrected = Vec::with_capacity(n_ligands);
let mut pearson = Vec::with_capacity(n_ligands);
let mut spearman = Vec::with_capacity(n_ligands);
for (a, ap, p, s) in results {
auroc.push(a);
aupr.push(ap);
aupr_corrected.push(ap - aupr_random);
pearson.push(p);
spearman.push(s);
}
LigandActivityScores {
auroc,
aupr,
aupr_corrected,
pearson,
spearman,
}
}
#[cfg(test)]
mod tests {
use super::*;
use faer::Mat;
#[test]
fn auroc_perfect_ranking() {
let ranks = vec![1.0_f64, 2.0, 3.0, 4.0];
let pos = vec![2usize, 3];
assert!((auroc_from_ranks(&ranks, &pos, 2, 2) - 1.0).abs() < 1e-12);
}
#[test]
fn auroc_inverse_ranking() {
let ranks = vec![1.0_f64, 2.0, 3.0, 4.0];
let pos = vec![0usize, 1];
assert!(auroc_from_ranks(&ranks, &pos, 2, 2).abs() < 1e-12);
}
#[test]
fn auroc_partial() {
let ranks = vec![1.0_f64, 2.0, 3.0, 4.0];
let pos = vec![0usize, 2];
assert!((auroc_from_ranks(&ranks, &pos, 2, 2) - 0.25).abs() < 1e-12);
}
#[test]
fn auroc_degenerate_returns_nan() {
let ranks = vec![1.0_f64, 2.0];
assert!(auroc_from_ranks(&ranks, &[], 0, 2).is_nan());
assert!(auroc_from_ranks(&ranks, &[0, 1], 2, 0).is_nan());
}
#[test]
fn aupr_perfect_ranking() {
let pred = vec![4.0_f64, 3.0, 2.0, 1.0];
let response = vec![true, true, false, false];
assert!((aupr_value(&pred, &response, 2) - 1.0).abs() < 1e-12);
}
#[test]
fn aupr_degenerate_returns_nan() {
let pred = vec![1.0_f64, 2.0];
assert!(aupr_value(&pred, &[true, true], 2).is_nan());
assert!(aupr_value(&pred, &[false, false], 0).is_nan());
}
#[test]
fn pearson_perfect_positive() {
let x = vec![1.0_f64, 2.0, 3.0, 4.0];
let y = vec![2.0_f64, 4.0, 6.0, 8.0];
assert!((pearson_generic(&x, &y) - 1.0).abs() < 1e-12);
}
#[test]
fn pearson_perfect_negative() {
let x = vec![1.0_f64, 2.0, 3.0, 4.0];
let y = vec![4.0_f64, 3.0, 2.0, 1.0];
assert!((pearson_generic(&x, &y) + 1.0).abs() < 1e-12);
}
#[test]
fn pearson_zero_variance_returns_nan() {
let x = vec![1.0_f64, 1.0, 1.0];
let y = vec![1.0_f64, 2.0, 3.0];
assert!(pearson_generic(&x, &y).is_nan());
}
#[test]
fn ligand_activity_perfect_separation() {
let mut pred = Mat::<f64>::zeros(1, 4);
pred[(0, 0)] = 4.0;
pred[(0, 1)] = 3.0;
pred[(0, 2)] = 2.0;
pred[(0, 3)] = 1.0;
let response = vec![true, true, false, false];
let s = ligand_activity_scores(&pred.as_ref(), &response);
assert!((s.auroc[0] - 1.0).abs() < 1e-12);
assert!((s.aupr[0] - 1.0).abs() < 1e-12);
assert!((s.aupr_corrected[0] - 0.5).abs() < 1e-12);
assert!(s.pearson[0] > 0.0);
assert!(s.spearman[0] > 0.0);
}
}