From 172861d1ba2a8b048dae1abaaa1c49aba6f3f614 Mon Sep 17 00:00:00 2001 From: Euller Pereira Date: Fri, 17 Feb 2023 20:29:22 -0300 Subject: [PATCH] refactoring some internal code to better handle pointers, implemented Bradley-Terry with partial pairing --- bradley_terry_full.go | 17 ++++------- bradley_terry_part.go | 58 ++++++++++++++++++++++++++++++++++++ plackett_luce.go | 17 ++++------- util.go | 68 ++++++++++++++++++++++++++++++++++--------- 4 files changed, 124 insertions(+), 36 deletions(-) create mode 100644 bradley_terry_part.go diff --git a/bradley_terry_full.go b/bradley_terry_full.go index e86808b..7465221 100644 --- a/bradley_terry_full.go +++ b/bradley_terry_full.go @@ -18,19 +18,14 @@ func BradleyTerryFull(game []Team, options *Options) []Team { teamRatings := teamRatings(options)(game) - return lo.Map(teamRatings, func(item teamRating, index int) Team { + return lo.Map(teamRatings, func(item *teamRating, index int) Team { var iMu, iSigmaSq, iTeam, iRank = item.TeamMu, item.TeamSigmaSq, item.Team, item.Rank - type _sums struct { - omegaSum float64 - deltaSum float64 - } - - filteredRatings := lo.Filter(teamRatings, func(localItem teamRating, localIndex int) bool { + filteredRatings := lo.Filter(teamRatings, func(localItem *teamRating, localIndex int) bool { return localIndex != index }) - sums := lo.Reduce(filteredRatings, func(agg _sums, localItem teamRating, index int) _sums { + _sums := lo.Reduce(filteredRatings, func(agg sums, localItem *teamRating, index int) sums { var qMu, qSigmaSq, qRank = localItem.TeamMu, localItem.TeamSigmaSq, localItem.Rank ciq := math.Sqrt(iSigmaSq + qSigmaSq + tbs) @@ -44,12 +39,12 @@ func BradleyTerryFull(game []Team, options *Options) []Team { agg.deltaSum += ((iGamma * sigSqToCiq) / ciq) * piq * (1 - piq) return agg - }, _sums{omegaSum: 0, deltaSum: 0}) + }, sums{omegaSum: 0, deltaSum: 0}) result := lo.Map([]*Rating(*iTeam), func(finalItem *Rating, index int) *Rating { sigmaSq := math.Pow(finalItem.SkillUncertaintyDegree, 2) - mu := finalItem.AveragePlayerSkill + (sigmaSq/iSigmaSq)*sums.omegaSum - sigma := finalItem.SkillUncertaintyDegree * math.Sqrt(math.Max(1-(sigmaSq/iSigmaSq)*sums.deltaSum, epsilon)) + mu := finalItem.AveragePlayerSkill + (sigmaSq/iSigmaSq)*_sums.omegaSum + sigma := finalItem.SkillUncertaintyDegree * math.Sqrt(math.Max(1-(sigmaSq/iSigmaSq)*_sums.deltaSum, epsilon)) return &Rating{ AveragePlayerSkill: mu, diff --git a/bradley_terry_part.go b/bradley_terry_part.go new file mode 100644 index 0000000..691efbd --- /dev/null +++ b/bradley_terry_part.go @@ -0,0 +1,58 @@ +package openskill + +import ( + "math" + + "github.com/samber/lo" +) + +// BradleyTerryPart is a implementation of the Bradley-Terry ranking model +// that uses partial pairing. The Bradley-Terry model uses logistic distribution +// to properly rank the teams. Partial pairing is less accurate than a +// full pairing, but works better in situations with a high number of teams. +// This function accepts the a slice with the team that are +// competing, plus an options parameter, with things such as scores and +// previous rankings. The function return a slice of teams that are properly ranked. +func BradleyTerryPart(game []Team, options *Options) []Team { + epsilon := epsilon(options) + tbs := betaSq(options) * 2 + _gamma := gamma(options) + + teamRatings := teamRatings(options)(game) + adjacentTeams := ladderPairs(teamRatings) + + zipper := lo.Zip2(teamRatings, adjacentTeams) + + return lo.Map(zipper, func(item lo.Tuple2[*teamRating, []*teamRating], index int) Team { + iTeamRating, iAdjacents := item.Unpack() + + var iMu, iSigmaSq, iTeam, iRank = iTeamRating.TeamMu, iTeamRating.TeamSigmaSq, iTeamRating.Team, iTeamRating.Rank + + _sums := lo.Reduce(iAdjacents, func(agg sums, localItem *teamRating, index int) sums { + var qMu, qSigmaSq, qRank = localItem.TeamMu, localItem.TeamSigmaSq, localItem.Rank + + ciq := math.Sqrt(iSigmaSq + qSigmaSq + tbs) + piq := 1 / (1 + math.Exp((qMu-iMu)/ciq)) + sigSqToCiq := iSigmaSq / ciq + iGamma := _gamma(ciq, int64(len(teamRatings)), iTeamRating.TeamMu, iTeamRating.TeamSigmaSq, iTeamRating.Team, iTeamRating.Rank) + + agg.omegaSum += sigSqToCiq * (score(qRank, iRank) - piq) + agg.deltaSum += ((iGamma * sigSqToCiq) / ciq) * piq * (1 - piq) + + return agg + }, sums{omegaSum: 0, deltaSum: 0}) + + result := lo.Map([]*Rating(*iTeam), func(finalItem *Rating, index int) *Rating { + sigmaSq := math.Pow(finalItem.SkillUncertaintyDegree, 2) + mu := finalItem.AveragePlayerSkill + (sigmaSq/iSigmaSq)*_sums.omegaSum + sigma := finalItem.SkillUncertaintyDegree * math.Sqrt(math.Max(1-(sigmaSq/iSigmaSq)*_sums.deltaSum, epsilon)) + + return &Rating{ + AveragePlayerSkill: mu, + SkillUncertaintyDegree: sigma, + } + }) + + return Team(result) + }) +} diff --git a/plackett_luce.go b/plackett_luce.go index 3fb1a16..0e90486 100644 --- a/plackett_luce.go +++ b/plackett_luce.go @@ -15,30 +15,25 @@ func PlackettLuce(game []Team, options *Options) []Team { a := utilA(teamRatings) gamma := gamma(options) - return lo.Map(teamRatings, func(item teamRating, index int) Team { + return lo.Map(teamRatings, func(item *teamRating, index int) Team { iMuOverCe := math.Exp(item.TeamMu / c) - type _sums struct { - omegaSum float64 - deltaSum float64 - } - - filteredRatings := lo.Filter(teamRatings, func(localItem teamRating, localIndex int) bool { + filteredRatings := lo.Filter(teamRatings, func(localItem *teamRating, localIndex int) bool { return localItem.Rank <= item.Rank }) - sums := lo.Reduce(filteredRatings, func(agg _sums, item teamRating, localIndex int) _sums { + _sums := lo.Reduce(filteredRatings, func(agg sums, item *teamRating, localIndex int) sums { quotient := iMuOverCe / sumQ[localIndex] agg.omegaSum = agg.omegaSum + lo.Ternary(index == localIndex, 1-quotient, -quotient)/float64(a[localIndex]) agg.deltaSum = agg.deltaSum + (quotient*(1-quotient))/float64(a[localIndex]) return agg - }, _sums{omegaSum: 0, deltaSum: 0}) + }, sums{omegaSum: 0, deltaSum: 0}) iGamma := gamma(c, int64(len(teamRatings)), item.TeamMu, item.TeamSigmaSq, item.Team, item.Rank) - iOmega := sums.omegaSum * (item.TeamSigmaSq / c) - iDelta := iGamma * sums.deltaSum * (item.TeamSigmaSq / math.Pow(c, 2)) + iOmega := _sums.omegaSum * (item.TeamSigmaSq / c) + iDelta := iGamma * _sums.deltaSum * (item.TeamSigmaSq / math.Pow(c, 2)) result := lo.Map([]*Rating(*item.Team), func(finalItem *Rating, index int) *Rating { return &Rating{ diff --git a/util.go b/util.go index fb23419..3772305 100644 --- a/util.go +++ b/util.go @@ -15,6 +15,11 @@ type teamRating struct { Rank int64 } +type sums struct { + omegaSum float64 + deltaSum float64 +} + func rankings(teams []Team, ranks []int64) []int64 { teamScores := lo.Map(teams, func(item Team, index int) int64 { if index < len(ranks) { @@ -37,11 +42,11 @@ func rankings(teams []Team, ranks []int64) []int64 { return outrank } -func teamRatings(options *Options) func(game []Team) []teamRating { - return func(game []Team) []teamRating { +func teamRatings(options *Options) func(game []Team) []*teamRating { + return func(game []Team) []*teamRating { rank := rankings(game, options.Rankings) - return lo.Map(game, func(item Team, index int) teamRating { + return lo.Map(game, func(item Team, index int) *teamRating { mu := lo.Sum(lo.Map([]*Rating(item), func(item *Rating, index int) float64 { return item.AveragePlayerSkill })) @@ -49,7 +54,7 @@ func teamRatings(options *Options) func(game []Team) []teamRating { return math.Pow(item.SkillUncertaintyDegree, 2) })) - return teamRating{ + return &teamRating{ Team: &item, TeamMu: mu, TeamSigmaSq: sigma, @@ -59,24 +64,59 @@ func teamRatings(options *Options) func(game []Team) []teamRating { } } -func utilC(options *Options) func(teamRatings []teamRating) float64 { +func ladderPairs[T any](slc []*T) [][]*T { + size := len(slc) + + var left, right []*T = make([]*T, 0), make([]*T, 0) + + // bail earlier + if size == 1 { + return [][]*T{} + } + + left = append(left, nil) + left = append(left, slc[0:size-1]...) + + right = append(right, slc[1:]...) + right = append(right, nil) + + zip := lo.Zip2(left, right) + + return lo.Map(zip, func(item lo.Tuple2[*T, *T], index int) []*T { + l, r := item.Unpack() + + if l != nil && r != nil { + return []*T{l, r} + } + if l != nil && r == nil { + return []*T{l} + } + if l == nil && r != nil { + return []*T{r} + } + + return []*T{} // this should really only happen when size == 1 + }) +} + +func utilC(options *Options) func(teamRatings []*teamRating) float64 { betasq := betaSq(options) - return func(teamRatings []teamRating) float64 { + return func(teamRatings []*teamRating) float64 { return math.Sqrt( - lo.Sum(lo.Map(teamRatings, func(item teamRating, index int) float64 { + lo.Sum(lo.Map(teamRatings, func(item *teamRating, index int) float64 { return item.TeamSigmaSq + betasq })), ) } } -func utilSumQ(teamRatings []teamRating, c float64) []float64 { - return lo.Map(teamRatings, func(item teamRating, index int) float64 { - filteredRatings := lo.Filter(teamRatings, func(localItem teamRating, index int) bool { +func utilSumQ(teamRatings []*teamRating, c float64) []float64 { + return lo.Map(teamRatings, func(item *teamRating, index int) float64 { + filteredRatings := lo.Filter(teamRatings, func(localItem *teamRating, index int) bool { return localItem.Rank >= item.Rank }) - mappedFilteredRatings := lo.Map(filteredRatings, func(localItem teamRating, index int) float64 { + mappedFilteredRatings := lo.Map(filteredRatings, func(localItem *teamRating, index int) float64 { return math.Exp(localItem.TeamMu / c) }) @@ -84,9 +124,9 @@ func utilSumQ(teamRatings []teamRating, c float64) []float64 { }) } -func utilA(teamRatings []teamRating) []int64 { - return lo.Map(teamRatings, func(item teamRating, index int) int64 { - filteredRatings := lo.Filter(teamRatings, func(localItem teamRating, index int) bool { +func utilA(teamRatings []*teamRating) []int64 { + return lo.Map(teamRatings, func(item *teamRating, index int) int64 { + filteredRatings := lo.Filter(teamRatings, func(localItem *teamRating, index int) bool { return item.Rank == localItem.Rank })