UNPKG

@graperank/calculator

Version:

The GrapeRank Calculator module, calculates scorecards from input ratings.

353 lines (352 loc) 14.4 kB
"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; }