Skip to content

Add eCDF plot to SBC plots#1853

Open
MolinAlexei wants to merge 2 commits intosbi-dev:mainfrom
MolinAlexei:sbc_plots
Open

Add eCDF plot to SBC plots#1853
MolinAlexei wants to merge 2 commits intosbi-dev:mainfrom
MolinAlexei:sbc_plots

Conversation

@MolinAlexei
Copy link
Copy Markdown

I added an option to do eCDF plotting in the sbc_plot function.
This was apparently already done in pull request #1739, but has not been merged with the main branch since.

I used the same formatting as the existing functions used in sbc_plots. The way I compute the ellipse may be wrong, as I just transform the shaded area from the CDF to an eCDF.

@MolinAlexei
Copy link
Copy Markdown
Author

PS : Here is a look of the output plots

image image

@janfb
Copy link
Copy Markdown
Contributor

janfb commented Apr 16, 2026

Thanks a lot for you PR @MolinAlexei !

We will have will have a look at it soon.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 95.55556% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.96%. Comparing base (ea71482) to head (fe4ff88).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
sbi/analysis/plot.py 95.55% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1853      +/-   ##
==========================================
+ Coverage   87.94%   87.96%   +0.02%     
==========================================
  Files         140      140              
  Lines       12845    13192     +347     
==========================================
+ Hits        11297    11605     +308     
- Misses       1548     1587      +39     
Flag Coverage Δ
fast 82.71% <95.55%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sbi/analysis/plot.py 77.96% <95.55%> (+0.77%) ⬆️

... and 5 files with indirect coverage changes

@janfb
Copy link
Copy Markdown
Contributor

janfb commented Apr 18, 2026

Code review

Found 2 issues:

  1. Docstring for _plot_ranks_as_ecdf lists two parameters (show_legend, legend_kwargs) that are not in the function signature, and describes show_ylabel as toggling a "counts" y-label while the function actually sets plt.ylabel("empirical CDF - expected CDF"). These look copy-pasted from _plot_ranks_as_hist/_plot_ranks_as_cdf and should be cleaned up.

sbi/sbi/analysis/plot.py

Lines 2039 to 2056 in 9f1a6f1

) -> None:
"""Plot ranks as a delta of the empirical CDFs to the expected CDF
Args:
ranks: SBC ranks in shape (num_sbc_runs, )
num_bins: number of bins for the histogram, recommendation is num_sbc_runs / 20.
num_repeats: number of repeats of each CDF step, i.e., resolution of the eCDF.
ranks_label: label for the ranks, e.g., when comparing ranks of different
methods.
xlabel: label for the current parameter
color: line color for the cdf.
alpha: line transparency.
show_ylabel: whether to show y-label "counts".
show_legend: whether to show the legend, e.g., when comparing multiple ranks.
num_ticks: number of ticks on the x-axis.
legend_kwargs: kwargs for the legend.
"""

  1. The new "ecdf" plot type adds ~60 lines of plotting logic (_plot_ranks_as_ecdf, _plot_ecdf_region_expected_under_uniformity) with no corresponding test. Existing test_sbc_rank_plot parametrizes over ("hist", "cdf") and is not extended here.

sbi/sbi/analysis/plot.py

Lines 2028 to 2087 in 9f1a6f1

def _plot_ranks_as_ecdf(
ranks: np.ndarray,
num_bins: int,
num_repeats: int,
num_sbc_runs: int,
ranks_label: Optional[str] = None,
xlabel: Optional[str] = None,
color: Optional[str] = None,
alpha: float = 0.8,
show_ylabel: bool = True,
num_ticks: int = 3,
) -> None:
"""Plot ranks as a delta of the empirical CDFs to the expected CDF
Args:
ranks: SBC ranks in shape (num_sbc_runs, )
num_bins: number of bins for the histogram, recommendation is num_sbc_runs / 20.
num_repeats: number of repeats of each CDF step, i.e., resolution of the eCDF.
ranks_label: label for the ranks, e.g., when comparing ranks of different
methods.
xlabel: label for the current parameter
color: line color for the cdf.
alpha: line transparency.
show_ylabel: whether to show y-label "counts".
show_legend: whether to show the legend, e.g., when comparing multiple ranks.
num_ticks: number of ticks on the x-axis.
legend_kwargs: kwargs for the legend.
"""
# Construct uniform histogram.
uni_bins = binom(num_sbc_runs, p=1 / (num_bins)).ppf(0.5) * np.ones(num_bins)
uni_bins_cdf = uni_bins.cumsum() / uni_bins.sum()
# Compute the mean to substract to all cdfs
means = [binom(num_sbc_runs, p=p).ppf(0.5) for p in uni_bins_cdf]
means_norm = means / np.max(means)
# Generate histogram of ranks.
hist, *_ = np.histogram(ranks, bins=num_bins, density=False)
# Construct empirical CDF, don't include last bin because it is 1 by default
histcs = hist.cumsum()
histcs_norm = histcs / histcs.max()
# Plot cdf and repeat each stair step
plt.plot(
np.linspace(0, 1, num_repeats * (num_bins - 1)),
np.repeat(histcs_norm[:-1] - means_norm[:-1], num_repeats),
label=ranks_label,
color=color,
alpha=alpha,
)
if show_ylabel:
plt.ylabel("empirical CDF - expected CDF")
plt.xlim(0, 1)
plt.xticks(np.linspace(0, 1, num_ticks))
plt.xlabel("posterior rank" if xlabel is None else xlabel)

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Copy link
Copy Markdown
Contributor

