@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.
Files changed (168) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.ja.md +36 -20
  3. package/README.md +34 -18
  4. package/THIRD_PARTY_NOTICES.md +25 -5
  5. package/dist/chunks/chunk-2JCIB5H2.js +2 -0
  6. package/dist/chunks/chunk-7JVSDGQQ.js +2 -0
  7. package/dist/chunks/chunk-7NK4SA6B.js +2 -0
  8. package/dist/chunks/chunk-AJYQNFXK.js +2 -0
  9. package/dist/chunks/chunk-DRNMIEYH.js +2 -0
  10. package/dist/chunks/chunk-NAYDS7CL.js +2 -0
  11. package/dist/chunks/chunk-NOCEAM44.js +2 -0
  12. package/dist/chunks/chunk-QNAIK4PY.js +2 -0
  13. package/dist/chunks/chunk-TPC6MTYC.js +2 -0
  14. package/dist/chunks/chunk-UGUQYWRQ.js +2 -0
  15. package/dist/chunks/chunk-XM2TVD63.js +2 -0
  16. package/dist/chunks/chunk-Y4IZV7XK.js +2 -0
  17. package/dist/chunks/chunk-ZWJ42QN6.js +2 -0
  18. package/dist/descriptive/basic.d.ts +73 -0
  19. package/dist/descriptive/basic.js +256 -0
  20. package/dist/descriptive/cov.d.ts +39 -0
  21. package/dist/descriptive/cov.js +105 -0
  22. package/dist/descriptive/hist.d.ts +64 -0
  23. package/dist/descriptive/hist.js +304 -0
  24. package/dist/descriptive/moments.d.ts +63 -0
  25. package/dist/descriptive/moments.js +158 -0
  26. package/dist/descriptive/quantile.d.ts +50 -0
  27. package/dist/descriptive/quantile.js +131 -0
  28. package/dist/descriptive/rank.d.ts +23 -0
  29. package/dist/descriptive/rank.js +72 -0
  30. package/dist/descriptive/table.d.ts +54 -0
  31. package/dist/descriptive/table.js +160 -0
  32. package/dist/descriptive.js +2 -0
  33. package/dist/distributions/arcsine.d.ts +24 -0
  34. package/dist/distributions/arcsine.js +73 -0
  35. package/dist/distributions/invgamma.d.ts +29 -0
  36. package/dist/distributions/invgamma.js +85 -0
  37. package/dist/distributions/kumaraswamy.d.ts +17 -0
  38. package/dist/distributions/kumaraswamy.js +96 -0
  39. package/dist/distributions/laplace.d.ts +23 -0
  40. package/dist/distributions/laplace.js +69 -0
  41. package/dist/distributions/multinomial.d.ts +24 -0
  42. package/dist/distributions/multinomial.js +70 -0
  43. package/dist/distributions/nbeta.js +28 -10
  44. package/dist/distributions/nchisq.js +20 -3
  45. package/dist/distributions/pareto.d.ts +23 -0
  46. package/dist/distributions/pareto.js +76 -0
  47. package/dist/distributions/summary.d.ts +25 -0
  48. package/dist/distributions/summary.js +289 -0
  49. package/dist/distributions/triangular.d.ts +25 -0
  50. package/dist/distributions/triangular.js +87 -0
  51. package/dist/distributions.js +2 -0
  52. package/dist/entries/descriptive.d.ts +7 -0
  53. package/dist/entries/descriptive.js +8 -0
  54. package/dist/entries/distributions.d.ts +41 -0
  55. package/dist/entries/distributions.js +41 -0
  56. package/dist/entries/linalg.d.ts +11 -0
  57. package/dist/entries/linalg.js +11 -0
  58. package/dist/entries/numerics.d.ts +8 -0
  59. package/dist/entries/numerics.js +6 -0
  60. package/dist/entries/special.d.ts +9 -0
  61. package/dist/entries/special.js +9 -0
  62. package/dist/entries/tests.d.ts +21 -0
  63. package/dist/entries/tests.js +20 -0
  64. package/dist/htest/ansariTest.d.ts +13 -0
  65. package/dist/htest/ansariTest.js +80 -0
  66. package/dist/htest/aov.d.ts +85 -0
  67. package/dist/htest/aov.js +178 -0
  68. package/dist/htest/binomTest.d.ts +36 -0
  69. package/dist/htest/binomTest.js +121 -0
  70. package/dist/htest/cor.d.ts +14 -0
  71. package/dist/htest/cor.js +4 -3
  72. package/dist/htest/core/ansari.d.ts +18 -0
  73. package/dist/htest/core/ansari.js +121 -0
  74. package/dist/htest/core/kolmogorov.d.ts +26 -0
  75. package/dist/htest/core/kolmogorov.js +221 -0
  76. package/dist/htest/core/mantelhaen.d.ts +26 -0
  77. package/dist/htest/core/mantelhaen.js +145 -0
  78. package/dist/htest/core/shapiro.d.ts +24 -0
  79. package/dist/htest/core/shapiro.js +142 -0
  80. package/dist/htest/core/smirnov.d.ts +20 -0
  81. package/dist/htest/core/smirnov.js +75 -0
  82. package/dist/htest/fisherTest.js +134 -120
  83. package/dist/htest/groups.d.ts +20 -0
  84. package/dist/htest/groups.js +62 -0
  85. package/dist/htest/ksTest.d.ts +31 -0
  86. package/dist/htest/ksTest.js +106 -0
  87. package/dist/htest/mantelhaenTest.d.ts +26 -0
  88. package/dist/htest/mantelhaenTest.js +196 -0
  89. package/dist/htest/mcnemarTest.d.ts +26 -0
  90. package/dist/htest/mcnemarTest.js +99 -0
  91. package/dist/htest/onewayTest.d.ts +26 -0
  92. package/dist/htest/onewayTest.js +97 -0
  93. package/dist/htest/pairwise.d.ts +64 -0
  94. package/dist/htest/pairwise.js +152 -0
  95. package/dist/htest/poissonTest.d.ts +35 -0
  96. package/dist/htest/poissonTest.js +170 -0
  97. package/dist/htest/propTest.d.ts +37 -0
  98. package/dist/htest/propTest.js +164 -0
  99. package/dist/htest/rankTests.d.ts +52 -0
  100. package/dist/htest/rankTests.js +234 -0
  101. package/dist/htest/types.d.ts +2 -0
  102. package/dist/htest/types.js +6 -0
  103. package/dist/htest/varTest.d.ts +48 -0
  104. package/dist/htest/varTest.js +118 -0
  105. package/dist/htest/wilcoxTest.js +136 -174
  106. package/dist/index.d.ts +6 -50
  107. package/dist/index.js +8 -50
  108. package/dist/integrate/quadpack.d.ts +29 -0
  109. package/dist/integrate/quadpack.js +525 -0
  110. package/dist/internal/seq.d.ts +5 -0
  111. package/dist/internal/seq.js +14 -0
  112. package/dist/internal/uniroot.js +10 -71
  113. package/dist/linalg/lu.d.ts +45 -0
  114. package/dist/linalg/lu.js +88 -0
  115. package/dist/linalg/ops.d.ts +90 -0
  116. package/dist/linalg/ops.js +320 -0
  117. package/dist/linalg.js +2 -0
  118. package/dist/models/lm.d.ts +98 -0
  119. package/dist/models/lm.js +187 -0
  120. package/dist/numerics/deriv.d.ts +37 -0
  121. package/dist/numerics/deriv.js +150 -0
  122. package/dist/numerics/integrate.d.ts +91 -0
  123. package/dist/numerics/integrate.js +264 -0
  124. package/dist/numerics/interp.d.ts +103 -0
  125. package/dist/numerics/interp.js +581 -0
  126. package/dist/numerics/ode.d.ts +23 -0
  127. package/dist/numerics/ode.js +59 -0
  128. package/dist/numerics.js +2 -0
  129. package/dist/optim/localmin.d.ts +16 -0
  130. package/dist/optim/localmin.js +97 -0
  131. package/dist/random/beta.js +1 -1
  132. package/dist/random/binomial.js +1 -1
  133. package/dist/random/continuous.js +1 -1
  134. package/dist/random/extra.d.ts +33 -0
  135. package/dist/random/extra.js +62 -0
  136. package/dist/random/gamma.js +1 -1
  137. package/dist/random/hyper.js +1 -1
  138. package/dist/random/mt.js +0 -1
  139. package/dist/random/poisson.js +0 -1
  140. package/dist/random/rank.js +1 -1
  141. package/dist/random/rng.js +1 -1
  142. package/dist/random/sample.js +1 -1
  143. package/dist/rstats.js +2 -2
  144. package/dist/special/besselIK.d.ts +38 -0
  145. package/dist/special/besselIK.js +315 -0
  146. package/dist/special/besselJY.d.ts +32 -0
  147. package/dist/special/besselJY.js +508 -0
  148. package/dist/special/besselSeries.d.ts +8 -0
  149. package/dist/special/besselSeries.js +24 -0
  150. package/dist/special/beta.js +22 -5
  151. package/dist/special/choose.d.ts +75 -0
  152. package/dist/special/choose.js +233 -0
  153. package/dist/special/erf.d.ts +28 -2
  154. package/dist/special/erf.js +37 -10
  155. package/dist/special/extra.d.ts +47 -0
  156. package/dist/special/extra.js +220 -0
  157. package/dist/special/igam.d.ts +4 -0
  158. package/dist/special/igam.js +57 -2
  159. package/dist/special/incbet.js +77 -22
  160. package/dist/special/lgammaDiff.d.ts +7 -0
  161. package/dist/special/lgammaDiff.js +16 -3
  162. package/dist/special/psigamma.d.ts +48 -0
  163. package/dist/special/psigamma.js +318 -0
  164. package/dist/special/rCompatible.d.ts +8 -0
  165. package/dist/special/rCompatible.js +1 -0
  166. package/dist/special.js +2 -0
  167. package/dist/tests.js +2 -0
  168. 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
