@graperank/calculator
Version:
The GrapeRank Calculator module, calculates scorecards from input ratings.
353 lines (352 loc) • 14.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Calculator = void 0;
const util_1 = require("@graperank/util");
// var params : Required<CalculatorParams>
class Calculator {
observer;
ratings;
updateStatus;
updateComplete;
constructor(observer, ratings, params, updateStatus, updateComplete) {
this.observer = observer;
this.ratings = ratings;
this.updateStatus = updateStatus;
this.updateComplete = updateComplete;
if (params)
this.params = { ...this.params, ...params };
}
params = {
// incrementally decrease influence weight
attenuation: .5,
// factor for calculating confidence
// MUST be bellow 1 or confidence will ALWAYS be 0
// CAUTION : too high (eg:.7) and users beyond a certain DOS (eg:2) will always have a score of zero
rigor: .5,
// minimum score ABOVE WHICH scorecard will be included in output
minscore: 0,
// max difference between calculator iterations
// ZERO == most precise
precision: 0.00001,
// devmode if off by default
devmode: false
};
calculators = new Map();
_stopped = false;
stop() {
this._stopped = true;
}
/**
* Calculate new scorecards from interpreted ratings and input scorecards
*/
async calculate() {
console.log("GrapeRank : Calculator : instantiated with ", this.ratings.length, " ratings and params : ", this.params);
// setup
// STEP A : initialize ratee scorecard
// Retrieve or create a ScorecardCalculator for each ratee in ratings
for (let r in this.ratings) {
if (!this.ratings[r])
continue;
let ratee = this.ratings[r].ratee;
let rater = this.ratings[r].rater;
if (ratee && !this.calculators.get(ratee)) {
// TODO get scores for each rater from worldview input cards
// let ratercard = getInputScorecard(rater as string)
let calculator = new ScorecardCalculator(this.observer, ratee, this.params);
if (calculator)
this.calculators.set(ratee, calculator);
}
}
console.log("GrapeRank : Calculator : setup with ", this.calculators.size, " calculators.");
await this.iterate();
if (this.updateComplete)
await this.updateComplete();
return this.scorecards;
}
// returns number of scorecards calculated
async iterate() {
let calculating = 0;
let calculated = 0;
let uncalculated;
let notcalculatedwarning = 0;
let prevcalculating = 0;
let prevcalculated = 0;
let iteration = 0;
let iterationscores = [];
let iterationstatus;
while (calculated < this.calculators.size) {
if (this._stopped)
return undefined;
iteration++;
prevcalculating = calculating;
prevcalculated = calculated;
calculating = 0;
calculated = 0;
uncalculated = [];
iterationstatus = {};
console.log("------------ BEGIN ITERATION : ", iteration, " --------------------");
// STEP B : calculate sums
// Add rater's rating to the sum of weights & products for the ratee scorecard
for (let r in this.ratings) {
if (!this.ratings[r])
continue;
let calculator = this.calculators.get(this.ratings[r].ratee);
let raterscore = this.calculators.get(this.ratings[r].rater)?.score;
if (calculator) {
calculator.sum(this.ratings[r], raterscore);
}
}
// STEP C : calculate influence
// calculate final influence and confidence for each ratee scorecard
// call calculate again if calculation is NOT complete
this.calculators.forEach((calculator, rater) => {
var dos = calculator.dos || 0;
iterationstatus[dos] = iterationstatus[dos] || {
calculated: 0,
uncalculated: 0,
average: 0
};
if (!calculator.calculated) {
calculator.calculate();
calculating++;
}
if (calculator.calculated) {
calculated++;
// add to dos status for calculated
iterationstatus[dos].calculated++;
// DOS average is SUM of all calculated scores UNTIL converted to an average
iterationstatus[dos].average += calculator.score;
}
else {
uncalculated.push(rater);
// add to dos status for uncalculated
iterationstatus[dos].uncalculated++;
}
});
// calculate averages scores for each DOS status
for (var dos in iterationstatus) {
iterationstatus[dos].average = iterationstatus[dos].average / iterationstatus[dos].calculated;
}
if (this.updateStatus)
await this.updateStatus({ ...iterationstatus });
// LOG iteration
iterationscores = logScoresForIteration(this.calculators);
console.log("TOTAL number scorecards : ", this.calculators.size);
console.log("TOTAL scorecards calculating this iteration : ", calculating);
console.log("TOTAL scorecards calculated : ", calculated);
// halt iterator if needed
if (uncalculated.length) {
if (calculated == prevcalculated && calculating == prevcalculating) {
notcalculatedwarning++;
console.log("WARNING ", notcalculatedwarning, " : scores did not change for ", calculating, " scorecards in calculate()");
if (notcalculatedwarning > 4) {
console.log("HALTING iterator : due to unchanging scores for the following raters : ", uncalculated);
calculated = this.calculators.size;
}
}
if (iteration > 100) {
console.log("HALTING iterator : exeded MAX 100 iterations in calculate() ");
calculated = this.calculators.size;
}
}
console.log("------------ END ITERATION : ", iteration, " --------------------");
}
return calculated;
}
get scorecards() {
let scorecards = [];
this.calculators.forEach((calculator) => {
if (calculator.output)
scorecards.push(calculator.output);
});
// sort first : scorecards with higher scores and most ratings
return scorecards.sort((a, b) => {
return a[1].score - b[1].score || a[1].interpretersums['nostr-follows']?.numRatings - b[1].interpretersums['nostr-follows']?.numRatings;
});
}
}
exports.Calculator = Calculator;
const zerosums = {
weights: 0,
products: 0
};
/**
* Calculates a single scorecard for a given subject (ratee)
*/
class ScorecardCalculator {
observer;
params;
get output() {
if (!this.calculated || this._data.score < this.params.minscore)
return undefined;
return [this._subject, this._data];
}
// get scorecard() : Required<Scorecard> | undefined {
// // if(!this.calculated) return undefined
// return {
// ...this.keys,
// subject : this._subject,
// ...this._data
// }
// }
// sum() can only be run as many times as we have ratings
// get summed(){ return this._sumcount < ratings.length ? false : true }
get calculated() {
return this._calculated ? true : false;
}
get dos() {
// TODO interpreter should designate a protocol that determines DOS
// using 'nostr-follows' for now
return this._meta.get('nostr-follows')?.dos || 0;
}
get score() {
return this._data?.score || 0;
}
// constructor(subject : elemId)
// constructor(scorecard : Scorecard)
constructor(observer, input, params) {
this.observer = observer;
this.params = params;
// input is subject of new scorecard
this._subject = typeof input == 'string' ? input : input.subject;
}
// STEP B : calculate sums
// calculate sum of weights & sum of products
sum(rating, raterscore) {
// // do nothing if ratercard.subject does not match rating.rater
// if(ratercard && ratercard.subject != rating.rater){
// console.log("GrapeRank : ScorecardCalculator : WARNING ratercard.subject does not match rating.rater")
// return
// }
// ALWAYS run calculater ... to assure score convergence for "more distant" dos
// if(this.calculated) {
// return
// }
// determine rater influence
let influence = rating.rater == this.observer ? 1 : raterscore || 0;
let weight = influence * rating.confidence;
// no attenuation for observer
if (rating.rater != this.observer)
weight = weight * (this.params.attenuation);
// add to sums
this._sums.weights += weight;
this._sums.products += weight * rating.score;
// get the metadata entry for this protocol
let protocolmeta = this._meta.get(rating.protocol);
// create new metadata entry for this protocol, using existing values as available
protocolmeta = {
// dos = the minimum nonzero iteration number for ratings used to calculate this scorecard
dos: rating.dos && protocolmeta?.dos && rating.dos < protocolmeta.dos ?
rating.dos : protocolmeta?.dos || rating.dos,
// weighted = weighted sum of protocol ratings calculated in this scorecard
weighted: weight + (protocolmeta?.weighted || 0),
// numRatings = number of protocol ratings for this subject
numRatings: 1 + (protocolmeta?.numRatings || 0),
// numRatedBy = number of protocol ratings for observer by this subject
numRatedBy: (rating.ratee == this.observer ? 1 : 0) + (protocolmeta?.numRatedBy || 0)
};
// assure that the metadata entry is updated for this protocol, in case it was undefined before.
this._meta.set(rating.protocol, protocolmeta);
// DEBUG
if (this._subject == util_1.DEBUGTARGET) {
console.log('DEBUGTARGET : calculator._sums for target : ', this._sums);
console.log('DEBUGTARGET : calculator._meta for target : ', this._meta);
}
}
// STEP C : calculate influence
// returns true if score was updated
calculate() {
if (this._subject == util_1.DEBUGTARGET) {
console.log('DEBUGTARGET : caling calculator.calculate() for target : ');
}
// ALWAYS run calculater ... to assure score convergence for "more distant" dos
// if(this.calculated) return true
// calculate score
let confidence = 0;
let score = 0;
let interpretersums = {};
// convert metadata map to pojo
this._meta.forEach((scorecardmeta, protocol) => interpretersums[protocol] = scorecardmeta);
// If weights == 0 then confidence and score will also be 0
if (this._sums.weights > 0) {
// STEP D : calculate confidence
confidence = this.confidence;
score = this._average * confidence;
}
// determine if calculator iterations are complete based on calculator.precision
// ONLY after scores have been calculated at least ONCE (if `this._claculated` has been set)
this._calculated = this._calculated === undefined ? false
: Math.abs(score - this._data.score) <= this.params.precision ? true : false;
// zero the sums
this._sums = { ...zerosums };
// output the scorecard
this._data = { confidence, score, interpretersums };
return this.calculated;
}
_calculated;
_subject;
_data;
_meta = new Map();
// TODO refactor this._sums as this._input in the format of scorecard.input
_sums = { ...zerosums };
get _average() {
let average = 0;
try {
average = this._sums.products / this._sums.weights;
}
catch (e) { }
return average <= 0 ? 0 : average;
}
// STEP D : calculate confidence
get confidence() {
// TODO get default rigor
let rigor = this.params.rigor !== undefined ? this.params.rigor : .25;
const rigority = -Math.log(rigor);
const fooB = -this._sums.weights * rigority;
const fooA = Math.exp(fooB);
const certainty = 1 - fooA;
return certainty.toPrecision(4); // FIXME
}
}
// LOG iteration
function logScoresForIteration(calculators) {
let scorecards = [];
let increment = .1;
let scores;
let v = "", ov = "";
calculators.forEach((calculator) => {
if (calculator.output)
scorecards.push(calculator.output[1]);
});
scores = countScorecardsByScore(scorecards, increment);
// console.log("scores counted", scores)
for (let i in scores) {
ov = v || "0";
v = (i * increment).toPrecision(2);
console.log("number of cards having scores from " + ov + " to " + v + " = ", scores[i]);
}
return scores;
}
function countScorecardsByScore(scorecards, increment) {
let grouped = groupScorecardsByScore(scorecards, increment);
let count = [];
let index = 0;
for (let g in grouped) {
count[index] = grouped[g].length;
index++;
}
return count;
}
function groupScorecardsByScore(scorecards, increment) {
let group = [];
for (let s in scorecards) {
let card = scorecards[s];
if (card?.score != undefined) {
let groupid = Math.floor(card.score / increment);
if (!group[groupid])
group[groupid] = [];
group[groupid].push(card);
}
}
return group;
}