Giter VIP home page Giter VIP logo

glicko2js's Introduction

glicko2js's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

glicko2js's Issues

npm version behind latest

Hello, thanks for making this library, I've learned a lot from it.

It looks like the version hosted on npm is missing the latest fix (for the undetypeof(matches)fined typo). If you have time, are you able to bump the version and upload it to npm?

Cheers

Question about increased RD over time

In reading about Glicko2 I read that the RD of a player should increase over time if they haven't played any matches. Is that accurate and if so, does this library have a mechanism to handle it? If we only track rating, RD, and volitility how would the system know if a match hasn't been played for a long time?

[Suggestion] Reduce player initialization complexity

In order to initialize a player they have to be entered in the //Make players section and then also in the var players = [ section. I have to manage a tournament with a huge number of participants and this doubles the work.

is there any way to initialize a player only with glicko.makePlayer? Maybe an optional mode to reduce complexity.

Many thanks for your work. I follow and use it since the beginning. In 2013 I reported a bug: #2

Typescript

I try to put this file in typescript but maybe its not finished but you have a "get started" ...

const scalingFactor = 173.7178;

class Race {
    matches: any[]
    constructor(results: any) { this.matches = this.computeMatches(results); }
    computeMatches = function (results) {
        let players: { player: string, position: number }[] = [];
        let position = 0;

        results.forEach(function (rank) {
            position += 1;
            rank.forEach(function (player) { players.push({ "player": player, "position": position }); })
        })

        function computeMatches(players: any) {
            if (players.length === 0) return [];

            const player1 = players.shift()
            const player1_results = players.map(function (player2) {
                return [player1.player, player2.player, (player1.position < player2.position) ? 1 : 0.5];
            });

            return player1_results.concat(computeMatches(players));
        }
        return computeMatches(players)
    }
}

class Player {
    _tau = 0; defaultRating = 1500; volatility_algorithm: (v: any, delta: any) => number | undefined = undefined; id = ""; adv_ranks: number[] = []; adv_rds: number[] = []; outcomes = []; __rating = 0; __rd = 0; __vol = 0;

    getRating = function () { return this.__rating * scalingFactor + this.defaultRating; };
    setRating = function (rating) { this.__rating = (rating - this.defaultRating) / scalingFactor; };
    getRd = function () { return this.__rd * scalingFactor; };
    setRd = function (rd) { this.__rd = rd / scalingFactor; };
    getVol = function () { return this.__vol; };
    hasPlayed = function () { return this.outcomes.length > 0; };
    _preRatingRD = function () { this.__rd = Math.sqrt(Math.pow(this.__rd, 2) + Math.pow(this.__vol, 2)); };

    addResult = function (opponent: Player, outcome) {
        this.adv_ranks.push(opponent.__rating);
        this.adv_rds.push(opponent.__rd);
        this.outcomes.push(outcome);
    };

    update_rank = function () {
        if (!this.hasPlayed()) { this._preRatingRD(); return; }
        const v: number = this._variance();
        const delta = this._delta(v);
        this.__vol = this.volatility_algorithm(v, delta);
        this._preRatingRD();
        this.__rd = 1 / Math.sqrt((1 / Math.pow(this.__rd, 2)) + (1 / v));

        let tempSum = 0;
        for (let i = 0, len = this.adv_ranks.length; i < len; i++) {
            tempSum += this._g(this.adv_rds[i]) * (this.outcomes[i] - this._E(this.adv_ranks[i], this.adv_rds[i]));
        }
        this.__rating += Math.pow(this.__rd, 2) * tempSum;
    };

    _variance = function (): number {
        let tempSum = 0;
        for (let i = 0, len = this.adv_ranks.length; i < len; i++) {
            let tempE = this._E(this.adv_ranks[i], this.adv_rds[i]);
            tempSum += Math.pow(this._g(this.adv_rds[i]), 2) * tempE * (1 - tempE);
        }
        return 1 / tempSum;
    };

    // The Glicko E function.
    _E = function (p2rating: number, p2RD: number) { return 1 / (1 + Math.exp(-1 * this._g(p2RD) * (this.__rating - p2rating))); };

    predict = function (p2) {
        const diffRD = Math.sqrt(Math.pow(this.__rd, 2) + Math.pow(p2.__rd, 2));
        return 1 / (1 + Math.exp(-1 * this._g(diffRD) * (this.__rating - p2.__rating)));
    };

    _g = function (RD: number) { return 1 / Math.sqrt(1 + 3 * Math.pow(RD, 2) / Math.pow(Math.PI, 2)); };

