Metadata-Version: 2.4
Name: modist
Version: 0.3.0
Summary: Interactive distribution widgets for marimo, in the style of wigglystuff
Keywords: marimo,anywidget,statistics,distribution,visualization
Author: Will Dean
Author-email: Will Dean <wd60622@gmail.com>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Dist: anywidget>=0.11.0
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Dist: marimo>=0.9 ; extra == 'dev'
Requires-Dist: conjugate-models ; extra == 'dev'
Requires-Dist: jupyter>=1.0 ; extra == 'dev'
Requires-Dist: ipykernel>=7.3.0 ; extra == 'dev'
Requires-Dist: jupytext>=1.19.5 ; extra == 'dev'
Requires-Dist: marimo>=0.24 ; extra == 'marimo'
Requires-Dist: pymc>=5.10 ; extra == 'pymc'
Requires-Dist: pymc-extras>=0.0.4 ; extra == 'pymc'
Requires-Dist: scipy>=1.12 ; extra == 'scipy'
Requires-Python: >=3.12, <3.15
Project-URL: Documentation, https://github.com/williambdean/modist#readme
Project-URL: Homepage, https://github.com/williambdean/modist
Project-URL: Repository, https://github.com/williambdean/modist
Provides-Extra: dev
Provides-Extra: marimo
Provides-Extra: pymc
Provides-Extra: scipy
Description-Content-Type: text/markdown

# `modist`

Interactive distribution widgets for [marimo](https://marimo.io), in the style
of [`koaning/wigglystuff`](https://github.com/koaning/wigglystuff). Drag the
density curve to shape a distribution, then feed the params straight into a
distribution constructor with a single splat.

![modist widget example](https://raw.githubusercontent.com/williambdean/modist/main/docs/widget-example.png)

## Install

```sh
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

```python
import marimo as mo
import modist as md

w = mo.ui.anywidget(md.Normal())
w
```

```python
params = w.value            # {'mu': ..., 'sigma': ...}
```

```python
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:

```python
import modist as md

priors = {"intercept": md.Normal(), "slope": md.Normal(), "sigma": md.Gamma()}
ui = md.ui.create_tabs(priors)
ui
```

```python
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`](https://github.com/pymc-devs/pymc-extras)
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.pymc` is imported lazily, so plain
> `import modist` doesn'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:

```python
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:

```python
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.

```python
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`](src/modist/normal.py)  | `mu`, `sigma`      | free             | mean line → `mu`, ±1σ squares → `sigma` |
| [`Beta`](src/modist/beta.py)      | `alpha`, `beta`    | fixed `[0, 1]`    | mean line → translate, q25/q75 squares → concentrate |
| [`Gamma`](src/modist/gamma.py)    | `alpha`, `beta`    | edge pinned at 0  | mean line → translate, q25/q75 squares → reshape |
| [`StudentT`](src/modist/studentt.py) | `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](https://www.pymc.io) / statistics convention
(`Gamma`'s `beta` is the **rate**, not scipy's `scale`). The lazy `.scipy` and
`.pymc` adapters map to the right parametrization automatically:

```python
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(...)`:

```python
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`](demos/jupyter_example.ipynb).

From a checkout:

```sh
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](https://jupyter.org/install) (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/) (`js/base.js` shared scaffold + one family file,
all importing a vendored copy of [jStat](https://jstat.github.io/) for
`pdf`/`cdf`/quantile math). Anywidget delivers `_esm` as a Blob URL, which
cannot resolve relative imports, so [esbuild](https://esbuild.github.io)
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

```sh
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

```sh
make venv        # creates .venv with dev deps + esbuild
make test        # pytest
npm run test:js  # Playwright JS integration probes (headless Chromium)
```

## Acknowledgements

- [jStat](https://jstat.github.io/) — JavaScript statistics library (MIT), vendored and bundled for the pdf/cdf/quantile math.
- [wigglystuff](https://github.com/koaning/wigglystuff) — the interaction and architecture model (one class per family, prebuilt ESM per class).
