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
129 lines (105 loc) • 5.56 kB
JavaScript
import { describe, it } from 'node:test';
import assert from 'node:assert';
import Regression from '../../../lib/analyze/regression/index.js';
function in01(x) { return x >= 0 && x <= 1; }
describe('LogisticRegression (wrapper with steps)', () => {
it('autodetects predictors when xNames omitted', () => {
const table = {
X1: [0, 1, 2, 3, 4],
X2: [1, 1, 1, 0, 0],
Y: [0, 0, 0, 1, 1],
};
const model = new Regression(table, { yName: 'Y', xNames: undefined, type: 'logistic' }); // без xNames
const step0 = model.steps[0];
// X1 и X2 должны быть выбраны (исключая Y)
assert.deepStrictEqual(step0.xNames.sort(), ['X1', 'X2'].sort());
assert.strictEqual(step0.yName, 'Y');
assert.strictEqual(step0.step, 0);
});
it('learns a simple separable boundary (accuracy >= 0.8)', () => {
const table = {
X: [-2, -1, 0, 1, 2],
Y: [0, 0, 0, 1, 1],
};
const model = new Regression(table, { yName: 'Y', xNames: ['X'], type: 'logistic' });
const step0 = model.steps[0];
assert.ok(step0.accuracy >= 0.8, `Expected accuracy >= 0.8, got ${step0.accuracy}`);
assert.strictEqual(step0.coefficients.length, 2); // [Intercept, X]
assert.ok(step0.coefficients.every(Number.isFinite));
});
it('results returns array with proper structure for each step', () => {
const table = {
X: [0, 1, 2, 3, 4],
Y: [0, 0, 0, 1, 1],
};
const reg = new Regression(table, { yName: 'Y', xNames: ['X'], type: 'logistic' }).next([]); // создадим 2 шага (второй без новых предикторов)
const results = reg.results;
assert.ok(Array.isArray(results));
assert.strictEqual(results.length, 2);
for (const r of results) {
assert.ok(Array.isArray(r.Variable));
assert.ok(Array.isArray(r.Coefficient));
assert.strictEqual(r.Variable.length, r.Coefficient.length);
assert.ok(r.n > 0);
assert.ok(r.step >= 0);
assert.ok(r.Accuracy >= 0 && r.Accuracy <= 1);
}
});
it('throws if next() receives unknown predictor name (non-interaction)', () => {
const table = {
X: [0, 1, 2, 3, 4],
Y: [0, 0, 0, 1, 1],
};
const reg = new Regression(table, { yName: 'Y', xNames: ['X'], type: 'logistic' });
assert.throws(() => reg.next(['Q']).steps[1], /undefined|values|Cannot read/i);
});
it('next() adds moderator and interaction on cloned table', () => {
const table = { X: [0, 1, 2, 3, 4], Z: [0, 1, 0, 1, 0], Y: [0, 0, 0, 1, 1] };
const reg = new Regression(table, { yName: 'Y', xNames: ['X'], type: 'logistic' });
const beforeCols = Object.keys(table);
reg.next(['Z', 'X*Z']); // добавляем модератор и интеракцию
const step1 = reg.steps[1];
// в модели появился новый признак и интеракция
assert.ok(step1.xNames.includes('Z'));
assert.ok(step1.xNames.includes('X*Z'));
// интеракция добавилась в КЛОН step1.table, исходная таблица не изменилась
assert.ok(!Object.keys(table).includes('X*Z'), 'Original table should not have X*Z');
assert.ok(Object.keys(step1.columns).includes('X*Z'), 'Cloned table should have X*Z');
// и точность посчиталась корректно (в [0,1])
assert.ok(step1.accuracy >= 0 && step1.accuracy <= 1);
assert.deepStrictEqual(Object.keys(table), beforeCols); // исходник не мутирован
});
});
describe('Regression.LogisticRegression (core)', () => {
it('predictProba returns values in [0,1] and predict returns 0/1', () => {
const table = {
X: [-2, -1, 0, 1, 2],
Y: [0, 0, 0, 1, 1],
};
const core = new Regression.LogisticRegression(table, 'Y', ['X'], 'logistic').calculate();
const proba = core.predictProba(core.X);
assert.strictEqual(proba.length, core.n);
assert.ok(proba.every(p => in01(p)));
const yhat = core.predict(core.X, 0.5);
assert.strictEqual(yhat.length, core.n);
assert.ok(yhat.every(v => v === 0 || v === 1));
// согласованность с рассчитанной точностью
const correct = yhat.reduce((acc, v, i) => acc + (v === core.y[i] ? 1 : 0), 0) / core.n;
assert.ok(Math.abs(correct - core.accuracy) < 1e-12);
});
it('works with two predictors and converges to sensible accuracy', () => {
const table = {
X1: [0, 1, 2, 3, 4, 5],
X2: [1, 0, 1, 0, 1, 0],
Y: [0, 0, 0, 1, 1, 1],
};
const core = new Regression.LogisticRegression(table, 'Y', ['X1', 'X2'], 0).calculate();
assert.ok(core.accuracy >= 0.66, `Expected accuracy >= 0.66, got ${core.accuracy}`);
assert.strictEqual(core.coefficients.length, 3); // [Intercept, X1, X2]
});
it('throws if used with empty xNames (defensive check recommended)', () => {
const table = { Y: [0, 1, 0, 1], X: [1, 2, 3, 4] };
// сейчас без твоего дополнительного guard-а это упадёт TypeError-ом — тест ожидает throw
assert.throws(() => new Regression.LogisticRegression(table, 'Y', [], 0).calculate());
});
});