Skip to main content

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).

Open with marimo

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

  • @parametrized decorator — all canonical parameterizations come for free
  • distparams.stats — scipy.stats-shaped namespace where every call accepts any registered parametrization
  • distparams.random — the stdlib random module 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, and get_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 stdlib random module
  • 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)

Source distribution for distparams 0.3.0
File Size Uploaded
distparams-0.3.0.tar.gz 53.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for distparams 0.3.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page