/* Chalkboard - Statistics Namespace Version 3.0.2 Euler Released April 13th, 2026 */ /* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /// namespace Chalkboard { /** * The statistics namespace. * @namespace */ export namespace stat { /** @ignore */ const $quickselect = (arr: number[], k: number): number => { const select = (left: number, right: number, k: number): number => { if (left === right) return arr[left]; let pivotIndex = Math.floor(Math.random() * (right - left + 1)) + left; const pivotValue = arr[pivotIndex]; arr[pivotIndex] = arr[right]; arr[right] = pivotValue; let storeIndex = left; for (let i = left; i < right; i++) { if (arr[i] < pivotValue) { const temp = arr[storeIndex]; arr[storeIndex] = arr[i]; arr[i] = temp; storeIndex++; } } arr[right] = arr[storeIndex]; arr[storeIndex] = pivotValue; if (k === storeIndex) return arr[k]; else if (k < storeIndex) return select(left, storeIndex - 1, k); else return select(storeIndex + 1, right, k); }; return select(0, arr.length - 1, k); }; /** * Calculates the absolute value of all the elements of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const absolute = (arr: number[]): number[] => { const result: number[] = []; for (let i = 0; i < arr.length; i++) { result.push(Math.abs(arr[i])); } return result; }; /** * Calculates the addition of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number[]} */ export const add = (arr1: number[], arr2: number[]): number[] => { if (arr1.length !== arr2.length) { throw new RangeError('Parameters "arr1" and "arr2" must have the same length.'); } const result: number[] = []; for (let i = 0; i < arr1.length; i++) { result.push(arr1[i] + arr2[i]); } return result; }; /** * Returns an array with linearly-spaced elements. * @param {number} inf - The lower bound * @param {number} sup - The upper bound * @param {number} [length=sup-inf+1] - The length of the array * @returns {number[]} */ export const array = (inf: number, sup: number, length: number = sup - inf + 1): number[] => { const result = []; const step = (sup - inf) / (length - 1); for (let i = 0; i < length; i++) { result.push(inf + step * i); } return result; }; /** * Calculates the autocorrelation of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const autocorrelation = (arr: number[]): number[] => { return Chalkboard.stat.correlation(arr, arr); }; /** * Calculates the posterior probability using Bayes' theorem. * @param {number} pA - The prior probability of A (i.e. P(A)) * @param {number} pGivenA - The probability of B given A (i.e. P(B|A)) * @param {number} pGivenNotA - The probability of B given not A (i.e. P(B|!A)) * @returns {number} */ export const Bayes = (pA: number, pGivenA: number, pGivenNotA: number): number => { if (pA < 0 || pA > 1 || pGivenA < 0 || pGivenA > 1 || pGivenNotA < 0 || pGivenNotA > 1) { throw new RangeError('All probabilities must be between 0 and 1.'); } return (pGivenA * pA) / (pGivenA * pA + pGivenNotA * (1 - pA)); }; /** * Calculates the change of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number[]} */ export const change = (arr1: number[], arr2: number[]): number[] => { if (arr1.length !== arr2.length) { throw new RangeError('Parameters "arr1" and "arr2" must have the same length.'); } const result = []; for (let i = 0; i < arr1.length; i++) { result.push(Chalkboard.numb.change(arr1[i], arr2[i])); } return result; }; /** * Calculates the chi-squared test of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number[]} */ export const chiSquared = (arr1: number[], arr2: number[]): number[] => { if (arr1.length !== arr2.length) { throw new RangeError('Parameters "arr1" and "arr2" must have the same length.'); } const result = []; for (let i = 0; i < arr1.length; i++) { result.push(((arr1[i] - arr2[i]) * (arr1[i] - arr2[i])) / arr2[i]); } return result; }; /** * Calculates the 95% confidence interval of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const confidenceInterval = (arr: number[], confidence: number = 0.95): [number, number] => { if (confidence <= 0 || confidence >= 1) { throw new RangeError('Parameter "confidence" must be between 0 and 1 (exclusive).'); } const z = Chalkboard.stat.inormal(1 - (1 - confidence) / 2); const mean = Chalkboard.stat.mean(arr); const standardError = Chalkboard.stat.error(arr); return [mean - z * standardError, mean + z * standardError]; }; /** * Returns an array constrained within a range. * @param {number[]} arr - The array * @param {number[]} range - The range * @returns {number[]} */ export const constrain = (arr: number[], range: [number, number] = [0, 1]): number[] => { const result = []; for (let i = 0; i < arr.length; i++) { result.push(Chalkboard.numb.constrain(arr[i], range)); } return result; }; /** * Calculates the convolution of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number[]} */ export const convolution = (arr1: number[], arr2: number[]): number[] => { const result = []; for (let i = 0; i < arr1.length + arr2.length - 1; i++) { let sum = 0; for (let j = Math.max(0, i - arr2.length + 1); j < Math.min(arr1.length, i + 1); j++) { sum += arr1[j] * arr2[i - j]; } result.push(sum); } return result; }; /** * Calculates the cross-correlation of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number[]} */ export const correlation = (arr1: number[], arr2: number[]): number[] => { const result = []; for (let i = 0; i < arr1.length + arr2.length - 1; i++) { let sum = 0; for (let j = Math.max(0, i - arr2.length + 1); j < Math.min(arr1.length, i + 1); j++) { sum += arr1[j] * arr2[arr2.length - 1 - i + j]; } result.push(sum); } return result; }; /** * Calculates the Pearson correlation coefficient of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number} */ export const correlationCoefficient = (arr1: number[], arr2: number[]): number => { return Chalkboard.stat.covariance(arr1, arr2) / (Chalkboard.stat.deviation(arr1) * Chalkboard.stat.deviation(arr2)); }; /** * Calculates the covariance of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number} */ export const covariance = (arr1: number[], arr2: number[]): number => { if (arr1.length !== arr2.length) { throw new RangeError('Parameters "arr1" and "arr2" must have the same length.'); } const mean1 = Chalkboard.stat.mean(arr1); const mean2 = Chalkboard.stat.mean(arr2); let sum = 0; for (let i = 0; i < arr1.length; i++) { sum += (arr1[i] - mean1) * (arr2[i] - mean2); } return sum / arr1.length; }; /** * Calculates the cumulative maximum of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const cummax = (arr: number[]): number[] => { const result = []; let max = -Infinity; for (const value of arr) { max = Math.max(max, value); result.push(max); } return result; }; /** * Calculates the cumulative minimum of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const cummin = (arr: number[]): number[] => { const result = []; let min = Infinity; for (const value of arr) { min = Math.min(min, value); result.push(min); } return result; }; /** * Calculates the cumulative product of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const cummul = (arr: number[]): number[] => { const result = []; let mul = 1; for (let i = 0; i < arr.length; i++) { mul *= arr[i]; result.push(mul); } return result; }; /** * Calculates the cumulative sum of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const cumsum = (arr: number[]): number[] => { const result = []; let sum = 0; for (let i = 0; i < arr.length; i++) { sum += arr[i]; result.push(sum); } return result; }; /** * Calculates the standard deviation of an array. * @param {number[]} arr - The array * @returns {number} */ export const deviation = (arr: number[]): number => { let result = 0; for (let i = 0; i < arr.length; i++) { result += (arr[i] - Chalkboard.stat.mean(arr)) * (arr[i] - Chalkboard.stat.mean(arr)); } return Chalkboard.real.sqrt(result / arr.length); }; /** * Calculates the dot product of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number} */ export const dot = (arr1: number[], arr2: number[]): number => { if (arr1.length !== arr2.length) { throw new RangeError('Parameters "arr1" and "arr2" must have the same length.'); } let result = 0; for (let i = 0; i < arr1.length; i++) { result += arr1[i] * arr2[i]; } return result; }; /** * Calculates the standard error of an array. * @param {number[]} arr - The array * @returns {number} */ export const error = (arr: number[]): number => { return Chalkboard.stat.deviation(arr) / Chalkboard.real.sqrt(arr.length); }; /** * Checks if the elements of an array are equal to a number or the elements of another array, and then returns an array with the elements that pass the check. * @param {number[]} arr - The array * @param {number | {number}[]} arrORnum - The array or number * @returns {number[]} */ export const eq = (arr: number[], arrORnum: number | number[]): number[] => { const result = []; if (Array.isArray(arrORnum)) { if (arr.length === arrORnum.length) { for (let i = 0; i < arr.length; i++) { if (arr[i] === arrORnum[i]) { result.push(arr[i]); } } } } else { for (let i = 0; i < arr.length; i++) { if (arr[i] === arrORnum) { result.push(arr[i]); } } } return result; }; /** * Calculates the expected value of an array. * @param {number[]} arr - The array * @param {number[]} [probabilities] - The probabilities of the corresponding elements of the array (optional, defaults to equiprobable) * @returns {number} */ export const expected = (arr: number[], probabilities?: number[]): number => { if (!probabilities) { probabilities = Array(arr.length).fill(1 / arr.length); } if (arr.length !== probabilities.length) { throw new RangeError('Parameters "values" and "probabilities" must have the same length.'); } let result = 0; for (let i = 0; i < arr.length; i++) { result += arr[i] * probabilities[i]; } return result; }; /** * Defines a Gaussian function. * @param {number} height - The height of the distribution * @param {number} mean - The mean of the distribution * @param {number} deviation - The standard deviation of the distribution * @returns {ChalkboardFunction} */ export const Gaussian = (height: number, mean: number, deviation: number): ChalkboardFunction => { return Chalkboard.real.define((x) => height * Math.exp(-((x - mean) * (x - mean)) / (2 * deviation * deviation))); }; /** * Checks if the elements of an array are greater than (or equal to) a number or the elements of another array, and then returns an array with the elements that pass the check. * @param {number[]} arr - The array * @param {number | number[]} arrORnum - The array or number * @param {boolean} [includeEnd=false] - Whether the check is "less than" (false) or "less than or equal to" (true) * @returns {number[]} */ export const gt = (arr: number[], arrORnum: number | number[], includeEnd: boolean = false): number[] => { const result = []; if (Array.isArray(arrORnum)) { if (arr.length === arrORnum.length) { for (let i = 0; i < arr.length; i++) { if (includeEnd) { if (arr[i] >= arrORnum[i]) { result.push(arr[i]); } } else { if (arr[i] > arrORnum[i]) { result.push(arr[i]); } } } } } else { for (let i = 0; i < arr.length; i++) { if (includeEnd) { if (arr[i] >= arrORnum) { result.push(arr[i]); } } else { if (arr[i] > arrORnum) { result.push(arr[i]); } } } } return result; }; /** * Checks if the elements of an array are less than (or equal to) and greater than (or equal to) a number or the elements of another array, and then returns an array with the elements that pass the check. * @param {number[]} arr - The array * @param {number | number[]} inf - The array or number to check "less than (or equal to)" with * @param {number | number[]} sup - The array or number to check "greater than (or equal to)" with * @param {boolean} [includeInf=false] - Whether the check is "less than" (false) or "less than or equal to" (true) * @param {boolean} [includeSup=false] - Whether the check is "greater than" (false) or "greater than or equal to" (true) * @returns {number[]} */ export const ineq = (arr: number[], inf: number, sup: number, includeInf: boolean = false, includeSup: boolean = false): number[] => { const result = []; if (Array.isArray(inf) && Array.isArray(sup)) { if (arr.length === inf.length && arr.length === sup.length) { for (let i = 0; i < arr.length; i++) { if (includeInf) { if (includeSup) { if (arr[i] >= inf[i] && arr[i] <= sup[i]) { result.push(arr[i]); } } else { if (arr[i] >= inf[i] && arr[i] < sup[i]) { result.push(arr[i]); } } } else { if (includeSup) { if (arr[i] > inf[i] && arr[i] <= sup[i]) { result.push(arr[i]); } } else { if (arr[i] > inf[i] && arr[i] < sup[i]) { result.push(arr[i]); } } } } } } else { for (let i = 0; i < arr.length; i++) { if (includeInf) { if (includeSup) { if (arr[i] >= inf && arr[i] <= sup) { result.push(arr[i]); } } else { if (arr[i] >= inf && arr[i] < sup) { result.push(arr[i]); } } } else { if (includeSup) { if (arr[i] > inf && arr[i] <= sup) { result.push(arr[i]); } } else { if (arr[i] > inf && arr[i] < sup) { result.push(arr[i]); } } } } } return result; }; /** * Calculates an approximation of the inverse of the cumulative distribution function (CDF) of the standard normal distribution using the Beasley-Springer-Moro algorithm. * @param {number} p - The probability (must be between 0 and 1) * @returns {number} */ export const inormal = (p: number): number => { if (p <= 0 || p >= 1) { throw new RangeError('Parameter "p" must be between 0 and 1 (exclusive).'); } const a = [2.50662823884, -18.61500062529, 41.39119773534, -25.44106049637]; const b = [-8.4735109309, 23.08336743743, -21.06224101826, 3.13082909833]; const c = [0.3374754822726147, 0.9761690190917186, 0.1607979714918209, 0.0276438810333863, 0.0038405729373609, 0.0003951896511919, 0.0000321767881768, 0.0000002888167364, 0.0000003960315187]; let x = p - 0.5; if (Math.abs(x) < 0.42) { const r = x * x; return x * (((a[3] * r + a[2]) * r + a[1]) * r + a[0]) / ((((b[3] * r + b[2]) * r + b[1]) * r + b[0]) * r + 1); } else { const r = p < 0.5 ? p : 1 - p; const s = Math.log(-Math.log(r)); let t = c[0]; for (let i = 1; i < c.length; i++) { t += c[i] * Math.pow(s, i); } return p < 0.5 ? -t : t; } }; /** * Interpolates missing values (null or undefined) in an array using linear or quadratic interpolation. * @param {(number | null | undefined)[]} arr - The array with missing values * @param {"linear" | "quadratic"} [type="linear"] - The interpolation method, either "linear" or "quadratic" * @returns {number[]} */ export const interpolate = (arr: (number | null | undefined)[], type: "linear" | "quadratic" = "linear"): number[] => { const result = arr.slice(); for (let i = 0; i < result.length; i++) { if (result[i] == null) { let prevIndex = i - 1; let nextIndex = i + 1; while (prevIndex >= 0 && result[prevIndex] == null) prevIndex--; while (nextIndex < result.length && result[nextIndex] == null) nextIndex++; const prevValue = prevIndex >= 0 ? result[prevIndex] as number : 0; const nextValue = nextIndex < result.length ? result[nextIndex] as number : 0; if (type === "linear") { const t = (i - prevIndex) / (nextIndex - prevIndex); result[i] = Chalkboard.real.lerp([prevValue, nextValue], t); } else if (type === "quadratic" && prevIndex > 0 && nextIndex < result.length) { const prevPrevIndex = prevIndex - 1; const prevPrevValue = prevPrevIndex >= 0 ? result[prevPrevIndex] as number : prevValue; const t = (i - prevIndex) / (nextIndex - prevIndex); result[i] = Chalkboard.real.qerp( [prevPrevIndex, prevPrevValue], [prevIndex, prevValue], [nextIndex, nextValue], prevIndex + t * (nextIndex - prevIndex) ); } else { const t = (i - prevIndex) / (nextIndex - prevIndex); result[i] = Chalkboard.real.lerp([prevValue, nextValue], t); } } } return result as number[]; }; /** * Calculates the interquartile range of an array. * @param {number[]} arr - The array * @returns {number} */ export const interquartileRange = (arr: number[]): number => { return Chalkboard.stat.quartile(arr, "Q3") - Chalkboard.stat.quartile(arr, "Q1"); }; /** * Calculates the kurtosis of an array. * @param {number[]} arr - The array * @returns {number} */ export const kurtosis = (arr: number[]): number => { let result = 0; const mean = Chalkboard.stat.mean(arr); const deviation = Chalkboard.stat.deviation(arr); for (let i = 0; i < arr.length; i++) { result += (arr[i] - mean) * (arr[i] - mean) * (arr[i] - mean) * (arr[i] - mean); } return result / (deviation * deviation * deviation * deviation) - 3; }; /** * Checks if the elements of an array are less than (or equal to) a number or the elements of another array, and then returns an array with the elements that pass the check. * @param {number[]} arr - The array * @param {number | number[]} arrORnum - The array or number * @param {boolean} [includeEnd=false] - Whether the check is "less than" (false) or "less than or equal to" (true) * @returns {number[]} */ export const lt = (arr: number[], arrORnum: number | number[], includeEnd: boolean = false): number[] => { const result = []; if (Array.isArray(arrORnum)) { if (arr.length === arrORnum.length) { for (let i = 0; i < arr.length; i++) { if (includeEnd) { if (arr[i] <= arrORnum[i]) { result.push(arr[i]); } } else { if (arr[i] < arrORnum[i]) { result.push(arr[i]); } } } } } else { for (let i = 0; i < arr.length; i++) { if (includeEnd) { if (arr[i] <= arrORnum) { result.push(arr[i]); } } else { if (arr[i] < arrORnum) { result.push(arr[i]); } } } } return result; }; /** * Calculates the mean absolute deviation of an array. * @param {number[]} arr - The array * @returns {number} */ export const mad = (arr: number[]): number => { let result = 0; for (let i = 0; i < arr.length; i++) { result += Math.abs(arr[i] - Chalkboard.stat.mean(arr)); } return result / arr.length; }; /** * Returns the maximum value of an array. * @param {number[]} arr - The array * @returns {number} */ export const max = (arr: number[]): number => { let max = arr[0]; for (let i = 0; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } return max; }; /** * Calculates the mean of an array. * @param {number[]} arr - The array * @param {"arithmetic" | "geometric" | "harmonic"} [type="arithmetic"] - The type of mean, which can be "arithmetic", "geometric", or "harmonic" * @returns {number} */ export const mean = (arr: number[], type: "arithmetic" | "geometric" | "harmonic" = "arithmetic"): number => { let result = 0; if (type === "arithmetic") { for (let i = 0; i < arr.length; i++) { result += arr[i]; } return result / arr.length; } else if (type === "geometric") { result = 1; for (let i = 0; i < arr.length; i++) { result *= arr[i]; } return Chalkboard.real.root(Math.abs(result), arr.length); } else if (type === "harmonic") { for (let i = 0; i < arr.length; i++) { result += 1 / arr[i]; } return arr.length / result; } else { throw new TypeError('Parameter "type" must be "arithmetic", "geometric", or "harmonic".'); } }; /** * Calculates the moving mean of an array. * @param {number[]} arr - The array * @param {number} windowSize - The size of the moving window * @returns {number[]} */ export const meanMoving = (arr: number[], windowSize: number): number[] => { if (windowSize <= 0 || windowSize > arr.length) { throw new RangeError('Parameter "windowSize" must be greater than 0 and less than or equal to the array length.'); } const result = []; for (let i = 0; i <= arr.length - windowSize; i++) { const windowArr = arr.slice(i, i + windowSize); result.push(Chalkboard.stat.sum(windowArr) / windowSize); } return result; }; /** * Calculates the weighted mean of an array. * @param {number[]} arr - The array * @param {number[]} weights - The weights * @returns {number} */ export const meanWeighted = (arr: number[], weights: number[]): number => { if (arr.length !== weights.length) { throw new RangeError('Parameters "values" and "weights" must have the same length.'); } let sum = 0, weightSum = 0; for (let i = 0; i < arr.length; i++) { sum += arr[i] * weights[i]; weightSum += weights[i]; } return sum / weightSum; }; /** * Returns the median value of an array. * @param {number[]} arr - The array * @returns {number} */ export const median = (arr: number[]): number => { if (arr.length === 0) return NaN; const copy = arr.slice(); const mid = Math.floor(copy.length / 2); if (copy.length % 2 === 1) { return $quickselect(copy, mid); } else { return ($quickselect(copy, mid - 1) + $quickselect(copy, mid)) / 2.0; } }; /** * Returns the minimum value of an array. * @param {number[]} arr - The array * @returns {number} */ export const min = (arr: number[]): number => { let min = arr[0]; for (let i = 0; i < arr.length; i++) { if (arr[i] < min) { min = arr[i]; } } return min; }; /** * Returns the mode (the most recurring value) of an array. * @param {number[]} arr - The array * @returns {number} */ export const mode = (arr: number[]): number => { if (arr.length === 0) return NaN; const frequency = new Map(); let maxFreq = 0; let result = arr[0]; for (let i = 0; i < arr.length; i++) { const val = arr[i]; const count = (frequency.get(val) || 0) + 1; frequency.set(val, count); if (count > maxFreq) { maxFreq = count; result = val; } } return result; }; /** * Calculates the product of all elements in an array. * @param {number[]} arr - The array * @returns {number} */ export const mul = (arr: number[]): number => { let result = 1; for (let i = 0; i < arr.length; i++) { result *= arr[i]; } return result; }; /** * Calculates the negation of all the elements of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const negate = (arr: number[]): number[] => { const result: number[] = []; for (let i = 0; i < arr.length; i++) { result.push(-arr[i]); } return result; }; /** * Calculates the norm of an array. * @param {number[]} arr - The array * @param {"L0" | "L1" | "L2" | "LInfinity"} [type="L2"] - The type of norm, which can be "L0", "L1", "L2", or "LInfinity" * @returns {number} */ export const norm = (arr: number[], type: "L0" | "L1" | "L2" | "LInfinity" = "L2"): number => { let result = 0; if (type === "L0") { for (let i = 0; i < arr.length; i++) { if (arr[i] !== 0) { result += 1; } } return result; } else if (type === "L1") { for (let i = 0; i < arr.length; i++) { result += Math.abs(arr[i]); } return result; } else if (type === "L2") { for (let i = 0; i < arr.length; i++) { result += arr[i] * arr[i]; } return Chalkboard.real.sqrt(result); } else if (type === "LInfinity") { return Math.abs(Chalkboard.stat.max(arr)); } else { throw new TypeError('Parameter "type" must be "L0", "L1", "L2", or "LInfinity".'); } }; /** * Calculates the value of the standard normal distribution at a given point. * @param {number} x - The point * @returns {number} */ export const normal = (x: number): number => { const standardNormal = Chalkboard.real.define((x) => 1 / Math.sqrt(2 * Math.PI) * Math.exp(-0.5 * x * x)); const f = standardNormal.rule as (x: number) => number; return f(x); }; /** * Calculates the normalization of an array. * @param {number[]} arr - The array * @param {"L0" | "L1" | "L2" | "LInfinity"} [type="L2"] - The type of norm to normalize with, which can be "L0", "L1", "L2", or "LInfinity" * @returns {number[]} */ export const normalize = (arr: number[], type: "L0" | "L1" | "L2" | "LInfinity" = "L2"): number[] => { const result = []; const norm = Chalkboard.stat.norm(arr, type); for (let i = 0; i < arr.length; i++) { result.push(arr[i] / norm); } return result; }; /** * Calculates the norm squared of an array. * @param {number[]} arr - The array * @param {"L0" | "L1" | "L2" | "LInfinity"} [type="L2"] - The type of norm squared, which can be "L0", "L1", "L2", or "LInfinity" * @returns {number} */ export const normsq = (arr: number[], type: "L0" | "L1" | "L2" | "LInfinity" = "L2"): number => { let result = 0; if (type === "L0") { for (let i = 0; i < arr.length; i++) { if (arr[i] !== 0) { result += 1; } } return result * result; } else if (type === "L1") { for (let i = 0; i < arr.length; i++) { result += Math.abs(arr[i]); } return result * result; } else if (type === "L2") { for (let i = 0; i < arr.length; i++) { result += arr[i] * arr[i]; } return result; } else if (type === "LInfinity") { return Math.abs(Chalkboard.stat.max(arr)) * Math.abs(Chalkboard.stat.max(arr)); } else { throw new TypeError('Parameter "type" must be "L0", "L1", "L2", or "LInfinity".'); } }; /** * Pads an array with a specified number to a given length. * @param {number[]} arr - The array * @param {number} length - The desired length of the array * @param {number} [num=0] - The number to pad with (default is 0) * @returns {number[]} */ export const pad = (arr: number[], length: number, num: number = 0): number[] => { const result = arr.slice(); while (result.length < length) { result.push(num); } return result; }; /** * Calculates the percentile of a number in an array. * @param {number[]} arr - The array * @param {number} num - The number * @returns {number} */ export const percentile = (arr: number[], num: number): number => { let result = 0; for (let i = 0; i < arr.length; i++) { if (num >= arr[i]) { result++; } } return (result / arr.length) * 100; }; /** * Prints an array in the console. * @param {number[]} arr - The array * @returns {void} */ export const print = (arr: number[]): void => { console.log(Chalkboard.stat.toString(arr)); }; /** * Calculates a quartile of an array. * @param {number[]} arr - The array * @param {"Q1" | "Q2" | "Q3"} type - The type of quartile, which can be "Q1", "Q2", or "Q3" * @returns {number} */ export const quartile = (arr: number[], type: "Q1" | "Q2" | "Q3"): number => { if (arr.length === 0) return NaN; const copy = arr.slice(); if (type === "Q2") return Chalkboard.stat.median(copy); const mid = Math.floor(copy.length / 2); if (type === "Q1") { const q1Mid = Math.floor(mid / 2); if (mid % 2 === 1) return $quickselect(copy, q1Mid); else return ($quickselect(copy, q1Mid - 1) + $quickselect(copy, q1Mid)) / 2.0; } else if (type === "Q3") { const offset = copy.length % 2 === 0 ? mid : mid + 1; const q3Mid = offset + Math.floor(mid / 2); if (mid % 2 === 1) return $quickselect(copy, q3Mid); else return ($quickselect(copy, q3Mid - 1) + $quickselect(copy, q3Mid)) / 2.0; } else { throw new TypeError('Parameter "type" must be "Q1", "Q2", or "Q3".'); } }; /** * Returns an array with random elements. * @param {number} length - The length of the array * @param {number} [inf=0] - The lower bound (optional, defaults to 0) * @param {number} [sup=1] - The upper bound (optional, defaults to 1) * @returns {number[]} */ export const random = (length: number, inf: number = 0, sup: number = 1): number[] => { const result = []; for (let i = 0; i < length; i++) { result.push(Chalkboard.numb.random(inf, sup)); } return result; }; /** * Returns the range (the maximum value minus the minimum value) of an array. * @param {number[]} arr - The array * @returns {number} */ export const range = (arr: number[]): number => { return Chalkboard.stat.max(arr) - Chalkboard.stat.min(arr); }; /** * Calculates a regression model for an array of data. * @param {number[][]} data - The data, an array of arrays that represent an ordered pair of 2D coordinates * @param {"linear" | "polynomial" | "power" | "exponential" | "logarithmic"} [type="linear"] - The type of regression model, which can be "linear", "polynomial", "power", "exponential", or "logarithmic" * @param {number} [degree=2] - The degree of the leading coefficient of the polynomial regression model * @returns {ChalkboardFunction} */ export const regression = (data: number[][], type: "linear" | "polynomial" | "power" | "exponential" | "logarithmic" = "linear", degree: number = 2): ChalkboardFunction => { if (type === "linear") { let x = 0, y = 0; let xx = 0, xy = 0; for (let i = 0; i < data.length; i++) { x += data[i][0]; y += data[i][1]; xx += data[i][0] * data[i][0]; xy += data[i][0] * data[i][1]; } const a = (data.length * xy - x * y) / (data.length * xx - x * x); const b = y / data.length - (a * x) / data.length; return Chalkboard.real.define((x) => a * x + b); } else if (type === "polynomial") { const A = Chalkboard.matr.init(); for (let i = 0; i < data.length; i++) { A.push([]); for (let j = 0; j <= degree; j++) { A[i].push(Chalkboard.real.pow(data[i][0], j) as number); } } const AT = Chalkboard.matr.transpose(A); const B = Chalkboard.matr.init(); for (let i = 0; i < data.length; i++) { B.push([data[i][1]]); } const ATA = Chalkboard.matr.mul(AT, A); const ATAI = Chalkboard.matr.invert(ATA); const x = Chalkboard.matr.mul(Chalkboard.matr.mul(ATAI, AT), B); const coeff: number[] = []; for (let i = 0; i < x.length; i++) { coeff.push(x[i][0]); } return Chalkboard.real.define((x) => { let result = coeff[0]; result += coeff[1] * x; for (let i = 2; i <= degree; i++) { result += coeff[i] * Math.pow(x, i); } return result; }); } else if (type === "power") { const arr = [0, 0, 0, 0]; for (let i = 0; i < data.length; i++) { arr[0] += Math.log(data[i][0]); arr[1] += Math.log(data[i][1]) * Math.log(data[i][0]); arr[2] += Math.log(data[i][1]); arr[3] += Math.log(data[i][0]) * Math.log(data[i][0]); } const a = Chalkboard.E((arr[2] - ((data.length * arr[1] - arr[2] * arr[0]) / (data.length * arr[3] - arr[0] * arr[0])) * arr[0]) / data.length); const b = (data.length * arr[1] - arr[2] * arr[0]) / (data.length * arr[3] - arr[0] * arr[0]); return Chalkboard.real.define((x) => a * Math.pow(x, b)); } else if (type === "exponential") { const arr = [0, 0, 0, 0, 0, 0]; for (let i = 0; i < data.length; i++) { arr[0] += data[i][0]; arr[1] += data[i][1]; arr[2] += data[i][0] * data[i][0] * data[i][1]; arr[3] += data[i][1] * Math.log(data[i][1]); arr[4] += data[i][0] * (data[i][1] * Math.log(data[i][1])); arr[5] += data[i][0] * data[i][1]; } const a = Chalkboard.E((arr[2] * arr[3] - arr[5] * arr[4]) / (arr[1] * arr[2] - arr[5] * arr[5])); const b = (arr[1] * arr[4] - arr[5] * arr[3]) / (arr[1] * arr[2] - arr[5] * arr[5]); return Chalkboard.real.define((x) => a * Math.exp(b * x)); } else if (type === "logarithmic") { const arr = [0, 0, 0, 0]; for (let i = 0; i < data.length; i++) { arr[0] += Math.log(data[i][0]); arr[1] += data[i][1] * Math.log(data[i][0]); arr[2] += data[i][1]; arr[3] += Math.log(data[i][0]) * Math.log(data[i][0]); } const a = (arr[2] - ((data.length * arr[1] - arr[2] * arr[0]) / (data.length * arr[3] - arr[0] * arr[0])) * arr[0]) / data.length; const b = (data.length * arr[1] - arr[2] * arr[0]) / (data.length * arr[3] - arr[0] * arr[0]); return Chalkboard.real.define((x) => a + b * Math.log(x)); } else { throw new TypeError('Parameter "type" must be "linear", "polynomial", "power", "exponential", or "logarithmic".'); } }; /** * Performs resampling on an array depending on the specified resampling method. * @param {number[]} arr - The array * @param {number} [samples] - The number of samples (optional, default is 100 for "bootstrap" and the length of the array for "jackknife") * @param {"bootstrap" | "jackknife"} [type="bootstrap"] - The type of resampling method, which can be "bootstrap" or "jackknife" * @returns {number[][]} */ export const resampling = (arr: number[], samples?: number, type: "bootstrap" | "jackknife" = "bootstrap"): number[][] => { if (type === "bootstrap") { const numSamples = samples ?? 100; const result = []; for (let i = 0; i < numSamples; i++) { const sample = []; for (let j = 0; j < arr.length; j++) { sample.push(arr[Math.floor(Math.random() * arr.length)]); } result.push(sample); } return result; } else if (type === "jackknife") { const numSamples = samples ?? arr.length; const allJackknifeSamples = []; for (let i = 0; i < arr.length; i++) { allJackknifeSamples.push(arr.slice(0, i).concat(arr.slice(i + 1))); } if (numSamples < allJackknifeSamples.length) { const selectedSamples = []; const usedIndices = new Set(); while (selectedSamples.length < numSamples) { const randomIndex = Math.floor(Math.random() * allJackknifeSamples.length); if (!usedIndices.has(randomIndex)) { usedIndices.add(randomIndex); selectedSamples.push(allJackknifeSamples[randomIndex]); } } return selectedSamples; } return allJackknifeSamples; } else { throw new TypeError('Parameter "type" must be "bootstrap" or "jackknife".'); } }; /** * Reverses the elements of an array. * @param {number[]} arr - The array * @returns {number[]} */ export const reverse = (arr: number[]): number[] => { const result: number[] = []; for (let i = arr.length - 1; i >= 0; i--) { result.push(arr[i]); } return result; }; /** * Calculates the scalar multiplication of an array. * @param {number[]} arr - The array * @param {number} num - The scalar * @returns {number[]} */ export const scl = (arr: number[], num: number): number[] => { const result: number[] = []; for (let i = 0; i < arr.length; i++) { result.push(arr[i] * num); } return result; }; /** * Returns an array with its elements randomly shuffled. * @param {number[]} arr - The array * @returns {number[]} */ export const shuffle = (arr: number[]): number[] => { let index, temp, rindex; for (index = arr.length - 1; index > 0; index--) { rindex = Math.floor(Chalkboard.numb.random(0, index + 1)); temp = arr[index]; arr[index] = arr[rindex]; arr[rindex] = temp; } return arr; }; /** * Calculates the skewness of an array. * @param {number[]} arr - The array * @returns {number} */ export const skewness = (arr: number[]): number => { let result = 0; const mean = Chalkboard.stat.mean(arr); const deviation = Chalkboard.stat.deviation(arr); for (let i = 0; i < arr.length; i++) { result += (arr[i] - mean) * (arr[i] - mean) * (arr[i] - mean); } return result / ((arr.length - 1) * (deviation * deviation * deviation)); }; /** * Calculates the subtraction of two arrays. * @param {number[]} arr1 - The first array * @param {number[]} arr2 - The second array * @returns {number[]} */ export const sub = (arr1: number[], arr2: number[]): number[] => { if (arr1.length !== arr2.length) { throw new RangeError('Parameters "arr1" and "arr2" must have the same length.'); } const result: number[] = []; for (let i = 0; i < arr1.length; i++) { result.push(arr1[i] - arr2[i]); } return result; }; /** * Returns an array of all the subsets of an array. * @param {number[]} arr - The array * @returns {number[][]} */ export const subsets = (arr: number[]): number[][] => { let result: number[][] = [[]]; arr.sort(); for (let i = 0; i < arr.length; i++) { if (i === 0 || arr[i] !== arr[i - 1]) { const curr = arr[i]; const subsetsWithCurr = []; for (let j = 0; j < result.length; j++) { const subset = result[j].slice(); subset.push(curr); subsetsWithCurr.push(subset); } result = result.concat(subsetsWithCurr); } } return result; }; /** * Calculates the sum of all elements in an array. * @param {number[]} arr - The array * @returns {number} */ export const sum = (arr: number[]): number => { let result = 0; for (let i = 0; i < arr.length; i++) { result += arr[i]; } return result; }; /** * Converts an array to a matrix. * @param {number[]} arr - The array * @param {number} rows - The number of rows of the matrix * @param {number} [cols=rows] - The number of columns of the matrix (optional, defaults to the number of rows to make a square matrix) * @returns {ChalkboardMatrix} */ export const toMatrix = (arr: number[], rows: number, cols: number = rows): ChalkboardMatrix => { const result = Chalkboard.matr.init(); let index = 0; for (let i = 0; i < rows; i++) { result[i] = []; for (let j = 0; j < cols; j++) { if (index < arr.length) { result[i].push(arr[index]); } else { result[i].push(0); } index++; } } return result; }; /** * Converts an array to an object. * @param {number[]} arr - The array * @returns {object} */ export const toObject = (arr: number[]): object => { const result: { [key: string]: number } = {}; for (let i = 0; i < arr.length; i++) { result["_" + i.toString()] = arr[i]; } return result; }; /** * Converts an array to a set. * @param {number[]} arr - The array * @returns {ChalkboardSet} */ export const toSet = (arr: number[]): ChalkboardSet => { return Chalkboard.abal.set(arr); }; /** * Converts an array to a string. * @param {number[]} arr - The array * @returns {string} */ export const toString = (arr: number[]): string => { return "[" + arr.join(", ") + "]"; }; /** * Converts an array to a tensor. * @param {number[]} arr - The array * @param {number[]} size - The size of the tensor * @returns {ChalkboardTensor} */ export const toTensor = (arr: number[], ...size: number[]): ChalkboardTensor => { if (Array.isArray(size[0])) { size = size[0]; } return Chalkboard.tens.resize(arr, ...size); }; /** * Converts an array to a vector. * @param {number[]} arr - The array * @param {number} dimension - The dimension of the vector, which can be 2, 3, or 4 * @param {number} [index=0] - The index of the array to start the vector * @returns {ChalkboardVector} */ export const toVector = (arr: number[], dimension: 2 | 3 | 4, index: number = 0): ChalkboardVector => { if (dimension === 2) { return Chalkboard.vect.init(arr[index], arr[index + 1]); } else if (dimension === 3) { return Chalkboard.vect.init(arr[index], arr[index + 1], arr[index + 2]); } else if (dimension === 4) { return Chalkboard.vect.init(arr[index], arr[index + 1], arr[index + 2], arr[index + 3]); } else { throw new RangeError('Parameter "dimension" must be 2, 3, or 4.'); } }; /** * Returns the unique elements of an array. * @template T * @param {T[]} arr - The array * @returns {T[]} */ export const unique = (arr: T[]): T[] => { if (arr.length === 0) return []; const firstType = typeof arr[0]; if (firstType === "number" || firstType === "string" || firstType === "boolean") return Array.from(new Set(arr)); const stableStringify = (obj: any): string => { const replacer = (key: string, value: any) => { if (value !== null && typeof value === "object" && !Array.isArray(value)) { const sortedObj: Record = {}; Object.keys(value).sort().forEach(k => { sortedObj[k] = value[k]; }); return sortedObj; } return value; }; return JSON.stringify(obj, replacer); }; const seen = new Map(); for (const item of arr) { const typePrefix = item === null ? "null" : typeof item; const valuePart = (typePrefix === "undefined" || Number.isNaN(item as any)) ? "" : stableStringify(item); const key = `${typePrefix}:${valuePart}`; if (!seen.has(key)) seen.set(key, item); } return Array.from(seen.values()); }; /** * Calculates the variance of an array. * @param {number[]} arr - The array * @returns {number} */ export const variance = (arr: number[]): number => { let result = 0; for (let i = 0; i < arr.length; i++) { result += (arr[i] - Chalkboard.stat.mean(arr)) * (arr[i] - Chalkboard.stat.mean(arr)); } return result / arr.length; }; /** * Calculates the standardization of the elements of an array according to their mean and standard deviation. * @param {number[]} arr - The array * @returns {number[]} */ export const zscored = (arr: number[]): number[] => { let result: number[] = []; const mean = Chalkboard.stat.mean(arr); const deviation = Chalkboard.stat.deviation(arr); for (let i = 0; i < arr.length; i++) { result.push((arr[i] - mean) / deviation); } return result; }; } }