rStats.js

English 日本語

R-compatible statistical functions for JavaScript and the browser, written in TypeScript. Try it in the browser (every function, with the values R gives).

Status: 1.3.0. The goal is to cover the numerical core of R’s stats package (distributions, descriptive statistics, hypothesis tests, linear models, special functions, numerical methods and the basics of linear algebra) with R-compatible results; see ROADMAP.md for what remains. See CHANGELOG.md.

Install

npm install @numero-uno-jpn/rstats

Requires an ES2022 environment (current browsers, Node.js 22 or later, Deno, Bun).

Examples

Distributions

import { pnorm, qnorm, pt, qbeta, dbinom } from "@numero-uno-jpn/rstats";

pnorm(1.96);                                  // ≈ 0.9750021049
qnorm([0.025, 0.975]);                        // ≈ [-1.959963985, 1.959963985]
pt(-40, 3);                                   // ≈ 1.719034039e-5
pt(2, 10, { lowerTail: false, logP: true });  // ≈ -3.305141551 (log of the upper tail)
qbeta(1e-10, 2, 3);                           // ≈ 4.082494016e-6
dbinom(3, 10, 0.2);                           // ≈ 0.201326592

The first argument may be a number, an array or a Float64Array; the result has the same shape. Distribution parameters are scalars.

Random numbers that match set.seed()

import { setSeed, rnorm, RNGkind, getRandomSeed, setRandomSeed, nextRNGStream, runif } from "@numero-uno-jpn/rstats";

setSeed(42);
rnorm(3);  // [1.3709584471466685, -0.5646981713960887, 0.3631284113373392]
           // R: set.seed(42); rnorm(3) gives the same numbers (up to the last bit)

// R's other settings: RNGkind("L'Ecuyer-CMRG") with parallel::nextRNGStream, sample.kind = "Rounding"
RNGkind({ kind: "L'Ecuyer-CMRG" });
setSeed(1);                                   // R: set.seed(1, kind = "L'Ecuyer-CMRG")
setRandomSeed(nextRNGStream(getRandomSeed())); // R: .Random.seed <- nextRNGStream(.Random.seed)
runif(2);  // [0.31369782407981056, 0.9280126525616814]

Hypothesis tests

import { tTest, fisherTest, pAdjust } from "@numero-uno-jpn/rstats";

const r = tTest([5.1, 4.9, 6.2, 5.8, 6.0, 5.5], [4.1, 4.5, 5.0, 4.8, 4.3]);
r.pValue;    // ≈ 0.003536530888 (Welch, the default in R)
r.confInt;   // ≈ [0.4423253335, 1.644341333]
r.method;    // "Welch Two Sample t-test"

fisherTest([[3, 1], [1, 3]]).pValue;                     // ≈ 0.4857142857
pAdjust([0.01, 0.02, 0.03, 0.04], { method: "BH" });    // ≈ [0.04, 0.04, 0.04, 0.04]

Generalized linear models

The predictors are a matrix (array of rows) or a vector, as for lm; a binomial response can be rows of (successes, failures).

import { glm, glmSummary, glmPredict, glmConfint, glmAnova } from "@numero-uno-jpn/rstats";

const dose = [1, 2, 3, 4, 5, 6];
const counts = [[1, 19], [4, 16], [9, 11], [13, 7], [18, 2], [19, 1]];
const fit = glm(dose, counts, { family: "binomial" });   // R: glm(cbind(s, f) ~ dose, binomial)
fit.coefficients;                                  // ≈ [-3.77413, 1.14807]
glmSummary(fit).pValue;                            // ≈ [5.357e-8, 3.579e-9] (z tests)
glmPredict(fit, { newdata: [3.5], type: "response" }); // { fit: ≈ [0.56073], seFit: ≈ [0.06140] }
glmConfint(fit).lower;                             // ≈ [-5.27166, 0.80033] (profile likelihood)
glmAnova(fit).table[1];                            // ≈ [1, 62.978, 4, 0.5196, 2.09e-15]
fit.warnings;                                      // [] (R's warnings, such as non-convergence)

