概率分布#

SciPy 有两种用于处理概率分布的基础设施。本教程是关于较旧的基础设施,它有许多预定义的分布;但是,新的基础设施可以与大多数这些分布一起使用,并且有许多优点。关于新的基础设施,请参阅 随机变量转换指南

已经实现了两个通用的分布类,用于封装连续随机变量离散随机变量。 使用这些类已经实现了 100 多个连续随机变量 (RV) 和 20 个离散随机变量。 关于各个分布的数学参考信息,请参阅连续统计分布离散统计分布

所有统计函数都位于子包 scipy.stats 中,并且从 stats 子包的文档字符串中也可以获得这些函数和可用随机变量的相当完整的列表。

在下面的讨论中,我们主要关注连续 RV。几乎所有内容也适用于离散变量,但我们在此指出一些差异:离散分布的特定点

在下面的代码示例中,我们假设 scipy.stats 包导入为

>>> from scipy import stats

在某些情况下,我们假设单个对象导入为

>>> from scipy.stats import norm

获取帮助#

首先,所有分布都附带有帮助函数。 要获取一些基本信息,我们打印相关的文档字符串:print(stats.norm.__doc__)

要查找支持,即分布的上限和下限,请调用

>>> print('bounds of distribution lower: %s, upper: %s' % norm.support())
bounds of distribution lower: -inf, upper: inf

我们可以使用 dir(norm) 列出分布的所有方法和属性。 事实证明,某些方法是私有的,尽管它们没有这样命名(它们的名称不是以开头下划线),例如 veccdf,仅可用于内部计算(当尝试使用这些方法时会给出警告,并且将在某个时候删除)。

要获取真正的主要方法,我们列出冻结分布的方法。(我们将在下面解释 冻结 分布的含义)。

>>> rv = norm()
>>> dir(rv)  # reformatted
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
 '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
 '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
 '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
 '__str__', '__subclasshook__', '__weakref__', 'a', 'args', 'b', 'cdf',
 'dist', 'entropy', 'expect', 'interval', 'isf', 'kwds', 'logcdf',
 'logpdf', 'logpmf', 'logsf', 'mean', 'median', 'moment', 'pdf', 'pmf',
 'ppf', 'random_state', 'rvs', 'sf', 'stats', 'std', 'var']

最后,我们可以通过内省获取可用分布的列表

>>> dist_continu = [d for d in dir(stats) if
...                 isinstance(getattr(stats, d), stats.rv_continuous)]
>>> dist_discrete = [d for d in dir(stats) if
...                  isinstance(getattr(stats, d), stats.rv_discrete)]
>>> print('number of continuous distributions: %d' % len(dist_continu))
number of continuous distributions: 109
>>> print('number of discrete distributions:   %d' % len(dist_discrete))
number of discrete distributions:   21

常用方法#

连续 RV 的主要公共方法是

  • rvs:随机变量

  • pdf:概率密度函数

  • cdf:累积分布函数

  • sf:生存函数 (1-CDF)

  • ppf:百分点函数(CDF 的逆函数)

  • isf:逆生存函数(SF 的逆函数)

  • stats:返回均值、方差、(Fisher 的)偏度或(Fisher 的)峰度

  • moment:分布的非中心矩

让我们以正态 RV 为例。

>>> norm.cdf(0)
0.5

要计算多个点的 cdf,我们可以传递一个列表或一个 numpy 数组。

>>> norm.cdf([-1., 0, 1])
array([ 0.15865525,  0.5,  0.84134475])
>>> import numpy as np
>>> norm.cdf(np.array([-1., 0, 1]))
array([ 0.15865525,  0.5,  0.84134475])

因此,基本方法,如 pdfcdf 等,都是矢量化的。

也支持其他常用的方法

>>> norm.mean(), norm.std(), norm.var()
(0.0, 1.0, 1.0)
>>> norm.stats(moments="mv")
(array(0.0), array(1.0))

要查找分布的中位数,我们可以使用百分点函数 ppf,它是 cdf 的逆函数

>>> norm.ppf(0.5)
0.0

要生成一系列随机变量,请使用 size 关键字参数

>>> norm.rvs(size=3)
array([-0.35687759,  1.34347647, -0.11710531])   # random

不要认为 norm.rvs(5) 生成 5 个变量

>>> norm.rvs(5)
5.471435163732493  # random