+ }
@@ -1,15 +1,19 @@
1
1
  // Fisher's exact test (R's fisher.test).
2
2
  //
3
- // 2x2 tables: with the margins fixed, the (1, 1) cell follows a noncentral hypergeometric distribution
4
- // (noncentrality = odds ratio; Fisher 1935). The conditional MLE is the noncentrality whose mean equals the
5
- // cell value; the confidence limits are the noncentralities at which a one-sided probability equals α. This construction
6
- // is the same as scipy.stats.contingency.odds_ratio (scipy/stats/_odds_ratio.py; SciPy commit a087e61,
7
- // BSD-3-Clause, see THIRD_PARTY_NOTICES.md). The root finding and defaults follow observed R output.
8
- // - Roots are found with uniroot (Brent, tolerance eps^0.25): if the noncentrality ψ is below 1, search
9
- // ψ ∈ [0, 1]; if above 1, search 1/ψ ∈ [ε, 1] (R's estimate and interval endpoints are 1e-6 to 1e-2 away
10
- // from the true roots, but this procedure reproduces them to the last few digits)
11
- // - The two-sided p-value is the sum of probabilities of tables whose probability is at most (1 + 1e-7) times the observed one
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
- /** Noncentral hypergeometric distribution (the (1, 1) cell of a 2x2 table) */
82
- class NoncentralHyper {
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
- logdc;
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
- // log P(X = x). Computed with dhyper every 64 points, linked in between by the ratio of neighbours
96
- // P(x + 1) / P(x) = (m - x)(k - x) / ((x + 1)(n - k + x + 1)) (the log-ratio error is about
97
- // 1 ulp, so 63 chained steps stay within 1e-14; calling dhyper at every point is slow for large tables)
98
- const len = this.hi - this.lo + 1;
99
- this.logdc = new Float64Array(len);
100
- for (let i = 0; i < len; i++) {
101
- const x = this.lo + i;
102
- if (i % 64 === 0)
103
- this.logdc[i] = dhyper1(x, m, n, k, true);
104
- else {
105
- const y = x - 1;
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
- /** Probabilities (in support order). The noncentrality ncp satisfies 0 < ncp < ∞ */
111
- density(ncp) {
112
- const lc = Math.log(ncp);
113
- const d = new Float64Array(this.logdc.length);
114
- let max = -Infinity;
115
- for (let i = 0; i < d.length; i++) {
116
- d[i] = this.logdc[i] + lc * (this.lo + i);
117
- if (d[i] > max)
118
- max = d[i];
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
- // Skip exp where exp(d - max) underflows to 0 (R also gives 0 there)
121
- for (let i = 0; i < d.length; i++)
122
- d[i] = d[i] - max < -760 ? 0 : Math.exp(d[i] - max);
123
- const s = sumDD(d, 0, d.length);
124
- for (let i = 0; i < d.length; i++)
125
- d[i] = d[i] / s;
126
- return d;
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
- mean(ncp) {
129
- if (ncp === 0)
153
+ /** E[X; psi] */
154
+ mean(psi) {
155
+ if (psi === 0)
130
156
  return this.lo;
131
- if (ncp === Infinity)
157
+ if (psi === Infinity)
132
158
  return this.hi;
133
- const d = this.density(ncp);
134
- let s = [0, 0];
135
- for (let i = 0; i < d.length; i++)
136
- s = ddAdd(s, [(this.lo + i) * d[i], 0]);
137
- return s[0];
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) (P(X >= q) if upper) */
140
- cdf(q, ncp, upper) {
141
- if (ncp === 1) {
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 (ncp === 0)
145
- return Number(upper ? q <= this.lo : q >= this.lo);
146
- if (ncp === Infinity)
147
- return Number(upper ? q <= this.hi : q >= this.hi);
148
- const d = this.density(ncp);
149
- const i = q - this.lo;
150
- return upper ? sumDD(d, i, d.length) : sumDD(d, 0, i + 1);
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 the increasing function f(ψ) = 0. If f(1) < 0 there is no root in ψ ∈ [0, 1], so search 1/ψ ∈ [ε, 1]
161
- * (the caller chooses the interval from the sign of f(1))
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 rootBelowOne(f) {
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 x = t[0][0];
178
- const m = x + t[1][0];
179
- const n = t[0][1] + t[1][1];
180
- const k = x + t[0][1];
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 = h.cdf(x, or, false);
201
+ pValue = dist.tail(x, or, false);
185
202
  else if (alternative === "greater")
186
- pValue = h.cdf(x, or, true);
203
+ pValue = dist.tail(x, or, true);
187
204
  else if (or === 0)
188
- pValue = Number(x === h.lo);
205
+ pValue = x === lo ? 1 : 0;
189
206
  else if (or === Infinity)
190
- pValue = Number(x === h.hi);
207
+ pValue = x === hi ? 1 : 0;
191
208
  else {
192
- const d = h.density(or);
193
- const limit = d[x - h.lo] * REL_ERR;
194
- let s = [0, 0];
195
- for (const v of d)
196
- if (v <= limit)
197
- s = ddAdd(s, [v, 0]);
198
- pValue = s[0];
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 === h.lo)
218
+ if (x === lo)
203
219
  estimate = 0;
204
- else if (x === h.hi)
220
+ else if (x === hi)
205
221
  estimate = Infinity;
206
222
  else {
207
- const mu = h.mean(1);
208
- const f = (psi) => h.mean(psi) - x;
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
- // Upper limit: P(X <= x; ψ) = α (P is decreasing in ψ)
222
- const upper = (alpha) => {
223
- if (x === h.hi)
224
- return Infinity;
225
- const p = h.cdf(x, 1, false);
226
- const f = (psi) => alpha - h.cdf(x, psi, false);
227
- return p < alpha ? rootBelowOne(f) : p > alpha ? rootAboveOne(f) : 1;
228
- };
229
- // Lower limit: P(X >= x; ψ) = α (P is increasing in ψ)
230
- const lower = (alpha) => {
231
- if (x === h.lo)
232
- return 0;
233
- const p = h.cdf(x, 1, true);
234
- const f = (psi) => h.cdf(x, psi, true) - alpha;
235
- return p > alpha ? rootBelowOne(f) : p < alpha ? rootAboveOne(f) : 1;
236
- };
237
- result.confInt =
238
- alternative === "less"
239
- ? [0, upper(1 - confLevel)]
240
- : alternative === "greater"
241
- ? [lower(1 - confLevel), Infinity]
242
- : [lower((1 - confLevel) / 2), upper((1 - confLevel) / 2)];
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[][];