Robust covariances (sandwich’s vcovHC and vcovCL) go into lmtest’s coeftest and coefci. A modified Poisson regression (a Poisson glm of a 0/1 outcome) gives a risk ratio with a robust interval:

import { glm, vcovHC, coefci } from "@numero-uno-jpn/rstats";

const exposed = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const event   = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0];
const fit = glm(exposed, event, { family: "poisson" });   // R: glm(event ~ exposed, family = poisson)
fit.coefficients.map(Math.exp);                         // ≈ [0.3, 2] (risk 0.3 unexposed; risk ratio 2)
const ci = coefci(fit, { vcov: vcovHC(fit, { type: "HC0" }) });
// R: lmtest::coefci(fit, vcov. = sandwich::vcovHC(fit, type = "HC0"))
ci.lower.map(Math.exp);                                 // ≈ [0.11640, 0.68361]
ci.upper.map(Math.exp);                                 // ≈ [0.77320, 5.8513]

Analysis of variance with formulas

lm and aov also take an R formula and a data frame (an object of columns; strings are factors). anova gives R’s sequential table, Anova car’s type II and III tests, aovEz afex’s repeated-measures ANOVA with the Greenhouse-Geisser correction and generalized eta squared.

import { lm, anova, Anova, aovEz } from "@numero-uno-jpn/rstats";

const d = { y: [12.1, 14.3, 13.8, 17.2, 19.5, 22.4, 11.6, 13.9, 15.7, 18.8, 21.1, 25.3],
            A: ["a", "a", "b", "b", "c", "c", "a", "a", "b", "b", "c", "c"],
            B: ["p", "q", "p", "q", "p", "q", "p", "q", "p", "q", "p", "q"] };
const fit = lm("y ~ A * B", d, { contrasts: { A: "contr.sum", B: "contr.sum" } });
anova(fit).rowNames;                   // ["A", "B", "A:B", "Residuals"]
anova(fit).table[0];                   // ≈ [2, 169.147, 84.573, 57.828, 0.00011997] (Df, Sum Sq, Mean Sq, F, p)
Anova(fit, { type: 3 }).table[1];      // ≈ [169.147, 2, 57.828, 0.00011997] (car::Anova(fit, type = 3))

// repeated measures (R: afex::aov_ez("id", "y", long, within = "W"))
const long = { id: ["s1", "s1", "s1", "s2", "s2", "s2", "s3", "s3", "s3", "s4", "s4", "s4", "s5", "s5", "s5"],
               W: ["pre", "mid", "post", "pre", "mid", "post", "pre", "mid", "post", "pre", "mid", "post", "pre", "mid", "post"],
               y: [5.1, 6.3, 7.2, 4.8, 5.9, 7.5, 6.0, 6.1, 8.3, 5.5, 6.8, 7.1, 4.9, 6.6, 7.8] };
aovEz(long, { id: "id", dv: "y", within: "W" }).anova.table[0];
// ≈ [1.735, 6.940, 0.21653, 35.876, 0.84607, 0.00026363] (num Df, den Df, MSE, F, ges, p; GG-corrected)

Mixed models

lmer fits lme4’s linear mixed models by REML or maximum likelihood. It is in its own area, @numero-uno-jpn/rstats/mixed (dist/mixed.js on a CDN), not in the single-file bundle.

import { lmer, lmerSummary, lmerAnova } from "@numero-uno-jpn/rstats/mixed";

const d = { subj: ["s1", "s2", "s3", "s4", "s5", "s6"].flatMap((s) => [s, s, s, s]),
            day: [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3],
            rt: [251, 262, 280, 293, 230, 251, 265, 282, 270, 275, 290, 318,
                 242, 263, 270, 296, 260, 284, 309, 330, 238, 245, 262, 268] };
const fit = lmer("rt ~ day + (day | subj)", d);   // R: lme4::lmer(rt ~ day + (day | subj), d)
fit.fixef;                       // [247.65, 16.4]
fit.coefTable.stdError;          // ≈ [5.3177, 1.7104]
fit.varCorr[0].sd;               // ≈ [12.485, 3.6894] (subj: intercept and slope; VarCorr)
fit.varCorr[0].corr[0][1];       // ≈ 0.6737
fit.sigma;                       // ≈ 4.4385
fit.ranef[0].values[1];          // ≈ [-13.118, -1.0038] (ranef for s2)
fit.REMLcrit;                    // ≈ 158.563

