comparative-judgement
Version:
Comparative Judgement Algorithms
348 lines (307 loc) • 9.2 kB
JavaScript
/*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,
};