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
126 lines (100 loc) • 5.27 kB
JavaScript
import { describe, it } from 'node:test';
import assert from 'node:assert';
import Regression from '../../../lib/analyze/regression/index.js';
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 = { X: [1, 2, 3, 4, 5], Y: [2, 4, 6, 8, 10] };
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0];
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 = {
X: [1, 2, 3, 4, 5, 6],
M: [2, 4, 5, 7, 7, 9],
Y: [3, 5, 7, 9, 10, 13]
};
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).next(['M']).steps[1];
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('Regression with low R² (noisy data)', () => {
const table = { X: [1, 2, 3, 4, 5], Y: [1, 1.1, 1.2, 0.9, 1] };
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0];
assert.ok(model.r2 < 0.5, `Expected low R², got ${model.r2}`);
});
it('Returns correct result structure', () => {
const table = { X: [1, 2, 3], Y: [2, 4, 6] };
const result = new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0].result;
assert.ok(Array.isArray(result.Variable), 'Variable should be array');
assert.strictEqual(result.Variable.length, 2);
assert.strictEqual(result.Coefficient.length, 2);
assert.strictEqual(result.StdError.length, 2);
assert.strictEqual(result.pValue.length, 2);
});
it('predict() returns the same as yHat', () => {
const table = { X: [1, 2, 3, 4, 5], Y: [2, 4, 6, 8, 10] };
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0];
const predicted = model.predict(model.X);
assert.deepStrictEqual(predicted, model.yHat);
});
it('Throws on constant X (singular matrix)', () => {
const table = { X: [1, 1, 1, 1, 1], Y: [2, 3, 4, 5, 6] };
assert.throws(() => new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0], /singular|constant/i);
});
it('Detects impact of outliers on R²', () => {
const table = { X: [1, 2, 3, 4, 100], Y: [2, 4, 6, 8, 15] };
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0];
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 = { X: [1, 2, 3], Y: [2, 4, 6] };
assert.throws(() => new Regression(table, { yName: 'Y', xNames: ['Z'] }).steps[0], /undefined|not found/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));
const table = { X, Y };
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0];
almostEqual(model.coefficients[0], 3, 1);
almostEqual(model.coefficients[1], 5, 0.1);
assert.ok(model.r2 > 0.99);
});
it('Produces near-zero R² when no linear relationship', () => {
const table = { X: [1, 2, 3, 4, 5], Y: [5, 3, 6, 2, 4] };
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).steps[0];
assert.ok(model.r2 < 0.1);
});
it('Automatically includes moderator and interaction term', () => {
const table = { X: [1, 2, 3, 4, 5], Z: [0, 1, 0, 1, 0], Y: [2, 3, 6, 7, 10] };
const result = new Regression(table, { yName: 'Y', xNames: ['X'] }).next(['Z', 'X*Z']).steps[1].result;
assert.ok(result.Variable.includes('Z'));
assert.ok(result.Variable.includes('X*Z'));
});
it('Autodetect xNames if not provided', () => {
const table = { X1: [1, 2, 3, 4, 5], X2: [2, 1, 4, 3, 5], Y: [3, 4, 6, 7, 10] };
const model = new Regression(table, { yName: 'Y' }).steps[0];
assert.deepStrictEqual(model.xNames.sort(), ['X1', 'X2'].sort());
});
it('With Moderator Z (interaction X*Z)', () => {
const table = { X: [1, 2, 3, 4, 5], Z: [1, 2, 1, 2, 1], Y: [3, 6, 5, 10, 7] };
const model = new Regression(table, { yName: 'Y', xNames: ['X'] }).next(['Z', 'X*Z']).steps[1];
const interactionName = 'X*Z';
assert.ok(model.result.Variable.includes(interactionName), 'Interaction term missing');
});
});
describe('Linear Regression — edges', () => {
it('fits simple line y = 2x + 1', () => {
const x = [1, 2, 3, 4];
const y = [3, 5, 7, 9];
const m = new Regression({ x, y }, { yName: 'y', xNames: ['x'] });
const [b0, b1] = m.results[0].Coefficient;
assert.ok(Number.isFinite(b0) && Number.isFinite(b1));
assert.ok(Math.abs(b0 - 1) < 1e-9);
assert.ok(Math.abs(b1 - 2) < 1e-9);
});
});