422 lines
11 KiB
Rust
422 lines
11 KiB
Rust
#[macro_use]
|
|
extern crate log;
|
|
extern crate statrs;
|
|
|
|
#[cfg(test)]
|
|
#[macro_use]
|
|
extern crate approx;
|
|
|
|
#[cfg(test)]
|
|
extern crate env_logger;
|
|
|
|
mod factor_graph;
|
|
mod gaussian;
|
|
mod math;
|
|
mod matrix;
|
|
|
|
use factor_graph::*;
|
|
use gaussian::Gaussian;
|
|
use matrix::Matrix;
|
|
|
|
/// Default initial mean of ratings.
|
|
pub const MU: f64 = 25.0;
|
|
|
|
/// Default initial standard deviation of ratings.
|
|
pub const SIGMA: f64 = MU / 3.0;
|
|
|
|
/// Default distance that guarantees about 76% chance of winning.
|
|
const BETA: f64 = SIGMA / 2.0;
|
|
|
|
/// Default dynamic factor.
|
|
const TAU: f64 = SIGMA / 100.0;
|
|
|
|
/// Default draw probability of the game.
|
|
const DRAW_PROBABILITY: f64 = 0.10;
|
|
|
|
/// A basis to check reliability of the result.
|
|
const DELTA: f64 = 0.0001;
|
|
|
|
pub trait Rateable {
|
|
fn mu(&self) -> f64;
|
|
fn sigma(&self) -> f64;
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct Rating {
|
|
pub mu: f64,
|
|
pub sigma: f64,
|
|
}
|
|
|
|
impl Rating {
|
|
pub fn new(mu: f64, sigma: f64) -> Rating {
|
|
Rating { mu, sigma }
|
|
}
|
|
}
|
|
|
|
impl Rateable for Rating {
|
|
fn mu(&self) -> f64 {
|
|
self.mu
|
|
}
|
|
|
|
fn sigma(&self) -> f64 {
|
|
self.sigma
|
|
}
|
|
}
|
|
|
|
impl Rateable for Gaussian {
|
|
fn mu(&self) -> f64 {
|
|
self.mu()
|
|
}
|
|
|
|
fn sigma(&self) -> f64 {
|
|
self.sigma()
|
|
}
|
|
}
|
|
|
|
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<R>(ratings: &[(R, u16)], ranks: &[u16]) -> Vec<Rating>
|
|
where
|
|
R: Rateable,
|
|
{
|
|
// TODO Validate rating_groups is orderded in teams.
|
|
// TODO Validate that teams are orderd after rank.
|
|
|
|
let tau_sqr = TAU.powi(2);
|
|
let beta_sqr = BETA.powi(2);
|
|
|
|
let mut variable_arena = VariableArena::new();
|
|
|
|
let rating_count = ratings.len();
|
|
let team_count = ranks.len();
|
|
|
|
let rating_vars = (0..rating_count).map(|_| variable_arena.create()).collect::<Vec<_>>();
|
|
let perf_vars = ratings
|
|
.iter()
|
|
.map(|(_, team)| {
|
|
(variable_arena.create(), *team)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let team_perf_vars = (0..team_count).map(|_| variable_arena.create()).collect::<Vec<_>>();
|
|
let team_diff_vars = (0..team_count - 1).map(|_| variable_arena.create()).collect::<Vec<_>>();
|
|
|
|
let mut factor_id = 0;
|
|
|
|
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());
|
|
|
|
factor_id += 1;
|
|
|
|
PriorFactor::new(
|
|
&mut variable_arena,
|
|
factor_id,
|
|
*rating_var,
|
|
gaussian,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let perf_layer = rating_vars
|
|
.iter()
|
|
.zip(perf_vars.iter().map(|(variable, _)| variable))
|
|
.map(|(rating_var, perf)| {
|
|
factor_id += 1;
|
|
|
|
LikelihoodFactor::new(
|
|
&mut variable_arena,
|
|
factor_id,
|
|
*rating_var,
|
|
*perf,
|
|
beta_sqr,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let team_perf_layer = team_perf_vars
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, variable)| {
|
|
factor_id += 1;
|
|
|
|
let team = perf_vars
|
|
.iter()
|
|
.filter(|(_, team)| *team as usize == i)
|
|
.map(|(variable, _)| *variable)
|
|
.collect::<Vec<_>>();
|
|
|
|
let team_count = team.len();
|
|
|
|
SumFactor::new(
|
|
&mut variable_arena,
|
|
factor_id,
|
|
*variable,
|
|
team,
|
|
vec![1.0; team_count],
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let team_diff_layer = team_diff_vars
|
|
.iter()
|
|
.zip(team_perf_vars.windows(2))
|
|
.map(|(variable, teams)| {
|
|
factor_id += 1;
|
|
|
|
SumFactor::new(
|
|
&mut variable_arena,
|
|
factor_id,
|
|
*variable,
|
|
teams.to_vec(),
|
|
vec![1.0, -1.0],
|
|
)
|
|
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let trunc_layer = team_diff_vars
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, variable)| {
|
|
factor_id += 1;
|
|
|
|
let player_count = perf_vars
|
|
.iter()
|
|
.filter(|(_, team)| *team as usize == i || *team as usize == i + 1)
|
|
.count();
|
|
|
|
TruncateFactor::new(
|
|
&mut variable_arena,
|
|
factor_id,
|
|
*variable,
|
|
draw_margin(DRAW_PROBABILITY, BETA, player_count as f64),
|
|
ranks[i] == ranks[i + 1],
|
|
)
|
|
|
|
})
|
|
.collect::<Vec<_>>();;
|
|
|
|
for factor in &rating_layer {
|
|
factor.start(&mut variable_arena);
|
|
}
|
|
|
|
for factor in &perf_layer {
|
|
factor.update_value(&mut variable_arena);
|
|
}
|
|
|
|
for factor in &team_perf_layer {
|
|
factor.update_sum(&mut variable_arena);
|
|
}
|
|
|
|
for _ in 0..5 {
|
|
for factor in &team_diff_layer {
|
|
factor.update_sum(&mut variable_arena);
|
|
}
|
|
|
|
for factor in &trunc_layer {
|
|
factor.update(&mut variable_arena);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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::<Vec<_>>()
|
|
}
|
|
|
|
pub fn quality<R>(rating_groups: &[&[R]]) -> f64
|
|
where
|
|
R: Rateable,
|
|
{
|
|
let flatten_ratings = rating_groups
|
|
.iter()
|
|
.flat_map(|group| group.iter())
|
|
.collect::<Vec<_>>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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)]
|
|
mod tests {
|
|
use approx::{AbsDiffEq, RelativeEq};
|
|
use env_logger;
|
|
|
|
use super::*;
|
|
|
|
const EPSILON: f64 = 2e-14;
|
|
|
|
impl AbsDiffEq for Rating {
|
|
type Epsilon = f64;
|
|
|
|
fn default_epsilon() -> Self::Epsilon {
|
|
f64::default_epsilon()
|
|
}
|
|
|
|
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
|
|
self.mu.abs_diff_eq(&other.mu, epsilon) && self.sigma.abs_diff_eq(&other.sigma, epsilon)
|
|
}
|
|
}
|
|
|
|
impl RelativeEq for Rating {
|
|
fn default_max_relative() -> Self::Epsilon {
|
|
f64::default_max_relative()
|
|
}
|
|
|
|
fn relative_eq(
|
|
&self,
|
|
other: &Self,
|
|
epsilon: Self::Epsilon,
|
|
max_relative: Self::Epsilon,
|
|
) -> bool {
|
|
self.mu.relative_eq(&other.mu, epsilon, max_relative)
|
|
&& self.sigma.relative_eq(&other.sigma, epsilon, max_relative)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_quality_1vs1() {
|
|
let alice = Rating::new(MU, SIGMA);
|
|
let bob = Rating::new(MU, SIGMA);
|
|
|
|
assert_relative_eq!(
|
|
quality(&[&[alice], &[bob]]),
|
|
0.4472135954999579,
|
|
epsilon = EPSILON
|
|
);
|
|
}
|
|
|
|
#[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 expected_ratings = vec![
|
|
Rating::new(33.20778932559525, 6.347937214998893),
|
|
Rating::new(27.401497882797486, 5.787057812482782),
|
|
Rating::new(22.598576351652632, 5.7871159419307645),
|
|
Rating::new(16.79337409436942, 6.348053083319977),
|
|
];
|
|
|
|
let ratings = rate(&[(alice, 0), (bob, 1), (chris, 2), (darren, 3)], &[0, 1, 2, 3]);
|
|
|
|
for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) {
|
|
assert_relative_eq!(rating, expected, epsilon = EPSILON);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_1vs1_draw() {
|
|
let alice = Rating::new(MU, SIGMA);
|
|
let bob = Rating::new(MU, SIGMA);
|
|
|
|
let expected_ratings = vec![
|
|
Rating::new(25.0, 6.457515683245051),
|
|
Rating::new(25.0, 6.457515683245051),
|
|
];
|
|
|
|
let ratings = rate(&[(alice, 0), (bob, 1)], &[0, 0]);
|
|
|
|
for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) {
|
|
assert_relative_eq!(rating, expected, epsilon = EPSILON);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_2vs2() {
|
|
let _ = env_logger::try_init();
|
|
|
|
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 expected_ratings = vec![
|
|
Rating::new(28.108322399069035, 7.77436345109384),
|
|
Rating::new(28.108322399069035, 7.77436345109384),
|
|
Rating::new(21.891677600930958, 7.77436345109384),
|
|
Rating::new(21.891677600930958, 7.77436345109384),
|
|
];
|
|
|
|
let ratings = rate(&[(alice, 0), (bob, 0), (chris, 1), (darren, 1)], &[0, 1]);
|
|
|
|
for (rating, expected) in ratings.iter().zip(expected_ratings.iter()) {
|
|
assert_relative_eq!(rating, expected, epsilon = EPSILON);
|
|
}
|
|
}
|
|
}
|