// lmerTest's tests, with Satterthwaite's degrees of freedom
const s = lmerSummary(fit);      // R: summary(lmerTest::lmer(rt ~ day + (day | subj), d))
s.df;                            // ≈ [5, 5]
s.pValue;                        // ≈ [8.6215e-8, 0.00020903]
lmerAnova(fit).F;                // ≈ [91.942] (Type III; R: anova(fit))

The estimates are the exact optimum of the REML criterion; lme4’s optimiser stops close to it, so the variance components agree with lme4 to about 5 to 9 significant digits and the fixed effects and log-likelihood to more. lmerSummary, lmerContest and lmerAnova give lmerTest’s t- and F-tests with Satterthwaite’s degrees of freedom (computed with exact derivatives at that optimum; lmerTest differentiates numerically at lme4’s) and the Type I, II and III ANOVA tables; lmerAnova([fit1, fit2]) compares fits by likelihood-ratio tests, as lme4’s anova().

glmer fits lme4’s generalized linear mixed models (binomial, poisson, Gamma, inverse.gaussian and gaussian families with their links) by the Laplace approximation, or by adaptive Gauss-Hermite quadrature (nAGQ 2 to 100) for a single scalar random-effects term: glmer("y ~ x + (1 | g)", d, { family: "poisson" }). glmerSummary gives the coefficient table with p-values, glmerAnova the ANOVA table or the likelihood-ratio comparison of glmer, lmer and glm fits, and glmerPredict and lmerPredict the predictions (at new data, with some or none of the random effects, with standard errors). As for lmer, the estimates are the exact optimum; see the differences for the standard errors and nAGQ > 1.

Effect sizes and power

import { cohensD, etaSquared, Anova, powerTTest, pwrAnovaTest } from "@numero-uno-jpn/rstats";

cohensD([5.1, 4.9, 6.2, 5.8, 6.0, 5.5], [4.1, 4.5, 5.0, 4.8, 4.3]);
// ≈ { name: "Cohens_d", value: 2.30726, ci: 0.95, ciLow: 0.68824, ciHigh: 3.85666 } (effectsize::cohens_d)
etaSquared(Anova(fit, { type: 3 })).table[0];   // ≈ [0.95068, 0.95, 0.82023, 1] (fit from the example above; partial eta², one-sided CI)
powerTTest({ delta: 0.5, power: 0.8 }).n;       // ≈ 63.766 per group (power.t.test)
pwrAnovaTest({ k: 3, f: 0.25, power: 0.8 }).n;  // ≈ 52.397 (pwr::pwr.anova.test)

The confidence limits are the exact solutions; effectsize finds them with an optimiser and is up to about 1e-9 (sometimes 3e-3) off.

Linear algebra

Matrices are arrays of rows.

import { solve, qr, qrCoef, svd, eigen } from "@numero-uno-jpn/rstats";

solve([[2, 1], [1, 3]]);            // [[0.6, -0.2], [-0.2, 0.4]]
const q = qr([[1, 1], [1, 2], [1, 3]]);
qrCoef(q, [1, 3, 4]);               // ≈ [-0.333, 1.5] (least squares)
svd([[1, 4], [2, 5], [3, 6]]).d;    // ≈ [9.508032001, 0.7728696357]
eigen([[2, 1], [1, 3]]).values;     // ≈ [3.618033989, 1.381966011]

What is included

