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

1,210 lines (889 loc) 41.4 kB
# ALS Statistics **ALS Statistics** is a modular JS toolkit for statistical work. It’s designed to be: [![Goldens verified](https://img.shields.io/badge/Goldens-verified-brightgreen)](#goldens) ![Python parity](https://img.shields.io/badge/Python%20parity-NumPy%2FSciPy%20verified-brightgreen) ![Deterministic tests](https://img.shields.io/badge/tests-deterministic-blue) ![EPS tolerances](https://img.shields.io/badge/EPS-documented-informational) ![Module format](https://img.shields.io/badge/module-ESM-informational) [![npm downloads](https://img.shields.io/npm/dm/als-statistics.svg)](https://www.npmjs.com/package/als-statistics) [![Bundle size](https://img.shields.io/bundlephobia/minzip/als-statistics)](https://bundlephobia.com/package/als-statistics) - **Quality:** **Numerics verified:** this release matches Python (NumPy/SciPy) reference outputs across modules and passes the deterministic **Golden Test Suite** on Node.js 20.x, all within published EPS tolerances. Reproducible via `node goldens/test.js` and `npm test`. - **Easy to use like `Math`** for small one-liners; - **Composable** for multi-step analyses (filter → group → compare → summarize); - **Runtime-agnostic** — the same API in **Node.js** and in the **browser**; - **Data-model light** — works with plain arrays (`number[]`) and small helpers like `Column` and `Table`. - **Browser-ready.** No native dependencies; works in the browser (as ESM or via the included UMD bundle). Think of it as a “batteries-included” **stats toolbox** rather than a full data-frame ecosystem. If you know **SPSS**: ALS gives you many of the common *procedures* (correlations, t-tests, ANOVA, reliability, basic clustering, regression) with code-first ergonomics. If you know **NumPy/SciPy**: ALS focuses on *analytics primitives and convenience wrappers* (no heavy data containers, no plotting). ### Why the rewrite? The v1 architecture had grown too complex (intertwined modules, heavy abstractions), which made adding features and maintaining consistency difficult. v2 was rebuilt from scratch with a simpler core (plain arrays + lightweight `Column`/`Table`), clear module boundaries, and predictable numerics—so new analytical tools can be added quickly without increasing complexity. --- ## Key ideas - **Plain data in / plain results out.** Most functions take `{ [name]: number[] }` or `number[]` and return simple objects (e.g. `{ r, t, df, p }`). - **Two modes of use:** 1. **One-liners** via descriptive helpers (mean, stdDev, percentiles…). 2. **Structured analyzers** for correlations, mean comparisons, regressions, clustering, etc. - **Table utilities.** Sort, filter, split by group, compute derived columns, and feed the result to an analyzer. --- ## Installation ```bash npm i als-statistics ``` ## Usage in browser ```html <script type="module" src="/node_modules/als-statistics/lib/index.js"></script> or <script src="/node_modules/als-statistics/statistics.js"></script> or <script type="module"> import Statistics from '/node_modules/als-statistics/lib/index.js' </script> ``` ## NodeJS ```js import { Analyze, Stats, Table, Column } from 'als-statistics'; // or const { Analyze, Stats, Table, Column } = require('als-statistics/statistics.cjs') const { CDF, CompareMeans, Correlate, Clustering, Regression } = Analyze; const { constants, t, f, phi } = CDF; const { IndependentTTest, OneWayAnova, PairedTTest, OneSampleTTest } = CompareMeans; const { CronbachAlpha, Pearson, Spearman, Kendall } = Correlate; const { Dbscan, Hdbscan, computeDistances } = Clustering; const { LinearRegression, LogisticRegression } = Regression; // Descriptive stats (one-liners) const { sum, mean,median,mode, min, max, // central tendency variance, varianceSample, stdDev, stdDevSample, cv, range, iqr, mad, // dispersion & scale percentile, q1, q3, p10, p90, // position & percentiles zScore, zScores, zScoresSorted, outliersZScore, outliersIQR, // z-scores & outliers weightedMean, confidenceInterval, slope, regressionSlope, // misc spectralPowerDensityArray, spectralPowerDensityMetric, sorted, ma, sumOfSquares, flatness, skewness, kurtosis, // other statistics skewnessSample, kurtosisSample, geometricMean, harmonicMean, noiseStability, frequencies, relativeFrequencies, relativeDispersion, normalizedValues, xValues, recode, // recode values } = Stats; ``` > The package is modular — import only what you use. --- ## Quick starts ### 1) Use it like `Math` (one-liners) ```js import { Stats } from 'als-statistics'; const X = [10, 12, 13, 9, 14]; const mu = Stats.mean(X); const sd = Stats.stdDevSample(X); const p90 = Stats.p90(X); console.log({ mu, sd, p90 }); // → { mu: 11.6, sd: 1.923..., p90: 13.8 } ``` You can also access many metrics via `Column`: ```js import { Column } from 'als-statistics'; const col = new Column([10, 12, 13, 9, 14], 'Score'); const { mean, stdDev, median, frequencies, flatness } = col; ``` ### 2) Quick analysis: correlation in one line ```js import { Analyze } from 'als-statistics'; const data = { gender: [0, 1, 0, 1, 1, 0], // 0=female, 1=male score: [62, 75, 70, 81, 64, 78], }; const pearson = new Analyze.Correlate(data).pearson('gender', 'score'); const { r, t, df, p } = pearson; console.log({ r, t, df, p }); // r in [-1, 1], two-sided p-value in [0, 1] ``` ### 3) Compare means: Welch t-test (unequal variances) ```js import { Analyze } from 'als-statistics'; const data = { men: [62, 75, 70, 81, 64], women: [78, 73, 69, 71, 74, 77], }; const test = new Analyze.CompareMeans(data).independentWelch('men', 'women'); console.log({ t: test.t, df: test.df, p: test.p }); ``` ### 4) One-way ANOVA (classic & Welch) ```js import { Analyze } from 'als-statistics'; const { CompareMeans } = Analyze; const data = { A: [10, 11, 9, 10], B: [10, 30, -10, 50, -20], C: [12, 13, 12, 11, 14], }; const classic = new CompareMeans(data).anova(); // pooled (equal variances) const welch = new CompareMeans(data).anovaWelch(); // unequal variances console.log({ classic: { F: classic.F, df1: classic.dfBetween, df2: classic.dfWithin, p: classic.p }, welch: { F: welch.F, df1: welch.dfBetween, df2: welch.dfWithin, p: welch.p }, }); ``` ### 5) Table-first workflow (filter → split → analyze) ```js import { Table } from 'als-statistics'; const t = new Table( { gender: [0,1,0,1,1,0], age: [21,22,20,23,19,22], score: [62,75,70,81,64,78] }, { name: 'Survey' } ); // Keep adults 21+ t.filterRowsBy('age', a => a >= 21); // Compare score by gender with Welch // Option A: already split into columns: import { Analyze } from 'als-statistics'; const { CompareMeans } = Analyze; const cm = new CompareMeans({ men: [...], women: [...] }).independentWelch('men', 'women'); // Option B: split first, then pass to CompareMeans: const groups = t.splitBy('gender'); // returns { groupName: number[] } const test = new CompareMeans(groups).independentWelch('0', '1'); ``` --- # Data managing (Tables and Columns) This section explains how data flows through **Columns**, **Tables** and **Statistics**: validation rules, caching, safe updates, and the most common operations you’ll use before running analytics. --- ## Notes & pitfalls - **Always mutate via API.** Use `Column` mutators or the `values` setter; avoid direct array mutation to keep caches correct. - **Invalids.** `Column.invalid` stores indices of rejected values; descriptives and analyses ignore them. - **Mutability.** Most `Table` methods are in-place and return `this`. Prefer `clone()` when you need a safe branch. - **Alignment.** If you disable alignment and keep ragged columns, be mindful when exporting rows or running analyses that expect equal lengths. - **HTML output.** `htmlTable()` is for quick previews; for full reports, prefer exporting rows and rendering via your own templates. ## Column ### Quick API snapshot ```js import Statistics ,{ Table, Column } from 'als-statistics'; // Column // static Column.key(name, ...parts) // properties/getters col.name col.labels? // optional labels aligned with values col.invalid // indices of invalid inputs col.values // get/set (validated) col.n // length // cache/events col.$(key, compute) // memoize custom computations col.onchange(fn) // subscribe to structural changes // mutation helpers (invalidate caches automatically) col.addValue(value, index?) col.deleteValue(index) col.clone(name?) col.insertAt(index, ...items) col.setAt(index, item) col.removeAt(index, deleteCount=1) col.splice(start, deleteCount, ...items) col.push(...items) // descriptive on Column (same names as Stats one-liners) col.sum, col.mean, col.median, col.mode col.variance, col.varianceSample, col.stdDev, col.stdDevSample, col.cv, col.range, col.iqr, col.mad col.percentile(p), col.q1, col.q3, col.p10, col.p90 col.zScore(v), col.zScores(), col.zScoresSorted(), col.outliersZScore(z=3), col.outliersIQR() col.weightedMean(weights), col.confidenceInterval, col.slope, col.regressionSlope(customX) col.spectralPowerDensityArray, col.spectralPowerDensityMetric ``` --- ### How it works (principles) - **Validation-first.** Columns accept **only finite numbers**. Any non-finite input (`NaN`, `±Infinity`, non-number) is rejected or tracked via `col.invalid`, and excluded from descriptive metrics. - **Cached results.** Many results are cached (e.g., `col.mean`, `col.stdDev`). To keep caches correct, you must **not** mutate the underlying array directly. Instead, either: - assign a **new array** via the validated setter: `col.values = [...newNumbers]`, **or** - use the **provided mutators** (`setAt`, `splice`, `push`, …). These paths automatically **invalidate** caches and fire `onchange` events. - **Alignment in tables.** By default, a `Table` aligns columns to a common length (truncates to the **shortest** column). You can change this behavior with constructor options (e.g., `alignColumns: false`, `minK`) or call `t.alignColumns()` explicitly. - **In-place transforms.** Most `Table` methods mutate. Chain them freely, or use `clone()` to keep the original around. ### Creating and validating ```js import { Column } from 'als-statistics'; const scores = new Column([10, 12, 13, 9, 14], 'Score'); // set a new validated series (replaces data, clears caches) scores.values = [11, 11, 10, 12, 15]; // invalid values are tracked and excluded from stats scores.values = [11, 12, NaN, 10, 9, Infinity]; console.log(scores.invalid); // [2, 5] console.log(scores.mean); // mean over valid entries only ``` > Do **not** mutate `scores.values` in place (e.g., `scores.values[0] = 999`), as caches won’t know about it. Use `setAt(...)` instead. ### Safe mutations (cache-aware) ```js // append values scores.push(10, 11); // insert at position scores.insertAt(1, 99); // replace a single value scores.setAt(0, 12); // delete & splice scores.deleteValue(2); scores.splice(3, 1, 50, 51); ``` All of these **invalidate caches** and emit `onchange`: ```js scores.onchange((col, prev, meta) => { console.log('column changed:', meta.type) }); ``` ### Caching your own computations ```js // memoize expensive custom metric const kurt = scores.$('kurtosis', () => { // compute once, then served from cache until data changes return scores.kurtosis; // or any custom formula }); ``` ### Descriptives on Column Every descriptive method available in `Stats` exists on `Column` too and always respects validation/caching: ```js console.log({ mean: scores.mean, sd : scores.stdDevSample, q1 : scores.q1, p90 : scores.p90, outliersZ: scores.outliersZScore(3) }); ``` --- ## Table ### Quick API snapshot ```js import { Table } from 'als-statistics'; const t = new Table(data?, { name?, minK?, alignColumns? }) // properties/getters t.n // rows count t.k // columns count t.columns // map of Column t.colNames // string[] t.colValues // Record<string, number[]> t.json // plain object view // row/column transforms (in-place; use clone() to branch) t.addColumn(name, values, labels?) -> Column t.deleteColumn(name) -> this t.addRow(row, index?) -> this t.addRows(rows, index?) -> this t.deleteRow(index) -> this t.alignColumns() -> this // data shaping t.recode(colName, mapper, newColName?) -> void t.compute(fn, name) -> Column t.filterRows(indexes) -> this t.filterRowsBy(colName, predicate) -> this t.sortBy(colName, asc=true) -> this t.clone(name?, colFilter=[]) -> Table t.splitBy(colName, labels?) -> Statistics t.transpose(colNames=[]) -> Table t.where(rowPredicate) -> number[] t.rows(withKeys=true) -> object[] | any[][] t.htmlTable(colFilter=[], options?) -> string t.descriptive(...metricNames) -> Object{} // Descriptive statistics for all columns // analysis shortcuts t.correlate(...colFilter) -> Correlate t.compareMeans(...colFilter) -> CompareMeans t.dbscan(colFilter, options?) -> Dbscan t.hdbscan(colFilter, options?) -> Hdbscan t.regression(yName, xNames, type='linear'|'logistic') -> Regression t.linear(yName, xNames) t.logistic(yName, xNames) ``` > Tip: operations on `Table` are **mutable** by default (they change the same instance). Use `t.clone(...)` to branch a copy for “what-if” scenarios. --- ### Constructing and alignment ```js import { Table } from 'als-statistics'; const t = new Table( { gender: [0,1,0,1,1,0], age: [21,22,20,23,19], score: [62,75,70,81,64,78] }, { name: 'Survey', alignColumns: true, minK: 2 } ); // When alignColumns=true (default), columns are trimmed to the shortest length. // You can turn this off via { alignColumns: false } if you need ragged columns. console.log(t.n, t.k, t.colNames); // rows, columns, names // Access Column objects const scoreCol = t.columns['score']; console.log(scoreCol.mean); ``` ### Rows & columns (synchronization) ```js // add/delete columns t.addColumn('bmi', [22.1, 24.0, 23.7, 25.3, 21.8]); t.deleteColumn('age'); // add rows (object keys match column names) t.addRow({ gender: 0, score: 71, bmi: 23.1 }); t.addRows([ { gender: 1, score: 68, bmi: 24.2 }, { gender: 0, score: 77, bmi: 22.7 } ]); // delete rows t.deleteRow(0); // re-align explicitly if needed t.alignColumns(); ``` ### Data shaping ```js // recode values (e.g., 0/1 -> 'F'/'M'), optionally write to a new column t.recode('gender', g => (g === 0 ? 'F' : 'M'), 'genderLabel'); // compute a derived numeric column t.compute(row => row.score / (row.bmi ?? 1), 'scorePerBmi'); // filter & sort (in place) t.filterRowsBy('score', s => s >= 70); t.sortBy('score', /*asc=*/false); // pick rows by predicate (returns indices) const adultIdx = t.where(row => row.bmi >= 22 && row.bmi <= 25); // grab data in different shapes const rowsAsObjects = t.rows(true); const rowsAsArrays = t.rows(false); const html = t.htmlTable(['genderLabel','score','bmi']); ``` ### Split & analyze ```js // split one column into groups, then run an analysis const groups = t.splitBy('genderLabel'); // => { F: number[], M: number[] } import { Analyze } from 'als-statistics'; const test = new Analyze.CompareMeans(groups).independentWelch('F', 'M'); console.log({ t: test.t, df: test.df, p: test.p }); // or use shortcuts directly from Table const corr = t.correlate('score','bmi').pearson(); console.log({ r: corr.r, p: corr.p }); ``` ### Transpose and clone ```js // transpose a subset of columns (handy for certain distance/clustering operations) const t2 = t.transpose(['score','bmi']); // clone to branch a scenario without touching the original const tClone = t.clone('scenario: filtered', ['score','bmi']); ``` --- # Statistics (multi-table manager) `Statistics` is a lightweight coordinator for **multiple** `Table`s. It lets you: - register tables (`addTable`), - compute the union of available column names (`colNames`), - **combine the same columns from different tables** into a new `Table` (`columns(...)`), - remove tables (`deleteTable`), - and access the module namespace (static): `Statistics.Table`, `Statistics.Stats`, `Statistics.Analyze`, `Statistics.Column`. > It’s especially handy for **before/after** designs, or when you **split** one table by a factor and then want to analyze the resulting groups together. --- ## API ```ts new Statistics(name?: string) statistics.addTable(obj: Record<string, number[]>, options?: { name?: string, minK?: number, alignColumns?: boolean }): Table statistics.deleteTable(tableName: string): void // set of distinct column names across all registered tables statistics.colNames: string[] // Combine selected columns (from *every* table that has them) into a new Table. // Result columns are named `${tableName}_${colName}`. statistics.columns(name: string, ...colFilter: (string|RegExp)[]): Table // Static accessors (namespaces) Statistics.Table Statistics.Stats Statistics.Analyze Statistics.Column ``` ### Column selection (`colFilter`) `columns(name, ...colFilter)` uses the same filtering helper as `Table`: - pass exact names: `columns('X', 'score')` - pass regex: `columns('X', /^score|age$/)` - exclude by prefixing with `-`: `columns('X', 'score', '-score_z')` --- ## Examples ### 1) Before/After (paired) ```js import Statistics from 'als-statistics'; const { CompareMeans } = Statistics.Analyze; const S = new Statistics('A/B'); // register two tables with the same column name "score" S.addTable({ score: [62, 71, 69, 73, 75] }, { name: 'before' }); S.addTable({ score: [70, 76, 70, 78, 79] }, { name: 'after' }); // collect score columns from all tables into one Table const merged = S.columns('Scores', 'score'); // -> columns: before_score, after_score // run paired t-test using the Table shortcut const paired = merged.compareMeans('before_score', 'after_score').paired(); console.log({ t: paired.t, df: paired.df, p: paired.p }); ``` ### 2) Split → Combine → Independent Welch ```js import { Table } from 'als-statistics'; import Statistics from 'als-statistics'; const { CompareMeans } = Statistics.Analyze; const t = new Table( { group: [0,1,0,1,0,1], score: [62,75,70,81,64,78] }, { name: 'Survey' } ); // split by "group" → returns a Statistics instance with one table per group const S = t.splitBy('group', { 0: 'control', 1: 'treat' }); // bring the "score" columns from each split table into ONE Table const merged = S.columns('scored', 'score'); // control_score, treat_score const test = merged.compareMeans('control_score','treat_score').independentWelch(); console.log({ t: test.t, df: test.df, p: test.p }); ``` ### 3) Cross-table correlation ```js const merged = S.columns('ab', 'score'); // e.g., before_score, after_score const corr = merged.correlate('before_score','after_score').pearson(); console.log({ r: corr.r, p: corr.p }); ``` ## Scenarios ### A) Before/After (pre→post) in separate tables ```js import Statistics from 'als-statistics'; const S = new Statistics(); S.addTable('pre', preTable); S.addTable('post', postTable); // Merge the same column name from multiple tables const merged = S.columns('merged', 'score'); // pre_score, post_score const cm = merged.compareMeans('pre_score','post_score').paired(); console.log({ t: cm.t, df: cm.df, p: cm.p }); ``` ### B) Split → Combine workflow ```js const S = new Statistics(); S.addTable('raw', rawTable); // Split by factor into two new tables const { control, treat } = S.split('raw', by => by.group === 'A' ? 'control' : 'treat'); // Combine same-named columns for cross-table analysis const merged = S.columns('combined', 'score'); // control_score, treat_score const res = merged.compareMeans('control_score','treat_score').independentWelch(); ``` ## How‑to recipes - **Compute cross-table correlation** between `before_score` and `after_score` ```js const merged = S.columns('ab', 'score'); merged.correlate('before_score','after_score').pearson(); ``` - **Build a summary sheet** for multiple tables (mean, sd, n) ```js const names = S.colNames(); const rows = names.map(col => { const t = S.columns('tmp', col); const d = t.describe(`${col}_0`); // first return { col, mean: d.mean, sd: d.stdDevSample, n: d.n }; }); ``` > Live CodePen demos: _add your links here_. ## Practical patterns ### A. Pipeline “sort → split → test” ```js import { Table } from 'als-statistics'; import { Analyze } from 'als-statistics'; const { CompareMeans } = Analyze; // sort by score, keep top 100 rows, split by gender, compare means const t = new Table(data).sortBy('score', false); const top = t.clone('Top').filterRows([...Array(100).keys()]); // keep first 100 indices const groups = top.splitBy('gender'); // returns small structure per group const cm = new CompareMeans(groups); const res = cm.independentWelch('0','1'); console.log(res.p < 0.05 ? 'Significant' : 'NS'); ``` ### B. Correlations with filters ```js import { Table } from 'als-statistics'; const t = new Table(data); t.filterRowsBy('age', a => a >= 25 && a <= 40); const corr = t.correlate('height','weight').pearson(); console.log(corr.r, corr.p); ``` ### C. Quick reliability check ```js import { Analyze } from 'als-statistics'; const items = { Q1: [...], Q2: [...], Q3: [...], Q4: [...] }; const alpha = new Analyze.Correlate.CronbachAlpha(items); console.log(alpha.alpha, alpha.htmlTable); ``` ### D. Minimal regression report ```js import { Analyze } from 'als-statistics'; const reg = new Analyze.Regression(dataset, { yName: 'y', xNames: ['x1','x2'], type: 'linear' }); // step 1 reg.steps[0].calculate(); console.log(reg.steps[0].result); // table-like object for reporting ``` --- # Analyze · CDF Cumulative distribution functions used by other tests. ## Exports - `CDF.regularizedIncompleteBeta(x, a, b): number` – Regularized incomplete beta **Iₓ(a,b)**. Clamps to `[0,1]` when `x0` or `x1`. - `CDF.t(x, df): number` – CDF of the **Student t** distribution. `df` must be positive. - `CDF.f(x, df1, df2): number` – CDF of the **F** distribution. `df1, df2` must be positive. - `CDF.phi(x): number` – Standard normal CDF **Φ(x)**. Returns `0`/`1` for large negative/positive tails and supports `±Infinity`. # Analyze · Compare Means Entry-point wrapper `CompareMeans` for mean-comparison tests (t-tests, ANOVA). ## Class: `CompareMeans` **Constructor** ```js new CompareMeans(data: Record<string, number[]>) ``` - `data` – object mapping **group name → numeric array**. ### Methods - `paired(...colNames): PairedTTest` – paired t-test on two named columns; trims to equal length. - `independent(...colNames): IndependentTTest` – two-sample **Student** t-test (pooled variance). - `independentWelch(...colNames): IndependentTTest` – two-sample **Welch** t-test. - `anova(...colNames): OneWayAnova` – one‑way ANOVA (pooled/“classic”). - `anovaWelch(...colNames): OneWayAnova` – Welch’s one‑way ANOVA. - `oneSample(colName?, mu0=0): OneSampleTTest` – one-sample t‑test for a single column (defaults to the first key if `colName` omitted). All methods accept optional **column names**. If omitted, the test uses all keys from the constructor `data`. # One‑Way ANOVA Classic (pooled) and Welch’s ANOVA. ## Class: `OneWayAnova` (returned by `CompareMeans.anova` / `anovaWelch`) **Constructor** ```js new OneWayAnova(data: Record<string, number[]>, welch=false) ``` - `data` – `group → values`. - Set `welch=true` for **Welch ANOVA**. ### Public fields - `F: number` - `dfBetween: number` - `dfWithin: number` - `p: number` – right‑tail p‑value via F CDF. - `k: number` – number of groups. - `msw: number` – mean square within. # Independent Samples T‑Test Two-sample t‑test. Supports **Student (pooled)** and **Welch** variants. ## Class: `IndependentTTest` (returned by `CompareMeans.independent` / `independentWelch`) **Constructor** ```js new IndependentTTest({ g1: number[], g2: number[] }, welch=false) ``` - Set `welch=true` for Welch’s unequal-variance t‑test. ### Public fields - `t: number` – t statistic. - `df: number` – degrees of freedom (Welch uses Satterthwaite). - `p: number` – two‑sided p‑value (getter). - `F: number` – ANOVA-equivalent `` (getter). - `leveneF: number` – Levene’s F for equality of variances. - `leveneDf1: number`, `leveneDf2: number`, `leveneP: number` – Levene’s test details. - `k: number` – number of groups (always 2 here). # One-Sample T‑Test ## Class: `OneSampleTTest` (returned by `CompareMeans.oneSample`) **Constructor** ```js new OneSampleTTest({ X: number[] }, mu0=0) ``` - Requires **n ≥ 2**. ### Public fields - `t: number` – t statistic. - `df: number` – `n - 1`. - `p: number` – two‑sided p‑value. - `sd: number` – sample standard deviation. - `se: number` – standard error `sd / sqrt(n)`. - `mu0: number` – hypothesized mean. # Paired T‑Test Paired (dependent) samples t‑test. ## Class: `PairedTTest` (returned by `CompareMeans.paired`) **Constructor** ```js new PairedTTest({ A: number[], B: number[] }) ``` - Requires **at least two** arrays; internally trims to the **same length**. ### Public fields - `t: number` – t statistic. - `df: number` – degrees of freedom (`n - 1`). - `p: number` – two‑sided p‑value (Student t). - `meanDelta: number` – mean of pairwise differences. - `sdDelta: number` – sample SD of differences. - `n: number` – number of paired observations. - `diffs: number[]` – raw pairwise deltas (A[i] − B[i]). # Correlate — practical usage ## Two columns vs matrix ```js import { Analyze } from 'als-statistics'; const { Correlate } = Analyze; // 1) EXACTLY TWO columns → returns a single test instance const one = new Correlate({ X: [1,2,3], Y: [2,4,9] }).pearson('X', 'Y'); console.log(one.r, one.t, one.df, one.p); // 2) THREE OR MORE columns → returns a map of pairwise results const all = new Correlate({ A:[...], B:[...], C:[...] }).pearson(); console.log(Object.keys(all)); // ['A|B','A|C','B|C'] console.log(all['A|B'].r, all['A|B'].p); ``` ### Population vs sample (Pearson) - `pearson()` — uses **population** covariance in the r-formula. - `pearsonSample()` — uses **sample** covariance. - Both provide two-sided `p` via the t-distribution with `df = n - 2`. ```js const p1 = new Correlate(data).pearson(); // population r const p2 = new Correlate(data).pearsonSample(); // sample r ``` ### Spearman & Kendall (ties handled) ```js const s = new Correlate({ X:[...], Y:[...] }).spearman('X','Y'); const k = new Correlate({ X:[...], Y:[...] }).kendall('X','Y'); console.log(s.r, s.p, k.tau, k.p); ``` > Two-sided helpers: `.spearmanTwoSided()` и `.kendallTwoSided()`. ## Reliability — Cronbach’s alpha ```js // Option A: import the class directly import { CronbachAlpha } from 'als-statistics/analyze/correlate/cronbach-alpha.js'; // Option B: via the namespace import { Analyze } from 'als-statistics'; const { Correlate } = Analyze; // new Correlate.CronbachAlpha(table) // same class const items = { Q1:[...], Q2:[...], Q3:[...] }; const alpha = new CronbachAlpha(items); console.log(alpha.alpha); // overall alpha console.log(alpha.ifItemsDeleted); // { Q1: α_if_deleted, ... } console.log(alpha.htmlTable); // ready-to-embed HTML with a small table ``` > Notes: > - `Correlate` methods **auto-trim** vectors to the shortest length where needed (e.g., Spearman). > - Pairwise matrices return a plain object of test instances keyed as `'A|B'`. # Cronbach’s Alpha ## Class: `CronbachAlpha` **Constructor** ```js new CronbachAlpha(data: Record<string, number[]>) ``` - Requires **≥ 2** parallel scales/items of equal length. ### Public fields - `alpha: number` – reliability estimate. - `sumOfVariances: number` – sum of item variances (sample). - `sumColumnVariance: number` – variance of summed score across rows. - `bessel: number` – correction factor `k/(k-1)`. - `ifItemsDeleted: Record<string, number>` – getter recomputed lazily. - `htmlTable: string` – formatted summary table (getter). # Kendall Rank Correlation (τ) ## Class: `Kendall` **Constructor** ```js new Kendall({ X: number[], Y: number[] }, twoSided=true) ``` ### Public fields - `tau: number` - `z: number` – normal approximation for significance - `p: number` – p‑value (two‑sided by default) - `t: number` – alias of `z` (for consistency with other tests) - `df: number` – `Infinity` (normal approximation) # Pearson Correlation ## Class: `Pearson` **Constructor** ```js new Pearson({ X: number[], Y: number[] }, population=false) ``` - When `population=true`, covariance uses population denominator. ### Public fields - `covariance: number` - `df: number` – `n - 2` - `r: number` – correlation coefficient in `[-1, 1]` - `t: number` – test statistic - `p: number` – two‑sided p‑value # Spearman Rank Correlation ## Class: `Spearman` **Constructor** ```js new Spearman({ X: number[], Y: number[] }) ``` ### Public fields - `r: number` – Spearman’s rho - `t: number` – t‑approximation of significance - `p: number` – two‑sided p‑value - `n: number` – number of paired observations (shorter input is trimmed) # Analyze · Clustering Density-based clustering over **columns** using precomputed distances between series. ## Class: `Dbscan` **Constructor** ```js new Dbscan(data: Record<string, number[]>, options?: { eps?: number, minPts?: number, metric?: 'mad' }) ``` - `eps` (default `0.4`), `minPts` (default `3`), `metric` (default `'mad'`). ### Public fields - `metric: string` - `eps: number` · `minPts: number` - `labels: number[]` – `0` unvisited, `-1` noise, `1..` cluster id per column. - `clusters: Array<{ id:number, columns:string[] }>` – built by `buildClusters`. - `distances: number[][]` – symmetric distance matrix. - Core methods (invoked by constructor): `findNeighbors(i)`, `expandCluster(i, clusterId)`, `run()`. ## Class: `Hdbscan` **Constructor** ```js new Hdbscan(data: Record<string, number[]>, options?: { metric?: 'mad', minClusterSize?: number }) ``` - `minClusterSize` defaults to `2`. ### Public fields - `metric: string`, `minClusterSize: number` - `labels: number[]` – final labels per column. - `clusters: Array<{ id:number, columns:string[] }>` - `mreachDistances: number[][]` – mutual reachability distances. - `mst: Array<[i,j,weight]>` – minimum spanning tree. - `hierarchy: Array<{ clusterId, lambdaBirth, lambdaDeath, points, size, children }>` # Regression — practical usage The `Regression` wrapper builds a **sequence of models** (*steps*). Start with a baseline, then call `next([...])` to add more predictors. Interaction terms are supported via the **`'X*Z'`** notation. ```ts new Regression(data, { yName: string, xNames?: string[], type?: 'linear'|'logistic' }) reg.next(newPredictors: string[]): this reg.steps: Array<RegressionBase> // each step is a fitted model reg.results: Array<Record<string, any>> // array of .result from each step reg.htmlTables: string // combined HTML of all steps ``` ## A) Linear — baseline, then moderator (interaction) ```js import { Analyze } from 'als-statistics'; const { Regression } = Analyze; const data = { X:[1,2,3,4,5], Z:[0,1,0,1,0], Y:[2,3,6,7,10] }; // Step 0: Y ~ X const reg = new Regression(data, { yName:'Y', xNames:['X'], type:'linear' }); // Step 1: add moderator Z and interaction X*Z reg.next(['Z', 'X*Z']); const step0 = reg.steps[0].result; // { step, n, Variable[], Coefficient[], StdError[], pValue[] } const step1 = reg.steps[1].result; // includes the 'X*Z' row console.log(step1.Variable.includes('X*Z')); // true ``` ## B) “Mediator‐like” step (add M, compare steps) There’s **no built-in mediation test** (Sobel/bootstrapping). However, you can *model* a putative mediator by adding it as a predictor on the next step and comparing coefficients/R². ```js const data = { X:[1,2,3,4,5,6], M:[2,4,5,7,7,9], Y:[3,5,7,9,10,13] }; // Step 0: Y ~ X const reg = new Regression(data, { yName:'Y', xNames:['X'], type:'linear' }); // Step 1: Y ~ X + M reg.next(['M']); console.log(reg.steps[0].r2, reg.steps[1].r2); // change in R² console.log(reg.steps[1].result.Variable.includes('M')); // true ``` ## C) Logistic — classification with accuracy ```js const data = { X:[0,1,2,3,4], Y:[0,0,0,1,1] }; const logit = new Regression(data, { yName:'Y', xNames:['X'], type:'logistic' }); const s0 = logit.steps[0]; console.log(s0.result.Accuracy); // in [0,1] console.log(s0.predict(s0.X)); // -> [0/1,...] console.log(s0.predictProba(s0.X)); // -> probabilities in [0,1] ``` ### Notes & tips - If you omit `xNames`, the wrapper uses **all columns except `yName`** as predictors. - `next([...])` creates a **clone** of the previous step’s columns and (if a name contains `'*'`) generates the interaction term by multiplying the two source predictors element-wise. - Linear steps expose `StdError[]` and `pValue[]`. Logistic steps expose `Accuracy`. - The wrapper and cores are **deterministic** for the same inputs. # Linear Regression (Core) ## Class: `Regression.LinearRegression` **Constructor** ```js new Regression.LinearRegression(table: Record<string, number[]>, yName: string, xNames: string[], stepIndex: number) ``` ### Public fields / getters - `coefficients: number[]` – `[Intercept, β1, …]`. - `y: number[]`, `X: number[][]`, `yHat: number[]` - `residuals: number[]` - `r2: number` - `standardErrors: number[]` - `pValues: number[]` - `n: number`, `k: number` (obs & parameters) - `result: { step, n, Variable, Coefficient, StdError, pValue }` - `htmlTable: string` ### Methods - `calculate(): this` - `predict(X: number[][]): number[]` # Logistic Regression (Core) ## Class: `Regression.LogisticRegression` **Constructor** ```js new Regression.LogisticRegression(table: Record<string, number[]>, yName: string, xNames: string[], stepIndex: number, learningRate=0.01, epochs=1000) ``` ### Public fields / getters - `coefficients: number[]` – `[Intercept, β1, …]` - `y: number[]`, `X: number[][]`, `yHat: number[]` (predicted classes) - `accuracy: number` - `n: number`, `k: number` - `result: { step, n, Variable, Coefficient, Accuracy }` - `htmlTable: string` ### Methods - `calculate(): this` - `predictProba(X: number[][]): number[]` – probabilities via sigmoid. - `predict(X: number[][], threshold=0.5): number[]` – hard labels. # Analyze — overview & patterns This section ties together the shortcuts across `Table`, `Statistics`, and `Analyze`. ## From Table to analysis ```js import { Table } from 'als-statistics'; import { Analyze } from 'als-statistics'; const t = new Table(data, { name: 'Survey' }); // Correlation (single pair) const r1 = t.correlate('height','weight').pearson(); // Correlation matrix (3+ columns) const rAll = t.correlate('height','weight','age').pearson(); // Compare means (Welch, unequal variances) const w = t.compareMeans('groupA','groupB').independentWelch(); // One-way ANOVA (classic/Welch) const a1 = t.compareMeans('A','B','C').anova(); const aW = t.compareMeans('A','B','C').anovaWelch(); // Regression (linear/logistic) const lin = new Analyze.Regression(t.columns, { yName:'score', xNames:['age','hours'] }); const log = new Analyze.Regression(t.columns, { yName:'passed', xNames:['score'], type:'logistic' }); ``` ## Split → Combine (Statistics) → Analyze ```js // Split one table by a factor (returns Statistics with per-group tables) const S = t.splitBy('group', { 0:'ctrl', 1:'treat' }); // Combine the same column across all split tables into one Table const merged = S.columns('byGroup', 'score'); // -> ctrl_score, treat_score // Now analyze as usual const test = merged.compareMeans('ctrl_score','treat_score').independentWelch(); ``` > Keep **mutations** API-only (`addRow`, `setAt`, `splice`, `values=`). Avoid in-place array edits to preserve caches and consistent results. # Descriptive Statistics Static utility functions used across the library. These are also mixed into `Column` (arity‑1 functions as getters; others as methods). ## Selected functions - `sum(values)` · `mean(values)` · `median(values)` · `mode(values)` - `variance(values)` · `varianceSample(values)` · `stdDev(values)` · `stdDevSample(values)` · `cv(values)` - `min(values)` · `max(values)` · `range(values)` · `iqr(values)` · `mad(values)` - `zScore(values, v)` · `zScores(values)` · `zScoresSorted(values)` - `percentile(values, p)` · `q1(values)` · `q3(values)` · `p10(values)` · `p90(values)` - `weightedMean(values, weights)` - `confidenceInterval({ mean, stdDevSample, values })` - `outliersZScore(values, z=3)` · `outliersIQR(values)` - `slope({ values })` · `regressionSlope({ X, Y })` - `spectralPowerDensityArray(values)` · `spectralPowerDensityMetric(values)` Refer to JSDoc in code for exact parameter objects where applicable. # Utils General helper utilities. ## Functions - `htmlTable(rows, headers, options?) → string` – render a simple HTML table (escapes content; supports `firstColHeader`, `fixed` decimals, transposition). - `round(value, fixed=8) → number | string` – numeric rounding with fixed decimals. - `range(start=0, end, step=1) → number[]` – numeric range. - `filterKeys(keys: string[], filters: (string|number|RegExp)[]): string[]` – include names, regex filter, and `'-name'` exclusions. - `Counter` – simple name counter with `getName(name?)`. # EPS (Golden Test Tolerances) | Class | Key | Value | Notes | |----------------|------------|---------|----------------------------------------| | Descriptives | `stat` | 1e-6 | Means, medians, quantiles, variance | | Z-scores | `z` | 1e-9 | Summary mean/std of z | | Regression | `reg` | 1e-5 | Coefficients, metrics | | CDF | `cdf` | 1e-9 | CDF/PPF checks | | Correlations | `r` | 1e-7 | Pearson/Spearman/Kendall | | Degrees of Fr. | `df` | 1e-6 | Welch df (float) | | ANOVA F | `anovaF` | 1e-6 | | | Flatness | `flatness` | 1e-12 | GM/AM stability | | SPD Flatness | `spd` | 1e-12 | GM/AM on SPD | | p-values | `p` | 1e-6 | | Change these in `goldens/settings.js` if needed. # How‑to - **Split a table by predicate and compare groups (Welch):** ```js const { A, B } = Table.split(raw, r => r.group === 'A' ? 'A' : 'B'); const t = A.compareMeans('score','B.score').independentWelch(); ``` - **Detect z‑outliers and keep sorted indices:** ```js const { zScores, indexes } = Stats.zScoresSorted({ values }); const top3 = indexes.slice(-3); // largest |z| ``` - **Compute spectral flatness of a spectrum:** ```js const spd = Stats.spectralPowerDensityArray({ values: magnitudes }); const flat = Stats.spectralPowerDensityMetric({ spectralPowerDensityArray: spd, values: magnitudes }); ``` > Live CodePen demos: _add your links here_. ## Changelog [2.1.0] - 2025-09-04 > **Breaking change:** `als-statistics` v2 is a **ground-up rewrite** with **no backward compatibility** with v1.x. **If you rely on v1:** pin your dependency to the latest 1.x release. ```bash npm i als-statistics@^1 ``` ### Changed - **`Stats.harmonicMean(...)`** — inputs `0` are now clamped to ε (`1e-12`) before computation (aligns with Python goldens), preventing `NaN`/division-by-zero surprises. - **`Stats.zScores({ values }, sample = false)`** — added a second parameter: - `sample = false` (default): population std (ddof = 0) — backward-compatible. - `sample = true`: sample std (ddof = 1) — matches NumPy/SciPy z-scores and golden summaries. - **`Stats.flatness({ values })`** — now returns `0` when the arithmetic mean is `0` (previously `NaN`), making all-zero vectors well-defined. ### Fixed - **`Stats.mad(...)`** — corrected median absolute deviation for edge cases. ### Tests - Added golden cross-checks against Python (NumPy/SciPy) and HDBSCAN labels; all pass within documented EPS tolerances. > **Notes:** Default behavior remains the same for `zScores` (ddof=0) unless `sample=true` is provided. If your code relied on `NaN` from `flatness`/`harmonicMean` for zero/negative inputs, update downstream checks accordingly.