modist 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.
modist-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: modist
3
+ Version: 0.1.0
4
+ Summary: Interactive distribution widgets for marimo, in the style of wigglystuff
5
+ Keywords: marimo,anywidget,statistics,distribution,visualization
6
+ Author: Will Dean
7
+ Author-email: Will Dean <wd60622@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
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 :: Visualization
17
+ Requires-Dist: anywidget>=0.11.0
18
+ Requires-Dist: pytest>=8 ; extra == 'dev'
19
+ Requires-Dist: marimo>=0.9 ; extra == 'dev'
20
+ Requires-Dist: conjugate-models ; extra == 'dev'
21
+ Requires-Dist: pymc>=5.10 ; extra == 'pymc'
22
+ Requires-Dist: scipy>=1.12 ; extra == 'scipy'
23
+ Requires-Python: >=3.12, <3.15
24
+ Project-URL: Documentation, https://github.com/williambdean/modist#readme
25
+ Project-URL: Homepage, https://github.com/williambdean/modist
26
+ Project-URL: Repository, https://github.com/williambdean/modist
27
+ Provides-Extra: dev
28
+ Provides-Extra: pymc
29
+ Provides-Extra: scipy
30
+ Description-Content-Type: text/markdown
31
+
32
+ # `modist`
33
+
34
+ Interactive distribution widgets for [marimo](https://marimo.io), in the style
35
+ of [`koaning/wigglystuff`](https://github.com/koaning/wigglystuff). Drag the
36
+ density curve to shape a distribution, then feed the params straight into a
37
+ distribution constructor with a single splat.
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ uv add modist # or: uv pip install modist (pip install modist)
43
+ ```
44
+
45
+ ## Quickstart
46
+
47
+ ```python
48
+ import marimo as mo
49
+ import modist as md
50
+
51
+ w = mo.ui.anywidget(md.Normal())
52
+ w
53
+ ```
54
+
55
+ ```python
56
+ params = w.value # {'mu': ..., 'sigma': ...}
57
+ ```
58
+
59
+ ```python
60
+ import pymc as pm
61
+ dist = pm.Normal.dist(**params) # or pm.Beta / pm.Gamma
62
+ ```
63
+
64
+ ## Families
65
+
66
+ | Widget | Params | Domain | Drag affordances |
67
+ | ---------------------------- | ------------- | ----------------- | ------------------------- |
68
+ | [`Normal`](src/modist/normal.py) | `mu`, `sigma` | free | mean line → `mu`, ±1σ squares → `sigma` |
69
+ | [`Beta`](src/modist/beta.py) | `alpha`, `beta` | fixed `[0, 1]` | mean line → translate, q25/q75 squares → concentrate |
70
+ | [`Gamma`](src/modist/gamma.py) | `alpha`, `beta` | edge pinned at 0 | mean line → translate, q25/q75 squares → reshape |
71
+
72
+ `alpha`/`beta` follow the [PyMC](https://www.pymc.io) / statistics convention
73
+ (`Gamma`'s `beta` is the **rate**, not scipy's `scale`). The lazy `.scipy` and
74
+ `.pymc` adapters map to the right parametrization automatically:
75
+
76
+ ```python
77
+ n = md.Normal(mu=2.0, sigma=3.0)
78
+ n.scipy # <scipy.stats.norm> via loc=/scale=
79
+ n.pymc # pm.Normal.dist(mu=2.0, sigma=3.0)
80
+
81
+ g = md.Gamma(alpha=2.0, beta=3.0)
82
+ g.scipy # scipy.stats.gamma(a=2.0, scale=1/3) -- rate handled for you
83
+ ```
84
+
85
+ `w.value` is a plain dict of the synced traits, so `pm.X.dist(**w.value)` works
86
+ with no conversion.
87
+
88
+ ## How it works
89
+
90
+ Each family is its own anywidget class with a small set of synced parameter
91
+ traits (no `x_min`/`x_max`/`n_points`). The view — SVG scaffold, pan/zoom,
92
+ draggable hit lines, and per-family math — lives in a self-contained ESM module.
93
+
94
+ Source JS lives in [`js/`](js/) (`js/base.js` shared scaffold + one family file,
95
+ all importing a vendored copy of [jStat](https://jstat.github.io/) for
96
+ `pdf`/`cdf`/quantile math). Anywidget delivers `_esm` as a Blob URL, which
97
+ cannot resolve relative imports, so [esbuild](https://esbuild.github.io)
98
+ bundles each family (jStat inlined) into the committed `src/modist/static/*.js`
99
+ files — the same pattern wigglystuff uses for its JS-heavy widgets.
100
+
101
+ ### Rebuilding the JS
102
+
103
+ ```sh
104
+ make js # esbuild js/*.js -> src/modist/static/*.js
105
+ make js-watch # rebuild on every edit (for anywidget hot-reload dev)
106
+ ```
107
+
108
+ Requires a local esbuild (`npm install --no-save esbuild`).
109
+
110
+ ## Development
111
+
112
+ ```sh
113
+ make venv # creates .venv with dev deps + esbuild
114
+ make test # pytest
115
+ npm run test:js # Playwright JS integration probes (headless Chromium)
116
+ ```
117
+
118
+ ## Acknowledgements
119
+
120
+ - [jStat](https://jstat.github.io/) — JavaScript statistics library (MIT), vendored and bundled for the pdf/cdf/quantile math.
121
+ - [wigglystuff](https://github.com/koaning/wigglystuff) — the interaction and architecture model (one class per family, prebuilt ESM per class).
modist-0.1.0/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # `modist`
2
+
3
+ Interactive distribution widgets for [marimo](https://marimo.io), in the style
4
+ of [`koaning/wigglystuff`](https://github.com/koaning/wigglystuff). Drag the
5
+ density curve to shape a distribution, then feed the params straight into a
6
+ distribution constructor with a single splat.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ uv add modist # or: uv pip install modist (pip install modist)
12
+ ```
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ import marimo as mo
18
+ import modist as md
19
+
20
+ w = mo.ui.anywidget(md.Normal())
21
+ w
22
+ ```
23
+
24
+ ```python
25
+ params = w.value # {'mu': ..., 'sigma': ...}
26
+ ```
27
+
28
+ ```python
29
+ import pymc as pm
30
+ dist = pm.Normal.dist(**params) # or pm.Beta / pm.Gamma
31
+ ```
32
+
33
+ ## Families
34
+
35
+ | Widget | Params | Domain | Drag affordances |
36
+ | ---------------------------- | ------------- | ----------------- | ------------------------- |
37
+ | [`Normal`](src/modist/normal.py) | `mu`, `sigma` | free | mean line → `mu`, ±1σ squares → `sigma` |
38
+ | [`Beta`](src/modist/beta.py) | `alpha`, `beta` | fixed `[0, 1]` | mean line → translate, q25/q75 squares → concentrate |
39
+ | [`Gamma`](src/modist/gamma.py) | `alpha`, `beta` | edge pinned at 0 | mean line → translate, q25/q75 squares → reshape |
40
+
41
+ `alpha`/`beta` follow the [PyMC](https://www.pymc.io) / statistics convention
42
+ (`Gamma`'s `beta` is the **rate**, not scipy's `scale`). The lazy `.scipy` and
43
+ `.pymc` adapters map to the right parametrization automatically:
44
+
45
+ ```python
46
+ n = md.Normal(mu=2.0, sigma=3.0)
47
+ n.scipy # <scipy.stats.norm> via loc=/scale=
48
+ n.pymc # pm.Normal.dist(mu=2.0, sigma=3.0)
49
+
50
+ g = md.Gamma(alpha=2.0, beta=3.0)
51
+ g.scipy # scipy.stats.gamma(a=2.0, scale=1/3) -- rate handled for you
52
+ ```
53
+
54
+ `w.value` is a plain dict of the synced traits, so `pm.X.dist(**w.value)` works
55
+ with no conversion.
56
+
57
+ ## How it works
58
+
59
+ Each family is its own anywidget class with a small set of synced parameter
60
+ traits (no `x_min`/`x_max`/`n_points`). The view — SVG scaffold, pan/zoom,
61
+ draggable hit lines, and per-family math — lives in a self-contained ESM module.
62
+
63
+ Source JS lives in [`js/`](js/) (`js/base.js` shared scaffold + one family file,
64
+ all importing a vendored copy of [jStat](https://jstat.github.io/) for
65
+ `pdf`/`cdf`/quantile math). Anywidget delivers `_esm` as a Blob URL, which
66
+ cannot resolve relative imports, so [esbuild](https://esbuild.github.io)
67
+ bundles each family (jStat inlined) into the committed `src/modist/static/*.js`
68
+ files — the same pattern wigglystuff uses for its JS-heavy widgets.
69
+
70
+ ### Rebuilding the JS
71
+
72
+ ```sh
73
+ make js # esbuild js/*.js -> src/modist/static/*.js
74
+ make js-watch # rebuild on every edit (for anywidget hot-reload dev)
75
+ ```
76
+
77
+ Requires a local esbuild (`npm install --no-save esbuild`).
78
+
79
+ ## Development
80
+
81
+ ```sh
82
+ make venv # creates .venv with dev deps + esbuild
83
+ make test # pytest
84
+ npm run test:js # Playwright JS integration probes (headless Chromium)
85
+ ```
86
+
87
+ ## Acknowledgements
88
+
89
+ - [jStat](https://jstat.github.io/) — JavaScript statistics library (MIT), vendored and bundled for the pdf/cdf/quantile math.
90
+ - [wigglystuff](https://github.com/koaning/wigglystuff) — the interaction and architecture model (one class per family, prebuilt ESM per class).
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "modist"
3
+ version = "0.1.0"
4
+ description = "Interactive distribution widgets for marimo, in the style of wigglystuff"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Will Dean", email = "wd60622@gmail.com" }
8
+ ]
9
+ license = "MIT"
10
+ keywords = [
11
+ "marimo",
12
+ "anywidget",
13
+ "statistics",
14
+ "distribution",
15
+ "visualization",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Scientific/Engineering :: Visualization",
26
+ ]
27
+ requires-python = ">=3.12,<3.15"
28
+ dependencies = ["anywidget>=0.11.0"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/williambdean/modist"
32
+ Repository = "https://github.com/williambdean/modist"
33
+ Documentation = "https://github.com/williambdean/modist#readme"
34
+
35
+ [project.optional-dependencies]
36
+ scipy = ["scipy>=1.12"]
37
+ pymc = ["pymc>=5.10"]
38
+ dev = ["pytest>=8", "marimo>=0.9", "conjugate-models"]
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.8.22,<0.9.0"]
42
+ build-backend = "uv_build"
@@ -0,0 +1,21 @@
1
+ """modist - interactive distribution widgets for marimo.
2
+
3
+ Each widget renders a draggable density curve. The synced parameter traits
4
+ make ``mo.ui.anywidget(w).value`` a dict that splats directly into a
5
+ distribution constructor, e.g. ``pm.Normal.dist(**w.value)``.
6
+
7
+ Families
8
+ --------
9
+ - :class:`Normal` -- ``mu`` / ``sigma``
10
+ - :class:`Beta` -- ``alpha`` / ``beta`` (fixed [0, 1])
11
+ - :class:`Gamma` -- ``alpha`` / ``beta`` (shape / rate, edge at 0)
12
+ """
13
+
14
+ from ._base import DistMixin
15
+ from .beta import Beta
16
+ from .gamma import Gamma
17
+ from .normal import Normal
18
+
19
+ __all__ = ["Normal", "Beta", "Gamma", "DistMixin"]
20
+
21
+ __version__ = "0.1.0"
@@ -0,0 +1,60 @@
1
+ """Shared lazy adapters for modist widgets.
2
+
3
+ Each widget exposes a ``params`` dict of its canonical synced traits plus lazy
4
+ ``.scipy`` and ``.pymc`` attributes that construct a frozen scipy distribution
5
+ or a pymc distribution from those params. Imports happen only on first access.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict
11
+
12
+
13
+ class DistMixin:
14
+ """Provides ``params`` plus lazy ``.scipy`` / ``.pymc`` distribution adapters."""
15
+
16
+ # Family subclasses set these:
17
+ _param_names: tuple[str, ...] = ()
18
+ _dist_name: str = ""
19
+
20
+ @property
21
+ def params(self) -> Dict[str, float]:
22
+ """The canonical parameters of this distribution (the synced traits)."""
23
+ return {name: getattr(self, name) for name in self._param_names}
24
+
25
+ @property
26
+ def scipy(self) -> Any:
27
+ """A frozen ``scipy.stats`` distribution for the current params (lazy import)."""
28
+ from scipy import stats # type: ignore[import-not-found]
29
+
30
+ return self._make_scipy(stats)
31
+
32
+ @property
33
+ def pymc(self) -> Any:
34
+ """A ``pymc`` distribution object from the current params (lazy import)."""
35
+ import pymc as pm # type: ignore[import-not-found]
36
+
37
+ dist = getattr(pm, self._dist_name)
38
+ return dist.dist(**self.params)
39
+
40
+ def create_variable(self, name: str) -> Any:
41
+ """A symbolic pymc distribution whose parameters are named pytensor
42
+ scalars (``{name}_{param}``), ready for ``pm.compile`` with
43
+ ``pytensor.graph.traversal.explicit_graph_inputs``.
44
+
45
+ This is the compiled-input counterpart to :attr:`pymc`/:attr:`params`:
46
+ instead of baking the current values in, each parameter becomes a
47
+ ``pt.scalar(f"{name}_{param}")`` so the graph can be compiled once and
48
+ re-called with new values without rebuilding. E.g.
49
+
50
+ ``w_int.create_variable("intercept")`` gives ``pm.Normal.dist(
51
+ mu=pt.scalar("intercept_mu"), sigma=pt.scalar("intercept_sigma"))``.
52
+ """
53
+ import pymc as pm # type: ignore[import-not-found]
54
+ import pytensor.tensor as pt # type: ignore[import-not-found]
55
+
56
+ kwargs = {p: pt.scalar(f"{name}_{p}") for p in self._param_names}
57
+ return getattr(pm, self._dist_name).dist(**kwargs)
58
+
59
+ def _make_scipy(self, stats: Any) -> Any:
60
+ raise NotImplementedError
@@ -0,0 +1,41 @@
1
+ """Interactive Beta distribution widget on the fixed [0, 1] domain.
2
+
3
+ Drag the mean line to translate (at a fixed concentration) or either ``q25`` /
4
+ ``q75`` square to concentrate / spread out. Synced ``alpha`` / ``beta`` traits
5
+ make ``mo.ui.anywidget(...).value`` splat into ``pm.Beta.dist(**w.value)``.
6
+
7
+ Examples
8
+ --------
9
+ >>> import marimo as mo
10
+ >>> import modist as md
11
+ >>> w = mo.ui.anywidget(md.Beta(alpha=2, beta=2))
12
+ >>> w
13
+ >>> params = w.value # {'alpha': ..., 'beta': ...}
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+
20
+ import anywidget
21
+ import traitlets
22
+
23
+ from ._base import DistMixin
24
+
25
+ _ESM = Path(__file__).parent / "static" / "beta.js"
26
+ _CSS = Path(__file__).parent / "styles.css"
27
+
28
+
29
+ class Beta(DistMixin, anywidget.AnyWidget):
30
+ """An interactive Beta distribution with draggable mean and concentration."""
31
+
32
+ _esm = _ESM
33
+ _css = _CSS
34
+ _param_names = ("alpha", "beta")
35
+ _dist_name = "Beta"
36
+
37
+ alpha = traitlets.Float(2.0).tag(sync=True)
38
+ beta = traitlets.Float(2.0).tag(sync=True)
39
+
40
+ def _make_scipy(self, stats):
41
+ return stats.beta(a=self.alpha, b=self.beta)
@@ -0,0 +1,43 @@
1
+ """Interactive Gamma distribution widget, left edge pinned at 0.
2
+
3
+ Drag the mean line to translate (at a fixed shape) or either ``q25`` / ``q75``
4
+ square to reshape. ``alpha`` is the shape and ``beta`` the rate (pymc / stats
5
+ convention, not the scipy ``scale``). Synced traits make
6
+ ``mo.ui.anywidget(...).value`` splat into ``pm.Gamma.dist(**w.value)``.
7
+
8
+ Examples
9
+ --------
10
+ >>> import marimo as mo
11
+ >>> import modist as md
12
+ >>> w = mo.ui.anywidget(md.Gamma(alpha=2, beta=2))
13
+ >>> w
14
+ >>> params = w.value # {'alpha': ..., 'beta': ...}
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+
21
+ import anywidget
22
+ import traitlets
23
+
24
+ from ._base import DistMixin
25
+
26
+ _ESM = Path(__file__).parent / "static" / "gamma.js"
27
+ _CSS = Path(__file__).parent / "styles.css"
28
+
29
+
30
+ class Gamma(DistMixin, anywidget.AnyWidget):
31
+ """An interactive Gamma distribution with draggable mean and shape."""
32
+
33
+ _esm = _ESM
34
+ _css = _CSS
35
+ _param_names = ("alpha", "beta")
36
+ _dist_name = "Gamma"
37
+
38
+ alpha = traitlets.Float(2.0).tag(sync=True)
39
+ beta = traitlets.Float(2.0).tag(sync=True)
40
+
41
+ def _make_scipy(self, stats):
42
+ # scipy gamma parametrizes by (shape, scale); here beta is the rate.
43
+ return stats.gamma(a=self.alpha, scale=1.0 / self.beta)
@@ -0,0 +1,42 @@
1
+ """Interactive Normal distribution widget.
2
+
3
+ A draggable Normal curve: drag the mean line to reposition, or either of the
4
+ ``\u00b11\u03c3`` squares to reshape the spread. The synced ``mu`` / ``sigma``
5
+ traits make ``mo.ui.anywidget(...).value`` splat directly into a distribution
6
+ constructor, e.g. ``pm.Normal.dist(**w.value)``.
7
+
8
+ Examples
9
+ --------
10
+ >>> import marimo as mo
11
+ >>> import modist as md
12
+ >>> w = mo.ui.anywidget(md.Normal(mu=0, sigma=1))
13
+ >>> w
14
+ >>> params = w.value # {'mu': ..., 'sigma': ...}
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+
21
+ import anywidget
22
+ import traitlets
23
+
24
+ from ._base import DistMixin
25
+
26
+ _ESM = Path(__file__).parent / "static" / "normal.js"
27
+ _CSS = Path(__file__).parent / "styles.css"
28
+
29
+
30
+ class Normal(DistMixin, anywidget.AnyWidget):
31
+ """An interactive Normal distribution with draggable mean and spread."""
32
+
33
+ _esm = _ESM
34
+ _css = _CSS
35
+ _param_names = ("mu", "sigma")
36
+ _dist_name = "Normal"
37
+
38
+ mu = traitlets.Float(0.0).tag(sync=True)
39
+ sigma = traitlets.Float(1.0).tag(sync=True)
40
+
41
+ def _make_scipy(self, stats):
42
+ return stats.norm(loc=self.mu, scale=self.sigma)
File without changes