distparams 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. distparams-0.1.0/PKG-INFO +496 -0
  2. distparams-0.1.0/README.md +475 -0
  3. distparams-0.1.0/pyproject.toml +78 -0
  4. distparams-0.1.0/src/distparams/__init__.py +72 -0
  5. distparams-0.1.0/src/distparams/_converter.py +557 -0
  6. distparams-0.1.0/src/distparams/_decorator.py +385 -0
  7. distparams-0.1.0/src/distparams/_distributions/__init__.py +159 -0
  8. distparams-0.1.0/src/distparams/_distributions/asymmetric_laplace.py +42 -0
  9. distparams-0.1.0/src/distparams/_distributions/bernoulli.py +39 -0
  10. distparams-0.1.0/src/distparams/_distributions/beta.py +125 -0
  11. distparams-0.1.0/src/distparams/_distributions/beta_binomial.py +21 -0
  12. distparams-0.1.0/src/distparams/_distributions/beta_negative_binomial.py +27 -0
  13. distparams-0.1.0/src/distparams/_distributions/binomial.py +24 -0
  14. distparams-0.1.0/src/distparams/_distributions/burr12.py +30 -0
  15. distparams-0.1.0/src/distparams/_distributions/categorical.py +21 -0
  16. distparams-0.1.0/src/distparams/_distributions/cauchy.py +24 -0
  17. distparams-0.1.0/src/distparams/_distributions/chi.py +21 -0
  18. distparams-0.1.0/src/distparams/_distributions/chi_squared.py +17 -0
  19. distparams-0.1.0/src/distparams/_distributions/dirichlet.py +17 -0
  20. distparams-0.1.0/src/distparams/_distributions/dirichlet_multinomial.py +19 -0
  21. distparams-0.1.0/src/distparams/_distributions/discrete_laplace.py +30 -0
  22. distparams-0.1.0/src/distparams/_distributions/discrete_uniform.py +22 -0
  23. distparams-0.1.0/src/distparams/_distributions/discrete_weibull.py +24 -0
  24. distparams-0.1.0/src/distparams/_distributions/exgaussian.py +50 -0
  25. distparams-0.1.0/src/distparams/_distributions/exponential.py +22 -0
  26. distparams-0.1.0/src/distparams/_distributions/f.py +19 -0
  27. distparams-0.1.0/src/distparams/_distributions/gamma.py +74 -0
  28. distparams-0.1.0/src/distparams/_distributions/generalized_extreme.py +35 -0
  29. distparams-0.1.0/src/distparams/_distributions/generalized_normal.py +32 -0
  30. distparams-0.1.0/src/distparams/_distributions/generalized_pareto.py +29 -0
  31. distparams-0.1.0/src/distparams/_distributions/generalized_poisson.py +25 -0
  32. distparams-0.1.0/src/distparams/_distributions/geometric.py +21 -0
  33. distparams-0.1.0/src/distparams/_distributions/gompertz.py +32 -0
  34. distparams-0.1.0/src/distparams/_distributions/gumbel.py +23 -0
  35. distparams-0.1.0/src/distparams/_distributions/half_cauchy.py +21 -0
  36. distparams-0.1.0/src/distparams/_distributions/half_generalized_normal.py +29 -0
  37. distparams-0.1.0/src/distparams/_distributions/half_normal.py +23 -0
  38. distparams-0.1.0/src/distparams/_distributions/half_student_t.py +28 -0
  39. distparams-0.1.0/src/distparams/_distributions/hypergeometric.py +21 -0
  40. distparams-0.1.0/src/distparams/_distributions/inverse_gamma.py +25 -0
  41. distparams-0.1.0/src/distparams/_distributions/johnson_su.py +35 -0
  42. distparams-0.1.0/src/distparams/_distributions/kumaraswamy.py +19 -0
  43. distparams-0.1.0/src/distparams/_distributions/laplace.py +23 -0
  44. distparams-0.1.0/src/distparams/_distributions/levy.py +23 -0
  45. distparams-0.1.0/src/distparams/_distributions/lkj.py +17 -0
  46. distparams-0.1.0/src/distparams/_distributions/log_logistic.py +30 -0
  47. distparams-0.1.0/src/distparams/_distributions/log_normal.py +26 -0
  48. distparams-0.1.0/src/distparams/_distributions/log_uniform.py +30 -0
  49. distparams-0.1.0/src/distparams/_distributions/logistic.py +24 -0
  50. distparams-0.1.0/src/distparams/_distributions/logit_normal.py +26 -0
  51. distparams-0.1.0/src/distparams/_distributions/lomax.py +29 -0
  52. distparams-0.1.0/src/distparams/_distributions/matrix_normal.py +21 -0
  53. distparams-0.1.0/src/distparams/_distributions/maxwell.py +25 -0
  54. distparams-0.1.0/src/distparams/_distributions/moyal.py +19 -0
  55. distparams-0.1.0/src/distparams/_distributions/multinomial.py +24 -0
  56. distparams-0.1.0/src/distparams/_distributions/multivariate_normal.py +19 -0
  57. distparams-0.1.0/src/distparams/_distributions/negative_binomial.py +44 -0
  58. distparams-0.1.0/src/distparams/_distributions/normal.py +25 -0
  59. distparams-0.1.0/src/distparams/_distributions/pareto.py +30 -0
  60. distparams-0.1.0/src/distparams/_distributions/poisson.py +23 -0
  61. distparams-0.1.0/src/distparams/_distributions/rayleigh.py +21 -0
  62. distparams-0.1.0/src/distparams/_distributions/rice.py +28 -0
  63. distparams-0.1.0/src/distparams/_distributions/skellam.py +17 -0
  64. distparams-0.1.0/src/distparams/_distributions/skew_normal.py +33 -0
  65. distparams-0.1.0/src/distparams/_distributions/skew_student_t.py +33 -0
  66. distparams-0.1.0/src/distparams/_distributions/student_t.py +24 -0
  67. distparams-0.1.0/src/distparams/_distributions/trapezoid.py +55 -0
  68. distparams-0.1.0/src/distparams/_distributions/triangular.py +24 -0
  69. distparams-0.1.0/src/distparams/_distributions/truncated_normal.py +72 -0
  70. distparams-0.1.0/src/distparams/_distributions/uniform.py +44 -0
  71. distparams-0.1.0/src/distparams/_distributions/von_mises.py +30 -0
  72. distparams-0.1.0/src/distparams/_distributions/wald.py +19 -0
  73. distparams-0.1.0/src/distparams/_distributions/weibull.py +27 -0
  74. distparams-0.1.0/src/distparams/_distributions/wishart.py +19 -0
  75. distparams-0.1.0/src/distparams/_distributions/zipf.py +25 -0
  76. distparams-0.1.0/src/distparams/_ops.py +82 -0
  77. distparams-0.1.0/src/distparams/_plugins.py +156 -0
  78. distparams-0.1.0/src/distparams/_registry.py +46 -0
  79. distparams-0.1.0/src/distparams/_types.py +387 -0
  80. distparams-0.1.0/src/distparams/_vocabularies.py +193 -0
  81. distparams-0.1.0/src/distparams/integrations/__init__.py +12 -0
  82. distparams-0.1.0/src/distparams/integrations/scipy.py +158 -0
  83. distparams-0.1.0/src/distparams/plugins.py +36 -0
  84. distparams-0.1.0/src/distparams/py.typed +0 -0
  85. distparams-0.1.0/src/distparams/random.py +212 -0
  86. distparams-0.1.0/src/distparams/stats.py +93 -0
