Skip to content

Add percentile-bootstrap confidence intervals for aggregated metrics - #62

Open
ipezygj wants to merge 4 commits into
Project-MONAI:mainfrom
ipezygj:feat/bootstrap-ci-for-aggregates
Open

Add percentile-bootstrap confidence intervals for aggregated metrics#62
ipezygj wants to merge 4 commits into
Project-MONAI:mainfrom
ipezygj:feat/bootstrap-ci-for-aggregates

Conversation

@ipezygj

@ipezygj ipezygj commented Aug 8, 2026

Copy link
Copy Markdown

Why

A mean metric over N cases is a point estimate: a mean DSC of 0.85 over 20 cases and over 2,000 cases support very different conclusions, and validation studies routinely compare methods whose intervals overlap entirely. The Metrics Reloaded framework's recommendations call for reporting variability alongside aggregated performance — the library computes the aggregates but currently reports no uncertainty for them (no confidence interval or bootstrap machinery in the package).

What

  • MetricsReloaded/utility/uncertainty.pypercentile_bootstrap_ci(values): percentile-bootstrap CI for the mean of per-case metric values. NaN-ignored (consistent with the package's masked aggregations), seeded by default so reported intervals are reproducible run-to-run. And stats_with_ci(df): a describe() summary augmented with ci95_low / ci95_high rows per metric column.
  • ProcessEvaluation.get_stats_res() now produces stats_all with the interval rows appended. Columns are unchanged, so existing consumers (incl. test_overall_process's column assertion) are unaffected — the change is additive rows only.
  • Tests (test/test_utility/test_uncertainty.py): interval brackets the mean, determinism under the default seed, narrowing with N, agreement with the analytic normal interval on Gaussian data, NaN handling, degenerate inputs. 6/6 passing; the full suite shows the same 3 pre-existing failures as the untouched base on my environment (numpy/scipy drift, unrelated).

Context

I work on eval-integrity tooling (statistical checks for benchmark and leaderboard claims — e.g. a published audit showing a clinical AUC of 0.978 corresponds to ~12% PPV at 1% prevalence, which is a Metrics Reloaded pitfall measured in the wild). Happy to extend this to per-label stats, paired method-comparison intervals, or whatever shape fits the framework's roadmap best.

Summary by CodeRabbit

  • New Features

    • Added percentile-bootstrap confidence intervals to metric summaries.
    • Summary statistics now include confidence-interval rows alongside descriptive statistics.
    • Bootstrap settings are configurable and reproducible, with support for excluding identifier columns.
    • NaN values are excluded from calculations.
    • Overall evaluation summaries now include uncertainty estimates.
  • Bug Fixes

    • Improved handling of insufficient or degenerate data by returning unavailable intervals instead of errors.
    • Added validation for invalid bootstrap and confidence-level settings.

A mean metric over N cases is a point estimate: a mean DSC of 0.85 over
20 cases and over 2,000 cases support very different conclusions, and
validation studies routinely compare methods whose intervals overlap
entirely. The Metrics Reloaded recommendations call for reporting
variability alongside aggregates.

- utility/uncertainty.py: percentile_bootstrap_ci for the mean of
  per-case values (NaN-ignored, consistent with the package's masked
  aggregations; seeded by default so reported intervals reproduce) and
  stats_with_ci, which appends ci95_low/ci95_high rows to a describe()
  summary
- ProcessEvaluation.get_stats_res now reports stats_all with the
  interval rows; columns unchanged, existing consumers unaffected
- tests: interval brackets the mean, determinism under seed, narrowing
  with N, agreement with the analytic normal interval on Gaussian data,
  NaN handling, degenerate inputs
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0108999b-bdd7-4233-a369-b3031007e65d

📥 Commits

Reviewing files that changed from the base of the PR and between 7150bed and 2bcc4a3.

📒 Files selected for processing (2)
  • MetricsReloaded/utility/uncertainty.py
  • test/test_utility/test_uncertainty.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • MetricsReloaded/utility/uncertainty.py
  • test/test_utility/test_uncertainty.py

Walkthrough

Adds seeded percentile-bootstrap confidence intervals, dataframe summary rows, overall evaluation integration, and tests for interval and summary behavior.

Changes

Uncertainty statistics

Layer / File(s) Summary
Bootstrap utilities and summary statistics
MetricsReloaded/utility/uncertainty.py
Defines seeded percentile-bootstrap intervals and dataframe confidence-interval rows. NaN values are filtered, invalid options raise ValueError, and excluded columns retain descriptive statistics with NaN intervals.
Overall evaluation integration
MetricsReloaded/processes/overall_process.py
Uses stats_with_ci for overall evaluation statistics. Per-label statistics remain unchanged.
Uncertainty behavior tests
test/test_utility/test_uncertainty.py
Tests deterministic intervals, statistical behavior, NaN handling, validation, confidence labels, summary rows, and excluded columns.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProcessEvaluation
  participant stats_with_ci
  participant percentile_bootstrap_ci
  ProcessEvaluation->>stats_with_ci: pass grouped_lab and excluded identifiers
  stats_with_ci->>percentile_bootstrap_ci: compute metric intervals
  percentile_bootstrap_ci-->>stats_with_ci: return confidence bounds
  stats_with_ci-->>ProcessEvaluation: return summary statistics with interval rows
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding percentile-bootstrap confidence intervals for aggregated metrics.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MetricsReloaded/processes/overall_process.py`:
- Line 692: Update the call to stats_with_ci in the surrounding process flow so
identifier columns such as case are excluded from confidence-interval
bootstrapping. Preserve the existing summary column if needed, calculate CI rows
only for metric columns, and leave identifier CI cells empty.

In `@MetricsReloaded/utility/uncertainty.py`:
- Around line 33-59: Update percentile_bootstrap_ci to reject invalid n_boot and
alpha values before resampling, ensuring n_boot is positive and alpha defines a
valid confidence interval with ordered quantiles. Then update the CI row-name
construction near the existing ci98 labeling to derive the label from the full
confidence level (for example, 1 - alpha) rather than truncating or rounding
97.5% to ci98.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54299000-d415-460c-85bb-5fd1bcfc449f

📥 Commits

Reviewing files that changed from the base of the PR and between aa26562 and abcb8ab.

📒 Files selected for processing (3)
  • MetricsReloaded/processes/overall_process.py
  • MetricsReloaded/utility/uncertainty.py
  • test/test_utility/test_uncertainty.py

Comment thread MetricsReloaded/processes/overall_process.py Outdated
Comment thread MetricsReloaded/utility/uncertainty.py
ipezygj added 3 commits August 9, 2026 06:59
…tifier columns

- percentile_bootstrap_ci rejects n_boot < 1 and alpha outside (0, 1)
  instead of failing inside np.quantile or returning inverted bounds
- interval row names preserve fractional confidence levels
  (alpha=0.025 -> ci97.5_low, previously mislabelled ci98)
- stats_with_ci grew an exclude= option; get_stats_res passes the
  identifier columns so 'case'/'index' keep their describe() rows but
  no longer get a meaningless bootstrap interval
- three new tests cover each point
Both bounds are quantiles of the resample distribution, so a single
resample makes them the same number: percentile_bootstrap_ci(values,
n_boot=1) returned (0.5, 0.5) on a sample spanning 0.1 to 0.9. The old
check only asked whether n_boot was positive, so the input that produces
the strongest possible claim -- an interval of exactly zero width -- was
also the cheapest one to pass.

Requires n_boot >= 2 and documents the sharper limit behind it: while
n_boot < 2 / alpha, both bounds are decided by the most extreme draws
alone (fewer than 40 resamples at the default alpha=0.05), so the interval
comes out too narrow rather than merely noisy.
@ipezygj

ipezygj commented Aug 11, 2026

Copy link
Copy Markdown
Author

Pushed 2bcc4a3, closing a hole I left in my own validation.

percentile_bootstrap_ci only checked that n_boot was positive, so n_boot=1 was accepted — and with one resample both bounds are quantiles of a single number, so the function returned a zero-width interval. On a sample spanning 0.1 to 0.9 it reported (0.5000, 0.5000): exact precision, produced by the least evidence the function would accept. For contrast, the same values give a width of 0.15 at n_boot=2 and 0.48 at the default 2000.

Now requires n_boot >= 2, with the sharper limit written into the docstring rather than left implicit: while n_boot < 2 / alpha — fewer than 40 resamples at the default alpha=0.05 — both bounds are decided by the most extreme draws alone, so the interval comes out too narrow rather than merely noisy. That is a documentation matter, not a hard error, since the right minimum depends on the level requested.

The new test asserts the rejection rather than the old value. I checked that it discriminates by reverting the guard and re-running: 9 pass either way, that one fails without it. 10 tests green.

Found this while auditing my other open PRs after a reviewer elsewhere caught the same shape in code of mine — a degenerate sample size that reached a confident number instead of an absent one. Sorry for the extra round on this one.

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