1use crate::error::{StatsError, StatsResult};
14use scirs2_core::ndarray::{Array1, ArrayView1};
15use scirs2_core::numeric::{Float, NumCast};
16
17fn resample_with_replacement<F: Float + NumCast>(data: &[F], rng_state: &mut u64) -> Vec<F> {
24 let n = data.len();
25 let mut out = Vec::with_capacity(n);
26 for _ in 0..n {
27 let idx = lcg_next(rng_state) as usize % n;
28 out.push(data[idx]);
29 }
30 out
31}
32
33fn lcg_next(state: &mut u64) -> u64 {
36 *state = state
38 .wrapping_mul(6_364_136_223_846_793_005)
39 .wrapping_add(1_442_695_040_888_963_407);
40 *state >> 1 }
42
43fn sorted_f64_vec(v: &[f64]) -> Vec<f64> {
44 let mut s = v.to_vec();
45 s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
46 s
47}
48
49fn quantile_f64(sorted: &[f64], q: f64) -> f64 {
50 if sorted.is_empty() {
51 return 0.0;
52 }
53 if sorted.len() == 1 {
54 return sorted[0];
55 }
56 let idx = q * (sorted.len() as f64 - 1.0);
57 let lo = idx.floor() as usize;
58 let hi = idx.ceil() as usize;
59 let frac = idx - lo as f64;
60 sorted[lo.min(sorted.len() - 1)] * (1.0 - frac) + sorted[hi.min(sorted.len() - 1)] * frac
61}
62
63fn norm_cdf(x: f64) -> f64 {
65 if x < -8.0 {
66 return 0.0;
67 }
68 if x > 8.0 {
69 return 1.0;
70 }
71 let t = 1.0 / (1.0 + 0.231_641_9 * x.abs());
72 let d = 0.398_942_28 * (-0.5 * x * x).exp();
73 let p = t
74 * (0.319_381_530
75 + t * (-0.356_563_782
76 + t * (1.781_477_937 + t * (-1.821_255_978 + t * 1.330_274_429))));
77 if x >= 0.0 {
78 1.0 - d * p
79 } else {
80 d * p
81 }
82}
83
84fn norm_ppf(p: f64) -> f64 {
86 if p <= 0.0 {
87 return f64::NEG_INFINITY;
88 }
89 if p >= 1.0 {
90 return f64::INFINITY;
91 }
92 if (p - 0.5).abs() < f64::EPSILON {
93 return 0.0;
94 }
95 let (sign, pp) = if p < 0.5 { (-1.0, p) } else { (1.0, 1.0 - p) };
96 let t = (-2.0 * pp.ln()).sqrt();
97 let c0 = 2.515_517;
98 let c1 = 0.802_853;
99 let c2 = 0.010_328;
100 let d1 = 1.432_788;
101 let d2 = 0.189_269;
102 let d3 = 0.001_308;
103 sign * (t - (c0 + t * (c1 + t * c2)) / (1.0 + t * (d1 + t * (d2 + t * d3))))
104}
105
106#[derive(Debug, Clone)]
112pub struct BootstrapCI<F> {
113 pub estimate: F,
115 pub ci_lower: F,
117 pub ci_upper: F,
119 pub confidence_level: f64,
121 pub standard_error: F,
123 pub replicates: Vec<F>,
125}
126
127#[derive(Debug, Clone)]
129pub struct BcaBootstrapResult<F> {
130 pub ci: BootstrapCI<F>,
132 pub bias_correction: f64,
134 pub acceleration: f64,
136}
137
138pub fn percentile_bootstrap<F, S>(
178 x: &ArrayView1<F>,
179 statistic: S,
180 n_bootstrap: Option<usize>,
181 confidence_level: Option<f64>,
182 seed: Option<u64>,
183) -> StatsResult<BootstrapCI<F>>
184where
185 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
186 S: Fn(&[f64]) -> f64,
187{
188 if x.is_empty() {
189 return Err(StatsError::InvalidArgument(
190 "Input array cannot be empty".to_string(),
191 ));
192 }
193
194 let n_boot = n_bootstrap.unwrap_or(1000);
195 let conf = confidence_level.unwrap_or(0.95);
196 if conf <= 0.0 || conf >= 1.0 {
197 return Err(StatsError::InvalidArgument(
198 "confidence_level must be in (0, 1)".to_string(),
199 ));
200 }
201
202 let data_f64: Vec<f64> = x
203 .iter()
204 .map(|v| <f64 as NumCast>::from(*v).unwrap_or(0.0))
205 .collect();
206
207 let theta_hat = statistic(&data_f64);
208
209 let mut rng_state = seed.unwrap_or(12345);
210 let mut replicates_f64 = Vec::with_capacity(n_boot);
211
212 for _ in 0..n_boot {
213 let sample = resample_with_replacement(&data_f64, &mut rng_state);
214 replicates_f64.push(statistic(&sample));
215 }
216
217 let sorted_reps = sorted_f64_vec(&replicates_f64);
218 let alpha = 1.0 - conf;
219 let ci_lower_f64 = quantile_f64(&sorted_reps, alpha / 2.0);
220 let ci_upper_f64 = quantile_f64(&sorted_reps, 1.0 - alpha / 2.0);
221
222 let mean_rep = replicates_f64.iter().sum::<f64>() / replicates_f64.len() as f64;
224 let var_rep = replicates_f64
225 .iter()
226 .map(|&r| (r - mean_rep) * (r - mean_rep))
227 .sum::<f64>()
228 / (replicates_f64.len() as f64 - 1.0);
229 let se = var_rep.sqrt();
230
231 let replicates: Result<Vec<F>, _> = replicates_f64
232 .iter()
233 .map(|&v| F::from(v).ok_or_else(|| StatsError::ComputationError("cast".into())))
234 .collect();
235
236 Ok(BootstrapCI {
237 estimate: F::from(theta_hat).ok_or_else(|| StatsError::ComputationError("cast".into()))?,
238 ci_lower: F::from(ci_lower_f64)
239 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
240 ci_upper: F::from(ci_upper_f64)
241 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
242 confidence_level: conf,
243 standard_error: F::from(se).ok_or_else(|| StatsError::ComputationError("cast".into()))?,
244 replicates: replicates?,
245 })
246}
247
248pub fn bca_bootstrap<F, S>(
294 x: &ArrayView1<F>,
295 statistic: S,
296 n_bootstrap: Option<usize>,
297 confidence_level: Option<f64>,
298 seed: Option<u64>,
299) -> StatsResult<BcaBootstrapResult<F>>
300where
301 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
302 S: Fn(&[f64]) -> f64,
303{
304 if x.is_empty() {
305 return Err(StatsError::InvalidArgument(
306 "Input array cannot be empty".to_string(),
307 ));
308 }
309 let n = x.len();
310 if n < 2 {
311 return Err(StatsError::InvalidArgument(
312 "Need at least 2 observations for BCa bootstrap".to_string(),
313 ));
314 }
315
316 let n_boot = n_bootstrap.unwrap_or(2000);
317 let conf = confidence_level.unwrap_or(0.95);
318 if conf <= 0.0 || conf >= 1.0 {
319 return Err(StatsError::InvalidArgument(
320 "confidence_level must be in (0, 1)".to_string(),
321 ));
322 }
323
324 let data_f64: Vec<f64> = x
325 .iter()
326 .map(|v| <f64 as NumCast>::from(*v).unwrap_or(0.0))
327 .collect();
328
329 let theta_hat = statistic(&data_f64);
330
331 let mut rng_state = seed.unwrap_or(12345);
333 let mut replicates_f64 = Vec::with_capacity(n_boot);
334 for _ in 0..n_boot {
335 let sample = resample_with_replacement(&data_f64, &mut rng_state);
336 replicates_f64.push(statistic(&sample));
337 }
338
339 let prop_less =
341 replicates_f64.iter().filter(|&&r| r < theta_hat).count() as f64 / n_boot as f64;
342 let z0 = norm_ppf(prop_less.max(0.001).min(0.999));
343
344 let mut jackknife_vals = Vec::with_capacity(n);
347 for i in 0..n {
348 let jack_sample: Vec<f64> = data_f64
349 .iter()
350 .enumerate()
351 .filter_map(|(j, &v)| if j != i { Some(v) } else { None })
352 .collect();
353 jackknife_vals.push(statistic(&jack_sample));
354 }
355
356 let theta_dot = jackknife_vals.iter().sum::<f64>() / n as f64;
357 let d: Vec<f64> = jackknife_vals.iter().map(|&t| theta_dot - t).collect();
358
359 let sum_d2: f64 = d.iter().map(|&di| di * di).sum();
360 let sum_d3: f64 = d.iter().map(|&di| di * di * di).sum();
361
362 let acceleration = if sum_d2 > f64::EPSILON {
363 sum_d3 / (6.0 * sum_d2.powf(1.5))
364 } else {
365 0.0
366 };
367
368 let alpha = 1.0 - conf;
370 let z_alpha_lower = norm_ppf(alpha / 2.0);
371 let z_alpha_upper = norm_ppf(1.0 - alpha / 2.0);
372
373 let adjusted_lower = {
374 let num = z0 + z_alpha_lower;
375 let denom = 1.0 - acceleration * num;
376 if denom.abs() > f64::EPSILON {
377 norm_cdf(z0 + num / denom)
378 } else {
379 alpha / 2.0
380 }
381 };
382
383 let adjusted_upper = {
384 let num = z0 + z_alpha_upper;
385 let denom = 1.0 - acceleration * num;
386 if denom.abs() > f64::EPSILON {
387 norm_cdf(z0 + num / denom)
388 } else {
389 1.0 - alpha / 2.0
390 }
391 };
392
393 let sorted_reps = sorted_f64_vec(&replicates_f64);
394 let ci_lower_f64 = quantile_f64(&sorted_reps, adjusted_lower.max(0.0).min(1.0));
395 let ci_upper_f64 = quantile_f64(&sorted_reps, adjusted_upper.max(0.0).min(1.0));
396
397 let mean_rep = replicates_f64.iter().sum::<f64>() / replicates_f64.len() as f64;
399 let var_rep = replicates_f64
400 .iter()
401 .map(|&r| (r - mean_rep) * (r - mean_rep))
402 .sum::<f64>()
403 / (replicates_f64.len() as f64 - 1.0);
404 let se = var_rep.sqrt();
405
406 let replicates: Result<Vec<F>, _> = replicates_f64
407 .iter()
408 .map(|&v| F::from(v).ok_or_else(|| StatsError::ComputationError("cast".into())))
409 .collect();
410
411 Ok(BcaBootstrapResult {
412 ci: BootstrapCI {
413 estimate: F::from(theta_hat)
414 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
415 ci_lower: F::from(ci_lower_f64)
416 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
417 ci_upper: F::from(ci_upper_f64)
418 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
419 confidence_level: conf,
420 standard_error: F::from(se)
421 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
422 replicates: replicates?,
423 },
424 bias_correction: z0,
425 acceleration,
426 })
427}
428
429pub fn parametric_bootstrap<F, S>(
470 x: &ArrayView1<F>,
471 statistic: S,
472 n_bootstrap: Option<usize>,
473 confidence_level: Option<f64>,
474 seed: Option<u64>,
475) -> StatsResult<BootstrapCI<F>>
476where
477 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
478 S: Fn(&[f64]) -> f64,
479{
480 if x.is_empty() {
481 return Err(StatsError::InvalidArgument(
482 "Input array cannot be empty".to_string(),
483 ));
484 }
485 let n = x.len();
486 if n < 2 {
487 return Err(StatsError::InvalidArgument(
488 "Need at least 2 observations for parametric bootstrap".to_string(),
489 ));
490 }
491
492 let n_boot = n_bootstrap.unwrap_or(1000);
493 let conf = confidence_level.unwrap_or(0.95);
494
495 let data_f64: Vec<f64> = x
496 .iter()
497 .map(|v| <f64 as NumCast>::from(*v).unwrap_or(0.0))
498 .collect();
499
500 let theta_hat = statistic(&data_f64);
501
502 let sample_mean = data_f64.iter().sum::<f64>() / n as f64;
504 let sample_var = data_f64
505 .iter()
506 .map(|&v| (v - sample_mean) * (v - sample_mean))
507 .sum::<f64>()
508 / (n as f64 - 1.0);
509 let sample_sd = sample_var.sqrt();
510
511 let mut rng_state = seed.unwrap_or(12345);
513 let mut replicates_f64 = Vec::with_capacity(n_boot);
514
515 for _ in 0..n_boot {
516 let mut sample = Vec::with_capacity(n);
517 let mut i = 0;
518 while i < n {
519 let u1 = (lcg_next(&mut rng_state) as f64) / (u64::MAX as f64 / 2.0);
521 let u2 = (lcg_next(&mut rng_state) as f64) / (u64::MAX as f64 / 2.0);
522 let u1 = u1.max(1e-300); let r = (-2.0 * u1.ln()).sqrt();
524 let theta = 2.0 * std::f64::consts::PI * u2;
525
526 let z1 = r * theta.cos();
527 sample.push(sample_mean + sample_sd * z1);
528 i += 1;
529
530 if i < n {
531 let z2 = r * theta.sin();
532 sample.push(sample_mean + sample_sd * z2);
533 i += 1;
534 }
535 }
536 replicates_f64.push(statistic(&sample));
537 }
538
539 let sorted_reps = sorted_f64_vec(&replicates_f64);
540 let alpha = 1.0 - conf;
541 let ci_lower_f64 = quantile_f64(&sorted_reps, alpha / 2.0);
542 let ci_upper_f64 = quantile_f64(&sorted_reps, 1.0 - alpha / 2.0);
543
544 let mean_rep = replicates_f64.iter().sum::<f64>() / replicates_f64.len() as f64;
545 let var_rep = replicates_f64
546 .iter()
547 .map(|&r| (r - mean_rep) * (r - mean_rep))
548 .sum::<f64>()
549 / (replicates_f64.len() as f64 - 1.0);
550 let se = var_rep.sqrt();
551
552 let replicates: Result<Vec<F>, _> = replicates_f64
553 .iter()
554 .map(|&v| F::from(v).ok_or_else(|| StatsError::ComputationError("cast".into())))
555 .collect();
556
557 Ok(BootstrapCI {
558 estimate: F::from(theta_hat).ok_or_else(|| StatsError::ComputationError("cast".into()))?,
559 ci_lower: F::from(ci_lower_f64)
560 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
561 ci_upper: F::from(ci_upper_f64)
562 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
563 confidence_level: conf,
564 standard_error: F::from(se).ok_or_else(|| StatsError::ComputationError("cast".into()))?,
565 replicates: replicates?,
566 })
567}
568
569pub fn block_bootstrap_ci<F, S>(
612 x: &ArrayView1<F>,
613 statistic: S,
614 block_length: Option<usize>,
615 n_bootstrap: Option<usize>,
616 confidence_level: Option<f64>,
617 seed: Option<u64>,
618) -> StatsResult<BootstrapCI<F>>
619where
620 F: Float + std::iter::Sum<F> + NumCast + std::fmt::Display,
621 S: Fn(&[f64]) -> f64,
622{
623 let n = x.len();
624 if n < 3 {
625 return Err(StatsError::InvalidArgument(
626 "Need at least 3 observations for block bootstrap".to_string(),
627 ));
628 }
629
630 let block_len = block_length.unwrap_or_else(|| {
631 let auto = (n as f64).powf(1.0 / 3.0).ceil() as usize;
632 auto.max(1).min(n)
633 });
634
635 if block_len == 0 || block_len > n {
636 return Err(StatsError::InvalidArgument(format!(
637 "block_length must be between 1 and {} (n)",
638 n
639 )));
640 }
641
642 let n_boot = n_bootstrap.unwrap_or(1000);
643 let conf = confidence_level.unwrap_or(0.95);
644
645 let data_f64: Vec<f64> = x
646 .iter()
647 .map(|v| <f64 as NumCast>::from(*v).unwrap_or(0.0))
648 .collect();
649
650 let theta_hat = statistic(&data_f64);
651
652 let n_blocks_possible = n - block_len + 1;
654 let n_blocks_needed = (n as f64 / block_len as f64).ceil() as usize;
656
657 let mut rng_state = seed.unwrap_or(12345);
658 let mut replicates_f64 = Vec::with_capacity(n_boot);
659
660 for _ in 0..n_boot {
661 let mut sample = Vec::with_capacity(n_blocks_needed * block_len);
662
663 for _ in 0..n_blocks_needed {
664 let start = lcg_next(&mut rng_state) as usize % n_blocks_possible;
665 for j in 0..block_len {
666 sample.push(data_f64[start + j]);
667 }
668 }
669
670 sample.truncate(n);
672
673 replicates_f64.push(statistic(&sample));
674 }
675
676 let sorted_reps = sorted_f64_vec(&replicates_f64);
677 let alpha = 1.0 - conf;
678 let ci_lower_f64 = quantile_f64(&sorted_reps, alpha / 2.0);
679 let ci_upper_f64 = quantile_f64(&sorted_reps, 1.0 - alpha / 2.0);
680
681 let mean_rep = replicates_f64.iter().sum::<f64>() / replicates_f64.len() as f64;
682 let var_rep = replicates_f64
683 .iter()
684 .map(|&r| (r - mean_rep) * (r - mean_rep))
685 .sum::<f64>()
686 / (replicates_f64.len() as f64 - 1.0);
687 let se = var_rep.sqrt();
688
689 let replicates: Result<Vec<F>, _> = replicates_f64
690 .iter()
691 .map(|&v| F::from(v).ok_or_else(|| StatsError::ComputationError("cast".into())))
692 .collect();
693
694 Ok(BootstrapCI {
695 estimate: F::from(theta_hat).ok_or_else(|| StatsError::ComputationError("cast".into()))?,
696 ci_lower: F::from(ci_lower_f64)
697 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
698 ci_upper: F::from(ci_upper_f64)
699 .ok_or_else(|| StatsError::ComputationError("cast".into()))?,
700 confidence_level: conf,
701 standard_error: F::from(se).ok_or_else(|| StatsError::ComputationError("cast".into()))?,
702 replicates: replicates?,
703 })
704}
705
706#[cfg(test)]
711mod tests {
712 use super::*;
713 use scirs2_core::ndarray::{array, Array1};
714
715 fn sample_mean(data: &[f64]) -> f64 {
716 if data.is_empty() {
717 return 0.0;
718 }
719 data.iter().sum::<f64>() / data.len() as f64
720 }
721
722 fn sample_median(data: &[f64]) -> f64 {
723 let mut s = data.to_vec();
724 s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
725 let n = s.len();
726 if n == 0 {
727 return 0.0;
728 }
729 if n % 2 == 0 {
730 (s[n / 2 - 1] + s[n / 2]) / 2.0
731 } else {
732 s[n / 2]
733 }
734 }
735
736 #[test]
741 fn test_percentile_bootstrap_mean() {
742 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
743 let result =
744 percentile_bootstrap(&data.view(), sample_mean, Some(2000), Some(0.95), Some(42))
745 .expect("should succeed");
746 let lower: f64 = NumCast::from(result.ci_lower).expect("cast");
747 let upper: f64 = NumCast::from(result.ci_upper).expect("cast");
748 assert!(lower < 5.5);
750 assert!(upper > 5.5);
751 }
752
753 #[test]
754 fn test_percentile_bootstrap_median() {
755 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
756 let result = percentile_bootstrap(
757 &data.view(),
758 sample_median,
759 Some(2000),
760 Some(0.95),
761 Some(42),
762 )
763 .expect("should succeed");
764 let lower: f64 = NumCast::from(result.ci_lower).expect("cast");
765 let upper: f64 = NumCast::from(result.ci_upper).expect("cast");
766 assert!(lower < upper);
767 }
768
769 #[test]
770 fn test_percentile_bootstrap_se() {
771 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
772 let result =
773 percentile_bootstrap(&data.view(), sample_mean, Some(1000), Some(0.95), Some(42))
774 .expect("should succeed");
775 let se: f64 = NumCast::from(result.standard_error).expect("cast");
776 assert!(se > 0.0);
777 assert!(se < 3.0); }
779
780 #[test]
781 fn test_percentile_bootstrap_replicates_count() {
782 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
783 let n_boot = 500;
784 let result = percentile_bootstrap(&data.view(), sample_mean, Some(n_boot), None, Some(42))
785 .expect("should succeed");
786 assert_eq!(result.replicates.len(), n_boot);
787 }
788
789 #[test]
790 fn test_percentile_bootstrap_empty_error() {
791 let data = Array1::<f64>::zeros(0);
792 assert!(percentile_bootstrap(&data.view(), sample_mean, None, None, None).is_err());
793 }
794
795 #[test]
796 fn test_percentile_bootstrap_single_element() {
797 let data = array![5.0];
798 let result = percentile_bootstrap(&data.view(), sample_mean, Some(100), None, Some(42))
799 .expect("should succeed");
800 let lower: f64 = NumCast::from(result.ci_lower).expect("cast");
802 let upper: f64 = NumCast::from(result.ci_upper).expect("cast");
803 assert!((lower - 5.0).abs() < 1e-10);
804 assert!((upper - 5.0).abs() < 1e-10);
805 }
806
807 #[test]
812 fn test_bca_bootstrap_mean() {
813 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
814 let result = bca_bootstrap(&data.view(), sample_mean, Some(2000), Some(0.95), Some(42))
815 .expect("should succeed");
816 let lower: f64 = NumCast::from(result.ci.ci_lower).expect("cast");
817 let upper: f64 = NumCast::from(result.ci.ci_upper).expect("cast");
818 assert!(lower < 5.5);
819 assert!(upper > 5.5);
820 }
821
822 #[test]
823 fn test_bca_bootstrap_bias_correction() {
824 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
825 let result = bca_bootstrap(&data.view(), sample_mean, Some(2000), Some(0.95), Some(42))
826 .expect("should succeed");
827 assert!(result.bias_correction.abs() < 1.0);
829 }
830
831 #[test]
832 fn test_bca_bootstrap_acceleration() {
833 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
834 let result = bca_bootstrap(&data.view(), sample_mean, Some(1000), Some(0.95), Some(42))
835 .expect("should succeed");
836 assert!(result.acceleration.abs() < 0.5);
838 }
839
840 #[test]
841 fn test_bca_bootstrap_skewed_data() {
842 let data = array![1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 10.0];
844 let result = bca_bootstrap(&data.view(), sample_mean, Some(2000), Some(0.95), Some(42))
845 .expect("should succeed");
846 let lower: f64 = NumCast::from(result.ci.ci_lower).expect("cast");
847 let upper: f64 = NumCast::from(result.ci.ci_upper).expect("cast");
848 assert!(lower < upper);
849 }
850
851 #[test]
852 fn test_bca_bootstrap_empty_error() {
853 let data = Array1::<f64>::zeros(0);
854 assert!(bca_bootstrap(&data.view(), sample_mean, None, None, None).is_err());
855 }
856
857 #[test]
858 fn test_bca_bootstrap_single_error() {
859 let data = array![5.0];
860 assert!(bca_bootstrap(&data.view(), sample_mean, None, None, None).is_err());
861 }
862
863 #[test]
868 fn test_parametric_bootstrap_mean() {
869 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
870 let result =
871 parametric_bootstrap(&data.view(), sample_mean, Some(2000), Some(0.95), Some(42))
872 .expect("should succeed");
873 let lower: f64 = NumCast::from(result.ci_lower).expect("cast");
874 let upper: f64 = NumCast::from(result.ci_upper).expect("cast");
875 assert!(lower < 5.5);
876 assert!(upper > 5.5);
877 }
878
879 #[test]
880 fn test_parametric_bootstrap_ci_width() {
881 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
882 let ci_90 =
883 parametric_bootstrap(&data.view(), sample_mean, Some(2000), Some(0.90), Some(42))
884 .expect("90%");
885 let ci_99 =
886 parametric_bootstrap(&data.view(), sample_mean, Some(2000), Some(0.99), Some(42))
887 .expect("99%");
888
889 let width_90: f64 = ci_90.ci_upper - ci_90.ci_lower;
890 let width_99: f64 = ci_99.ci_upper - ci_99.ci_lower;
891 assert!(width_99 > width_90);
893 }
894
895 #[test]
896 fn test_parametric_bootstrap_se_positive() {
897 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
898 let result = parametric_bootstrap(&data.view(), sample_mean, Some(500), None, Some(42))
899 .expect("should succeed");
900 let se: f64 = NumCast::from(result.standard_error).expect("cast");
901 assert!(se > 0.0);
902 }
903
904 #[test]
905 fn test_parametric_bootstrap_empty_error() {
906 let data = Array1::<f64>::zeros(0);
907 assert!(parametric_bootstrap(&data.view(), sample_mean, None, None, None).is_err());
908 }
909
910 #[test]
911 fn test_parametric_bootstrap_single_error() {
912 let data = array![5.0];
913 assert!(parametric_bootstrap(&data.view(), sample_mean, None, None, None).is_err());
914 }
915
916 #[test]
921 fn test_block_bootstrap_basic() {
922 let data = array![
923 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0,
924 9.5, 10.0, 10.5
925 ];
926 let result = block_bootstrap_ci(
927 &data.view(),
928 sample_mean,
929 None,
930 Some(1000),
931 Some(0.95),
932 Some(42),
933 )
934 .expect("should succeed");
935 let lower: f64 = NumCast::from(result.ci_lower).expect("cast");
936 let upper: f64 = NumCast::from(result.ci_upper).expect("cast");
937 assert!(lower < upper);
938 }
939
940 #[test]
941 fn test_block_bootstrap_custom_block_length() {
942 let data =
943 array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
944 let result = block_bootstrap_ci(
945 &data.view(),
946 sample_mean,
947 Some(3),
948 Some(500),
949 Some(0.95),
950 Some(42),
951 )
952 .expect("should succeed");
953 let lower: f64 = NumCast::from(result.ci_lower).expect("cast");
954 let upper: f64 = NumCast::from(result.ci_upper).expect("cast");
955 assert!(lower < upper);
956 }
957
958 #[test]
959 fn test_block_bootstrap_block_length_1() {
960 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
962 let result = block_bootstrap_ci(
963 &data.view(),
964 sample_mean,
965 Some(1),
966 Some(500),
967 Some(0.95),
968 Some(42),
969 )
970 .expect("should succeed");
971 assert_eq!(result.replicates.len(), 500);
972 }
973
974 #[test]
975 fn test_block_bootstrap_replicates() {
976 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
977 let n_boot = 300;
978 let result = block_bootstrap_ci(
979 &data.view(),
980 sample_mean,
981 Some(2),
982 Some(n_boot),
983 None,
984 Some(42),
985 )
986 .expect("should succeed");
987 assert_eq!(result.replicates.len(), n_boot);
988 }
989
990 #[test]
991 fn test_block_bootstrap_too_small_error() {
992 let data = array![1.0, 2.0];
993 assert!(block_bootstrap_ci(&data.view(), sample_mean, None, None, None, None).is_err());
994 }
995
996 #[test]
997 fn test_block_bootstrap_invalid_block_length() {
998 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
999 assert!(block_bootstrap_ci(&data.view(), sample_mean, Some(0), None, None, None).is_err());
1000 assert!(block_bootstrap_ci(&data.view(), sample_mean, Some(6), None, None, None).is_err());
1001 }
1002
1003 #[test]
1008 fn test_norm_cdf_ppf_roundtrip() {
1009 for &p in &[0.01, 0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95, 0.99] {
1010 let z = norm_ppf(p);
1011 let p_back = norm_cdf(z);
1012 assert!(
1013 (p - p_back).abs() < 0.02,
1014 "roundtrip failed for p={}: z={}, p_back={}",
1015 p,
1016 z,
1017 p_back
1018 );
1019 }
1020 }
1021
1022 #[test]
1023 fn test_lcg_different_seeds() {
1024 let mut s1 = 1u64;
1025 let mut s2 = 2u64;
1026 let v1 = lcg_next(&mut s1);
1027 let v2 = lcg_next(&mut s2);
1028 assert_ne!(v1, v2);
1029 }
1030}