UNPKG

bwsample

Version:

The JS implementation of the pypi package bwsample

634 lines (580 loc) 18.6 kB
/** * * @param {*} lil * @param {*} id1 * @param {*} id2 * @returns * * * Example: * var lil = undefined * lil = lilIncrement(lil, "la", "li"); * lil = lilIncrement(lil, "la", "li"); * lil = lilIncrement(lil, "la", "lu"); * console.log(lil) */ const lilIncrement = (lil, id1, id2) => { if (lil == undefined){ lil = {}; } if (lil[id1] == undefined){ lil[id1] = {}; } if (lil[id1][id2] == undefined){ lil[id1][id2] = 1; }else{ lil[id1][id2] += 1; } return lil; } /** * Add count values of two LIL objects together * * @param {*} a * @param {*} b * @returns */ const lilAdd = (a, b) => { var c = JSON.parse(JSON.stringify(a)); for(var id1 in b){ if(c[id1] === undefined){ c[id1] = JSON.parse(JSON.stringify(b[id1])); }else{ for (var id2 in b[id1]){ if(c[id1][id2] === undefined){ c[id1][id2] = parseInt(b[id1][id2]); }else{ c[id1][id2] += parseInt(b[id1][id2]); } } } } return c; } /** * Add count values of LIL object "b" to "a" * * @param {*} a * @param {*} b * @returns */ const lilAddInplace = (a, b) => { for(var id1 in b){ if(a[id1] === undefined){ a[id1] = JSON.parse(JSON.stringify(b[id1])); }else{ for (var id2 in b[id1]){ if(a[id1][id2] === undefined){ a[id1][id2] = parseInt(b[id1][id2]); }else{ a[id1][id2] += parseInt(b[id1][id2]); } } } } return undefined; } /** * Add count values of an list of LIL objects toegther * * @param {*} arr * @returns */ const lilMerge = (arr) => { var a = {}; for (var b of arr){ a = lilAdd(a, b); } return a; } // def count(evaluations: List[Tuple[List[ItemState], List[ItemID]]], // direct_dok: Optional[Dict[Tuple[ItemID, ItemID], int]] = None, // direct_detail: Optional[Dict[Tuple[ItemID, ItemID], int]] = None, // use_logical: Optional[bool] = True, // logical_dok: Optional[Dict[Tuple[ItemID, ItemID], int]] = None, // logical_detail: Optional[dict] = None, // logical_database: List[Tuple[List[ItemState], List[ItemID]]] = None, // ) -> ( // Dict[Tuple[ItemID, ItemID], int], // Dict[Tuple[ItemID, ItemID], int], // dict, // Dict[Tuple[ItemID, ItemID], int], // dict): // """Extract pairs from evaluated BWS sets // Parameters: // ----------- // evaluations : List[Tuple[List[ItemState], List[ItemID]]] // A list of new BWS sets to be evaluated. // direct_dok : Dict[Tuple[ItemID, ItemID], int] // (default: None) Previously recorded frequencies for all directly // extracted pairs. // direct_detail : dict // (default: None) Previously recorded frequencies for each type of // pair: "BEST>WORST" (bw), "BEST>NOT" (bn), "NOT>WORST" (nw) // use_logical : Optional[bool] = True // flag to deactivate logical inference // logical_dok : Optional[Dict[Tuple[ItemID, ItemID], int]] // The previous counts/frequencies of logical inferred pairs // that need to be updated. // logical_detail : Optional[dict] // A dictionary of previously stored DOKs for each variant of // logically inferred pairs. // logical_database : List[Tuple[List[ItemState], List[ItemID]]] // A database of previously processed BWS sets // Returns: // -------- // logical_dok: Optional[Dict[Tuple[ItemID, ItemID], int]] // The counts/frequencies of logical inferred pairs. // logical_detail: Optional[dict] // A dictionary that stores seperate DOKs for each variant of // logically inferred pairs. // Example: // -------- // import bwsample as bws // agg_dok, dir_dok, dir_detail, logi_dok, logi_detail = bws.count( // evaluations) // """ const count = (evaluations, direct_lil=undefined, direct_detail=undefined, use_logical=true, logical_lil=undefined, logical_detail=undefined, logical_database=undefined) => { // extract from each BWS set [direct_lil, direct_detail] = directExtractBatch( evaluations, direct_lil, direct_detail); // search for logical inferences if (use_logical){ [logical_lil, logical_detail] = logicalInferUpdate( evaluations, logical_database, logical_lil, logical_detail); } // merge agg_lil=direct_dok+logical_dok if (use_logical){ var agg_lil = lilAdd(logical_lil, direct_lil); }else{ var agg_lil = JSON.parse(JSON.stringify(direct_lil)); } // done return [agg_lil, direct_lil, direct_detail, logical_lil, logical_detail] } /** * Extract ">" Pairs from one evaluated BWS set * * @param {Array[ID]} stateids A list of IDs (e.g. uuid) corresponding to the `combostates` list. * @param {Array[Int]} combostates Combinatorial state variable. Each element of the list * - corresponds to an ID in the `stateids` list, * - represents an item state variable (or the i-th FSM), and * - can habe one of the three states: * - 0: NOT, unselected (initial state) * - 1: BEST * - 2: WORST * @param {JSON} agg Dictionary with counts for each ">" pair, e.g. an * entry `{..., ('B', 'C'): 1, ...} means `B>C` was * counted `1` times. * We can extract 3 types of pairs from 1 BWS set: * - "BEST > WORST" (see dok_bw) * - "BEST > NOT" (see dok_bn) * - "NOT > WORST" (see dok_nw) * The `dok` dictionary contains the aggregate counts of the * types of pairs. Use `dok_bw`, `dok_bn` and `dok_nw` for * attribution analysis. * @param {JSON} bw Dictionary with counts for explicit "BEST > WORST" pairs. * @param {JSON} bn Dictionary with counts for "BEST > NOT" pairs. * @param {JSON} nw Dictionary with counts for "NOT > WORST" pairs. * @returns agg, bw, bn, nw * * Example * // process the 1st evaluation * var stateids = ['A', 'B', 'C', 'D'] * var combostates = [0, 0, 2, 1] # BEST=1, WORST=2 * var [agg, bw, bn, nw] = directExtract(stateids, combostates); * // update with the next evaluation * stateids = ['D', 'E', 'F', 'A'] * combostates = [0, 1, 0, 2] * [agg, bw, bn, nw] = directExtract( * stateids, combostates, agg, bw, bn, nw); */ const directExtract = (stateids, combostates, agg=undefined, bw=undefined, bn=undefined, nw=undefined) => { // check args if (stateids.length != combostates.length){ throw new Error(`stateids.length='${stateids.length}' and combostates.length=${combostates.length} are not equal.`); } // set defaults if (agg === undefined){ agg = {}; } if (bw === undefined){ bw = {}; } if (bn === undefined){ bn = {}; } if (nw === undefined){ nw = {}; } // find `best` and `worst` py index // (this is 2-3x faster than a loop with if-else) // If no element has the state `1` and `2`, then skip const best_idx = combostates.indexOf(1); const worst_idx = combostates.indexOf(2); if ( best_idx === -1 || worst_idx === -1){ return [agg, bw, bn, nw]; } // add the direct "BEST > WORST" observation const best_uuid = stateids[best_idx]; const worst_uuid = stateids[worst_idx]; agg = lilIncrement(agg, best_uuid, worst_uuid); bw = lilIncrement(bw, best_uuid, worst_uuid); // loop over all other elements for ( var [middle_idx, middle_uuid] of stateids.entries() ){ if (middle_idx != best_idx && middle_idx != worst_idx){ // add `BEST > NOT` agg = lilIncrement(agg, best_uuid, middle_uuid); bn = lilIncrement(bn, best_uuid, middle_uuid); // add `NOT > WORST` agg = lilIncrement(agg, middle_uuid, worst_uuid); nw = lilIncrement(nw, middle_uuid, worst_uuid); } } // done return [agg, bw, bn, nw]; }; /** * Loop over an batch of BWS sets * * @param {Array} evaluations A list of combinatorial states and associated identifiers. * @param {JSON} agg Previously recorded frequencies for all directly extracted pairs. * @param {JSON} detail Previously recorded frequencies for each type of pair: "BEST>WORST" * (bw), "BEST>NOT" (bn), "NOT>WORST" (nw) * @returns agg, detail * * Example * const evaluations = [ [[0, 0, 2, 1], ['id1', 'id2', 'id3', 'id4']], * [[0, 1, 0, 2], ['id4', 'id5', 'id6', 'id1']] ]; * const [dok, detail] = directExtractBatch(evaluations); */ const directExtractBatch = (evaluations, agg=undefined, detail=undefined) => { // initialize empty dict objects if (agg === undefined){ agg = {}; } if (detail === undefined){ detail = {"bw": {}, "bn": {}, "nw": {}} } // query `detail` object var bw = detail["bw"]; var bn = detail["bn"]; var nw = detail["nw"]; // loop over all evaluated BWS sets, and post-process each for (var [combostates, stateids] of evaluations){ [agg, bw, bn, nw] = directExtract( stateids, combostates, agg, bw, bn, nw); } // copy details detail["bw"] = bw; detail["bn"] = bn; detail["nw"] = nw; // done return [agg, detail]; } /** * Find IDs by state * * @param {Array} ids IDs, e.g. UUID * @param {Array} states States, e.g. 0,1,2 * @param {Array} s_ List of states to search for * @returns */ const findByState = (ids, states, s_) => { var out = []; for(var i = 0; i < ids.length; i++){ if( s_.includes(states[i]) ){ out.push(ids[i]); } } return out; } /** * Logical Inference rules * * @param {Array[ID]} ids1 List of IDs * @param {Array[ID]} ids2 see "ids1" * @param {Array[Int]} states1 Combinatorial states, i.e. a list of item states. * Each item state is encoded as * - 0: NOT * - 1: BEST * - 2: WORST * @param {Array[Int]} states2 see "states1" * @param {Int} s1 The item state of the overlapping item * @param {Int} s2 see "s1" * @param {JSON} agg Previous counts/frequencies of logically inferred pairs. * @param {JSON} nn Previously counts/frequencies for different variants of logically inferred pairs counted separately * @param {JSON} nb see "nn" * @param {JSON} nw see "nn" * @param {JSON} bn see "nn" * @param {JSON} bw see "nn" * @param {JSON} wn see "nn" * @param {JSON} wb see "nn" * @returns agg, nn, nb, nw, bn, bw, wn, wb * * Literature * Hamster, U. A. (2021, March 9). Extracting Pairwise Comparisons Data * from Best-Worst Scaling Surveys by Logical Inference. * https://doi.org/10.31219/osf.io/qkxej */ const logicalRules = (ids1, ids2, states1, states2, s1, s2, agg, nn, nb, nw, bn, bw, wn, wb) => { // set defaults if (agg === undefined){ agg = {}; } if (nn === undefined){ nn = {}; } if (nb === undefined){ nb = {}; } if (nw === undefined){ nw = {}; } if (bn === undefined){ bn = {}; } if (bw === undefined){ bw = {}; } if (wn === undefined){ wn = {}; } if (wb === undefined){ wb = {}; } // Logical Inferences rules if (s1 === 0){ // 0:NOT if (s2 === 0){ // 0:NOT // nn: D>Z for (var i of findByState(ids1, states1, [1]) ){ for (var j of findByState(ids2, states2, [2]) ){ agg = lilIncrement(agg, i, j); nn = lilIncrement(nn, i, j); } } // nn: X>F for (var i of findByState(ids2, states2, [1]) ){ for (var j of findByState(ids1, states1, [2]) ){ agg = lilIncrement(agg, i, j); nn = lilIncrement(nn, i, j); } } } else if (s2 === 1){ // 1:BEST // nb: D>Y, D>Z for (var i of findByState(ids1, states1, [1]) ){ for (var j of findByState(ids2, states2, [0, 2]) ){ agg = lilIncrement(agg, i, j); nb = lilIncrement(nb, i, j); } } } else if (s2 === 2){ // 2:WORST // nw: X>F, Y>F for (var j of findByState(ids1, states1, [2]) ){ for (var i of findByState(ids2, states2, [0, 1]) ){ agg = lilIncrement(agg, i, j); nw = lilIncrement(nw, i, j); } } } } else if (s1 === 1){ // 1:BEST if (s2 === 0){ // bn: X>E, X>F for (var i of findByState(ids2, states2, [1]) ){ for (var j of findByState(ids1, states1, [0, 2]) ){ agg = lilIncrement(agg, i, j); bn = lilIncrement(bn, i, j); } } } else if (s2 === 2){ // bw: X>E, X>F, Y>E, Y>F for (var j of findByState(ids1, states1, [0, 2]) ){ for (var i of findByState(ids2, states2, [0, 1]) ){ agg = lilIncrement(agg, i, j); bw = lilIncrement(bw, i, j); } } } } else if (s1 === 2){ // 2:WORST if (s2 === 0){ // wn: D>Z, E>Z for (var i of findByState(ids1, states1, [0, 1]) ){ for (var j of findByState(ids2, states2, [2]) ){ agg = lilIncrement(agg, i, j); wn = lilIncrement(wn, i, j); } } } else if (s2 === 1){ // wb: D>Y, D>Z, E>Y, E>Z for (var i of findByState(ids1, states1, [0, 1]) ){ for (var j of findByState(ids2, states2, [0, 2]) ){ agg = lilIncrement(agg, i, j); wb = lilIncrement(wb, i, j); } } } } // done return [agg, nn, nb, nw, bn, bw, wn, wb]; } /** * Logical Inference between 2 BWS sets (See `logicalRules`) * * @param see logicalRules * * Example * const ids1 = ['D', 'E', 'F']; * const ids2 = ['F', 'Y', 'Z']; * const states1 = [1, 0, 2]; * const states2 = [1, 0, 2]; * const [agg, nn, nb, nw, bn, bw, wn, wb] = logicalInfer( * ids1, ids2, states1, states2) */ const logicalInfer = (ids1, ids2, states1, states2, agg, nn, nb, nw, bn, bw, wn, wb) => { // set defaults if (agg === undefined){ agg = {}; } if (nn === undefined){ nn = {}; } if (nb === undefined){ nb = {}; } if (nw === undefined){ nw = {}; } if (bn === undefined){ bn = {}; } if (bw === undefined){ bw = {}; } if (wn === undefined){ wn = {}; } if (wb === undefined){ wb = {}; } // find common IDs, and loop over them const commonids = ids1.filter(x => ids2.includes(x)); // intersection for(var uid of commonids){ try{ // find positions of the ID var p1 = ids1.indexOf(uid); var p2 = ids2.indexOf(uid); // lookup states of the ID var s1 = states1[p1]; var s2 = states2[p2]; // apply rules [agg, nn, nb, nw, bn, bw, wn, wb] = logicalRules( ids1, ids2, states1, states2, s1, s2, agg, nn, nb, nw, bn, bw, wn, wb); } catch (err){ console.log(`Error: ${err.message}`); } } // done return [agg, nn, nb, nw, bn, bw, wn, wb]; } // def logicalInferUpdate( // evaluations: List[Tuple[List[ItemState], List[ItemID]]], // database: List[Tuple[List[ItemState], List[ItemID]]] = None, // dok: Optional[Dict[Tuple[ItemID, ItemID], int]] = None, // detail: Optional[dict] = None) -> ( // Dict[Tuple[ItemID, ItemID], int], dict): // """Run logical inference from a batch/list of BWS sets against ad database // Parameters: // ----------- // evaluations: List[Tuple[List[ItemState], List[ItemID]]] // A list of new BWS sets to be evaluated. // database: List[Tuple[List[ItemState], List[ItemID]]] // A database of previously processed BWS sets // dok: Optional[Dict[Tuple[ItemID, ItemID], int]] // The previous counts/frequencies of logical inferred pairs // that need to be updated. // detail: Optional[dict] // A dictionary of previously stored DOKs for each variant of // logically inferred pairs. // Returns: // -------- // dok: Optional[Dict[Tuple[ItemID, ItemID], int]] // The counts/frequencies of logical inferred pairs. // detail: Optional[dict] // A dictionary that stores seperate DOKs for each variant of // logically inferred pairs. // """ const logicalInferUpdate = (evaluations, database=undefined, agg=undefined, detail=undefined) => { // initialize empty dict objects if (agg === undefined){ agg = {}; } if (detail === undefined){ detail = {"nn": {}, "nb": {}, "nw": {}, "bn": {}, "bw": {}, "wn": {}, "wb": {}}; } // query `detail` object var nn = detail["nn"]; var nb = detail["nb"]; var nw = detail["nw"]; var bn = detail["bn"]; var bw = detail["bw"]; var wn = detail["wn"]; var wb = detail["wb"]; // Create new database if (database === undefined){ database = evaluations; } // start searching for logical inferences for (const [states1, ids1] of evaluations){ for (const [states2, ids2] of database){ [agg, nn, nb, nw, bn, bw, wn, wb] = logicalInfer( ids1, ids2, states1, states2, agg, nn, nb, nw, bn, bw, wn, wb); } } // copy details detail["nn"] = nn; detail["nb"] = nb; detail["nw"] = nw; detail["bn"] = bn; detail["bw"] = bw; detail["wn"] = wn; detail["wb"] = wb; // done return [agg, detail]; } module.exports = { lilIncrement, lilAdd, lilAddInplace, lilMerge, directExtract, directExtractBatch, findByState, logicalInfer, logicalInferUpdate, count };