pub fn wilson_score(positive: u32, total: u32, confidence: f64) -> f64 {
if total == 0 {
return 0.5; }
let n = total as f64;
let p = positive as f64 / n;
let z = z_score(confidence);
let z2 = z * z;
let denominator = 1.0 + z2 / n;
let center = p + z2 / (2.0 * n);
let spread = z * ((p * (1.0 - p) + z2 / (4.0 * n)) / n).sqrt();
let lower_bound = (center - spread) / denominator;
lower_bound.clamp(0.0, 1.0)
}
pub fn wilson_score_default(positive: u32, total: u32) -> f64 {
wilson_score(positive, total, 0.95)
}
fn z_score(confidence: f64) -> f64 {
match confidence {
c if c >= 0.99 => 2.576,
c if c >= 0.975 => 2.241,
c if c >= 0.95 => 1.96,
c if c >= 0.90 => 1.645,
c if c >= 0.85 => 1.44,
c if c >= 0.80 => 1.282,
_ => 1.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_data() {
assert_eq!(wilson_score(0, 0, 0.95), 0.5);
}
#[test]
fn test_perfect_score() {
let score = wilson_score(10, 10, 0.95);
assert!(score < 1.0);
assert!(score > 0.7);
}
#[test]
fn test_all_negative() {
let score = wilson_score(0, 10, 0.95);
assert!(score >= 0.0);
assert!(score < 0.1);
}
#[test]
fn test_low_sample_uncertainty() {
let low_sample = wilson_score(1, 1, 0.95); let high_sample = wilson_score(100, 100, 0.95);
assert!(high_sample > low_sample);
}
#[test]
fn test_same_ratio_different_samples() {
let small = wilson_score(5, 10, 0.95); let large = wilson_score(50, 100, 0.95);
assert!(large > small);
}
#[test]
fn test_default_confidence() {
let with_95 = wilson_score(5, 10, 0.95);
let default = wilson_score_default(5, 10);
assert!((with_95 - default).abs() < 0.001);
}
#[test]
fn test_higher_confidence_lower_bound() {
let conf_90 = wilson_score(5, 10, 0.90);
let conf_95 = wilson_score(5, 10, 0.95);
let conf_99 = wilson_score(5, 10, 0.99);
assert!(conf_90 > conf_95);
assert!(conf_95 > conf_99);
}
}