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
66 lines (57 loc) • 2.44 kB
JavaScript
import CDF from '../cdf/index.js';
import { TestBase } from '../test-base/index.js';
function rankAverage(arr) { /** Compute average ranks with tie handling. Returns { ranks, tieGroups } */
const n = arr.length;
const idx = Array.from({ length: n }, (_, i) => i).filter(i => Number.isFinite(arr[i]));
const pairs = idx.map(i => [arr[i], i]).sort((a, b) => (a[0] - b[0]));
const ranks = new Array(n).fill(NaN);
const tieGroups = []; // sizes of tied groups
let i = 0; let r = 1;
while (i < pairs.length) {
let j = i + 1;
while (j < pairs.length && pairs[j][0] === pairs[i][0]) j++;
const size = j - i;
const avgRank = (r + (r + size - 1)) / 2;
for (let k = i; k < j; k++) { ranks[pairs[k][1]] = avgRank; }
if (size > 1) tieGroups.push(size);
r += size; i = j;
}
return { ranks, tieGroups };
}
export class Spearman extends TestBase { /** Spearman rank correlation (двухвыборочный) */
constructor(samples) {
super(samples, 'Spearman', ['r', 't', 'df', 'p'], { sameSize: false, minK: 2 });
this.#calc()
}
#calc() {
const s1 = this.samples[0], s2 = this.samples[1];
const n = Math.min(s1.n, s2.n);
if (s1.n !== n) s1.values = s1.values.slice(0, n);
if (s2.n !== n) s2.values = s2.values.slice(0, n);
this.n = n;
const x = s1.values, y = s2.values;
const rx = rankAverage(x).ranks, ry = rankAverage(y).ranks;
const mx = rx.reduce((a, b) => a + b, 0) / n, my = ry.reduce((a, b) => a + b, 0) / n; // 4) Пирсон по рангам
let num = 0, sx = 0, sy = 0;
for (let i = 0; i < n; i++) {
const dx = rx[i] - mx, dy = ry[i] - my;
num += dx * dy;
sx += dx * dx;
sy += dy * dy;
}
if (sx === 0 || sy === 0) this.#zeroXY(); // Если дисперсия в одном из рангов нулевая (все значения одинаковые)
else this.#calcpr(sx,sy,num)
}
#zeroXY() { this.r = 0; this.df = this.n - 2; this.t = 0; this.p = 1; }
#calcpr(sx,sy,num) {
this.r = num / Math.sqrt(sx * sy);
this.df = this.n - 2;
if (Math.abs(this.r) >= 1) {
this.t = this.r > 0 ? Infinity : -Infinity;
this.p = 0;
} else {
this.t = this.r * Math.sqrt(this.df / (1 - this.r * this.r));
this.p = 2 * (1 - CDF.t(Math.abs(this.t), this.df));
}
}
}