sim_lib_numbers_stats/
robust.rs1use super::{StatsError, StatsResult, mean, validate_values};
4use crate::exact_quantile;
5
6#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct BootstrapControl {
12 pub seed: u64,
14 pub resamples: usize,
16 pub confidence_level: f64,
18 pub max_work: u64,
20}
21
22impl BootstrapControl {
23 pub fn new(
25 seed: u64,
26 resamples: usize,
27 confidence_level: f64,
28 max_work: u64,
29 ) -> StatsResult<Self> {
30 let control = Self {
31 seed,
32 resamples,
33 confidence_level,
34 max_work,
35 };
36 control.validate()?;
37 Ok(control)
38 }
39
40 fn validate(self) -> StatsResult<()> {
41 if self.resamples < 2 {
42 return Err(StatsError::InvalidControl {
43 field: "resamples",
44 reason: "must be at least two",
45 });
46 }
47 if !self.confidence_level.is_finite() || !(0.0..1.0).contains(&self.confidence_level) {
48 return Err(StatsError::InvalidControl {
49 field: "confidence_level",
50 reason: "must be finite and strictly between zero and one",
51 });
52 }
53 Ok(())
54 }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct BootstrapEffectInterval {
60 pub point_effect: f64,
62 pub lower: f64,
64 pub upper: f64,
66 pub confidence_level: f64,
68 pub seed: u64,
70 pub resamples: usize,
72 pub baseline_samples: usize,
74 pub candidate_samples: usize,
76}
77
78pub fn median_absolute_deviation(values: &[f64]) -> StatsResult<f64> {
83 validate_values("median_absolute_deviation", values)?;
84 let median = exact_quantile(values, 0.5).map_err(|_| StatsError::InvalidControl {
85 field: "quantile",
86 reason: "internal median quantile must remain valid",
87 })?;
88 let deviations = values
89 .iter()
90 .map(|value| (value - median).abs())
91 .collect::<Vec<_>>();
92 exact_quantile(&deviations, 0.5).map_err(|_| StatsError::InvalidControl {
93 field: "quantile",
94 reason: "internal deviation quantile must remain valid",
95 })
96}
97
98pub fn bootstrap_mean_difference_interval(
105 baseline: &[f64],
106 candidate: &[f64],
107 control: BootstrapControl,
108) -> StatsResult<BootstrapEffectInterval> {
109 validate_values("bootstrap baseline", baseline)?;
110 validate_values("bootstrap candidate", candidate)?;
111 control.validate()?;
112
113 let observations = baseline
114 .len()
115 .checked_add(candidate.len())
116 .and_then(|count| u64::try_from(count).ok())
117 .ok_or(StatsError::WorkLimitExceeded {
118 required: u64::MAX,
119 limit: control.max_work,
120 })?;
121 let required = observations.checked_mul(control.resamples as u64).ok_or(
122 StatsError::WorkLimitExceeded {
123 required: u64::MAX,
124 limit: control.max_work,
125 },
126 )?;
127 if required > control.max_work {
128 return Err(StatsError::WorkLimitExceeded {
129 required,
130 limit: control.max_work,
131 });
132 }
133
134 let mut rng = SplitMix64(control.seed);
135 let mut effects = Vec::with_capacity(control.resamples);
136 for _ in 0..control.resamples {
137 let baseline_mean = resampled_mean(baseline, &mut rng);
138 let candidate_mean = resampled_mean(candidate, &mut rng);
139 effects.push(candidate_mean - baseline_mean);
140 }
141 let tail = (1.0 - control.confidence_level) / 2.0;
142 let lower = exact_quantile(&effects, tail).expect("validated non-empty bootstrap quantile");
143 let upper =
144 exact_quantile(&effects, 1.0 - tail).expect("validated non-empty bootstrap quantile");
145
146 Ok(BootstrapEffectInterval {
147 point_effect: mean(candidate)? - mean(baseline)?,
148 lower,
149 upper,
150 confidence_level: control.confidence_level,
151 seed: control.seed,
152 resamples: control.resamples,
153 baseline_samples: baseline.len(),
154 candidate_samples: candidate.len(),
155 })
156}
157
158fn resampled_mean(values: &[f64], rng: &mut SplitMix64) -> f64 {
159 let sum = (0..values.len())
160 .map(|_| values[rng.index(values.len())])
161 .sum::<f64>();
162 sum / values.len() as f64
163}
164
165#[derive(Clone, Copy, Debug)]
166struct SplitMix64(u64);
167
168impl SplitMix64 {
169 fn next(&mut self) -> u64 {
170 self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
171 let mut value = self.0;
172 value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
173 value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
174 value ^ (value >> 31)
175 }
176
177 fn index(&mut self, len: usize) -> usize {
178 ((u128::from(self.next()) * len as u128) >> 64) as usize
179 }
180}