UNPKG

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