From f16cc5090188bf56795166cea4444c2cb2d3d61e Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 26 Oct 2018 15:06:17 +0200 Subject: [PATCH] Added a TrueSkill struct that holds default config. --- src/lib.rs | 475 +++++++++++++++++++++++++++++------------------------ 1 file changed, 260 insertions(+), 215 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 46f2e6d..9df79a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,20 +25,29 @@ pub const MU: f64 = 25.0; pub const SIGMA: f64 = MU / 3.0; /// Default distance that guarantees about 76% chance of winning. -const BETA: f64 = SIGMA / 2.0; +pub const BETA: f64 = SIGMA / 2.0; /// Default dynamic factor. -const TAU: f64 = SIGMA / 100.0; +pub const TAU: f64 = SIGMA / 100.0; /// Default draw probability of the game. -const DRAW_PROBABILITY: f64 = 0.10; +pub const DRAW_PROBABILITY: f64 = 0.10; /// A basis to check reliability of the result. -const DELTA: f64 = 0.0001; +pub const DELTA: f64 = 0.0001; pub trait Rateable { fn mu(&self) -> f64; fn sigma(&self) -> f64; + + fn skill(&self) -> f64 { + self.mu() - 3.0 * self.sigma() + } +} + +pub trait RateableMut { + fn mu_mut(&mut self) -> &mut f64; + fn sigma_mut(&mut self) -> &mut f64; } #[derive(Debug, PartialEq)] @@ -63,13 +72,13 @@ impl Rateable for Rating { } } -impl Rateable for Gaussian { - fn mu(&self) -> f64 { - self.mu() +impl RateableMut for Rating { + fn mu_mut(&mut self) -> &mut f64 { + &mut self.mu } - fn sigma(&self) -> f64 { - self.sigma() + fn sigma_mut(&mut self) -> &mut f64 { + &mut self.sigma } } @@ -77,236 +86,262 @@ fn draw_margin(p: f64, beta: f64, total_players: f64) -> f64 { math::icdf((p + 1.0) / 2.0) * total_players.sqrt() * beta } -pub fn rate(ratings: &[(R, u16)], ranks: &[u16], min_delta: f64) -> Vec -where - R: Rateable, -{ - // TODO Validate rating_groups is orderded in teams. - // TODO Validate that teams are orderd after rank. +pub struct TrueSkill { + mu: f64, + sigma: f64, + beta: f64, + tau: f64, + draw_probability: f64, + delta: f64, +} - let tau_sqr = TAU.powi(2); - let beta_sqr = BETA.powi(2); +impl TrueSkill { + pub fn rate(&self, ratings: &[(R, u16)], ranks: &[u16], min_delta: f64) -> Vec + where + R: Rateable, + { + // TODO Validate ratings is orderded in teams. + // TODO Validate that teams are orderd after rank. - let mut variable_arena = VariableArena::new(); + let tau_sqr = self.tau.powi(2); + let beta_sqr = self.beta.powi(2); - let rating_count = ratings.len(); - let team_count = ranks.len(); + let mut variable_arena = VariableArena::new(); - let rating_vars = (0..rating_count) - .map(|_| variable_arena.create()) - .collect::>(); - let perf_vars = ratings - .iter() - .map(|(_, team)| (variable_arena.create(), *team)) - .collect::>(); + let rating_count = ratings.len(); + let team_count = ranks.len(); - let team_perf_vars = (0..team_count) - .map(|_| variable_arena.create()) - .collect::>(); - let team_diff_vars = (0..team_count - 1) - .map(|_| variable_arena.create()) - .collect::>(); + let rating_vars = (0..rating_count) + .map(|_| variable_arena.create()) + .collect::>(); - let mut factor_id = 0; + let perf_vars = ratings + .iter() + .map(|(_, team)| (variable_arena.create(), *team)) + .collect::>(); - let rating_layer = rating_vars - .iter() - .zip(ratings.iter().map(|(rating, _)| rating)) - .map(|(rating_var, rating)| { - let gaussian = - Gaussian::from_mu_sigma(rating.mu(), (rating.sigma().powi(2) + tau_sqr).sqrt()); + let team_perf_vars = (0..team_count) + .map(|_| variable_arena.create()) + .collect::>(); - factor_id += 1; + let team_diff_vars = (0..team_count - 1) + .map(|_| variable_arena.create()) + .collect::>(); - PriorFactor::new(&mut variable_arena, factor_id, *rating_var, gaussian) - }) - .collect::>(); + let mut factor_id = 0; - let perf_layer = rating_vars - .iter() - .zip(perf_vars.iter().map(|(variable, _)| variable)) - .map(|(rating_var, perf)| { - factor_id += 1; + let rating_layer = rating_vars + .iter() + .zip(ratings.iter().map(|(rating, _)| rating)) + .map(|(rating_var, rating)| { + let gaussian = + Gaussian::from_mu_sigma(rating.mu(), (rating.sigma().powi(2) + tau_sqr).sqrt()); - LikelihoodFactor::new(&mut variable_arena, factor_id, *rating_var, *perf, beta_sqr) - }) - .collect::>(); + factor_id += 1; - let team_perf_layer = team_perf_vars - .iter() - .enumerate() - .map(|(i, variable)| { - factor_id += 1; + PriorFactor::new(&mut variable_arena, factor_id, *rating_var, gaussian) + }) + .collect::>(); - let team = perf_vars - .iter() - .filter(|(_, team)| *team as usize == i) - .map(|(variable, _)| *variable) - .collect::>(); + let perf_layer = rating_vars + .iter() + .zip(perf_vars.iter().map(|(variable, _)| variable)) + .map(|(rating_var, perf)| { + factor_id += 1; - let team_count = team.len(); + LikelihoodFactor::new(&mut variable_arena, factor_id, *rating_var, *perf, beta_sqr) + }) + .collect::>(); - SumFactor::new( - &mut variable_arena, - factor_id, - *variable, - team, - vec![1.0; team_count], - ) - }) - .collect::>(); + let team_perf_layer = team_perf_vars + .iter() + .enumerate() + .map(|(i, variable)| { + factor_id += 1; - let team_diff_layer = team_diff_vars - .iter() - .zip(team_perf_vars.windows(2)) - .map(|(variable, teams)| { - factor_id += 1; + let team = perf_vars + .iter() + .filter(|(_, team)| *team as usize == i) + .map(|(variable, _)| *variable) + .collect::>(); - SumFactor::new( - &mut variable_arena, - factor_id, - *variable, - teams.to_vec(), - vec![1.0, -1.0], - ) - }) - .collect::>(); + let team_count = team.len(); - let trunc_layer = team_diff_vars - .iter() - .enumerate() - .map(|(i, variable)| { - factor_id += 1; + SumFactor::new( + &mut variable_arena, + factor_id, + *variable, + team, + vec![1.0; team_count], + ) + }) + .collect::>(); - let player_count = perf_vars - .iter() - .filter(|(_, team)| *team as usize == i || *team as usize == i + 1) - .count(); + let team_diff_layer = team_diff_vars + .iter() + .zip(team_perf_vars.windows(2)) + .map(|(variable, teams)| { + factor_id += 1; - TruncateFactor::new( - &mut variable_arena, - factor_id, - *variable, - draw_margin(DRAW_PROBABILITY, BETA, player_count as f64), - ranks[i] == ranks[i + 1], - ) - }) - .collect::>();; + SumFactor::new( + &mut variable_arena, + factor_id, + *variable, + teams.to_vec(), + vec![1.0, -1.0], + ) + }) + .collect::>(); - for factor in &rating_layer { - factor.start(&mut variable_arena); - } + let trunc_layer = team_diff_vars + .iter() + .enumerate() + .map(|(i, variable)| { + factor_id += 1; - for factor in &perf_layer { - factor.update_value(&mut variable_arena); - } + let player_count = perf_vars + .iter() + .filter(|(_, team)| *team as usize == i || *team as usize == i + 1) + .count(); - for factor in &team_perf_layer { - factor.update_sum(&mut variable_arena); - } + TruncateFactor::new( + &mut variable_arena, + factor_id, + *variable, + draw_margin(self.draw_probability, self.beta, player_count as f64), + ranks[i] == ranks[i + 1], + ) + }) + .collect::>();; - for _ in 0..10 { - let mut delta = 0.0; + for factor in &rating_layer { + factor.start(&mut variable_arena); + } - for factor in &team_diff_layer { + for factor in &perf_layer { + factor.update_value(&mut variable_arena); + } + + for factor in &team_perf_layer { factor.update_sum(&mut variable_arena); } - for factor in &trunc_layer { - let d = factor.update(&mut variable_arena); + for _ in 0..10 { + let mut delta = 0.0; - if d > delta { - delta = d; + for factor in &team_diff_layer { + factor.update_sum(&mut variable_arena); + } + + for factor in &trunc_layer { + let d = factor.update(&mut variable_arena); + + if d > delta { + delta = d; + } + } + + for factor in &team_diff_layer { + factor.update_term(&mut variable_arena, 0); + factor.update_term(&mut variable_arena, 1); + } + + if delta < min_delta { + break; } } - for factor in &team_diff_layer { - factor.update_term(&mut variable_arena, 0); - factor.update_term(&mut variable_arena, 1); + for factor in &team_perf_layer { + factor.update_all_terms(&mut variable_arena); } - if delta < min_delta { - break; + for factor in &perf_layer { + factor.update_mean(&mut variable_arena); } + + rating_vars + .iter() + .map(|variable| variable_arena.get(*variable).unwrap().get_value()) + .map(|value| Rating { + mu: value.mu(), + sigma: value.sigma(), + }) + .collect::>() } - for factor in &team_perf_layer { - factor.update_all_terms(&mut variable_arena); - } + pub fn quality(&self, rating_groups: &[&[R]]) -> f64 + where + R: Rateable, + { + let flatten_ratings = rating_groups + .iter() + .flat_map(|group| group.iter()) + .collect::>(); - for factor in &perf_layer { - factor.update_mean(&mut variable_arena); - } + let flatten_weights = vec![1.0; flatten_ratings.len()].into_boxed_slice(); - rating_vars - .iter() - .map(|variable| variable_arena.get(*variable).unwrap().get_value()) - .map(|value| Rating { - mu: value.mu(), - sigma: value.sigma(), - }) - .collect::>() + let length = flatten_ratings.len(); + + let mut mean_matrix = Matrix::new(length, 1); + + for (i, rating) in flatten_ratings.iter().enumerate() { + mean_matrix[(i, 0)] = rating.mu(); + } + + let mut variance_matrix = Matrix::new(length, length); + + for (i, rating) in flatten_ratings.iter().enumerate() { + variance_matrix[(i, i)] = rating.sigma().powi(2); + } + + let mut rotated_a_matrix = Matrix::new(rating_groups.len() - 1, length); + + let mut t = 0; + let mut x = 0; + + for (row, group) in rating_groups.windows(2).enumerate() { + let current = group[0]; + let next = group[1]; + + for n in t..t + current.len() { + rotated_a_matrix[(row, n)] = flatten_weights[n]; + t += 1; + x += 1; + } + + for n in x..x + next.len() { + rotated_a_matrix[(row, n)] = -flatten_weights[n]; + x += 1; + } + } + + let a_matrix = rotated_a_matrix.transpose(); + + let _ata = self.beta.powi(2) * &rotated_a_matrix * &a_matrix; + let _atsa = &rotated_a_matrix * &variance_matrix * &a_matrix; + + let start = mean_matrix.transpose() * &a_matrix; + let middle = &_ata + &_atsa; + let end = &rotated_a_matrix * &mean_matrix; + + let e_arg = (-0.5 * &start * &middle.inverse() * &end).determinant(); + let s_arg = _ata.determinant() / middle.determinant(); + + e_arg.exp() * s_arg.sqrt() + } } -pub fn quality(rating_groups: &[&[R]]) -> f64 -where - R: Rateable, -{ - let flatten_ratings = rating_groups - .iter() - .flat_map(|group| group.iter()) - .collect::>(); - - let flatten_weights = vec![1.0; flatten_ratings.len()].into_boxed_slice(); - - let length = flatten_ratings.len(); - - let mut mean_matrix = Matrix::new(length, 1); - - for (i, rating) in flatten_ratings.iter().enumerate() { - mean_matrix[(i, 0)] = rating.mu(); - } - - let mut variance_matrix = Matrix::new(length, length); - - for (i, rating) in flatten_ratings.iter().enumerate() { - variance_matrix[(i, i)] = rating.sigma().powi(2); - } - - let mut rotated_a_matrix = Matrix::new(rating_groups.len() - 1, length); - - let mut t = 0; - let mut x = 0; - - for (row, group) in rating_groups.windows(2).enumerate() { - let current = group[0]; - let next = group[1]; - - for n in t..t + current.len() { - rotated_a_matrix[(row, n)] = flatten_weights[n]; - t += 1; - x += 1; - } - - for n in x..x + next.len() { - rotated_a_matrix[(row, n)] = -flatten_weights[n]; - x += 1; +impl Default for TrueSkill { + fn default() -> TrueSkill { + TrueSkill { + mu: MU, + sigma: SIGMA, + beta: BETA, + tau: TAU, + draw_probability: DRAW_PROBABILITY, + delta: DELTA, } } - - let a_matrix = rotated_a_matrix.transpose(); - - let _ata = BETA.powi(2) * &rotated_a_matrix * &a_matrix; - let _atsa = &rotated_a_matrix * &variance_matrix * &a_matrix; - - let start = mean_matrix.transpose() * &a_matrix; - let middle = &_ata + &_atsa; - let end = &rotated_a_matrix * &mean_matrix; - - let e_arg = (-0.5 * &start * &middle.inverse() * &end).determinant(); - let s_arg = _ata.determinant() / middle.determinant(); - - e_arg.exp() * s_arg.sqrt() } #[cfg(test)] @@ -360,11 +395,13 @@ mod tests { #[test] fn test_quality_1vs1() { + let ts = TrueSkill::default(); + let alice = Rating::new(MU, SIGMA); let bob = Rating::new(MU, SIGMA); assert_relative_eq!( - quality(&[&[alice], &[bob]]), + ts.quality(&[&[alice], &[bob]]), 0.4472135954999579, epsilon = EPSILON ); @@ -372,10 +409,9 @@ mod tests { #[test] fn test_rate_4_free_for_all() { - let alice = Rating::new(MU, SIGMA); - let bob = Rating::new(MU, SIGMA); - let chris = Rating::new(MU, SIGMA); - let darren = Rating::new(MU, SIGMA); + let ts = TrueSkill::default(); + + let (ratings, ranks) = generate_free_for_all(4); let expected_ratings = vec![ Rating::new(33.20668089876779, 6.34810941351329), @@ -384,11 +420,7 @@ mod tests { Rating::new(16.79331910018712, 6.34810938603116), ]; - let ratings = rate( - &[(alice, 0), (bob, 1), (chris, 2), (darren, 3)], - &[0, 1, 2, 3], - DELTA, - ); + let ratings = ts.rate(ratings.as_ref(), ranks.as_ref(), DELTA); for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) { assert_relative_eq!(rating, expected, epsilon = EPSILON); @@ -398,6 +430,7 @@ mod tests { #[test] fn test_rate_8_free_for_all() { // Example from http://research.microsoft.com/en-us/projects/trueskill/details.aspx + let ts = TrueSkill::default(); let (ratings, ranks) = generate_free_for_all(8); @@ -412,7 +445,7 @@ mod tests { Rating::new(13.22891063797913, 5.74928289201801), ]; - let ratings = rate(ratings.as_ref(), ranks.as_ref(), DELTA); + let ratings = ts.rate(ratings.as_ref(), ranks.as_ref(), DELTA); for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) { assert_relative_eq!(rating, expected, epsilon = EPSILON); @@ -421,6 +454,8 @@ mod tests { #[test] fn test_rate_8_free_for_all_draw() { + let ts = TrueSkill::default(); + let (ratings, ranks) = generate_free_for_all_draw(8); let expected_ratings = vec![ @@ -434,7 +469,7 @@ mod tests { Rating::new(25.00000000000000, 4.59217372356178), ]; - let ratings = rate(ratings.as_ref(), ranks.as_ref(), DELTA); + let ratings = ts.rate(ratings.as_ref(), ranks.as_ref(), DELTA); for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) { assert_relative_eq!(rating, expected, epsilon = EPSILON); @@ -443,6 +478,8 @@ mod tests { #[test] fn test_rate_16_free_for_all() { + let ts = TrueSkill::default(); + let (ratings, ranks) = generate_free_for_all(16); let expected_ratings = vec![ @@ -464,7 +501,7 @@ mod tests { Rating::new(9.461957485668828, 5.27776869816230), ]; - let ratings = rate(ratings.as_ref(), ranks.as_ref(), DELTA); + let ratings = ts.rate(ratings.as_ref(), ranks.as_ref(), DELTA); for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) { assert_relative_eq!(rating, expected, epsilon = EPSILON); @@ -473,6 +510,8 @@ mod tests { #[test] fn test_rate_1vs1_draw() { + let ts = TrueSkill::default(); + let alice = Rating::new(MU, SIGMA); let bob = Rating::new(MU, SIGMA); @@ -481,7 +520,7 @@ mod tests { Rating::new(25.00000000000000, 6.45751568324505), ]; - let ratings = rate(&[(alice, 0), (bob, 1)], &[0, 0], DELTA); + let ratings = ts.rate(&[(alice, 0), (bob, 1)], &[0, 0], DELTA); for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) { assert_relative_eq!(rating, expected, epsilon = EPSILON); @@ -490,6 +529,8 @@ mod tests { #[test] fn test_rate_2vs2() { + let ts = TrueSkill::default(); + let alice = Rating::new(MU, SIGMA); let bob = Rating::new(MU, SIGMA); let chris = Rating::new(MU, SIGMA); @@ -502,7 +543,7 @@ mod tests { Rating::new(21.89167760093095, 7.77436345109384), ]; - let ratings = rate( + let ratings = ts.rate( &[(alice, 0), (bob, 0), (chris, 1), (darren, 1)], &[0, 1], DELTA, @@ -515,6 +556,8 @@ mod tests { #[test] fn test_rate_4vs4() { + let ts = TrueSkill::default(); + let alice = Rating::new(MU, SIGMA); let bob = Rating::new(MU, SIGMA); let chris = Rating::new(MU, SIGMA); @@ -535,7 +578,7 @@ mod tests { Rating::new(22.80208415350423, 8.05891171184399), ]; - let ratings = rate( + let ratings = ts.rate( &[ (alice, 0), (bob, 0), @@ -557,6 +600,8 @@ mod tests { #[test] fn test_rate_sublee_trueskill_issue_3_case_1() { + let ts = TrueSkill::default(); + let ratings = vec![ (Rating::new(42.234, 3.728), 0), (Rating::new(43.290, 3.842), 0), @@ -595,7 +640,7 @@ mod tests { Rating::new(16.54109749124066, 0.50668947816812), ]; - let ratings = rate(ratings.as_ref(), &[0, 1], DELTA); + let ratings = ts.rate(ratings.as_ref(), &[0, 1], DELTA); for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) { assert_relative_eq!(rating, expected, epsilon = EPSILON); @@ -604,7 +649,7 @@ mod tests { #[test] fn test_rate_sublee_trueskill_issue_3_case_2() { - let _ = env_logger::try_init(); + let ts = TrueSkill::default(); let ratings = vec![ (Rating::new(25.000, 0.500), 0), @@ -644,7 +689,7 @@ mod tests { Rating::new(43.29000000000000, 3.84290364756188), ]; - let ratings = rate(ratings.as_ref(), &[0, 1], DELTA); + let ratings = ts.rate(ratings.as_ref(), &[0, 1], DELTA); for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) { assert_relative_eq!(rating, expected, epsilon = EPSILON);