Skip to content

Commit 21176f0

Browse files
committed
Add expected_score_multi_team for Weng-Lin
Also renamed the expected_score_teams functions for Weng-Lin & Trueskill
1 parent b0c465c commit 21176f0

File tree

9 files changed

+159
-28
lines changed

9 files changed

+159
-28
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

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

5+
## v0.23.0 - 2022-12-31
6+
7+
- Added `expected_score_multi_team` function for `weng_lin`
8+
- Renamed `expected_score_teams` -> `expected_score_two_teams` for both `weng_lin` and `trueskill`
9+
510
## v0.22.0 - 2022-12-19
611

712
- Added `weng_lin_multi_team`, and `MultiTeamOutcome` struct

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "skillratings"
3-
version = "0.22.0"
3+
version = "0.23.0"
44
edition = "2021"
55
description = "Calculate a player's skill rating using algorithms like Elo, Glicko, Glicko-2, TrueSkill and many more."
66
readme = "README.md"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Alternatively, you can add the following to your `Cargo.toml` file manually:
3939

4040
```toml
4141
[dependencies]
42-
skillratings = "0.22"
42+
skillratings = "0.23"
4343
```
4444

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

5757
```toml
5858
[dependencies]
59-
skillratings = {version = "0.22", features = ["serde"]}
59+
skillratings = {version = "0.23", features = ["serde"]}
6060
```
6161

6262
## Usage and Examples

