UNPKG

@bdelab/jscat

Version:

A library to support IRT-based computer adaptive testing in JavaScript

282 lines (281 loc) 12.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.prepareClowderCorpus = exports.filterItemsByCatParameterAvailability = exports.checkNoDuplicateCatNames = exports.convertZeta = exports.fillZetaDefaults = exports.validateZetaParams = exports.defaultZeta = exports.ZETA_KEY_MAP = void 0; const flatten_1 = __importDefault(require("lodash/flatten")); const invert_1 = __importDefault(require("lodash/invert")); const isEmpty_1 = __importDefault(require("lodash/isEmpty")); const mapKeys_1 = __importDefault(require("lodash/mapKeys")); const union_1 = __importDefault(require("lodash/union")); const uniq_1 = __importDefault(require("lodash/uniq")); const omit_1 = __importDefault(require("lodash/omit")); /** * A constant map from the symbolic item parameter names to their semantic * counterparts. */ exports.ZETA_KEY_MAP = { a: 'discrimination', b: 'difficulty', c: 'guessing', d: 'slipping', }; /** * Return default item parameters (i.e., zeta) * * @param {'symbolic' | 'semantic'} desiredFormat - The desired format for the output zeta object. * @returns {Zeta} the default zeta object in the specified format. */ const defaultZeta = (desiredFormat = 'symbolic') => { const defaultZeta = { a: 1, b: 0, c: 0, d: 1, }; return (0, exports.convertZeta)(defaultZeta, desiredFormat); }; exports.defaultZeta = defaultZeta; /** * Validates the item (a.k.a. zeta) parameters, prohibiting redundant keys and * optionally requiring all parameters. * * @param {Zeta} zeta - The zeta parameters to validate. * @param {boolean} requireAll - If `true`, ensures that all required keys are present. Default is `false`. * * @throws {Error} Will throw an error if any of the validation rules are violated. */ const validateZetaParams = (zeta, requireAll = false) => { if (zeta.a !== undefined && zeta.discrimination !== undefined) { throw new Error('This item has both an `a` key and `discrimination` key. Please provide only one.'); } if (zeta.b !== undefined && zeta.difficulty !== undefined) { throw new Error('This item has both a `b` key and `difficulty` key. Please provide only one.'); } if (zeta.c !== undefined && zeta.guessing !== undefined) { throw new Error('This item has both a `c` key and `guessing` key. Please provide only one.'); } if (zeta.d !== undefined && zeta.slipping !== undefined) { throw new Error('This item has both a `d` key and `slipping` key. Please provide only one.'); } if (requireAll) { if (zeta.a === undefined && zeta.discrimination === undefined) { throw new Error('This item is missing the key `a` or `discrimination`.'); } if (zeta.b === undefined && zeta.difficulty === undefined) { throw new Error('This item is missing the key `b` or `difficulty`.'); } if (zeta.c === undefined && zeta.guessing === undefined) { throw new Error('This item is missing the key `c` or `guessing`.'); } if (zeta.d === undefined && zeta.slipping === undefined) { throw new Error('This item is missing the key `d` or `slipping`.'); } } }; exports.validateZetaParams = validateZetaParams; /** * Fills in default zeta parameters for any missing keys in the provided zeta object. * * @remarks * This function merges the provided zeta object with the default zeta object, converting * the keys to the desired format if specified. If no desired format is provided, the * keys will remain in their original format. * * @param {Zeta} zeta - The zeta parameters to fill in defaults for. * @param {'symbolic' | 'semantic'} desiredFormat - The desired format for the output zeta object. Default is 'symbolic'. * * @returns A new zeta object with default values filled in for any missing keys, * and converted to the desired format if specified. */ const fillZetaDefaults = (zeta, desiredFormat = 'symbolic') => { return Object.assign(Object.assign({}, (0, exports.defaultZeta)(desiredFormat)), (0, exports.convertZeta)(zeta, desiredFormat)); }; exports.fillZetaDefaults = fillZetaDefaults; /** * Converts zeta parameters between symbolic and semantic formats. * * @remarks * This function takes a zeta object and a desired format as input. It converts * the keys of the zeta object from their current format to the desired format. * If the desired format is 'symbolic', the function maps the keys to their * symbolic counterparts using the `ZETA_KEY_MAP`. If the desired format is * 'semantic', the function maps the keys to their semantic counterparts using * the inverse of `ZETA_KEY_MAP`. * * @param {Zeta} zeta - The zeta parameters to convert. * @param {'symbolic' | 'semantic'} desiredFormat - The desired format for the output zeta object. Must be either 'symbolic' or 'semantic'. * * @throws {Error} - Will throw an error if the desired format is not 'symbolic' or 'semantic'. * * @returns {Zeta} A new zeta object with keys converted to the desired format. */ const convertZeta = (zeta, desiredFormat) => { if (!['symbolic', 'semantic'].includes(desiredFormat)) { throw new Error(`Invalid desired format. Expected 'symbolic' or'semantic'. Received ${desiredFormat} instead.`); } return (0, mapKeys_1.default)(zeta, (value, key) => { if (desiredFormat === 'symbolic') { const inverseMap = (0, invert_1.default)(exports.ZETA_KEY_MAP); if (key in inverseMap) { return inverseMap[key]; } else { return key; } } else { if (key in exports.ZETA_KEY_MAP) { return exports.ZETA_KEY_MAP[key]; } else { return key; } } }); }; exports.convertZeta = convertZeta; /** * Validates a corpus of multi-zeta stimuli to ensure that no cat names are * duplicated. * * @remarks * This function takes an array of `MultiZetaStimulus` objects, where each * object represents an item containing item parameters (zetas) associated with * different CAT models. The function checks for any duplicate cat names across * each item's array of zeta values. It throws an error if any are found. * * @param {MultiZetaStimulus[]} corpus - An array of `MultiZetaStimulus` objects representing the corpora to validate. * * @throws {Error} - Throws an error if any duplicate cat names are found across the corpora. */ const checkNoDuplicateCatNames = (corpus) => { const zetaCatMapsArray = corpus.map((item) => item.zetas); for (const zetaCatMaps of zetaCatMapsArray) { const cats = zetaCatMaps.map(({ cats }) => cats); // Check to see if there are any duplicate names by comparing the union // (which removed duplicates) to the flattened array. const union = (0, union_1.default)(...cats); const flattened = (0, flatten_1.default)(cats); if (union.length !== flattened.length) { // If there are duplicates, remove the first occurence of each cat name in // the union array from the flattened array. The remaining items in the // flattened array should contain the duplicated cat names. for (const cat of union) { const idx = flattened.findIndex((c) => c === cat); if (idx >= 0) { flattened.splice(idx, 1); } } throw new Error(`The cat names ${(0, uniq_1.default)(flattened).join(', ')} are present in multiple corpora.`); } } }; exports.checkNoDuplicateCatNames = checkNoDuplicateCatNames; /** * Filters a list of multi-zeta stimuli based on the availability of model parameters for a specific CAT. * * This function takes an array of `MultiZetaStimulus` objects and a `catName` as input. It then filters * the items based on whether the specified CAT model parameter is present in the item's zeta values. * The function returns an object containing two arrays: `available` and `missing`. The `available` array * contains items where the specified CAT model parameter is present, while the `missing` array contains * items where the parameter is not present. * * @param {MultiZetaStimulus[]} items - An array of `MultiZetaStimulus` objects representing the stimuli to filter. * @param {string} catName - The name of the CAT model parameter to check for. * * @returns An object with two arrays: `available` and `missing`. * * @example * ```typescript * const items: MultiZetaStimulus[] = [ * { * stimulus: 'Item 1', * zetas: [ * { cats: ['Model A', 'Model B'], zeta: { a: 1, b: 0.5, c: 0.2, d: 0.8 } }, * { cats: ['Model C'], zeta: { a: 2, b: 0.7, c: 0.3, d: 0.9 } }, * ], * }, * { * stimulus: 'Item 2', * zetas: [ * { cats: ['Model B', 'Model C'], zeta: { a: 2.5, b: 0.8, c: 0.35, d: 0.95 } }, * ], * }, * ]; * * const result = filterItemsByCatParameterAvailability(items, 'Model A'); * console.log(result.available); * // Output: [ * // { * // stimulus: 'Item 1', * // zetas: [ * // { cats: ['Model A', 'Model B'], zeta: { a: 1, b: 0.5, c: 0.2, d: 0.8 } }, * // { cats: ['Model C'], zeta: { a: 2, b: 0.7, c: 0.3, d: 0.9 } }, * // ], * // }, * // ] * console.log(result.missing); * // Output: [ * // { * // stimulus: 'Item 2', * // zetas: [ * // { cats: ['Model B', 'Model C'], zeta: { a: 2.5, b: 0.8, c: 0.35, d: 0.95 } }, * // ], * // }, * // ] * ``` */ const filterItemsByCatParameterAvailability = (items, catName) => { const paramsExist = items.filter((item) => item.zetas.some((zetaCatMap) => zetaCatMap.cats.includes(catName))); const paramsMissing = items.filter((item) => !item.zetas.some((zetaCatMap) => zetaCatMap.cats.includes(catName))); return { available: paramsExist, missing: paramsMissing, }; }; exports.filterItemsByCatParameterAvailability = filterItemsByCatParameterAvailability; /** * Converts an array of Stimulus objects into an array of MultiZetaStimulus objects. * The user specifies cat names and a delimiter to identify and group parameters. * * @param {Stimulus[]} items - An array of stimuli, where each stimulus contains parameters * for different CAT instances. * @param {string[]} catNames - A list of CAT names to be mapped to their corresponding zeta values. * @param {string} delimiter - A delimiter used to separate CAT instance names from the parameter keys in the stimulus object. * @param {'symbolic' | 'semantic'} itemParameterFormat - Defines the format to convert zeta values ('symbolic' or 'semantic'). * @returns {MultiZetaStimulus[]} - An array of MultiZetaStimulus objects, each containing * the cleaned stimulus and associated zeta values for each CAT instance. * * This function iterates through each stimulus, extracts parameters relevant to the specified * CAT instances, converts them to the desired format, and returns a cleaned structure of stimuli * with the associated zeta values. */ const prepareClowderCorpus = (items, catNames, delimiter, itemParameterFormat = 'symbolic') => { return items.map((item) => { const zetas = catNames .map((cat) => { const zeta = {}; // Extract parameters that match the category Object.keys(item).forEach((key) => { if (key.startsWith(cat + delimiter)) { const paramKey = key.split(delimiter)[1]; zeta[paramKey] = item[key]; } }); return { cats: [cat], zeta: (0, exports.convertZeta)(zeta, itemParameterFormat), }; }) .filter((zeta) => { // Check if zeta has no `NA` values and is not empty return !(0, isEmpty_1.default)(zeta.zeta) && Object.values(zeta.zeta).every((value) => value !== 'NA'); }); // Create the MultiZetaStimulus structure without the category keys const cleanItem = (0, omit_1.default)(item, Object.keys(item).filter((key) => catNames.some((cat) => key.startsWith(cat + delimiter)))); return Object.assign(Object.assign({}, cleanItem), { zetas }); }); }; exports.prepareClowderCorpus = prepareClowderCorpus;