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, ∞)"

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

  • @distribution 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
  • 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 distribution


@distribution("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:

@distribution("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 distribution


@distribution("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
    @distribution("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.

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

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}

The @distribution 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.

All of these are the same Normal:

from distparams import distribution

# Register the distribution first (see registration below),
# then define your constructor against the canonical parameters.


@distribution("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:

@distribution("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):

@distribution("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:

@distribution("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:

@distribution("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.1.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.1.0
File Size Uploaded
distparams-0.1.0.tar.gz 46.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for distparams 0.1.0
File Interpreter ABI Platform
distparams-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 130.3 kB

Release files / distparams-0.1.0.tar.gz

Download URL distparams-0.1.0.tar.gz
Size 46.9 kB
Tags Source
SHA-256 checksum
How to use checksums
7e42af5ba448f883f4e7821ba25d037193b96f7908f0624bc971a0330750226b
BLAKE2b-256 checksum
How to use checksums
5b3afb3ecfdd8bee0721e9ec6ebdb5d180c21280d08b3412d190a7355f397d96
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 5, 2026.

Transparency log

Release files / distparams-0.1.0-py3-none-any.whl

Download URL distparams-0.1.0-py3-none-any.whl
Size 83.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fee12c11ff3c61d2181bf6718f52f1dc02823b521af234f563ec27d3a9dfb651
BLAKE2b-256 checksum
How to use checksums
5c78ebaf026c39368196e22a1a9f126f08f9a5255618f9d669ad37a915a40ed1
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 5, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.0

2 release files

This release

0.1.0 This release

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