diff --git a/Essentials.DistributionProviders.Binomial/BinomialDistributionProvider.cs b/Essentials.DistributionProviders.Binomial/BinomialDistributionProvider.cs index d856106..3377d9f 100644 --- a/Essentials.DistributionProviders.Binomial/BinomialDistributionProvider.cs +++ b/Essentials.DistributionProviders.Binomial/BinomialDistributionProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.Essentials.DistributionProviders.Binomial; @@ -118,6 +118,21 @@ public double Cdf(int value) : SpecialFunctions.RegularizedIncompleteBeta(Trials - value, value + 1.0, failureProbability); } + /// + public double SurvivalFunction(int value) + { + if (value < 0) + { + return 1.0; + } + + // The upper tail is P(X > k) = I(p; k + 1, n - k). Taking it as one minus the CDF would round + // every tail below about 1e-16 to zero, and those small tails are usually the reason to ask. + return value >= Trials + ? 0.0 + : SpecialFunctions.RegularizedIncompleteBeta(value + 1.0, Trials - value, Probability); + } + /// public int Sample(IRandomProvider random) { diff --git a/Essentials.Tests/DistributionProviderTests.cs b/Essentials.Tests/DistributionProviderTests.cs index 8e00dfc..1545c4a 100644 --- a/Essentials.Tests/DistributionProviderTests.cs +++ b/Essentials.Tests/DistributionProviderTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.Essentials.Tests; @@ -622,6 +622,23 @@ public void Binomial_Stays_Finite_Where_The_Coefficient_Would_Overflow() Assert.AreEqual(1.0, (2.0 * distribution.Cdf(999)) + distribution.Pmf(1000), 1e-11); } + [TestMethod] + public void Binomial_Keeps_Its_Precision_Far_Out_In_The_Upper_Tail() + { + // Both values were computed exactly, as rationals, outside this codebase. One minus the CDF rounds + // the first to zero and keeps only eight digits of the second. + IDiscreteDistribution distribution = new BinomialDistributionProvider(100, 0.01); + Assert.AreEqual(9.576655593219767e-22, distribution.SurvivalFunction(20), 9.576655593219767e-22 * 1e-12); + Assert.AreEqual(6.255518382834089e-09, distribution.SurvivalFunction(10), 6.255518382834089e-09 * 1e-12); + + Assert.AreEqual(1.0, distribution.SurvivalFunction(-1), Tight); + Assert.AreEqual(0.0, distribution.SurvivalFunction(100), Tight); + IDiscreteDistribution never = new BinomialDistributionProvider(5, 0.0); + Assert.AreEqual(0.0, never.SurvivalFunction(0), Tight); + IDiscreteDistribution always = new BinomialDistributionProvider(5, 1.0); + Assert.AreEqual(1.0, always.SurvivalFunction(4), Tight); + } + [TestMethod] public void Poisson_Matches_Values_Summed_Independently() {