@bdelab/jscat
Version:
A library to support IRT-based computer adaptive testing in JavaScript
104 lines (103 loc) • 4.08 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findClosest = 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");
/**
* 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 distribution = [];
for (let i = min; i <= max; i += stepSize) {
distribution.push([i, y(i)]);
}
return distribution;
function y(x) {
return (1 / (Math.sqrt(2 * Math.PI) * stdDev)) * Math.exp(-Math.pow(x - mean, 2) / (2 * Math.pow(stdDev, 2)));
}
};
exports.normal = normal;
/**
* 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;