@@ -0,0 +1,496 @@
1
+ Metadata-Version: 2.4
2
+ Name: distparams
3
+ Version: 0.1.0
4
+ Summary: Single source of truth for statistical distribution parameterizations
5
+ Keywords: statistics,distribution,parameterization,bayesian,pymc,scipy
6
+ Author: Will Dean
7
+ Author-email: Will Dean <wd60622@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Requires-Python: >=3.12, <3.15
18
+ Project-URL: Homepage, https://github.com/williambdean/distparams
19
+ Project-URL: Repository, https://github.com/williambdean/distparams
20
+ Description-Content-Type: text/markdown
21
+
22
+ # distparams
23
+
24
+ Single source of truth for statistical distribution parameterizations.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install distparams
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ from distparams import get_distribution, Support
36
+
37
+ # Get distribution info
38
+ normal = get_distribution("normal")
39
+ sigma = normal.canonical.parameters["sigma"]
40
+
41
+ # Check parameter support
42
+ sigma.support.contains(0.5) # True
43
+ sigma.support.contains(-1) # False
44
+ str(sigma.support) # "(0, ∞)"
45
+
46
+ # Convert between ecosystems
47
+ from distparams import convert
48
+
49
+ convert("gamma", "scipy", "pymc", a=2, scale=3)
50
+ # → {"alpha": 2, "beta": 0.333}
51
+ ```
52
+
53
+ ## Core concepts
54
+
55
+ | Term | What it means |
56
+ |---|---|
57
+ | **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`. |
58
+ | **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). |
59
+ | **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. |
60
+ | **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. |
61
+ | **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. |
62
+ | **Distribution** | Canonical parameters + parameter sets + naming overrides. The canonical parametrization name is derived from the parameter declaration order. |
63
+
64
+ Conversion flows *source names → canonical parameters → target names*:
65
+ names resolve through conventions, vocabularies, and bijections; values
66
+ transform through parameter bijections and parameter-set transforms.
67
+ Derived parameterizations are self-describing:
68
+ `get_distribution("beta").canonical.parameter_sets` includes the `(mu, sigma)`
69
+ set with its name and defining formulas.
70
+
71
+ ## Coming from scipy?
72
+
73
+ Keep your scipy distributions — lose the parameter-name bookkeeping.
74
+ `distparams.stats` is a drop-in-shaped stand-in for `scipy.stats`: the same
75
+ attribute names (`norm`, `t`, `chi2`, `binom`, ...), but every call accepts
76
+ any parametrization the registry knows and returns a frozen scipy
77
+ distribution:
78
+
79
+ ```python
80
+ from distparams.stats import norm
81
+
82
+ norm(mu=0, sigma=1) # ✓ canonical
83
+ norm(loc=0, scale=1) # ✓ plain scipy
84
+ norm(mu=0, scale=1) # ✓ mix freely — names resolve through the registry
85
+ norm(tau=4) # ✓ precision → sigma = 0.5
86
+ # → a frozen scipy distribution: .pdf, .rvs, .mean, ...
87
+ ```
88
+
89
+ ```python
90
+ from distparams import stats
91
+
92
+ stats.gamma(mu=4, sigma=2) # ✓ mean/sd parameterization
93
+ stats.binom(n=10, p=0.3) # ✓ discrete, scipy names
94
+ stats.expon(scale=2) # ✓ registry names, scipy spelling
95
+ ```
96
+
97
+ `dir(distparams.stats)` lists every scipy-style name the registry covers;
98
+ anything else (`stats.normal`, distributions scipy doesn't have) raises an
99
+ `AttributeError` listing what is available. Attributes resolve lazily, so
100
+ importing `distparams` never imports scipy — scipy is only needed when a
101
+ distribution is actually used.
102
+
103
+ Prefer the explicit form? `from_scipy` wraps any `scipy.stats` distribution
104
+ the same way:
105
+
106
+ ```python
107
+ from scipy import stats
108
+ from distparams.integrations import from_scipy
109
+
110
+ new_normal = from_scipy(stats.norm)
111
+
112
+ new_normal(mu=0, sigma=1) # ✓ canonical
113
+ new_normal(loc=0, scale=1) # ✓ plain scipy
114
+ new_normal(mean=0, std=1) # ✓ jStat-style names
115
+ new_normal(tau=4) # ✓ precision → sigma = 0.5
116
+ new_normal(0, 1) # ✓ positional
117
+ # → a frozen scipy distribution: .pdf, .rvs, .mean, ...
118
+ ```
119
+
120
+ Aliases, alternative parameterizations, and even whole derived
121
+ parametrizations come straight from the registry — no mapping code per
122
+ distribution, and anything registered later works too:
123
+
124
+ ```python
125
+ new_gamma = from_scipy(stats.gamma)
126
+
127
+ new_gamma(alpha=2, beta=3) # ✓ shape/rate → a=2, scale=1/3
128
+ new_gamma(2) # ✓ partial — scipy defaults fill the rest
129
+ new_gamma(mu=4, sigma=2) # ✓ mean/sd parameterization
130
+
131
+ new_uniform = from_scipy(stats.uniform)
132
+ new_uniform(lower=1, upper=3) # ✓ even loc/scale via registry transforms
133
+ ```
134
+
135
+ Unknown names are caught at call time instead of silently misbehaving, and a
136
+ scipy distribution that has no registry entry raises with instructions rather
137
+ than guessing.
138
+
139
+ ## Coming from the stdlib random module?
140
+
141
+ `distparams.random` mirrors the `random` module with zero dependencies: the
142
+ variate generators accept every registered parametrization, and everything
143
+ else (`seed`, `choice`, `shuffle`, …) delegates to the RNG untouched:
144
+
145
+ ```python
146
+ import distparams.random
147
+
148
+ distparams.random.gamma(shape=2, scale=3) # canonical names
149
+ distparams.random.gamma(alpha=2, beta=1 / 3) # pymc names (beta is the rate)
150
+ distparams.random.weibull(alpha=2, beta=1.5) # pymc order — alpha is the shape, beta the scale
151
+
152
+ from distparams.random import Random
153
+
154
+ Random(42).gauss(mu=2, variance=2) # variance → sigma
155
+ Random(42).gammavariate(2, 3) # stdlib spelling stays drop-in: beta is the scale
156
+ ```
157
+
158
+ The stdlib method names (`gammavariate`, `gauss`, `weibullvariate`, …) keep
159
+ stdlib semantics exactly — the same call on `random.Random` draws the same
160
+ number for the same seed, and the `Random` class adds nothing to the
161
+ `random.Random` surface (`dir()` matches stdlib exactly). The registry names
162
+ (`gamma`, `normal`, `weibull`, …) arbitrate the cross-ecosystem traps
163
+ instead: `weibullvariate` swaps alpha/beta relative to PyMC and Stan, and
164
+ `gammavariate`'s `beta` is a scale where pymc's `beta` is a rate.
165
+
166
+ ## Interactive Example
167
+
168
+ The registry types are *executable metadata*. In this
169
+ [marimo](https://marimo.dev) notebook, `Support` decides which control each
170
+ parameter gets — sliders for bounded probabilities, floored numbers for
171
+ scales, step-1 counters for trial counts — and `convert()` feeds the values
172
+ straight into scipy:
173
+
174
+ ```python
175
+ from distparams import get_distribution
176
+
177
+ params = get_distribution("binomial").params # {"n": Parameter, "p": Parameter}
178
+ params["p"].support # [0, 1]
179
+ params["p"].support.is_bounded # True → render a slider
180
+
181
+ get_distribution("normal").params["sigma"].support
182
+ # (0, ∞) → render a number floored at 0
183
+ ```
184
+
185
+ Every `Parameter` in the dict also carries its `.description` and
186
+ `.bijections` — the alternative names and forms the registry knows (normal's
187
+ `tau` is the precision: `sigma = tau**-0.5`).
188
+
189
+ [![Open with marimo](https://marimo.io/shield.svg)](https://marimo.app/https://github.com/williambdean/distparams/blob/main/examples/scipy_distributions.py)
190
+
191
+ Run it locally (the notebook needs scipy and marimo, which live in the dev
192
+ group — the library itself stays zero-dependency):
193
+
194
+ ```bash
195
+ uv run --group dev marimo edit examples/scipy_distributions.py
196
+ ```
197
+
198
+ ## Features
199
+
200
+ - **`@distribution` decorator** — all canonical parameterizations come for free
201
+ - **`distparams.stats`** — scipy.stats-shaped namespace where every call accepts any registered parametrization
202
+ - **`distparams.random`** — the stdlib `random` module accepting every registered parametrization (zero-dependency, seed-exact drop-in)
203
+ - **`from_scipy`** — wrap any scipy distribution into a callable that accepts every registered parametrization
204
+ - **Support** dataclass for parameter domains (positive, unit interval, real, etc.)
205
+ — every parameter's description, support, and aliases are reachable via
206
+ `get_distribution(name).params`
207
+ - **Cross-ecosystem conversion** between PyMC, scipy, Stan, jStat, TFP, NumPyro, R (base `stats`), and the stdlib `random` module
208
+ - **Ecosystem vocabularies** — an ecosystem's parameter names are written once per *parametrization*, not once per distribution
209
+ - **Plugin system** for external packages — add an ecosystem, alias, or whole parameterization without forking
210
+ - **Zero dependencies** — pure Python, ~160 KB of source, ~50 ms import, µs-scale conversions
211
+
212
+ ## Why not PreliZ?
213
+
214
+ PreliZ is for prior elicitation — it computes with distributions and brings
215
+ `numba`, `pytensor`, `scipy`, and `matplotlib` with it. distparams is the
216
+ zero-dependency metadata layer underneath: it knows the parameterizations
217
+ (and every ecosystem's names for them) so your library doesn't have to.
218
+
219
+ ## Use it in your own package
220
+
221
+ Wrap your constructor once and every ecosystem's parameter names — plus
222
+ alternative parameterizations from the registry — just work:
223
+
224
+ ```python
225
+ from distparams import distribution
226
+
227
+
228
+ @distribution("beta")
229
+ def beta(alpha, beta):
230
+ return stats.beta(alpha, beta)
231
+
232
+
233
+ beta(1, 1) # canonical, positional
234
+ beta(successes=2, failures=3) # → alpha=3, beta=4
235
+ ```
236
+
237
+ Your function's parameter names are honored exactly — declare them in
238
+ whatever convention your library uses:
239
+
240
+ ```python
241
+ @distribution("beta")
242
+ def beta(a, b): # scipy-style names
243
+ return stats.beta(a, b)
244
+
245
+
246
+ beta(alpha=1, beta=2) # → a=1, b=2
247
+ beta(successes=2, failures=3) # → a=3, b=4
248
+ ```
249
+
250
+ Prefer to keep your dataclasses? The decorator works on them directly — and
251
+ preserves the class (isinstance, classmethods, `dataclasses.fields` all keep
252
+ working):
253
+
254
+ ```python
255
+ from dataclasses import dataclass
256
+ from distparams import distribution
257
+
258
+
259
+ @distribution("beta")
260
+ @dataclass
261
+ class Beta:
262
+ alpha: float
263
+ beta: float
264
+
265
+
266
+ Beta(a=1, b=2) # → Beta(alpha=1, beta=2)
267
+ Beta(successes=2, failures=3) # → Beta(alpha=3, beta=4)
268
+ ```
269
+
270
+ PyMC-style `.dist()` classmethods work too — decorate the raw function below
271
+ `@classmethod`, and the bound `cls` is passed through untouched. Context
272
+ parameters ride along: anything the callable declares that the registry
273
+ doesn't know — plus everything in `**kwargs` — is forwarded untouched.
274
+ Unknown kwargs without `**kwargs` stay strict errors.
275
+
276
+ ```python
277
+ class Normal:
278
+ @classmethod
279
+ @distribution("normal")
280
+ def dist(cls, mu=0, sigma=1, **kwargs): ...
281
+
282
+
283
+ Normal.dist(loc=0, scale=1, size=(2, 3)) # ✓ size/dtype-style extras forward
284
+ ```
285
+
286
+ Note that PyMC's `.dist()` only accepts extras like `size`, `shape`, and
287
+ `dtype` — `observed`, `dims`, and `initval` belong to the constructor in
288
+ model context (PyMC itself raises on them in `.dist()`). Registry-known
289
+ names always resolve: for gamma, `shape` is the shape parameter, not array
290
+ shape.
291
+
292
+ And downstream packages can extend the registry — names, aliases,
293
+ parameterizations — without forking, via `distparams.plugins`. To teach every
294
+ distribution your package's parameter names for a parametrization, register a
295
+ vocabulary once:
296
+
297
+ ```python
298
+ from distparams import register_ecosystem_vocabulary
299
+
300
+ register_ecosystem_vocabulary("my_pkg", "mu_sigma", {"center": "mu", "spread": "sigma"})
301
+ # every registered mu_sigma distribution now speaks my_pkg
302
+
303
+ convert("normal", "scipy", "my_pkg", loc=0, scale=1) # {"center": 0, "spread": 1}
304
+ convert("cauchy", "my_pkg", "scipy", center=1, spread=2) # {"loc": 1, "scale": 2}
305
+ ```
306
+
307
+ A single distribution that names parameters differently keeps a
308
+ per-distribution override via `register_ecosystem_mapping` (it takes
309
+ precedence over the vocabulary row).
310
+
311
+ Rows are **pure renames** — they change what a parameter is called, never its
312
+ value. In the example above `spread` is an sd, so the rename alone is correct.
313
+ If `spread` carried a different quantity — a variance — the row alone would
314
+ silently pass values through (`spread=4` would convert to `scale=4`). Value
315
+ transforms live on the parameter, declared once and inherited by every
316
+ ecosystem that maps a name onto that role:
317
+
318
+ ```python
319
+ from distparams import Bijection, register_parameter_alias
320
+
321
+ register_parameter_alias("normal", "sigma", "spread", Bijection.power(0.5))
322
+
323
+ convert("normal", "my_pkg", "scipy", center=0, spread=4) # {"loc": 0, "scale": 2.0}
324
+ ```
325
+
326
+ ## The `@distribution` Decorator
327
+
328
+ The flagship feature. Wrap your distribution constructor once and every
329
+ ecosystem's parameter names just work — aliases and alternative
330
+ parameterizations resolve automatically from the registry.
331
+
332
+ All of these are the *same* Normal:
333
+
334
+ ```python
335
+ from distparams import distribution
336
+
337
+ # Register the distribution first (see registration below),
338
+ # then define your constructor against the canonical parameters.
339
+
340
+
341
+ @distribution("normal", canonical=["mu", "sigma"])
342
+ def normal(mu, sigma):
343
+ return stats.norm(loc=mu, scale=sigma)
344
+
345
+
346
+ normal(mu=0, sigma=1) # ✓ canonical (Gaussian notation)
347
+ normal(mu=0, tau=4) # ✓ tau -> sigma (precision parameterization)
348
+ normal(mu=0, precision=4) # ✓ precision -> sigma (same as tau)
349
+ normal(mu=0, variance=4) # ✓ variance -> sigma
350
+ normal(mu=0, var=4) # ✓ var -> sigma (same as variance)
351
+ normal(loc=0, scale=1) # ✓ scipy notation
352
+ normal(mu=0, std=1) # ✓ your own aliases
353
+ ```
354
+
355
+ `Poisson(mu=...)` and `Poisson(lam=...)` are the same parameter — the
356
+ decorator resolves them transparently:
357
+
358
+ ```python
359
+ @distribution("poisson", canonical=["rate"])
360
+ def poisson(rate):
361
+ return stats.poisson(mu=rate)
362
+
363
+
364
+ poisson(mu=5) # ✓ mean parameterization
365
+ poisson(lam=5) # ✓ rate/lambda parameterization
366
+ poisson(rate=5) # ✓ canonical
367
+ ```
368
+
369
+ And entire *parameterizations* (not just names) come for free too — a Gamma
370
+ written against `(shape, rate)` happily accepts `(shape, scale)`, `(a, scale)`,
371
+ `(mu, sigma)`, or `(mu, variance)`:
372
+
373
+ ```python
374
+ @distribution("gamma", canonical=["shape", "rate"])
375
+ def gamma(shape, rate):
376
+ return stats.gamma(a=shape, scale=1 / rate)
377
+
378
+
379
+ gamma(shape=2, rate=3) # ✓ canonical
380
+ gamma(shape=2, scale=3) # ✓ scale -> rate = 1/3
381
+ gamma(a=2, scale=3) # ✓ scipy notation
382
+ gamma(mu=2, sigma=2) # ✓ mean/std parameterization
383
+ gamma(mu=2, variance=4) # ✓ mean/variance (same as sigma=2; var= works too)
384
+ ```
385
+
386
+ Conflicting or unknown parameters are caught at call time instead of
387
+ silently misbehaving:
388
+
389
+ ```python
390
+ normal(mu=0, sigma=1, tau=4) # ✗ ValueError: both map to 'sigma'
391
+ normal(mu=0, foo=1) # ✗ ValueError: unknown parameter 'foo'
392
+ ```
393
+
394
+ To wire this up, define the distribution once (canonical parameters and
395
+ every ecosystem's naming). The fluent authoring API makes this read like
396
+ the math:
397
+
398
+ ```python
399
+ from distparams import Bijection, Distribution, NamingConvention, Parameter, Support, register_distribution
400
+
401
+ # your own distribution's name — "normal" here for familiarity (the built-in
402
+ # is already registered; a fresh name avoids the duplicate-registration error)
403
+ mu = Parameter("mu", "Location parameter", Support.real())
404
+ mu.alias("mean")
405
+
406
+ sigma = Parameter("sigma", "Scale parameter", Support.positive())
407
+ sigma.alias("scale").alias("std")
408
+ sigma.alternative("tau", Bijection.power(-0.5)) # precision → sigma
409
+
410
+ register_distribution(
411
+ Distribution(
412
+ "normal",
413
+ params={"mu": mu, "sigma": sigma}, # canonical name derived: "mu_sigma"
414
+ naming_conventions=[
415
+ NamingConvention("pymc", {"mu": "mu", "sigma": "sigma"}),
416
+ NamingConvention("scipy", {"loc": "mu", "scale": "sigma"}),
417
+ NamingConvention("jstat", {"mean": "mu", "std": "sigma"}),
418
+ ],
419
+ )
420
+ )
421
+ ```
422
+
423
+ Ecosystems that share a parametrization are covered once by the built-in
424
+ vocabularies (`src/distparams/_vocabularies.py`) — per-distribution
425
+ conventions are only needed for genuine exceptions, and the parametrization
426
+ name is derived from the parameter declaration order.
427
+
428
+ ## Typing
429
+
430
+ The decorator preserves your function's signature, so canonical calls are
431
+ checked by mypy, pyright, and ty — and IDE autocomplete works on parameter
432
+ names and types:
433
+
434
+ ```python
435
+ @distribution("normal")
436
+ def normal(mu: float, sigma: float) -> float: ...
437
+
438
+
439
+ normal(mu=0, sigma=1) # ✓ fully type-checked, like a plain function
440
+ normal(0, 1) # ✓ positional works too
441
+ ```
442
+
443
+ Alias calls resolve at runtime from the registry, which is beyond what any
444
+ static analyzer can see — a strict checker will flag them even though they
445
+ run correctly:
446
+
447
+ ```python
448
+ normal(loc=0, scale=1) # ✓ runs fine, but checkers see unknown names
449
+ ```
450
+
451
+ If you want alias resolution to be statically visible, resolve first and
452
+ unpack the result — `resolve_parameters` returns canonical parameters:
453
+
454
+ ```python
455
+ params = resolve_parameters("normal", loc=0, scale=1) # {"mu": 0, "sigma": 1}
456
+ normal(**params)
457
+ ```
458
+
459
+ Rule of thumb: **canonical names are the typed calling convention; aliases
460
+ are a runtime convenience.** Typos in alias names are caught at call time with
461
+ a `ValueError` listing every valid name, so nothing fails silently.
462
+
463
+ ## Defaults and alternative parameterizations
464
+
465
+ Your function's own defaults do the heavy lifting — supply what you know,
466
+ let the rest default:
467
+
468
+ ```python
469
+ @distribution("normal")
470
+ def normal(mu=0, sigma=1):
471
+ return stats.norm(mu, sigma)
472
+
473
+
474
+ normal() # mu=0, sigma=1 (pure defaults)
475
+ normal(sigma=2) # mu=0, sigma=2 (mu by default)
476
+ normal(tau=4) # mu=0, sigma=0.5 (precision → sigma)
477
+ normal(precision=4) # mu=0, sigma=0.5 (same as tau)
478
+ normal(variance=4) # mu=0, sigma=2 (variance → sigma)
479
+ normal(var=4) # mu=0, sigma=2 (same as variance)
480
+ normal(mean=0, std=1) # jStat-style names work too
481
+ ```
482
+
483
+ One name per parameter: `normal(sigma=2, tau=4)` raises a conflict — both
484
+ map to sigma — rather than guessing which wins. Values are transformed,
485
+ not guessed: `tau=4` means `sigma=0.5`, `tau=2` means `sigma≈0.707`.
486
+
487
+ An unknown parameter names the distribution, lists every valid name grouped
488
+ by role, and suggests the closest match for typos:
489
+
490
+ ```python
491
+ normal(precission=4)
492
+ # Unknown parameter 'precission' for distribution 'normal'.
493
+ # Known parameters: (mu or loc or mean) and
494
+ # (sigma or scale or std or tau or precision or variance or var or sd).
495
+ # Did you mean 'precision'?
496
+ ```