Skip to content

Commit 5f312ab

Browse files
committed
Add Altair demo tab to landing page (#16)
* Add Altair demo tab to landing page * Enable vegafusion data transformer for large Altair datasets * Fix Altair rendering: vegafusion, vl-convert, and faceted chart sizing
1 parent 28643f9 commit 5f312ab

11 files changed

Lines changed: 314 additions & 159 deletions

AGENTS.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
tidydraws is a tidybayes-inspired data layer for Bayesian visualisation in Python. It extracts MCMC draws from ArviZ 1.0 DataTrees into tidy Polars DataFrames. This file holds the hard rules, signatures, and design rationale for agents working in the repo.
66

7+
* Temporary files should be placed in .scratch/ and ignored by git. Do not commit or push scratch files.
78
---
89

910
## Hard Rules (never violate)
@@ -80,7 +81,6 @@ def compare_draws(
8081

8182
### Core helpers (in `_extract.py`)
8283

83-
- `_parse_var_spec(spec)``("beta", ["groups"])`; raise on malformed specs (`"beta["`, `"beta]"`, `"beta[]"`).
8484
- `_datatree_group_to_df(dt, group)``pl.DataFrame` with chain, draw, and all coord columns.
8585
- `_align_dims(frames)` → inner-join same-dim frames; cross-join different-dim frames with a logged warning.
8686
- `_coerce_to_dataframe(newdata)``pl.DataFrame` from `pl.DataFrame` / `pd.DataFrame`.
@@ -90,8 +90,7 @@ def compare_draws(
9090
## Common Pitfalls
9191

9292
- Returning `pl.LazyFrame` or leaving a `.lazy()` / `.collect()` round-trip in the extraction path — the data layer is eager by design.
93-
- Parser not splitting nested dims on `,` inside brackets.
94-
- Confusing dimension names with coordinate names in xarray.
93+
- Confusing dimension names with coordinate names in xarray — the DataArray already knows its dims, so auto-detection gets this right.
9594
- Forgetting groups are accessed via `.children[group].to_dataset()`.
9695
- Running `pytest`/`python` without `uv run` (wrong environment).
9796

docs/examples/01-parameter_draws.qmd

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ status: stable
66

77
`parameter_draws()` is the entry point for parameter-space plots: it pulls posterior draws out of an ArviZ `DataTree` into a tidy Polars `DataFrame` — one row per `chain × draw × coordinate`, each variable a column. This example starts with simulated observed data, fits a real PyMC model, and then uses the resulting posterior draws for densities, intervals, contrasts, and cross-parameter plots.
88

9-
## The string spec
9+
## Auto-detected dimensions
1010

11-
A spec names a variable and, in brackets, the dimensions to spread over. The bracketed names must match coordinate names in the `DataTree`.
11+
Dimensions are read from the xarray DataArray and spread automatically — no bracket syntax is needed. `chain` and `draw` are the only dimensions that are *not* included in the output columns.
1212

13-
| Spec | Meaning | Rows |
13+
| Variable | Meaning | Rows |
1414
| --- | --- | --- |
1515
| `"sigma"` | scalar parameter | `chain × draw` |
16-
| `"beta[groups]"` | one-dimensional array | `chain × draw × groups` |
17-
| `"intercept[groups]"` | another group-level array | `chain × draw × groups` |
16+
| `"beta"` | one-dimensional array | `chain × draw × groups` |
17+
| `"intercept"` | another group-level array | `chain × draw × groups` |
1818

1919
Request several variables in one call and `tidydraws` joins them: variables sharing a dimension are inner-joined; a scalar is broadcast across an array's dimensions.
2020

@@ -110,7 +110,7 @@ dt
110110
One `parameter_draws()` call extracts group slopes and intercepts from the fitted posterior:
111111

112112
```{python}
113-
beta_df = td.parameter_draws(dt, "beta[groups]", "intercept[groups]")
113+
beta_df = td.parameter_draws(dt, "beta", "intercept")
114114
beta_df.head()
115115
```
116116

@@ -375,7 +375,7 @@ truth_contrasts = truth.filter(pl.col("groups") != reference_group).select(
375375
Request `beta[groups]` and the scalar `sigma` together: `sigma` is broadcast onto every `beta[groups]` row, so you can colour one by the other directly.
376376

377377
```{python}
378-
mixed = td.parameter_draws(dt, "beta[groups]", "sigma")
378+
mixed = td.parameter_draws(dt, "beta", "sigma")
379379
```
380380

381381
::: {.panel-tabset}

docs/examples/02-compare_draws.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ dt
9090
One call stacks both sources:
9191

9292
```{python}
93-
compare = td.compare_draws(dt, "beta[groups]")
93+
compare = td.compare_draws(dt, "beta")
9494
compare.head()
9595
```
9696

docs/examples/04-showcase.qmd

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ One-dimensional `alpha[group]` with string coordinate labels. Exercises `_datat
130130
```{python}
131131
# | cache: true
132132
dt, obs = varying_intercepts(seed=2027)
133-
draws = td.parameter_draws(dt, "alpha[group]")
133+
draws = td.parameter_draws(dt, "alpha")
134134
forest = td.point_interval(draws, "alpha", group_by="group", probs=(0.50, 0.89))
135135
```
136136

@@ -179,7 +179,7 @@ dt, obs = varying_slopes(seed=2028)
179179

180180
### Use tidydraws
181181
```{python}
182-
draws = td.parameter_draws(dt, "beta[group]", "sigma")
182+
draws = td.parameter_draws(dt, "beta", "sigma")
183183
```
184184

185185
### Beta vs sigma scatter
@@ -230,7 +230,7 @@ Two 1-d arrays on the same dimensions: `alpha[group]` and `beta[group]`. `param
230230
```{python}
231231
# | cache: true
232232
dt, obs = varying_both(seed=2029)
233-
draws = td.parameter_draws(dt, "alpha[group]", "beta[group]")
233+
draws = td.parameter_draws(dt, "alpha", "beta")
234234
```
235235

236236
### 2D density by group
@@ -284,7 +284,7 @@ dt, obs = multiple_regression(seed=2030)
284284

285285
### Use tidydraws
286286
```{python}
287-
draws = td.parameter_draws(dt, "b1", "b2", "b3", "alpha[group]")
287+
draws = td.parameter_draws(dt, "b1", "b2", "b3", "alpha")
288288
289289
coefs = pl.concat([
290290
draws.select(pl.col("group"), pl.col("b1").alias("value")).with_columns(
@@ -353,7 +353,7 @@ dt, obs, grid = logistic(seed=2031)
353353

354354
### Use tidydraws
355355
```{python}
356-
draws = td.parameter_draws(dt, "alpha[group]", "beta")
356+
draws = td.parameter_draws(dt, "alpha", "beta")
357357
```
358358

359359
```{python}
@@ -441,7 +441,7 @@ dt, _obs = varying_slopes(seed=2028) # same seed as §3 — already has prior
441441

442442
### Use tidydraws
443443
```{python}
444-
compare = td.compare_draws(dt, "beta[group]", groups=["prior", "posterior"])
444+
compare = td.compare_draws(dt, "beta", groups=["prior", "posterior"])
445445
```
446446

447447
### Overlaid prior and posterior densities

index.qmd

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import plotnine as p9
2828
import seaborn as sns
2929
import seaborn.objects as so
3030
from matplotlib.figure import Figure
31+
import altair as alt
32+
import pandas as pd
3133
import polars as pl
3234
import pymc as pm
3335
import tidydraws as td
@@ -99,9 +101,9 @@ dt.update(prior)
99101
## Use tidydraws
100102

101103
```{python}
102-
beta_df = td.parameter_draws(dt, "beta[groups]")
104+
beta_df = td.parameter_draws(dt, "beta")
103105
beta_summary = td.point_interval(beta_df, "beta", group_by="groups").sort("groups")
104-
compare = td.compare_draws(dt, "beta[groups]")
106+
compare = td.compare_draws(dt, "beta")
105107
pred = td.prediction_draws(dt, newdata=observed, var_name="mu")
106108
pred_summary = td.point_interval(pred, "mu", group_by=["obs_ind", "x", "group"]).sort(
107109
"x"
@@ -231,6 +233,73 @@ fig = Figure(figsize=(8, 6))
231233
232234
fig
233235
```
236+
237+
#### altair
238+
239+
```{python}
240+
# | fig-cap: "Posterior forest plot, density, prior vs posterior, and predictive fit via Altair."
241+
alt.data_transformers.enable("vegafusion")
242+
# Forest plot
243+
alt_forest = (
244+
alt
245+
.layer(
246+
alt
247+
.Chart(beta_summary.to_pandas())
248+
.mark_errorbar(ticks=True)
249+
.encode(x="groups:N", y="beta_lower:Q", y2="beta_upper:Q"),
250+
alt
251+
.Chart(beta_summary.to_pandas())
252+
.mark_point(color="steelblue")
253+
.encode(x="groups:N", y="beta:Q"),
254+
alt
255+
.Chart(pd.DataFrame({"y0": [0]}))
256+
.mark_rule(color="#888888", strokeDash=[4, 4])
257+
.encode(y="y0:Q"),
258+
)
259+
.resolve_scale(y="shared")
260+
.properties(title="Forest plot", width=200, height=200)
261+
)
262+
263+
# Density
264+
alt_density = (
265+
alt
266+
.Chart(beta_df.to_pandas())
267+
.transform_density("beta", groupby=["groups"], as_=["beta", "density"])
268+
.mark_area(opacity=0.5)
269+
.encode(x="beta:Q", y="density:Q", color="groups:N")
270+
.properties(title="Posterior density", width=200, height=200)
271+
)
272+
alt_compare = (
273+
alt
274+
.Chart(compare.to_pandas())
275+
.transform_density("beta", groupby=["groups", "source"], as_=["beta", "density"])
276+
.mark_area(opacity=0.5)
277+
.encode(x="beta:Q", y="density:Q", color="source:N")
278+
.properties(width=95, height=85)
279+
.facet("groups:N", columns=2)
280+
.properties(title="Prior vs posterior")
281+
)
282+
# Predictive fit
283+
alt_pred = (
284+
alt
285+
.layer(
286+
alt
287+
.Chart(pred_summary.to_pandas())
288+
.mark_area(opacity=0.25)
289+
.encode(x="x:Q", y="mu_lower:Q", y2="mu_upper:Q", color="group:N"),
290+
alt
291+
.Chart(pred_summary.to_pandas())
292+
.mark_line()
293+
.encode(x="x:Q", y="mu:Q", color="group:N"),
294+
)
295+
.resolve_scale(y="shared")
296+
.properties(title="Predictive fit", width=200, height=200)
297+
)
298+
299+
(
300+
alt.hconcat(alt_forest, alt_density) & alt.hconcat(alt_compare, alt_pred)
301+
).resolve_scale(color="independent")
302+
```
234303
::::
235304

236305

pyproject.toml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "tidydraws"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
readme = "README.md"
55
license = "MIT"
66
license-files = ["LICENSE"]
@@ -33,6 +33,7 @@ dependencies = [
3333
"xarray",
3434
"numpy",
3535
"pyarrow>=24.0.0",
36+
"vegafusion-python-embed>=1.6",
3637
]
3738

3839
[project.optional-dependencies]
@@ -43,9 +44,16 @@ plotnine = [
4344
"plotnine",
4445
"seaborn",
4546
]
47+
altair = [
48+
"altair>=6.2.2",
49+
"vegafusion>=2.0",
50+
"vegafusion-python-embed>=1.6",
51+
"vl-convert-python>=1.9.0",
52+
]
4653
all = [
4754
"tidydraws[letsplot]",
4855
"tidydraws[plotnine]",
56+
"tidydraws[altair]",
4957
]
5058

5159
dev = [
@@ -57,8 +65,11 @@ dev = [
5765
"ipykernel",
5866
"quarto >=0.1",
5967
"great-docs>=0.14.0",
60-
"lets-plot",
6168
"plotnine",
69+
"altair>=6.2.2",
70+
"vegafusion>=2.0.3",
71+
"vegafusion-python-embed>=1.6",
72+
"vl-convert-python>=1.9.0",
6273
]
6374

6475
[project.urls]
@@ -78,6 +89,9 @@ dev = [
7889
"plotnine",
7990
"pymc>=6.0.1",
8091
"pytest-cov>=7.1.0",
92+
"altair>=6.2.2",
93+
"vegafusion>=2.0.3",
94+
"vl-convert-python>=1.9.0",
8195
]
8296

8397
[tool.ruff]

tests/test_compare_draws.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,10 @@ def synthetic_dt():
8989

9090
def test_compare_draws_basic(synthetic_dt):
9191
# Test basic functionality with default groups
92-
lf = compare_draws(synthetic_dt, "beta[groups]")
92+
lf = compare_draws(synthetic_dt, "beta")
9393
df = lf
9494

95-
# Should have 2 * 5 * 3 * 2 (chains * draws * groups * groups) rows
96-
# since we're comparing posterior and prior
95+
# Should have 2 * 5 * 3 * 2 (chains * draws * groups * sources) rows
9796
assert df.height == 2 * 5 * 3 * 2
9897

9998
# Check that we have the source column
@@ -108,14 +107,12 @@ def test_compare_draws_basic(synthetic_dt):
108107

109108

110109
def test_compare_draws_custom_groups(synthetic_dt):
111-
# Test with custom groups including a custom group
112110
lf = compare_draws(
113-
synthetic_dt, "beta[groups]", groups=["posterior", "prior", "prior_pred"]
111+
synthetic_dt, "beta", groups=["posterior", "prior", "prior_pred"]
114112
)
115113
df = lf
116114

117-
# Should have 2 * 5 * 3 * 3 (chains * draws * groups * groups) rows
118-
assert df.height == 2 * 5 * 3 * 3
115+
# Should have 2 * 5 * 3 * 3 (chains * draws * groups * sources) rows
119116

120117
# Check that we have the source column with correct values
121118
assert "source" in df.columns
@@ -128,11 +125,10 @@ def test_compare_draws_custom_groups(synthetic_dt):
128125

129126
def test_compare_draws_multiple_vars(synthetic_dt):
130127
# Test with multiple variables
131-
lf = compare_draws(synthetic_dt, "beta[groups]", "sigma")
128+
lf = compare_draws(synthetic_dt, "beta", "sigma")
132129
df = lf
133130

134-
# Should have 2 * 5 * 3 * 2 (chains * draws * groups * groups) rows
135-
assert df.height == 2 * 5 * 3 * 2
131+
# Should have 2 * 5 * 3 * 2 (chains * draws * groups * sources) rows
136132

137133
# Check that we have the expected columns
138134
assert "chain" in df.columns
@@ -145,7 +141,7 @@ def test_compare_draws_multiple_vars(synthetic_dt):
145141

146142
def test_compare_draws_custom_group_name(synthetic_dt):
147143
# Test with custom group column name
148-
lf = compare_draws(synthetic_dt, "beta[groups]", group_name="model_type")
144+
lf = compare_draws(synthetic_dt, "beta", group_name="model_type")
149145
df = lf
150146

151147
# Check that we have the custom group column
@@ -155,7 +151,7 @@ def test_compare_draws_custom_group_name(synthetic_dt):
155151

156152
def test_compare_draws_eager_semantics(synthetic_dt):
157153
# Verify return type is pl.DataFrame (eager)
158-
df = compare_draws(synthetic_dt, "beta[groups]")
154+
df = compare_draws(synthetic_dt, "beta")
159155
assert isinstance(df, pl.DataFrame)
160156

161157
# Eager frames expose .height directly
@@ -168,15 +164,14 @@ def test_compare_draws_error_invalid_group(synthetic_dt):
168164
compare_draws(synthetic_dt, "sigma", groups=["nonexistent"])
169165

170166

171-
def test_compare_draws_error_malformed_spec(synthetic_dt):
172-
# Test error handling for malformed spec (should be passed through from parameter_draws)
173-
with pytest.raises(ValueError, match="Malformed variable specification"):
174-
compare_draws(synthetic_dt, "beta[groups")
167+
def test_compare_draws_error_variable_not_found(synthetic_dt):
168+
with pytest.raises(KeyError, match="Variable 'missing' not found"):
169+
compare_draws(synthetic_dt, "missing")
175170

176171

177172
def test_compare_draws_numerical_correctness(synthetic_dt):
178173
# Spot-check data integrity
179-
lf = compare_draws(synthetic_dt, "beta[groups]")
174+
lf = compare_draws(synthetic_dt, "beta")
180175
df = lf
181176

182177
# Check some values from posterior group

0 commit comments

Comments
 (0)