@bdelab/jscat
Version:
A library to support IRT-based computer adaptive testing in JavaScript
282 lines (281 loc) • 12.6 kB
JavaScript
;
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;