@@ -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+
727819fn 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