Area Functions (R names in parentheses where they differ)
Special functions gamma, lgamma, beta, lbeta, choose, lchoose, factorial, lfactorial, digamma, trigamma, psigamma, besselI, besselK, besselJ, besselY; also erf, erfc, erfinv, erfcinv, gammainc and perm, which R lacks, and log1pmx, lgamma1p, log1mexp, log1pexp from R’s C API (Rmath.h)
Distributions (d/p/q) normal, t, chi-squared, F, beta, gamma, binomial, Poisson, uniform, exponential, log-normal, logistic, Cauchy, Weibull, geometric, negative binomial, hypergeometric, Wilcoxon signed rank and rank sum, studentized range (ptukey, qtukey), non-central chi-squared, t, F and beta (ncp); the multinomial probability (dmultinom); inverse gamma, Kumaraswamy, Pareto, triangular, Laplace (extraDistr) and arcsine; distSummary (mean, variance, sd, median, mode, skewness, kurtosis)
Random numbers setSeed (set.seed), RNGkind (Mersenne-Twister and L’Ecuyer-CMRG, sample.kind Rejection and Rounding), getRandomSeed/setRandomSeed (.Random.seed), nextRNGStream/nextRNGSubStream (parallel), 26 r* functions, sample, sampleInt (sample.int), r2dtable
Descriptive statistics sum, prod, mean, weightedMean (weighted.mean), variance (var), sd, median, range, cumsum, cumprod, cummax, cummin, diff, quantile, IQR, fivenum, mad, rank, cov, cor, tabulate, table, unique, cut, findInterval, pretty, hist, ecdf, scale, skewness, kurtosis, geometricMean, modes, meanAD, cv
Hypothesis tests tTest, chisqTest, fisherTest, wilcoxTest, corTest, binomTest, poissonTest, propTest, mcnemarTest, varTest, bartlettTest, kruskalTest, friedmanTest, quadeTest, flignerTest, moodTest, onewayTest, shapiroTest, mantelhaenTest, ansariTest, ksTest, pairwiseTTest, pairwiseWilcoxTest, pairwisePropTest, aov (one way, a fit whose table is aovSummary), TukeyHSD, zTest, brunnerMunzelTest, pAdjust (p.adjust)
Analysis of variance aov and aovSummary with a formula (several factors, interactions, covariates, Error() strata), anova (anova.lm, sequential and model comparison), Anova (car: types II and III), aovEz (afex: repeated-measures and mixed designs, Greenhouse-Geisser and Huynh-Feldt corrections, generalized and partial eta squared), mauchlyTest (mauchly.test)
Effect sizes and power cohensD, hedgesG (effectsize, with exact confidence limits), etaSquared, omegaSquared, epsilonSquared (partial, generalized, from anova and Anova tables); powerTTest, powerPropTest, powerAnovaTest (power.*.test); pwrTTest, pwrT2nTest, pwr2pTest, pwr2p2nTest, pwrPTest, pwrNormTest, pwrRTest, pwrChisqTest, pwrAnovaTest, pwrF2Test, esH, esW1, esW2, cohenES (pwr)
Linear models lm (a formula and a data frame, or a matrix, with the summary quantities), modelMatrix (model.matrix), confint, predict; glm (matrix interface, every family and link of R’s stats) with glmSummary (summary.glm), glmResiduals (the four kinds), glmHatvalues, glmRstandard, glmCooksDistance, glmPredict (with standard errors), glmConfint (profile likelihood, exact limits), glmAnova (anova.glm); vcovHC, vcovCL (sandwich: HC0 to HC5, one-way and multi-way clustered), coeftest, coefci (lmtest)
Mixed models lmer (lme4: REML and ML, correlated and uncorrelated random effects, crossed and nested grouping factors; fixef, vcov, VarCorr, ranef, logLik, AIC, BIC, REMLcrit, isSingular), lmerSummary, lmerContest and lmerAnova (lmerTest: Satterthwaite’s t- and F-tests, Type I/II/III ANOVA tables; likelihood-ratio comparison of fits), glmer (lme4: all glm families, Laplace approximation and adaptive Gauss-Hermite quadrature), glmerSummary, glmerAnova, glmerPredict and lmerPredict, from @numero-uno-jpn/rstats/mixed
More tests leveneTest, propTrendTest (prop.trend.test), jonckheereTest, cochranQTest, adTest, lillieTest, cvmTest, sfTest, pearsonTest (car, DescTools, clinfun, nortest); tTOST, tsumTOST (TOSTER: equivalence and minimal-effect t tests)
Contingency tables and proportions binomCI, binomDiffCI, binomRatioCI (DescTools’ BinomCI, BinomDiffCI, BinomRatioCI), oddsRatio (epitools, and epiR’s score interval), fisherTest(x, { midp: true }) (exact2x2), boschlooTest (Exact), gTest, stuartMaxwellTest, goodmanKruskalGamma (DescTools), lblTest (coin), chisqTest(x, { nMinus1: true }) (Campbell’s ‘N − 1’ test) and Freeman-Tukey residuals, tost (equivalence), binomDiffEquivalenceTest (score TOST of two proportions)
Multiple comparisons dscfAllPairsTest (Steel-Dwass), kwAllPairsDunnTest, kwManyOneDunnTest, manyOneUTest (Steel’s many-one rank test), dunnettTest, gamesHowellTest, tukeyTest (PMCMRplus), dunnTest (dunn.test), glhtDunnett (multcomp)
Survival analysis survdiff (log-rank), survfit (Kaplan-Meier with intervals and median), coxph (Cox regression: Efron and Breslow ties, case weights, strata, the Wald, likelihood-ratio and score tests, the robust variance and cluster)
Diagnostic accuracy and method comparison roc, auc, ciAuc (ci.auc), rocTest (roc.test), coords (pROC: DeLong’s interval and test, the bootstrap, partial AUC), diagnosticTests (epiR’s epi.tests), blandAltman (BlandAltmanLeh’s bland.altman.stats), mcreg (mcr: Deming, weighted Deming and Passing-Bablok regression)
Agreement and reliability kappa2, kappamFleiss, icc (irr), ICC (psych: the six forms of Shrout and Fleiss), cronbachAlpha, partialR (psych), polychor, polyserial (polycor), pbvnorm, factanal, omega (McDonald’s ω, psych)
Data handling completeCases (complete.cases), naOmit (na.omit), subset, merge, rbind and cbind of data frames, tapply, split, aggregate, pivotLonger and pivotWider (tidyr), fromRows and toRows (a data frame from an array of row objects and back); formula forms of the tests (tTest("y ~ g", data), kruskalTest, friedmanTest("y ~ w \| b", data), corTest("~ x + y", data), …)
Linear algebra matMul (%*%), add, subtract, multiply, divide, outer, t, diag, cbind, rbind, matrix, kronecker, seq, crossprod, tcrossprod, norm, backsolve, forwardsolve, chol, chol2inv, qr with qrQ, qrR, qrX, qrCoef, qrQy, qrQty, qrResid, qrFitted, qrSolve (qr.Q …), solve, det, determinant, rcond, kappa, svd, eigen, lu, prcomp, ginv (MASS), schur, expm
Numerical methods integrate, optimize, optim, optimHess, uniroot, approx, spline, splinefun, splinefunH, barylag, hermiteInterp, ode, grad, jacobian, hessian

