@numero-uno-jpn/rstats 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/README.ja.md +36 -20
- package/README.md +34 -18
- package/THIRD_PARTY_NOTICES.md +25 -5
- package/dist/chunks/chunk-2JCIB5H2.js +2 -0
- package/dist/chunks/chunk-7JVSDGQQ.js +2 -0
- package/dist/chunks/chunk-7NK4SA6B.js +2 -0
- package/dist/chunks/chunk-AJYQNFXK.js +2 -0
- package/dist/chunks/chunk-DRNMIEYH.js +2 -0
- package/dist/chunks/chunk-NAYDS7CL.js +2 -0
- package/dist/chunks/chunk-NOCEAM44.js +2 -0
- package/dist/chunks/chunk-QNAIK4PY.js +2 -0
- package/dist/chunks/chunk-TPC6MTYC.js +2 -0
- package/dist/chunks/chunk-UGUQYWRQ.js +2 -0
- package/dist/chunks/chunk-XM2TVD63.js +2 -0
- package/dist/chunks/chunk-Y4IZV7XK.js +2 -0
- package/dist/chunks/chunk-ZWJ42QN6.js +2 -0
- package/dist/descriptive/basic.d.ts +73 -0
- package/dist/descriptive/basic.js +256 -0
- package/dist/descriptive/cov.d.ts +39 -0
- package/dist/descriptive/cov.js +105 -0
- package/dist/descriptive/hist.d.ts +64 -0
- package/dist/descriptive/hist.js +304 -0
- package/dist/descriptive/moments.d.ts +63 -0
- package/dist/descriptive/moments.js +158 -0
- package/dist/descriptive/quantile.d.ts +50 -0
- package/dist/descriptive/quantile.js +131 -0
- package/dist/descriptive/rank.d.ts +23 -0
- package/dist/descriptive/rank.js +72 -0
- package/dist/descriptive/table.d.ts +54 -0
- package/dist/descriptive/table.js +160 -0
- package/dist/descriptive.js +2 -0
- package/dist/distributions/arcsine.d.ts +24 -0
- package/dist/distributions/arcsine.js +73 -0
- package/dist/distributions/invgamma.d.ts +29 -0
- package/dist/distributions/invgamma.js +85 -0
- package/dist/distributions/kumaraswamy.d.ts +17 -0
- package/dist/distributions/kumaraswamy.js +96 -0
- package/dist/distributions/laplace.d.ts +23 -0
- package/dist/distributions/laplace.js +69 -0
- package/dist/distributions/multinomial.d.ts +24 -0
- package/dist/distributions/multinomial.js +70 -0
- package/dist/distributions/nbeta.js +28 -10
- package/dist/distributions/nchisq.js +20 -3
- package/dist/distributions/pareto.d.ts +23 -0
- package/dist/distributions/pareto.js +76 -0
- package/dist/distributions/summary.d.ts +25 -0
- package/dist/distributions/summary.js +289 -0
- package/dist/distributions/triangular.d.ts +25 -0
- package/dist/distributions/triangular.js +87 -0
- package/dist/distributions.js +2 -0
- package/dist/entries/descriptive.d.ts +7 -0
- package/dist/entries/descriptive.js +8 -0
- package/dist/entries/distributions.d.ts +41 -0
- package/dist/entries/distributions.js +41 -0
- package/dist/entries/linalg.d.ts +11 -0
- package/dist/entries/linalg.js +11 -0
- package/dist/entries/numerics.d.ts +8 -0
- package/dist/entries/numerics.js +6 -0
- package/dist/entries/special.d.ts +9 -0
- package/dist/entries/special.js +9 -0
- package/dist/entries/tests.d.ts +21 -0
- package/dist/entries/tests.js +20 -0
- package/dist/htest/ansariTest.d.ts +13 -0
- package/dist/htest/ansariTest.js +80 -0
- package/dist/htest/aov.d.ts +85 -0
- package/dist/htest/aov.js +178 -0
- package/dist/htest/binomTest.d.ts +36 -0
- package/dist/htest/binomTest.js +121 -0
- package/dist/htest/cor.d.ts +14 -0
- package/dist/htest/cor.js +4 -3
- package/dist/htest/core/ansari.d.ts +18 -0
- package/dist/htest/core/ansari.js +121 -0
- package/dist/htest/core/kolmogorov.d.ts +26 -0
- package/dist/htest/core/kolmogorov.js +221 -0
- package/dist/htest/core/mantelhaen.d.ts +26 -0
- package/dist/htest/core/mantelhaen.js +145 -0
- package/dist/htest/core/shapiro.d.ts +24 -0
- package/dist/htest/core/shapiro.js +142 -0
- package/dist/htest/core/smirnov.d.ts +20 -0
- package/dist/htest/core/smirnov.js +75 -0
- package/dist/htest/fisherTest.js +134 -120
- package/dist/htest/groups.d.ts +20 -0
- package/dist/htest/groups.js +62 -0
- package/dist/htest/ksTest.d.ts +31 -0
- package/dist/htest/ksTest.js +106 -0
- package/dist/htest/mantelhaenTest.d.ts +26 -0
- package/dist/htest/mantelhaenTest.js +196 -0
- package/dist/htest/mcnemarTest.d.ts +26 -0
- package/dist/htest/mcnemarTest.js +99 -0
- package/dist/htest/onewayTest.d.ts +26 -0
- package/dist/htest/onewayTest.js +97 -0
- package/dist/htest/pairwise.d.ts +64 -0
- package/dist/htest/pairwise.js +152 -0
- package/dist/htest/poissonTest.d.ts +35 -0
- package/dist/htest/poissonTest.js +170 -0
- package/dist/htest/propTest.d.ts +37 -0
- package/dist/htest/propTest.js +164 -0
- package/dist/htest/rankTests.d.ts +52 -0
- package/dist/htest/rankTests.js +234 -0
- package/dist/htest/types.d.ts +2 -0
- package/dist/htest/types.js +6 -0
- package/dist/htest/varTest.d.ts +48 -0
- package/dist/htest/varTest.js +118 -0
- package/dist/htest/wilcoxTest.js +136 -174
- package/dist/index.d.ts +6 -50
- package/dist/index.js +8 -50
- package/dist/integrate/quadpack.d.ts +29 -0
- package/dist/integrate/quadpack.js +525 -0
- package/dist/internal/seq.d.ts +5 -0
- package/dist/internal/seq.js +14 -0
- package/dist/internal/uniroot.js +10 -71
- package/dist/linalg/lu.d.ts +45 -0
- package/dist/linalg/lu.js +88 -0
- package/dist/linalg/ops.d.ts +90 -0
- package/dist/linalg/ops.js +320 -0
- package/dist/linalg.js +2 -0
- package/dist/models/lm.d.ts +98 -0
- package/dist/models/lm.js +187 -0
- package/dist/numerics/deriv.d.ts +37 -0
- package/dist/numerics/deriv.js +150 -0
- package/dist/numerics/integrate.d.ts +91 -0
- package/dist/numerics/integrate.js +264 -0
- package/dist/numerics/interp.d.ts +103 -0
- package/dist/numerics/interp.js +581 -0
- package/dist/numerics/ode.d.ts +23 -0
- package/dist/numerics/ode.js +59 -0
- package/dist/numerics.js +2 -0
- package/dist/optim/localmin.d.ts +16 -0
- package/dist/optim/localmin.js +97 -0
- package/dist/random/beta.js +1 -1
- package/dist/random/binomial.js +1 -1
- package/dist/random/continuous.js +1 -1
- package/dist/random/extra.d.ts +33 -0
- package/dist/random/extra.js +62 -0
- package/dist/random/gamma.js +1 -1
- package/dist/random/hyper.js +1 -1
- package/dist/random/mt.js +0 -1
- package/dist/random/poisson.js +0 -1
- package/dist/random/rank.js +1 -1
- package/dist/random/rng.js +1 -1
- package/dist/random/sample.js +1 -1
- package/dist/rstats.js +2 -2
- package/dist/special/besselIK.d.ts +38 -0
- package/dist/special/besselIK.js +315 -0
- package/dist/special/besselJY.d.ts +32 -0
- package/dist/special/besselJY.js +508 -0
- package/dist/special/besselSeries.d.ts +8 -0
- package/dist/special/besselSeries.js +24 -0
- package/dist/special/beta.js +22 -5
- package/dist/special/choose.d.ts +75 -0
- package/dist/special/choose.js +233 -0
- package/dist/special/erf.d.ts +28 -2
- package/dist/special/erf.js +37 -10
- package/dist/special/extra.d.ts +47 -0
- package/dist/special/extra.js +220 -0
- package/dist/special/igam.d.ts +4 -0
- package/dist/special/igam.js +57 -2
- package/dist/special/incbet.js +77 -22
- package/dist/special/lgammaDiff.d.ts +7 -0
- package/dist/special/lgammaDiff.js +16 -3
- package/dist/special/psigamma.d.ts +48 -0
- package/dist/special/psigamma.js +318 -0
- package/dist/special/rCompatible.d.ts +8 -0
- package/dist/special/rCompatible.js +1 -0
- package/dist/special.js +2 -0
- package/dist/tests.js +2 -0
- package/package.json +26 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Grouped data for the k-sample tests: either an array of samples (R's list form) or a sample with a parallel array
|
|
2
|
+
// of group labels (R's x, g form). Pairs with a missing value or label are dropped, and groups are kept in order
|
|
3
|
+
// of the sorted labels (as R orders factor levels).
|
|
4
|
+
/**
|
|
5
|
+
* Normalise the two input forms to groups with NaN removed, each with its label. In the x, g form a label whose
|
|
6
|
+
* values are all missing has no group (as R drops an unused factor level). In the list form a group left empty is
|
|
7
|
+
* dropped, or rejected with R's message when `emptyGroups` is `"error"` (kruskal.test, fligner.test).
|
|
8
|
+
*/
|
|
9
|
+
export function labelledGroups(x, g, emptyGroups = "drop") {
|
|
10
|
+
if (g === undefined) {
|
|
11
|
+
const list = x;
|
|
12
|
+
if (list.length > 0 && typeof list[0] === "number")
|
|
13
|
+
throw new TypeError("'g' is required when 'x' is a single sample");
|
|
14
|
+
const all = list.map((s) => Array.from(s).filter((v) => !Number.isNaN(v)));
|
|
15
|
+
if (emptyGroups === "error" && all.some((s) => s.length === 0))
|
|
16
|
+
throw new RangeError("all groups must contain data");
|
|
17
|
+
const labels = [];
|
|
18
|
+
const groups = [];
|
|
19
|
+
all.forEach((s, i) => {
|
|
20
|
+
if (s.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
labels.push(String(i + 1));
|
|
23
|
+
groups.push(s);
|
|
24
|
+
});
|
|
25
|
+
return { labels, groups };
|
|
26
|
+
}
|
|
27
|
+
const xs = Array.from(x);
|
|
28
|
+
if (xs.length !== g.length)
|
|
29
|
+
throw new RangeError("'x' and 'g' must have the same length");
|
|
30
|
+
const byLabel = new Map();
|
|
31
|
+
for (let i = 0; i < xs.length; i++) {
|
|
32
|
+
const v = xs[i];
|
|
33
|
+
const l = g[i];
|
|
34
|
+
if (Number.isNaN(v) || (typeof l === "number" && Number.isNaN(l)))
|
|
35
|
+
continue;
|
|
36
|
+
let arr = byLabel.get(l);
|
|
37
|
+
if (arr === undefined)
|
|
38
|
+
byLabel.set(l, (arr = []));
|
|
39
|
+
arr.push(v);
|
|
40
|
+
}
|
|
41
|
+
const labels = Array.from(byLabel.keys()).sort((a, b) => typeof a === "number" && typeof b === "number" ? a - b : String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0);
|
|
42
|
+
return { labels: labels.map(String), groups: labels.map((l) => byLabel.get(l)) };
|
|
43
|
+
}
|
|
44
|
+
/** Normalise the two input forms to an array of non-empty groups with NaN removed (see {@link labelledGroups}) */
|
|
45
|
+
export function toGroups(x, g, emptyGroups = "drop") {
|
|
46
|
+
return labelledGroups(x, g, emptyGroups).groups;
|
|
47
|
+
}
|
|
48
|
+
/** Median (the mean of the two middle values for an even count) */
|
|
49
|
+
export function median(x) {
|
|
50
|
+
const s = [...x].sort((a, b) => a - b);
|
|
51
|
+
const n = s.length;
|
|
52
|
+
return n % 2 === 1 ? s[(n - 1) / 2] : (s[n / 2 - 1] + s[n / 2]) / 2;
|
|
53
|
+
}
|
|
54
|
+
/** Rows of a blocks x treatments table with any NaN dropped (R's complete.cases) */
|
|
55
|
+
export function completeRows(y) {
|
|
56
|
+
if (y.length === 0)
|
|
57
|
+
throw new RangeError("'y' must have at least one row");
|
|
58
|
+
const k = y[0].length;
|
|
59
|
+
if (y.some((r) => r.length !== k))
|
|
60
|
+
throw new RangeError("all rows of 'y' must have the same length");
|
|
61
|
+
return y.filter((r) => r.every((v) => !Number.isNaN(v))).map((r) => Array.from(r));
|
|
62
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Alternative, type HTest, type Sample } from "./types.js";
|
|
2
|
+
/** Options for {@link ksTest}. */
|
|
3
|
+
export interface KsTestOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Alternative hypothesis (unambiguous abbreviations accepted). `"greater"`: the distribution function of `x` lies
|
|
6
|
+
* above that of `y` (statistic D^+); `"less"`: below (D^-). Default `"two.sided"`.
|
|
7
|
+
*/
|
|
8
|
+
alternative?: Alternative | "two" | "l" | "g";
|
|
9
|
+
/**
|
|
10
|
+
* Exact p-value. Default: exact for one sample of fewer than 100 values without ties, and for two samples with
|
|
11
|
+
* m n < 10000.
|
|
12
|
+
*/
|
|
13
|
+
exact?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Result of {@link ksTest}. As in R, `alternative` describes the hypothesis in words ("two-sided", "the CDF of x lies
|
|
17
|
+
* above the null hypothesis", ...), and `exact` tells whether the p-value is exact.
|
|
18
|
+
*/
|
|
19
|
+
export interface KsTestResult extends Omit<HTest, "alternative"> {
|
|
20
|
+
alternative: string;
|
|
21
|
+
exact: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Kolmogorov-Smirnov test. Equivalent to R's `ks.test()`, with a distribution function given as a function (for
|
|
25
|
+
* example `(q) => pnorm(q)`) or a second sample. `NaN` values are dropped.
|
|
26
|
+
*
|
|
27
|
+
* @param x Sample.
|
|
28
|
+
* @param y A continuous distribution function, or a second sample.
|
|
29
|
+
* @param options See {@link KsTestOptions}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ksTest(x: Sample, y: Sample | ((q: number) => number), options?: KsTestOptions): KsTestResult;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Kolmogorov-Smirnov tests (R's ks.test): one sample against a continuous distribution function, or two samples.
|
|
2
|
+
//
|
|
3
|
+
// Written for rStats.js (no ported code); the distributions are in src/htest/core (kolmogorov.ts, smirnov.ts).
|
|
4
|
+
// One sample: D+ = max(i/n - F(x_(i))), D- = max(F(x_(i)) - (i - 1)/n); exact p-values (by default for n < 100
|
|
5
|
+
// without ties) from Marsaglia, Tsang and Wang (two-sided) and Birnbaum and Tingey (one-sided), asymptotic ones from
|
|
6
|
+
// Kolmogorov's limit and exp(-2 n D^2). Two samples: the largest differences of the empirical distribution functions,
|
|
7
|
+
// taken only where the pooled sorted values change; exact p-values (by default for m n < 10000, with or without ties)
|
|
8
|
+
// by counting lattice paths, asymptotic ones as for one sample with n replaced by m n / (m + n).
|
|
9
|
+
import { pkolmogorovExact, pkolmogorovLimit, pkolmogorovOneSidedUpper } from "./core/kolmogorov.js";
|
|
10
|
+
import { psmirnovUpper } from "./core/smirnov.js";
|
|
11
|
+
import { matchAlternative } from "./types.js";
|
|
12
|
+
function describe(alternative, oneSample) {
|
|
13
|
+
if (alternative === "two.sided")
|
|
14
|
+
return "two-sided";
|
|
15
|
+
const where = alternative === "less" ? "below" : "above";
|
|
16
|
+
return `the CDF of x lies ${where} ${oneSample ? "the null hypothesis" : "that of y"}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Kolmogorov-Smirnov test. Equivalent to R's `ks.test()`, with a distribution function given as a function (for
|
|
20
|
+
* example `(q) => pnorm(q)`) or a second sample. `NaN` values are dropped.
|
|
21
|
+
*
|
|
22
|
+
* @param x Sample.
|
|
23
|
+
* @param y A continuous distribution function, or a second sample.
|
|
24
|
+
* @param options See {@link KsTestOptions}.
|
|
25
|
+
*/
|
|
26
|
+
export function ksTest(x, y, options = {}) {
|
|
27
|
+
const xs = Array.from(x)
|
|
28
|
+
.filter((v) => !Number.isNaN(v))
|
|
29
|
+
.sort((a, b) => a - b);
|
|
30
|
+
const n = xs.length;
|
|
31
|
+
if (n < 1)
|
|
32
|
+
throw new RangeError("not enough 'x' data");
|
|
33
|
+
const alternative = matchAlternative(options.alternative);
|
|
34
|
+
const statisticName = alternative === "two.sided" ? "D" : alternative === "greater" ? "D^+" : "D^-";
|
|
35
|
+
if (typeof y === "function") {
|
|
36
|
+
let dplus = -Infinity;
|
|
37
|
+
let dminus = -Infinity;
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const f = y(xs[i]);
|
|
40
|
+
dplus = Math.max(dplus, (i + 1) / n - f);
|
|
41
|
+
dminus = Math.max(dminus, f - i / n);
|
|
42
|
+
}
|
|
43
|
+
const statistic = alternative === "two.sided" ? Math.max(dplus, dminus) : alternative === "greater" ? dplus : dminus;
|
|
44
|
+
const ties = new Set(xs).size < n;
|
|
45
|
+
const exact = options.exact ?? (n < 100 && !ties);
|
|
46
|
+
let pValue;
|
|
47
|
+
if (alternative === "two.sided") {
|
|
48
|
+
pValue = exact ? 1 - pkolmogorovExact(n, statistic) : pkolmogorovLimit(Math.sqrt(n) * statistic, false);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
pValue = exact ? pkolmogorovOneSidedUpper(n, statistic) : Math.exp(-2 * n * statistic * statistic);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
method: `${exact ? "Exact" : "Asymptotic"} one-sample Kolmogorov-Smirnov test`,
|
|
55
|
+
alternative: describe(alternative, true),
|
|
56
|
+
exact,
|
|
57
|
+
statistic,
|
|
58
|
+
statisticName,
|
|
59
|
+
pValue: Math.min(1, Math.max(0, pValue)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const ys = Array.from(y).filter((v) => !Number.isNaN(v));
|
|
63
|
+
const m = ys.length;
|
|
64
|
+
if (m < 1)
|
|
65
|
+
throw new RangeError("not enough 'y' data");
|
|
66
|
+
// walk the pooled sorted values, recording F_x - F_y after each run of equal values
|
|
67
|
+
const pooled = xs.map((v) => [v, 0]).concat(ys.map((v) => [v, 1]));
|
|
68
|
+
pooled.sort((a, b) => a[0] - b[0]);
|
|
69
|
+
let fx = 0;
|
|
70
|
+
let fy = 0;
|
|
71
|
+
let dplus = 0;
|
|
72
|
+
let dminus = 0;
|
|
73
|
+
for (let i = 0; i < pooled.length; i++) {
|
|
74
|
+
if (pooled[i][1] === 0)
|
|
75
|
+
fx++;
|
|
76
|
+
else
|
|
77
|
+
fy++;
|
|
78
|
+
if (i + 1 < pooled.length && pooled[i + 1][0] === pooled[i][0])
|
|
79
|
+
continue;
|
|
80
|
+
const diff = fx / n - fy / m;
|
|
81
|
+
dplus = Math.max(dplus, diff);
|
|
82
|
+
dminus = Math.max(dminus, -diff);
|
|
83
|
+
}
|
|
84
|
+
const statistic = alternative === "two.sided" ? Math.max(dplus, dminus) : alternative === "greater" ? dplus : dminus;
|
|
85
|
+
const ties = new Set(pooled.map((p) => p[0])).size < pooled.length;
|
|
86
|
+
const exact = options.exact ?? n * m < 10000;
|
|
87
|
+
let pValue;
|
|
88
|
+
if (exact) {
|
|
89
|
+
pValue = psmirnovUpper(statistic, n, m, ties ? { alternative, z: pooled.map((p) => p[0]) } : { alternative });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const en = (n * m) / (n + m);
|
|
93
|
+
pValue =
|
|
94
|
+
alternative === "two.sided"
|
|
95
|
+
? pkolmogorovLimit(Math.sqrt(en) * statistic, false)
|
|
96
|
+
: Math.exp(-2 * en * statistic * statistic);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
method: `${exact ? "Exact" : "Asymptotic"} two-sample Kolmogorov-Smirnov test`,
|
|
100
|
+
alternative: describe(alternative, false),
|
|
101
|
+
exact,
|
|
102
|
+
statistic,
|
|
103
|
+
statisticName,
|
|
104
|
+
pValue: Math.min(1, Math.max(0, pValue)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Alternative, type HTest } from "./types.js";
|
|
2
|
+
/** Options for {@link mantelhaenTest}. */
|
|
3
|
+
export interface MantelhaenTestOptions {
|
|
4
|
+
/** Alternative hypothesis (2 x 2 x K only). Default `"two.sided"`. */
|
|
5
|
+
alternative?: Alternative | "two" | "l" | "g";
|
|
6
|
+
/** Continuity correction (2 x 2 x K, asymptotic test). Default `true`. */
|
|
7
|
+
correct?: boolean;
|
|
8
|
+
/** Exact conditional test (2 x 2 x K). Default `false`. */
|
|
9
|
+
exact?: boolean;
|
|
10
|
+
/** Confidence level of the interval for the common odds ratio. Default `0.95`. */
|
|
11
|
+
confLevel?: number;
|
|
12
|
+
}
|
|
13
|
+
/** Result of {@link mantelhaenTest}. 2 x 2 x K tables add the common odds ratio and its interval. */
|
|
14
|
+
export interface MantelhaenTestResult extends Omit<HTest, "alternative"> {
|
|
15
|
+
/** Present for 2 x 2 x K tables. */
|
|
16
|
+
alternative?: Alternative;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Cochran-Mantel-Haenszel chi-squared test of conditional independence in a set of contingency tables, one per
|
|
20
|
+
* stratum. Equivalent to R's `mantelhaen.test()` with a three-dimensional array.
|
|
21
|
+
*
|
|
22
|
+
* @param strata The K strata, each an I x J table of counts as an array of rows (all of the same shape; I, J >= 2,
|
|
23
|
+
* K >= 2 for the asymptotic test).
|
|
24
|
+
* @param options See {@link MantelhaenTestOptions}.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mantelhaenTest(strata: readonly (readonly (readonly number[])[])[], options?: MantelhaenTestOptions): MantelhaenTestResult;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Cochran-Mantel-Haenszel tests for stratified contingency tables (R's mantelhaen.test).
|
|
2
|
+
//
|
|
3
|
+
// Written for rStats.js from the published methods and R's behaviour:
|
|
4
|
+
// - 2 x 2 x K: the Mantel-Haenszel statistic (|sum a - sum E(a)| - c)^2 / sum Var(a) with E(a) = r1 c1 / t and
|
|
5
|
+
// Var(a) = r1 r2 c1 c2 / (t^2 (t - 1)) per stratum (Mantel and Haenszel, JNCI 22, 1959), continuity correction
|
|
6
|
+
// c = 1/2 unless |sum a - sum E(a)| < 1/2; the Mantel-Haenszel common odds ratio sum(a d / t) / sum(b c / t), with
|
|
7
|
+
// the interval from the variance of its log by Robins, Breslow and Greenland (Biometrics 42, 1986).
|
|
8
|
+
// - 2 x 2 x K exact (exact = TRUE): the conditional distribution of S = sum a given all margins, a convolution of
|
|
9
|
+
// noncentral hypergeometric distributions with the common odds ratio as noncentrality (src/htest/core/mantelhaen.ts;
|
|
10
|
+
// Birch, JRSS B 26, 1964). One-sided p-values are tail probabilities under psi = 1; the two-sided p-value adds the
|
|
11
|
+
// null probabilities no larger than the observed one (relative slack 1e-7), and the estimate and limits are the
|
|
12
|
+
// conditional MLE and the noncentralities at which a one-sided probability equals alpha.
|
|
13
|
+
// - I x J x K: the generalized statistic (n - m)' V^-1 (n - m) on the first (I - 1)(J - 1) cells summed over strata,
|
|
14
|
+
// with the hypergeometric covariance V = (t^2 (t - 1))^-1 (t diag(r) - r r') (x) (t diag(c) - c c') per stratum
|
|
15
|
+
// (Landis, Heyman and Koch, Int. Stat. Review 46, 1978); chi-squared with (I - 1)(J - 1) degrees of freedom.
|
|
16
|
+
import { pgamma1 } from "../distributions/gamma.js";
|
|
17
|
+
import { pnorm, qnorm } from "../distributions/normal.js";
|
|
18
|
+
import { solve } from "../linalg/solve.js";
|
|
19
|
+
import { ddAdd } from "../special/dd.js";
|
|
20
|
+
import { ConvolvedNoncentralHyper, conditionalMLE, lowerLimit, upperLimit } from "./core/mantelhaen.js";
|
|
21
|
+
import { checkConfLevelOpen, matchAlternative } from "./types.js";
|
|
22
|
+
/**
|
|
23
|
+
* Cochran-Mantel-Haenszel chi-squared test of conditional independence in a set of contingency tables, one per
|
|
24
|
+
* stratum. Equivalent to R's `mantelhaen.test()` with a three-dimensional array.
|
|
25
|
+
*
|
|
26
|
+
* @param strata The K strata, each an I x J table of counts as an array of rows (all of the same shape; I, J >= 2,
|
|
27
|
+
* K >= 2 for the asymptotic test).
|
|
28
|
+
* @param options See {@link MantelhaenTestOptions}.
|
|
29
|
+
*/
|
|
30
|
+
export function mantelhaenTest(strata, options = {}) {
|
|
31
|
+
const K = strata.length;
|
|
32
|
+
if (K < 1)
|
|
33
|
+
throw new RangeError("'x' must have at least one stratum");
|
|
34
|
+
const I = strata[0].length;
|
|
35
|
+
const J = strata[0][0]?.length ?? 0;
|
|
36
|
+
if (I < 2 || J < 2 || K < 2)
|
|
37
|
+
throw new RangeError("each dimension in table must be >= 2");
|
|
38
|
+
for (const t of strata) {
|
|
39
|
+
if (t.length !== I || t.some((r) => r.length !== J))
|
|
40
|
+
throw new RangeError("all strata must have the same dimensions");
|
|
41
|
+
if (t.some((r) => r.some((v) => !Number.isFinite(v) || v < 0)))
|
|
42
|
+
throw new RangeError("all entries of 'x' must be nonnegative and finite");
|
|
43
|
+
}
|
|
44
|
+
const totals = strata.map((t) => t.reduce((a, r) => a + r.reduce((b, v) => b + v, 0), 0));
|
|
45
|
+
if (totals.some((t) => t < 2))
|
|
46
|
+
throw new RangeError("sample size in each stratum must be > 1");
|
|
47
|
+
const alternative = matchAlternative(options.alternative);
|
|
48
|
+
const correct = options.correct ?? true;
|
|
49
|
+
const exact = options.exact ?? false;
|
|
50
|
+
const confLevel = options.confLevel ?? 0.95;
|
|
51
|
+
checkConfLevelOpen(confLevel);
|
|
52
|
+
if (I === 2 && J === 2) {
|
|
53
|
+
const tables = strata.map((t) => [[t[0][0], t[0][1]], [t[1][0], t[1][1]]]);
|
|
54
|
+
return exact ? exact2x2xK(tables, alternative, confLevel) : asymptotic2x2xK(tables, totals, alternative, correct, confLevel);
|
|
55
|
+
}
|
|
56
|
+
// as in R, exact applies to 2 x 2 x K tables only and is ignored otherwise
|
|
57
|
+
return general(strata, totals, I, J);
|
|
58
|
+
}
|
|
59
|
+
function asymptotic2x2xK(tables, totals, alternative, correct, confLevel) {
|
|
60
|
+
let sa = 0;
|
|
61
|
+
let se = 0;
|
|
62
|
+
let sv = 0;
|
|
63
|
+
let sR = 0;
|
|
64
|
+
let sS = 0;
|
|
65
|
+
let sPR = 0;
|
|
66
|
+
let sPSQR = 0;
|
|
67
|
+
let sQS = 0;
|
|
68
|
+
tables.forEach(([[a, b], [c, d]], k) => {
|
|
69
|
+
const t = totals[k];
|
|
70
|
+
const r1 = a + b;
|
|
71
|
+
const r2 = c + d;
|
|
72
|
+
const c1 = a + c;
|
|
73
|
+
const c2 = b + d;
|
|
74
|
+
sa += a;
|
|
75
|
+
se += (r1 * c1) / t;
|
|
76
|
+
sv += (r1 * r2 * c1 * c2) / (t * t * (t - 1));
|
|
77
|
+
const P = (a + d) / t;
|
|
78
|
+
const Q = (b + c) / t;
|
|
79
|
+
const R = (a * d) / t;
|
|
80
|
+
const S = (b * c) / t;
|
|
81
|
+
sR += R;
|
|
82
|
+
sS += S;
|
|
83
|
+
sPR += P * R;
|
|
84
|
+
sPSQR += P * S + Q * R;
|
|
85
|
+
sQS += Q * S;
|
|
86
|
+
});
|
|
87
|
+
const delta = sa - se;
|
|
88
|
+
const yates = correct && Math.abs(delta) >= 0.5 ? 0.5 : 0;
|
|
89
|
+
const statistic = (Math.abs(delta) - yates) ** 2 / sv;
|
|
90
|
+
const pValue = alternative === "two.sided"
|
|
91
|
+
? pgamma1(statistic, 0.5, 2, false, false)
|
|
92
|
+
: pnorm(Math.sign(delta) * Math.sqrt(statistic), { lowerTail: alternative === "less" });
|
|
93
|
+
const estimate = sR / sS;
|
|
94
|
+
const sd = Math.sqrt(sPR / (2 * sR * sR) + sPSQR / (2 * sR * sS) + sQS / (2 * sS * sS));
|
|
95
|
+
let confInt;
|
|
96
|
+
if (alternative === "less")
|
|
97
|
+
confInt = [0, estimate * Math.exp(qnorm(confLevel) * sd)];
|
|
98
|
+
else if (alternative === "greater")
|
|
99
|
+
confInt = [estimate * Math.exp(qnorm(confLevel, { lowerTail: false }) * sd), Infinity];
|
|
100
|
+
else {
|
|
101
|
+
const z = qnorm((1 + confLevel) / 2);
|
|
102
|
+
confInt = [estimate * Math.exp(-z * sd), estimate * Math.exp(z * sd)];
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
method: `Mantel-Haenszel chi-squared test ${yates !== 0 ? "with" : "without"} continuity correction`,
|
|
106
|
+
alternative,
|
|
107
|
+
statistic,
|
|
108
|
+
statisticName: "Mantel-Haenszel X-squared",
|
|
109
|
+
parameter: 1,
|
|
110
|
+
parameterName: "df",
|
|
111
|
+
pValue,
|
|
112
|
+
confInt,
|
|
113
|
+
confLevel,
|
|
114
|
+
estimate,
|
|
115
|
+
estimateNames: "common odds ratio",
|
|
116
|
+
nullValue: 1,
|
|
117
|
+
nullValueName: "common odds ratio",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Exact conditional test on 2 x 2 x K tables: S = sum of the (1, 1) cells given all margins */
|
|
121
|
+
function exact2x2xK(tables, alternative, confLevel) {
|
|
122
|
+
const h = new ConvolvedNoncentralHyper(tables);
|
|
123
|
+
const s = tables.reduce((acc, t) => acc + t[0][0], 0);
|
|
124
|
+
let pValue;
|
|
125
|
+
if (alternative === "two.sided") {
|
|
126
|
+
// add the null probabilities of the sums no more likely than the observed one (relative slack 1e-7)
|
|
127
|
+
const d0 = h.density(1);
|
|
128
|
+
const cut = d0[s - h.lo] * (1 + 1e-7);
|
|
129
|
+
let acc = [0, 0];
|
|
130
|
+
for (const v of d0)
|
|
131
|
+
if (v <= cut)
|
|
132
|
+
acc = ddAdd(acc, [v, 0]);
|
|
133
|
+
pValue = Math.min(1, acc[0]);
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
pValue = h.cdf(s, 1, alternative === "greater");
|
|
137
|
+
let confInt;
|
|
138
|
+
if (alternative === "less")
|
|
139
|
+
confInt = [0, upperLimit(h, s, 1 - confLevel)];
|
|
140
|
+
else if (alternative === "greater")
|
|
141
|
+
confInt = [lowerLimit(h, s, 1 - confLevel), Infinity];
|
|
142
|
+
else {
|
|
143
|
+
const alpha = (1 - confLevel) / 2;
|
|
144
|
+
confInt = [lowerLimit(h, s, alpha), upperLimit(h, s, alpha)];
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
method: "Exact conditional test of independence in 2 x 2 x k tables",
|
|
148
|
+
alternative,
|
|
149
|
+
statistic: s,
|
|
150
|
+
statisticName: "S",
|
|
151
|
+
pValue,
|
|
152
|
+
confInt,
|
|
153
|
+
confLevel,
|
|
154
|
+
estimate: conditionalMLE(h, s),
|
|
155
|
+
estimateNames: "common odds ratio",
|
|
156
|
+
nullValue: 1,
|
|
157
|
+
nullValueName: "common odds ratio",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function general(strata, totals, I, J) {
|
|
161
|
+
const p = (I - 1) * (J - 1);
|
|
162
|
+
const diff = new Array(p).fill(0);
|
|
163
|
+
const V = Array.from({ length: p }, () => new Array(p).fill(0));
|
|
164
|
+
strata.forEach((tab, k) => {
|
|
165
|
+
const t = totals[k];
|
|
166
|
+
const r = tab.map((row) => row.reduce((a, b) => a + b, 0));
|
|
167
|
+
const c = Array.from({ length: J }, (_, j) => tab.reduce((a, row) => a + row[j], 0));
|
|
168
|
+
const f = 1 / (t * t * (t - 1));
|
|
169
|
+
// cell (i, j) of the first I - 1 rows and J - 1 columns is element i + (I - 1) j (column-major, as in R)
|
|
170
|
+
for (let j = 0; j < J - 1; j++) {
|
|
171
|
+
for (let i = 0; i < I - 1; i++) {
|
|
172
|
+
const u = i + (I - 1) * j;
|
|
173
|
+
diff[u] += tab[i][j] - (r[i] * c[j]) / t;
|
|
174
|
+
for (let jj = 0; jj < J - 1; jj++) {
|
|
175
|
+
for (let ii = 0; ii < I - 1; ii++) {
|
|
176
|
+
const vr = (i === ii ? t * r[i] : 0) - r[i] * r[ii];
|
|
177
|
+
const vc = (j === jj ? t * c[j] : 0) - c[j] * c[jj];
|
|
178
|
+
V[u][ii + (I - 1) * jj] += f * vr * vc;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const sol = solve(V, diff);
|
|
185
|
+
let statistic = 0;
|
|
186
|
+
for (let u = 0; u < p; u++)
|
|
187
|
+
statistic += diff[u] * sol[u];
|
|
188
|
+
return {
|
|
189
|
+
method: "Cochran-Mantel-Haenszel test",
|
|
190
|
+
statistic,
|
|
191
|
+
statisticName: "Cochran-Mantel-Haenszel M^2",
|
|
192
|
+
parameter: p,
|
|
193
|
+
parameterName: "df",
|
|
194
|
+
pValue: pgamma1(statistic, p / 2, 2, false, false),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HTest } from "./types.js";
|
|
2
|
+
/** Options for {@link mcnemarTest}. */
|
|
3
|
+
export interface McnemarTestOptions {
|
|
4
|
+
/** Apply the continuity correction (2 x 2 tables only). Default `true`. */
|
|
5
|
+
correct?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Result of {@link mcnemarTest}. Has no `alternative`, as R's `mcnemar.test()` returns none. */
|
|
8
|
+
export interface McnemarTestResult extends Omit<HTest, "alternative"> {
|
|
9
|
+
/** Degrees of freedom (`parameterName` is `"df"`). */
|
|
10
|
+
parameter: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* McNemar's chi-squared test for symmetry of rows and columns in a square table of counts. Equivalent to R's
|
|
14
|
+
* `mcnemar.test()` with a matrix, or with two factors given as paired category codes.
|
|
15
|
+
*
|
|
16
|
+
* @param x Square table of counts as an array of rows, or the first of two paired samples of category codes.
|
|
17
|
+
* @param y Second paired sample of category codes, when `x` is a sample. May be omitted (and the options passed in
|
|
18
|
+
* its place) when `x` is a table.
|
|
19
|
+
* @param options See {@link McnemarTestOptions}.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* mcnemarTest([[794, 86], [150, 570]]).statistic; // 34.71186
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function mcnemarTest(x: readonly (readonly number[])[] | readonly (number | string)[], y?: readonly (number | string)[] | McnemarTestOptions, options?: McnemarTestOptions): McnemarTestResult;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// McNemar's test of symmetry for a square contingency table (R's mcnemar.test).
|
|
2
|
+
//
|
|
3
|
+
// Written for rStats.js from the textbook construction (McNemar, Psychometrika 12, 1947; no ported code):
|
|
4
|
+
// sum over i < j of (n_ij - n_ji)^2 / (n_ij + n_ji), chi-squared with r (r - 1) / 2 degrees of freedom. For a 2 x 2
|
|
5
|
+
// table the continuity correction replaces |n_12 - n_21| by |n_12 - n_21| - 1.
|
|
6
|
+
import { pgamma1 } from "../distributions/gamma.js";
|
|
7
|
+
/**
|
|
8
|
+
* McNemar's chi-squared test for symmetry of rows and columns in a square table of counts. Equivalent to R's
|
|
9
|
+
* `mcnemar.test()` with a matrix, or with two factors given as paired category codes.
|
|
10
|
+
*
|
|
11
|
+
* @param x Square table of counts as an array of rows, or the first of two paired samples of category codes.
|
|
12
|
+
* @param y Second paired sample of category codes, when `x` is a sample. May be omitted (and the options passed in
|
|
13
|
+
* its place) when `x` is a table.
|
|
14
|
+
* @param options See {@link McnemarTestOptions}.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* mcnemarTest([[794, 86], [150, 570]]).statistic; // 34.71186
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function mcnemarTest(x, y, options = {}) {
|
|
22
|
+
let table;
|
|
23
|
+
if (x.length > 0 && Array.isArray(x[0])) {
|
|
24
|
+
if (y !== undefined && !Array.isArray(y))
|
|
25
|
+
options = y;
|
|
26
|
+
table = x.map((row) => Array.from(row));
|
|
27
|
+
const r = table.length;
|
|
28
|
+
if (r < 2 || table.some((row) => row.length !== r))
|
|
29
|
+
throw new RangeError("'x' must be square with at least two rows and columns");
|
|
30
|
+
if (table.some((row) => row.some((v) => !Number.isFinite(v) || v < 0)))
|
|
31
|
+
throw new RangeError("all entries of 'x' must be nonnegative and finite");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!Array.isArray(y))
|
|
35
|
+
throw new TypeError("'y' is required when 'x' is not a table");
|
|
36
|
+
table = crossTable(x, y);
|
|
37
|
+
}
|
|
38
|
+
const correct = options.correct ?? true;
|
|
39
|
+
const r = table.length;
|
|
40
|
+
let statistic;
|
|
41
|
+
// as in R, a symmetric 2 x 2 table gets no correction (it would make the statistic positive)
|
|
42
|
+
const useCorrection = correct && r === 2 && table[0][1] !== table[1][0];
|
|
43
|
+
if (useCorrection) {
|
|
44
|
+
const b = table[0][1];
|
|
45
|
+
const c = table[1][0];
|
|
46
|
+
statistic = (Math.abs(b - c) - 1) ** 2 / (b + c);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
statistic = 0;
|
|
50
|
+
for (let i = 0; i < r; i++) {
|
|
51
|
+
for (let j = i + 1; j < r; j++) {
|
|
52
|
+
const a = table[i][j];
|
|
53
|
+
const b = table[j][i];
|
|
54
|
+
statistic += (a - b) ** 2 / (a + b);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const df = (r * (r - 1)) / 2;
|
|
59
|
+
return {
|
|
60
|
+
method: useCorrection ? "McNemar's Chi-squared test with continuity correction" : "McNemar's Chi-squared test",
|
|
61
|
+
statistic,
|
|
62
|
+
statisticName: "McNemar's chi-squared",
|
|
63
|
+
parameter: df,
|
|
64
|
+
parameterName: "df",
|
|
65
|
+
pValue: pgamma1(statistic, df / 2, 2, false, false),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cross-tabulation of two paired samples of category codes (pairs with a missing value dropped), as R tabulates two
|
|
70
|
+
* factors: the rows are the levels of x and the columns those of y, each sorted (numbers numerically, strings as R
|
|
71
|
+
* sorts factor levels in the C locale). As in R, both must have the same number of levels, at least 2; the levels
|
|
72
|
+
* are matched by position, not by value.
|
|
73
|
+
*/
|
|
74
|
+
function crossTable(x, y) {
|
|
75
|
+
if (x.length !== y.length)
|
|
76
|
+
throw new RangeError("'x' and 'y' must have the same length");
|
|
77
|
+
const keep = [];
|
|
78
|
+
for (let i = 0; i < x.length; i++) {
|
|
79
|
+
const a = x[i];
|
|
80
|
+
const b = y[i];
|
|
81
|
+
if (typeof a === "number" && Number.isNaN(a))
|
|
82
|
+
continue;
|
|
83
|
+
if (typeof b === "number" && Number.isNaN(b))
|
|
84
|
+
continue;
|
|
85
|
+
keep.push([a, b]);
|
|
86
|
+
}
|
|
87
|
+
const levelsOf = (v) => Array.from(new Set(v)).sort((a, b) => typeof a === "number" && typeof b === "number" ? a - b : String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0);
|
|
88
|
+
const lx = levelsOf(keep.map((p) => p[0]));
|
|
89
|
+
const ly = levelsOf(keep.map((p) => p[1]));
|
|
90
|
+
if (lx.length < 2 || lx.length !== ly.length) {
|
|
91
|
+
throw new RangeError("'x' and 'y' must have the same number of levels (minimum 2)");
|
|
92
|
+
}
|
|
93
|
+
const ix = new Map(lx.map((v, i) => [v, i]));
|
|
94
|
+
const iy = new Map(ly.map((v, i) => [v, i]));
|
|
95
|
+
const t = lx.map(() => new Array(lx.length).fill(0));
|
|
96
|
+
for (const [a, b] of keep)
|
|
97
|
+
t[ix.get(a)][iy.get(b)]++;
|
|
98
|
+
return t;
|
|
99
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type GroupLabel } from "./groups.js";
|
|
2
|
+
import type { HTest, Sample } from "./types.js";
|
|
3
|
+
/** Options for {@link onewayTest}. */
|
|
4
|
+
export interface OnewayTestOptions {
|
|
5
|
+
/** Assume equal variances (the classical F test). Default `false` (Welch's test). */
|
|
6
|
+
varEqual?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Result of {@link onewayTest}: an F statistic with two degrees of freedom. */
|
|
9
|
+
export interface OnewayTestResult extends Omit<HTest, "alternative" | "parameter" | "parameterName"> {
|
|
10
|
+
/** `[num df, denom df]`. */
|
|
11
|
+
parameter: [number, number];
|
|
12
|
+
parameterName: [string, string];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Test that several normal samples have equal means, without (default) or with the assumption of equal variances.
|
|
16
|
+
* Equivalent to R's `oneway.test()`.
|
|
17
|
+
*
|
|
18
|
+
* @param x An array of samples, or one sample with group labels `g`.
|
|
19
|
+
* @param g Group labels, parallel to `x` (or the options, when `x` is an array of samples).
|
|
20
|
+
*/
|
|
21
|
+
export declare function onewayTest(x: Sample | readonly Sample[], g?: readonly GroupLabel[] | OnewayTestOptions, options?: OnewayTestOptions): OnewayTestResult;
|
|
22
|
+
/**
|
|
23
|
+
* Shapiro-Wilk test of normality. Equivalent to R's `shapiro.test()`. `NaN` values are dropped; the sample size must
|
|
24
|
+
* be between 3 and 5000.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shapiroTest(x: Sample): Omit<HTest, "alternative">;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// One-way analysis of means (R's oneway.test) and the Shapiro-Wilk normality test (R's shapiro.test).
|
|
2
|
+
//
|
|
3
|
+
// Written for rStats.js from the published formulas (no ported code). oneway.test: with equal variances the classical
|
|
4
|
+
// F = (sum n_i (m_i - m)^2 / (k - 1)) / (sum (n_i - 1) s_i^2 / (N - k)); otherwise Welch's (Biometrika 38, 1951)
|
|
5
|
+
// weighted statistic with w_i = n_i / s_i^2, F = sum w_i (m_i - m_w)^2 / ((k - 1)(1 + 2 (k - 2) c)),
|
|
6
|
+
// c = sum (1 - w_i / sum w)^2 / (n_i - 1) / (k^2 - 1), on (k - 1, 1 / (3 c)) degrees of freedom.
|
|
7
|
+
// shapiro.test: W and its p-value from src/htest/core/shapiro.ts (Royston's AS R94).
|
|
8
|
+
import { pf1 } from "../distributions/f.js";
|
|
9
|
+
import { mean, variance } from "../internal/summary.js";
|
|
10
|
+
import { swilkOneMinusW, swilkPValueFromOneMinusW } from "./core/shapiro.js";
|
|
11
|
+
import { toGroups } from "./groups.js";
|
|
12
|
+
/**
|
|
13
|
+
* Test that several normal samples have equal means, without (default) or with the assumption of equal variances.
|
|
14
|
+
* Equivalent to R's `oneway.test()`.
|
|
15
|
+
*
|
|
16
|
+
* @param x An array of samples, or one sample with group labels `g`.
|
|
17
|
+
* @param g Group labels, parallel to `x` (or the options, when `x` is an array of samples).
|
|
18
|
+
*/
|
|
19
|
+
export function onewayTest(x, g, options = {}) {
|
|
20
|
+
let labels;
|
|
21
|
+
if (g !== undefined && !Array.isArray(g))
|
|
22
|
+
options = g;
|
|
23
|
+
else
|
|
24
|
+
labels = g;
|
|
25
|
+
const groups = toGroups(x, labels);
|
|
26
|
+
const varEqual = options.varEqual ?? false;
|
|
27
|
+
const k = groups.length;
|
|
28
|
+
if (k < 2)
|
|
29
|
+
throw new RangeError("not enough groups");
|
|
30
|
+
const ni = groups.map((s) => s.length);
|
|
31
|
+
if (ni.some((n) => n < 2))
|
|
32
|
+
throw new RangeError("not enough observations");
|
|
33
|
+
const mi = groups.map(mean);
|
|
34
|
+
const vi = groups.map(variance);
|
|
35
|
+
const N = ni.reduce((a, b) => a + b, 0);
|
|
36
|
+
let statistic;
|
|
37
|
+
let df2;
|
|
38
|
+
if (varEqual) {
|
|
39
|
+
const m = mean(groups.flat());
|
|
40
|
+
let between = 0;
|
|
41
|
+
let within = 0;
|
|
42
|
+
for (let i = 0; i < k; i++) {
|
|
43
|
+
between += ni[i] * (mi[i] - m) ** 2;
|
|
44
|
+
within += (ni[i] - 1) * vi[i];
|
|
45
|
+
}
|
|
46
|
+
df2 = N - k;
|
|
47
|
+
statistic = between / (k - 1) / (within / df2);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const wi = ni.map((n, i) => n / vi[i]);
|
|
51
|
+
const sw = wi.reduce((a, b) => a + b, 0);
|
|
52
|
+
let c = 0;
|
|
53
|
+
let mw = 0;
|
|
54
|
+
for (let i = 0; i < k; i++) {
|
|
55
|
+
c += (1 - wi[i] / sw) ** 2 / (ni[i] - 1);
|
|
56
|
+
mw += wi[i] * mi[i];
|
|
57
|
+
}
|
|
58
|
+
c /= k * k - 1;
|
|
59
|
+
mw /= sw;
|
|
60
|
+
let num = 0;
|
|
61
|
+
for (let i = 0; i < k; i++)
|
|
62
|
+
num += wi[i] * (mi[i] - mw) ** 2;
|
|
63
|
+
statistic = num / ((k - 1) * (1 + 2 * (k - 2) * c));
|
|
64
|
+
df2 = 1 / (3 * c);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
method: varEqual ? "One-way analysis of means" : "One-way analysis of means (not assuming equal variances)",
|
|
68
|
+
statistic,
|
|
69
|
+
statisticName: "F",
|
|
70
|
+
parameter: [k - 1, df2],
|
|
71
|
+
parameterName: ["num df", "denom df"],
|
|
72
|
+
pValue: pf1(statistic, k - 1, df2, false, false),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Shapiro-Wilk test of normality. Equivalent to R's `shapiro.test()`. `NaN` values are dropped; the sample size must
|
|
77
|
+
* be between 3 and 5000.
|
|
78
|
+
*/
|
|
79
|
+
export function shapiroTest(x) {
|
|
80
|
+
const xs = Array.from(x)
|
|
81
|
+
.filter((v) => !Number.isNaN(v))
|
|
82
|
+
.sort((a, b) => a - b);
|
|
83
|
+
const n = xs.length;
|
|
84
|
+
if (n < 3 || n > 5000)
|
|
85
|
+
throw new RangeError("sample size must be between 3 and 5000");
|
|
86
|
+
if (!Number.isFinite(xs[0]) || !Number.isFinite(xs[n - 1]))
|
|
87
|
+
throw new RangeError("'x' must be finite");
|
|
88
|
+
if (xs[n - 1] === xs[0])
|
|
89
|
+
throw new RangeError("all 'x' values are identical");
|
|
90
|
+
const w1 = swilkOneMinusW(xs);
|
|
91
|
+
return {
|
|
92
|
+
method: "Shapiro-Wilk normality test",
|
|
93
|
+
statistic: 1 - w1,
|
|
94
|
+
statisticName: "W",
|
|
95
|
+
pValue: swilkPValueFromOneMinusW(w1, n),
|
|
96
|
+
};
|
|
97
|
+
}
|