Skip to content

Instantly share code, notes, and snippets.

@grignaak
Created April 28, 2012 03:37
Show Gist options
  • Save grignaak/2515489 to your computer and use it in GitHub Desktop.
Save grignaak/2515489 to your computer and use it in GitHub Desktop.
Online ranking algorithm in java
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;
}
}
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();
}
}
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());
}
};
}
}
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;
}
}
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;
}
}
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