Skip to content

[plotting] Allow user to specify type of posterior data visualisation#680

Open
AllenDowney wants to merge 7 commits into
mainfrom
issue-671-plotting-visualization-types
Open

[plotting] Allow user to specify type of posterior data visualisation#680
AllenDowney wants to merge 7 commits into
mainfrom
issue-671-plotting-visualization-types

Conversation

@AllenDowney

@AllenDowney AllenDowney commented Jan 21, 2026

Copy link
Copy Markdown

Summary

This draft PR extends the plotting capabilities of CausalPy to support multiple visualization types for posterior data. The purpose of this draft PR is to discuss the API design and approach, not to finalize implementation or testing.

Currently, CausalPy only supports CI ribbon visualizations using Highest Density Intervals (HDI). This PR adds:

  • Support for Equal-Tailed Intervals (ETI) in addition to HDI
  • Support for histogram visualizations
  • Support for spaghetti plot visualizations (individual posterior sample trajectories)

The API design aligns with ArviZ's naming conventions (ci_prob, ci_kind) while maintaining backward compatibility with existing code.

Fixes #671

Changes

  • Extended plot_xY() function (causalpy/plot_utils.py):

    • Added kind parameter: "ribbon", "histogram", or "spaghetti" (default: "ribbon")
    • Added ci_kind parameter: "hdi" or "eti" (default: "hdi" to match current behavior)
    • Added ci_prob parameter (default: 0.94 to match current behavior)
    • Added num_samples parameter for spaghetti plots (default: 50)
    • Maintained backward compatibility with hdi_prob parameter
    • Implemented three visualization types:
      • Ribbon: HDI/ETI intervals using ArviZ functions
      • Histogram: Marginal histograms at key time points (basic implementation -- will be extended after API review)
      • Spaghetti: Random posterior sample trajectories with mean overlay
  • Updated BaseExperiment.plot() method (causalpy/experiments/base.py):

    • Added new parameters to method signature
    • Parameters are passed through to _bayesian_plot() and _ols_plot() methods
    • Maintains backward compatibility (all parameters optional with defaults)

Testing

  • Manual testing completed in marimo notebook (causalpy_test.py) demonstrating all visualization types
  • All visualization types verified to render correctly
  • Pre-commit checks pass (linting, formatting, type checking)
  • Note: Unit and integration tests are deferred until after API review and feedback

API Design Rationale

  1. kind parameter: Uses familiar naming convention from seaborn/pandas
  2. ci_prob and ci_kind naming: Aligns with ArviZ's naming conventions for ecosystem consistency
  3. Default values: Match current behavior (0.94, "hdi") to maintain backward compatibility
  4. Backward compatibility: Existing code using hdi_prob continues to work unchanged

Open Questions for Reviewers

  1. Any comments or suggestions on the proposed API?
  2. Should we proceed with updating all experiment classes (_bayesian_plot() methods) to explicitly accept and pass through these parameters, or is passing via **kwargs sufficient?
  3. Are the default values (0.94, "hdi") appropriate, or should we consider ArviZ's new defaults (0.89, "eti")?

Checklist

  • Pre-commit checks pass
  • Code follows project conventions
  • All tests pass (deferred until after API review)
  • Documentation updated (deferred until after API review)
  • Example notebooks created (deferred until after API review)

📚 Documentation preview 📚: https://causalpy--680.org.readthedocs.build/en/680/

@github-actions github-actions Bot added the plotting Improve or fix plotting label Jan 21, 2026
@drbenvincent

Copy link
Copy Markdown
Collaborator

Thanks @AllenDowney ! Hoping to look at this very soon. Just flagging up that there might be some conflicts to resolve if #643 gets merged first. That one allows the user to decide if they are looking at things related to the posterior expectation or the posterior predictive. So these PR's are nicely complementary, but it's possible there could be some code overlap/conflicts.

@drbenvincent drbenvincent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the API direction looks promising. I reviewed this mainly from a maintainability/reviewability perspective and have a few blocking + process suggestions.

Blocking issues

  • CI currently fails early due to import error in causalpy/plot_utils.py:
    • ImportError: cannot import name 'eti' from 'arviz_stats'
  • Because this import fails at module load, reviewers cannot run plotting tests or examples yet.

Requested updates before final review

  1. Fix ETI import/implementation path so test/docs/sdist are green.
  2. Add visual review artifacts to the PR description/comments, since GitHub diff does not show rendered plots:
    • kind="ribbon" with ci_kind="hdi"
    • kind="ribbon" with ci_kind="eti"
    • kind="histogram"
    • kind="spaghetti"
  3. Include one minimal reproducible script/snippet (not notebook-only) to generate mock posterior data and produce all plot kinds. This allows reviewers to validate quickly without rerunning full notebooks.

API/compatibility checks to confirm

  • plot_xY() now returns either (Line2D, PolyCollection) or (list[Line2D], None) depending on kind.
  • Please confirm experiment-level .plot() legend handling remains consistent for non-ribbon kinds, since several call sites still assume tuple-style handles.
  • Please clarify whether hdi_prob deprecation timeline is planned, or if it remains indefinitely as compatibility alias.

Efficient review request

Could you add 3–5 static PNGs (before/after where useful) generated from the same mock posterior dataset and style settings? That will make visual review possible without manual notebook execution.

Suggested mock posterior script for reviewers

import numpy as np
import pandas as pd
import xarray as xr
import matplotlib.pyplot as plt

from causalpy.plot_utils import plot_xY

# Reproducible synthetic posterior: (chain, draw, obs_ind)
rng = np.random.default_rng(42)
n_chains, n_draws, n_t = 2, 200, 40
x = pd.date_range("2022-01-01", periods=n_t, freq="D")

trend = 10 + 0.05 * np.arange(n_t) + 0.01 * np.arange(n_t) ** 2
samples = np.empty((n_chains, n_draws, n_t))
for c in range(n_chains):
    for d in range(n_draws):
        draw_mean = trend + rng.normal(0, 0.4, n_t)
        samples[c, d, :] = draw_mean + rng.normal(0, 0.8, n_t)

Y = xr.DataArray(
    samples,
    dims=["chain", "draw", "obs_ind"],
    coords={"chain": np.arange(n_chains), "draw": np.arange(n_draws), "obs_ind": x},
)

fig, axes = plt.subplots(2, 2, figsize=(12, 8), sharex=True)

plot_xY(x, Y, ax=axes[0, 0], kind="ribbon", ci_kind="hdi", ci_prob=0.94, label="HDI")
axes[0, 0].set_title("Ribbon (HDI)")

plot_xY(x, Y, ax=axes[0, 1], kind="ribbon", ci_kind="eti", ci_prob=0.94, label="ETI")
axes[0, 1].set_title("Ribbon (ETI)")

plot_xY(x, Y, ax=axes[1, 0], kind="histogram", label="Histogram")
axes[1, 0].set_title("Histogram")

plot_xY(x, Y, ax=axes[1, 1], kind="spaghetti", num_samples=60, label="Spaghetti")
axes[1, 1].set_title("Spaghetti")

for ax in axes.ravel():
    ax.legend(loc="best")
    ax.grid(alpha=0.3)

plt.tight_layout()
plt.show()

Conflict check with #643

I checked this directly with a branch-to-branch merge simulation.

There are real merge conflicts between #643 and #680 in these files:

  • causalpy/experiments/base.py
  • causalpy/plot_utils.py
  • causalpy/tests/test_plot_utils.py

So yes, conflict risk is concrete, not just theoretical.

Best sequencing: merge #643 first, then rebase/update #680 on top and re-run tests.

@drbenvincent

Copy link
Copy Markdown
Collaborator

Including images is optional - especially if there's a temp script to generate plots for dev/review purposes.

If the conflicts become gnarly when #643 is merged, then I'm happy to give that a stab - I feel guilty about that kind of thing :)

@drbenvincent

Copy link
Copy Markdown
Collaborator

Thanks for the nudge — adding a minimal reproducible script here to speed visual/API review without needing notebook execution.

This script creates synthetic posterior draws and renders all current plot kinds:

  • kind="ribbon" with ci_kind="hdi"
  • kind="ribbon" with ci_kind="eti"
  • kind="histogram"
  • kind="spaghetti"
import numpy as np
import pandas as pd
import xarray as xr
import matplotlib.pyplot as plt

from causalpy.plot_utils import plot_xY

# Reproducible synthetic posterior: (chain, draw, obs_ind)
rng = np.random.default_rng(42)
n_chains, n_draws, n_t = 2, 200, 40
x = pd.date_range("2022-01-01", periods=n_t, freq="D")

trend = 10 + 0.05 * np.arange(n_t) + 0.01 * np.arange(n_t) ** 2
samples = np.empty((n_chains, n_draws, n_t))
for c in range(n_chains):
    for d in range(n_draws):
        draw_mean = trend + rng.normal(0, 0.4, n_t)
        samples[c, d, :] = draw_mean + rng.normal(0, 0.8, n_t)

Y = xr.DataArray(
    samples,
    dims=["chain", "draw", "obs_ind"],
    coords={"chain": np.arange(n_chains), "draw": np.arange(n_draws), "obs_ind": x},
)

fig, axes = plt.subplots(2, 2, figsize=(12, 8), sharex=True)

plot_xY(x, Y, ax=axes[0, 0], kind="ribbon", ci_kind="hdi", ci_prob=0.94, label="HDI")
axes[0, 0].set_title("Ribbon (HDI)")

plot_xY(x, Y, ax=axes[0, 1], kind="ribbon", ci_kind="eti", ci_prob=0.94, label="ETI")
axes[0, 1].set_title("Ribbon (ETI)")

plot_xY(x, Y, ax=axes[1, 0], kind="histogram", label="Histogram")
axes[1, 0].set_title("Histogram")

plot_xY(x, Y, ax=axes[1, 1], kind="spaghetti", num_samples=60, label="Spaghetti")
axes[1, 1].set_title("Spaghetti")

for ax in axes.ravel():
    ax.legend(loc="best")
    ax.grid(alpha=0.3)

plt.tight_layout()
plt.show()

If useful, I can follow up with static PNGs from this same dataset/style setup.

@drbenvincent

drbenvincent commented Feb 22, 2026

Copy link
Copy Markdown
Collaborator

Looks cool!

all_kinds_grid

drbenvincent added a commit that referenced this pull request Apr 1, 2026
Sync PR #680 with the current main branch so the author is not blocked by stale conflicts or outdated workflow changes. Replace the ETI helper import with a local quantile-based interval calculation so plotting, docs, and package builds keep working against current dependencies.

Made-with: Cursor
@codecov

codecov Bot commented Apr 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.00000% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.65%. Comparing base (deb8774) to head (9397732).
⚠️ Report is 50 commits behind head on main.

Files with missing lines Patch % Lines
causalpy/plot_utils.py 89.65% 7 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #680      +/-   ##
==========================================
+ Coverage   94.60%   94.65%   +0.04%     
==========================================
  Files          80       80              
  Lines       12764    13058     +294     
  Branches      770      796      +26     
==========================================
+ Hits        12076    12360     +284     
- Misses        485      491       +6     
- Partials      203      207       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Add kind parameter: 'ribbon', 'histogram', 'spaghetti'
- Add ci_kind parameter: 'hdi' or 'eti' (default 'hdi')
- Add ci_prob parameter (default 0.94, matching current behavior)
- Add num_samples parameter for spaghetti plots
- Implement ribbon plots with HDI and ETI support
- Implement histogram visualization (basic)
- Implement spaghetti plot visualization
- Maintain backward compatibility with hdi_prob parameter

Addresses #671
- Add histogram visualization (2D heatmap) with global y-bins
- Add spaghetti plot visualization with configurable num_samples
- Add ETI (Equal-Tailed Interval) support in addition to HDI
- Add comprehensive test suite for all visualization types
- Maintain backward compatibility with hdi_prob parameter
- Update BaseExperiment.plot() to pass through new parameters

Addresses #671
@AllenDowney AllenDowney force-pushed the issue-671-plotting-visualization-types branch from 8f3cbe6 to 61a116d Compare April 19, 2026 18:19
- Add scripts/plot_xY_visualization_demo.py (synthetic posterior, 2x2 grid of kinds).
- Clarify plot_hdi_kwargs docstring; use is_datetime64_any_dtype for object dtypes (mypy).
- Extend panel_regression idata type ignores with union-attr for mypy.

Made-with: Cursor
Posterior mean can retain dimensions (e.g. treated_units); float(DataArray)
fails on NumPy 2 / Python 3.14. Reduce with np.asarray(...).mean() before float().

Made-with: Cursor
Add tests for histogram validation, single time point, object-dtype datetime
x, ribbon ETI with fill_kwargs, and custom colormap; omit mock-based ETI
edge case.

Made-with: Cursor
@AllenDowney AllenDowney marked this pull request as ready for review April 20, 2026 13:03
@AllenDowney

Copy link
Copy Markdown
Author

@drbenvincent Ready for review. Please take a look and let me know if there's anything else we should do.

- plot_utils: clarify ribbon vs histogram/spaghetti returns; hdi_prob as
  indefinite compatibility alias for ci_prob.
- BaseExperiment.plot: note when tuple (line, patch) legends apply and how
  legend_kwargs interacts with subclass-built handles.

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plotting Improve or fix plotting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[plotting] Allow user to specify type of posterior data visualisation

2 participants