use crate::error::FdarError;
use crate::matrix::FdMatrix;
fn column_ranks(data: &FdMatrix, col: usize) -> Vec<f64> {
let n = data.nrows();
let mut indexed: Vec<(f64, usize)> = (0..n).map(|i| (data[(i, col)], i)).collect();
indexed.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut ranks = vec![0.0_f64; n];
let mut k = 0;
while k < n {
let mut j = k + 1;
while j < n && indexed[j].0 == indexed[k].0 {
j += 1;
}
let avg_rank = (k as f64 + 1.0 + j as f64) / 2.0;
for item in &indexed[k..j] {
ranks[item.1] = avg_rank;
}
k = j;
}
ranks
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn extremal_depth_1d(data_obj: &FdMatrix, data_ori: &FdMatrix) -> Result<Vec<f64>, FdarError> {
let (n, m) = (data_ori.nrows(), data_ori.ncols());
if n == 0 || m == 0 || data_obj.nrows() == 0 || data_obj.ncols() == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data_ori",
expected: "non-empty matrix".to_string(),
actual: format!("{n}x{m}"),
});
}
if data_obj.ncols() != m {
return Err(FdarError::InvalidDimension {
parameter: "data_obj",
expected: format!("same number of columns as data_ori ({m})"),
actual: format!("{}", data_obj.ncols()),
});
}
if n < 3 {
return Err(FdarError::InvalidDimension {
parameter: "data_ori",
expected: "at least 3 curves for extremal depth".to_string(),
actual: format!("{n}"),
});
}
let mut d = vec![vec![0.0_f64; m]; n];
for t in 0..m {
let ranks = column_ranks(data_ori, t);
for i in 0..n {
d[i][t] = 1.0 - (2.0 * ranks[i] - n as f64 - 1.0).abs() / n as f64;
}
}
let eps = 1e-9;
let mut d_level = vec![0.0_f64; n];
let mut mass = vec![0.0_f64; n];
for i in 0..n {
let mut lvl = f64::INFINITY;
for t in 0..m {
if d[i][t] < lvl {
lvl = d[i][t];
}
}
let cnt = (0..m).filter(|&t| (d[i][t] - lvl).abs() < eps).count();
d_level[i] = lvl;
mass[i] = cnt as f64 / m as f64;
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
d_level[a]
.partial_cmp(&d_level[b])
.unwrap_or(std::cmp::Ordering::Equal)
.then(
mass[b]
.partial_cmp(&mass[a])
.unwrap_or(std::cmp::Ordering::Equal),
)
.then(a.cmp(&b))
});
let mut depth = vec![0.0_f64; n];
for (k, &idx) in order.iter().enumerate() {
depth[idx] = (k + 1) as f64 / n as f64;
}
Ok(depth)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::depth::dispatch::{functional_depth, DepthMethod};
fn sample(n: usize, m: usize) -> FdMatrix {
let mut col_major = vec![0.0; n * m];
for i in 0..n {
for t in 0..m {
let x = t as f64 / (m as f64 - 1.0);
col_major[i + t * n] = (x * std::f64::consts::PI).sin() + 0.05 * i as f64;
}
}
FdMatrix::from_column_major(col_major, n, m).unwrap()
}
fn sample_with_outlier(n: usize, m: usize, outlier_idx: usize) -> FdMatrix {
let mut col_major = vec![0.0; n * m];
for i in 0..n {
for t in 0..m {
let x = t as f64 / (m as f64 - 1.0);
let base = (x * std::f64::consts::PI).sin();
let val = if i == outlier_idx {
base + 100.0
} else {
base + 0.01 * i as f64
};
col_major[i + t * n] = val;
}
}
FdMatrix::from_column_major(col_major, n, m).unwrap()
}
#[test]
fn depths_in_unit_interval_and_deepest_is_one() {
let n = 9usize;
let data = sample(n, 25);
let ed = extremal_depth_1d(&data, &data).unwrap();
assert_eq!(ed.len(), n);
for &v in &ed {
assert!(v > 0.0 && v <= 1.0 + 1e-12, "ED out of range: {v}");
}
let max = ed.iter().cloned().fold(f64::MIN, f64::max);
assert!(
(max - 1.0).abs() < 1e-12,
"deepest ED should be 1.0, got {max}"
);
}
#[test]
fn central_deepest_and_outlier_among_shallowest() {
let outlier_idx = 3usize;
let data = sample_with_outlier(9, 25, outlier_idx);
let ed = extremal_depth_1d(&data, &data).unwrap();
let mut deepest = 0usize;
for i in 1..ed.len() {
if ed[i] > ed[deepest] {
deepest = i;
}
}
assert!(
(ed[deepest] - 1.0).abs() < 1e-12,
"median curve should have ED 1.0, got {}",
ed[deepest]
);
assert_ne!(deepest, outlier_idx, "outlier must not be deepest");
assert!(
ed[outlier_idx] <= 2.0 / 9.0 + 1e-12,
"outlier should be among the most extreme, got {}",
ed[outlier_idx]
);
}
#[test]
fn ordering_is_deterministic_across_runs() {
let n = 5usize;
let m = 10usize;
let mut col_major = vec![0.0; n * m];
for i in 0..n {
for t in 0..m {
let x = t as f64 / (m as f64 - 1.0);
let offset = if i == 2 { 1.0 } else { i as f64 };
col_major[i + t * n] = (x * std::f64::consts::PI).sin() + 0.1 * offset;
}
}
let data = FdMatrix::from_column_major(col_major, n, m).unwrap();
let a = extremal_depth_1d(&data, &data).unwrap();
let b = extremal_depth_1d(&data, &data).unwrap();
assert_eq!(a, b);
}
#[test]
fn dispatch_round_trip() {
let data = sample(6, 12);
let got = functional_depth(&data, DepthMethod::Extremal).unwrap();
assert_eq!(got, extremal_depth_1d(&data, &data).unwrap());
}
#[test]
fn empty_and_too_few_curves_return_err() {
let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
assert!(extremal_depth_1d(&empty, &empty).is_err());
let two = sample(2, 8);
assert!(extremal_depth_1d(&two, &two).is_err()); assert!(functional_depth(&two, DepthMethod::Extremal).is_err());
}
}