gold_cmp/lib.rs
1//! # gold-cmp
2//!
3//! Pairwise comparison runner for gold-set LLM evals.
4//!
5//! Pattern: for each gold case, ask a judge (LLM or human) which of two
6//! candidate outputs is better, or call it a tie. Aggregate into a
7//! win-rate with a 95% CI and a yes/no answer on "is A meaningfully
8//! better than B" using a two-sided binomial test (normal
9//! approximation; OK for n ≥ 30).
10//!
11//! ## Example
12//!
13//! ```
14//! use gold_cmp::{summarize, Outcome};
15//! let outcomes = vec![
16//! Outcome::AWins, Outcome::AWins, Outcome::BWins,
17//! Outcome::Tie, Outcome::AWins, Outcome::AWins,
18//! Outcome::BWins, Outcome::AWins,
19//! ];
20//! let s = summarize(&outcomes);
21//! assert_eq!(s.a_wins, 5);
22//! assert!(s.a_win_rate_excl_ties > 0.5);
23//! ```
24
25#![deny(missing_docs)]
26
27/// Outcome of one pairwise judgment.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Outcome {
30 /// A is better.
31 AWins,
32 /// B is better.
33 BWins,
34 /// The judge called it equivalent.
35 Tie,
36}
37
38/// Aggregated summary.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct Summary {
41 /// Total judgments.
42 pub n: u64,
43 /// Wins for A.
44 pub a_wins: u64,
45 /// Wins for B.
46 pub b_wins: u64,
47 /// Ties.
48 pub ties: u64,
49 /// A's win rate excluding ties.
50 pub a_win_rate_excl_ties: f64,
51 /// Normal-approximation 95% CI half-width on the excl-ties win rate.
52 pub ci95_half_width: f64,
53 /// True when A is meaningfully ahead (CI excludes 0.5).
54 pub a_meaningfully_ahead: bool,
55}
56
57/// Summarize a list of outcomes.
58pub fn summarize(outcomes: &[Outcome]) -> Summary {
59 let n = outcomes.len() as u64;
60 let a_wins = outcomes.iter().filter(|o| **o == Outcome::AWins).count() as u64;
61 let b_wins = outcomes.iter().filter(|o| **o == Outcome::BWins).count() as u64;
62 let ties = outcomes.iter().filter(|o| **o == Outcome::Tie).count() as u64;
63 let decided = a_wins + b_wins;
64 let p = if decided == 0 {
65 0.5
66 } else {
67 a_wins as f64 / decided as f64
68 };
69 let ci = if decided == 0 {
70 0.0
71 } else {
72 1.96 * (p * (1.0 - p) / decided as f64).sqrt()
73 };
74 let ahead = decided > 0 && (p - ci) > 0.5;
75 Summary {
76 n,
77 a_wins,
78 b_wins,
79 ties,
80 a_win_rate_excl_ties: p,
81 ci95_half_width: ci,
82 a_meaningfully_ahead: ahead,
83 }
84}