Skip to content

Fix dens(): honour p.rope and fail loudly on degenerate posteriors#3

Merged
mjuez merged 1 commit into
mainfrom
fix/mjg/dens-rope-and-var0
May 4, 2026
Merged

Fix dens(): honour p.rope and fail loudly on degenerate posteriors#3
mjuez merged 1 commit into
mainfrom
fix/mjg/dens-rope-and-var0

Conversation

@mjuez
Copy link
Copy Markdown
Owner

@mjuez mjuez commented May 4, 2026

Summary

Two bugs found while helping a user diagnose a confusing dens() plot with LOOCV:

  1. dens() ignored p.rope for the orange ROPE axvlines. They were hardcoded at ±0.01. If the user passed rope=0.05 or rope=0.001 to CorrelatedTTest, the plot showed lines at ±0.01 while posterior.probs() was correctly computed against the real rope — silently misleading.
  2. dens() crashed with ValueError: Axis limits cannot be NaN or Inf from inside matplotlib when the posterior was degenerate (var=0, typical when the two score arrays are identical). The user got a numpy/matplotlib stack trace with no hint of the cause.

Fixes

axvline honours p.rope

```diff

  • 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)
    ```

Explicit error on degenerate posterior

```python
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."
)
```

The check covers var=0, var<0 (shouldn't happen but defensive), and var=NaN. Users get an actionable message instead of a leaked traceback from matplotlib.

Tests

  • TestRopeAxvline — parametrised over rope ∈ {0.001, 0.01, 0.05}, asserts the two ROPE axvlines sit at exactly ±rope.
  • TestDegeneratePosterior — asserts ValueError (with \"degenerate\" in the message) when the posterior has var=0.

Compatibility

  • No public API changes. dens() still accepts the same arguments and returns the same figure object.
  • Behaviour change is strictly an improvement: same input → correct visual output (case 1), degenerate input → clear error instead of confusing traceback (case 2).
  • Existing pytest-mpl baselines reproduce identically because the original tests use rope=0.01, which is what was hardcoded — the fix is a no-op for that case.

Bumped to 1.2.1.

Test plan

  • pytest --mpl — 23/23 passing locally.
  • python -m build — builds wheel + sdist.
  • twine check dist/* — PASSED.
  • CI matrix (will run on this PR).
  • After merge: tag v1.2.1 → publish to PyPI via the existing release workflow.

🤖 Generated with Claude Code

Two bugs found while diagnosing a user-reported plotting glitch with
LOOCV.

1. dens() drew the orange ROPE axvlines at hardcoded +/-0.01 regardless
   of the posterior's `rope` parameter. Users passing rope=0.05 or
   rope=0.001 saw lines that did not match the rope used in the test —
   silently misleading the eye while p.probs() was based on the real
   value. Now the axvlines use p.rope.

2. _add_posterior() blew up with `ValueError: Axis limits cannot be NaN
   or Inf` from matplotlib when the posterior was degenerate (var=0,
   typical when the two input score arrays are identical). Replaced
   with an explicit ValueError up front explaining the cause, instead
   of leaking the numpy/matplotlib stack trace.

Tests
-----

* Parametrised test that the two ROPE axvlines fall at +/-rope for
  rope in {0.001, 0.01, 0.05}.
* Test that dens() raises ValueError on a degenerate (var=0) posterior.

Bump 1.2.0 -> 1.2.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mjuez mjuez merged commit 35fa9bf into main May 4, 2026
7 checks passed
@mjuez mjuez deleted the fix/mjg/dens-rope-and-var0 branch May 4, 2026 15:51
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.

1 participant