Skip to main content

fdars_core/classification/
dd.rs

1//! Depth-based DD-classifier.
2
3use crate::depth::fraiman_muniz_1d;
4use crate::error::FdarError;
5use crate::matrix::FdMatrix;
6
7use super::cv::extract_class_data;
8use super::kernel::{argmax_class, scalar_depth_for_obs};
9use super::{compute_accuracy, confusion_matrix, remap_labels, ClassifResult};
10
11/// Compute depth of all observations w.r.t. each class.
12fn compute_class_depths(data: &FdMatrix, class_indices: &[Vec<usize>], n: usize) -> FdMatrix {
13    let g = class_indices.len();
14    let mut depth_scores = FdMatrix::zeros(n, g);
15    for c in 0..g {
16        if class_indices[c].is_empty() {
17            continue;
18        }
19        let class_data = extract_class_data(data, &class_indices[c]);
20        let depths = fraiman_muniz_1d(data, &class_data, true);
21        for i in 0..n {
22            depth_scores[(i, c)] = depths[i];
23        }
24    }
25    depth_scores
26}
27
28/// Blend functional depth scores with scalar rank depth from covariates.
29pub(super) fn blend_scalar_depths(
30    depth_scores: &mut FdMatrix,
31    cov: &FdMatrix,
32    class_indices: &[Vec<usize>],
33    n: usize,
34) {
35    let g = class_indices.len();
36    let p = cov.ncols();
37    for c in 0..g {
38        for i in 0..n {
39            let sd = scalar_depth_for_obs(cov, i, &class_indices[c], p);
40            depth_scores[(i, c)] = 0.7 * depth_scores[(i, c)] + 0.3 * sd;
41        }
42    }
43}
44
45/// Depth-based DD-classifier for functional data.
46///
47/// # Errors
48///
49/// Returns [`FdarError::InvalidDimension`] if `data` has zero rows or `y.len() != n`.
50/// Returns [`FdarError::InvalidParameter`] if `y` contains fewer than 2 distinct classes.
51#[must_use = "expensive computation whose result should not be discarded"]
52pub fn fclassif_dd(
53    data: &FdMatrix,
54    y: &[usize],
55    scalar_covariates: Option<&FdMatrix>,
56) -> Result<ClassifResult, FdarError> {
57    let n = data.nrows();
58    if n == 0 || y.len() != n {
59        return Err(FdarError::InvalidDimension {
60            parameter: "data/y",
61            expected: "n > 0 and y.len() == n".to_string(),
62            actual: format!("n={}, y.len()={}", n, y.len()),
63        });
64    }
65
66    let (labels, g) = remap_labels(y);
67    if g < 2 {
68        return Err(FdarError::InvalidParameter {
69            parameter: "y",
70            message: format!("need at least 2 classes, got {g}"),
71        });
72    }
73
74    let class_indices: Vec<Vec<usize>> = (0..g)
75        .map(|c| (0..n).filter(|&i| labels[i] == c).collect())
76        .collect();
77
78    let mut depth_scores = compute_class_depths(data, &class_indices, n);
79
80    if let Some(cov) = scalar_covariates {
81        blend_scalar_depths(&mut depth_scores, cov, &class_indices, n);
82    }
83
84    let predicted: Vec<usize> = (0..n)
85        .map(|i| {
86            let scores: Vec<f64> = (0..g).map(|c| depth_scores[(i, c)]).collect();
87            argmax_class(&scores)
88        })
89        .collect();
90
91    let accuracy = compute_accuracy(&labels, &predicted);
92    let confusion = confusion_matrix(&labels, &predicted, g);
93
94    Ok(ClassifResult {
95        predicted,
96        probabilities: Some(depth_scores),
97        accuracy,
98        confusion,
99        n_classes: g,
100        ncomp: 0,
101    })
102}