    // The delta function of the Glicko2 system.
    // Calculation of the estimated improvement in rating (step 4 of the algorithm)
    _delta = function (v: number) {
        let tempSum = 0;
        for (let i = 0, len = this.adv_ranks.length; i < len; i++) {
            tempSum += this._g(this.adv_rds[i]) * (this.outcomes[i] - this._E(this.adv_ranks[i], this.adv_rds[i]));
        }
        return v * tempSum;
    };

    _makef = function (delta: number, v: number, a: number) {
        return function (x) {
            return Math.exp(x)
                * (Math.pow(delta, 2)
                    - Math.pow(this.__rd, 2)
                    - v - Math.exp(x)) / (2 * Math.pow(Math.pow(this.__rd, 2)
                        + v + Math.exp(x), 2)) - (x - a) / Math.pow(this._tau, 2);
        };
    };
    constructor(rating: number, rd: number, vol: number, tau: number, default_rating: number, volatility_algorithm: (v: any, delta: any) => number, id: string) {
        this.volatility_algorithm = volatility_algorithm; this.setRating(rating); this.setRd(rd);
        this._tau = tau; this.__rating = rating; this.__rd = rd; this.__vol = vol; this.id = id; this.defaultRating = default_rating;
    }
}

class Glicko2 {
    settings: Partial<{ _tau: number, rd: number, vol: number, volatility_algorithm: any, rating: number }> = {};
    _default_rating = 1500 || this.settings.rating;
    _default_rd = this.settings.rd || 350;
    _tau: number = 0.5;
    _default_vol = this.settings.vol || 0.06;
    _volatility_algorithm = volatility_algorithms[this.settings.volatility_algorithm || 'newprocedure'];
    players = [];
    players_index = 0;

    constructor(settings: Partial<{ _tau: number, rd: number, vol: number, volatility_algorithm: any, rating: number }>) { this.settings = settings; this._tau = settings._tau }

    makeRace = function (results) { return new Race(results) };
    removePlayers = function () { this.players = []; this.players_index = 0; };
    getPlayers = function () { return this.players; };
    cleanPreviousMatches = function () {
        for (let i = 0, len = this.players.length; i < len; i++) {
            this.players[i].adv_ranks = [];
            this.players[i].adv_rds = [];
            this.players[i].outcomes = [];
        }
    };

    calculatePlayersRatings = function () {
        const keys = Object.keys(this.players);
        for (let i = 0, len = keys.length; i < len; i++) { this.players[keys[i]].update_rank(); }
    };

    addMatch = function (player1, player2, outcome) {
        let pl1 = this._createInternalPlayer(player1.rating, player1.rd, player1.vol, player1.id);
        let pl2 = this._createInternalPlayer(player2.rating, player2.rd, player2.vol, player2.id);
        this.addResult(pl1, pl2, outcome);
        return { pl1: pl1, pl2: pl2 };
    };

    makePlayer = function (rating, rd, vol) {
        //We do not expose directly createInternalPlayer in order to prevent the assignation of a custom player id whose uniqueness could not be guaranteed
        return this._createInternalPlayer(rating, rd, vol);
    };

    _createInternalPlayer = function (rating, rd, vol, id) {
        if (id === undefined) { id = this.players_index; this.players_index = this.players_index + 1; }
        else { const candidate = this.players[id]; if (candidate !== undefined) { return candidate; } }
        const player = new Player(rating || this._default_rating, rd || this._default_rd, vol || this._default_vol,
            this._tau, this._default_rating, this._volatility_algorithm, id);

        this.players[id] = player;
        return player;
    };
    addResult = function (player1: Player, player2: Player, outcome: number) { player1.addResult(player2, outcome); player2.addResult(player1, 1 - outcome); };
    updateRatings = function (matches) {
        if (matches instanceof Race) { matches = matches.matches; }
        if (typeof (matches) !== 'undefined') {
            this.cleanPreviousMatches();
            for (let i = 0, len = matches.length; i < len; i++) { const match = matches[i]; this.addResult(match[0], match[1], match[2]); }
        }
        this.calculatePlayersRatings();
    };
    predict = function (player1, player2) { return player1.predict(player2); };
}

