use std::future::Future;
use super::baseline::{Baseline, Regression};
use super::scorer::Scorecard;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VarianceReport {
pub k: usize,
pub mean: f64,
pub median: f64,
pub min: f64,
pub max: f64,
pub stddev: f64,
}
impl VarianceReport {
pub fn from_scores(scores: &[f64]) -> Self {
if scores.is_empty() {
return Self {
k: 0,
mean: 0.0,
median: 0.0,
min: 0.0,
max: 0.0,
stddev: 0.0,
};
}
let k = scores.len();
let mean = scores.iter().sum::<f64>() / k as f64;
let min = scores.iter().copied().fold(f64::INFINITY, f64::min);
let max = scores.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let variance = scores.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / k as f64;
let mut sorted = scores.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = if k % 2 == 1 {
sorted[k / 2]
} else {
(sorted[k / 2 - 1] + sorted[k / 2]) / 2.0
};
Self {
k,
mean,
median,
min,
max,
stddev: variance.sqrt(),
}
}
pub fn failure_rate(scores: &[f64], threshold: f64) -> f64 {
if scores.is_empty() {
return 0.0;
}
let bad = scores.iter().filter(|&&s| s <= threshold).count();
bad as f64 / scores.len() as f64
}
pub fn render(&self) -> String {
format!(
"{} runs: mean={:.3} median={:.3} min={:.3} max={:.3} sd={:.3}",
self.k, self.mean, self.median, self.min, self.max, self.stddev
)
}
}
pub async fn repeat_k<F, Fut>(k: usize, mut op: F) -> (Vec<Scorecard>, VarianceReport)
where
F: FnMut(usize) -> Fut,
Fut: Future<Output = Scorecard>,
{
let mut cards = Vec::with_capacity(k);
for i in 0..k {
cards.push(op(i).await);
}
let scores: Vec<f64> = cards.iter().map(|c| c.overall()).collect();
let report = VarianceReport::from_scores(&scores);
(cards, report)
}
#[derive(Debug, Clone)]
pub struct LiveEvalOutcome {
pub variance: VarianceReport,
pub current: Baseline,
pub regressions: Vec<Regression>,
}
impl LiveEvalOutcome {
pub fn new(
label: impl Into<String>,
cards: &[Scorecard],
prior: Option<&Baseline>,
tolerance: f64,
) -> Self {
let scores: Vec<f64> = cards.iter().map(|c| c.overall()).collect();
let variance = VarianceReport::from_scores(&scores);
let current = Baseline::from_scorecards_mean(label, cards);
let regressions = prior
.map(|p| p.regressions(¤t, tolerance))
.unwrap_or_default();
Self {
variance,
current,
regressions,
}
}
pub fn holds(&self) -> bool {
self.regressions.is_empty()
}
}