@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,142 @@
|
|
|
1
|
+
// Shapiro-Wilk W statistic and its p-value, for shapiro.test.
|
|
2
|
+
//
|
|
3
|
+
// Written for rStats.js from the published approximations (no ported code): Royston, "Approximating the
|
|
4
|
+
// Shapiro-Wilk W-test for non-normality", Statistics and Computing 2, 1992, and Royston, "Remark AS R94: A remark on
|
|
5
|
+
// Algorithm AS 181: The W-test for normality", Applied Statistics 44, 1995.
|
|
6
|
+
//
|
|
7
|
+
// Coefficients: m_i = qnorm((i - 3/8) / (n + 1/4)) and, with u = 1/sqrt(n), polynomial corrections for the two
|
|
8
|
+
// largest coefficients; the others are m_i / sqrt(phi) so that sum a_i^2 = 1.
|
|
9
|
+
// p-value: n = 3 exactly; 4 <= n <= 11: -log(gamma - log(1 - W)) is normal; n >= 12: log(1 - W) is normal, with
|
|
10
|
+
// means and standard deviations given as polynomials in n or log(n).
|
|
11
|
+
// Beyond the range of Royston's approximation for 4 <= n <= 11 the p-value is 1e-99: 1e-99 is the value R reports
|
|
12
|
+
// here (observed behaviour).
|
|
13
|
+
import { pnorm, qnorm } from "../../distributions/normal.js";
|
|
14
|
+
/** Polynomial c[0] + c[1] x + c[2] x^2 + ... */
|
|
15
|
+
function poly(c, x) {
|
|
16
|
+
let s = 0;
|
|
17
|
+
for (let i = c.length - 1; i >= 0; i--)
|
|
18
|
+
s = s * x + c[i];
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
const C1 = [0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
|
|
22
|
+
const C2 = [0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
|
|
23
|
+
const C3 = [0.544, -0.39978, 0.025054, -6.714e-4];
|
|
24
|
+
const C4 = [1.3822, -0.77857, 0.062767, -0.0020322];
|
|
25
|
+
const C5 = [-1.5861, -0.31082, -0.083751, 0.0038915];
|
|
26
|
+
const C6 = [-0.4803, -0.082676, 0.0030302];
|
|
27
|
+
const G = [-2.273, 0.459];
|
|
28
|
+
/**
|
|
29
|
+
* The Shapiro-Wilk coefficients a_1..a_n for a sample of size n (3 <= n <= 5000), antisymmetric
|
|
30
|
+
* (a_i = -a_(n+1-i)) with sum a_i^2 = 1.
|
|
31
|
+
*/
|
|
32
|
+
export function swilkCoefficients(n) {
|
|
33
|
+
if (!(n >= 3) || n !== Math.floor(n))
|
|
34
|
+
throw new RangeError("n must be an integer of at least 3");
|
|
35
|
+
const a = new Float64Array(n);
|
|
36
|
+
if (n === 3) {
|
|
37
|
+
a[0] = -Math.SQRT1_2;
|
|
38
|
+
a[2] = Math.SQRT1_2;
|
|
39
|
+
return a;
|
|
40
|
+
}
|
|
41
|
+
const half = Math.floor(n / 2);
|
|
42
|
+
const m = new Float64Array(half); // m for the upper half, m[0] the largest
|
|
43
|
+
let summ2 = 0;
|
|
44
|
+
for (let i = 0; i < half; i++) {
|
|
45
|
+
// upper half: i-th largest expected normal order statistic
|
|
46
|
+
m[i] = -qnorm((i + 1 - 0.375) / (n + 0.25));
|
|
47
|
+
summ2 += m[i] * m[i];
|
|
48
|
+
}
|
|
49
|
+
summ2 *= 2;
|
|
50
|
+
const ssumm2 = Math.sqrt(summ2);
|
|
51
|
+
const rsn = 1 / Math.sqrt(n);
|
|
52
|
+
// the two largest coefficients a_n and a_(n-1), corrected by polynomials in u = 1/sqrt(n)
|
|
53
|
+
const an = m[0] / ssumm2 + poly(C1, rsn);
|
|
54
|
+
const up = new Float64Array(half);
|
|
55
|
+
up[0] = an;
|
|
56
|
+
let first;
|
|
57
|
+
let fac;
|
|
58
|
+
if (n > 5) {
|
|
59
|
+
const an1 = m[1] / ssumm2 + poly(C2, rsn);
|
|
60
|
+
up[1] = an1;
|
|
61
|
+
first = 2;
|
|
62
|
+
fac = Math.sqrt((summ2 - 2 * m[0] * m[0] - 2 * m[1] * m[1]) / (1 - 2 * an * an - 2 * an1 * an1));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
first = 1;
|
|
66
|
+
fac = Math.sqrt((summ2 - 2 * m[0] * m[0]) / (1 - 2 * an * an));
|
|
67
|
+
}
|
|
68
|
+
for (let i = first; i < half; i++)
|
|
69
|
+
up[i] = m[i] / fac;
|
|
70
|
+
// a_n, a_(n-1), ... positive for the largest observations
|
|
71
|
+
for (let i = 0; i < half; i++) {
|
|
72
|
+
a[n - 1 - i] = up[i];
|
|
73
|
+
a[i] = -up[i];
|
|
74
|
+
}
|
|
75
|
+
return a;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 1 - W for a sample sorted in increasing order, formed as (r + c)(r - c) / r^2 from c = sum a_i (x_i - mean) and
|
|
79
|
+
* r^2 = sum a_i^2 sum (x_i - mean)^2, so that it keeps its relative accuracy when W is close to 1 (the p-value
|
|
80
|
+
* depends on log(1 - W)). The data are scaled by their range first.
|
|
81
|
+
*/
|
|
82
|
+
export function swilkOneMinusW(sorted) {
|
|
83
|
+
const n = sorted.length;
|
|
84
|
+
const a = swilkCoefficients(n);
|
|
85
|
+
const range = sorted[n - 1] - sorted[0];
|
|
86
|
+
let mean = 0;
|
|
87
|
+
for (const v of sorted)
|
|
88
|
+
mean += v / range;
|
|
89
|
+
mean /= n;
|
|
90
|
+
let ssa = 0;
|
|
91
|
+
let ssx = 0;
|
|
92
|
+
let sax = 0;
|
|
93
|
+
for (let i = 0; i < n; i++) {
|
|
94
|
+
const d = sorted[i] / range - mean;
|
|
95
|
+
ssa += a[i] * a[i];
|
|
96
|
+
ssx += d * d;
|
|
97
|
+
sax += a[i] * d;
|
|
98
|
+
}
|
|
99
|
+
const r = Math.sqrt(ssa * ssx);
|
|
100
|
+
return ((r - sax) * (r + sax)) / (ssa * ssx);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The W statistic of a sample sorted in increasing order:
|
|
104
|
+
* W = (sum a_i x_(i))^2 / sum (x_i - mean)^2.
|
|
105
|
+
*/
|
|
106
|
+
export function swilkW(sorted) {
|
|
107
|
+
return 1 - swilkOneMinusW(sorted);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The p-value P(W' <= W) of the Shapiro-Wilk test for a sample of size n (Royston's approximations), from W.
|
|
111
|
+
*/
|
|
112
|
+
export function swilkPValue(w, n) {
|
|
113
|
+
return swilkPValueFromOneMinusW(1 - w, n);
|
|
114
|
+
}
|
|
115
|
+
/** 6/pi and asin(sqrt(3/4)) to 15 digits, the values R's swilk uses for n = 3 */
|
|
116
|
+
const PI6 = 1.90985931710274;
|
|
117
|
+
const STQR = 1.0471975511966;
|
|
118
|
+
/**
|
|
119
|
+
* The p-value P(W' <= W) of the Shapiro-Wilk test for a sample of size n, from w1 = 1 - W (see swilkOneMinusW).
|
|
120
|
+
*/
|
|
121
|
+
export function swilkPValueFromOneMinusW(w1, n) {
|
|
122
|
+
if (Number.isNaN(w1) || !(n >= 3))
|
|
123
|
+
return NaN;
|
|
124
|
+
if (n === 3) {
|
|
125
|
+
// exact: 6/pi (asin(sqrt(W)) - asin(sqrt(3/4))), W >= 3/4
|
|
126
|
+
const p = PI6 * (Math.asin(Math.sqrt(1 - w1)) - STQR);
|
|
127
|
+
return Math.max(p, 0);
|
|
128
|
+
}
|
|
129
|
+
const y = Math.log(w1);
|
|
130
|
+
if (n <= 11) {
|
|
131
|
+
const gamma = poly(G, n);
|
|
132
|
+
if (y >= gamma)
|
|
133
|
+
return 1e-99; // beyond the range of the approximation; 1e-99 is the value R reports here
|
|
134
|
+
const mu = poly(C3, n);
|
|
135
|
+
const sigma = Math.exp(poly(C4, n));
|
|
136
|
+
return pnorm(-Math.log(gamma - y), { mean: mu, sd: sigma, lowerTail: false });
|
|
137
|
+
}
|
|
138
|
+
const xx = Math.log(n);
|
|
139
|
+
const mu = poly(C5, xx);
|
|
140
|
+
const sigma = Math.exp(poly(C6, xx));
|
|
141
|
+
return pnorm(y, { mean: mu, sd: sigma, lowerTail: false });
|
|
142
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SmirnovAlternative = "two.sided" | "less" | "greater";
|
|
2
|
+
export interface SmirnovOptions {
|
|
3
|
+
/** Which statistic: max |F_x - F_y| ("two.sided", default), max (F_x - F_y) ("greater"), max (F_y - F_x) ("less"). */
|
|
4
|
+
alternative?: SmirnovAlternative;
|
|
5
|
+
/**
|
|
6
|
+
* The pooled sample (length m + n). With ties, the statistic is only observed where the sorted values change.
|
|
7
|
+
* Omit for continuous data without ties.
|
|
8
|
+
*/
|
|
9
|
+
z?: readonly number[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* P(D >= d) for the two-sample Smirnov statistic with sample sizes m and n, exactly.
|
|
13
|
+
* The comparison D >= d is done on the integer scale |i n - j m| >= d m n with a relative fuzz of 1e-10, so that a
|
|
14
|
+
* statistic computed in floating point from the data is not lost to rounding.
|
|
15
|
+
*
|
|
16
|
+
* @param d - Value of the statistic.
|
|
17
|
+
* @param m - Size of the first sample (x).
|
|
18
|
+
* @param n - Size of the second sample (y).
|
|
19
|
+
*/
|
|
20
|
+
export declare function psmirnovUpper(d: number, m: number, n: number, options?: SmirnovOptions): number;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Exact distribution of the two-sample Kolmogorov-Smirnov (Smirnov) statistic, for ks.test.
|
|
2
|
+
//
|
|
3
|
+
// Written for rStats.js from the published method (no ported code). Under the null hypothesis every arrangement of
|
|
4
|
+
// the m + n pooled observations is equally likely, i.e. every lattice path from (0, 0) to (m, n) (i steps for x,
|
|
5
|
+
// j steps for y). The statistic is the largest |i/m - j/n| (two-sided) or i/m - j/n ("greater") or j/n - i/m ("less")
|
|
6
|
+
// over the points where it can be observed: after every observation without ties, and only where the pooled sorted
|
|
7
|
+
// sample changes value when there are ties (Schröer and Trenkler, Computational Statistics & Data Analysis 20, 1995).
|
|
8
|
+
//
|
|
9
|
+
// P(D >= d) is summed over the points where a path first reaches the boundary: with v(i, j) the fraction of paths
|
|
10
|
+
// to (i, j) that stay inside,
|
|
11
|
+
// v(i, j) = i/(i+j) v(i-1, j) + j/(i+j) v(i, j-1),
|
|
12
|
+
// and a path through a boundary point is counted with the probability dhyper(i; m, n, i + j) of passing it. All
|
|
13
|
+
// terms are positive, so small p-values keep their relative accuracy (1 - P(D < d) would lose them).
|
|
14
|
+
import { dhyper1 } from "../../distributions/hypergeometric.js";
|
|
15
|
+
/**
|
|
16
|
+
* P(D >= d) for the two-sample Smirnov statistic with sample sizes m and n, exactly.
|
|
17
|
+
* The comparison D >= d is done on the integer scale |i n - j m| >= d m n with a relative fuzz of 1e-10, so that a
|
|
18
|
+
* statistic computed in floating point from the data is not lost to rounding.
|
|
19
|
+
*
|
|
20
|
+
* @param d - Value of the statistic.
|
|
21
|
+
* @param m - Size of the first sample (x).
|
|
22
|
+
* @param n - Size of the second sample (y).
|
|
23
|
+
*/
|
|
24
|
+
export function psmirnovUpper(d, m, n, options = {}) {
|
|
25
|
+
const alternative = options.alternative ?? "two.sided";
|
|
26
|
+
if (Number.isNaN(d) || !(m >= 1) || !(n >= 1) || m !== Math.floor(m) || n !== Math.floor(n))
|
|
27
|
+
return NaN;
|
|
28
|
+
if (d <= 0)
|
|
29
|
+
return 1;
|
|
30
|
+
if (d > 1)
|
|
31
|
+
return 0;
|
|
32
|
+
// where the statistic is observed: after step s = i + j (1..m+n-1), or only where z changes
|
|
33
|
+
const N = m + n;
|
|
34
|
+
let observed;
|
|
35
|
+
if (options.z !== undefined) {
|
|
36
|
+
if (options.z.length !== N)
|
|
37
|
+
throw new RangeError("z must have length m + n");
|
|
38
|
+
const zs = [...options.z].sort((a, b) => a - b);
|
|
39
|
+
observed = new Array(N + 1).fill(false);
|
|
40
|
+
for (let s = 1; s < N; s++)
|
|
41
|
+
observed[s] = zs[s] !== zs[s - 1];
|
|
42
|
+
}
|
|
43
|
+
const threshold = d * m * n * (1 - 1e-10);
|
|
44
|
+
const crosses = (i, j) => {
|
|
45
|
+
const diff = i * n - j * m;
|
|
46
|
+
const v = alternative === "two.sided" ? Math.abs(diff) : alternative === "greater" ? diff : -diff;
|
|
47
|
+
return v >= threshold;
|
|
48
|
+
};
|
|
49
|
+
// v over j for the current i (rolling row)
|
|
50
|
+
let prev = new Float64Array(n + 1);
|
|
51
|
+
let cur = new Float64Array(n + 1);
|
|
52
|
+
let p = 0;
|
|
53
|
+
for (let i = 0; i <= m; i++) {
|
|
54
|
+
for (let j = 0; j <= n; j++) {
|
|
55
|
+
if (i === 0 && j === 0) {
|
|
56
|
+
cur[0] = 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const s = i + j;
|
|
60
|
+
const reach = (i > 0 ? (i / s) * prev[j] : 0) + (j > 0 ? (j / s) * cur[j - 1] : 0);
|
|
61
|
+
const obs = observed === undefined ? s < N : observed[s];
|
|
62
|
+
if (obs && crosses(i, j)) {
|
|
63
|
+
if (reach > 0)
|
|
64
|
+
p += reach * dhyper1(i, m, n, s, false);
|
|
65
|
+
cur[j] = 0;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
cur[j] = reach;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
[prev, cur] = [cur, prev];
|
|
72
|
+
cur.fill(0);
|
|
73
|
+
}
|
|
74
|
+
return Math.min(p, 1);
|
|
75
|
+
}
|
package/dist/htest/fisherTest.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Fisher's exact test (R's fisher.test).
|
|
2
2
|
//
|
|
3
|
-
// 2x2 tables
|
|
4
|
-
// (
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
3
|
+
// 2x2 tables, written for rStats.js from the textbook construction and R's documented behaviour: with the margins
|
|
4
|
+
// fixed, the (1, 1) cell follows Fisher's noncentral hypergeometric distribution, the central hypergeometric density
|
|
5
|
+
// tilted by psi^x (noncentrality psi = odds ratio; Fisher 1935, Cornfield 1956). The conditional MLE is the psi whose
|
|
6
|
+
// mean equals the observed cell; the confidence limits are the psi at which a one-sided tail probability equals α.
|
|
7
|
+
// - Densities are formed in logs from the accurate log dhyper (evaluated every 16 points, exact ratios in
|
|
8
|
+
// between) and normalised with double-double sums, so the root
|
|
9
|
+
// search sees accurate function values; at psi = 1 the tails are the central hypergeometric distribution function
|
|
10
|
+
// - Roots are found with R's uniroot conventions (Brent, tolerance eps^0.25; src/internal/uniroot.ts): if the root
|
|
11
|
+
// lies below 1, on psi in [0, 1]; otherwise on s = 1/psi in [eps, 1]. R's estimate and limits are where this
|
|
12
|
+
// iteration stops (1e-6 to 1e-2 from the true roots), so the brackets and tolerance are part of the result
|
|
13
|
+
// - The two-sided p-value is the sum of probabilities of tables whose probability is at most (1 + 1e-7) times the
|
|
14
|
+
// observed one (R's convention); for or = 0 or Inf the distribution is a point mass at the end of the support
|
|
12
15
|
// - Counts are made integer by rounding half to even (R rounds with a warning)
|
|
16
|
+
// The same distribution and root conventions are used for the exact Mantel-Haenszel test (src/htest/core/mantelhaen.ts).
|
|
13
17
|
//
|
|
14
18
|
// rxc tables: the probabilities of tables with fixed margins (multivariate hypergeometric) are
|
|
15
19
|
// traversed via a network that fixes one column at a time (Mehta & Patel 1983, JASA 78, 427-434),
|
|
@@ -27,8 +31,6 @@ import { roundHalfEven } from "../internal/integer.js";
|
|
|
27
31
|
import { uniroot } from "../internal/uniroot.js";
|
|
28
32
|
import { ddAdd, ddLog, ddNeg, ddSub } from "../special/dd.js";
|
|
29
33
|
import { matchAlternative } from "./types.js";
|
|
30
|
-
/** Relative tolerance for treating a table as "equally probable as the observed table" in the two-sided p-value */
|
|
31
|
-
const REL_ERR = 1 + 1e-7;
|
|
32
34
|
/**
|
|
33
35
|
* Fisher's exact test for count data. Equivalent to R's `fisher.test()`.
|
|
34
36
|
*
|
|
@@ -78,93 +80,109 @@ export function fisherTest(x, options = {}) {
|
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
// ---- 2×2 ----
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
+
/** Relative slack for counting a table as "no more likely than the observed one" in the two-sided p-value */
|
|
84
|
+
const SLACK = 1 + 1e-7;
|
|
85
|
+
/** Spacing of the points at which the central log density is evaluated directly */
|
|
86
|
+
const ANCHOR = 16;
|
|
87
|
+
/** Double-double sum of v[from..to] (inclusive, clipped to the array) */
|
|
88
|
+
function accurateSum(v, from, to) {
|
|
89
|
+
let acc = [0, 0];
|
|
90
|
+
for (let i = Math.max(0, from); i <= Math.min(to, v.length - 1); i++)
|
|
91
|
+
if (v[i] !== 0)
|
|
92
|
+
acc = ddAdd(acc, [v[i], 0]);
|
|
93
|
+
return acc[0];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fisher's noncentral hypergeometric distribution of the (1, 1) cell of a 2 x 2 table with margins fixed:
|
|
97
|
+
* P(X = i; psi) proportional to dhyper(i; m, n, k) psi^i on lo..hi
|
|
98
|
+
*/
|
|
99
|
+
class OddsRatioDistribution {
|
|
83
100
|
m;
|
|
84
101
|
n;
|
|
85
102
|
k;
|
|
86
103
|
lo;
|
|
87
104
|
hi;
|
|
88
|
-
|
|
105
|
+
/** log dhyper(i; m, n, k) for i = lo..hi */
|
|
106
|
+
logCentral;
|
|
89
107
|
constructor(m, n, k) {
|
|
90
108
|
this.m = m;
|
|
91
109
|
this.n = n;
|
|
92
110
|
this.k = k;
|
|
93
111
|
this.lo = Math.max(0, k - n);
|
|
94
112
|
this.hi = Math.min(k, m);
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
// 1
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (let i =
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.logdc[i] = this.logdc[i - 1] + Math.log(((m - y) * (k - y)) / ((y + 1) * (n - k + y + 1)));
|
|
113
|
+
this.logCentral = new Float64Array(this.hi - this.lo + 1);
|
|
114
|
+
// dhyper1 at every ANCHOR-th point; in between, multiply by the exact ratio
|
|
115
|
+
// c(i+1)/c(i) = (m - i)(k - i) / ((i + 1)(n - k + i + 1)) (the product stays within a few ulps)
|
|
116
|
+
let anchor = 0;
|
|
117
|
+
let prod = 1;
|
|
118
|
+
for (let i = this.lo; i <= this.hi; i++) {
|
|
119
|
+
const j = i - this.lo;
|
|
120
|
+
if (j % ANCHOR === 0 || !(prod > 0 && prod < Infinity)) {
|
|
121
|
+
anchor = dhyper1(i, m, n, k, true);
|
|
122
|
+
prod = 1;
|
|
123
|
+
this.logCentral[j] = anchor;
|
|
107
124
|
}
|
|
125
|
+
else
|
|
126
|
+
this.logCentral[j] = anchor + Math.log(prod);
|
|
127
|
+
prod *= ((m - i) * (k - i)) / ((i + 1) * (n - k + i + 1));
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
|
-
/**
|
|
111
|
-
density(
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
/** Normalised density on lo..hi for 0 < psi < Infinity */
|
|
131
|
+
density(psi) {
|
|
132
|
+
const len = this.logCentral.length;
|
|
133
|
+
const lp = Math.log(psi);
|
|
134
|
+
const out = new Float64Array(len);
|
|
135
|
+
let top = -Infinity;
|
|
136
|
+
for (let j = 0; j < len; j++) {
|
|
137
|
+
const v = this.logCentral[j] + (this.lo + j) * lp;
|
|
138
|
+
out[j] = v;
|
|
139
|
+
if (v > top)
|
|
140
|
+
top = v;
|
|
119
141
|
}
|
|
120
|
-
//
|
|
121
|
-
for (let
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
// exp underflows to exactly 0 below -746; skipping those terms leaves every sum unchanged
|
|
143
|
+
for (let j = 0; j < len; j++) {
|
|
144
|
+
const e = out[j] - top;
|
|
145
|
+
out[j] = e < -746 ? 0 : Math.exp(e);
|
|
146
|
+
}
|
|
147
|
+
const total = accurateSum(out, 0, len - 1);
|
|
148
|
+
for (let j = 0; j < len; j++)
|
|
149
|
+
if (out[j] !== 0)
|
|
150
|
+
out[j] = out[j] / total;
|
|
151
|
+
return out;
|
|
127
152
|
}
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
/** E[X; psi] */
|
|
154
|
+
mean(psi) {
|
|
155
|
+
if (psi === 0)
|
|
130
156
|
return this.lo;
|
|
131
|
-
if (
|
|
157
|
+
if (psi === Infinity)
|
|
132
158
|
return this.hi;
|
|
133
|
-
const d = this.density(
|
|
134
|
-
let
|
|
135
|
-
for (let
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
const d = this.density(psi);
|
|
160
|
+
let acc = [0, 0];
|
|
161
|
+
for (let j = 0; j < d.length; j++)
|
|
162
|
+
if (d[j] !== 0)
|
|
163
|
+
acc = ddAdd(acc, [(this.lo + j) * d[j], 0]);
|
|
164
|
+
return acc[0];
|
|
138
165
|
}
|
|
139
|
-
/** P(X <= q)
|
|
140
|
-
|
|
141
|
-
if (
|
|
166
|
+
/** P(X <= q; psi), or P(X >= q; psi) when upper */
|
|
167
|
+
tail(q, psi, upper) {
|
|
168
|
+
if (psi === 1) {
|
|
142
169
|
return upper ? phyper1(q - 1, this.m, this.n, this.k, false, false) : phyper1(q, this.m, this.n, this.k, true, false);
|
|
143
170
|
}
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const d = this.density(
|
|
149
|
-
const
|
|
150
|
-
return upper ?
|
|
171
|
+
if (psi === 0 || psi === Infinity) {
|
|
172
|
+
const at = psi === 0 ? this.lo : this.hi;
|
|
173
|
+
return (upper ? q <= at : q >= at) ? 1 : 0;
|
|
174
|
+
}
|
|
175
|
+
const d = this.density(psi);
|
|
176
|
+
const j = q - this.lo;
|
|
177
|
+
return upper ? accurateSum(d, j, d.length - 1) : accurateSum(d, 0, j);
|
|
151
178
|
}
|
|
152
179
|
}
|
|
153
|
-
function sumDD(d, from, to) {
|
|
154
|
-
let s = [0, 0];
|
|
155
|
-
for (let i = Math.max(0, from); i < Math.min(to, d.length); i++)
|
|
156
|
-
s = ddAdd(s, [d[i], 0]);
|
|
157
|
-
return s[0];
|
|
158
|
-
}
|
|
159
180
|
/**
|
|
160
|
-
* Root of
|
|
161
|
-
*
|
|
181
|
+
* Root of f, monotone in the odds ratio psi: searched for as psi in [0, 1] when it lies below 1, otherwise as
|
|
182
|
+
* s = 1/psi in [eps, 1] (uniroot with its default tolerance in both cases)
|
|
162
183
|
*/
|
|
163
|
-
function
|
|
164
|
-
return uniroot(f, 0, 1);
|
|
165
|
-
}
|
|
166
|
-
function rootAboveOne(f) {
|
|
167
|
-
return 1 / uniroot((t) => f(1 / t), Number.EPSILON, 1);
|
|
184
|
+
function oddsRatioRoot(f, belowOne) {
|
|
185
|
+
return belowOne ? uniroot(f, 0, 1) : 1 / uniroot((s) => f(1 / s), Number.EPSILON, 1);
|
|
168
186
|
}
|
|
169
187
|
function fisher2x2(t, options, method) {
|
|
170
188
|
const alternative = matchAlternative(options.alternative);
|
|
@@ -174,39 +192,36 @@ function fisher2x2(t, options, method) {
|
|
|
174
192
|
const confLevel = options.confLevel ?? 0.95;
|
|
175
193
|
if (!(confLevel > 0 && confLevel < 1))
|
|
176
194
|
throw new RangeError("confLevel must be a single number between 0 and 1");
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const h = new NoncentralHyper(m, n, k);
|
|
195
|
+
const [[a, b], [c, d]] = t;
|
|
196
|
+
const x = a;
|
|
197
|
+
const dist = new OddsRatioDistribution(a + c, b + d, a + b);
|
|
198
|
+
const { lo, hi } = dist;
|
|
182
199
|
let pValue;
|
|
183
200
|
if (alternative === "less")
|
|
184
|
-
pValue =
|
|
201
|
+
pValue = dist.tail(x, or, false);
|
|
185
202
|
else if (alternative === "greater")
|
|
186
|
-
pValue =
|
|
203
|
+
pValue = dist.tail(x, or, true);
|
|
187
204
|
else if (or === 0)
|
|
188
|
-
pValue =
|
|
205
|
+
pValue = x === lo ? 1 : 0;
|
|
189
206
|
else if (or === Infinity)
|
|
190
|
-
pValue =
|
|
207
|
+
pValue = x === hi ? 1 : 0;
|
|
191
208
|
else {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
let
|
|
195
|
-
for (const v of
|
|
196
|
-
if (v <=
|
|
197
|
-
|
|
198
|
-
pValue =
|
|
209
|
+
const dens = dist.density(or);
|
|
210
|
+
const cut = dens[x - lo] * SLACK;
|
|
211
|
+
let acc = [0, 0];
|
|
212
|
+
for (const v of dens)
|
|
213
|
+
if (v <= cut)
|
|
214
|
+
acc = ddAdd(acc, [v, 0]);
|
|
215
|
+
pValue = acc[0];
|
|
199
216
|
}
|
|
200
|
-
// Conditional MLE: the noncentrality whose mean is x
|
|
201
217
|
let estimate;
|
|
202
|
-
if (x ===
|
|
218
|
+
if (x === lo)
|
|
203
219
|
estimate = 0;
|
|
204
|
-
else if (x ===
|
|
220
|
+
else if (x === hi)
|
|
205
221
|
estimate = Infinity;
|
|
206
222
|
else {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
estimate = mu > x ? rootBelowOne(f) : mu < x ? rootAboveOne(f) : 1;
|
|
223
|
+
const mu1 = dist.mean(1);
|
|
224
|
+
estimate = mu1 === x ? 1 : oddsRatioRoot((psi) => dist.mean(psi) - x, mu1 > x);
|
|
210
225
|
}
|
|
211
226
|
const result = {
|
|
212
227
|
method,
|
|
@@ -217,32 +232,31 @@ function fisher2x2(t, options, method) {
|
|
|
217
232
|
nullValue: or,
|
|
218
233
|
nullValueName: "odds ratio",
|
|
219
234
|
};
|
|
220
|
-
if (options.confInt ?? true)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
result.confLevel = confLevel;
|
|
235
|
+
if (!(options.confInt ?? true))
|
|
236
|
+
return result;
|
|
237
|
+
// P(X <= x; psi) decreases and P(X >= x; psi) increases in psi
|
|
238
|
+
const upperLimit = (level) => {
|
|
239
|
+
if (x === hi)
|
|
240
|
+
return Infinity;
|
|
241
|
+
const p1 = dist.tail(x, 1, false);
|
|
242
|
+
return p1 === level ? 1 : oddsRatioRoot((psi) => dist.tail(x, psi, false) - level, p1 < level);
|
|
243
|
+
};
|
|
244
|
+
const lowerLimit = (level) => {
|
|
245
|
+
if (x === lo)
|
|
246
|
+
return 0;
|
|
247
|
+
const p1 = dist.tail(x, 1, true);
|
|
248
|
+
return p1 === level ? 1 : oddsRatioRoot((psi) => dist.tail(x, psi, true) - level, p1 > level);
|
|
249
|
+
};
|
|
250
|
+
let confInt;
|
|
251
|
+
if (alternative === "less")
|
|
252
|
+
confInt = [0, upperLimit(1 - confLevel)];
|
|
253
|
+
else if (alternative === "greater")
|
|
254
|
+
confInt = [lowerLimit(1 - confLevel), Infinity];
|
|
255
|
+
else {
|
|
256
|
+
const alpha = (1 - confLevel) / 2;
|
|
257
|
+
confInt = [lowerLimit(alpha), upperLimit(alpha)];
|
|
244
258
|
}
|
|
245
|
-
return result;
|
|
259
|
+
return { ...result, confInt, confLevel };
|
|
246
260
|
}
|
|
247
261
|
// ---- r×c ----
|
|
248
262
|
/** Table of log k! in double-double (extended as needed) */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Sample } from "./types.js";
|
|
2
|
+
/** Group label: a number or a string. */
|
|
3
|
+
export type GroupLabel = number | string;
|
|
4
|
+
/** Groups with their labels: the sorted labels (x, g form) or the 1-based positions in the list (list form) */
|
|
5
|
+
export interface LabelledGroups {
|
|
6
|
+
labels: string[];
|
|
7
|
+
groups: number[][];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Normalise the two input forms to groups with NaN removed, each with its label. In the x, g form a label whose
|
|
11
|
+
* values are all missing has no group (as R drops an unused factor level). In the list form a group left empty is
|
|
12
|
+
* dropped, or rejected with R's message when `emptyGroups` is `"error"` (kruskal.test, fligner.test).
|
|
13
|
+
*/
|
|
14
|
+
export declare function labelledGroups(x: Sample | readonly Sample[], g?: readonly GroupLabel[], emptyGroups?: "drop" | "error"): LabelledGroups;
|
|
15
|
+
/** Normalise the two input forms to an array of non-empty groups with NaN removed (see {@link labelledGroups}) */
|
|
16
|
+
export declare function toGroups(x: Sample | readonly Sample[], g?: readonly GroupLabel[], emptyGroups?: "drop" | "error"): number[][];
|
|
17
|
+
/** Median (the mean of the two middle values for an even count) */
|
|
18
|
+
export declare function median(x: readonly number[]): number;
|
|
19
|
+
/** Rows of a blocks x treatments table with any NaN dropped (R's complete.cases) */
|
|
20
|
+
export declare function completeRows(y: readonly (readonly number[])[]): number[][];
|