const volatility_algorithms = {
    oldprocedure: function (v, delta) {
        const sigma = this.__vol;
        const phi = this.__rd;
        const tau = this._tau;
        let a, x1, x2, x3, y1, y2, y3, upper;
        let result;

        upper = find_upper_falsep(phi, v, delta, tau);
        a = Math.log(Math.pow(sigma, 2));
        y1 = equation(phi, v, 0, a, tau, delta);
        if (y1 > 0) { result = upper; }
        else {
            x1 = 0; x2 = x1; y2 = y1; x1 = x1 - 1; y1 = equation(phi, v, x1, a, tau, delta);
            while (y1 < 0) { x2 = x1; y2 = y1; x1 = x1 - 1; y1 = equation(phi, v, x1, a, tau, delta); }
            for (let i = 0; i < 21; i++) {
                x3 = y1 * (x1 - x2) / (y2 - y1) + x1;
                y3 = equation(phi, v, x3, a, tau, delta);
                if (y3 > 0) { x1 = x3; y1 = y3; }
                else { x2 = x3; y2 = y3; }
            }
            if (Math.exp((y1 * (x1 - x2) / (y2 - y1) + x1) / 2) > upper) { result = upper; }
            else { result = Math.exp((y1 * (x1 - x2) / (y2 - y1) + x1) / 2); }
        }
        return result;

        function new_sigma(sigma, phi, v, delta, tau) {
            const a = Math.log(Math.pow(sigma, 2));
            let x = a;
            let old_x = 0;
            while (x != old_x) {
                old_x = x;
                const d = Math.pow(phi, 2) + v + Math.exp(old_x);
                const h1 = -(old_x - a) / Math.pow(tau, 2) - 0.5 * Math.exp(old_x) / d + 0.5 * Math.exp(old_x) * Math.pow((delta / d), 2);
                const h2 = -1 / Math.pow(tau, 2) - 0.5 * Math.exp(old_x) * (Math.pow(phi, 2) + v) / Math.pow(d, 2) + 0.5 * Math.pow(delta, 2) * Math.exp(old_x) * (Math.pow(phi, 2) + v - Math.exp(old_x)) / Math.pow(d, 3);
                x = old_x - h1 / h2;
            }
            return Math.exp(x / 2);
        }

        function equation(phi: number, v: number, x: number, a: number, tau: number, delta: number) {
            const d = Math.pow(phi, 2) + v + Math.exp(x);
            return -(x - a) / Math.pow(tau, 2) - 0.5 * Math.exp(x) / d + 0.5 * Math.exp(x) * Math.pow((delta / d), 2);
        }

        function new_sigma_bisection(sigma, phi, v, delta, tau) {
            let a, x1, x2, x3;
            a = Math.log(Math.pow(sigma, 2));
            if (equation(phi, v, 0, a, tau, delta) < 0) {
                x1 = -1;
                while (equation(phi, v, x1, a, tau, delta) < 0) { x1 = x1 - 1; }
                x2 = x1 + 1;
            }
            else {
                x2 = 1;
                while (equation(phi, v, x2, a, tau, delta) > 0) { x2 = x2 + 1; }
                x1 = x2 - 1;
            }

            for (let i = 0; i < 27; i++) {
                x3 = (x1 + x2) / 2;
                if (equation(phi, v, x3, a, tau, delta) > 0) { x1 = x3; }
                else { x2 = x3; }
            }
            return Math.exp((x1 + x2) / 4);
        }

        function Dequation(phi: number, v: number, x: number, tau: number, delta: number) {
            const d = Math.pow(phi, 2) + v + Math.exp(x);
            return -1 / Math.pow(tau, 2) - 0.5 * Math.exp(x) / d + 0.5 * Math.exp(x) * (Math.exp(x) + Math.pow(delta, 2)) / Math.pow(d, 2) - Math.pow(Math.exp(x), 2) * Math.pow(delta, 2) / Math.pow(d, 3);
        }

        function find_upper_falsep(phi: number, v: number, delta: number, tau: number) {
            let x1: number, x2: number, x3: number, y1: number, y2: number, y3: number;
            y1 = Dequation(phi, v, 0, tau, delta);
            if (y1 < 0) { return 1; }
            else {
                x1 = 0; x2 = x1; y2 = y1; x1 = x1 - 1; y1 = Dequation(phi, v, x1, tau, delta);
                while (y1 > 0) { x2 = x1; y2 = y1; x1 = x1 - 1; y1 = Dequation(phi, v, x1, tau, delta); }
                for (let i = 0; i < 21; i++) {
                    x3 = y1 * (x1 - x2) / (y2 - y1) + x1;
                    y3 = Dequation(phi, v, x3, tau, delta);
                    if (y3 > 0) { x1 = x3; y1 = y3; }
                    else { x2 = x3; y2 = y3; }
                }
                return Math.exp((y1 * (x1 - x2) / (y2 - y1) + x1) / 2);
            }
        }
    },
    newprocedure: function (v: number, delta: number) {
        let A = Math.log(Math.pow(this.__vol, 2));
        let f = this._makef(delta, v, A);
        let epsilon = 0.0000001;

        let B: undefined | number;
        let k: undefined | number;
        if (Math.pow(delta, 2) > Math.pow(this.__rd, 2) + v) { B = Math.log(Math.pow(delta, 2) - Math.pow(this.__rd, 2) - v); }
        else {
            k = 1;
            while (f(A - k * this._tau) < 0) { k = k + 1; }
            B = A - k * this._tau;
        }
        let fA = f(A); let fB = f(B); let C, fC;
        while (Math.abs(B - A) > epsilon) {
            C = A + (A - B) * fA / (fB - fA);
            fC = f(C);
            if (fC * fB <= 0) { A = B; fA = fB; }
            else { fA = fA / 2; }
            B = C;
            fB = fC;
        }
        return Math.exp(A / 2);
    },
    newprocedure_mod: function (v: number, delta: number) {
        let A = Math.log(Math.pow(this.__vol, 2));
        const f = this._makef(delta, v, A);
        const epsilon = 0.0000001;
        let B: number, k: number;
        if (delta > Math.pow(this.__rd, 2) + v) { B = Math.log(delta - Math.pow(this.__rd, 2) - v); }
        else {
            k = 1;
            while (f(A - k * this._tau) < 0) { k = k + 1; }
            B = A - k * this._tau;
        }

        let fA = f(A); let fB = f(B);
        let C, fC;
        while (Math.abs(B - A) > epsilon) {
            C = A + (A - B) * fA / (fB - fA);
            fC = f(C);
            if (fC * fB < 0) { A = B; fA = fB; }
            else { fA = fA / 2; }
            B = C;
            fB = fC;
        }
        return Math.exp(A / 2);
    },
    oldprocedure_simple: function (v: number, delta: number) {
        let a = Math.log(Math.pow(this.__vol, 2)); let tau = this._tau; let x0 = a; let x1 = 0; let d, h1, h2;
        while (Math.abs(x0 - x1) > 0.00000001) {
            x0 = x1;
            d = Math.pow(this.__rating, 2) + v + Math.exp(x0);
            h1 = -(x0 - a) / Math.pow(tau, 2) - 0.5 * Math.exp(x0) / d + 0.5 * Math.exp(x0) * Math.pow(delta / d, 2);
            h2 = -1 / Math.pow(tau, 2) - 0.5 * Math.exp(x0) * (Math.pow(this.__rating, 2) + v) / Math.pow(d, 2) + 0.5 * Math.pow(delta, 2) * Math.exp(x0) * (Math.pow(this.__rating, 2) + v - Math.exp(x0)) / Math.pow(d, 3);
            x1 = x0 - (h1 / h2);
        }
        return Math.exp(x1 / 2);
    }
};

