distparams
Single source of truth for statistical distribution parameterizations.
Installation
pip install distparams
Quick Start
from distparams import get_distribution, Support
# Get distribution info
normal = get_distribution("normal")
sigma = normal.canonical.parameters["sigma"]
# Check parameter support
sigma.support.contains(0.5) # True
sigma.support.contains(-1) # False
str(sigma.support) # "(0, ∞)"
# Where a distribution's outcomes live — no values needed for the structure
from distparams import get_support
support = get_support("binomial") # symbolic: bounds are the parameter names
support.left_value, support.right_value # (0, 'n')
get_support("binomial", n=10).right_value # 10 — resolved
# Convert between ecosystems
from distparams import convert
convert("gamma", "scipy", "pymc", a=2, scale=3)
# → {"alpha": 2, "beta": 0.333}
Core concepts
| Term | What it means |
|---|---|
| Parameter | The canonical name for one semantic parameter — normal's sigma is PyMC's sigma, scipy's scale, jStat's std: one parameter, many names. Parameters own their Support (admissible values) and their bijections. A parameter is a mutable builder while authoring; once registered, its bijections change only via register_parameter_alias. |
| Bijection | A name's value relation to a canonical parameter: a one-to-one map with its inverse. The identity bijection is a pure rename — a true alias (loc ↔ mu); a transformed one reparameterizes the value (tau → sigma = tau**-0.5, via Bijection.power/Bijection.reciprocal, or an explicit pair when the inverse isn't mechanical, like logit ↔ probability). |
| ParameterSet | A complete set of parameters that determines the distribution — what PPL docs call a "parameterization" ({mu, sigma}, {mu, tau}). Derived parameterizations live here too: their members are functions of several canonical parameters, so they are not bijections at all (gamma's mu = shape/rate; beta's (mu, sigma) combines the mean and sd relations). Derived sets carry name/description with the defining formulas, so the registry can explain them. |
| NamingConvention | One ecosystem's parameter names for one distribution. Purely a naming relation — value transforms never live here; a value-transforming name pairs the row with a parameter bijection. |
| Vocabulary | One ecosystem's names for a parametrization, written once and applying to every distribution that uses it (keyed (ecosystem, parametrization)). Per-distribution conventions override it. |
| Distribution | Canonical parameters + parameter sets + naming overrides, plus support — where the distribution's outcomes live (normal: all of ℝ, binomial: [0, n]), static or parameter-dependent. The canonical parametrization name is derived from the parameter declaration order. |
Conversion flows source names → canonical parameters → target names:
names resolve through conventions, vocabularies, and bijections; values
transform through parameter bijections and parameter-set transforms.
Derived parameterizations are self-describing:
get_distribution("beta").canonical.parameter_sets includes the (mu, sigma)
set with its name and defining formulas.
Coming from scipy?
Keep your scipy distributions — lose the parameter-name bookkeeping.
distparams.stats is a drop-in-shaped stand-in for scipy.stats: the same
attribute names (norm, t, chi2, binom, ...), but every call accepts
any parametrization the registry knows and returns a frozen scipy
distribution:
from distparams.stats import norm
norm(mu=0, sigma=1) # ✓ canonical
norm(loc=0, scale=1) # ✓ plain scipy
norm(mu=0, scale=1) # ✓ mix freely — names resolve through the registry
norm(tau=4) # ✓ precision → sigma = 0.5
# → a frozen scipy distribution: .pdf, .rvs, .mean, ...
from distparams import stats
stats.gamma(mu=4, sigma=2) # ✓ mean/sd parameterization
stats.binom(n=10, p=0.3) # ✓ discrete, scipy names
stats.expon(scale=2) # ✓ registry names, scipy spelling
dir(distparams.stats) lists every scipy-style name the registry covers;
anything else (stats.normal, distributions scipy doesn't have) raises an
AttributeError listing what is available. Attributes resolve lazily, so
importing distparams never imports scipy — scipy is only needed when a
distribution is actually used.
Prefer the explicit form? from_scipy wraps any scipy.stats distribution
the same way:
from scipy import stats
from distparams.integrations import from_scipy
new_normal = from_scipy(stats.norm)
new_normal(mu=0, sigma=1) # ✓ canonical
new_normal(loc=0, scale=1) # ✓ plain scipy
new_normal(mean=0, std=1) # ✓ jStat-style names
new_normal(tau=4) # ✓ precision → sigma = 0.5
new_normal(0, 1) # ✓ positional
# → a frozen scipy distribution: .pdf, .rvs, .mean, ...
Aliases, alternative parameterizations, and even whole derived parametrizations come straight from the registry — no mapping code per distribution, and anything registered later works too:
new_gamma = from_scipy(stats.gamma)
new_gamma(alpha=2, beta=3) # ✓ shape/rate → a=2, scale=1/3
new_gamma(2) # ✓ partial — scipy defaults fill the rest
new_gamma(mu=4, sigma=2) # ✓ mean/sd parameterization
new_uniform = from_scipy(stats.uniform)
new_uniform(lower=1, upper=3) # ✓ even loc/scale via registry transforms
Unknown names are caught at call time instead of silently misbehaving, and a scipy distribution that has no registry entry raises with instructions rather than guessing.
Coming from the stdlib random module?
distparams.random mirrors the random module with zero dependencies: the
variate generators accept every registered parametrization, and everything
else (seed, choice, shuffle, …) delegates to the RNG untouched:
import distparams.random
distparams.random.gamma(shape=2, scale=3) # canonical names
distparams.random.gamma(alpha=2, beta=1 / 3) # pymc names (beta is the rate)
distparams.random.weibull(alpha=2, beta=1.5) # pymc order — alpha is the shape, beta the scale
from distparams.random import Random
Random(42).gauss(mu=2, variance=2) # variance → sigma
Random(42).gammavariate(2, 3) # stdlib spelling stays drop-in: beta is the scale
The stdlib method names (gammavariate, gauss, weibullvariate, …) keep
stdlib semantics exactly — the same call on random.Random draws the same
number for the same seed, and the Random class adds nothing to the
random.Random surface (dir() matches stdlib exactly). The registry names
(gamma, normal, weibull, …) arbitrate the cross-ecosystem traps
instead: weibullvariate swaps alpha/beta relative to PyMC and Stan, and
gammavariate's beta is a scale where pymc's beta is a rate.
Interactive Example
The registry types are executable metadata. In this
marimo notebook, Support decides which control each
parameter gets — sliders for bounded probabilities, floored numbers for
scales, step-1 counters for trial counts — and convert() feeds the values
straight into scipy:
from distparams import get_distribution
params = get_distribution("binomial").params # {"n": Parameter, "p": Parameter}
params["p"].support # [0, 1]
params["p"].support.is_bounded # True → render a slider
get_distribution("normal").params["sigma"].support
# (0, ∞) → render a number floored at 0
Every Parameter in the dict also carries its .description and
.bijections — the alternative names and forms the registry knows (normal's
tau is the precision: sigma = tau**-0.5).
Run it locally (the notebook needs scipy and marimo, which live in the dev group — the library itself stays zero-dependency):
uv run --group dev marimo edit examples/scipy_distributions.py
Features
@parametrizeddecorator — all canonical parameterizations come for freedistparams.stats— scipy.stats-shaped namespace where every call accepts any registered parametrizationdistparams.random— the stdlibrandommodule accepting every registered parametrization (zero-dependency, seed-exact drop-in)from_scipy— wrap any scipy distribution into a callable that accepts every registered parametrization- Support dataclass for parameter domains (positive, unit interval, real, etc.)
— every parameter's description, support, and aliases are reachable via
get_distribution(name).params, andget_support(name, **params)resolves where a distribution's outcomes live — with values (scalars or arrays), or symbolically without them (bounds come back as the parameter names) - Cross-ecosystem conversion between PyMC, scipy, Stan, jStat, TFP, NumPyro, R (base
stats), and the stdlibrandommodule - Ecosystem vocabularies — an ecosystem's parameter names are written once per parametrization, not once per distribution
- Plugin system for external packages — add an ecosystem, alias, or whole parameterization without forking
- Zero dependencies — pure Python, ~160 KB of source, ~50 ms import, µs-scale conversions
Why not PreliZ?
PreliZ is for prior elicitation — it computes with distributions and brings
numba, pytensor, scipy, and matplotlib with it. distparams is the
zero-dependency metadata layer underneath: it knows the parameterizations
(and every ecosystem's names for them) so your library doesn't have to.
Use it in your own package
Wrap your constructor once and every ecosystem's parameter names — plus alternative parameterizations from the registry — just work:
from distparams import parametrized
@parametrized("beta")
def beta(alpha, beta):
return stats.beta(alpha, beta)
beta(1, 1) # canonical, positional
beta(successes=2, failures=3) # → alpha=3, beta=4
Your function's parameter names are honored exactly — declare them in whatever convention your library uses:
@parametrized("beta")
def beta(a, b): # scipy-style names
return stats.beta(a, b)
beta(alpha=1, beta=2) # → a=1, b=2
beta(successes=2, failures=3) # → a=3, b=4
Prefer to keep your dataclasses? The decorator works on them directly — and
preserves the class (isinstance, classmethods, dataclasses.fields all keep
working):
from dataclasses import dataclass
from distparams import parametrized
@parametrized("beta")
@dataclass
class Beta:
alpha: float
beta: float
Beta(a=1, b=2) # → Beta(alpha=1, beta=2)
Beta(successes=2, failures=3) # → Beta(alpha=3, beta=4)
PyMC-style .dist() classmethods work too — decorate the raw function below
@classmethod, and the bound cls is passed through untouched. Context
parameters ride along: anything the callable declares that the registry
doesn't know — plus everything in **kwargs — is forwarded untouched.
Unknown kwargs without **kwargs stay strict errors.
class Normal:
@classmethod
@parametrized("normal")
def dist(cls, mu=0, sigma=1, **kwargs): ...
Normal.dist(loc=0, scale=1, size=(2, 3)) # ✓ size/dtype-style extras forward
Note that PyMC's .dist() only accepts extras like size, shape, and
dtype — observed, dims, and initval belong to the constructor in
model context (PyMC itself raises on them in .dist()). Registry-known
names always resolve: for gamma, shape is the shape parameter, not array
shape.
Logp-style signatures work too. A value argument like x isn't a registry
parameter, so it's a context parameter: it passes through untouched and
takes positionals in declaration order, like plain Python:
@parametrized("normal")
def normal_logp(x, mu, sigma):
return -0.5 * ((x - mu) / sigma) ** 2 - np.log(sigma) - 0.5 * np.log(2 * np.pi)
normal_logp(data, 0, 1) # ✓ x=data, mu=0, sigma=1
normal_logp(data, mu=0, sigma=1) # ✓ positional and keyword mix freely
normal_logp(data, loc=0, scale=1) # ✓ the parameters resolve across ecosystems
Trailing value arguments (def beta_logp(alpha, beta, x)) and defaults
(def normal_logp(x, mu=0, sigma=1) called as normal_logp(data)) bind the
same way. Surplus positionals forward into a declared *args, and
keyword-only context parameters must be passed by keyword.
And downstream packages can extend the registry — names, aliases,
parameterizations — without forking, via distparams.plugins. To teach every
distribution your package's parameter names for a parametrization, register a
vocabulary once:
from distparams import register_ecosystem_vocabulary
register_ecosystem_vocabulary("my_pkg", "mu_sigma", {"center": "mu", "spread": "sigma"})
# every registered mu_sigma distribution now speaks my_pkg
convert("normal", "scipy", "my_pkg", loc=0, scale=1) # {"center": 0, "spread": 1}
convert("cauchy", "my_pkg", "scipy", center=1, spread=2) # {"loc": 1, "scale": 2}
A single distribution that names parameters differently keeps a
per-distribution override via register_ecosystem_mapping (it takes
precedence over the vocabulary row):
from distparams import NamingConvention, register_ecosystem_mapping
register_ecosystem_mapping("uniform", "my_pkg", NamingConvention("my_pkg", {"min": "lower", "max": "upper"}))
convert("uniform", "my_pkg", "scipy", min=0, max=5) # {"loc": 0, "scale": 5}
Rows are pure renames — they change what a parameter is called, never its
value. In the example above spread is an sd, so the rename alone is correct.
If spread carried a different quantity — a variance — the row alone would
silently pass values through (spread=4 would convert to scale=4). Value
transforms live on the parameter, declared once and inherited by every
ecosystem that maps a name onto that role:
from distparams import Bijection, register_parameter_alias
register_parameter_alias("normal", "sigma", "spread", Bijection.power(0.5))
convert("normal", "my_pkg", "scipy", center=0, spread=4) # {"loc": 0, "scale": 2.0}
Decorate your constructor classes and every registered naming resolves at construction — declare the parameters in your names (after registering them, as above), and callers may use any of them:
from distparams import parametrized
@parametrized("uniform")
class Uniform:
def __init__(self, min=0.0, max=1.0): ...
Uniform(min=1, max=3) # direct
Uniform(a=1, b=3) # jstat names
Uniform(loc=2, scale=3) # scipy spelling → min=2, max=5
Resolution happens before your body runs, so the body keeps its own typing
contract — and value transforms compose before it too: a tau=4 precision
becomes sigma=0.5 (an int your body never sees), and lazily-computed
values such as Polars expressions transform into new expressions, which pass
through untouched. Two things to know: unknown names raise
UnknownParameterError (a ValueError with grouped suggestions) rather
than TypeError, and value-carrying names should be registered with
register_parameter_alias first, as above.
The transcendental transforms (log/exp/sqrt) can target your
expression engine as well, so an expression transforms into another
expression instead of hitting the math module. Register an ops backend and
pass ops= to the decorator — parameter resolution, and only that, then
runs through your engine's methods:
import math
from distparams import parametrized, register_distribution_ops
# Backend ops serve scalars and expressions through one path — guard the
# method call so plain floats keep working:
register_distribution_ops(
"polars",
log=lambda x: x.log() if hasattr(x, "log") else math.log(x),
exp=lambda x: x.exp() if hasattr(x, "exp") else math.exp(x),
)
@parametrized("log_normal", ops="polars")
class LogNormal:
def __init__(self, mu=0.0, sigma=1.0): ...
LogNormal(s=0.5, scale=expr) # mu resolves to expr.log(), still lazy
A call-site context overrides the class's baked-in backend — ops= is a
default, not a lock:
with use_ops("verbose"):
LogNormal(s=0.5, scale=expr) # resolution routes through the override
use_ops remains for wider scoping — plain functions, with blocks, or
classes whose bodies also need the backend. It sets a process-global backend
for the block — not thread-local — and arithmetic transforms (powers,
reciprocals) never consult it: they compose through operators alone.
The @parametrized Decorator
The flagship feature. Wrap your distribution constructor once and every
ecosystem's parameter names just work — aliases and alternative
parameterizations resolve automatically from the registry. (Previously
@distribution, which is deprecated since 0.2.0 and will be removed in
0.4.0.)
All of these are the same Normal:
from distparams import parametrized
# Register the distribution first (see registration below),
# then define your constructor against the canonical parameters.
@parametrized("normal", canonical=["mu", "sigma"])
def normal(mu, sigma):
return stats.norm(loc=mu, scale=sigma)
normal(mu=0, sigma=1) # ✓ canonical (Gaussian notation)
normal(mu=0, tau=4) # ✓ tau -> sigma (precision parameterization)
normal(mu=0, precision=4) # ✓ precision -> sigma (same as tau)
normal(mu=0, variance=4) # ✓ variance -> sigma
normal(mu=0, var=4) # ✓ var -> sigma (same as variance)
normal(loc=0, scale=1) # ✓ scipy notation
normal(mu=0, std=1) # ✓ your own aliases
Poisson(mu=...) and Poisson(lam=...) are the same parameter — the
decorator resolves them transparently:
@parametrized("poisson", canonical=["rate"])
def poisson(rate):
return stats.poisson(mu=rate)
poisson(mu=5) # ✓ mean parameterization
poisson(lam=5) # ✓ rate/lambda parameterization
poisson(rate=5) # ✓ canonical
And entire parameterizations (not just names) come for free too — a Gamma
written against (shape, rate) happily accepts (shape, scale), (a, scale),
(mu, sigma), or (mu, variance):
@parametrized("gamma", canonical=["shape", "rate"])
def gamma(shape, rate):
return stats.gamma(a=shape, scale=1 / rate)
gamma(shape=2, rate=3) # ✓ canonical
gamma(shape=2, scale=3) # ✓ scale -> rate = 1/3
gamma(a=2, scale=3) # ✓ scipy notation
gamma(mu=2, sigma=2) # ✓ mean/std parameterization
gamma(mu=2, variance=4) # ✓ mean/variance (same as sigma=2; var= works too)
Conflicting or unknown parameters are caught at call time instead of silently misbehaving:
normal(mu=0, sigma=1, tau=4) # ✗ ValueError: both map to 'sigma'
normal(mu=0, foo=1) # ✗ ValueError: unknown parameter 'foo'
To wire this up, define the distribution once (canonical parameters and every ecosystem's naming). The fluent authoring API makes this read like the math:
from distparams import Bijection, Distribution, NamingConvention, Parameter, Support, register_distribution
# your own distribution's name — "normal" here for familiarity (the built-in
# is already registered; a fresh name avoids the duplicate-registration error)
mu = Parameter("mu", "Location parameter", Support.real())
mu.alias("mean")
sigma = Parameter("sigma", "Scale parameter", Support.positive())
sigma.alias("scale").alias("std")
sigma.alternative("tau", Bijection.power(-0.5)) # precision → sigma
register_distribution(
Distribution(
"normal",
params={"mu": mu, "sigma": sigma}, # canonical name derived: "mu_sigma"
naming_conventions=[
NamingConvention("pymc", {"mu": "mu", "sigma": "sigma"}),
NamingConvention("scipy", {"loc": "mu", "scale": "sigma"}),
NamingConvention("jstat", {"mean": "mu", "std": "sigma"}),
],
)
)
Ecosystems that share a parametrization are covered once by the built-in
vocabularies (src/distparams/_vocabularies.py) — per-distribution
conventions are only needed for genuine exceptions, and the parametrization
name is derived from the parameter declaration order.
Typing
The decorator preserves your function's signature, so canonical calls are checked by mypy, pyright, and ty — and IDE autocomplete works on parameter names and types:
@parametrized("normal")
def normal(mu: float, sigma: float) -> float: ...
normal(mu=0, sigma=1) # ✓ fully type-checked, like a plain function
normal(0, 1) # ✓ positional works too
Alias calls resolve at runtime from the registry, which is beyond what any static analyzer can see — a strict checker will flag them even though they run correctly:
normal(loc=0, scale=1) # ✓ runs fine, but checkers see unknown names
If you want alias resolution to be statically visible, resolve first and
unpack the result — resolve_parameters returns canonical parameters:
params = resolve_parameters("normal", loc=0, scale=1) # {"mu": 0, "sigma": 1}
normal(**params)
Rule of thumb: canonical names are the typed calling convention; aliases
are a runtime convenience. Typos in alias names are caught at call time with
a ValueError listing every valid name, so nothing fails silently.
Defaults and alternative parameterizations
Your function's own defaults do the heavy lifting — supply what you know, let the rest default:
@parametrized("normal")
def normal(mu=0, sigma=1):
return stats.norm(mu, sigma)
normal() # mu=0, sigma=1 (pure defaults)
normal(sigma=2) # mu=0, sigma=2 (mu by default)
normal(tau=4) # mu=0, sigma=0.5 (precision → sigma)
normal(precision=4) # mu=0, sigma=0.5 (same as tau)
normal(variance=4) # mu=0, sigma=2 (variance → sigma)
normal(var=4) # mu=0, sigma=2 (same as variance)
normal(mean=0, std=1) # jStat-style names work too
One name per parameter: normal(sigma=2, tau=4) raises a conflict — both
map to sigma — rather than guessing which wins. Values are transformed,
not guessed: tau=4 means sigma=0.5, tau=2 means sigma≈0.707.
An unknown parameter names the distribution, lists every valid name grouped by role, and suggests the closest match for typos:
normal(precission=4)
# Unknown parameter 'precission' for distribution 'normal'.
# Known parameters: (mu or loc or mean) and
# (sigma or scale or std or tau or precision or variance or var or sd).
# Did you mean 'precision'?
Metadata
Release files for distparams 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| distparams-0.3.0.tar.gz | 53.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| distparams-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 144.4 kB
Release files / distparams-0.3.0.tar.gz
| Download URL | distparams-0.3.0.tar.gz |
|---|---|
| Size | 53.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d5365958c95398cff8c1f0c5feb624500f8af20aac05dbe0f7f20a68195ef44f
|
|
BLAKE2b-256 checksum How to use checksums |
8f9e0bfe25acc49c3a868c8d1d400c67937139a500577dd772048a485c41bfff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 9, 2026.
Transparency logRelease files / distparams-0.3.0-py3-none-any.whl
| Download URL | distparams-0.3.0-py3-none-any.whl |
|---|---|
| Size | 91.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0c76f3b8f9e9f9945c04c054237cc41ac199801a177c78179534d257072ba3f1
|
|
BLAKE2b-256 checksum How to use checksums |
d6172c1a332b403d5e24b17707d0930fb023497edc587b9f2b7a78f2a1dc9d57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 9, 2026.
Transparency log