sim_lib_numbers_stats/
robust.rs1use super::{StatsError, StatsResult, mean, validate_values};
4use crate::SeededSampler;
5use crate::exact_quantile;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct BootstrapControl {
13 pub seed: u64,
15 pub resamples: usize,
17 pub confidence_level: f64,
19 pub max_work: u64,
21}
22
23impl BootstrapControl {
24 pub fn new(
26 seed: u64,
27 resamples: usize,
28 confidence_level: f64,
29 max_work: u64,
30 ) -> StatsResult<Self> {
31 let control = Self {
32 seed,
33 resamples,
34 confidence_level,
35 max_work,
36 };
37 control.validate()?;
38 Ok(control)
39 }
40
41 fn validate(self) -> StatsResult<()> {
42 if self.resamples < 2 {
43 return Err(StatsError::InvalidControl {
44 field: "resamples",
45 reason: "must be at least two",
46 });
47 }
48 if !self.confidence_level.is_finite() || !(0.0..1.0).contains(&self.confidence_level) {
49 return Err(StatsError::InvalidControl {
50 field: "confidence_level",
51 reason: "must be finite and strictly between zero and one",
52 });
53 }
54 Ok(())
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct BootstrapEffectInterval {
61 pub point_effect: f64,
63 pub lower: f64,
65 pub upper: f64,
67 pub confidence_level: f64,
69 pub seed: u64,
71 pub resamples: usize,
73 pub baseline_samples: usize,
75 pub candidate_samples: usize,
77 pub exclusions: usize,
79 pub cluster_count: usize,
81 pub admitted_work: u64,
83}
84
85pub fn median_absolute_deviation(values: &[f64]) -> StatsResult<f64> {
90 validate_values("median_absolute_deviation", values)?;
91 let median = exact_quantile(values, 0.5).map_err(|_| StatsError::InvalidControl {
92 field: "quantile",
93 reason: "internal median quantile must remain valid",
94 })?;
95 let deviations = values
96 .iter()
97 .map(|value| (value - median).abs())
98 .collect::<Vec<_>>();
99 exact_quantile(&deviations, 0.5).map_err(|_| StatsError::InvalidControl {
100 field: "quantile",
101 reason: "internal deviation quantile must remain valid",
102 })
103}
104
105pub fn bootstrap_mean_difference_interval(
112 baseline: &[f64],
113 candidate: &[f64],
114 control: BootstrapControl,
115) -> StatsResult<BootstrapEffectInterval> {
116 validate_values("bootstrap baseline", baseline)?;
117 validate_values("bootstrap candidate", candidate)?;
118 control.validate()?;
119
120 let observations = baseline
121 .len()
122 .checked_add(candidate.len())
123 .and_then(|count| u64::try_from(count).ok())
124 .ok_or(StatsError::WorkLimitExceeded {
125 required: u64::MAX,
126 limit: control.max_work,
127 })?;
128 let required = observations.checked_mul(control.resamples as u64).ok_or(
129 StatsError::WorkLimitExceeded {
130 required: u64::MAX,
131 limit: control.max_work,
132 },
133 )?;
134 if required > control.max_work {
135 return Err(StatsError::WorkLimitExceeded {
136 required,
137 limit: control.max_work,
138 });
139 }
140
141 let mut rng = SeededSampler::new(control.seed);
142 let mut effects = Vec::with_capacity(control.resamples);
143 for _ in 0..control.resamples {
144 let baseline_mean = resampled_mean(baseline, &mut rng);
145 let candidate_mean = resampled_mean(candidate, &mut rng);
146 effects.push(candidate_mean - baseline_mean);
147 }
148 let tail = (1.0 - control.confidence_level) / 2.0;
149 let lower = exact_quantile(&effects, tail).expect("validated non-empty bootstrap quantile");
150 let upper =
151 exact_quantile(&effects, 1.0 - tail).expect("validated non-empty bootstrap quantile");
152
153 Ok(BootstrapEffectInterval {
154 point_effect: mean(candidate)? - mean(baseline)?,
155 lower,
156 upper,
157 confidence_level: control.confidence_level,
158 seed: control.seed,
159 resamples: control.resamples,
160 baseline_samples: baseline.len(),
161 candidate_samples: candidate.len(),
162 exclusions: 0,
163 cluster_count: 0,
164 admitted_work: required,
165 })
166}
167
168fn resampled_mean(values: &[f64], rng: &mut SeededSampler) -> f64 {
169 let sum = (0..values.len())
170 .map(|_| values[rng.index_multiply_high(values.len())])
171 .sum::<f64>();
172 sum / values.len() as f64
173}