1use crate::depth::{band_1d, fraiman_muniz_1d, modified_band_1d, random_projection_1d_seeded};
14use crate::error::FdarError;
15use crate::matrix::FdMatrix;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
25#[non_exhaustive]
26pub enum DepthMethod {
27 FraimanMuniz {
29 scale: bool,
31 },
32 Band,
34 ModifiedBand,
36 RandomProjection {
38 nproj: usize,
40 seed: u64,
42 },
43}
44
45pub fn functional_depth(data: &FdMatrix, method: DepthMethod) -> Result<Vec<f64>, FdarError> {
57 let (n, m) = (data.nrows(), data.ncols());
58 if n == 0 || m == 0 {
59 return Err(FdarError::InvalidDimension {
60 parameter: "data",
61 expected: "non-empty matrix (nrows > 0 and ncols > 0)".to_string(),
62 actual: format!("{n}x{m}"),
63 });
64 }
65
66 let depths = match method {
67 DepthMethod::FraimanMuniz { scale } => fraiman_muniz_1d(data, data, scale),
68 DepthMethod::Band => {
69 if n < 2 {
70 return Err(FdarError::InvalidDimension {
71 parameter: "data",
72 expected: "at least 2 curves for band depth".to_string(),
73 actual: format!("{n}"),
74 });
75 }
76 band_1d(data, data)
77 }
78 DepthMethod::ModifiedBand => {
79 if n < 2 {
80 return Err(FdarError::InvalidDimension {
81 parameter: "data",
82 expected: "at least 2 curves for modified band depth".to_string(),
83 actual: format!("{n}"),
84 });
85 }
86 modified_band_1d(data, data)
87 }
88 DepthMethod::RandomProjection { nproj, seed } => {
89 if nproj == 0 {
90 return Err(FdarError::InvalidParameter {
91 parameter: "nproj",
92 message: "must be >= 1".to_string(),
93 });
94 }
95 random_projection_1d_seeded(data, data, nproj, Some(seed))
96 }
97 };
98
99 Ok(depths)
100}
101
102#[derive(Debug, Clone, PartialEq)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108#[non_exhaustive]
109pub struct FunctionalBoxplotResult {
110 pub median: Vec<f64>,
112 pub central_lower: Vec<f64>,
114 pub central_upper: Vec<f64>,
116 pub whisker_lower: Vec<f64>,
118 pub whisker_upper: Vec<f64>,
120 pub outliers: Vec<usize>,
122 pub depths: Vec<f64>,
124}
125
126pub fn functional_boxplot(
141 data: &FdMatrix,
142 method: DepthMethod,
143 factor: f64,
144) -> Result<FunctionalBoxplotResult, FdarError> {
145 let (n, m) = (data.nrows(), data.ncols());
146 if n == 0 || m == 0 {
147 return Err(FdarError::InvalidDimension {
148 parameter: "data",
149 expected: "non-empty matrix (nrows > 0 and ncols > 0)".to_string(),
150 actual: format!("{n}x{m}"),
151 });
152 }
153 if n < 2 {
154 return Err(FdarError::InvalidDimension {
155 parameter: "data",
156 expected: "at least 2 curves for a functional boxplot".to_string(),
157 actual: format!("{n}"),
158 });
159 }
160 if !factor.is_finite() || factor < 0.0 {
161 return Err(FdarError::InvalidParameter {
162 parameter: "factor",
163 message: "must be a finite value >= 0.0".to_string(),
164 });
165 }
166
167 let depths = functional_depth(data, method)?;
168
169 let mut median_row = 0usize;
171 for i in 1..n {
172 if depths[i] > depths[median_row] {
173 median_row = i;
174 }
175 }
176 let median: Vec<f64> = (0..m).map(|t| data[(median_row, t)]).collect();
177
178 let half = n.div_ceil(2);
180 let mut order: Vec<usize> = (0..n).collect();
181 order.sort_by(|&a, &b| {
182 depths[b]
183 .partial_cmp(&depths[a])
184 .unwrap_or(std::cmp::Ordering::Equal)
185 .then(a.cmp(&b))
186 });
187 let central_rows = &order[..half];
188
189 let mut central_lower = vec![f64::INFINITY; m];
191 let mut central_upper = vec![f64::NEG_INFINITY; m];
192 for &i in central_rows {
193 for t in 0..m {
194 let v = data[(i, t)];
195 if v < central_lower[t] {
196 central_lower[t] = v;
197 }
198 if v > central_upper[t] {
199 central_upper[t] = v;
200 }
201 }
202 }
203
204 let mut whisker_lower = vec![0.0; m];
206 let mut whisker_upper = vec![0.0; m];
207 for t in 0..m {
208 let width = central_upper[t] - central_lower[t];
209 whisker_lower[t] = central_lower[t] - factor * width;
210 whisker_upper[t] = central_upper[t] + factor * width;
211 }
212
213 let mut outliers = Vec::new();
215 for i in 0..n {
216 let mut flagged = false;
217 for t in 0..m {
218 let v = data[(i, t)];
219 if v < whisker_lower[t] || v > whisker_upper[t] {
220 flagged = true;
221 break;
222 }
223 }
224 if flagged {
225 outliers.push(i);
226 }
227 }
228
229 Ok(FunctionalBoxplotResult {
230 median,
231 central_lower,
232 central_upper,
233 whisker_lower,
234 whisker_upper,
235 outliers,
236 depths,
237 })
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 fn sample(n: usize, m: usize) -> FdMatrix {
246 let mut col_major = vec![0.0; n * m];
247 for i in 0..n {
248 for t in 0..m {
249 let x = t as f64 / (m as f64 - 1.0);
250 col_major[i + t * n] = (x * std::f64::consts::PI).sin() + 0.05 * i as f64;
252 }
253 }
254 FdMatrix::from_column_major(col_major, n, m).unwrap()
255 }
256
257 #[test]
258 fn fraiman_muniz_dispatch_equals_underlying() {
259 let data = sample(6, 12);
260 for scale in [true, false] {
261 let got = functional_depth(&data, DepthMethod::FraimanMuniz { scale }).unwrap();
262 let want = fraiman_muniz_1d(&data, &data, scale);
263 assert_eq!(got, want);
264 assert_eq!(got.len(), data.nrows());
265 }
266 }
267
268 #[test]
269 fn band_dispatch_equals_underlying() {
270 let data = sample(6, 12);
271 let got = functional_depth(&data, DepthMethod::Band).unwrap();
272 assert_eq!(got, band_1d(&data, &data));
273 assert_eq!(got.len(), 6);
274 }
275
276 #[test]
277 fn modified_band_dispatch_equals_underlying() {
278 let data = sample(6, 12);
279 let got = functional_depth(&data, DepthMethod::ModifiedBand).unwrap();
280 assert_eq!(got, modified_band_1d(&data, &data));
281 assert_eq!(got.len(), 6);
282 }
283
284 #[test]
285 fn random_projection_dispatch_equals_underlying_and_is_reproducible() {
286 let data = sample(6, 12);
287 let method = DepthMethod::RandomProjection {
288 nproj: 20,
289 seed: 42,
290 };
291 let got = functional_depth(&data, method).unwrap();
292 let want = random_projection_1d_seeded(&data, &data, 20, Some(42));
293 assert_eq!(got, want);
294 let got2 = functional_depth(&data, method).unwrap();
296 assert_eq!(got, got2);
297 }
298
299 #[test]
300 fn empty_matrix_returns_err() {
301 let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
302 assert!(functional_depth(&empty, DepthMethod::FraimanMuniz { scale: true }).is_err());
303 }
304
305 #[test]
306 fn too_few_curves_for_band_returns_err() {
307 let one = sample(1, 8);
308 assert!(functional_depth(&one, DepthMethod::Band).is_err());
309 assert!(functional_depth(&one, DepthMethod::ModifiedBand).is_err());
310 }
311
312 #[test]
313 fn zero_nproj_returns_err() {
314 let data = sample(6, 12);
315 assert!(
316 functional_depth(&data, DepthMethod::RandomProjection { nproj: 0, seed: 1 }).is_err()
317 );
318 }
319
320 fn sample_with_outlier(n: usize, m: usize, outlier_idx: usize) -> FdMatrix {
324 let mut col_major = vec![0.0; n * m];
325 for i in 0..n {
326 for t in 0..m {
327 let x = t as f64 / (m as f64 - 1.0);
328 let base = (x * std::f64::consts::PI).sin();
329 let val = if i == outlier_idx {
330 base + 100.0 } else {
332 base + 0.01 * i as f64 };
334 col_major[i + t * n] = val;
335 }
336 }
337 FdMatrix::from_column_major(col_major, n, m).unwrap()
338 }
339
340 #[test]
341 fn boxplot_flags_planted_outlier_and_spares_inliers() {
342 let outlier_idx = 3;
343 let data = sample_with_outlier(8, 15, outlier_idx);
344 let res = functional_boxplot(&data, DepthMethod::ModifiedBand, 1.5).unwrap();
345 assert!(res.outliers.contains(&outlier_idx));
346 for i in 0..8 {
347 if i != outlier_idx {
348 assert!(!res.outliers.contains(&i), "inlier {i} wrongly flagged");
349 }
350 }
351 }
352
353 #[test]
354 fn boxplot_median_equals_deepest_and_central_brackets_median() {
355 let data = sample_with_outlier(8, 15, 3);
356 let res = functional_boxplot(&data, DepthMethod::ModifiedBand, 1.5).unwrap();
357 let mut deepest = 0usize;
359 for i in 1..res.depths.len() {
360 if res.depths[i] > res.depths[deepest] {
361 deepest = i;
362 }
363 }
364 let expected_median: Vec<f64> = (0..data.ncols()).map(|t| data[(deepest, t)]).collect();
365 assert_eq!(res.median, expected_median);
366 for t in 0..data.ncols() {
367 assert!(res.central_lower[t] <= res.median[t] + 1e-12);
368 assert!(res.median[t] <= res.central_upper[t] + 1e-12);
369 }
370 }
371
372 #[test]
373 fn boxplot_fence_contains_central_region() {
374 let data = sample_with_outlier(8, 15, 3);
375 let res = functional_boxplot(&data, DepthMethod::ModifiedBand, 1.5).unwrap();
376 for t in 0..data.ncols() {
377 assert!(res.whisker_lower[t] <= res.central_lower[t] + 1e-12);
378 assert!(res.whisker_upper[t] >= res.central_upper[t] - 1e-12);
379 }
380 }
381
382 #[test]
383 fn boxplot_random_projection_is_seed_reproducible() {
384 let data = sample_with_outlier(8, 15, 3);
385 let method = DepthMethod::RandomProjection { nproj: 25, seed: 7 };
386 let a = functional_boxplot(&data, method, 1.5).unwrap();
387 let b = functional_boxplot(&data, method, 1.5).unwrap();
388 assert_eq!(a, b);
389 }
390
391 #[test]
392 fn boxplot_invalid_input_returns_err() {
393 let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
394 assert!(functional_boxplot(&empty, DepthMethod::ModifiedBand, 1.5).is_err());
395 let single = sample(1, 8);
396 assert!(functional_boxplot(&single, DepthMethod::ModifiedBand, 1.5).is_err());
397 let data = sample(6, 12);
398 assert!(functional_boxplot(&data, DepthMethod::ModifiedBand, -1.0).is_err());
399 }
400}