@tangent.to/mc
v0.4.0
Published
JavaScript Markov Chain Monte Carlo - A PyMC-inspired probabilistic programming library for Bayesian inference in JavaScript
Maintainers
Readme
@tangent/mc - JavaScript Markov Chain Monte Carlo
A PyMC-inspired probabilistic programming library for Bayesian inference in JavaScript. Built on TensorFlow.js with automatic differentiation support for efficient MCMC sampling.
Overview
MC brings the power of Bayesian statistical modeling to JavaScript, providing an intuitive API similar to PyMC for defining probabilistic models as Directed Acyclic Graphs (DAGs) and performing inference using Markov Chain Monte Carlo methods.
API conventions
MC follows the same API conventions as its sibling data-science package @tangent.to/ds:
- Namespaced + flat exports. Import individual symbols (
import { Normal } from '@tangent.to/mc'), grouped namespaces (import { distributions, samplers } from '@tangent.to/mc'), or the whole library as a default export (import mc from '@tangent.to/mc'→mc.distributions.Normal). The namespaces aredistributions,samplers,diagnostics,io, andplot. - Options-object constructors. Every configurable class accepts a single options object in addition to positional arguments, e.g.
new Normal({ mean: 0, sd: 1 })ornew MetropolisHastings({ proposalStd: 0.5 }). Positional forms continue to work. - Introspection. Distributions and samplers expose
getParams().
Key Features
- PyMC-like DAG structure: Define models by connecting distributions in a directed acyclic graph
- TensorFlow.js integration: Automatic differentiation for gradient-based samplers
- Multiple MCMC samplers: Metropolis-Hastings and Hamiltonian Monte Carlo
- Rich distribution library: Normal, Uniform, Beta, Gamma, Bernoulli, and more
- Posterior predictions: Generate predictions with uncertainty from MCMC samples
- Model persistence: Save and load traces and model configurations to JSON
- Trace analysis utilities: Summary statistics, effective sample size, convergence diagnostics
- Hierarchical models: Support for multilevel Bayesian models
- Browser compatible: Run in Node.js or in the browser (including ObservableHQ)
Installation
@tangent.to/mc ships a single browser-first build and uses
TensorFlow.js (@tensorflow/tfjs) for tensor math
and automatic differentiation. tfjs is a peer dependency — it is not bundled
into mc, so you load it once and share it (mixing two tfjs copies breaks tensor
interop). See Loading TensorFlow.js below.
Node.js / npm
npm install @tangent.to/mc @tensorflow/tfjsimport { Model, Normal, MetropolisHastings } from '@tangent.to/mc';Deno
import { Model, Normal, MetropolisHastings } from "npm:@tangent.to/mc";Loading TensorFlow.js
How you provide tfjs depends on whether you use a build step:
With a bundler (Vite, webpack, esbuild, …) — install both packages and import as
usual; the bundler resolves @tensorflow/tfjs for you. Nothing else to do.
Without a build step (plain <script type="module">, CDN) — bare imports don't
resolve in the browser, so add an import map
before importing mc:
<script type="importmap">
{
"imports": {
"@tensorflow/tfjs": "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4/+esm"
}
}
</script>
<script type="module">
import { Model, Normal, MetropolisHastings } from 'https://cdn.jsdelivr.net/npm/@tangent.to/mc/+esm';
// ... build and sample your model
</script>mc also re-exports the shared instance, so you can grab tf from mc itself
instead of importing it separately:
import { tf } from '@tangent.to/mc';Observable
jsDelivr's +esm endpoint auto-resolves the tfjs dependency, so a single import works:
mc = import("https://cdn.jsdelivr.net/npm/@tangent.to/mc/+esm")Quick Start
Here's a simple Bayesian linear regression example:
import { Model, Normal, Uniform, MetropolisHastings, printSummary } from '@tangent.to/mc';
// Create model
const model = new Model({ name: 'linear_regression' });
// Define priors (options-object form; positional `new Normal(0, 10, 'alpha')` also works)
const alpha = new Normal({ mean: 0, sd: 10, name: 'alpha' });
const beta = new Normal({ mean: 0, sd: 10, name: 'beta' });
const sigma = new Uniform({ min: 0.01, max: 5, name: 'sigma' });
model.addVariable('alpha', alpha);
model.addVariable('beta', beta);
model.addVariable('sigma', sigma);
// Define likelihood (connecting distributions in a DAG)
model.logProb = function(params) {
let logProb = alpha.logProb(params.alpha)
.add(beta.logProb(params.beta))
.add(sigma.logProb(params.sigma));
// Add likelihood for observations
for (let i = 0; i < x.length; i++) {
const mu = params.alpha + params.beta * x[i];
const likelihood = new Normal(mu, params.sigma);
logProb = logProb.add(likelihood.logProb(y[i]));
}
return logProb;
};
// Run MCMC sampling (options-object form; positional args also work)
const sampler = new MetropolisHastings({ proposalStd: 0.5 });
const trace = sampler.sample(model, initialValues, { nSamples: 1000, burnIn: 500, thin: 1 });
// Analyze results
printSummary(trace);Namespaced / default imports
import mc from '@tangent.to/mc';
const model = new mc.Model({ name: 'linear_regression' });
model.addVariable('alpha', new mc.distributions.Normal({ mean: 0, sd: 10, name: 'alpha' }));
const sampler = new mc.samplers.MetropolisHastings({ proposalStd: 0.5 });
// mc.diagnostics, mc.io, mc.plot are also availableCore Concepts
Models as DAGs
Like PyMC, mc uses a Directed Acyclic Graph (DAG) structure to represent probabilistic models. Variables can depend on other variables, creating a natural flow from priors through transformations to likelihoods:
// Hyperpriors
const mu_global = new Normal(0, 10);
const sigma_global = new Uniform(0, 5);
// Group-level parameters (depend on hyperpriors)
const mu_group = new Normal(mu_global, sigma_global);
// Observations (depend on group parameters)
const y = new Normal(mu_group, sigma_obs);Distributions
mc provides a rich set of probability distributions:
Each constructor accepts positional arguments or an options object (shown second).
Continuous Distributions
- Normal:
new Normal(mu, sigma)/new Normal({ mean, sd })- Gaussian distribution - Uniform:
new Uniform(lower, upper)/new Uniform({ min, max })- Uniform distribution - Beta:
new Beta(alpha, beta)/new Beta({ alpha, beta })- Beta distribution (for probabilities) - Gamma:
new Gamma(alpha, beta)/new Gamma({ shape, rate })- Gamma distribution (for positive values)
Discrete Distributions
- Bernoulli:
new Bernoulli(p)/new Bernoulli({ p })- Binary outcomes
All distributions support:
logProb(value)- Compute log probability density/masspdf(value)- Compute probability density/mass (exp(logProb))sample(shape)- Generate random samplesmean()- Get the distribution meanvariance()- Get the distribution variancegetParams()- Get the distribution's parameters as a plain object
Model Predictions
Generate posterior predictive samples for new data:
// Define prediction function
const predictFn = (params) => {
return params.alpha + params.beta * x_new;
};
// Get posterior predictions with uncertainty
const predictions = model.predictPosteriorSummary(
trace,
predictFn,
credibleInterval=0.95
);
// Returns: { mean: [...], lower: [...], upper: [...] }Model Persistence
File-based persistence is Node-only (node:fs) and is intentionally kept out of
the browser-first main entry. Import it directly from its module in Node:
import {
saveTrace,
loadTrace,
saveModelState,
exportTraceForBrowser
} from '@tangent.to/mc/persistence';
// Save trace to JSON
saveTrace(trace, 'trace.json');
// Load trace
const loadedTrace = loadTrace('trace.json');
// Save complete model state
saveModelState(model, trace, 'model_state.json');
// In the browser, serialize to a JSON string instead (no filesystem)
const jsonString = exportTraceForBrowser(trace);MCMC Samplers
Metropolis-Hastings
A simple but effective random-walk sampler:
const sampler = new MetropolisHastings({ proposalStd });
const trace = sampler.sample(model, initialValues, { nSamples, burnIn, thin });Parameters:
proposalStd: Standard deviation of the Gaussian proposal distributionnSamples: Number of samples to collectburnIn: Number of initial samples to discardthin: Keep every nth sample
Best for: Simple models, initial exploration
Hamiltonian Monte Carlo
A gradient-based sampler that uses automatic differentiation:
const sampler = new HamiltonianMC({ stepSize, nSteps });
const trace = sampler.sample(model, initialValues, { nSamples, burnIn, thin });Parameters:
stepSize: Leapfrog integration step size (epsilon)nSteps: Number of leapfrog steps (L)
Best for: Complex models with many parameters, faster convergence
Trace Analysis
mc provides utilities for analyzing MCMC samples:
import { summarize, effectiveSampleSize, gelmanRubin, printSummary } from '@tangent.to/mc';
// Print comprehensive summary
printSummary(trace);
// Get statistics for a variable
const stats = summarize(trace.trace.alpha);
// Returns: { mean, median, std, variance, hdi_2_5, hdi_97_5, n }
// Compute effective sample size
const ess = effectiveSampleSize(trace.trace.alpha);
// Check convergence with multiple chains
const rHat = gelmanRubin([chain1.alpha, chain2.alpha, chain3.alpha]);Examples
The examples/ directory contains complete working examples:
Linear Regression
node examples/linear_regression.jsDemonstrates basic Bayesian linear regression with normal priors.
Logistic Regression
node examples/logistic_regression.jsBinary classification with a logistic link function.
Hierarchical Model
node examples/hierarchical_model.jsMultilevel model with partial pooling across groups, showcasing complex DAG structures.
API Reference
Model Class
const model = new Model(name)Methods:
addVariable(name, distribution, observed)- Add a variable to the modelgetVariable(name)- Retrieve a variablelogProb(params)- Compute log probabilitylogProbAndGradient(params)- Compute log prob and gradientssamplePrior(nSamples)- Sample from prior distributionsgetFreeVariableNames()- Get unobserved variable namessummary()- Print model structure
Distribution Classes
All distributions inherit from the base Distribution class:
class Distribution {
logProb(value) // Log probability
pdf(value) // Probability density/mass (exp of logProb)
sample(shape) // Generate samples
observe(data) // Set observed data
mean() // Distribution mean
variance() // Distribution variance
getParams() // Parameters as a plain object
}Sampler Classes
Constructors and sample() accept either positional arguments or a single
options object.
class MetropolisHastings {
constructor(proposalStd) // or ({ proposalStd })
sample(model, initialValues, nSamples, burnIn, thin) // or (model, init, { nSamples, burnIn, thin })
tuneProposal(acceptanceRate)
getParams()
}
class HamiltonianMC {
constructor(stepSize, nSteps) // or ({ stepSize, nSteps })
sample(model, initialValues, nSamples, burnIn, thin)
getParams()
}
class NUTS {
constructor(stepSize, maxTreeDepth, targetAcceptance) // or ({ stepSize, maxTreeDepth, targetAcceptance })
sample(model, initialValues, nSamples, nWarmup, thin)
getParams()
}Browser & ObservableHQ
mc is a single browser-first build that runs the same in the browser, Node, and
ObservableHQ — see Installation for loading tfjs. In Observable:
mc = import("https://cdn.jsdelivr.net/npm/@tangent.to/mc/+esm")
{
const { Model, Normal, MetropolisHastings } = mc;
// ... define and run your model
}Notes for browser/Observable use:
tfjsruns on its CPU/WebGL backend (there is no@tensorflow/tfjs-node— the single build uses@tensorflow/tfjseverywhere), which enables interactive visualization.- File-based persistence (
saveTrace,loadTrace) is Node-only (node:fs) and is not part of the browser entry. Import it from the@tangent.to/mc/persistencesubpath in Node if needed; in the browser, serialize withtraceToJSON(trace).
Technical Details
Built on TensorFlow.js
mc leverages TensorFlow.js for:
- Automatic differentiation: Essential for gradient-based samplers like HMC/NUTS
- Efficient tensor operations: Fast computation of log probabilities
- WebGL acceleration: GPU-backed tensor math in the browser via the WebGL backend
Comparison with PyMC
| Feature | PyMC | mc | |---------|------|------| | Language | Python | JavaScript | | Backend | Aesara/JAX | TensorFlow.js | | DAG Structure | Yes | Yes | | MCMC Samplers | NUTS, HMC, MH | HMC, MH | | Variational Inference | Yes | Planned | | GPU Support | Yes | Browser only (TF.js WebGL) |
Performance Tips
Tune sampler parameters:
- MH: Aim for 20-40% acceptance rate by adjusting
proposalStd - HMC: Start with small
stepSize(~0.01) and moderatenSteps(~10)
- MH: Aim for 20-40% acceptance rate by adjusting
Use appropriate burn-in: Discard at least 500-1000 initial samples
Check convergence:
- Visual inspection of trace plots
- R-hat < 1.1 for multiple chains
- Effective sample size > 100 per chain
Hierarchical models: Use HMC for faster convergence with many parameters
Development
# Clone repository
git clone https://github.com/tangent-to/mc.git
cd mc
# Install dependencies
npm install
# Run examples
npm run example
# Run tests
npm testContributing
Contributions are welcome! Please feel free to submit issues and pull requests.
License
Apache-2.0
Roadmap
Completed in v0.2.0:
- [x] Posterior predictive sampling
- [x] Model persistence (save/load)
- [x] Browser/Observable support
Planned:
- [ ] Additional distributions (Poisson, Student-t, Exponential)
- [ ] Variational inference (ADVI)
- [ ] Model comparison utilities (WAIC, LOO)
- [ ] Trace visualization tools
- [ ] PyMC model import/export
Documentation
- Observable Guide - Using mc in ObservableHQ notebooks
- Considerations - Best practices, limitations, and design decisions
- Examples - Complete working examples
References
Citation
If you use mc in your research, please cite:
@software{mc,
title = {mc: JavaScript Markov Chain Monte Carlo},
author = {},
year = {2025},
url = {https://github.com/tangent-to/mc}
}