use super::{StatsError, StatsResult, mean, validate_values};
use crate::SeededSampler;
use crate::exact_quantile;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BootstrapControl {
pub seed: u64,
pub resamples: usize,
pub confidence_level: f64,
pub max_work: u64,
}
impl BootstrapControl {
pub fn new(
seed: u64,
resamples: usize,
confidence_level: f64,
max_work: u64,
) -> StatsResult<Self> {
let control = Self {
seed,
resamples,
confidence_level,
max_work,
};
control.validate()?;
Ok(control)
}
fn validate(self) -> StatsResult<()> {
if self.resamples < 2 {
return Err(StatsError::InvalidControl {
field: "resamples",
reason: "must be at least two",
});
}
if !self.confidence_level.is_finite() || !(0.0..1.0).contains(&self.confidence_level) {
return Err(StatsError::InvalidControl {
field: "confidence_level",
reason: "must be finite and strictly between zero and one",
});
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BootstrapEffectInterval {
pub point_effect: f64,
pub lower: f64,
pub upper: f64,
pub confidence_level: f64,
pub seed: u64,
pub resamples: usize,
pub baseline_samples: usize,
pub candidate_samples: usize,
pub exclusions: usize,
pub cluster_count: usize,
pub admitted_work: u64,
}
pub fn median_absolute_deviation(values: &[f64]) -> StatsResult<f64> {
validate_values("median_absolute_deviation", values)?;
let median = exact_quantile(values, 0.5).map_err(|_| StatsError::InvalidControl {
field: "quantile",
reason: "internal median quantile must remain valid",
})?;
let deviations = values
.iter()
.map(|value| (value - median).abs())
.collect::<Vec<_>>();
exact_quantile(&deviations, 0.5).map_err(|_| StatsError::InvalidControl {
field: "quantile",
reason: "internal deviation quantile must remain valid",
})
}
pub fn bootstrap_mean_difference_interval(
baseline: &[f64],
candidate: &[f64],
control: BootstrapControl,
) -> StatsResult<BootstrapEffectInterval> {
validate_values("bootstrap baseline", baseline)?;
validate_values("bootstrap candidate", candidate)?;
control.validate()?;
let observations = baseline
.len()
.checked_add(candidate.len())
.and_then(|count| u64::try_from(count).ok())
.ok_or(StatsError::WorkLimitExceeded {
required: u64::MAX,
limit: control.max_work,
})?;
let required = observations.checked_mul(control.resamples as u64).ok_or(
StatsError::WorkLimitExceeded {
required: u64::MAX,
limit: control.max_work,
},
)?;
if required > control.max_work {
return Err(StatsError::WorkLimitExceeded {
required,
limit: control.max_work,
});
}
let mut rng = SeededSampler::new(control.seed);
let mut effects = Vec::with_capacity(control.resamples);
for _ in 0..control.resamples {
let baseline_mean = resampled_mean(baseline, &mut rng);
let candidate_mean = resampled_mean(candidate, &mut rng);
effects.push(candidate_mean - baseline_mean);
}
let tail = (1.0 - control.confidence_level) / 2.0;
let lower = exact_quantile(&effects, tail).expect("validated non-empty bootstrap quantile");
let upper =
exact_quantile(&effects, 1.0 - tail).expect("validated non-empty bootstrap quantile");
Ok(BootstrapEffectInterval {
point_effect: mean(candidate)? - mean(baseline)?,
lower,
upper,
confidence_level: control.confidence_level,
seed: control.seed,
resamples: control.resamples,
baseline_samples: baseline.len(),
candidate_samples: candidate.len(),
exclusions: 0,
cluster_count: 0,
admitted_work: required,
})
}
fn resampled_mean(values: &[f64], rng: &mut SeededSampler) -> f64 {
let sum = (0..values.len())
.map(|_| values[rng.index_multiply_high(values.len())])
.sum::<f64>();
sum / values.len() as f64
}