Skip to content

Commit

Permalink
Add expected_score_multi_team for Weng-Lin
Browse files Browse the repository at this point in the history
Also renamed the expected_score_teams functions for Weng-Lin & Trueskill
  • Loading branch information
atomflunder committed Dec 31, 2022
1 parent b0c465c commit 21176f0
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 28 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

This is a broad overview of the changes that have been made over the lifespan of this library.

## v0.23.0 - 2022-12-31

- Added `expected_score_multi_team` function for `weng_lin`
- Renamed `expected_score_teams` -> `expected_score_two_teams` for both `weng_lin` and `trueskill`

## v0.22.0 - 2022-12-19

- Added `weng_lin_multi_team`, and `MultiTeamOutcome` struct
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "skillratings"
version = "0.22.0"
version = "0.23.0"
edition = "2021"
description = "Calculate a player's skill rating using algorithms like Elo, Glicko, Glicko-2, TrueSkill and many more."
readme = "README.md"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Alternatively, you can add the following to your `Cargo.toml` file manually:

```toml
[dependencies]
skillratings = "0.22"
skillratings = "0.23"
```

### Serde support
Expand All @@ -56,7 +56,7 @@ By editing `Cargo.toml` manually:

```toml
[dependencies]
skillratings = {version = "0.22", features = ["serde"]}
skillratings = {version = "0.23", features = ["serde"]}
```

## Usage and Examples
Expand Down
4 changes: 2 additions & 2 deletions benches/benchmarks/trueskill_bench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use skillratings::{
trueskill::{
expected_score, expected_score_teams, trueskill, trueskill_rating_period,
expected_score, expected_score_two_teams, trueskill, trueskill_rating_period,
trueskill_two_teams, TrueSkillConfig, TrueSkillRating,
},
Outcomes,
Expand Down Expand Up @@ -150,7 +150,7 @@ pub fn expected_trueskill_teams(c: &mut Criterion) {

c.bench_function("TrueSkill 4v4 Expected Score", |b| {
b.iter(|| {
expected_score_teams(
expected_score_two_teams(
black_box(&team_one),
black_box(&team_two),
black_box(&config),
Expand Down
4 changes: 2 additions & 2 deletions benches/benchmarks/weng_lin_bench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use skillratings::{
weng_lin::{
expected_score, expected_score_teams, weng_lin, weng_lin_multi_team,
expected_score, expected_score_two_teams, weng_lin, weng_lin_multi_team,
weng_lin_rating_period, weng_lin_two_teams, WengLinConfig, WengLinRating,
},
MultiTeamOutcome, Outcomes,
Expand Down Expand Up @@ -219,7 +219,7 @@ pub fn expected_wenglin_teams(c: &mut Criterion) {

c.bench_function("WengLin 4v4 Expected Score", |b| {
b.iter(|| {
expected_score_teams(
expected_score_two_teams(
black_box(&team_one),
black_box(&team_two),
black_box(&config),
Expand Down
4 changes: 1 addition & 3 deletions src/glicko.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@
//! // The config allows you to specify certain values in the Glicko calculation.
//! // Here we set the c value to 23.75, instead of the default 63.2.
//! // This will decrease the amount by which rating deviation increases per rating period.
//! let config = GlickoConfig {
//! c: 23.75,
//! };
//! let config = GlickoConfig { c: 23.75 };
//!
//! // The glicko function will calculate the new ratings for both players and return them.
//! let (new_player_one, new_player_two) = glicko(&player_one, &player_two, &outcome, &config);
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
//!
//! ```toml
//! [dependencies]
//! skillratings = "0.22"
//! skillratings = "0.23"
//! ```
//!
//! ## Serde support
Expand All @@ -64,7 +64,7 @@
//!
//! ```toml
//! [dependencies]
//! skillratings = {version = "0.22", features = ["serde"]}
//! skillratings = {version = "0.23", features = ["serde"]}
//! ```
//!
//! # Usage and Examples
Expand Down
12 changes: 6 additions & 6 deletions src/trueskill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ pub fn match_quality_teams(
/// 1.0 means a certain victory for the player, 0.0 means certain loss.
/// Values near 0.5 mean a draw is likely to occur.
///
/// Similar to [`expected_score_teams`].
/// Similar to [`expected_score_two_teams`].
///
/// To see the actual chances of a draw occurring, please use [`match_quality`].
///
Expand Down Expand Up @@ -703,7 +703,7 @@ pub fn expected_score(
///
/// # Examples
/// ```
/// use skillratings::trueskill::{expected_score_teams, TrueSkillConfig, TrueSkillRating};
/// use skillratings::trueskill::{expected_score_two_teams, TrueSkillConfig, TrueSkillRating};
///
/// let player_one = TrueSkillRating {
/// rating: 38.0,
Expand All @@ -723,7 +723,7 @@ pub fn expected_score(
/// uncertainty: 3.0,
/// };
///
/// let (exp1, exp2) = expected_score_teams(
/// let (exp1, exp2) = expected_score_two_teams(
/// &vec![player_one, player_two],
/// &vec![player_three, player_four],
/// &TrueSkillConfig::new(),
Expand All @@ -735,7 +735,7 @@ pub fn expected_score(
/// assert!(((exp1 * 100.0).round() - 12.0).abs() < f64::EPSILON);
/// assert!(((exp2 * 100.0).round() - 88.0).abs() < f64::EPSILON);
/// ```
pub fn expected_score_teams(
pub fn expected_score_two_teams(
team_one: &[TrueSkillRating],
team_two: &[TrueSkillRating],
config: &TrueSkillConfig,
Expand Down Expand Up @@ -1309,7 +1309,7 @@ mod tests {
}

#[test]
fn test_expected_score_teams() {
fn test_expected_score_two_teams() {
let player_one = TrueSkillRating {
rating: 38.0,
uncertainty: 3.0,
Expand All @@ -1328,7 +1328,7 @@ mod tests {
uncertainty: 3.0,
};

let (exp1, exp2) = expected_score_teams(
let (exp1, exp2) = expected_score_two_teams(
&[player_one, player_two],
&[player_three, player_four],
&TrueSkillConfig::new(),
Expand Down
148 changes: 138 additions & 10 deletions src/weng_lin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,7 @@ pub fn weng_lin_two_teams(
/// (&team_three[..], MultiTeamOutcome::new(3)), // Team 3 takes the third place.
/// ];
///
/// let new_teams =
/// weng_lin_multi_team(&teams_and_ranks, &WengLinConfig::new());
/// let new_teams = weng_lin_multi_team(&teams_and_ranks, &WengLinConfig::new());
///
/// assert_eq!(new_teams.len(), 3);
///
Expand Down Expand Up @@ -668,11 +667,11 @@ pub fn expected_score(
/// 1.0 means a certain victory for the player, 0.0 means certain loss.
/// Values near 0.5 mean a draw is likely to occur.
///
/// Similar to [`expected_score`].
/// Similar to [`expected_score`] and [`expected_score_multi_team`].
///
/// # Examples
/// ```
/// use skillratings::weng_lin::{expected_score_teams, WengLinConfig, WengLinRating};
/// use skillratings::weng_lin::{expected_score_two_teams, WengLinConfig, WengLinRating};
///
/// let team_one = vec![
/// WengLinRating {
Expand All @@ -697,13 +696,13 @@ pub fn expected_score(
/// },
/// ];
///
/// let (exp1, exp2) = expected_score_teams(&team_one, &team_two, &WengLinConfig::new());
/// let (exp1, exp2) = expected_score_two_teams(&team_one, &team_two, &WengLinConfig::new());
///
/// assert!((exp1 + exp2 - 1.0).abs() < f64::EPSILON);
///
/// assert!(((exp1 * 100.0).round() - 21.0).abs() < f64::EPSILON);
/// ```
pub fn expected_score_teams(
pub fn expected_score_two_teams(
team_one: &[WengLinRating],
team_two: &[WengLinRating],
config: &WengLinConfig,
Expand All @@ -724,6 +723,99 @@ pub fn expected_score_teams(
p_value(team_one_rating, team_two_rating, c)
}

#[must_use]
/// Calculates the expected outcome of mulitple teams based on the Bradley-Terry model.
///
/// Takes in a slice of teams as a slice of [`WengLinRating`]s and a [`WengLinConfig`],
/// and returns the probability of victory for each team as an [`f64`] between 1.0 and 0.0.
///
/// 1.0 means a certain victory for the team, 0.0 means certain loss.
/// Values near `1 / Number of Teams` mean a draw is likely to occur.
///
/// Similar to [`expected_score`] and [`expected_score_two_teams`].
///
/// # Examples
/// ```
/// use skillratings::weng_lin::{expected_score_multi_team, WengLinConfig, WengLinRating};
///
/// let team_one = vec![
/// WengLinRating {
/// rating: 42.0,
/// uncertainty: 2.1,
/// },
/// WengLinRating::new(),
/// WengLinRating {
/// rating: 12.0,
/// uncertainty: 3.2,
/// },
/// ];
/// let team_two = vec![
/// WengLinRating {
/// rating: 31.0,
/// uncertainty: 1.2,
/// },
/// WengLinRating::new(),
/// WengLinRating {
/// rating: 41.0,
/// uncertainty: 1.2,
/// },
/// ];
/// let team_three = vec![
/// WengLinRating {
/// rating: 31.0,
/// uncertainty: 1.2,
/// },
/// WengLinRating::new(),
/// WengLinRating {
/// rating: 41.0,
/// uncertainty: 1.2,
/// },
/// ];
///
/// let exp =
/// expected_score_multi_team(&[&team_one, &team_two, &team_three], &WengLinConfig::new());
///
/// assert!((exp[0] + exp[1] + exp[2] - 1.0).abs() < f64::EPSILON);
/// assert_eq!((exp[0] * 100.0).round(), 14.0);
/// assert_eq!((exp[1] * 100.0).round(), 43.0);
/// assert_eq!((exp[2] * 100.0).round(), 43.0);
/// ```
pub fn expected_score_multi_team(teams: &[&[WengLinRating]], config: &WengLinConfig) -> Vec<f64> {
let mut ratings = Vec::with_capacity(teams.len());

for team in teams {
let team_rating: f64 = team.iter().map(|p| p.rating).sum();
ratings.push(team_rating);
}

let mut uncertainties_sq = Vec::with_capacity(teams.len());

for team in teams {
let team_uncertainty_sq: f64 = team.iter().map(|p| p.uncertainty.powi(2)).sum();
uncertainties_sq.push(team_uncertainty_sq);
}

let c = 2.0f64
.mul_add(config.beta.powi(2), uncertainties_sq.iter().sum::<f64>())
.sqrt();

let mut exps = Vec::with_capacity(ratings.len());

let mut sum = 0.0;

for rating in ratings {
let e = (rating / c).exp();
exps.push(e);
sum += e;
}

for exp in &mut exps {
*exp /= sum;
}

exps
}

fn p_value(rating_one: f64, rating_two: f64, c_value: f64) -> (f64, f64) {
let e1 = (rating_one / c_value).exp();
let e2 = (rating_two / c_value).exp();
Expand Down Expand Up @@ -1081,7 +1173,7 @@ mod tests {
let p1 = vec![WengLinRating::new()];
let p2 = vec![WengLinRating::new()];

let (exp1, exp2) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
let (exp1, exp2) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());

assert!((exp1 - exp2).abs() < f64::EPSILON);

Expand All @@ -1094,7 +1186,7 @@ mod tests {
uncertainty: 1.2,
}];

let (exp1, exp2) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
let (exp1, exp2) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());

assert!((exp1 + exp2 - 1.0).abs() < f64::EPSILON);

Expand All @@ -1112,7 +1204,7 @@ mod tests {
uncertainty: 1.2,
});

let (exp1, exp2) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
let (exp1, exp2) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());

assert!((exp1 + exp2 - 1.0).abs() < f64::EPSILON);

Expand All @@ -1121,11 +1213,47 @@ mod tests {

p2.push(WengLinRating::new());

let (exp1, _) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
let (exp1, _) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());

assert!((exp1 - 0.213_836_440_502_453_18).abs() < f64::EPSILON);
}

#[test]
fn test_expected_score_multi_teams() {
let team_one = vec![WengLinRating::new()];
let team_two = vec![WengLinRating::new()];
let team_three = vec![WengLinRating::new()];
let team_four = vec![WengLinRating::new()];

let exp = expected_score_multi_team(
&[&team_one, &team_two, &team_three, &team_four],
&WengLinConfig::new(),
);

assert_eq!(exp.len(), 4);
assert!((exp.iter().sum::<f64>() - 1.0).abs() < f64::EPSILON);
assert!((exp[0] - 0.25).abs() < f64::EPSILON);
assert!((exp[1] - 0.25).abs() < f64::EPSILON);
assert!((exp[2] - 0.25).abs() < f64::EPSILON);
assert!((exp[3] - 0.25).abs() < f64::EPSILON);

let team_one = vec![WengLinRating {
rating: 42.0,
uncertainty: 2.1,
}];
let team_two = vec![WengLinRating {
rating: 31.0,
uncertainty: 1.2,
}];

let exp = expected_score_multi_team(&[&team_one, &team_two], &WengLinConfig::new());

assert!((exp[0] + exp[1] - 1.0).abs() < f64::EPSILON);

assert!((exp[0] - 0.849_021_123_412_260_5).abs() < f64::EPSILON);
assert!((exp[1] - 0.150_978_876_587_739_42).abs() < f64::EPSILON);
}

#[test]
fn test_rating_period() {
let player = WengLinRating::new();
Expand Down

0 comments on commit 21176f0

Please sign in to comment.