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