New to R’s way of writing an analysis (data frames, formulas, factors)? docs/guide-for-js-developers.md explains it from a JavaScript developer’s point of view, with a worked example.

Coming from jStat? docs/migrating-from-jstat.md lists the replacement for every jStat function.

The API reference documents every function, with an example for each (the examples are run by the tests). docs/r-functions.md lists the R function that each one corresponds to. (npm run docs builds the reference locally.)

For AI coding assistants, llms.txt summarises how R code translates to rStats.js calls (also in the npm package and at https://numero-uno-jpn.github.io/rStats.js/llms.txt).

Accuracy

Expected values are produced by R scripts in oracle/ and stored in test/fixtures/, so the test suite runs without R. A case passes if it matches R, or if R is wrong there and rStats.js matches a high-precision value computed with Rmpfr or gmp. In the far tails and for large parameters rStats.js is often more accurate than R, because it computes exponents in double-double arithmetic. Details: docs/accuracy.md.

Differences from R

Most differences are cases where R loses accuracy or fails. The deliberate ones that affect everyday use:

Full list: docs/differences-from-r.md.

Using it in the browser

With a bundler (Vite, esbuild, webpack, Rollup), import named functions; unused functions are removed. Approximate sizes after minification and gzip:

Import gzip
pnorm 1.6 KB
setSeed, rnorm 2.0 KB
solve / svd / eigen 2.4 / 2.4 / 5.1 KB
wilcoxTest / fisherTest / tTest 18.9 / 27.9 / 31.0 KB
qbeta / pt and qt 43.9 / 40.4 KB
glm and glmSummary / glm with all its methods 35.0 / 51.4 KB
lmer (from /mixed) / with lmerSummary, lmerContest, lmerAnova 19.4 / 70.0 KB
glmer (from /mixed) / with glmerSummary, glmerAnova, glmerPredict 47.2 / 66.1 KB
everything (except /mixed) 289.5 KB

(npm run size measures these.)

Each area is also a subpath export, for importing one area without the rest: @numero-uno-jpn/rstats/distributions, /tests (hypothesis tests, lm, confint and predict, and glm with its methods), /linalg, /descriptive, /numerics, /special and /mixed (lmer, glmer and their methods; this area is only available this way, not from the main entry point).

Without a bundler, load the single-file build dist/rstats.js (one minified ES module with every function except those of /mixed, 290 KB gzipped) from jsDelivr, which serves each npm release:

<script type="module">
  import { pnorm } from "https://cdn.jsdelivr.net/npm/@numero-uno-jpn/rstats@1.3.0/dist/rstats.js";
  console.log(pnorm(1.96)); // ≈ 0.9750021049
</script>

The functions are named exports with R’s names, without a prefix such as jStat. or _.. To keep them apart from other names, import the module as a namespace (import * as rs from the URL above) and call rs.pnorm(1.96), rs.tTest(x, y). With a bundler, unused functions are still removed (import * as rs from "@numero-uno-jpn/rstats"); from a CDN the whole file is loaded either way. Nothing is added to window: to use the functions from other scripts or the browser console, assign window.rs = rs yourself.

A page that needs only some areas can load the per-area bundles instead: dist/distributions.js, dist/tests.js, dist/linalg.js, dist/descriptive.js, dist/numerics.js, dist/special.js and dist/mixed.js export the same functions as the subpaths above, and share their common code through dist/chunks/, so loading two areas does not load that code twice. With its chunks, one area transfers 39 KB (special) to 250 KB (tests) gzipped.

Use either dist/rstats.js or the per-area bundles on one page, not both: they are separate sets of modules, so a page that mixes them loads the common code twice and has two random number states (setSeed from one does not seed the generators of the other). The same holds for dist/rstats.js with dist/mixed.js, which brings its own copy of the common code.

Other npm-based CDNs work as well, for example https://esm.sh/@numero-uno-jpn/rstats. Release tags also contain the build output (dist/), which is not on the main branch, but jsDelivr’s GitHub route (cdn.jsdelivr.net/gh/...) does not serve tags from 0.12.0 on: with the test fixtures, the repository exceeds its size limit of 50 MB. Use the npm route above.

The browser build is tested in headless Chromium, both bundled and as dist/rstats.js loaded as an ES module (npm run test:browser).

Demo: https://numero-uno-jpn.github.io/rStats.js/demo/. demo/ is a static page that loads dist/rstats.js from jsDelivr and exercises every function: distributions with plots, special functions, random numbers, hypothesis tests and linear algebra. Each form shows the equivalent R and JavaScript code for its inputs, and a “Compare with R” tab checks curated examples against values printed by R (source("examples.R") runs them all in R). To use your own build instead of the CDN, run npm run build, serve the repository root with a static server (for example npx serve .) and open /demo/?src=local.

Versioning

While the version is 0.x, a minor release may contain breaking changes; they are listed in the changelog.

From 1.0.0, rStats.js follows Semantic Versioning. The public API is what the package root and its subpaths export, with the documented names, arguments, options, result fields and their types.

Contributing

See CONTRIBUTING.md for how the comparison with R works and the licensing rules (no code from R or GPL packages). Notes on speed and the double-double arithmetic are in docs/performance.md.

License

MIT. Numerical kernels are ported from SciPy’s xsf, scipy.stats and C translation of QUADPACK (BSD-3), Boost.Math (BSL-1.0), statsmodels (BSD-3) and the LAPACK reference implementation (modified BSD), plus public-domain LINPACK, EISPACK, Cephes, QUADPACK and netlib code. See THIRD_PARTY_NOTICES.md.