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
505 lines (407 loc) • 19.9 kB
JavaScript
import { describe, it, before, beforeEach } from 'node:test';
import assert from 'node:assert';
import Statistics from '../../lib/index.js';
const { Table, Column } = Statistics;
// import Table from '../../lib/table/index.js';
// import Column from '../../lib/column/index.js';
import { sortBy } from '../../lib/table/simple-table/sort-by.js';
describe('Table class', () => {
it('should create a table', () => {
const table = new Table();
assert.deepStrictEqual(table.columns, {});
assert.strictEqual(table.n, 0);
});
it('should add columns', () => {
const table = new Table();
const ratio = table.addColumn('Numbers', [10, 20, 30]);
assert(ratio instanceof Column);
});
it('should add empty columns', () => {
const keys = ['test', 'test1']
const table = new Table(keys);
assert.deepStrictEqual(Object.keys(table.columns), keys)
});
it('should add name', () => {
const table = new Table(undefined, { name: 'test' });
assert(table.name === 'test')
});
it('should compare two ratio columns', () => {
const table = new Table();
table.addColumn('A', [1, 2, 3, 4, 5]);
table.addColumn('B', [2, 3, 4, 5, 6]);
const comparison = table.correlate('A', 'B');
assert.strictEqual(comparison.pearson().covariance.toFixed(2), '2.00');
});
it('should compute a new column based on existing columns', () => {
const table = new Table();
table.addColumn('A', [1, 2, 3]);
table.addColumn('B', [4, 5, 6]);
const newColumn = table.compute(({ A, B }) => A + B, 'Sum');
assert.deepStrictEqual(newColumn.values, [5, 7, 9]);
assert(table.columns['Sum'] instanceof Column);
});
it('should transpose the table', () => {
const table = new Table();
table.addColumn('A', [1, 2, 3]);
table.addColumn('B', [4, 5, 6]);
const transposed = table.transpose();
assert.strictEqual(transposed.n, 2);
assert.strictEqual(Object.keys(transposed.columns).length, 3);
});
it('should calculate descriptive statistics', () => {
const table = new Table();
table.addColumn('Numbers', [10, 20, 30, 40, 50]);
const desc = table.descriptive('mean');
assert.deepStrictEqual(desc, { Numbers: { mean: 30 } });
});
it('should delete a column', () => {
const table = new Table();
table.addColumn('A', [1, 2, 3]);
table.addColumn('B', [4, 5, 6]);
table.deleteColumn('A');
assert.strictEqual(Object.keys(table.columns).length, 1);
assert.strictEqual(table.columns['A'], undefined);
assert.strictEqual(table.n, 3); // n не уменьшается, что может быть нежелательно
});
});
describe('Cloning with filter columns', () => {
let table
before(() => {
table = new Table();
table.addColumn("A", [1, 2, 3]);
table.addColumn("B", [4, 5, 6]);
table.addColumn("C", [7, 8, 9]);
})
it('should clone table with explicit include', () => {
const cloned = table.clone('newname', ["A", "C"]);
assert.deepStrictEqual(cloned.columns["A"].values, [1, 2, 3]);
assert.deepStrictEqual(cloned.columns["C"].values, [7, 8, 9]);
assert.strictEqual(cloned.columns["B"], undefined);
});
it('should clone table with exclude only', () => {
const cloned = table.clone('newname', ["-B"]);
assert.deepStrictEqual(cloned.columns["A"].values, [1, 2, 3]);
assert.deepStrictEqual(cloned.columns["C"].values, [7, 8, 9]);
assert.strictEqual(cloned.columns["B"], undefined);
});
it('should clone table with regex and exclude', () => {
table.addColumn("A1", [10, 11, 12]);
const cloned = table.clone('newname', [/^A/, "-A1"]);
assert.deepStrictEqual(cloned.columns["A"].values, [1, 2, 3]);
assert.strictEqual(cloned.columns["A1"], undefined);
assert.strictEqual(cloned.columns["B"], undefined);
});
it('should ignore exclude with include', () => {
const cloned = table.clone('newName', ["A", "-B"]);
assert.deepStrictEqual(cloned.columns["A"].values, [1, 2, 3]);
assert.strictEqual(cloned.columns["B"], undefined);
assert.strictEqual(cloned.columns["C"], undefined);
});
})
describe('Table class - New Features', () => {
it('should create table with initial data using constructor', () => {
const data = {
Numbers: [10, 20, 30],
};
const table = new Table(data);
assert.strictEqual(table.n, 3);
assert.strictEqual(table.k, 1);
assert.ok(table.columns['Numbers'] instanceof Column);
assert.deepStrictEqual(table.columns['Numbers'].values, [10, 20, 30]);
});
it('should add row at the end when index is null', () => {
const table = new Table();
table.addColumn('Numbers', [1, 2]);
const result = table.addRow({ Numbers: 3 });
// assert.strictEqual(result, true);
assert.strictEqual(table.n, 3);
assert.deepStrictEqual(table.columns['Numbers'].values, [1, 2, 3]);
});
it('should add row at specific index', () => {
const table = new Table();
table.addColumn('Numbers', [1, 3]);
const result = table.addRow({ Numbers: 2 }, 1);
// assert.strictEqual(result, true);
assert.strictEqual(table.n, 3);
assert.deepStrictEqual(table.columns['Numbers'].values, [1, 2, 3]);
});
});
describe('table recode method', () => {
let table
beforeEach(() => { table = new Table({ Numbers: [10, 20, 30, 40] }); })
it('recode to existing column', () => {
table.recode('Numbers', (v) => v / 10)
assert.deepStrictEqual(table.colNames, ['Numbers'])
assert.deepStrictEqual(table.columns.Numbers.values, [1, 2, 3, 4])
})
it('recode to new column', () => {
table.recode('Numbers', (v) => v / 10, 'Test')
assert.deepStrictEqual(Object.keys(table.columns), ['Numbers', 'Test'])
assert.deepStrictEqual(table.columns.Test.values, [1, 2, 3, 4])
assert.deepStrictEqual(table.columns.Numbers.values, [10, 20, 30, 40])
})
})
describe('split method', () => {
let table
beforeEach(() => {
table = new Table({
gender: [1, 1, 2, 2, 1, 2],
numbers: [10, 20, 30, 40, 50, 60],
});
})
it('Should return Statistics object with tables', () => {
const statistics = table.splitBy('gender')
const tables = Object.values(statistics.tables)
assert(tables.length === 2)
assert.deepStrictEqual(tables[0].columns.numbers.values, [10, 20, 50])
assert.deepStrictEqual(tables[1].columns.numbers.values, [30, 40, 60])
assert(tables[0].columns.gender.values.every(v => v === 1))
assert(tables[1].columns.gender.values.every(v => v === 2))
})
it('splitBy creates columns per category with filtered values and sets name to category col', () => {
const table = new Table({
gender: [1, 1, 2, 2, 1],
score: [10, 20, 30, 40, 50],
}, 'original');
const splited = table.splitBy('gender', { 1: 'male', 2: 'female' });
assert.deepStrictEqual(Object.keys(splited.tables), ['male', 'female'])
assert(Object.values(splited.tables).every(v => v instanceof Table))
// Значения отфильтрованы корректно (соответствуют позициям категории)
assert.deepStrictEqual(splited.tables.male.columns.score.values, [10, 20, 50]); // gender==1 → indices 0,1,4
assert.deepStrictEqual(splited.tables.female.columns.score.values, [30, 40]); // gender==2 → indices 2,3
});
})
describe('sortBy helper', () => {
it('sorts numerically ascending and reorders all columns consistently', () => {
const columns = {
key: { values: [3, 1, 2], type: Number },
other: { values: ['a', 'b', 'c'] },
num2: { values: [30, 10, 20], type: Number },
};
sortBy({ columns }, 'key', true);
// key по возрастанию: 1 (idx1), 2 (idx2), 3 (idx0)
assert.deepStrictEqual(columns.key.values, [1, 2, 3]);
// порядок переносится на все колонки
assert.deepStrictEqual(columns.other.values, ['b', 'c', 'a']);
assert.deepStrictEqual(columns.num2.values, [10, 20, 30]);
});
it('sorts numerically descending and reorders all columns consistently', () => {
const columns = {
key: { values: [3, 1, 2], type: Number },
other: { values: ['a', 'b', 'c'] },
};
sortBy({ columns }, 'key', false);
// key по убыванию: 3 (idx0), 2 (idx2), 1 (idx1)
assert.deepStrictEqual(columns.key.values, [3, 2, 1]);
assert.deepStrictEqual(columns.other.values, ['a', 'c', 'b']);
});
it('sorts strings ascending using localeCompare and reorders all columns', () => {
const columns = {
key: { values: [2, 1, 3] }, // type не Number → строковая сортировка
other: { values: [100, 200, 300], type: Number },
};
sortBy({ columns }, 'key', true);
// строковая сортировка: a (idx1), b (idx0), c (idx2)
assert.deepStrictEqual(columns.key.values, [1, 2, 3]);
assert.deepStrictEqual(columns.other.values, [200, 100, 300]);
});
});
describe('Filter', () => {
it('should filter indexes with where method', () => {
const table = new Table();
table.addColumn('Numbers', [10, 20, 30, 40]);
const result = table.where((row) => row.Numbers < 25);
assert.deepStrictEqual(result, [0, 1]);
});
it('should respect existing filters in where method', () => {
const table = new Table();
table.addColumn('Numbers', [10, 20, 30, 40]);
table.filterRows([1]); // Фильтруем строку с 20
const result = table.where((row) => row.Numbers > 15);
assert.deepStrictEqual(result, [1, 2]);
});
it('should clone table with filtered values', () => {
const table = new Table();
table.addColumn('A', [1, 2, 3, 4, 5]);
table.filterRows([1, 2, 3]);
const cloned = table.clone(true);
assert.deepStrictEqual(cloned.columns['A'].values, [1, 5]);
assert.strictEqual(cloned.n, 2);
});
it('should transpose with filtered rows', () => {
const table = new Table();
table.addColumn('A', [1, 2, 3]);
table.filterRows([1]);
const transposed = table.transpose();
assert.strictEqual(Object.keys(transposed.columns).length, 2); // 0 и 2
});
})
describe('Integration Tests', () => {
it('should preserve filtering when transposed', () => {
const table = new Table();
table.addColumn('A', [10, 20, 30, 40, 50]);
table.addColumn('B', [5, 15, 25, 35, 45]);
table.filterRows([1, 2, 3]); // Фильтруем индексы 1, 2, 3
const transposed = table.transpose();
assert.strictEqual(Object.keys(transposed.columns).length, 2); // Осталось 2 строки (0 и 4)
});
it('should compute new column correctly with filtering', () => {
const table = new Table();
table.addColumn('A', [1, 2, 3, 4, 5]);
table.addColumn('B', [10, 20, 30, 40, 50]);
table.filterRows([1, 2, 3]);
const sumCol = table.compute(({ A = 0, B = 0 }) => A + B, 'Sum');
assert.deepStrictEqual(sumCol.values, [11, 55]); // Остались индексы 0 и 4
});
it('should correctly compare two filtered columns', () => {
const table = new Table();
table.addColumn('A', [10, 20, 30, 40, 50]);
table.addColumn('B', [5, 15, 25, 35, 45]);
table.filterRows([2, 3, 4]);
const comparison = table.correlate('A', 'B');
assert.strictEqual(comparison.pearson().r.toFixed(2), '1.00'); // Линейная зависимость
});
it('should apply filtering across all metrics', () => {
const table = new Table();
table.addColumn('Numbers', [10, 20, 30, 40, 50]);
table.filterRows([0, 1, 2]); // Оставляем 40 и 50
assert.strictEqual(table.columns['Numbers'].mean, 45); // (40+50)/2
assert.strictEqual(table.columns['Numbers'].n, 2);
});
it('should clone table with filtered values', () => {
const table = new Table();
table.addColumn('A', [10, 20, 30, 40, 50]);
table.filterRows([1, 2, 3]);
const cloned = table.clone(true);
assert.strictEqual(cloned.columns['A'].n, 2); // Остались 10 и 50
assert.deepStrictEqual(cloned.columns['A'].values, [10, 50]);
});
it('should apply descriptive metrics correctly after filtering', () => {
const table = new Table();
table.addColumn('Numbers', [10, 20, 30, 40, 50]);
table.filterRows([2, 3, 4]);
const { Numbers: { mean } } = table.descriptive('mean');
assert.strictEqual(mean, 15); // Среднее из [10, 20]
});
it('should correctly perform two-sample t-test after filtering', () => {
const table = new Table();
table.addColumn('Group1', [10, 20, 30, 40, 50]);
table.addColumn('Group2', [12, 22, 32, 42, 52]);
table.filterRows([0, 1, 2]); // Оставляем последние два
const comparison = table.compareMeans('Group1', 'Group2');
const { t } = comparison.independent();
assert.ok(t > -1 && t < 1); // Небольшое отличие
});
});
describe('Skiped', () => {
it('should return false for invalid row data', () => {
const table = new Table({ Numbers: [1, 2] });
assert.throws(() => table.addRow({ Numbers: '3' }))
// Отсутствует ключ
assert.throws(() => table.addRow({ numbers: 3 }))
// Лишний ключ
assert.doesNotThrow(() => table.addRow({ Numbers: 3, Extra: 1 }))
});
it('htmlTable renders a table with header "<name> data", thead headers, and only <td> in tbody', () => {
const table = new Table({
A: [1, 2],
B: [10, 20],
C: [100, 200],
}, { name: 'Test' });
// отрендерим только A и B
const html = table.htmlTable(['A', 'B']);
assert.ok(typeof html === 'string');
// Заголовок включает "<name> data"
assert.ok(html.includes('<h3>Test data</h3>'));
// thead содержит заголовки колонок
assert.ok(html.includes('<thead>'));
// Порядок и формат заголовков (<th>A</th><th>B</th>)
assert.ok(html.includes('<th>A</th>'));
assert.ok(html.includes('<th>B</th>'));
// Тело таблицы есть
assert.ok(html.includes('<tbody>'));
// Проверяем, что в tbody нет <th> (firstColHeader=false)
const tbody = html.slice(html.indexOf('<tbody>'), html.indexOf('</tbody>') + 9);
assert.ok(!/<th>/.test(tbody), 'tbody should not contain <th> cells');
// Присутствуют значения ячеек (как <td>…</td>)
assert.ok(tbody.includes('<td>1</td>'));
assert.ok(tbody.includes('<td>10</td>'));
assert.ok(tbody.includes('<td>2</td>'));
assert.ok(tbody.includes('<td>20</td>'));
});
it('should correctly handle different column lengths and cut it', () => {
const table = new Table();
table.addColumn('Short', [10, 20, 30]);
table.addColumn('Long', [5, 15, 25, 35, 45]);
assert.strictEqual(table.n, 3); // Максимальная длина
table.filterRows([0, 1, 2]);
assert.strictEqual(table.columns['Short'].n, 0); // Остался только индекс 0
assert.strictEqual(table.columns['Long'].n, 0); // Остались индексы 0 и 4
});
it('Column.prepare: удаление невалидных значений синхронно по строкам', () => {
const table = new Table({
"A": [1, 2, NaN, 4, Infinity, 6],
"B": [10, 20, 30, 40, 50, 60]
})
assert.deepEqual(table.columns.A.values, [1, 2, 4, 6]);
assert.deepEqual(table.columns.B.values, [10, 20, 40, 60]);
});
it('json returns plain object with raw column arrays', () => {
const data = {
A: [1, 2, 3],
};
const table = new Table(data, 'T');
const json = table.json;
// Точное соответствие исходным массивам
assert.deepStrictEqual(json, data);
// Убедимся, что это новый объект, а не ссылка на table.columns
assert.ok(json !== table.columns);
});
it('intended behavior (documentary check): filtering same rows via manual indexes still works', () => {
// Этот тест не вызывает filterRowsBy, а показывает ожидаемый результат фильтрации
// через прямой filterRows — чтобы было ясно, как должно работать после фикса.
const table = new Table({
Numbers: [10, 20, 30, 40],
});
// Оставляем только значения >= 20 → удаляем индексы, где v < 20 (то есть 0)
const toRemove = table.columns.Numbers.values
.map((v, i) => (v >= 20 ? null : i))
.filter(i => i !== null);
table.filterRows(toRemove);
assert.deepStrictEqual(table.columns.Numbers.values, [20, 30, 40]);
});
it('Table.filterRowsBy: выбирает индексы по предикату и фильтрует синхронно', () => {
const t = new Table({ A: [1, 2, 3, 4], B: [10, 20, 30, 40] });
// уберём чётные A (2 и 4) → остаются индексы 0 и 2
const t2 = t.filterRowsBy('A', v => v % 2 === 1);
assert.deepEqual(t2.columns.A.values, [1, 3]);
assert.deepEqual(t2.columns.B.values, [10, 30]);
});
it('TableData.deleteRow: ошибка на несуществующем индексе и обычное удаление', () => {
const t = new Table({ A: [1, 2, 3], B: [10, 20, 30] });
assert.throws(() => t.deleteRow(5), /index not exists/i);
const t2 = new Table({ A: [1, 2, 3], B: [10, 20, 30] });
const before = t2.n;
t2.deleteRow(1); // удаляем вторую строку
assert.equal(t2.n, before - 1);
assert.deepEqual(t2.columns.A.values, [1, 3]);
assert.deepEqual(t2.columns.B.values, [10, 30]);
});
})
describe('Table addRows/addRow/filterRows', () => {
it('addRows вставляет пачку строк в середину и выравнивает данные', () => {
const t = new Table({ A: [1,2,5], B: [10,20,50] }, { name: 'T' });
// Вставим две строки на позицию 2 (перед "5,50"):
t.addRows([[3,30],[4,40]], 2);
assert.equal(t.n, 5);
assert.deepEqual(t.columns.A.values, [1,2,3,4,5]);
assert.deepEqual(t.columns.B.values, [10,20,30,40,50]);
});
it('filterRowsBy сохраняет строки, прошедшие предикат', () => {
const t = new Table({ A: [1,2,3,4], B: [10,20,30,40] });
t.filterRowsBy('A', v => v % 2 === 0); // оставим только чётные A
assert.equal(t.n, 2);
assert.deepEqual(t.columns.A.values, [2,4]);
assert.deepEqual(t.columns.B.values, [20,40]);
});
});