Skip to content

Return None from Empirical::variance for a single sample - #468

Open
youdie006 wants to merge 1 commit into
statrs-dev:mainfrom
youdie006:empirical-variance-single-sample
Open

Return None from Empirical::variance for a single sample#468
youdie006 wants to merge 1 commit into
statrs-dev:mainfrom
youdie006:empirical-variance-single-sample

Conversation

@youdie006

@youdie006 youdie006 commented Sep 4, 2026

Copy link
Copy Markdown

Empirical::variance() returns Some(NaN) for a single sample.

n=0   variance=None          std_dev=None
n=1   variance=Some(NaN)     std_dev=Some(NaN)
n=2   variance=Some(2.0)     std_dev=Some(1.4142135623730951)
n=100 variance=Some(0.0)     std_dev=Some(0.0)

src/distribution/empirical.rs:245 guards emptiness while the denominator is n - 1:

fn variance(&self) -> Option<f64> {
    if self.data.is_empty() {
        None
    } else {
        Some(self.var / (self.sum as f64 - 1.))
    }
}

At n == 1 the denominator is zero and self.var is zero too, so it is 0.0 / 0.0. std_dev() is
the default trait method variance().map(f64::sqrt), so the NaN reaches a second public method.

Option is already this crate's channel for "undefined here" - it is what n == 0 returns.

The siblings guard the denominator, not emptiness

  • src/statistics/online.rs:52, OnlineMoments<2>::variance - if self.count < 2 { None }, and its
    doc says so outright: "or None if fewer than two observations have been pushed"
  • src/statistics/online.rs:95, OnlineMoments<3>::variance - the same
  • src/distribution/hypergeometric.rs:332 - if self.population <= 1 { None }

Empirical is the one that checks the wrong thing.

The change

self.data.is_empty() becomes self.sum < 2. The struct's own invariant at empirical.rs:62 -
"Must be 0 iff data.is_empty()" - means the new predicate subsumes the old one, so n == 0 is
unchanged.

Observable change, plainly: Empirical::from_iter([x]).variance() goes from Some(NaN) to
None, and .std_dev() with it. Nothing else moves - n == 0 and n >= 2 are identical, and the
test below pins that from both sides.

Tests

Three lines added to the existing test_var, which covered vec![] and vec![4.0; 100] but never
n == 1. Reverting only the guard:

thread 'distribution::empirical::tests::test_var' panicked at src/distribution/empirical.rs:350:9:
assertion failed: one.variance().is_none()
test result: FAILED. 8 passed; 1 failed

I also mutation-checked the guard itself, because my first draft was not tight enough:

  • self.sum < 1 (the old empty-only guard, restated) -> test_var FAILS
  • self.sum < 3 (over-guarding n == 2) -> test_var FAILS

The second only started failing after I added test_var_for_samples(2.0, vec![1.0, 3.0]) to pin
n == 2 from the other side. 2.0 is exact in binary floating point, so that assertion is not
rounding-direction dependent.

CI gates: cargo fmt -- --check clean, cargo clippy --all-targets -- -D warnings clean,
cargo test gives 833 passed; 0 failed; 2 ignored and 195 passed; 0 failed.

Related

#460 (Triangular::pdf returning NaN when mode equals min) fixed the same 0/0-reaching-the-caller
class in this crate last week.


Disclosure: found and prepared with AI assistance (Claude). Every figure above is from a run on this
branch, and the sibling comparison and mutation checks are mine, not a summary of a tool's output.

Summary by CodeRabbit

  • Bug Fixes
    • Variance and standard deviation are now reported as undefined when a distribution contains fewer than two samples, avoiding invalid single-sample results.
  • Tests
    • Added coverage for single-sample distributions to verify the corrected behavior.

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 785defb4-2cc4-4990-8b3c-01442039cb67

📥 Commits

Reviewing files that changed from the base of the PR and between 50fcf4d and 20481a3.

📒 Files selected for processing (1)
  • src/distribution/empirical.rs

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The empirical distribution now returns undefined variance for fewer than two samples. Tests verify that both variance and standard deviation return None for a single-sample distribution.

Changes

Empirical variance handling

Layer / File(s) Summary
Single-sample variance guard and validation
src/distribution/empirical.rs
variance() returns None when fewer than two samples exist. Tests verify None for variance and standard deviation with one sample.

Estimated code review effort: 2 (Simple) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 77392

Empirical variance and standard deviation now report undefined results for single-sample distributions rather than NaN. The boundary behavior is covered without affecting multi-sample calculations, and no current merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 primary change: Empirical::variance() now returns None for a single sample.
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.

@YeungOnion

YeungOnion commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Thanks for this! I consider it a fix. I expect people will be okay with that.

@youdie006 would you be able to write the commit according to Conventional Commit with a fix: prefix to this commit message?

The guard checked emptiness while the denominator is n - 1, so one sample
gave 0.0 / 0.0. OnlineMoments::variance and Hypergeometric::variance both
guard the denominator instead.
@youdie006
youdie006 force-pushed the empirical-variance-single-sample branch from 20481a3 to 773927f Compare September 6, 2026 23:33
@youdie006

Copy link
Copy Markdown
Author

Done - amended to fix: return None from Empirical::variance for a single sample and force-pushed. Thanks for the quick look.

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