@elastic/statistics
Pure statistical functions for TypeScript and JavaScript.
Install
npm install @elastic/statistics
Usage
Paired t-test
Mirrors scipy.stats.ttest_rel.
When every difference is the same, the statistic is infinite (NaN if the difference is zero) and
the p-value is 1, where scipy returns 0 (NaN).
import { pairedT } from '@elastic/statistics';
const { statistic, pValue } = pairedT([0.9, 0.85, 0.7, 0.95, 0.8], [0.8, 0.75, 0.72, 0.88, 0.7]);
// statistic and pValue are null when fewer than two pairs are given
Wilcoxon signed-rank test
Mirrors scipy.stats.wilcoxon;
p-values match scipy to floating-point precision.
import { wilcoxonSignedRank } from '@elastic/statistics';
const target = [0.9, 0.85, 0.7, 0.95, 0.8, 0.6, 0.75];
const baseline = [0.8, 0.75, 0.72, 0.88, 0.7, 0.6, 0.7];
const { statistic, pValue, zStatistic } = wilcoxonSignedRank(target, baseline);
// { statistic: 1, pValue: 0.0625, zStatistic: null }
// Is target stochastically greater than baseline? Use the normal approximation with continuity correction.
wilcoxonSignedRank(target, baseline, {
alternative: 'greater',
method: 'asymptotic',
correction: true,
});
| Option | Values | Default | Notes |
|---|---|---|---|
alternative | 'two-sided', 'less', 'greater' | 'two-sided' | Hypothesis about the differences target - baseline. |
zeroMethod | 'wilcox', 'pratt', 'zsplit' | 'wilcox' | Discard zero differences, rank them but drop their ranks, or split their ranks between both sums. |
method | 'auto', 'exact', 'asymptotic' | 'auto' | 'auto' uses the exact distribution up to 50 pairs without ties or zeros, an exhaustive permutation test for up to 13 pairs otherwise, and the normal approximation above that. |
correction | boolean | false | Continuity correction for the normal approximation. |
statistic is the smaller of the positive and negative rank sums for 'two-sided', and the positive
rank sum otherwise. zStatistic is set whenever the normal approximation is used. All three are null
when the test cannot be computed (no pairs, NaN in a sample, or no variance left for the normal
approximation, e.g. every difference is zero); the function never throws for such inputs.
Differences are computed in floating point, so pairs that should tie can receive distinct ranks
because of roundoff (0.825 - 0.775 !== 0.5 - 0.45). Round scores first when exact ties matter.
Mann-Whitney U test
Non-parametric test for two independent samples. Mirrors
scipy.stats.mannwhitneyu;
p-values match scipy to floating-point precision.
import { mannWhitney } from '@elastic/statistics';
const target = [0.9, 0.85, 0.7, 0.95, 0.8];
const baseline = [0.8, 0.75, 0.72, 0.6];
const { statistic, pValue, zStatistic } = mannWhitney(target, baseline);
// { statistic: 16.5, pValue: 0.1399825237583975, zStatistic: 1.475856111043418 }
mannWhitney(target, baseline, { alternative: 'greater', method: 'asymptotic' });
// { statistic: 16.5, pValue: 0.06999126187919875, zStatistic: 1.475856111043418 }
| Option | Values | Default | Notes |
|---|---|---|---|
alternative | 'two-sided', 'less', 'greater' | 'two-sided' | Whether the distribution underlying target differs from, is stochastically less than, or greater than baseline's. |
method | 'auto', 'exact', 'asymptotic' | 'auto' | 'auto' uses the exact distribution when a sample has at most 8 values and there are no ties, the normal approximation otherwise. |
continuity | boolean | true | Continuity correction for the normal approximation. |
statistic is the U statistic of target; baseline's is target.length * baseline.length - statistic.
zStatistic is set whenever the normal approximation is used. All three are null when the test
cannot be computed (an empty sample, NaN in a sample, or every value tied under the normal approximation).
McNemar tests
Paired binary outcomes (true/false or 1/0) scored on the same examples. Only the discordant
pairs count: b (first sample succeeded, second failed) and c (the reverse). The input is either
the two paired arrays or a 2x2 table [[a, b], [c, d]] in the statsmodels layout (rows: first
sample success / failure, columns: second sample success / failure).
import {
mcnemar,
mcnemarExact,
mcnemarMidP,
mcnemarStratified,
pairedTable,
} from '@elastic/statistics';
const target = [1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1];
const baseline = [1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0];
mcnemar(target, baseline);
// { statistic: 0, pValue: 0.0625, method: 'mid-p' }
pairedTable(target, baseline);
// [[5, 4], [0, 3]]
mcnemarExact([
[5, 4],
[0, 3],
]);
// { statistic: 0, pValue: 0.125, method: 'exact' }
mcnemar(
[
[5, 4],
[0, 3],
],
{ method: 'asymptotic' },
);
// { statistic: 2.25, pValue: 0.13361440253771584, method: 'asymptotic' }
// Categorical labels: one-vs-rest McNemar per class, in order of first appearance.
mcnemarStratified(
['cat', 'dog', 'dog', 'bird', 'cat', 'cat'],
['cat', 'cat', 'dog', 'bird', 'dog', 'bird'],
);
// [
// { label: 'cat', statistic: 1, pValue: 0.625, method: 'mid-p' },
// { label: 'dog', statistic: 1, pValue: 1, method: 'mid-p' },
// { label: 'bird', statistic: 0, pValue: 0.5, method: 'mid-p' },
// ]
| Option | Values | Default | Notes |
|---|---|---|---|
method | 'mid-p', 'exact', 'asymptotic' | 'mid-p' | Mid-p variant of the exact conditional test (Fagerland, Lydersen & Laake, 2013), the exact conditional test, or the chi-squared approximation. |
correction | boolean | true | Continuity correction of 1 for the asymptotic statistic. |
'exact' and 'asymptotic' mirror
statsmodels.stats.contingency_tables.mcnemar.
statistic is min(b, c) for 'mid-p' and 'exact', and the chi-squared statistic for
'asymptotic'; method reports the variant used. With no discordant pairs every variant returns
statistic 0 and p-value 1 (statsmodels' asymptotic variant is undefined there). mcnemarExact and
mcnemarMidP force one variant. mcnemarStratified takes paired categorical labels and runs the
same test per class in a one-vs-rest setting; its p-values are not adjusted for multiple comparisons.
Fisher's exact test
Exact test of independence for a 2x2 table, given as rows (e.g. group) by columns (e.g. success /
failure). Mirrors
scipy.stats.fisher_exact,
including its two-sided p-value (the total probability of tables at most as likely as the observed one).
import { fisherExact } from '@elastic/statistics';
fisherExact([
[8, 2],
[1, 5],
]);
// { statistic: 20, pValue: 0.03496503496503496 }
fisherExact(
[
[8, 2],
[1, 5],
],
{ alternative: 'greater' },
);
// { statistic: 20, pValue: 0.024475524475524472 }
statistic is the sample odds ratio (a * d) / (b * c) of the table [[a, b], [c, d]]: Infinity
when b or c is zero, null when a row or column is empty (the p-value is then 1). The function
throws for tables that are not 2x2 or hold negative or non-integer counts.
Chi-squared test of independence
Pearson's chi-squared test for an R x C table given as rows. Mirrors
scipy.stats.chi2_contingency;
p-values match scipy to floating-point precision.
import { chiSquared } from '@elastic/statistics';
chiSquared([
[10, 10, 20],
[20, 20, 20],
]);
// { statistic: 2.7777777777777777, pValue: 0.2493522087772962, dof: 2,
// expected: [[12, 12, 16], [18, 18, 24]], minimumExpected: 12 }
// Yates' continuity correction is applied to 2x2 tables unless disabled.
chiSquared([
[12, 3],
[17, 16],
]).pValue; // 0.12063113097759684
chiSquared(
[
[12, 3],
[17, 16],
],
{ correction: false },
).pValue; // 0.061412253987091166
statistic and pValue are null when a row or column of expected counts is zero (scipy raises
in that case); dof, expected and minimumExpected are always reported. The approximation is
usually considered adequate when minimumExpected is at least 5; use fisherExact for 2x2 tables
below that. The function throws for empty or ragged tables and negative counts.
Shapiro-Wilk normality test
Tests whether a sample, typically the paired differences, is normally distributed. Mirrors
scipy.stats.shapiro.
import { shapiroWilk } from '@elastic/statistics';
shapiroWilk([0.12, -0.05, 0.08, 0.02, -0.11, 0.07, 0.01, 0.15, -0.03, 0.04]);
// { statistic: 0.9875422022224214, pValue: 0.992829252967651 }
shapiroWilk([0.01, 0.02, 0.02, 0.03, 0.05, 0.09, 0.2, 0.4, 0.9, 2.5]);
// { statistic: 0.6044891945571944, pValue: 0.00006255986205030777 }
statistic is W, in (0, 1]; values near 1 are consistent with normality. The test needs at least
3 values and, like scipy, gives an accurate W for any size but an approximate p-value above 5000
values. Both fields are null for fewer than three values, NaN in the sample, or a constant sample. The W statistic matches scipy to
floating-point precision; the p-value goes through log(1 - W) and matches to about 1e-10 relative
precision when W is very close to 1.
Scripts
npm ci
npm run build
npm test
npm run lint
Semver policy
This package follows semantic versioning:
- Patch — bug fixes that do not change the public API
- Minor — new functions or non-breaking additions
- Major — breaking changes to exported types or function signatures
Until 1.0.0, the 0.x series may introduce breaking changes in minor versions.
License
Elastic License 2.0. This may be revisited when the package is made public.