UNPKG

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

142 lines (118 loc) 5.53 kB
import { describe, it } from 'node:test'; import assert from 'node:assert'; import { OneWayAnova } from '../../../lib/analyze/compare-means/one-way-anova.js'; import { CompareMeans } from '../../../lib/analyze/compare-means/index.js'; // помощник для сравнения с допуском function approx(a, b, eps = 1e-10) { assert.ok(Math.abs(a - b) <= eps, `expected ~${b}, got ${a}`); } describe('ANOVA via Comparative', () => { it('selects anova when k > 2 (compare getter)', () => { const data = { A: [1, 2, 3], B: [2, 3, 4], C: [3, 4, 5], } const cmp = new OneWayAnova(data); const res = cmp; // должен отдать anova assert.ok('F' in res && 'p' in res && 'dfBetween' in res && 'dfWithin' in res); assert.strictEqual(res.dfBetween, 2); assert.strictEqual(res.dfWithin, 3 * (3 - 1)); }); it('F≈0 и p≈1, когда средние групп равны (SSB=0)', () => { // Все группы имеют среднее ровно 2, но разная внутри-групповая дисперсия. const data = { A: [1, 2, 3], B: [2, 1, 3], C: [1.5, 2.5, 2.0], } const cmp = new OneWayAnova(data); const { F, p, dfBetween, dfWithin, msb, msw, ssb, ssw, descriptive } = cmp; assert.strictEqual(dfBetween, 2); assert.strictEqual(dfWithin, 3 * (3 - 1)); // 6 approx(ssb, 0); // межгрупповая сумма квадратов 0 approx(msb, 0); approx(msw, 0.75); // посчитано вручную approx(ssw, 4.5); assert.strictEqual(F, 0); assert.ok(p > 0.999999); // p очень близко к 1 // descriptive: длина и n корректны (имя может быть undefined — не проверяем) assert.strictEqual(descriptive.length, 3); assert.ok(descriptive.every(d => d && d.n === 3)); assert.ok(descriptive.every(d => typeof d.mean === 'number')); }); it('даёт большой F и маленькое p при сильно различающихся средних', () => { const data = { A: [1, 2, 1, 2], B: [5, 6, 5, 6], C: [8, 9, 8, 9], } const cmp = new OneWayAnova(data); const { F, p, dfBetween, dfWithin, msb, msw, ssb, ssw } = cmp; assert.strictEqual(dfBetween, 2); assert.strictEqual(dfWithin, 3 * (4 - 1)); // 9 // Точные значения: // grand mean = 5.166666..., ssb = 98.666666..., ssw = 3, // msb = 49.333333..., msw = 0.333333..., F = 148 approx(ssb, 98.66666666666666, 1e-10); approx(ssw, 3.0, 1e-12); approx(msb, 49.33333333333333, 1e-10); approx(msw, 0.3333333333333333, 1e-12); approx(F, 148, 1e-10); // p очень маленькое — порог делаем разумным assert.ok(p < 1e-6, `p-value should be tiny, got ${p}`); }); it('возвращает корректные df даже при большем числе групп/наблюдений', () => { const k = 5, n = 6; const data = {} for (let i = 0; i < k; i++) { const values = [] for (let j = 0; j < n; j++) { values.push(i + j / 10) } data[`G${i + 1}`] = values } const cmp = new OneWayAnova(data); const { dfBetween, dfWithin } = cmp; assert.strictEqual(dfBetween, k - 1); assert.strictEqual(dfWithin, k * (n - 1)); }); it('устойчиво работает, когда SSW=0 (идеально однородные внутри групп)', () => { // Группы константные, но с разными средними: внутри-дисперсия 0 → F=∞. const data = { A: [1, 1, 1, 1], B: [2, 2, 2, 2], C: [3, 3, 3, 3], } const cmp = new OneWayAnova(data); const { F, p, msw } = cmp; approx(msw, 0); // внутри-групповая средняя сумма квадратов нулевая // F должен быть числом или Infinity, но не NaN assert.ok(Number.isFinite(F) || F === Infinity, `F must be finite or Infinity, got ${F}`); // p может быть 0 или NaN — обе интерпретации допустимы при F=∞ и msw=0 assert.ok(p === 0 || Number.isNaN(p), `p should be 0 or NaN, got ${p}`); }); it('Anova: htmlTable генерируется', () => { const a = new OneWayAnova({ A: [1, 2, 3, 4], B: [2, 3, 4, 5] }); const html = a.htmlTable; assert.equal(typeof html, 'string'); assert.ok(html.includes('One-way ANOVA')); }); it('Welch ANOVA даёт отличные df/статистику при неравенстве дисперсий', () => { // Группы с разной дисперсией: const data = { G1: [10, 11, 9, 10], G2: [10, 30, -10, 50, -20], G3: [12, 13, 12, 11, 14] }; const cm = new CompareMeans(data); const classic = cm.anova(); const welch = cm.anovaWelch(); // Для данных с сильно разными дисперсиями Welch должен отличаться: assert.notEqual(classic.dfWithin, welch.dfWithin); assert.notEqual(classic.F, welch.F); // p-value обеих — в [0,1] assert.ok(classic.p >= 0 && classic.p <= 1); assert.ok(welch.p >= 0 && welch.p <= 1); }); });