这里,没有关键字的 5 被解释为第一个可能的关键字参数 loc,它是所有连续分布采用的一对关键字参数中的第一个。这使我们进入了下一个小节的主题。

随机数生成#

随机数的抽取依赖于 numpy.random 包中的生成器。在上面的示例中,随机数的特定流在运行之间不可重现。要实现可重现性,您可以显式播种随机数生成器。在 NumPy 中,生成器是 numpy.random.Generator 的实例。以下是创建生成器的规范方法

>>> from numpy.random import default_rng
>>> rng = default_rng()

固定种子可以这样做

>>> # do NOT copy this value
>>> rng = default_rng(301439351238479871608357552876690613766)

警告

不要使用此数字或诸如 0 之类的常用值。仅使用一小组种子来实例化更大的状态空间意味着有些初始状态是无法到达的。如果每个人都使用这些值,则会产生一些偏差。获取种子的好方法是使用 numpy.random.SeedSequence

>>> from numpy.random import SeedSequence
>>> print(SeedSequence().entropy)
301439351238479871608357552876690613766  # random

分布中的 random_state 参数接受 numpy.random.Generator 类的实例,或一个整数,然后用于播种内部 Generator 对象

>>> norm.rvs(size=5, random_state=rng)
array([ 0.47143516, -1.19097569,  1.43270697, -0.3126519 , -0.72058873])  # random

有关更多信息,请参阅 NumPy 的文档

要了解有关 SciPy 中实现的随机数采样器的更多信息,请参阅非均匀随机数采样教程准蒙特卡罗教程

移位和缩放#

所有连续分布都将 locscale 作为关键字参数,用于调整分布的位置和比例,例如,对于标准正态分布,位置是均值,比例是标准差。

>>> norm.stats(loc=3, scale=4, moments="mv")
(array(3.0), array(16.0))

在许多情况下,随机变量 X 的标准化分布通过变换 (X - loc) / scale 获得。默认值是 loc = 0scale = 1

明智地使用 locscale 可以帮助以多种方式修改标准分布。为了进一步说明缩放,平均值为 \(1/\lambda\) 的指数分布 RV 的 cdf 由下式给出

\[F(x) = 1 - \exp(-\lambda x)\]

通过应用上述缩放规则,可以看出,通过取 scale  = 1./lambda,我们可以得到正确的缩放。

>>> from scipy.stats import expon
>>> expon.mean(scale=3.)
3.0

注意

接受形状参数的分布可能需要不仅仅是简单地应用 loc 和/或 scale 来实现所需的形状。例如,给定一个长度为 \(R\) 的常向量,其每个分量受到独立的 N(0, \(\sigma^2\)) 偏差的扰动,那么 2-D 向量长度的分布是 rice(\(R/\sigma\), scale= \(\sigma\))。第一个参数是一个形状参数,需要与 \(x\) 一起缩放。

均匀分布也很有趣

>>> from scipy.stats import uniform
>>> uniform.cdf([0, 1, 2, 3, 4, 5], loc=1, scale=4)
array([ 0.  ,  0.  ,  0.25,  0.5 ,  0.75,  1.  ])

最后,回顾上一段,我们仍然面临着 norm.rvs(5) 的含义问题。事实证明,像这样调用一个分布时,第一个参数,即 5,会被传递来设置 loc 参数。让我们看看

>>> np.mean(norm.rvs(5, size=500))
5.0098355106969992  # random

因此,为了解释上一节示例的输出:由于默认的 size=1norm.rvs(5) 生成一个均值为 loc=5 的单个正态分布随机变量。

我们建议您显式设置 locscale 参数,通过将值作为关键字而不是作为参数传递。当调用给定 RV 的多个方法时,可以通过使用冻结分布的技术来最大限度地减少重复,如下所述。

形状参数#

虽然可以使用 locscale 参数对一般的连续随机变量进行平移和缩放,但某些分布需要额外的形状参数。例如,密度为

\[\gamma(x, a) = \frac{\lambda (\lambda x)^{a-1}}{\Gamma(a)} e^{-\lambda x}\;,\]

的伽马分布需要形状参数 \(a\)。请注意,可以通过将 scale 关键字设置为 \(1/\lambda\) 来获得设置 \(\lambda\) 的效果。

让我们检查一下伽马分布的形状参数的数量和名称。(我们从上面知道这应该是 1。)

>>> from scipy.stats import gamma
>>> gamma.numargs
1
>>> gamma.shapes
'a'

现在,我们将形状变量的值设置为 1 以获得指数分布,以便我们轻松比较是否得到预期的结果。

>>> gamma(1, scale=2.).stats(moments="mv")
(array(2.0), array(4.0))

请注意,我们还可以将形状参数指定为关键字

>>> gamma(a=1, scale=2.).stats(moments="mv")
(array(2.0), array(4.0))

冻结分布#

一次又一次地传递 locscale 关键字可能会变得非常麻烦。冻结 RV 的概念用于解决此类问题。

>>> rv = gamma(1, scale=2.)

通过使用 rv,我们不再需要包含缩放或形状参数。因此,分布可以使用两种方式之一,要么将所有分布参数传递给每个方法调用(就像我们之前所做的那样),要么为分布实例冻结参数。让我们检查一下

>>> rv.mean(), rv.std()
(2.0, 2.0)

这确实是我们应该得到的。

广播#

基本方法 pdf 等满足通常的 numpy 广播规则。例如,我们可以计算不同概率和自由度的 t 分布上尾的临界值。

>>> stats.t.isf([0.1, 0.05, 0.01], [[10], [11]])
array([[ 1.37218364,  1.81246112,  2.76376946],
       [ 1.36343032,  1.79588482,  2.71807918]])

这里,第一行包含 10 个自由度的临界值,第二行包含 11 个自由度的临界值。因此,广播规则给出了两次调用 isf 的相同结果

>>> stats.t.isf([0.1, 0.05, 0.01], 10)
array([ 1.37218364,  1.81246112,  2.76376946])
>>> stats.t.isf([0.1, 0.05, 0.01], 11)
array([ 1.36343032,  1.79588482,  2.71807918])

如果概率数组(即 [0.1, 0.05, 0.01])和自由度数组(即 [10, 11, 12])具有相同的数组形状,则使用元素匹配。例如,我们可以通过调用来获得 10 个自由度的 10% 尾部、11 个自由度的 5% 尾部和 12 个自由度的 1% 尾部

>>> stats.t.isf([0.1, 0.05, 0.01], [10, 11, 12])
array([ 1.37218364,  1.79588482,  2.68099799])

离散分布的特定点#

离散分布与连续分布具有基本相同的方法。然而,pdf 被概率质量函数 pmf 替换,没有估计方法(例如 fit)可用,并且 scale 不是有效的关键字参数。位置参数,关键字 loc,仍然可以用于移动分布。

cdf 的计算需要特别注意。在连续分布的情况下,累积分布函数在大多数标准情况下在边界 (a,b) 内严格单调递增,因此具有唯一的逆函数。然而,离散分布的 cdf 是一个阶跃函数,因此逆 cdf,即百分位点函数,需要不同的定义

ppf(q) = min{x : cdf(x) >= q, x integer}

有关更多信息,请参阅文档 此处

我们可以以超几何分布为例

>>> from scipy.stats import hypergeom
>>> [M, n, N] = [20, 7, 12]

如果我们在一些整数点使用 cdf,然后评估这些 cdf 值的 ppf,我们将得到初始整数,例如

>>> x = np.arange(4) * 2
>>> x
array([0, 2, 4, 6])
>>> prb = hypergeom.cdf(x, M, n, N)
>>> prb
array([  1.03199174e-04,   5.21155831e-02,   6.08359133e-01,
         9.89783282e-01])
>>> hypergeom.ppf(prb, M, n, N)
array([ 0.,  2.,  4.,  6.])

如果我们使用不在 cdf 阶跃函数转折点的值,我们将得到下一个较高的整数

>>> hypergeom.ppf(prb + 1e-8, M, n, N)
array([ 1.,  3.,  5.,  7.])
>>> hypergeom.ppf(prb - 1e-8, M, n, N)
array([ 0.,  2.,  4.,  6.])

拟合分布#

未冻结分布的主要附加方法与分布参数的估计有关

  • fit:分布参数(包括位置)的最大似然估计

    和缩放

  • fit_loc_scale:当给出形状参数时,估计位置和缩放

  • nnlf:负对数似然函数

  • expect:计算函数对 pdf 或 pmf 的期望

性能问题和注意事项#

就速度而言,各个方法的性能因分布和方法而异。方法的运行结果通过以下两种方式之一获得:要么通过显式计算,要么通过独立于特定分布的通用算法。

一方面,显式计算要求该方法直接针对给定分布指定,或者通过解析公式,或者通过 scipy.specialnumpy.random 中的特殊函数(对于 rvs)。这些通常是相对较快的计算。

另一方面,如果分布未指定任何显式计算,则使用通用方法。要定义一个分布,只需要 pdf 或 cdf 中的一个;可以使用数值积分和求根推导出所有其他方法。然而,这些间接方法可能非常慢。例如,rgh = stats.gausshyper.rvs(0.5, 2, 2, 2, size=100) 以非常间接的方式创建随机变量,并且在我的计算机上生成 100 个随机变量需要大约 19 秒,而从标准正态或 t 分布中生成一百万个随机变量仅需要一秒多一点。

剩余问题#

scipy.stats 中的分布最近得到了纠正和改进,并获得了相当大的测试套件;然而,仍然存在一些问题

  • 分布已在一定的参数范围内进行了测试;但是,在某些角落范围内,可能仍存在一些不正确的结果。

  • fit 中的最大似然估计并非适用于所有分布的默认起始参数,并且用户需要提供良好的起始参数。此外,对于某些分布,使用最大似然估计器可能本质上不是最佳选择。

构建特定分布#

以下示例显示了如何构建自己的分布。其他示例显示了分布的用法和一些统计检验。

创建一个连续分布,即继承 rv_continuous#

创建连续分布相当简单。

>>> from scipy import stats
>>> class deterministic_gen(stats.rv_continuous):
...     def _cdf(self, x):
...         return np.where(x < 0, 0., 1.)
...     def _stats(self):
...         return 0., 0., 0., 0.
>>> deterministic = deterministic_gen(name="deterministic")
>>> deterministic.cdf(np.arange(-3, 3, 0.5))
array([ 0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,  1.,  1.,  1.,  1.])

有趣的是,现在会自动计算 pdf

>>> deterministic.pdf(np.arange(-3, 3, 0.5))
array([  0.00000000e+00,   0.00000000e+00,   0.00000000e+00,
         0.00000000e+00,   0.00000000e+00,   0.00000000e+00,
         5.83333333e+04,   4.16333634e-12,   4.16333634e-12,
         4.16333634e-12,   4.16333634e-12,   4.16333634e-12])

请注意性能问题和注意事项中提到的性能问题。未指定的公共方法的计算可能会变得非常缓慢,因为仅调用通用方法,这些方法就其性质而言,无法使用有关分布的任何特定信息。因此,作为一个警示示例

>>> from scipy.integrate import quad
>>> quad(deterministic.pdf, -1e-1, 1e-1)
(4.163336342344337e-13, 0.0)

但这是不正确的:此 pdf 的积分应为 1。让我们缩小积分区间

>>> quad(deterministic.pdf, -1e-3, 1e-3)  # warning removed
(1.000076872229173, 0.0010625571718182458)

这看起来好多了。然而,问题源于 pdf 没有在确定性分布的类定义中指定的事实。

继承 rv_discrete#

在下文中,我们使用 stats.rv_discrete 来生成一个离散分布,该分布具有以整数为中心的区间截断正态的概率。

一般信息

从 rv_discrete 的文档字符串 help(stats.rv_discrete) 中,

“您可以通过将序列元组 (xk, pk) 传递给 rv_discrete 初始化方法(通过 values= 关键字),来构建任意离散 rv,其中 P{X=xk} = pk,该元组仅描述了 X (xk) 的那些以非零概率 (pk) 出现的值。”

除此之外,这种方法要起作用,还需要一些进一步的要求

  • 需要关键字 name

  • 分布 xk 的支持点必须是整数。

  • 需要指定有效位数(小数位)。

事实上,如果最后两个要求不满足,可能会引发异常,或者导致计算出的数字不正确。

一个例子

让我们开始工作。首先

>>> npoints = 20   # number of integer support points of the distribution minus 1
>>> npointsh = npoints // 2
>>> npointsf = float(npoints)
>>> nbound = 4   # bounds for the truncated normal
>>> normbound = (1+1/npointsf) * nbound   # actual bounds of truncated normal
>>> grid = np.arange(-npointsh, npointsh+2, 1)   # integer grid
>>> gridlimitsnorm = (grid-0.5) / npointsh * nbound   # bin limits for the truncnorm
>>> gridlimits = grid - 0.5   # used later in the analysis
>>> grid = grid[:-1]
>>> probs = np.diff(stats.truncnorm.cdf(gridlimitsnorm, -normbound, normbound))
>>> gridint = grid

最后,我们可以继承 rv_discrete

>>> normdiscrete = stats.rv_discrete(values=(gridint,
...              np.round(probs, decimals=7)), name='normdiscrete')

现在我们已经定义了分布,我们可以访问离散分布的所有常用方法。

>>> print('mean = %6.4f, variance = %6.4f, skew = %6.4f, kurtosis = %6.4f' %
...       normdiscrete.stats(moments='mvsk'))
mean = -0.0000, variance = 6.3302, skew = 0.0000, kurtosis = -0.0076
>>> nd_std = np.sqrt(normdiscrete.stats(moments='v'))

测试实现

让我们生成一个随机样本,并将观察到的频率与概率进行比较。

>>> n_sample = 500
>>> rvs = normdiscrete.rvs(size=n_sample)
>>> f, l = np.histogram(rvs, bins=gridlimits)
>>> sfreq = np.vstack([gridint, f, probs*n_sample]).T
>>> print(sfreq)
[[-1.00000000e+01  0.00000000e+00  2.95019349e-02]  # random
 [-9.00000000e+00  0.00000000e+00  1.32294142e-01]
 [-8.00000000e+00  0.00000000e+00  5.06497902e-01]
 [-7.00000000e+00  2.00000000e+00  1.65568919e+00]
 [-6.00000000e+00  1.00000000e+00  4.62125309e+00]
 [-5.00000000e+00  9.00000000e+00  1.10137298e+01]
 [-4.00000000e+00  2.60000000e+01  2.24137683e+01]
 [-3.00000000e+00  3.70000000e+01  3.89503370e+01]
 [-2.00000000e+00  5.10000000e+01  5.78004747e+01]
 [-1.00000000e+00  7.10000000e+01  7.32455414e+01]
 [ 0.00000000e+00  7.40000000e+01  7.92618251e+01]
 [ 1.00000000e+00  8.90000000e+01  7.32455414e+01]
 [ 2.00000000e+00  5.50000000e+01  5.78004747e+01]
 [ 3.00000000e+00  5.00000000e+01  3.89503370e+01]
 [ 4.00000000e+00  1.70000000e+01  2.24137683e+01]
 [ 5.00000000e+00  1.10000000e+01  1.10137298e+01]
 [ 6.00000000e+00  4.00000000e+00  4.62125309e+00]
 [ 7.00000000e+00  3.00000000e+00  1.65568919e+00]
 [ 8.00000000e+00  0.00000000e+00  5.06497902e-01]
 [ 9.00000000e+00  0.00000000e+00  1.32294142e-01]
 [ 1.00000000e+01  0.00000000e+00  2.95019349e-02]]
"An X-Y histogram plot showing the distribution of random variates. A blue trace shows a normal bell curve. A blue bar chart perfectly approximates the curve showing the true distribution. A red bar chart representing the sample is well described by the blue trace but not exact."
"An X-Y histogram plot showing the cumulative distribution of random variates. A blue trace shows a CDF for a typical normal distribution. A blue bar chart perfectly approximates the curve showing the true distribution. A red bar chart representing the sample is well described by the blue trace but not exact."

接下来,我们可以使用卡方检验,scipy.stats.chisquare,来检验样本是否按照我们的规范离散分布的零假设。

该测试要求每个区间内至少有一定数量的观测值。我们将尾部区间合并成较大的区间,以便它们包含足够的观测值。

>>> f2 = np.hstack([f[:5].sum(), f[5:-5], f[-5:].sum()])
>>> p2 = np.hstack([probs[:5].sum(), probs[5:-5], probs[-5:].sum()])
>>> ch2, pval = stats.chisquare(f2, p2*n_sample)
>>> print('chisquare for normdiscrete: chi2 = %6.3f pvalue = %6.4f' % (ch2, pval))
chisquare for normdiscrete: chi2 = 12.466 pvalue = 0.4090  # random

从概念上讲,检验统计量 chi2 对观测频率与其在零假设下的期望频率之间的偏差很敏感。p 值是从假设分布中抽取样本,产生比我们观察到的统计值更极端的统计值的概率。我们的统计值不是很高;事实上,如果我们从 p2 定义的离散分布中抽取相同大小的样本,则有 40.9% 的概率统计值会高于 12.466。因此,该测试没有提供足够的证据来反驳样本是从我们的规范离散分布中抽取的零假设。