1use crate::depth::{
14 band_1d, epigraph_index_1d, extremal_depth_1d, extreme_rank_length_depth_1d, fraiman_muniz_1d,
15 half_region_depth_1d, hypograph_index_1d, linfinity_depth_1d, modified_band_1d,
16 modified_half_region_depth_1d, modified_hypograph_index_1d, random_projection_1d_seeded,
17 total_variation_depth_1d,
18};
19use crate::error::FdarError;
20use crate::matrix::FdMatrix;
21
22#[derive(Debug, Clone, Copy, PartialEq)]
30#[non_exhaustive]
31pub enum DepthMethod {
32 FraimanMuniz {
34 scale: bool,
36 },
37 Band,
39 ModifiedBand,
41 RandomProjection {
43 nproj: usize,
45 seed: u64,
47 },
48 HypographIndex,
51 ModifiedHypographIndex,
54 EpigraphIndex,
57 HalfRegion,
60 ModifiedHalfRegion,
63 Extremal,
66 ExtremeRankLength,
69 LInfinity,
71 TotalVariation,
74}
75
76pub fn functional_depth(data: &FdMatrix, method: DepthMethod) -> Result<Vec<f64>, FdarError> {
88 let (n, m) = (data.nrows(), data.ncols());
89 if n == 0 || m == 0 {
90 return Err(FdarError::InvalidDimension {
91 parameter: "data",
92 expected: "non-empty matrix (nrows > 0 and ncols > 0)".to_string(),
93 actual: format!("{n}x{m}"),
94 });
95 }
96
97 let depths = match method {
98 DepthMethod::FraimanMuniz { scale } => fraiman_muniz_1d(data, data, scale),
99 DepthMethod::Band => {
100 if n < 2 {
101 return Err(FdarError::InvalidDimension {
102 parameter: "data",
103 expected: "at least 2 curves for band depth".to_string(),
104 actual: format!("{n}"),
105 });
106 }
107 band_1d(data, data)
108 }
109 DepthMethod::ModifiedBand => {
110 if n < 2 {
111 return Err(FdarError::InvalidDimension {
112 parameter: "data",
113 expected: "at least 2 curves for modified band depth".to_string(),
114 actual: format!("{n}"),
115 });
116 }
117 modified_band_1d(data, data)
118 }
119 DepthMethod::RandomProjection { nproj, seed } => {
120 if nproj == 0 {
121 return Err(FdarError::InvalidParameter {
122 parameter: "nproj",
123 message: "must be >= 1".to_string(),
124 });
125 }
126 random_projection_1d_seeded(data, data, nproj, Some(seed))
127 }
128 DepthMethod::HypographIndex => {
129 if n < 2 {
130 return Err(FdarError::InvalidDimension {
131 parameter: "data",
132 expected: "at least 2 curves for hypograph index".to_string(),
133 actual: format!("{n}"),
134 });
135 }
136 hypograph_index_1d(data, data)?
137 }
138 DepthMethod::ModifiedHypographIndex => modified_hypograph_index_1d(data, data)?,
139 DepthMethod::EpigraphIndex => {
140 if n < 2 {
141 return Err(FdarError::InvalidDimension {
142 parameter: "data",
143 expected: "at least 2 curves for epigraph index".to_string(),
144 actual: format!("{n}"),
145 });
146 }
147 epigraph_index_1d(data, data)?
148 }
149 DepthMethod::HalfRegion => {
150 if n < 2 {
151 return Err(FdarError::InvalidDimension {
152 parameter: "data",
153 expected: "at least 2 curves for half-region depth".to_string(),
154 actual: format!("{n}"),
155 });
156 }
157 half_region_depth_1d(data, data)?
158 }
159 DepthMethod::ModifiedHalfRegion => {
160 if n < 2 {
161 return Err(FdarError::InvalidDimension {
162 parameter: "data",
163 expected: "at least 2 curves for modified half-region depth".to_string(),
164 actual: format!("{n}"),
165 });
166 }
167 modified_half_region_depth_1d(data, data)?
168 }
169 DepthMethod::Extremal => {
170 if n < 3 {
171 return Err(FdarError::InvalidDimension {
172 parameter: "data",
173 expected: "at least 3 curves for extremal depth".to_string(),
174 actual: format!("{n}"),
175 });
176 }
177 extremal_depth_1d(data, data)?
178 }
179 DepthMethod::ExtremeRankLength => {
180 if n < 2 {
181 return Err(FdarError::InvalidDimension {
182 parameter: "data",
183 expected: "at least 2 curves for extreme-rank-length depth".to_string(),
184 actual: format!("{n}"),
185 });
186 }
187 extreme_rank_length_depth_1d(data, data)?
188 }
189 DepthMethod::LInfinity => linfinity_depth_1d(data, data)?,
190 DepthMethod::TotalVariation => {
191 if n < 3 {
192 return Err(FdarError::InvalidDimension {
193 parameter: "data",
194 expected: "at least 3 curves for total variation depth".to_string(),
195 actual: format!("{n}"),
196 });
197 }
198 total_variation_depth_1d(data, data)?.tvd
199 }
200 };
201
202 Ok(depths)
203}
204
205#[derive(Debug, Clone, PartialEq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211#[non_exhaustive]
212pub struct FunctionalBoxplotResult {
213 pub median: Vec<f64>,
215 pub central_lower: Vec<f64>,
217 pub central_upper: Vec<f64>,
219 pub whisker_lower: Vec<f64>,
221 pub whisker_upper: Vec<f64>,
223 pub outliers: Vec<usize>,
225 pub depths: Vec<f64>,
227}
228
229pub fn functional_boxplot(
244 data: &FdMatrix,
245 method: DepthMethod,
246 factor: f64,
247) -> Result<FunctionalBoxplotResult, FdarError> {
248 let (n, m) = (data.nrows(), data.ncols());
249 if n == 0 || m == 0 {
250 return Err(FdarError::InvalidDimension {
251 parameter: "data",
252 expected: "non-empty matrix (nrows > 0 and ncols > 0)".to_string(),
253 actual: format!("{n}x{m}"),
254 });
255 }
256 if n < 2 {
257 return Err(FdarError::InvalidDimension {
258 parameter: "data",
259 expected: "at least 2 curves for a functional boxplot".to_string(),
260 actual: format!("{n}"),
261 });
262 }
263 if !factor.is_finite() || factor < 0.0 {
264 return Err(FdarError::InvalidParameter {
265 parameter: "factor",
266 message: "must be a finite value >= 0.0".to_string(),
267 });
268 }
269
270 let depths = functional_depth(data, method)?;
271
272 let mut median_row = 0usize;
274 for i in 1..n {
275 if depths[i] > depths[median_row] {
276 median_row = i;
277 }
278 }
279 let median: Vec<f64> = (0..m).map(|t| data[(median_row, t)]).collect();
280
281 let half = n.div_ceil(2);
283 let mut order: Vec<usize> = (0..n).collect();
284 order.sort_by(|&a, &b| {
285 depths[b]
286 .partial_cmp(&depths[a])
287 .unwrap_or(std::cmp::Ordering::Equal)
288 .then(a.cmp(&b))
289 });
290 let central_rows = &order[..half];
291
292 let mut central_lower = vec![f64::INFINITY; m];
294 let mut central_upper = vec![f64::NEG_INFINITY; m];
295 for &i in central_rows {
296 for t in 0..m {
297 let v = data[(i, t)];
298 if v < central_lower[t] {
299 central_lower[t] = v;
300 }
301 if v > central_upper[t] {
302 central_upper[t] = v;
303 }
304 }
305 }
306
307 let mut whisker_lower = vec![0.0; m];
309 let mut whisker_upper = vec![0.0; m];
310 for t in 0..m {
311 let width = central_upper[t] - central_lower[t];
312 whisker_lower[t] = central_lower[t] - factor * width;
313 whisker_upper[t] = central_upper[t] + factor * width;
314 }
315
316 let mut outliers = Vec::new();
318 for i in 0..n {
319 let mut flagged = false;
320 for t in 0..m {
321 let v = data[(i, t)];
322 if v < whisker_lower[t] || v > whisker_upper[t] {
323 flagged = true;
324 break;
325 }
326 }
327 if flagged {
328 outliers.push(i);
329 }
330 }
331
332 Ok(FunctionalBoxplotResult {
333 median,
334 central_lower,
335 central_upper,
336 whisker_lower,
337 whisker_upper,
338 outliers,
339 depths,
340 })
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 fn sample(n: usize, m: usize) -> FdMatrix {
349 let mut col_major = vec![0.0; n * m];
350 for i in 0..n {
351 for t in 0..m {
352 let x = t as f64 / (m as f64 - 1.0);
353 col_major[i + t * n] = (x * std::f64::consts::PI).sin() + 0.05 * i as f64;
355 }
356 }
357 FdMatrix::from_column_major(col_major, n, m).unwrap()
358 }
359
360 #[test]
361 fn fraiman_muniz_dispatch_equals_underlying() {
362 let data = sample(6, 12);
363 for scale in [true, false] {
364 let got = functional_depth(&data, DepthMethod::FraimanMuniz { scale }).unwrap();
365 let want = fraiman_muniz_1d(&data, &data, scale);
366 assert_eq!(got, want);
367 assert_eq!(got.len(), data.nrows());
368 }
369 }
370
371 #[test]
372 fn band_dispatch_equals_underlying() {
373 let data = sample(6, 12);
374 let got = functional_depth(&data, DepthMethod::Band).unwrap();
375 assert_eq!(got, band_1d(&data, &data));
376 assert_eq!(got.len(), 6);
377 }
378
379 #[test]
380 fn modified_band_dispatch_equals_underlying() {
381 let data = sample(6, 12);
382 let got = functional_depth(&data, DepthMethod::ModifiedBand).unwrap();
383 assert_eq!(got, modified_band_1d(&data, &data));
384 assert_eq!(got.len(), 6);
385 }
386
387 #[test]
388 fn random_projection_dispatch_equals_underlying_and_is_reproducible() {
389 let data = sample(6, 12);
390 let method = DepthMethod::RandomProjection {
391 nproj: 20,
392 seed: 42,
393 };
394 let got = functional_depth(&data, method).unwrap();
395 let want = random_projection_1d_seeded(&data, &data, 20, Some(42));
396 assert_eq!(got, want);
397 let got2 = functional_depth(&data, method).unwrap();
399 assert_eq!(got, got2);
400 }
401
402 #[test]
403 fn empty_matrix_returns_err() {
404 let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
405 assert!(functional_depth(&empty, DepthMethod::FraimanMuniz { scale: true }).is_err());
406 }
407
408 #[test]
409 fn too_few_curves_for_band_returns_err() {
410 let one = sample(1, 8);
411 assert!(functional_depth(&one, DepthMethod::Band).is_err());
412 assert!(functional_depth(&one, DepthMethod::ModifiedBand).is_err());
413 }
414
415 #[test]
416 fn zero_nproj_returns_err() {
417 let data = sample(6, 12);
418 assert!(
419 functional_depth(&data, DepthMethod::RandomProjection { nproj: 0, seed: 1 }).is_err()
420 );
421 }
422
423 fn sample_with_outlier(n: usize, m: usize, outlier_idx: usize) -> FdMatrix {
427 let mut col_major = vec![0.0; n * m];
428 for i in 0..n {
429 for t in 0..m {
430 let x = t as f64 / (m as f64 - 1.0);
431 let base = (x * std::f64::consts::PI).sin();
432 let val = if i == outlier_idx {
433 base + 100.0 } else {
435 base + 0.01 * i as f64 };
437 col_major[i + t * n] = val;
438 }
439 }
440 FdMatrix::from_column_major(col_major, n, m).unwrap()
441 }
442
443 #[test]
444 fn boxplot_flags_planted_outlier_and_spares_inliers() {
445 let outlier_idx = 3;
446 let data = sample_with_outlier(8, 15, outlier_idx);
447 let res = functional_boxplot(&data, DepthMethod::ModifiedBand, 1.5).unwrap();
448 assert!(res.outliers.contains(&outlier_idx));
449 for i in 0..8 {
450 if i != outlier_idx {
451 assert!(!res.outliers.contains(&i), "inlier {i} wrongly flagged");
452 }
453 }
454 }
455
456 #[test]
457 fn boxplot_median_equals_deepest_and_central_brackets_median() {
458 let data = sample_with_outlier(8, 15, 3);
459 let res = functional_boxplot(&data, DepthMethod::ModifiedBand, 1.5).unwrap();
460 let mut deepest = 0usize;
462 for i in 1..res.depths.len() {
463 if res.depths[i] > res.depths[deepest] {
464 deepest = i;
465 }
466 }
467 let expected_median: Vec<f64> = (0..data.ncols()).map(|t| data[(deepest, t)]).collect();
468 assert_eq!(res.median, expected_median);
469 for t in 0..data.ncols() {
470 assert!(res.central_lower[t] <= res.median[t] + 1e-12);
471 assert!(res.median[t] <= res.central_upper[t] + 1e-12);
472 }
473 }
474
475 #[test]
476 fn boxplot_fence_contains_central_region() {
477 let data = sample_with_outlier(8, 15, 3);
478 let res = functional_boxplot(&data, DepthMethod::ModifiedBand, 1.5).unwrap();
479 for t in 0..data.ncols() {
480 assert!(res.whisker_lower[t] <= res.central_lower[t] + 1e-12);
481 assert!(res.whisker_upper[t] >= res.central_upper[t] - 1e-12);
482 }
483 }
484
485 #[test]
486 fn boxplot_random_projection_is_seed_reproducible() {
487 let data = sample_with_outlier(8, 15, 3);
488 let method = DepthMethod::RandomProjection { nproj: 25, seed: 7 };
489 let a = functional_boxplot(&data, method, 1.5).unwrap();
490 let b = functional_boxplot(&data, method, 1.5).unwrap();
491 assert_eq!(a, b);
492 }
493
494 #[test]
495 fn boxplot_invalid_input_returns_err() {
496 let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
497 assert!(functional_boxplot(&empty, DepthMethod::ModifiedBand, 1.5).is_err());
498 let single = sample(1, 8);
499 assert!(functional_boxplot(&single, DepthMethod::ModifiedBand, 1.5).is_err());
500 let data = sample(6, 12);
501 assert!(functional_boxplot(&data, DepthMethod::ModifiedBand, -1.0).is_err());
502 }
503
504 const NEW_VARIANTS: [DepthMethod; 9] = [
508 DepthMethod::HalfRegion,
509 DepthMethod::ModifiedHalfRegion,
510 DepthMethod::HypographIndex,
511 DepthMethod::ModifiedHypographIndex,
512 DepthMethod::EpigraphIndex,
513 DepthMethod::Extremal,
514 DepthMethod::ExtremeRankLength,
515 DepthMethod::LInfinity,
516 DepthMethod::TotalVariation,
517 ];
518
519 #[test]
520 fn all_nine_new_variants_round_trip() {
521 let data = sample(6, 12); for method in NEW_VARIANTS {
523 let got = functional_depth(&data, method).unwrap();
524 assert_eq!(got.len(), data.nrows(), "wrong length for {method:?}");
525 }
526 }
527
528 #[test]
529 fn existing_variants_unchanged_regression() {
530 let data = sample(6, 12);
532 assert!(functional_depth(&data, DepthMethod::FraimanMuniz { scale: false }).is_ok());
533 assert!(functional_depth(&data, DepthMethod::Band).is_ok());
534 assert!(functional_depth(&data, DepthMethod::ModifiedBand).is_ok());
535 assert!(
536 functional_depth(&data, DepthMethod::RandomProjection { nproj: 10, seed: 1 }).is_ok()
537 );
538 }
539
540 #[test]
541 fn min_n_guards_return_err_without_panic() {
542 let single = sample(1, 8);
544 for method in [
545 DepthMethod::Band,
546 DepthMethod::HalfRegion,
547 DepthMethod::ModifiedHalfRegion,
548 DepthMethod::HypographIndex,
549 DepthMethod::EpigraphIndex,
550 DepthMethod::ExtremeRankLength,
551 DepthMethod::Extremal,
552 DepthMethod::TotalVariation,
553 ] {
554 assert!(
555 functional_depth(&single, method).is_err(),
556 "{method:?} should reject n=1"
557 );
558 }
559 let two = sample(2, 8);
561 assert!(functional_depth(&two, DepthMethod::Extremal).is_err());
562 assert!(functional_depth(&two, DepthMethod::TotalVariation).is_err());
563 let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
565 assert!(functional_depth(&empty, DepthMethod::LInfinity).is_err());
566 }
567}