Skip to content

Triangular variance cancels catastrophically for offset bounds: Triangular(1e9, 1e9+1, 1e9+2).Variance = 0, and at 1e12 it is −37,282,702 (expected 0.1667) #49

Description

@matt-edmondson

What's wrong

TriangularDistributionProvider.Variance (Essentials.DistributionProviders.Triangular/TriangularDistributionProvider.cs ~L64) evaluates (a² + b² + c² − ab − ac − bc) / 18 on the raw bounds. When the bounds are large relative to their spread, terms around 1e18 cancel down to an O(1) result, and every significant digit is lost.

Repro

new TriangularDistributionProvider(1e9,  1e9 + 1,  1e9 + 2).Variance;  // 0            expected 0.16666666666666666
new TriangularDistributionProvider(1e12, 1e12 + 1, 1e12 + 2).Variance; // -37282702.2  expected 0.1667
new TriangularDistributionProvider(1e8,  1e8 + 1,  1e8 + 3).Variance;  // 0.2222       expected 0.3889

Why it matters

Bounds like these are ordinary: millisecond timestamps, prices in cents, file offsets. A negative variance makes the default StandardDeviation (Math.Sqrt) return NaN.

Suggested fix

The provider already keeps the widths. Use the shift-invariant form, which is algebraically identical:

Variance = (lowerWidth² + upperWidth² + lowerWidth·upperWidth) / 18

with lowerWidth = Mode − Minimum and upperWidth = Maximum − Mode.

Optionally, compute Mean as Minimum + (lowerWidth + (Maximum − Minimum)) / 3 to avoid overflow near ±double.MaxValue.

Acceptance: a test that variance is identical, within a few ulps, for (0, 1, 2) and (1e12, 1e12+1, 1e12+2).

Activity

  1. matt-edmondson commented on Sep 28, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    • Category: Bug
    • Priority: Medium. For ordinary offset bounds (timestamps, prices in cents) Variance is 0 or even negative, which turns StandardDeviation into NaN. The fix is a one-line algebraic rewrite.
    • Area / suggested owner: Essentials distributions, TriangularDistributionProvider.Variance
    • Duplicates / in progress: None, and no open PR.

    Next step: Compute (lw² + uw² + lw·uw) / 18 from the stored widths, optionally rewrite Mean from Minimum, and add the shift-invariance test.


    Generated by Claude Code

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

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions