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
JavaScript
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);
});
});