rStats.js

Accuracy

English 日本語

Every exported function is tested against R 4.6.2 (erf and erfc, which R lacks, against mpmath). The expected values are generated by R scripts in oracle/ and stored in test/fixtures/ (one JSON line per case), so the test suite runs without R. Test inputs always include typical values, far tails (p = 1e-300), boundaries (0, 1, df → ∞), NaN and Infinity, negative values and huge parameters.

How a case passes

  1. Matches R within the tolerance below, or
  2. matches a high-precision reference (Rmpfr with 256–1300 bits, or exact rationals with gmp) within the same tolerance. R itself is wrong in that case, and the test output counts it as “more accurate than R”, or
  3. for quantile functions, the backward error is no larger than R’s: the distribution function evaluated at rStats.js’s answer is as close to p as it is at R’s answer. A quantile function has a large condition number where the distribution function is flat, so a tiny difference in p becomes a large difference in the answer and a relative comparison would be meaningless there. For discrete distributions, the answer must be the smallest integer that satisfies the definition, which is checked with the distribution function, or
  4. for tests, models and some other functions, matches a replaced expected value where rStats.js deliberately does not reproduce R’s result: an input for which R returns NaN or ±Inf because the result is not defined, where rStats.js throws, or an R error caused by a bug. The test replaces R’s expected value with the error or the right value (substitute in the test file) and prints the reason with the count.

Cases where rStats.js deliberately differs from R are excluded in the test file (knownDifference) with a written reason and listed in differences-from-r.md. The test output reports, for each fixture, how many cases matched R, matched the reference, passed by backward error and were excluded.

The tolerance is a bound, not the aim. Where R is right, rStats.js aims at R’s own bits: a different computation is used only where the result of the usual one is not finite, out of range or has lost its digits, so results in the ordinary range are not changed by a fix for the far range. Where R is wrong, rStats.js returns the right value, only in the range where R is wrong, and the difference is recorded in differences-from-r.md.

Details of the comparison:

Reference values

Tolerances

Area Relative tolerance
Special functions, densities, distribution functions 1e-14 (non-central t: 1e-13; ptukey: 2e-14)
Distributions outside base R (inverse gamma, Kumaraswamy, Pareto, triangular, Laplace, arcsine) 1e-14 for d/p/q, against extraDistr (arcsine: R’s beta(1/2, 1/2) moved to (a, b)) and mpmath
Quantile functions 1e-14, or backward error no larger than R’s; discrete quantiles exactly equal or verified by definition
Random numbers discrete values, sample and r2dtable exactly equal, and with L’Ecuyer-CMRG runif and the state after each case (.Random.seed); continuous values within 1e-12; the number of uniform draws consumed is checked exactly
Descriptive statistics 1e-14 (skewness and kurtosis near 0, which come from cancellation, measured absolutely)
Hypothesis tests 1e-12 for every numeric field (cor: 1e-15, pAdjust: exact; roots found by uniroot with tolerance 1e-4, such as the normal-approximation interval of ansariTest: 1e-4); text fields exactly equal
Linear models (lm, confint, predict) 1e-12 for every numeric field
Mixed models (lmer) against the reference at the exact optimum: logLik, AIC, BIC, deviance, REMLcrit 1e-12; θ, the fixed effects, their covariance and standard errors, σ, VarCorr, ranef, fitted values and residuals 1e-9 of the largest absolute value of each (largest observed: 1.2e-12, a flat optimum just inside the boundary); elements of θ that are 0 in the reference exactly 0; against lme4 only that the criterion is not larger than lme4’s. The profiled deviance at a given θ 1e-12
Linear algebra 1e-14 (crossprod, norm, backsolve, chol, qr, the 2-norm from svd, matrix arithmetic, seq, lu) or 1e-13 (solve, determinant, rcond, svd, kappa, ginv, eigen), measured relative to the norm of the result; prcomp 1e-12; pivots, ranks and signs of determinants exactly equal
Numerical methods 1e-15 (approx), 1e-14 (integrate, optimize, uniroot, barylag, hermiteInterp, ode, and grad, jacobian, hessian with an absolute floor of 1e-12) or 1e-13 (spline, splinefun, splinefunH)

The tolerances were chosen per area from measurement: the largest observed errors against R or the reference are just below them.

Why some tolerances are wider

Random numbers

The continuous values cannot match exactly because the last bits of Math.log, Math.exp, Math.pow and Math.tan differ between V8 and glibc’s libm. The largest observed difference is 1.2e-13, for gamma variates with very small shape (e.g. 0.001), where exp(log(u)/shape) magnifies the error by 1/shape. The number of uniform draws is checked by drawing runif(1) after each case: a single different accept/reject decision would show up there.

The other RNG kinds (rngRounding, rngLecuyer, rngStreams) are checked the same way, and also compare the state afterwards (getRandomSeed() against R’s .Random.seed: the kind code, and all seven integers for L’Ecuyer-CMRG). L’Ecuyer-CMRG’s uniforms are computed with exact integer arithmetic in doubles, so runif and everything discrete match R exactly; continuous variates (rnorm, rexp, rgamma) are compared within 1e-12 as above.

Hypothesis tests

Linear algebra

Known accuracy problems

None are known in the tested ranges: every case matches R or a high-precision reference within the tolerance.

Outside them, besselJ and besselY with orders in the thousands and x < ν have relative errors of up to about 1.5e-14 (for example 1.04e-14 for besselY(3000, 3949), 9.9e-15 for besselJ(1000, 1500) and 1.5e-14 for besselJ(2000, 2999); measured for 0.16.0 against Rmpfr at 25 points with orders 1100 to 9000), from rounding that accumulates over the many steps of the recurrence and the continued fractions.

R’s qnbinom does not terminate for 13 inputs (e.g. qnbinom(-1, 0.5, 1e-10, log.p = TRUE)). oracle/nbinom.R limits each case to 2 seconds and drops those rows from the fixture.

Where the accuracy comes from

Most kernels are ported from SciPy’s xsf (BSD-3) and Boost.Math (BSL-1.0); linear algebra follows the LAPACK reference implementation (modified BSD), LINPACK and EISPACK (public domain). See THIRD_PARTY_NOTICES.md. The main changes made to match or exceed R:

The cost of this in speed, and how to measure what double-double buys, is described in performance.md.