Skip to content

Commit

Permalink
refactoring some internal code to better handle pointers, implemented…
Browse files Browse the repository at this point in the history
… Bradley-Terry with partial pairing
  • Loading branch information
eullerpereira94 committed Feb 17, 2023
1 parent dff951c commit 172861d
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 36 deletions.
17 changes: 6 additions & 11 deletions bradley_terry_full.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
58 changes: 58 additions & 0 deletions bradley_terry_part.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
17 changes: 6 additions & 11 deletions plackett_luce.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
68 changes: 54 additions & 14 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -37,19 +42,19 @@ 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
}))
sigma := lo.Sum(lo.Map([]*Rating(item), func(item *Rating, index int) float64 {
return math.Pow(item.SkillUncertaintyDegree, 2)
}))

return teamRating{
return &teamRating{
Team: &item,
TeamMu: mu,
TeamSigmaSq: sigma,
Expand All @@ -59,34 +64,69 @@ 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)
})

return lo.Sum(mappedFilteredRatings)
})
}

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
})

Expand Down

0 comments on commit 172861d

Please sign in to comment.