UNPKG

comparative-judgement

Version:

Comparative Judgement Algorithms

348 lines (307 loc) 9.2 kB
/*jshint -W083 */ var _ = require('underscore'); var btmModel = function(dat, fixTheta, maxIter, conv, eps, callback){ if(!maxIter) maxIter = 100; if(!conv) conv = 0.0001; if(!eps) eps = 0.3; var start = new Date().getTime(); var delta = -99; var centerTheta = true; // Sorted array of teams var teams = uniqTeams(dat); // Fixed theta var anchorIn; if(fixTheta){ centerTheta=false; anchorIn = fixedThetaIndex(fixTheta, teams); } // Number of teams var TP = teams.length; // Winning teams var chosenTeams = _.pluck(dat, 'chosen'); var dat01 = teamIndex(teams, chosenTeams); var wins = calcScores(dat01, TP); // Losing teams var notChosenTeams = _.pluck(dat, 'notChosen'); var dat02 = teamIndex(teams, notChosenTeams); var losses = calcScores(dat02, TP); // Number of matches var maxscore = addVecs(wins, losses); // Epsilon adjustment var score = epsilonAdjustment(wins, maxscore,eps); var propScores = propScore(score, maxscore); // Initial estimate var theta = qlogis(propScores); // number of dyads var ND = dat01.length; var maxChange = 100000; var iter = 0; var incrfac = 0.98; var maxincr = 1; var seEta; var seTh; while( ( iter < maxIter ) & ( maxChange > conv) ) { var theta0 = theta.slice(0); var M11 = matchToTheta(dat01, theta); var M12 = matchToTheta(dat02, theta); var M13 = calcM13(M11, M12, delta); var expM11 = expVec(M11); var expM12 = expVec(M12); var expM13 = expVec(M13); var rowsums = rowSums(expM11,expM12,expM13); var dexpM11 = divVecs(expM11,rowsums); var dexpM12 = divVecs(expM12,rowsums); var dexpM13 = divVecs(expM13,rowsums); maxincr = maxincr * incrfac; // derivatives with respect to theta // first derivative var fac1 = fac(dexpM11,dexpM13); var fac2 = fac(dexpM12,dexpM13); var h1 = groupSum(fac1, dat01, TP); var h2 = groupSum(fac2, dat02, TP); var d1 = calcD(h1, h2, score); // Second derivative var d2a = secondDeriv(dexpM11, dexpM13); var d2b = secondDeriv(dexpM12, dexpM13); var h21 = groupSum(d2a, dat01, TP); var h22 = groupSum(d2b, dat02, TP); var d2 = calcD2(h21,h22); var incr = divVecs(d1,d2); incr = changeInc(incr, maxincr); theta = addVecs(theta, incr); seTh = seTheta(d2); if(centerTheta) theta = centTheta(theta); if(fixTheta){ for(var j=0; j <theta.length; j++){ if(anchorIn[j]){ theta[j]=anchorIn[j]; seTh[j] = 0; } } } // assess convergence iter++; maxChange = thetaChange(theta, theta0); //console.log("**** Iteration ", iter, " | Maximum parameter change = ", maxChange); } var end = new Date().getTime(); var time = end - start; //console.log('estimation time: ', time); var out = []; for(var i=0; i<theta.length; i++){ out.push({team:teams[i], theta: theta[i], seTheta:seTh[i], wins:wins[i], losses:losses[i], matches:maxscore[i], score:score[i],propScore:propScores[i]}); } return(callback(null, out)); }; var uniqTeams = function(dat){ // Create sorted array of teams var chosenTeams = _.pluck(dat, 'chosen'); var notChosenTeams = _.pluck(dat, 'notChosen'); var allTeams = _.union(chosenTeams, notChosenTeams); return allTeams.sort(); }; var teamIndex = function(teams, colData){ var teamIndices =_.map(colData, function(team) {return(_.indexOf(teams, team));}); return teamIndices; }; var calcScores = function(dat,nTeams){ var scores = []; for(var i=0; i<nTeams; i ++){ scores.push(0); } for(i = 0; i< dat.length; i++) { var num = dat[i]; scores[num] = scores[num]+1; } return scores; }; var epsilonAdjustment = function(scores, maxScores,eps){ var epsA = []; for(var i=0; i<scores.length; i++){ epsA[i] = eps + (maxScores[i]-2*eps)*scores[i] / maxScores[i]; } return epsA; }; var propScore = function(scores, maxScores){ var propScores = []; for(var i=0; i<scores.length; i++){ propScores.push(scores[i] / maxScores[i]); } return propScores; }; var qlogis = function(propscore){ var theta = []; for(var i=0; i<propscore.length; i++){ p = propscore[i]; theta.push(Math.log(p/(1-p))); } return theta; }; var matchToTheta = function(matches, theta){ var thetaVec = []; for(var i=0; i<matches.length; i ++){ thetaVec.push(0); } for(i=0; i<matches.length; i ++){ thetaVec[i] = theta[matches[i]]; } return thetaVec; }; var calcM13 = function(M11, M12, delta){ var M13 = []; for(var i=0; i<M11.length; i++){ M13.push( delta + (M11[i] + M12[i])/2); } return M13; }; var expVec = function(vec){ var out = []; for(var i=0; i<vec.length; i++){ out.push(Math.exp(vec[i])); } return out; }; var rowSums = function(expM11, expM12, expM13){ var rowsums = []; for(var i=0;i<expM11.length;i++){ rowsums.push(expM11[i] + expM12[i] + expM13[i]); } return rowsums; }; var divVecs = function(dat1, dat2){ var out = []; for(var i=0; i<dat1.length;i++){ out.push(dat1[i]/dat2[i]); } return out; }; var addVecs = function(dat1, dat2){ var out = []; for(var i=0; i<dat1.length;i++){ out.push(dat1[i]+dat2[i]); } return out; }; var thetaChange = function(theta, theta0){ var change = []; for(var i=0; i<theta.length;i++){ change.push(Math.abs(theta[i] - theta0[i])); } var mx = Math.max.apply( Math, change ); return mx; }; var fac = function(M11, M13){ var out = []; for(var i=0; i<M11.length;i++){ out.push(M11[i] + M13[i] / 2); } return out; }; var groupSum = function(vals, grp, TP){ var out = []; for(var i =0; i<TP; i++){ out.push(0); } for(i =0; i<vals.length; i++){ out[grp[i]] += vals[i]; } return out; }; var calcD = function(h1, h2, score){ var out = []; for(var i=0; i<h1.length; i++){ out.push(score[i]-(h1[i] + h2[i])); } return out; }; var calcD2 = function(h1, h2){ var out = []; for(var i=0; i<h1.length; i++){ out.push(h1[i] + h2[i] + 0.000000000000000000001); } return out; }; var seTheta = function(d2){ var out = []; for(var i=0; i<d2.length; i++){ out.push(Math.sqrt(1/d2[i])); } return out; }; var secondDeriv = function(col1, col2){ var out = []; var d2; for(var i=0; i<col1.length; i++){ d2 = col1[i] * (1 - col1[i] - col2[i] / 2 ) + col2[i]/2 * (1/2 - col1[i] - col2[i] / 2 ); out.push(d2); } return out; }; var changeInc = function(incr, maxincr){ var out=[]; for(var i=0;i<incr.length; i++){ if(Math.abs(incr[i])>maxincr){ out.push(maxincr*sign(incr[i])); } else { out.push(incr[i]); } } return out; }; var sign = function(val){ if(val>0){ return 1; } if(val<0){ return -1; } if(val===0){ return 0; } }; var centTheta = function(theta){ var out = []; var sum = theta.reduce(function(a, b) { return a + b; }); var avg = sum / theta.length; for(var i=0; i<theta.length; i++){ out.push(theta[i] - avg); } return out; }; var fixedThetaIndex = function(fixed, teams){ var out = []; for(var i=0; i<teams.length; i++){ out.push(null); } for(i =0; i<fixed.length; i++){ indx = _.findIndex(teams, function(team){return(team==fixed[i].team);}); out[indx]=fixed[i].theta; } return out; }; module.exports = { btmModel:btmModel, uniqTeams:uniqTeams, teamIndex:teamIndex, calcScores:calcScores, epsilonAdjustment: epsilonAdjustment, propScore: propScore, qlogis:qlogis, matchToTheta: matchToTheta, calcM13: calcM13, expVec: expVec, rowSums: rowSums, divVecs: divVecs, fac: fac, groupSum: groupSum, calcD: calcD, calcD2: calcD2, secondDeriv: secondDeriv, changeInc: changeInc, addVecs: addVecs, seTheta: seTheta, centTheta: centTheta, thetaChange: thetaChange, fixedThetaIndex: fixedThetaIndex, };