benches/benchmarks/trueskill_bench.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use skillratings::{
22
trueskill::{
3-
expected_score, expected_score_teams, trueskill, trueskill_rating_period,
3+
expected_score, expected_score_two_teams, trueskill, trueskill_rating_period,
44
trueskill_two_teams, TrueSkillConfig, TrueSkillRating,
55
},
66
Outcomes,
@@ -150,7 +150,7 @@ pub fn expected_trueskill_teams(c: &mut Criterion) {
150150

151151
c.bench_function("TrueSkill 4v4 Expected Score", |b| {
152152
b.iter(|| {
153-
expected_score_teams(
153+
expected_score_two_teams(
154154
black_box(&team_one),
155155
black_box(&team_two),
156156
black_box(&config),

benches/benchmarks/weng_lin_bench.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use skillratings::{
22
weng_lin::{
3-
expected_score, expected_score_teams, weng_lin, weng_lin_multi_team,
3+
expected_score, expected_score_two_teams, weng_lin, weng_lin_multi_team,
44
weng_lin_rating_period, weng_lin_two_teams, WengLinConfig, WengLinRating,
55
},
66
MultiTeamOutcome, Outcomes,
@@ -219,7 +219,7 @@ pub fn expected_wenglin_teams(c: &mut Criterion) {
219219

220220
c.bench_function("WengLin 4v4 Expected Score", |b| {
221221
b.iter(|| {
222-
expected_score_teams(
222+
expected_score_two_teams(
223223
black_box(&team_one),
224224
black_box(&team_two),
225225
black_box(&config),

src/glicko.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@
3636
//! // The config allows you to specify certain values in the Glicko calculation.
3737
//! // Here we set the c value to 23.75, instead of the default 63.2.
3838
//! // This will decrease the amount by which rating deviation increases per rating period.
39-
//! let config = GlickoConfig {
40-
//! c: 23.75,
41-
//! };
39+
//! let config = GlickoConfig { c: 23.75 };
4240
//!
4341
//! // The glicko function will calculate the new ratings for both players and return them.
4442
//! let (new_player_one, new_player_two) = glicko(&player_one, &player_two, &outcome, &config);

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
//!
4848
//! ```toml
4949
//! [dependencies]
50-
//! skillratings = "0.22"
50+
//! skillratings = "0.23"
5151
//! ```
5252
//!
5353
//! ## Serde support
@@ -64,7 +64,7 @@
6464
//!
6565
//! ```toml
6666
//! [dependencies]
67-
//! skillratings = {version = "0.22", features = ["serde"]}
67+
//! skillratings = {version = "0.23", features = ["serde"]}
6868
//! ```
6969
//!
7070
//! # Usage and Examples

src/trueskill.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ pub fn match_quality_teams(
642642
/// 1.0 means a certain victory for the player, 0.0 means certain loss.
643643
/// Values near 0.5 mean a draw is likely to occur.
644644
///
645-
/// Similar to [`expected_score_teams`].
645+
/// Similar to [`expected_score_two_teams`].
646646
///
647647
/// To see the actual chances of a draw occurring, please use [`match_quality`].
648648
///
@@ -703,7 +703,7 @@ pub fn expected_score(
703703
///
704704
/// # Examples
705705
/// ```
706-
/// use skillratings::trueskill::{expected_score_teams, TrueSkillConfig, TrueSkillRating};
706+
/// use skillratings::trueskill::{expected_score_two_teams, TrueSkillConfig, TrueSkillRating};
707707
///
708708
/// let player_one = TrueSkillRating {
709709
/// rating: 38.0,
@@ -723,7 +723,7 @@ pub fn expected_score(
723723
/// uncertainty: 3.0,
724724
/// };
725725
///
726-
/// let (exp1, exp2) = expected_score_teams(
726+
/// let (exp1, exp2) = expected_score_two_teams(
727727
/// &vec![player_one, player_two],
728728
/// &vec![player_three, player_four],
729729
/// &TrueSkillConfig::new(),
@@ -735,7 +735,7 @@ pub fn expected_score(
735735
/// assert!(((exp1 * 100.0).round() - 12.0).abs() < f64::EPSILON);
736736
/// assert!(((exp2 * 100.0).round() - 88.0).abs() < f64::EPSILON);
737737
/// ```
738-
pub fn expected_score_teams(
738+
pub fn expected_score_two_teams(
739739
team_one: &[TrueSkillRating],
740740
team_two: &[TrueSkillRating],
741741
config: &TrueSkillConfig,
@@ -1309,7 +1309,7 @@ mod tests {
13091309
}
13101310

13111311
#[test]
1312-
fn test_expected_score_teams() {
1312+
fn test_expected_score_two_teams() {
13131313
let player_one = TrueSkillRating {
13141314
rating: 38.0,
13151315
uncertainty: 3.0,
@@ -1328,7 +1328,7 @@ mod tests {
13281328
uncertainty: 3.0,
13291329
};
13301330

1331-
let (exp1, exp2) = expected_score_teams(
1331+
let (exp1, exp2) = expected_score_two_teams(
13321332
&[player_one, player_two],
13331333
&[player_three, player_four],
13341334
&TrueSkillConfig::new(),

src/weng_lin.rs

Lines changed: 138 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,7 @@ pub fn weng_lin_two_teams(
499499
/// (&team_three[..], MultiTeamOutcome::new(3)), // Team 3 takes the third place.
500500
/// ];
501501
///
502-
/// let new_teams =
503-
/// weng_lin_multi_team(&teams_and_ranks, &WengLinConfig::new());
502+
/// let new_teams = weng_lin_multi_team(&teams_and_ranks, &WengLinConfig::new());
504503
///
505504
/// assert_eq!(new_teams.len(), 3);
506505
///
@@ -668,11 +667,11 @@ pub fn expected_score(
668667
/// 1.0 means a certain victory for the player, 0.0 means certain loss.
669668
/// Values near 0.5 mean a draw is likely to occur.
670669
///
671-
/// Similar to [`expected_score`].
670+
/// Similar to [`expected_score`] and [`expected_score_multi_team`].
672671
///
673672
/// # Examples
674673
/// ```
675-
/// use skillratings::weng_lin::{expected_score_teams, WengLinConfig, WengLinRating};
674+
/// use skillratings::weng_lin::{expected_score_two_teams, WengLinConfig, WengLinRating};
676675
///
677676
/// let team_one = vec![
678677
/// WengLinRating {
@@ -697,13 +696,13 @@ pub fn expected_score(
697696
/// },
698697
/// ];
699698
///
700-
/// let (exp1, exp2) = expected_score_teams(&team_one, &team_two, &WengLinConfig::new());
699+
/// let (exp1, exp2) = expected_score_two_teams(&team_one, &team_two, &WengLinConfig::new());
701700
///
702701
/// assert!((exp1 + exp2 - 1.0).abs() < f64::EPSILON);
703702
///
704703
/// assert!(((exp1 * 100.0).round() - 21.0).abs() < f64::EPSILON);
705704
/// ```
706-
pub fn expected_score_teams(
705+
pub fn expected_score_two_teams(
707706
team_one: &[WengLinRating],
708707
team_two: &[WengLinRating],
709708
config: &WengLinConfig,
@@ -724,6 +723,99 @@ pub fn expected_score_teams(
724723
p_value(team_one_rating, team_two_rating, c)
725724
}
726725

726+
#[must_use]
727+
/// Calculates the expected outcome of mulitple teams based on the Bradley-Terry model.
728+
///
729+
/// Takes in a slice of teams as a slice of [`WengLinRating`]s and a [`WengLinConfig`],
730+
/// and returns the probability of victory for each team as an [`f64`] between 1.0 and 0.0.
731+
///
732+
/// 1.0 means a certain victory for the team, 0.0 means certain loss.
733+
/// Values near `1 / Number of Teams` mean a draw is likely to occur.
734+
///
735+
/// Similar to [`expected_score`] and [`expected_score_two_teams`].
736+
///
737+
/// # Examples
738+
/// ```
739+
/// use skillratings::weng_lin::{expected_score_multi_team, WengLinConfig, WengLinRating};
740+
///
741+
/// let team_one = vec![
742+
/// WengLinRating {
743+
/// rating: 42.0,
744+
/// uncertainty: 2.1,
745+
/// },
746+
/// WengLinRating::new(),
747+
/// WengLinRating {
748+
/// rating: 12.0,
749+
/// uncertainty: 3.2,
750+
/// },
751+
/// ];
752+
/// let team_two = vec![
753+
/// WengLinRating {
754+
/// rating: 31.0,
755+
/// uncertainty: 1.2,
756+
/// },
757+
/// WengLinRating::new(),
758+
/// WengLinRating {
759+
/// rating: 41.0,
760+
/// uncertainty: 1.2,
761+
/// },
762+
/// ];
763+
/// let team_three = vec![
764+
/// WengLinRating {
765+
/// rating: 31.0,
766+
/// uncertainty: 1.2,
767+
/// },
768+
/// WengLinRating::new(),
769+
/// WengLinRating {
770+
/// rating: 41.0,
771+
/// uncertainty: 1.2,
772+
/// },
773+
/// ];
774+
///
775+
/// let exp =
776+
/// expected_score_multi_team(&[&team_one, &team_two, &team_three], &WengLinConfig::new());
777+
///
778+
/// assert!((exp[0] + exp[1] + exp[2] - 1.0).abs() < f64::EPSILON);
779+
/// assert_eq!((exp[0] * 100.0).round(), 14.0);
780+
/// assert_eq!((exp[1] * 100.0).round(), 43.0);
781+
/// assert_eq!((exp[2] * 100.0).round(), 43.0);
782+
/// ```
783+
pub fn expected_score_multi_team(teams: &[&[WengLinRating]], config: &WengLinConfig) -> Vec<f64> {
784+
let mut ratings = Vec::with_capacity(teams.len());
785+
786+
for team in teams {
787+
let team_rating: f64 = team.iter().map(|p| p.rating).sum();
788+
ratings.push(team_rating);
789+
}
790+
791+
let mut uncertainties_sq = Vec::with_capacity(teams.len());
792+
793+
for team in teams {
794+
let team_uncertainty_sq: f64 = team.iter().map(|p| p.uncertainty.powi(2)).sum();
795+
uncertainties_sq.push(team_uncertainty_sq);
796+
}
797+
798+
let c = 2.0f64
799+
.mul_add(config.beta.powi(2), uncertainties_sq.iter().sum::<f64>())
800+
.sqrt();
801+
802+
let mut exps = Vec::with_capacity(ratings.len());
803+
804+
let mut sum = 0.0;
805+
806+
for rating in ratings {
807+
let e = (rating / c).exp();
808+
exps.push(e);
809+
sum += e;
810+
}
811+
812+
for exp in &mut exps {
813+
*exp /= sum;
814+
}
815+
816+
exps
817+
}
818+
727819
fn p_value(rating_one: f64, rating_two: f64, c_value: f64) -> (f64, f64) {
728820
let e1 = (rating_one / c_value).exp();
729821
let e2 = (rating_two / c_value).exp();
@@ -1081,7 +1173,7 @@ mod tests {
10811173
let p1 = vec![WengLinRating::new()];
10821174
let p2 = vec![WengLinRating::new()];
10831175

1084-
let (exp1, exp2) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
1176+
let (exp1, exp2) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());
10851177

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

@@ -1094,7 +1186,7 @@ mod tests {
10941186
uncertainty: 1.2,
10951187
}];
10961188

1097-
let (exp1, exp2) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
1189+
let (exp1, exp2) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());
10981190

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

@@ -1112,7 +1204,7 @@ mod tests {
11121204
uncertainty: 1.2,
11131205
});
11141206

1115-
let (exp1, exp2) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
1207+
let (exp1, exp2) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());
11161208

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

@@ -1121,11 +1213,47 @@ mod tests {
11211213

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

1124-
let (exp1, _) = expected_score_teams(&p1, &p2, &WengLinConfig::new());
1216+
let (exp1, _) = expected_score_two_teams(&p1, &p2, &WengLinConfig::new());
11251217

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

1221+
#[test]
1222+
fn test_expected_score_multi_teams() {
1223+
let team_one = vec![WengLinRating::new()];
1224+
let team_two = vec![WengLinRating::new()];
1225+
let team_three = vec![WengLinRating::new()];
1226+
let team_four = vec![WengLinRating::new()];
1227+
1228+
let exp = expected_score_multi_team(
1229+
&[&team_one, &team_two, &team_three, &team_four],
1230+
&WengLinConfig::new(),
1231+
);
1232+
1233+
assert_eq!(exp.len(), 4);
1234+
assert!((exp.iter().sum::<f64>() - 1.0).abs() < f64::EPSILON);
1235+
assert!((exp[0] - 0.25).abs() < f64::EPSILON);
1236+
assert!((exp[1] - 0.25).abs() < f64::EPSILON);
1237+
assert!((exp[2] - 0.25).abs() < f64::EPSILON);
1238+
assert!((exp[3] - 0.25).abs() < f64::EPSILON);
1239+
1240+
let team_one = vec![WengLinRating {
1241+
rating: 42.0,
1242+
uncertainty: 2.1,
1243+
}];
1244+
let team_two = vec![WengLinRating {
1245+
rating: 31.0,
1246+
uncertainty: 1.2,
1247+
}];
1248+
1249+
let exp = expected_score_multi_team(&[&team_one, &team_two], &WengLinConfig::new());
1250+
1251+
assert!((exp[0] + exp[1] - 1.0).abs() < f64::EPSILON);
1252+
1253+
assert!((exp[0] - 0.849_021_123_412_260_5).abs() < f64::EPSILON);
1254+
assert!((exp[1] - 0.150_978_876_587_739_42).abs() < f64::EPSILON);
1255+
}
1256+
11291257
#[test]
11301258
fn test_rating_period() {
11311259
let player = WengLinRating::new();

0 commit comments

Comments
 (0)