explanation on your example

Can you explain this example more please. It looks like 1700, 1400, 1550 are the players original ratings and 0.06 is the volatility. But what the values 300, 30, and 100 for and why are they different per player.

Bob = glicko.makePlayer(1400, 30, 0.06);
John = glicko.makePlayer(1550, 100, 0.06);

Handling bonus points

Let's say I wanted to factor in other measures, like man of the match or a high score record. Is there a good way to go about handling this in glicko?

[Suggestion] Change affect of races on rating, deviation, and volatility

To my knowledge Dr. Glickman doesn't talk about >2 person competitions at all, so the math behind the Race features is unique to this module. My understanding is that the way it's currently implemented, when people compete in a race the rating system treats that as if they each participated in a separate complete match with each other player.

This implies that if a player loses a game with 7 other competitors, it would be the same as losing to each of those competitors individually. While I would agree that losing a game with 7 competitors should be more impactful than losing 1 game to 1 competitor, I don't think it's the same as losing 7 separate games to single competitors. It ignores volatility and applies way too much confidence for a single performance.

I don't have a great solution for this. It seems like it would take a modification to the Glicko-2 algorithm itself to make this accurate. It "feels" like it should be akin to losing 7 games for how it affects your score, but should only affect your volatility as a single game would. Confidence would maybe be somewhere in the middle? I'm not sure.

If someone who has a better grasp of the algorithm itself and how all the pieces fits together has an idea of how this should work, it would be great if you chimed in.

