Created
April 28, 2012 03:37
-
-
Save grignaak/2515489 to your computer and use it in GitHub Desktop.
Online ranking algorithm in java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gaming.online.java; | |
public class Ability { | |
private final double mean; | |
private final double variance; | |
private Ability(final double mean, final double variance) { | |
this.mean = mean; | |
this.variance = variance; | |
} | |
public static Ability zero() { | |
return withStddev(0, 0); | |
} | |
/** | |
* A numerical representation of the ability, representing 95% confidence that the actual | |
* ability is above the returned number | |
*/ | |
public double skill() { | |
return mean - 3 * stddev(); | |
} | |
public Ability plus(final Ability other) { | |
return withVariance( | |
mean + other.mean, | |
variance + other.variance); | |
} | |
public double stddev() { | |
return Math.sqrt(variance); | |
} | |
public static Ability withVariance(final double mean, final double variance) { | |
return new Ability(mean, variance); | |
} | |
public static Ability withStddev(final double mean, final double stddev) { | |
return new Ability(mean, stddev * stddev); | |
} | |
public double variance() { | |
return variance; | |
} | |
public double mean() { | |
return mean; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gaming.online.java; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.Comparator; | |
import java.util.List; | |
import java.util.ListIterator; | |
public class BradleyTerryFullUpdater { | |
private final double betaSquared; { | |
final double init_mu = 25; | |
final double init_sigma = init_mu / 3; | |
final double beta = init_sigma * 0.5; | |
betaSquared = beta * beta; | |
} | |
private final Comparator<Team> byScoreAndAbility = ByScoreAndSkillComparator.forCalculation(); | |
private final double rankAllowance; | |
public BradleyTerryFullUpdater(final int rankAllowance) { | |
this.rankAllowance = rankAllowance; | |
} | |
public List<Player> updatePlayerRankings(final List<Team> teams_) { | |
final List<Team> teams = sortedByScoreAndAbility(teams_); | |
calculateRanks(teams); | |
final double gamma = 1.0 / teams.size(); | |
final List<Player> updatedPlayers = new ArrayList<Player>(); | |
for (final Team team : teams) { | |
double Omega = 0; | |
double Delta = 0; | |
final Ability teamAbility = team.ability(); | |
for (final Team opponent : teams) { | |
if (opponent == team) continue; | |
final Ability opAbility = opponent.ability(); | |
final double c = Math.sqrt(teamAbility.variance() + opAbility.variance() + 2 * betaSquared); | |
final double p = 1 / (1 + Math.exp((opAbility.mean() - teamAbility.mean()) / c)); | |
final double varianceToC = teamAbility.variance() / c; | |
final double s = s(team, opponent); | |
Omega += varianceToC * (s - p); | |
Delta += gamma * varianceToC / c * p * (1 - p); | |
} | |
updatedPlayers.addAll(resultingAbilities(team, Delta, Omega)); | |
} | |
return updatedPlayers; | |
} | |
public List<Player> resultingAbilities(final Team players, final double Delta, final double Omega) { | |
final List<Player> resultingAbilities = new ArrayList<Player>(); | |
final Ability teamAbility = players.ability(); | |
for (final Player player : players) { | |
final Ability ability = player.priorAbility(); | |
final double mean = ability.mean() + ability.variance() / teamAbility.variance() * Omega; | |
final double stddev = ability.stddev() * | |
Math.sqrt(Math.max(1 - ability.variance() / teamAbility.variance() * Delta, 0.0001)); | |
resultingAbilities.add(new Player(player.id(), Ability.withStddev(mean, stddev))); | |
} | |
return resultingAbilities; | |
} | |
private List<Team> sortedByScoreAndAbility(final List<Team> unsorted) { | |
final List<Team> teams = new ArrayList<Team>(unsorted); | |
Collections.sort(teams, byScoreAndAbility); | |
return teams; | |
} | |
private void calculateRanks(final List<Team> teams) { | |
final ListIterator<Team> iter = teams.listIterator(); | |
while (iter.hasNext()) { | |
final Team topOfRank = iter.next(); | |
final int currentRank = iter.previousIndex(); | |
topOfRank.setRank(currentRank); | |
while (iter.hasNext()) { | |
final Team team = iter.next(); | |
if (!isSameRank(topOfRank, team)) | |
break; | |
team.setRank(currentRank); | |
} | |
} | |
} | |
private double s(final Team team_i, final Team team_q) { | |
final int cmp = Double.compare(team_q.rank(), team_i.rank()); | |
if (cmp > 0) | |
return 1; | |
else if (cmp == 0) | |
return 0.5; | |
else | |
return 0.0; | |
} | |
private boolean isSameRank(final Team topOfRank, final Team team) { | |
return topOfRank.score() - rankAllowance <= team.score(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gaming.online.java; | |
import java.util.Comparator; | |
/** | |
* Comparators which sorts a team by score and ability. | |
* <p> | |
* These comparators will produce the same sorted results no matter the initial | |
* order of the teams. This is important when using a partial-pair updater | |
* instead of a full-pair updater. | |
* </p> | |
*/ | |
public final class ByScoreAndSkillComparator { | |
private ByScoreAndSkillComparator() {} | |
/** | |
* A comparator which gives preference to teams with | |
* <ol> | |
* <li>Higher score;</li> | |
* <li>More players;</li> | |
* <li>Higher skill;</li> | |
* <li>Lower id</li> | |
* </ol> | |
* | |
* <p> | |
* <b>Rationale for tie-breakers</b> A partial-pair updater only updates | |
* players' abilities based on how it compared to the surrounding teams by | |
* rank. When two teams tie, one team will be compared with a better team | |
* while the other is compared with a worse team, in terms of score. In | |
* other words, of these two teams the team this comparator puts first will | |
* get attributed a loss while the other team is attributed a win. (Both | |
* teams will also be attributed a draw.) | |
* </p> | |
* <p> | |
* The team with less players and equal score likely had better individual | |
* ability, and are accordingly attributed the "win." | |
* </p> | |
* <p> | |
* Accounting for number of players, it is assumed a team with higher skill | |
* should have beaten a team with lower skill. Thus, this comparator | |
* positions the underdog to gain the win. | |
* </p> | |
* <p> | |
* <b>Note:</b> The tie-breaking doesn't matter when using a full-pair | |
* updater because a team is always compared to every other team. | |
* </p> | |
*/ | |
public static Comparator<Team> forCalculation() { | |
return new Comparator<Team>() { | |
@Override | |
public int compare(Team a, Team b) { | |
final int scoreCmp = -Double.compare(a.score(), b.score()); | |
if (scoreCmp != 0) | |
return scoreCmp; | |
final int countCmp = -Double.compare(a.size(), b.size()); | |
if (countCmp != 0) | |
return countCmp; | |
final int abilityCmp = -Double.compare(a.skill(), b.skill()); | |
if (abilityCmp != 0) | |
return abilityCmp; | |
return Double.compare(a.id(), b.id()); | |
} | |
}; | |
} | |
/** | |
* A comparator which gives preference to teams with | |
* <ol> | |
* <li>Higher score;</li> | |
* <li>Less players;</li> | |
* <li>Lower skill;</li> | |
* <li>Higher id</li> | |
* </ol> | |
* | |
* See the documentation of {@link #forCalculation()} for rationale of the | |
* tie-breakers which are flipped in this method to provide better user | |
* experience. | |
*/ | |
public static Comparator<Team> forDisplay() { | |
return new Comparator<Team>() { | |
@Override | |
public int compare(Team a, Team b) { | |
final int scoreCmp = -Double.compare(a.score(), b.score()); | |
if (scoreCmp != 0) | |
return scoreCmp; | |
final int countCmp = Double.compare(a.size(), b.size()); | |
if (countCmp != 0) | |
return countCmp; | |
final int abilityCmp = Double.compare(a.skill(), b.skill()); | |
if (abilityCmp != 0) | |
return abilityCmp; | |
return -Double.compare(a.id(), b.id()); | |
} | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gaming.online.java; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.Comparator; | |
import java.util.List; | |
public class Go { | |
public static void main(final String[] args) { | |
final Team team1 = new Team(1, 500, | |
Arrays.asList(new Player(1, Ability.withStddev(25, 8)), | |
new Player(2, Ability.withStddev(27, 5)), | |
new Player(3, Ability.withStddev(22, 3)))); | |
final Team team2 = new Team(2, 400, | |
Arrays.asList(new Player(4, Ability.withStddev(25, 8)), | |
new Player(5, Ability.withStddev(27, 5)), | |
new Player(6, Ability.withStddev(22, 3)))); | |
final Team team3 = new Team(3, 395, | |
Arrays.asList(new Player(7, Ability.withStddev(25, 8)), | |
new Player(8, Ability.withStddev(27, 5)), | |
new Player(9, Ability.withStddev(22, 3)))); | |
final List<Team> teams = Arrays.asList(team1, team2, team3); | |
Collections.shuffle(teams); | |
System.out.println("Game outcome:"); | |
System.out.println(split(sort(teams, ByScoreAndSkillComparator.forDisplay()))); | |
final List<Player> updatedPlayers = new BradleyTerryFullUpdater(10).updatePlayerRankings(teams); | |
System.out.println(); | |
System.out.println("Updated abilities:"); | |
System.out.println(split(sort(updatedPlayers, byId()))); | |
} | |
private static Comparator<Player> byId() { | |
return new Comparator<Player>() { | |
public int compare(Player a, Player b) { | |
return Double.compare(a.id(), b.id()); | |
} | |
}; | |
} | |
private static <T> String split(final List<T> list) { | |
final StringBuilder builder = new StringBuilder(); | |
for (final T item : list) { | |
builder.append(item).append("\n"); | |
} | |
builder.setLength(builder.length() - 1); | |
return builder.toString(); | |
} | |
private static <T> List<T> sort(final List<T> list, final Comparator<? super T> cmp) { | |
final ArrayList<T> sorted = new ArrayList<T>(list); | |
Collections.sort(sorted, cmp); | |
return sorted; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gaming.online.java; | |
public final class Player { | |
private final long id; | |
private final Ability priorAbility; | |
public Player(final long id, final Ability ability) { | |
this.id = id; | |
this.priorAbility = ability; | |
} | |
public long id() { | |
return id; | |
} | |
public Ability priorAbility() { | |
return priorAbility; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gaming.online.java; | |
import java.util.Iterator; | |
import java.util.List; | |
public final class Team implements Iterable<Player> { | |
private final long id; | |
private final double score; | |
private final List<Player> players; | |
private final Ability ability; | |
private int rank = -1; | |
public Team(final long id, final double score, final List<Player> players) { | |
this.id = id; | |
this.score = score; | |
this.players = players; | |
this.ability = calculateAbility(players); | |
} | |
private static Ability calculateAbility(final List<Player> players) { | |
Ability aggregate = Ability.zero(); | |
for (final Player player : players) { | |
aggregate = aggregate.plus(player.priorAbility()); | |
} | |
return aggregate; | |
} | |
public double score() { | |
return score; | |
} | |
public int rank() { | |
if (rank < 0) | |
throw new IllegalArgumentException("Rank not calculated yet"); | |
return rank; | |
} | |
public void setRank(final int rank) { | |
this.rank = rank; | |
} | |
public double skill() { | |
return ability.skill(); | |
} | |
public Ability ability() { | |
return ability; | |
} | |
public long id() { | |
return id; | |
} | |
public int size() { | |
return players.size(); | |
} | |
@Override | |
public String toString() { | |
return Long.toString(id); | |
} | |
@Override | |
public Iterator<Player> iterator() { | |
return players.iterator(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment