Skip to content

Commit

Permalink
Add expected_score_rating_period functions
Browse files Browse the repository at this point in the history
  • Loading branch information
atomflunder committed Oct 8, 2023
1 parent 7c31977 commit f1b89f1
Show file tree
Hide file tree
Showing 16 changed files with 731 additions and 12 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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

## v0.26.0 - 2023-10-08

- Add expected_score_rating_period functions for all rating systems

## v0.25.1 - 2023-10-05

- Overhaul documentation
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.25.1"
version = "0.26.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 @@ -54,7 +54,7 @@ Alternatively, you can add the following to your `Cargo.toml` file manually:

```toml
[dependencies]
skillratings = "0.25"
skillratings = "0.26"
```

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

```toml
[dependencies]
skillratings = {version = "0.25", features = ["serde"]}
skillratings = {version = "0.26", features = ["serde"]}
```

## Usage and Examples
Expand Down
51 changes: 51 additions & 0 deletions src/dwz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ impl RatingPeriodSystem for DWZ {
fn rate(&self, player: &DWZRating, results: &[(DWZRating, Outcomes)]) -> DWZRating {
dwz_rating_period(player, results)
}

fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
expected_score_rating_period(player, opponents)
}
}

#[must_use]
Expand Down Expand Up @@ -394,6 +398,48 @@ pub fn expected_score(player_one: &DWZRating, player_two: &DWZRating) -> (f64, f
(exp_one, exp_two)
}

#[must_use]
/// Calculates the expected outcome of a player in a rating period or tournament.
///
/// Takes in a players as [`DWZRating`] and a list of opponents as a slice of [`DWZRating`]
/// and returns the probability of victory for each match as an Vec of [`f64`] between 1.0 and 0.0 from the perspective of the player.
/// 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.
///
/// # Examples
/// ```
/// use skillratings::dwz::{expected_score_rating_period, DWZRating};
///
/// let player = DWZRating {
/// rating: 1900.0,
/// index: 42,
/// age: 42,
/// };
///
/// let opponent1 = DWZRating {
/// rating: 1930.0,
/// index: 103,
/// age: 39,
/// };
///
/// let opponent2 = DWZRating {
/// rating: 1730.0,
/// index: 92,
/// age: 14,
/// };
///
/// let exp = expected_score_rating_period(&player, &[opponent1, opponent2]);
///
/// assert_eq!((exp[0] * 100.0).round(), 46.0);
/// assert_eq!((exp[1] * 100.0).round(), 73.0);
/// ```
pub fn expected_score_rating_period(player: &DWZRating, opponents: &[DWZRating]) -> Vec<f64> {
opponents
.iter()
.map(|o| (1.0 + 10.0_f64.powf(-(400.0_f64.recip()) * (player.rating - o.rating))).recip())
.collect()
}

/// Gets a proper first [`DWZRating`].
///
/// In the case that you do not have enough opponents to rate a player against,
Expand Down Expand Up @@ -997,6 +1043,11 @@ mod tests {
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);

let rating_period_system: DWZ = RatingPeriodSystem::new(());
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);

let player_one: DWZRating = Rating::new(Some(240.0), Some(90.0));
let player_two: DWZRating = Rating::new(Some(240.0), Some(90.0));

Expand Down
65 changes: 65 additions & 0 deletions src/egf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ impl RatingPeriodSystem for EGF {

egf_rating_period(player, &new_results[..])
}

fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
let new_opponents: Vec<(EGFRating, EGFConfig)> =
opponents.iter().map(|o| (*o, self.config)).collect();

expected_score_rating_period(player, &new_opponents)
}
}

#[must_use]
Expand Down Expand Up @@ -356,6 +363,54 @@ pub fn expected_score(
(exp_one, 1.0 - exp_one)
}

