UNPKG

als-statistics

Version:

A powerful and lightweight JavaScript library for descriptive statistics, regression, clustering, outlier detection, and noise analysis using a flexible table/column architecture.

190 lines (149 loc) 6.03 kB
const { describe, it } = require('node:test'); const assert = require('node:assert'); const LinearRegression = require('../../lib/table/instruments/linear-regression/index'); const { newTable } = require('../../lib/index'); function almostEqual(actual, expected, epsilon = 1e-6) { assert.ok(Math.abs(actual - expected) < epsilon, `Expected ${actual} ≈ ${expected}`); } describe('LinearRegression', () => { it('Simple Linear Regression (Y = 2X)', () => { const table = newTable({ X: [1, 2, 3, 4, 5], Y: [2, 4, 6, 8, 10] }); const model = new LinearRegression(table, 'Y', ['X']).calculate(); almostEqual(model.coefficients[0], 0); // intercept almostEqual(model.coefficients[1], 2); // slope almostEqual(model.r2, 1); }); it('With Mediator M (X -> M -> Y)', () => { const table = newTable({ X: [1, 2, 3, 4, 5, 6], M: [2, 4, 5, 7, 7, 9], Y: [3, 5, 7, 9, 10, 13] }); const model = new LinearRegression(table, 'Y', ['X']) .mediator('M') .calculate(); assert.ok(model.r2 > 0.8, `Expected R² > 0.8, got ${model.r2}`); assert.ok(model.result.Variable.includes('M'), 'Missing M as mediator'); }); it('With Moderator Z (interaction X*Z)', () => { const table = newTable({ X: [1, 2, 3, 4, 5], Z: [1, 2, 1, 2, 1], Y: [3, 6, 5, 10, 7] }); const model = new LinearRegression(table, 'Y', ['X']) .moderator('Z') .calculate(); const interactionName = 'X*Z'; assert.ok(model.result.Variable.includes(interactionName), 'Interaction term missing'); }); it('Regression with low R² (noisy data)', () => { const table = newTable({ X: [1, 2, 3, 4, 5], Y: [1, 1.1, 1.2, 0.9, 1] }); const model = new LinearRegression(table, 'Y', ['X']).calculate(); assert.ok(model.r2 < 0.5, `Expected low R², got ${model.r2}`); }); it('Autodetect xNames if not provided', () => { const table = newTable({ X1: [1, 2, 3, 4, 5], X2: [2, 1, 4, 3, 5], Y: [3, 4, 6, 7, 10] }); const model = new LinearRegression(table, 'Y').calculate(); assert.deepStrictEqual(model.xNames.sort(), ['X1', 'X2'].sort()); }); it('Returns correct result structure', () => { const table = newTable({ X: [1, 2, 3], Y: [2, 4, 6] }); const model = new LinearRegression(table, 'Y', ['X']).calculate(); const result = model.result; assert.ok(Array.isArray(result.Variable), 'Variable should be array'); assert.strictEqual(result.Variable.length, 2); // Intercept + X assert.strictEqual(result.Coefficient.length, 2); assert.strictEqual(result.StdError.length, 2); assert.strictEqual(result.pValue.length, 2); }); it('Automatically includes moderator and interaction term', () => { const table = newTable({ X: [1, 2, 3, 4, 5], Z: [0, 1, 0, 1, 0], Y: [2, 3, 6, 7, 10] }); const model = new LinearRegression(table, 'Y', ['X']) .moderator('Z') .calculate(); assert.ok(model.result.Variable.includes('Z')); assert.ok(model.result.Variable.includes('X*Z')); }); it('predict() returns the same as yHat', () => { const table = newTable({ X: [1, 2, 3, 4, 5], Y: [2, 4, 6, 8, 10] }); const model = new LinearRegression(table, 'Y', ['X']).calculate(); const predicted = model.predict(model.X); assert.deepStrictEqual(predicted, model.yHat); }); it('Throws on constant X (singular matrix)', () => { const table = newTable({ X: [1, 1, 1, 1, 1], Y: [2, 3, 4, 5, 6] }); assert.throws(() => { new LinearRegression(table, 'Y', ['X']).calculate(); }, /singular|constant/i); }); it('Detects impact of outliers on R²', () => { const table = newTable({ X: [1, 2, 3, 4, 100], // последний — выброс Y: [2, 4, 6, 8, 15] }); const model = new LinearRegression(table, 'Y', ['X']).calculate(); assert.ok(model.r2 < 0.95, `Expected R² < 0.95 due to outlier, got ${model.r2}`); }); it('Throws if predictor does not exist in table', () => { const table = newTable({ X: [1, 2, 3], Y: [2, 4, 6] }); assert.throws(() => { new LinearRegression(table, 'Y', ['Z']).calculate(); }, /undefined|not found/i); }); it('Throws if X and Y lengths are inconsistent', () => { const table = { columns: { X: { values: [1, 2, 3, 4] }, Y: { values: [2, 4] } // короче } }; assert.throws(() => { new LinearRegression(table, 'Y', ['X']).calculate(); }, /singular|constant/i); // теперь соответствует сообщению }); it('Works with large dataset (1000+ rows)', () => { const X = Array.from({ length: 1000 }, (_, i) => i + 1); const Y = X.map(x => 5 * x + 3 + (Math.random() - 0.5)); // шум ~ ±0.5 const table = newTable({ X, Y }); const model = new LinearRegression(table, 'Y', ['X']).calculate(); // ожидаем приблизительные коэффициенты almostEqual(model.coefficients[0], 3, 1); // intercept almostEqual(model.coefficients[1], 5, 0.1); // slope assert.ok(model.r2 > 0.99); }); it('Produces near-zero R² when no linear relationship', () => { const table = newTable({ X: [1, 2, 3, 4, 5], Y: [5, 3, 6, 2, 4] // случайный шум }); const model = new LinearRegression(table, 'Y', ['X']).calculate(); assert.ok(model.r2 < 0.1); }); });