/*
Copyright (C) 2008,2009 Martin Günther <mintar@gmx.de>
This file is part of GgpRatingSystem.
GgpRatingSystem is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
GgpRatingSystem is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GgpRatingSystem. If not, see <http://www.gnu.org/licenses/>.
*/
package ggpratingsystem.ratingsystems;
import static java.util.logging.Level.FINE;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import flanagan.analysis.Regression;
import ggpratingsystem.Configuration;
import ggpratingsystem.Game;
import ggpratingsystem.Match;
import ggpratingsystem.MatchSet;
import ggpratingsystem.Player;
/**
* @author martin
*
*/
public class LinearRegressionGameInfo extends AbstractGameInfo {
public static final double DEFAULT_EXPECTED_SCORE = 50.0;
private static final Logger log = Logger.getLogger(LinearRegressionGameInfo.class.getName());
static {
// inherit default level for package ggpratingsystem
log.setLevel(null);
}
private double coeffs[][];
// first dimension: target player
// second dimension: coefficients (index 0 is y-axis intercept, index n is player n-1)
// example:
// coeffs[2][0] is target player 2, intercept
// coeffs[0][3] is target player 0, coefficient for player 2
private final int numPlayers;
private final RatingSystemType ratingSystemType;
private int numMatches;
/**
* @deprecated Use {@link ggpratingsystem.ratingsystems.GameInfoFactory#makeGameInfo(RatingSystemType, Game)} instead.
*/
@Deprecated
protected LinearRegressionGameInfo(RatingSystemType ratingSystemType, Game game) {
super(game);
numPlayers = game.getRoles().size();
this.ratingSystemType = ratingSystemType;
reset();
}
@Override
public void reset() {
coeffs = new double[numPlayers][numPlayers + 1];
/*
* Initialize coefficients so that if expectedScore() is called before
* updateGameInfo() is ever called, DEFAULT_EXPECTED_SCORE will be
* returned for all players.
*/
for (int i = 0; i < numPlayers; i++) {
coeffs[i][0] = DEFAULT_EXPECTED_SCORE;
}
numMatches = 0;
}
@Override
public RatingSystemType getType() {
return ratingSystemType;
}
@Override
public void updateGameInfo(MatchSet matches) {
if (!matches.getGame().equals(this.getGame())) {
throw new IllegalArgumentException("Wrong game for this GameInfo!");
}
int newNumMatches = matches.getMatches().size();
double[][] coefficients = new double[numPlayers][numPlayers + 1];
/* one linear regression for each player */
for (int i = 0; i < numPlayers; i++) {
coefficients[i] = calcCoefficients(matches, i);
}
updateCoefficients(coefficients, newNumMatches);
}
/**
* @param matches
* @return overall (sum) expected score for all players in the given match
* set, based on the current coefficients
*/
public Map<Player, Double> expectedScores(MatchSet matches) {
if (!matches.getGame().equals(this.getGame())) {
throw new IllegalArgumentException("Wrong game for this GameInfo!");
}
Map<Player, Double> expectedScores = new HashMap<Player, Double>();
List<Match> matchList = matches.getMatches();
for (Match match : matchList) {
List<Player> players = match.getPlayers();
/* extract ratings */
double[] ratings = new double[numPlayers];
for (int j = 0; j < numPlayers; j++) {
ratings[j] = players.get(j).getRating(ratingSystemType).getCurRating();
}
for (int i = 0; i < numPlayers; i++) {
Player player = players.get(i);
Double expectedScore = expectedScores.get(player);
if (expectedScore == null) {
expectedScore = 0.0;
}
expectedScore += multiplyRatingsCoefficients(i, ratings);
expectedScores.put(player, expectedScore);
}
}
log.info(expectedScores.toString());
return expectedScores;
}
/**
* Calculates the y value for the following linear regression with
* intercept:
*
* y = c[0] + c[1]*x[0] + c[2]*x[1] +c[3]*x[2] + . . .
*
* where c[i] = coeffs[targetRole][i]
* and x[j] = rating of the player playing role j.
*
* @param targetRole
* number of the role for which to calculate the y value
* (expected score).
* @param ratings
* the ratings of the other players, in the order of their played
* roles.
* @return the expected score of role *targetRole*, given the *ratings* and
* the current coefficients *coeffs*. Bounded between 0 and 100.
*/
private double multiplyRatingsCoefficients(int targetRole, double[] ratings) {
assert (0 <= targetRole && targetRole < numPlayers);
assert (ratings.length == numPlayers);
double result = coeffs[targetRole][0]; // intercept
for (int i = 0; i < numPlayers; i++) {
// assert (ratings[i] > 0);
// This had to be uncommented; the ratings CAN become negative!
result += coeffs[targetRole][i + 1] * ratings[i];
}
if (result < 0.0) {
result = 0.0;
}
else if (result > 100.0) {
result = 100.0;
}
return result;
}
private void updateCoefficients(double[][] newCoeffs, int newNumMatches) {
if (newCoeffs.length != numPlayers) {
throw new IllegalArgumentException("wrong array size");
}
double[][] updatedCoeffs = new double[numPlayers][numPlayers + 1];
/*
* calculate the weighted average; I don't know if this is valid or if
* one should rather re-calculate the whole linear regression with ALL
* matches (this should be safer).
*/
for (int i = 0; i < numPlayers; i++) {
if (newCoeffs[i].length != numPlayers + 1) {
throw new IllegalArgumentException("wrong array size");
}
for (int j = 0; j < numPlayers + 1; j++) {
updatedCoeffs[i][j] =
(numMatches * coeffs[i][j] + newNumMatches * newCoeffs[i][j])
/ (numMatches + newNumMatches);
}
}
coeffs = updatedCoeffs;
numMatches = numMatches + newNumMatches;
}
private double[] calcCoefficients(MatchSet matches, int targetRole) {
double[] result;
/* calculate coefficients for all players */
result = calcCoefficientsInner(matches, targetRole, false);
/*
* if coefficient of current player < 0, re-run the
* regression without that player and set that coefficient to 0.
* this is necessary because otherwise players with a low rating are
* expected to score higher than players with a high rating and
* punished accordingly.
*/
if (result[targetRole] < 0) {
result = calcCoefficientsInner(matches, targetRole, true);
}
return result;
}
/**
* Calculates an array coefficients[numRoles], where numRoles is the number
* of roles in the game.
* coefficients[0] = y-axis intercept
* coefficients[n + 1] = coefficient of role n
*
* If the flag zeroTargetCoeff is set, then it will be ensured that
* coefficients[targetRole] = 0.0
*
* The reason for this is that sometimes, coefficients[targetRole] will be
* negative, which is not desirable. In this case, calcCoefficientsInner
* can be called again with zeroTargetCoeff = true.
*
* @param matches
* @param targetRole role number for which to calculate the coefficients
* @param zeroTargetCoeff force the coefficient of the target player to be 0
* @return
*/
private double[] calcCoefficientsInner(MatchSet matches, int targetRole, boolean zeroTargetCoeff) {
int numRoles = matches.getGame().getRoles().size();
int numMatches = matches.getMatches().size();
double[] coefficients = new double[numRoles + 1];
/* Number of non-ignored players must be at least 1 to perform a linear regression */
if (zeroTargetCoeff && (numRoles == 1)) {
/* fallback */
// coefficients[0] = matches.averageScorePerMatch();
coefficients[0] = matches.averageRoleScore().get(targetRole);
return coefficients;
}
/*
* Prepare the arrays xdata and ydata: inputs to the linear regression
* algorithm. ydata holds the target variable, xdata holds the source
* variables (see below).
*/
double[][] xdata;
double[] ydata = new double[numMatches];
if (zeroTargetCoeff) {
// role targetRole will not be written, so we need one column less in the array
xdata = new double[numRoles - 1][numMatches] ;
} else {
xdata = new double[numRoles][numMatches] ;
}
/* copy the match data into the xdata/ydata arrays */
for (int matchNumber = 0; matchNumber < numMatches; matchNumber++) {
Match match = matches.getMatches().get(matchNumber);
/*
* the target variable (the value that the linear regression is
* trying to predict) is the score of the target role
*/
ydata[matchNumber] = match.getScores().get(targetRole);
/*
* the source variables (the values used by the linear regression in
* its prediction) are the ratings of all players in the match, in the
* order of the roles that they played
*/
int roleToRead = 0;
int roleToWrite = 0;
while (true) {
if (zeroTargetCoeff && roleToRead == targetRole) {
// skip this role, don't include it in the xdata array
roleToRead++;
}
if (roleToRead >= numRoles) {
break;
}
Player player = match.getPlayers().get(roleToRead);
xdata[roleToWrite][matchNumber] = player.getRating(ratingSystemType).getCurRating();
roleToWrite++;
roleToRead++;
}
}
/*
* Degrees of freedom must be at least 1 to perform a linear regression
* (i.e., you have to have enough matches relative to the number of
* source variables (the number of players))
*/
int degreesOfFreedom = ydata.length - (xdata.length + 1);
if (degreesOfFreedom < 1) {
/* fallback */
// coefficients[0] = matches.averageScorePerMatch();
coefficients[0] = matches.averageRoleScore().get(targetRole);
return coefficients;
}
/* Everything seems to be okay, now we can perform the linear regression */
Regression reg = new Regression(xdata, ydata);
reg.linear();
/*
* If the debug level at least "FINE", output some debug info, but only
* the first time this function is called, not when it is called the
* second time with zeroTargetCoeff == true.
*/
if (log.isLoggable(FINE) && !zeroTargetCoeff) {
String filename = "output_" + ratingSystemType + "_" + matches.toString() + "_role_" + targetRole + "_run_.txt"; // the number after "run_" will be written by reg.print()
filename = filename.toLowerCase();
File outputfile = new File(Configuration.getOutputDir(), filename);
log.fine("Writing regression debug output to file " + filename + ". " +
"If you don't want this, set the log level higher than FINE.");
reg.print(outputfile.getPath());
}
double[] tempCoeff = reg.getCoeff();
/* If all roles were included in the regression, we are done */
if (!zeroTargetCoeff) {
return tempCoeff;
}
/*
* Otherwise (if a player was skipped), we have to adjust the
* coefficients again: the value for the removed player (which is zero)
* must be re-inserted into the coefficients
*/
int roleToRead = 0;
int roleToWrite = 0;
while (roleToWrite < numRoles) {
if (roleToRead == targetRole + 1) { // targetRole + 1, because
// tempCoeff[0] is the y-axis
// intercept --> everything
// shifts by 1 to the right
// skip writing (leave value at 0.0)
roleToWrite++;
}
coefficients[roleToWrite] = tempCoeff[roleToRead];
roleToWrite++;
roleToRead++;
}
return coefficients;
}
public double[][] getCoefficients() {
return coeffs.clone();
}
}