UNPKG

@bdelab/jscat

Version:

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

123 lines (122 loc) 5.32 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.findClosest = exports.uniform = exports.normal = exports.fisherInformation = exports.itemResponseFunction = void 0; /* eslint-disable @typescript-eslint/no-non-null-assertion */ const binary_search_1 = __importDefault(require("binary-search")); const corpus_1 = require("./corpus"); const range_1 = __importDefault(require("lodash/range")); const round_1 = __importDefault(require("lodash/round")); /** * Calculates the probability that someone with a given ability level theta will * answer correctly an item. Uses the 4 parameters logistic model * * @param {number} theta - ability estimate * @param {Zeta} zeta - item params * @returns {number} the probability */ const itemResponseFunction = (theta, zeta) => { const _zeta = (0, corpus_1.fillZetaDefaults)(zeta, 'symbolic'); return _zeta.c + (_zeta.d - _zeta.c) / (1 + Math.exp(-_zeta.a * (theta - _zeta.b))); }; exports.itemResponseFunction = itemResponseFunction; /** * A 3PL Fisher information function * * @param {number} theta - ability estimate * @param {Zeta} zeta - item params * @returns {number} - the expected value of the observed information */ const fisherInformation = (theta, zeta) => { const _zeta = (0, corpus_1.fillZetaDefaults)(zeta, 'symbolic'); const p = (0, exports.itemResponseFunction)(theta, _zeta); const q = 1 - p; return Math.pow(_zeta.a, 2) * (q / p) * (Math.pow(p - _zeta.c, 2) / Math.pow(1 - _zeta.c, 2)); }; exports.fisherInformation = fisherInformation; /** * Return a Gaussian distribution within a given range * * @param {number} mean * @param {number} stdDev * @param {number} min * @param {number} max * @param {number} stepSize - the quantization (step size) of the internal table, default = 0.1 * @returns {Array<[number, number]>} - a normal distribution */ const normal = (mean = 0, stdDev = 1, min = -4, max = 4, stepSize = 0.1) => { const x = (0, range_1.default)(min, max + stepSize, stepSize); function y(x) { return (1 / (Math.sqrt(2 * Math.PI) * stdDev)) * Math.exp(-Math.pow(x - mean, 2) / (2 * Math.pow(stdDev, 2))); } return x.map((x) => [(0, round_1.default)(x, 6), y(x)]); }; exports.normal = normal; /** * Return a uniform distribution within a given range * * @param {number} min - lower bound of the uniform distribution * @param {number} max - upper bound of the uniform distribution * @param {number} stepSize - the quantization (step size) of the internal table, default = 0.1 * @param {number} fullMin - full range minimum (defaults to min) * @param {number} fullMax - full range maximum (defaults to max) * @returns {Array<[number, number]>} - a uniform distribution */ const uniform = (min = -4, max = 4, stepSize = 0.1, fullMin, fullMax) => { const actualMin = fullMin !== null && fullMin !== void 0 ? fullMin : min; const actualMax = fullMax !== null && fullMax !== void 0 ? fullMax : max; // Create the grid with rounding const x = (0, range_1.default)(actualMin, actualMax + stepSize / 2, stepSize).map((n) => (0, round_1.default)(n, 6)); const support = x.filter((theta) => theta >= min && theta <= max); const probabilityMass = 1 / support.length; return x.map((theta) => [theta, theta >= min && theta <= max ? probabilityMass : 0]); }; exports.uniform = uniform; /** * Find the item in a given array that has the difficulty closest to the target value * * @remarks * The input array of stimuli must be sorted by difficulty. * * @param {Stimulus[]} inputStimuli - an array of stimuli sorted by difficulty * @param {number} target - ability estimate * @returns {number} the index of stimuli */ const findClosest = (inputStimuli, target) => { const stimuli = inputStimuli.map((stim) => (0, corpus_1.fillZetaDefaults)(stim, 'semantic')); // Let's consider the edge cases first if (target <= stimuli[0].difficulty) { return 0; } else if (target >= stimuli[stimuli.length - 1].difficulty) { return stimuli.length - 1; } const comparitor = (element, needle) => { return element.difficulty - needle; }; const indexOfTarget = (0, binary_search_1.default)(stimuli, target, comparitor); if (indexOfTarget >= 0) { // `bs` returns a positive integer index if it found an exact match. return indexOfTarget; } else { // If the value is not in the array, then -(index + 1) is returned, where // index is where the value should be inserted into the array to maintain // sorted order. Thus, the target is between the values at const lowIndex = -2 - indexOfTarget; const highIndex = -1 - indexOfTarget; // So we simply compare the differences between the target and the high and // low values, respectively const lowDiff = Math.abs(stimuli[lowIndex].difficulty - target); const highDiff = Math.abs(stimuli[highIndex].difficulty - target); if (lowDiff < highDiff) { return lowIndex; } else { return highIndex; } } }; exports.findClosest = findClosest;