What would be helpful in the interim would be some kind of dynamic weighting that could be passed into a match that would reduce it's overall affect on the results. Either something global, like "matchWeight" or something more granular, like "ratingWeight", "rdWeight", and "volWeight". These could be globally configured before every match, or passed into the Race and match objects.

If none of that, I'll probably need to do some kind of post processing hack, or download and edit the source rather than using the package, neither of which are great.

Is Glicko usable for team games?

Hi!
This is not really an issue, sorry... :-)

Is Glicko usable for team games? I mean, is there a possibility to take into account the strength of each player's team-mates, when calculating new ranking after a tournament?

I did also try Trueskill (https://www.npmjs.org/package/trueskill), which implements Trueskill algorithm from Microsoft, but it seem oriented to one-to-one games, too... :-(

doubts about volatility computation

Hi Henri,
upon testing this lib I've come across issues regarding the computation of the volatility parameter, namely that

  1. adaptions are usually wayyy too small to be reasonable, and
  2. one can create scenarios in which the parameter gets messed up completely by the algorithms.

(Everything that follows is under the assumption that I didn't mess something up... in which case I'd appreciate a quick heads up)

Basically what I did was play a single rating period, in which a very strong and very weak low-RD player with standard volatility compete N times, and the weak one always wins. This should lead to a jump in volatility for the strong player.
I've checked against the spreadsheet based calculator http://www.bjcox.com/?page_id=20&did=11 which works like a charm and gives reasonable values for the volatility. With glicko2js, the following happens, depending on the number N.

  • N < 30: the volatility is adapted, but only in decimal places that really don't matter (173*volatility should be of a magnitude sufficient to influence the RD). For N=29, for instance, we end up with about 0.08, but the spreadsheet suggests 0.3! Your test using the example from Glickman's PDF doesn't see this issue, as there the correct adjustment is already almost zero. In any case I would suggest adding a test where the volatility actually sees an update, for instance this one!
  • N is 30. Suddenly the volatility gets messed up completely, so there's definitely something wrong:
    Mary new rating: -7643,
    Mary new rating deviation: 235,
    Mary new volatility: 6.199250862403905,

I've used the below code.

var glicko2 = require('glicko2'),
    ranking = new glicko2.Glicko2(),
    Mary = ranking.makePlayer(1700, 30, 0.06),
    Bob = ranking.makePlayer(1000, 30, 0.06),
    matches = [],
    num = 10,
    i;
console.log('Mary is high-ranked. Bob is low-ranked. Both have small RD.')
console.log("Mary rating: " + Mary.getRating());
console.log("Mary rating deviation: " + Mary.getRd());
console.log("Mary volatility: " + Mary.getVol());
console.log("Bob rating: " + Bob.getRating());
console.log("Bob rating deviation: " + Bob.getRd());
console.log("Bob volatility: " + Bob.getVol());
for (i = 0; i < num; i += 1) {
    matches.push([Mary, Bob, 0]);
}
console.log('Mary lost against Bob %d times in a row.', num);

ranking.updateRatings(matches);

console.log("Mary new rating: " + Mary.getRating());
console.log("Mary new rating deviation: " + Mary.getRd());
console.log("Mary new volatility: " + Mary.getVol());

// num = 30: volatility goes crazy
// num < 30: volatility doesn't go crazy (<0.08) but in fact is too low
// num ~ 10: volatility stays so close to 0.06 it's not even funny. It should be around 0.3!

Does not clear "matches" array for subsequent tournaments

The bug can be seen in the example "index.html".

The first tournaments results are correct. But in subsequent tournaments it reapplies the first tournament results (as if the matches were repeated), resulting in incorrect rating calculation.

The issue can be clearly seen by having a 2nd tournament without matches, but with a rating update.

UPDATE: After looking more into it, the issue may be due to not clearing outcomes after each tournament.

Add support for prediction?

Hi!

Thanks for this! Haven't fully tested it yet, but seems to be working pretty well.

I was wondering if you considered adding a function that calculates the estimated outcome of a match based on the data available. Basically you provide the pairing and it returns the expected outcome, which is similar to the E() function, but not quite. Found this in an r/chess thread that looks good:
Let P2 = Expected outcome for player 2. Then:
P2 = 1 / (1 + e-A)
with
A = g(sqrt(r12+r22)) * (s2-s1))
and
g(x) = 1/sqrt(1+3x2/pi2)

Best,
Mariano.

Issues when reaching around 200 games

Hi. I am currently using this package to calculate player rating after matches (5v5). This works splendid, except when a player is closing in on around 200 games played. It then swings wildly, to the point that it will overflow an int after 1-3 games. Any idea as to what might cause this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.