#[must_use]
/// Calculates the expected outcome of a player in a rating period or tournament.
///
/// Takes in a players as [`EGFRating`] and a list of opponents as a slice of Tuples of [`EGFRating`]s and [`EGFConfig`]s
/// and returns the probability of victory for each match as an Vec of [`f64`] between 1.0 and 0.0 from the perspective of the player.
/// 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.
///
/// ---
///
/// 📌 _**Important note:**_ The parameters intentionally work different from other expected_score_rating_period functions here.
/// In most cases the config is not used, however it is required here, because of the handicaps that can change from game-to-game.
///
/// ---
///
/// # Examples
/// ```
/// use skillratings::egf::{expected_score_rating_period, EGFConfig, EGFRating};
///
/// let player = EGFRating { rating: 900.0 };
///
/// let opponent1 = (EGFRating { rating: 930.0 }, EGFConfig::new());
///
/// let opponent2 = (EGFRating { rating: 730.0 }, EGFConfig::new());
///
/// let exp = expected_score_rating_period(&player, &[opponent1, opponent2]);
///
/// assert_eq!((exp[0] * 100.0).round(), 48.0);
/// assert_eq!((exp[1] * 100.0).round(), 62.0);
/// ```
pub fn expected_score_rating_period(
player: &EGFRating,
opponents: &[(EGFRating, EGFConfig)],
) -> Vec<f64> {
opponents
.iter()
.map(|o| {
let (h1, h2) = if o.1.handicap.is_sign_negative() {
(o.1.handicap.abs(), 0.0)
} else {
(0.0, o.1.handicap.abs())
};

(1.0 + (beta(o.0.rating, h2) - beta(player.rating, h1)).exp()).recip()
})
.collect()
}

fn new_rating(rating: f64, con: f64, score: f64, exp_score: f64, bonus: f64) -> f64 {
// The absolute minimum rating is set to be -900.
(con.mul_add(score - exp_score, rating) + bonus).max(-900.0)
Expand Down Expand Up @@ -498,6 +553,16 @@ mod tests {
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);

let rating_period_system: EGF = RatingPeriodSystem::new(EGFConfig::new());
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);

let rating_period_system2: EGF = RatingPeriodSystem::new(EGFConfig { handicap: -0.0 });
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system2, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);

let player_one: EGFRating = Rating::new(Some(240.0), Some(90.0));
let player_two: EGFRating = Rating::new(Some(240.0), Some(90.0));

Expand Down
39 changes: 39 additions & 0 deletions src/elo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ impl RatingPeriodSystem for Elo {
fn rate(&self, player: &EloRating, results: &[(EloRating, Outcomes)]) -> EloRating {
elo_rating_period(player, results, &self.config)
}

fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
expected_score_rating_period(player, opponents)
}
}

/// Calculates the [`EloRating`]s of two players based on their old ratings and the outcome of the game.
Expand Down Expand Up @@ -330,6 +334,36 @@ pub fn expected_score(player_one: &EloRating, player_two: &EloRating) -> (f64, f
(exp_one, exp_two)
}

/// Calculates the expected outcome of a player in a rating period or tournament.
///
/// Takes in a players as [`EloRating`] and a list of opponents as a slice of [`EloRating`]
/// and returns the probability of victory for each match as an Vec of [`f64`] between 1.0 and 0.0 from the perspective of the player.
/// 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.
///
/// # Examples
/// ```
/// use skillratings::elo::{expected_score_rating_period, EloRating};
///
/// let player = EloRating { rating: 1900.0 };
///
/// let opponent1 = EloRating { rating: 1930.0 };
///
/// let opponent2 = EloRating { rating: 1730.0 };
///
/// let exp = expected_score_rating_period(&player, &[opponent1, opponent2]);
///
/// assert_eq!((exp[0] * 100.0).round(), 46.0);
/// assert_eq!((exp[1] * 100.0).round(), 73.0);
/// ```
#[must_use]
pub fn expected_score_rating_period(player: &EloRating, opponents: &[EloRating]) -> Vec<f64> {
opponents
.iter()
.map(|o| (1.0 + 10_f64.powf((o.rating - player.rating) / 400.0)).recip())
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -450,6 +484,11 @@ mod tests {
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);

let rating_period_system: Elo = RatingPeriodSystem::new(EloConfig::new());
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);

let player_one: EloRating = Rating::new(Some(240.0), Some(90.0));
let player_two: EloRating = Rating::new(Some(240.0), Some(90.0));

Expand Down
39 changes: 39 additions & 0 deletions src/fifa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ impl RatingPeriodSystem for Fifa {

fifa_rating_period(player, &new_results[..])
}

fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
expected_score_rating_period(player, opponents)
}
}

#[must_use]
Expand Down Expand Up @@ -414,6 +418,36 @@ pub fn expected_score(player_one: &FifaRating, player_two: &FifaRating) -> (f64,
(exp_one, exp_two)
}

#[must_use]
/// Calculates the expected outcome of a player in a rating period or tournament.
///
/// Takes in a players as [`FifaRating`] and a list of opponents as a slice of [`FifaRating`]
/// and returns the probability of victory for each match as an Vec of [`f64`] between 1.0 and 0.0 from the perspective of the player.
/// 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.
///
/// # Examples
/// ```
/// use skillratings::fifa::{expected_score_rating_period, FifaRating};
///
/// let player = FifaRating { rating: 1900.0 };
///
/// let opponent1 = FifaRating { rating: 1930.0 };
///
/// let opponent2 = FifaRating { rating: 1730.0 };
///
/// let exp = expected_score_rating_period(&player, &[opponent1, opponent2]);
///
/// assert_eq!((exp[0] * 100.0).round(), 47.0);
/// assert_eq!((exp[1] * 100.0).round(), 66.0);
/// ```
pub fn expected_score_rating_period(player: &FifaRating, opponents: &[FifaRating]) -> Vec<f64> {
opponents
.iter()
.map(|o| (1.0 + 10_f64.powf(-(player.rating - o.rating) / 600.0)).recip())
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -552,6 +586,11 @@ mod tests {
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);

let rating_period_system: Fifa = RatingPeriodSystem::new(FifaConfig::new());
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);

let player_one: FifaRating = Rating::new(Some(240.0), Some(90.0));
let player_two: FifaRating = Rating::new(Some(240.0), Some(90.0));

Expand Down
53 changes: 53 additions & 0 deletions src/glicko.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ impl RatingPeriodSystem for Glicko {
fn rate(&self, player: &GlickoRating, results: &[(GlickoRating, Outcomes)]) -> GlickoRating {
glicko_rating_period(player, results, &self.config)
}

fn expected_score(&self, player: &Self::RATING, opponents: &[Self::RATING]) -> Vec<f64> {
expected_score_rating_period(player, opponents)
}
}

#[must_use]
Expand Down Expand Up @@ -443,6 +447,50 @@ pub fn expected_score(player_one: &GlickoRating, player_two: &GlickoRating) -> (
(exp_one, exp_two)
}

#[must_use]
/// Calculates the expected outcome of a player in a rating period or tournament.
///
/// Takes in a players as [`GlickoRating`] and a list of opponents as a slice of [`GlickoRating`]
/// and returns the probability of victory for each match as an Vec of [`f64`] between 1.0 and 0.0 from the perspective of the player.
/// 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.
///
/// # Examples
/// ```
/// use skillratings::glicko::{expected_score_rating_period, GlickoRating};
///
/// let player = GlickoRating {
/// rating: 1900.0,
/// deviation: 120.0,
/// };
///
/// let opponent1 = GlickoRating {
/// rating: 1930.0,
/// deviation: 120.0,
/// };
///
/// let opponent2 = GlickoRating {
/// rating: 1730.0,
/// deviation: 120.0,
/// };
///
/// let exp = expected_score_rating_period(&player, &[opponent1, opponent2]);
///
/// assert_eq!((exp[0] * 100.0).round(), 46.0);
/// assert_eq!((exp[1] * 100.0).round(), 70.0);
/// ```
pub fn expected_score_rating_period(player: &GlickoRating, opponents: &[GlickoRating]) -> Vec<f64> {
opponents
.iter()
.map(|o| {
let q = 10_f64.ln() / 400.0;
let g = g_value(q, player.deviation.hypot(o.deviation));

(1.0 + 10_f64.powf(-g * (player.rating - o.rating) / 400.0)).recip()
})
.collect()
}

#[must_use]
/// Decays a Rating Deviation Value for a player, if they missed playing in a certain rating period.
///
Expand Down Expand Up @@ -765,6 +813,11 @@ mod tests {
assert!((exp1 - 0.5).abs() < f64::EPSILON);
assert!((exp2 - 0.5).abs() < f64::EPSILON);

let rating_period_system: Glicko = RatingPeriodSystem::new(GlickoConfig::new());
let exp_rp =
RatingPeriodSystem::expected_score(&rating_period_system, &player_one, &[player_two]);
assert!((exp1 - exp_rp[0]).abs() < f64::EPSILON);

let player_one: GlickoRating = Rating::new(Some(240.0), Some(90.0));
let player_two: GlickoRating = Rating::new(Some(240.0), Some(90.0));

Expand Down
Loading

0 comments on commit f1b89f1

Please sign in to comment.