diff --git a/pyproject.toml b/pyproject.toml index df09cbb..0fb0249 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "baycomp_plotting" -version = "1.2.0" +version = "1.2.1" description = "Extra plotting functionality for baycomp's Bayesian classifier comparison posteriors." readme = "README.md" license = { file = "LICENSE" } diff --git a/src/baycomp_plotting/plotting.py b/src/baycomp_plotting/plotting.py index 7958d25..2777270 100644 --- a/src/baycomp_plotting/plotting.py +++ b/src/baycomp_plotting/plotting.py @@ -90,6 +90,12 @@ def _add_posterior(ax, p, label: str, ls="-", color: str = Color.BLUE) -> None: upper bound) and ``zo`` (z-order counter, decremented for each posterior so earlier ones stay on top). """ + if not (p.var > 0): + raise ValueError( + f"Cannot draw density: posterior variance is {p.var!r}. " + "The two classifiers likely produced identical scores; " + "the posterior is degenerate and has no density to plot." + ) targs = (p.df, p.mean, np.sqrt(p.var)) x = np.linspace( min(stats.t.ppf(0.005, *targs), -1.05 * p.rope), @@ -205,8 +211,8 @@ def dens(p, label: str, ls="-", color: str = Color.BLUE): fig, ax = plt.subplots() fig.patch.set_alpha(0) - ax.axvline(0.01, c="darkorange", linewidth=2, zorder=101) - ax.axvline(-0.01, c="darkorange", linewidth=2, zorder=101) + ax.axvline(p.rope, c="darkorange", linewidth=2, zorder=101) + ax.axvline(-p.rope, c="darkorange", linewidth=2, zorder=101) ax.spines["right"].set_visible(False) ax.spines["top"].set_visible(False) ax.spines["bottom"].set_visible(False) diff --git a/tests/test_plots.py b/tests/test_plots.py index 46f8b1b..d9a8a6d 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -62,3 +62,42 @@ def test_tern_renders_three_vertex_labels(self, signed_rank_posterior): assert any("R" in t for t in text_contents) assert any("ROPE" in t for t in text_contents) plt.close(fig) + + +class TestRopeAxvline: + """The two orange axvlines must reflect the posterior's ``rope`` parameter, + not a hardcoded value. + """ + + @pytest.mark.parametrize("rope", [0.001, 0.01, 0.05]) + def test_axvlines_match_rope(self, rng, rope): + bc = pytest.importorskip("baycomp") + base = rng.normal(0.85, 0.015, 10) + x = base + rng.normal(0.0, 0.005, 10) + y = base + rng.normal(0.015, 0.005, 10) + posterior = bc.CorrelatedTTest(x, y, rope=rope, runs=1) + + fig = bplt.dens(posterior, label="X") + ax = fig.axes[0] + vlines = sorted( + line.get_xdata()[0] for line in ax.lines + if line.get_xdata()[0] == line.get_xdata()[1] # vertical line + ) + # Two of those vertical lines should be at +/- rope + assert pytest.approx(rope, abs=1e-12) in vlines + assert pytest.approx(-rope, abs=1e-12) in vlines + plt.close(fig) + + +class TestDegeneratePosterior: + """``dens`` should fail loudly when the posterior is degenerate (var=0), + instead of crashing on NaN axis limits inside matplotlib. + """ + + def test_var_zero_raises_value_error(self, rng): + bc = pytest.importorskip("baycomp") + identical = (rng.uniform(size=20) < 0.85).astype(float) + posterior = bc.CorrelatedTTest(identical, identical.copy(), rope=0.01, runs=1) + + with pytest.raises(ValueError, match="degenerate"): + bplt.dens(posterior, label="X")