如何有效地计算二项式累积分布函数?

时间:2009-07-08 01:13:36

标签: algorithm math probability binomial-cdf

让我们说我知道“成功”的概率是P.我进行了N次测试,我看到了S的成功。测试类似于抛掷不均匀加权的硬币(也许头部是成功的,尾部是失败的)。

我想知道看到S成功的几率,或者比S成功的可能性要小得多。

因此,例如,如果P为0.3,N为100,并且我获得20次成功,我正在寻找获得20次或更少次成功的概率。

如果,另一方面没有,P是0.3,N是100,我获得40次成功,我正在寻找获得40次成功的可能性。

我知道这个问题与在二项式曲线下找到区域有关:但是:

  1. 我的数学功能无法将这些知识转化为有效的代码
  2. 虽然我理解二项式曲线可以给出精确的结果,但我得到的印象是它本身效率低下。计算近似结果的快速方法就足够了。
  3. 我应该强调这个计算必须很快,理想情况下应该可以通过标准的64位或128位浮点计算来确定。

    我正在寻找一个带P,S和N的函数并返回一个概率。由于我比代码更熟悉数学符号,所以我更倾向于任何答案都使用伪代码或代码。

10 个答案:

答案 0 :(得分:24)

完全二项分布

def factorial(n): 
    if n < 2: return 1
    return reduce(lambda x, y: x*y, xrange(2, int(n)+1))

def prob(s, p, n):
    x = 1.0 - p

    a = n - s
    b = s + 1

    c = a + b - 1

    prob = 0.0

    for j in xrange(a, c + 1):
        prob += factorial(c) / (factorial(j)*factorial(c-j)) \
                * x**j * (1 - x)**(c-j)

    return prob

>>> prob(20, 0.3, 100)
0.016462853241869437

>>> 1-prob(40-1, 0.3, 100)
0.020988576003924564

正常估计,适合大n

import math
def erf(z):
        t = 1.0 / (1.0 + 0.5 * abs(z))
        # use Horner's method
        ans = 1 - t * math.exp( -z*z -  1.26551223 +
                                                t * ( 1.00002368 +
                                                t * ( 0.37409196 + 
                                                t * ( 0.09678418 + 
                                                t * (-0.18628806 + 
                                                t * ( 0.27886807 + 
                                                t * (-1.13520398 + 
                                                t * ( 1.48851587 + 
                                                t * (-0.82215223 + 
                                                t * ( 0.17087277))))))))))
        if z >= 0.0:
                return ans
        else:
                return -ans

def normal_estimate(s, p, n):
    u = n * p
    o = (u * (1-p)) ** 0.5

    return 0.5 * (1 + erf((s-u)/(o*2**0.5)))

>>> normal_estimate(20, 0.3, 100)
0.014548164531920815

>>> 1-normal_estimate(40-1, 0.3, 100)
0.024767304545069813

泊松估计:适用于大n和小p

import math

def poisson(s,p,n):
    L = n*p

    sum = 0
    for i in xrange(0, s+1):
        sum += L**i/factorial(i)

    return sum*math.e**(-L)

>>> poisson(20, 0.3, 100)
0.013411150012837811
>>> 1-poisson(40-1, 0.3, 100)
0.046253037645840323

答案 1 :(得分:6)

我在一个项目中,我们需要能够在没有定义阶乘或伽玛函数的环境中计算二项式CDF。我花了几个星期,但我最终提出了以下算法,该算法准确地计算了CDF(即没有必要的近似值)。 Python基本上和伪代码一样好,对吧?

import numpy as np

def binomial_cdf(x,n,p):
    cdf = 0
    b = 0
    for k in range(x+1):
        if k > 0:
            b += + np.log(n-k+1) - np.log(k) 
        log_pmf_k = b + k * np.log(p) + (n-k) * np.log(1-p)
        cdf += np.exp(log_pmf_k)
    return cdf

性能与x一致。对于较小的x值,此解决方案比scipy.stats.binom.cdf快一个数量级,在x = 10,000左右时具有相似的性能。

我不会完全推导出这种算法,因为stackoverflow不支持MathJax,但它的主旨是首先确定以下等价:

  • 对于所有k&gt; 0,sp.misc.comb(n,k) == np.prod([(n-k+1)/k for k in range(1,k+1)])

我们可以改写为:

  • sp.misc.comb(n,k) == sp.misc.comb(n,k-1) * (n-k+1)/k

或在日志空间中:

  • np.log( sp.misc.comb(n,k) ) == np.log(sp.misc.comb(n,k-1)) + np.log(n-k+1) - np.log(k)

因为CDF是PMF的总和,我们可以使用这个公式从我们计算的系数中计算PMF_ {x = i}的二项式系数(上面函数中的对数为b)对于PMF_ {x = i-1}。这意味着我们可以使用累加器在单个循环内完成所有操作,而且我们不需要计算任何阶乘!

大多数计算在日志空间中完成的原因是为了提高多项式项的数值稳定性,即p^x(1-p)^(1-x)有可能非常大或极小,这可以导致计算错误。

编辑:这是一种新颖的算法吗?在我发布这篇文章之前,我一直在四处寻找,我越来越想知道我是否应该更正式地写这篇文章并将其提交给期刊。

答案 2 :(得分:5)

我想你要评估incomplete beta function

使用“C中的数字配方”第6章:“特殊功能”中的连续分数表示有一个很好的实现。

答案 3 :(得分:4)

我不能完全保证效率,但Scipy有module for this

from scipy.stats.distributions import binom
binom.cdf(successes, attempts, chance_of_success_per_attempt)

答案 4 :(得分:2)

计算机辅助设计中使用的 Bezier曲线域中存在一种有效且更重要的数值稳定算法。它被称为 de Casteljau算法,用于评估用于定义Bezier曲线的 Bernstein多项式

我相信每个答案只允许一个链接,所以从Wikipedia - Bernstein Polynomials

开始

注意二项分布和伯恩斯坦多项式之间非常密切的关系。然后点击de Casteljau算法的链接。

  

让我们说我知道用特定硬币扔头的可能性是P.   我扔的概率是多少   硬币T次,至少得到   S先生?

  • 设置n = T
  • 为i = 0,... S - 1
  • 设置beta [i] = 0
  • 为i = S,... T
  • 设置beta [i] = 1
  • 设置t = p
  • 使用de Casteljau评估B(t)
  

或最多S头?

  • 设置n = T
  • 为i = 0,... S
  • 设置beta [i] = 1
  • 为i = S + 1,... T
  • 设置beta [i] = 0
  • 设置t = p
  • 使用de Casteljau评估B(t)

开源代码可能已经存在。 NURBS曲线(非均匀有理B样条曲线)是贝塞尔曲线的推广,广泛用于CAD。尝试使用openNurbs(许可证非常宽松)或者使用Open CASCADE(一个不太自由和不透明的许可证)失败。这两个工具包都是用C ++编写的,但是存在IIRC,.NET绑定。

答案 5 :(得分:2)

如果您使用的是Python,则无需自行编写代码。 Scipy让你满意:

from scipy.stats import binom
# probability that you get 20 or less successes out of 100, when p=0.3
binom.cdf(20, 100, 0.3)
>>> 0.016462853241869434

# probability that you get exactly 20 successes out of 100, when p=0.3
binom.pmf(20, 100, 0.3)
>>> 0.0075756449257260777

答案 6 :(得分:1)

从你的问题部分“至少获得S头”你想要累积二项分布函数。有关等式,请参阅http://en.wikipedia.org/wiki/Binomial_distribution,该等式被描述为根据“正则化不完全β函数”(已经回答)。如果您只是想在不必自己实现整个解决方案的情况下计算答案,那么GNU Scientific Library提供了以下功能:gsl_cdf_binomial_P和gsl_cdf_binomial_Q。

答案 7 :(得分:1)

DCDFLIB Project具有C#函数(围绕C代码的包装)来评估许多CDF函数,包括二项分布。您可以找到原始的C和FORTRAN代码here。此代码经过了充分测试和准确。

如果您想编写自己的代码以避免依赖外部库,则可以使用其他答案中提到的二项式的常规近似值。以下是how good the approximation is在各种情况下的一些注释。如果你走那条路并需要代码来计算正常的CDF,那么Python code就是这样做的。它只有大约十几行代码,可以很容易地移植到任何其他语言。但是如果你想要高精度和高效的代码,你最好使用像DCDFLIB这样的第三方代码。几个人年来开始制作这个图书馆。

答案 8 :(得分:0)

尝试在GMP中使用的this one。另一个参考是this

答案 9 :(得分:0)

import numpy as np
np.random.seed(1)
x=np.random.binomial(20,0.6,10000) #20 flips of coin,probability of 
                                 heads percentage and 10000 times 
                                  done.
sum(x>12)/len(x)

The output is 41% of times we got 12 heads.