modist
Interactive distribution widgets for marimo, in the style
of koaning/wigglystuff. Drag the
density curve to shape a distribution, then feed the params straight into a
distribution constructor with a single splat.
Install
uv add modist # or: uv pip install modist (pip install modist)
The grouped-priors UI (md.ui) additionally requires marimo:
uv add 'modist[marimo]' (or pip install modist[marimo]). The
priors-from-model helpers (md.pymc) additionally require pymc:
uv add 'modist[marimo,pymc]'.
Quickstart
import marimo as mo
import modist as md
w = mo.ui.anywidget(md.Normal())
w
params = w.value # {'mu': ..., 'sigma': ...}
import pymc as pm
dist = pm.Normal.dist(**params) # or pm.Beta / pm.Gamma / pm.StudentT
Priors UI
Allocate a whole set of priors at once with a tabbed panel — one draggable distribution per prior:
import modist as md
priors = {"intercept": md.Normal(), "slope": md.Normal(), "sigma": md.Gamma()}
ui = md.ui.create_tabs(priors)
ui
ui.value # {'intercept': {'mu': ..., 'sigma': ...}, 'sigma': {'alpha': ..., 'beta': ...}, ...}
ui.value re-runs live as you drag, and each prior splats straight into its
constructor: pm.Normal.dist(**ui.value["intercept"]). Use
md.ui.create_tabs(priors, orientation="vertical") for a vertical tab bar,
md.ui.create_tabs(priors, height=260) for shorter panels (the widgets size by
aspect ratio, so a smaller height just narrows them), or
md.ui.create_stack(priors) to show every prior at once.
ui.priors maps each name to a pymc_extras.Prior
object (pip install pymc-extras). Your original distribution instances stay
live as you drag, so symbolic flows built off them
(w.create_variable(...), w.params, w.scipy) keep working.
Requires marimo (modist[marimo]). import modist itself stays marimo-free —
md.ui is imported lazily on first access.
Priors from a PyMC model
Requires
modist[marimo,pymc].md.pymcis imported lazily, so plainimport modistdoesn't pull in pymc.
The same idea, lifted from a built model. md.pymc.create_priors(model)
finds the model's root priors — distributions whose parameters don't depend
on other distributions — replaces each with a draggable widget, compiles a
sampler once, and bundles everything into a Priors panel you can drag, draw
from, and hand straight to inference. The whole loop, from model to
pm.sample, is one short session:
import numpy as np, pymc as pm, modist as md
x = np.random.default_rng(0).normal(size=(50, 3))
y = x @ [1.0, -0.5, 2.0] + np.random.default_rng(1).normal(size=50)
with pm.Model(coords={"covariate": ["retention", "content", "price"]}) as model:
alpha = pm.Normal("alpha", mu=pm.Normal("alpha_mu", sigma=5), sigma=2)
beta = pm.Normal("beta", dims="covariate") # one widget per covariate
sigma = pm.HalfNormal("sigma") # auto-mapped to a Gamma widget
pm.Normal("obs", mu=alpha + x @ beta, sigma=sigma, observed=y)
ui = md.pymc.create_priors(model) # tabs: alpha_mu, sigma, and a per-covariate beta group
ui # drag the density curves to reshape the priors
Then use for prior predictive, etc:
ui.value # live params, ready to splat into pm.*.dist(**p)
ui.draw(1_000) # draws of every model RV, driven by the widgets
ui.draw(1_000, beta_price_mu=1.5) # ... with a named per-parameter override
ui.sample_prior_predictive(1_000) # -> xr.DataTree: prior / prior_predictive groups
Use the set_distributions method in order to define a new PyMC model.
new_model = ui.set_distributions() # the widget families replace the priors
idata = pm.sample(model=new_model) # ordinary pm.sample, ready for arviz
Families
| Widget | Params | Domain | Drag affordances |
|---|---|---|---|
Normal |
mu, sigma |
free | mean line → mu, ±1σ squares → sigma |
Beta |
alpha, beta |
fixed [0, 1] |
mean line → translate, q25/q75 squares → concentrate |
Gamma |
alpha, beta |
edge pinned at 0 | mean line → translate, q25/q75 squares → reshape |
StudentT |
mu, sigma, nu |
free | mean line → mu, q75 square → sigma, tails dial → nu |
StudentT's third parameter is a tails dial: drag it up for fatter tails
(lower nu) or down for thinner tails (higher nu). Because nu has no
natural on-curve landmark, its drag is a separate 1-D slider rather than a
point you move on the density curve.
alpha/beta follow the PyMC / statistics convention
(Gamma's beta is the rate, not scipy's scale). The lazy .scipy and
.pymc adapters map to the right parametrization automatically:
n = md.Normal(mu=2.0, sigma=3.0)
n.scipy # <scipy.stats.norm> via loc=/scale=
n.pymc # pm.Normal.dist(mu=2.0, sigma=3.0)
g = md.Gamma(alpha=2.0, beta=3.0)
g.scipy # scipy.stats.gamma(a=2.0, scale=1/3) -- rate handled for you
w.value is a plain dict of the synced traits, so pm.X.dist(**w.value) works
with no conversion.
Jupyter
The widgets are anywidget/ipywidgets under the hood, so they run in plain
Jupyter too — no marimo required. Just display() the widget and read its
.params (or .scipy) instead of wrapping it in mo.ui.anywidget(...):
import modist as md
from IPython.display import display
w = md.Normal(mu=0, sigma=1)
display(w) # drag the curve to reshape it
w.params # {'mu': ..., 'sigma': ...}
A full walkthrough notebook — all five families, live scipy stats, and a
beta-prior combination example — lives at
demos/jupyter_example.ipynb.
From a checkout:
uv sync --extra dev --extra scipy # installs jupyter, ipykernel, jupytext
make jupyter # opens demos/jupyter_example.ipynb in JupyterLab
make jupyter registers the repo's .venv as a modist kernel, so the
notebook uses exactly the installed packages. Requires a local
JupyterLab (installed alongside jupyter via the
dev extras).
How it works
Each family is its own anywidget class with a small set of synced parameter
traits (no x_min/x_max/n_points). The view — SVG scaffold, pan/zoom,
draggable hit lines, and per-family math — lives in a self-contained ESM module.
Source JS lives in js/ (js/base.js shared scaffold + one family file,
all importing a vendored copy of jStat for
pdf/cdf/quantile math). Anywidget delivers _esm as a Blob URL, which
cannot resolve relative imports, so esbuild
bundles each family (jStat inlined) into the committed src/modist/static/*.js
files — the same pattern wigglystuff uses for its JS-heavy widgets.
Rebuilding the JS
make js # esbuild js/*.js -> src/modist/static/*.js
make js-watch # rebuild on every edit (for anywidget hot-reload dev)
Requires a local esbuild (npm install --no-save esbuild).
Development
make venv # creates .venv with dev deps + esbuild
make test # pytest
npm run test:js # Playwright JS integration probes (headless Chromium)
Acknowledgements
- jStat — JavaScript statistics library (MIT), vendored and bundled for the pdf/cdf/quantile math.
- wigglystuff — the interaction and architecture model (one class per family, prebuilt ESM per class).
Metadata
Release files for modist 0.5.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 | |
|---|---|---|---|
| modist-0.5.0.tar.gz | 137.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| modist-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 280.9 kB
Release files / modist-0.5.0.tar.gz
| Download URL | modist-0.5.0.tar.gz |
|---|---|
| Size | 137.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1339947cc343cc411d5b57ba09b8fe46e5b668687263c161608b13828ab96e1f
|
|
BLAKE2b-256 checksum How to use checksums |
502f569cfb2b3347acfed50fb0ebb93ad24dd8d626b6e64f46d215ef1113e3dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / modist-0.5.0-py3-none-any.whl
| Download URL | modist-0.5.0-py3-none-any.whl |
|---|---|
| Size | 143.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
71937b56290b4234cd9baba2518ad1f1b24c5c43a2ec8b776279604cf850d9b1
|
|
BLAKE2b-256 checksum How to use checksums |
d327044638300b4e6b376211db897ec6005844d9a8d53caebad6a41b0ceb514a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|