UNPKG

@bdelab/jscat

Version:

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

101 lines (91 loc) 3.55 kB
/* eslint-disable @typescript-eslint/no-non-null-assertion */ import bs from 'binary-search'; import { Stimulus, Zeta, ZetaSymbolic } from './type'; import { fillZetaDefaults } from './corpus'; /** * 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 */ export const itemResponseFunction = (theta: number, zeta: Zeta) => { const _zeta = fillZetaDefaults(zeta, 'symbolic') as ZetaSymbolic; return _zeta.c + (_zeta.d - _zeta.c) / (1 + Math.exp(-_zeta.a * (theta - _zeta.b))); }; /** * A 3PL Fisher information function * * @param {number} theta - ability estimate * @param {Zeta} zeta - item params * @returns {number} - the expected value of the observed information */ export const fisherInformation = (theta: number, zeta: Zeta) => { const _zeta = fillZetaDefaults(zeta, 'symbolic') as ZetaSymbolic; const p = 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)); }; /** * 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 */ export const normal = (mean = 0, stdDev = 1, min = -4, max = 4, stepSize = 0.1) => { const distribution = []; for (let i = min; i <= max; i += stepSize) { distribution.push([i, y(i)]); } return distribution; function y(x: number) { return (1 / (Math.sqrt(2 * Math.PI) * stdDev)) * Math.exp(-Math.pow(x - mean, 2) / (2 * Math.pow(stdDev, 2))); } }; /** * 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 */ export const findClosest = (inputStimuli: Array<Stimulus>, target: number) => { const stimuli = inputStimuli.map((stim) => 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: Stimulus, needle: number) => { return element.difficulty! - needle; }; const indexOfTarget = bs(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; } } };