als-statistics
Version:
Modular JS statistics toolkit for Node.js and the browser: descriptive stats, correlations (Pearson/Spearman/Kendall), t-tests & ANOVA (Student/Welch), reliability (Cronbach’s alpha), regression (linear/logistic), clustering (DBSCAN/HDBSCAN), and table/co
262 lines (229 loc) • 11.2 kB
JavaScript
const nd = v => v === undefined
import MovingAverage from './ma.js';
import confidenceInterval from './confidence.js';
import mode from './mode.js';
import outliersZScore from './z-outliers.js';
import weightedMean from './weighted-mean.js';
import slopeByIndex from './slope-by-index.js';
import { frequencies, recode } from './frequencies.js';
class Stats {
static sorted({ values }) { return values.slice().sort((a, b) => a - b) }
static sum({ values }) { return values.reduce((acc, val) => acc + val, 0) }
static mean({ values, sum }) { return ((sum ?? Stats.sum({ values })) / values.length) }
static mode({ values }) { return mode(values) }
static slope({ values }) { return slopeByIndex({ values }) }
static confidenceInterval({ mean, stdDevSample, values }) {
if (nd(mean)) mean = Stats.mean({ values })
if (nd(stdDevSample)) stdDevSample = Stats.stdDevSample({ values })
return confidenceInterval({ n: values.length, mean, stdDevSample })
}
static weightedMean({ values }, weights) { return weightedMean(weights, values) }
static outliersZScore({ values, zScores }, threshold, twoFactors, below) {
if (nd(threshold)) threshold = 3
if (nd(twoFactors)) twoFactors = true
if (nd(zScores)) zScores = Stats.zScores({ values })
return outliersZScore({ zScores, values }, threshold, twoFactors, below)
}
static ma({ values }, windowSize) { return new MovingAverage({ values, n: values.length }, windowSize).result }
static sumOfSquares({ values }) { return values.reduce((acc, v) => acc + v ** 2, 0) } // Сумма квадратов (энергетическая норма). Σ(x_i^2)
static varianceSample({ values, mean }) { // Sample (selected): Bessel's correction. s^2 = (1/(n-1)) * sum( (x - mean)^2 )
const n = values.length
if (nd(mean)) mean = Stats.mean({ values })
return n < 2 ? 0 : values.reduce((acc, val) => acc + (val - mean) ** 2, 0) / (n - 1);
}
static variance({ values, mean }) {
if (nd(mean)) mean = Stats.mean({ values })
return values.reduce((acc, v) => acc + Math.pow(v - mean, 2), 0) / values.length;
}
static stdDev({ variance, values }) {
if (nd(variance)) variance = Stats.variance({ values })
return Math.sqrt(variance)
}
static stdDevSample({ values, varianceSample }) {
if (nd(varianceSample)) varianceSample = Stats.varianceSample({ values })
return Math.sqrt(varianceSample)
}
static cv({ mean, stdDev, values }) {
if (nd(mean)) mean = Stats.mean({ values })
if (nd(stdDev)) stdDev = Stats.stdDev({ values })
return mean === 0 ? 0 : stdDev / mean
}
static zScores({ values, stdDev, mean },sample = false) {
if (nd(mean)) mean = Stats.mean({ values })
if (nd(stdDev)) {
stdDev = sample ? Stats.stdDevSample({ values }) : Stats.stdDev({ values })
}
return stdDev === 0 ? values.map(v => 0) : values.map(v => (v - mean) / stdDev)
}
static skewness({ values, mean, stdDev }) {
const n = values.length
if (nd(mean)) mean = Stats.mean({ values })
if (nd(stdDev)) stdDev = Stats.stdDev({ values })
if (stdDev === 0) return 0; // If stdDev = 0
return values.reduce((acc, v) => acc + ((v - mean) / stdDev) ** 3, 0) / n;
}
static kurtosis({ zScores, values }) {
if (nd(zScores)) zScores = Stats.zScores({ values })
return (zScores.reduce((acc, z) => acc + z ** 4, 0) / zScores.length) - 3;
}
static skewnessSample({ mean, values, stdDevSample }) {
const n = values.length
if (nd(mean)) mean = Stats.mean({ values })
if (nd(stdDevSample)) stdDevSample = Stats.stdDevSample({ values })
if (n < 3) return 0;
const numerator = values.reduce((acc, val) => acc + ((val - mean) / stdDevSample) ** 3, 0);
const factor = n / ((n - 1) * (n - 2));
return factor * numerator;
}
static kurtosisSample({ mean, values, stdDevSample }) {
const n = values.length
if (nd(mean)) mean = Stats.mean({ values })
if (nd(stdDevSample)) stdDevSample = Stats.stdDevSample({ values })
if (n < 4) return 0;
const z4sum = values.reduce((acc, val) => acc + Math.pow((val - mean) / stdDevSample, 4), 0);
const a = (n * (n + 1)) / ((n - 1) * (n - 2) * (n - 3));
const b = 3 * Math.pow(n - 1, 2) / ((n - 2) * (n - 3));
return a * z4sum - b;
}
static geometricMean({ values, min = 1e-12 }) {
const n = values.length;
if (!n) return NaN;
let sumLog = 0;
for (let v of values) {
if (!Number.isFinite(v)) return NaN;
if (v < min) v = min;
sumLog += Math.log(v);
}
return Math.exp(sumLog / n);
}
static flatness({ geometricMean, mean, values }) {
if (nd(geometricMean)) geometricMean = Stats.geometricMean({ values })
if (nd(mean)) mean = Stats.mean({ values })
return mean === 0 ? 0 : geometricMean / mean;
}
static harmonicMean({ values }) { // HM = n / Σ(1/x_i) . Only for x_i > 0
const n = values.length
if (!values.every(v => v > 0)) return NaN;
const denom = values.reduce((acc, v) => acc + 1 / v, 0);
return n / denom;
}
static noiseStability({ values, mean }) {
const n = values.length
if (nd(mean)) mean = Stats.mean({ values })
if (n < 2) return 0;
const variance = values.reduce((acc, v) => acc + (v - mean) ** 2, 0) / n;
return Math.sqrt(variance); // stdDev
}
static spectralPowerDensityArray({ values, sumOfSquares }) {
if (nd(sumOfSquares)) sumOfSquares = Stats.sumOfSquares({ values })
return sumOfSquares === 0 ? values.map(() => 0) : values.map(v => (v ** 2) / sumOfSquares);
}
static spectralPowerDensityMetric({ spectralPowerDensityArray, values }) {
if (nd(spectralPowerDensityArray)) spectralPowerDensityArray = Stats.spectralPowerDensityArray({ values })
const spdArray = spectralPowerDensityArray;
const geometricMean = Math.exp(spdArray.reduce((sum, x) => sum + Math.log(x + Number.EPSILON), 0) / spdArray.length);
const arithmeticMean = spdArray.reduce((sum, x) => sum + x, 0) / spdArray.length;
return geometricMean / arithmeticMean; // Близкое к 1 → равномерный шум, ближе к 0 → голос
}
static zScoresSorted({ zScores: zs, values }) {
if (nd(zs)) zs = Stats.zScores({ values })
const objs = zs.map((z, i) => ({ z, i })).sort((a, b) => a.z - b.z);
const zScores = [], indexes = [];
objs.forEach(({ z, i }) => { zScores.push(z); indexes.push(i); });
return { zScores, indexes }
}
static median({ values, sorted }) {
const s = sorted ? sorted : Stats.sorted({ values });
const m = s.length >> 1;
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
}
static percentile({ values, sorted }, q) {
if (q > 1) q = q / 100
const s = sorted ? sorted : Stats.sorted({ values });
const n = s.length;
if (!n) return undefined;
const pos = (n - 1) * q;
const i = Math.floor(pos), f = pos - i;
return f ? s[i] * (1 - f) + s[i + 1] * f : s[i];
}
static q1(obj) { return Stats.percentile(obj, 25) }
static q3(obj) { return Stats.percentile(obj, 75) }
static p10(obj) { return Stats.percentile(obj, 10) }
static p90(obj) { return Stats.percentile(obj, 90) }
static frequencies({ values, labels }) { return frequencies({ values, labels }) }
static recode({ values }, ranges) { return recode({ values }, ranges) }
static relativeFrequencies({ frequencies, values, labels }) { // Relative frequencies (the proportion of each value relative to the total)
const n = values.length, relFreq = {}
if (nd(frequencies)) frequencies = Stats.frequencies({ values, labels })
for (const key in frequencies) { relFreq[key] = frequencies[key] / n }
return relFreq;
}
static mad({ sorted, median, values }) { // Median Absolute Deviation
if (values.length === 0) return 0;
if (!sorted) sorted = Stats.sorted({ values });
if (nd(median)) median = Stats.median({ values, sorted })
const deviations = sorted.map(x => Math.abs(x - median)).sort((a, b) => a - b);
const m = Math.floor(deviations.length / 2);
return (deviations.length % 2 === 0) ? (deviations[m - 1] + deviations[m]) / 2 : deviations[m];
}
static relativeDispersion({ values, median, stdDev }) {
if (nd(median)) median = Stats.median({ values })
if (nd(stdDev)) stdDev = Stats.stdDev({ values })
return median === 0 ? 0 : stdDev / median
}
static iqr({ values, q1, q3, sorted }) {
if (nd(q1)) q1 = Stats.q1({ values, sorted })
if (nd(q3)) q3 = Stats.q3({ values, sorted })
return q3 - q1
}
static range({ min, max, sorted, values }) {
if (nd(min) || nd(max)) {
if (nd(sorted)) sorted = Stats.sorted({ values })
min = Stats.min({ sorted })
max = Stats.max({ sorted })
}
return max - min
}
static min({ sorted, values }) {
if (nd(sorted)) sorted = Stats.sorted({ values })
return sorted[0]
}
static max({ sorted, values }) {
if (nd(sorted)) sorted = Stats.sorted({ values })
return sorted[sorted.length - 1]
}
static zScore({ stdDev, mean, values }, v) {
if (nd(stdDev)) stdDev = Stats.stdDev({ values })
if (nd(mean)) mean = Stats.stdDev({ values })
return stdDev === 0 ? 0 : (v - mean) / stdDev
}
static normalizedValues({ values, range, min }) {
if (nd(min)) min = Stats.min({ values })
if (nd(range)) range = Stats.range({ values, min })
return range === 0 ? values.map(() => 0) : values.map(v => (v - min) / range);
}
static outliersIQR({ q1, q3, iqr, values }) {
if (nd(q1)) {
const sorted = Stats.sorted({ values })
q1 = Stats.q1({ values, sorted })
q3 = Stats.q3({ values, sorted })
iqr = Stats.iqr({ values, sorted })
}
const lowerBound = q1 - 1.5 * iqr, upperBound = q3 + 1.5 * iqr;
return values.filter(v => v < lowerBound || v > upperBound);
}
static xValues = ({ values }) => Array.from({ length: values.length }, (_, i) => i + 1)
static regressionSlope({ values, xValues, mean }, customX) {
const x = customX || xValues || Stats.xValues;
if (!Array.isArray(x) || x.length !== values.length) throw new Error("x must be an array of same length as values");
if (nd(mean)) mean = Stats.mean
const n = values.length
const mean2 = Stats.mean({ values: x })
const varianceSample = Stats.varianceSample({ values: x });
let sumCov = 0;
for (let i = 0; i < n; i++) { sumCov += (values[i] - mean) * (x[i] - mean2) }
const cov = n < 2 ? 0 : sumCov / (n - 1)
return varianceSample === 0 ? 0 : cov / varianceSample;
}
}
export default Stats