Parameter estimation
What is parameter estimation?
Parameter estimation is the process of finding the distribution parameters that best explain
an observed dataset. ranjs uses maximum likelihood estimation (MLE): it finds the
parameter values that maximise the probability of seeing the data under the assumed model.
This is done by a derivative-free Nelder-Mead simplex optimizer, which makes it robust to
distributions with non-smooth or discontinuous likelihoods.
Once parameters are estimated you can inspect them, compare models by AIC/BIC, or run a
built-in goodness-of-fit test — all with the same distribution instance returned by
fit().
The full workflow
fit() closes the full statistical cycle: define a model, generate (or collect) data,
estimate parameters from that data, then test whether the fit is adequate.
import { dist } from 'ranjs'
// 1. Define and sample
const model = new dist.Normal(3, 1).seed(42)
const data = model.sample(500)
// 2. Fit parameters from data via MLE
const fitted = dist.Normal.fit(data)
console.log(fitted.p) // { mu: 3.000, sigma: 1.000 }
// 3. Test goodness of fit
console.log(fitted.test(data)) // { statistics: 0.42, passed: true }
The same pattern works for any distribution in the library. Here is a discrete example
fitting a Poisson model to count data:
import { dist } from 'ranjs'
const data = new dist.Poisson(4.2).seed(42).sample(200)
const fitted = dist.Poisson.fit(data)
console.log(fitted.p) // { lambda: 4.225 }
console.log(fitted.test(data)) // { statistics: 2.471, passed: true }
Static method
Note: fit() is a static method called on the class, not on an instance.
Use dist.Normal.fit(data), not model.fit(data).
| Correct | Wrong |
|---|
dist.Normal.fit(data) | model.fit(data) // TypeError |
All distributions
All 140 exported distributions support fit(). Most have a data-aware
static _fitInit(data) override that seeds the Nelder-Mead optimizer from a
method-of-moments estimate, improving convergence reliability. The base-class fallback
uses random positive-value retries when no override is present.
Zero-parameter distributions (e.g. Gilbrat, HalfLogistic) skip
optimisation entirely: Cls.fit(data) simply returns new Cls().
Goodness of fit
The instance returned by fit() is a fully-featured distribution object. You can
chain any distribution method on it:
const fitted = dist.Gamma.fit(data)
fitted.test(data) // Anderson-Darling goodness-of-fit test
fitted.aic(data) // Akaike information criterion
fitted.bic(data) // Bayesian information criterion
fitted.pdf(x) // density at any point
fitted.sample(100) // generate new variates from the fitted model
See the API reference for the full list of methods available on
every distribution instance.
Porting from scipy.stats
This guide maps scipy.stats APIs to their ranjs equivalents for the 20 most-used
distributions. Each section shows the constructor parameters side-by-side and a
brief usage example in both Python and JavaScript.
Method mapping
All ranjs distribution instances share the same methods regardless of distribution type.
| scipy.stats | ranjs | Notes |
|---|
dist.rvs(size=n) | dist.sample(n) | returns Array of length n |
dist.rvs() | dist.sample() | returns a single number |
dist.pdf(x) | dist.pdf(x) | same name; also covers discrete pmf |
dist.pmf(k) | dist.pdf(k) | ranjs uses pdf() for both continuous and discrete |
dist.cdf(x) | dist.cdf(x) | same name |
dist.ppf(q) | dist.q(q) | quantile / percent-point function |
dist.sf(x) | dist.survival(x) | survival function = 1 − CDF |
dist.logpdf(x) | dist.lnPdf(x) | log-density |
scipy.stats.kstest(data, dist) | new Dist(...).test(data) | built-in KS goodness-of-fit test |
Distributions
Normal
| scipy | ranjs | Parameter mapping |
|---|
norm(loc=0, scale=1) | new Normal(mu=0, sigma=1) | loc → mu, scale → sigma |
from scipy.stats import norm
d = norm(loc=2, scale=0.5)
d.pdf(2.0) # 0.7979
d.rvs(size=5)
const d = new ranjs.dist.Normal(2, 0.5)
d.pdf(2.0) // 0.7979
d.sample(5)
| scipy | ranjs | Parameter mapping |
|---|
uniform(loc=0, scale=1) | new Uniform(xmin=0, xmax=1) | loc → xmin; xmax = loc + scale |
⚠ Parameter difference: scipy uses start + width (loc, scale); ranjs uses start + end (xmin, xmax).
Convert: xmin = loc, xmax = loc + scale.
from scipy.stats import uniform
d = uniform(loc=1, scale=3) # support [1, 4]
d.pdf(2.5)
d.rvs(size=5)
const d = new ranjs.dist.Uniform(1, 4) // support [1, 4]
d.pdf(2.5)
d.sample(5)
Exponential
| scipy | ranjs | Parameter mapping |
|---|
expon(loc=0, scale=1) | new Exponential(lambda=1) | scale = 1/lambda (scipy uses mean; ranjs uses rate) |
⚠ Parameter difference: scipy's scale is the mean (1/λ); ranjs lambda is the rate (λ).
Convert: lambda = 1 / scale.
from scipy.stats import expon
d = expon(scale=0.5) # rate = 2
d.pdf(1.0)
d.rvs(size=5)
const d = new ranjs.dist.Exponential(2) // rate = 2
d.pdf(1.0)
d.sample(5)
Gamma
| scipy | ranjs | Parameter mapping |
|---|
gamma(a, loc=0, scale=1) | new Gamma(alpha, beta=1) | a → alpha; beta = 1/scale (rate parameterization) |
⚠ Parameter difference: scipy uses a scale parameter (mean/shape); ranjs uses a rate parameter (beta = 1/scale).
Convert: beta = 1 / scale.
from scipy.stats import gamma
d = gamma(a=2, scale=0.5) # rate = 2
d.pdf(1.0)
d.rvs(size=5)
const d = new ranjs.dist.Gamma(2, 2) // alpha=2, beta=2 (rate)
d.pdf(1.0)
d.sample(5)
Beta
| scipy | ranjs | Parameter mapping |
|---|
beta(a, b) | new Beta(alpha, beta) | a → alpha, b → beta (rename only) |
from scipy.stats import beta
d = beta(a=2, b=5)
d.pdf(0.3)
d.rvs(size=5)
const d = new ranjs.dist.Beta(2, 5)
d.pdf(0.3)
d.sample(5)
Chi-squared
| scipy | ranjs | Parameter mapping |
|---|
chi2(df) | new Chi2(k) | df → k (rename only) |
from scipy.stats import chi2
d = chi2(df=4)
d.pdf(2.0)
d.rvs(size=5)
const d = new ranjs.dist.Chi2(4)
d.pdf(2.0)
d.sample(5)
Student's t
| scipy | ranjs | Parameter mapping |
|---|
t(df) | new StudentT(nu) | df → nu (rename only) |
from scipy.stats import t
d = t(df=10)
d.pdf(0.0)
d.rvs(size=5)
const d = new ranjs.dist.StudentT(10)
d.pdf(0.0)
d.sample(5)
F
| scipy | ranjs | Parameter mapping |
|---|
f(dfn, dfd) | new F(d1, d2) | dfn → d1, dfd → d2 (rename only) |
from scipy.stats import f
d = f(dfn=5, dfd=10)
d.pdf(1.0)
d.rvs(size=5)
const d = new ranjs.dist.F(5, 10)
d.pdf(1.0)
d.sample(5)
Log-normal
| scipy | ranjs | Parameter mapping |
|---|
lognorm(s, loc=0, scale=1) | new LogNormal(mu=0, sigma=1) | s → sigma; mu = log(scale) |
⚠ Non-trivial conversion: scipy parameterizes lognorm with s (shape = σ of ln X) and scale (= exp(μ of ln X)).
ranjs uses log-space parameters directly: mu = mean of ln X, sigma = std dev of ln X.
Convert: mu = Math.log(scale), sigma = s.
from scipy.stats import lognorm
import numpy as np
# X ~ LogNormal with log-mean=1, log-sd=0.5
d = lognorm(s=0.5, scale=np.exp(1))
d.pdf(3.0)
d.rvs(size=5)
// mu=1, sigma=0.5 are log-space parameters
const d = new ranjs.dist.LogNormal(1, 0.5)
d.pdf(3.0)
d.sample(5)
Weibull
| scipy | ranjs | Parameter mapping |
|---|
weibull_min(c, loc=0, scale=1) | new Weibull(lambda=1, k=1) | c → k (shape); scale → lambda (scale) — order reversed |
⚠ Parameter order reversed: scipy's first positional argument c is the shape (k in ranjs); scipy's scale is ranjs lambda.
Convert: Weibull(lambda=scale, k=c).
from scipy.stats import weibull_min
d = weibull_min(c=1.5, scale=2.0)
d.pdf(1.0)
d.rvs(size=5)
// Weibull(lambda, k): lambda=scale=2.0, k=c=1.5
const d = new ranjs.dist.Weibull(2.0, 1.5)
d.pdf(1.0)
d.sample(5)
Pareto
| scipy | ranjs | Parameter mapping |
|---|
pareto(b, loc=0, scale=1) | new Pareto(xmin=1, alpha=1) | b → alpha (shape); scale → xmin (minimum value) |
from scipy.stats import pareto
d = pareto(b=3, scale=1) # xmin=1, alpha=3
d.pdf(2.0)
d.rvs(size=5)
const d = new ranjs.dist.Pareto(1, 3) // xmin=1, alpha=3
d.pdf(2.0)
d.sample(5)
Cauchy
| scipy | ranjs | Parameter mapping |
|---|
cauchy(loc=0, scale=1) | new Cauchy(x0=0, gamma=1) | loc → x0, scale → gamma (rename only) |
from scipy.stats import cauchy
d = cauchy(loc=0, scale=1)
d.pdf(0.0)
d.rvs(size=5)
const d = new ranjs.dist.Cauchy(0, 1)
d.pdf(0.0)
d.sample(5)
Laplace
| scipy | ranjs | Parameter mapping |
|---|
laplace(loc=0, scale=1) | new Laplace(mu=0, b=1) | loc → mu, scale → b (rename only) |
from scipy.stats import laplace
d = laplace(loc=0, scale=1)
d.pdf(0.0)
d.rvs(size=5)
const d = new ranjs.dist.Laplace(0, 1)
d.pdf(0.0)
d.sample(5)
Logistic
| scipy | ranjs | Parameter mapping |
|---|
logistic(loc=0, scale=1) | new Logistic(mu=0, s=1) | loc → mu, scale → s (rename only) |
from scipy.stats import logistic
d = logistic(loc=0, scale=1)
d.pdf(0.0)
d.rvs(size=5)
const d = new ranjs.dist.Logistic(0, 1)
d.pdf(0.0)
d.sample(5)
Triangular
| scipy | ranjs | Parameter mapping |
|---|
triang(c, loc=0, scale=1) | new Triangular(a=0, b=1, c=0.5) | a=loc, b=loc+scale, c=a+c_scipy*scale |
⚠ Non-trivial conversion: scipy's c is the normalized mode (0–1 within [loc, loc+scale]).
ranjs uses absolute bounds a (lower), b (upper), and c (mode).
Convert: a = loc, b = loc + scale, c = loc + c_scipy * scale.
from scipy.stats import triang
# lower=1, upper=4, mode=2: c=(2-1)/(4-1)=0.333
d = triang(c=0.333, loc=1, scale=3)
d.pdf(2.0)
d.rvs(size=5)
// Triangular(a, b, c): a=1, b=4, mode=2
const d = new ranjs.dist.Triangular(1, 4, 2)
d.pdf(2.0)
d.sample(5)
Poisson
| scipy | ranjs | Parameter mapping |
|---|
poisson(mu) | new Poisson(lambda=1) | mu → lambda (rename only) |
from scipy.stats import poisson
d = poisson(mu=3)
d.pmf(2)
d.rvs(size=5)
const d = new ranjs.dist.Poisson(3)
d.pdf(2) // pdf() covers pmf for discrete
d.sample(5)
Binomial
| scipy | ranjs | Parameter mapping |
|---|
binom(n, p) | new Binomial(n, p) | identical |
from scipy.stats import binom
d = binom(n=10, p=0.3)
d.pmf(3)
d.rvs(size=5)
const d = new ranjs.dist.Binomial(10, 0.3)
d.pdf(3)
d.sample(5)
Negative Binomial
| scipy | ranjs | Parameter mapping |
|---|
nbinom(n, p) | new NegativeBinomial(r, p) | n → r; p_ranjs = 1 − p_scipy (p convention differs) |
⚠ p convention differs: scipy nbinom counts failures before n successes, with p = P(success).
ranjs NegativeBinomial counts successes before r failures, with p = P(success).
The PMFs are duals: scipy uses p^n·(1−p)^k; ranjs uses (1−p)^r·p^x.
Convert: r = n, p_ranjs = 1 − p_scipy.
from scipy.stats import nbinom
d = nbinom(n=5, p=0.4)
d.pmf(3)
d.rvs(size=5)
// p_ranjs = 1 - p_scipy = 0.6
const d = new ranjs.dist.NegativeBinomial(5, 0.6)
d.pdf(3)
d.sample(5)
Geometric
| scipy | ranjs | Parameter mapping |
|---|
geom(p) | new Geometric(p) | same p; different support convention |
⚠ Different support: scipy geom has support {1, 2, 3, …} and counts trials until the first success.
ranjs Geometric has support {0, 1, 2, …} and counts failures before the first success.
Convert: ranjs_value = scipy_value − 1.
from scipy.stats import geom
d = geom(p=0.3) # support {1, 2, 3, ...}
d.pmf(1) # P(X=1) = p = 0.3
d.rvs(size=5)
const d = new ranjs.dist.Geometric(0.3) // support {0, 1, 2, ...}
d.pdf(0) // P(X=0) = p = 0.3
d.sample(5)
Hypergeometric
| scipy | ranjs | Parameter mapping |
|---|
hypergeom(M, n, N) | new Hypergeometric(N, K, n) | M → N (population); n → K (successes in pop.); N → n (draws) |
⚠ Parameter name collision: scipy and ranjs both use N, but they mean different things.
scipy: M = population size, n = successes in population, N = number of draws.
ranjs: N = population size, K = successes in population, n = number of draws.
Convert: Hypergeometric(N=M, K=n_scipy, n=N_scipy).
from scipy.stats import hypergeom
# population=20, successes_in_pop=7, draws=5
d = hypergeom(M=20, n=7, N=5)
d.pmf(2)
d.rvs(size=5)
// Hypergeometric(N, K, n): N=20, K=7, n=5
const d = new ranjs.dist.Hypergeometric(20, 7, 5)
d.pdf(2)
d.sample(5)