@janfb janfb left a comment

Choose a reason for hiding this comment

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

looks good, thanks @MolinAlexei!

Please check the automated PR comment above, plus a couple of suggestions inline below, e.g., I suggest to add a ylim as the current default view is a bit too close.

Comment thread sbi/analysis/plot.py Outdated
Comment on lines 1741 to 1744
plot_types = ["hist", "cdf", "ecdf"]
assert plot_type in plot_types, (
"plot type {plot_type} not implemented, use one in {plot_types}."
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ecdf as third option is a bit misleading, I suggest calling it cdf-diff.

Also, can you please change the assert into an if-else that raises a ValueError if the plot_type is wrong?

Comment thread sbi/analysis/plot.py
Comment thread sbi/analysis/plot.py
Comment thread sbi/analysis/plot.py
Comment thread sbi/analysis/plot.py
Comment thread sbi/analysis/plot.py
Comment thread sbi/analysis/plot.py
colors: Optional[List[str]] = None,
fig: Optional[Figure] = None,
ax: Optional[Axes] = None,
figsize: Optional[tuple] = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add ylim kwarg here (see comment below), set the default: ylim: Optional[tuple[float, float]] = (-0.1, 0.1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please add the ylim arg here explicitly, that's the user facing level. in the docstring say that it applies to the cdf-diff only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

here is where we set the default. the downstream functions then require the ylim and have no defaut.

…e cdf-diff plot, and corrected a mistake in the docstring for the plotting function
@MolinAlexei MolinAlexei requested a review from janfb April 20, 2026 14:33
@MolinAlexei
Copy link
Copy Markdown
Author

Hi, I implemented all requested changes in a new commit, let me know if that works for you

Copy link
Copy Markdown
Contributor

@janfb janfb left a comment

Choose a reason for hiding this comment

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

thanks for the updates @MolinAlexei !

I had another look and realized it would be better to set the default at the user-facing function and remove the default in the low-lever private functions. Can you please accept my suggestions and implement the comments? Thanks 🙏

Comment thread sbi/analysis/plot.py
Comment on lines +2090 to +2095
if ylim is not None:
plt.ylim(ylim)
else:
plt.ylim(-0.125, 0.125)

plt.xlim(0, 1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if ylim is not None:
plt.ylim(ylim)
else:
plt.ylim(-0.125, 0.125)
plt.xlim(0, 1)
plt.ylim(*ylim)
plt.xlim(0, 1)

Comment thread sbi/analysis/plot.py
obtained from different methods.
num_bins: number of bins used for binning the ranks, default is
num_sbc_runs / 20.
plot_type: type of SBC plot, histograms ("hist") or empirical cdfs ("cdf").
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please adapt the docstring here as well. please check for other locations that might need updating.

Comment thread sbi/analysis/plot.py
num_ticks: int = 3,
ylim: Optional[tuple[float, float]] = None,
) -> None:
"""Plot ranks as a delta of the empirical CDFs to the expected CDF
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"""Plot ranks as a delta of the empirical CDFs to the expected CDF
"""Plot ranks as a difference of the empirical to the expected CDF

Comment thread sbi/analysis/plot.py
plt.xlabel("posterior rank" if xlabel is None else xlabel)


def _plot_ranks_as_ecdf(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suggest to rename this helper funcation to _plot_ranks_as_cdf_diff

Comment thread sbi/analysis/plot.py
num_cols: int = 4,
params_in_subplots: bool = False,
show_ylabel: bool = False,
ylim: Optional[Tuple[float, float]] = (-0.125, 0.125),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should actually make this required here and have the default at the top, user facing level:

Suggested change
ylim: Optional[Tuple[float, float]] = (-0.125, 0.125),
ylim: Tuple[float, float],

Comment thread sbi/analysis/plot.py
alpha: float = 0.8,
show_ylabel: bool = True,
num_ticks: int = 3,
ylim: Optional[tuple[float, float]] = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should actually make this required here and have the default at the top, user facing level:

Suggested change
ylim: Optional[tuple[float, float]] = None,
ylim: Tuple[float, float],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

then we don't need the if-else anymore, see below.

Comment thread sbi/analysis/plot.py
params_in_subplots: whether to show each parameter in a separate subplot, or
all in one.
show_ylabel: whether to show ylabels and ticks.
ylim: limits on the y-axis
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add, that this applies to the cdf-diff case only

Comment thread sbi/analysis/plot.py
colors: Optional[List[str]] = None,
fig: Optional[Figure] = None,
ax: Optional[Axes] = None,
figsize: Optional[tuple] = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please add the ylim arg here explicitly, that's the user facing level. in the docstring say that it applies to the cdf-diff only

Comment thread sbi/analysis/plot.py
colors: Optional[List[str]] = None,
fig: Optional[Figure] = None,
ax: Optional[Axes] = None,
figsize: Optional[tuple] = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

here is where we set the default. the downstream functions then require the ylim and have no defaut.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants