PEP 450 – 向标准库添加统计模块
- 作者:
- Steven D’Aprano <steve at pearwood.info>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2013年8月1日
- Python版本:
- 3.4
- 历史记录:
- 2013年9月13日
摘要
本PEP提议向Python标准库添加一个用于常见统计函数(如平均值、中位数、方差和标准差)的模块。另请参阅http://bugs.python.org/issue18606
基本原理
提议的统计模块的动机是Python标准库的“自带电池”理念。Raymond Hettinger和其他高级开发人员曾请求一个高质量的统计库,该库介于高端统计库和临时代码之间。[1] 诸如平均值、标准差等统计函数是显而易见且有用的“电池”,任何中学生都熟悉它们。即使是廉价的科学计算器通常也包含多种统计函数,例如
- 平均值
- 总体和样本方差
- 总体和样本标准差
- 线性回归
- 相关系数
针对中学生的图形计算器通常包括以上所有内容,以及以下部分或全部内容
- 中位数
- 众数
- 用于计算正态、t、卡方和F分布的随机变量概率的函数
- 均值的推断
以及其他[2]。同样,Microsoft Excel、LibreOffice和Gnumeric等电子表格应用程序也包含丰富的统计函数集合[3]。
相比之下,Python目前还没有标准的方法来计算即使是最简单、最明显的统计函数,例如平均值。对于那些需要在Python中使用统计函数的人来说,有两个明显的解决方案
- 安装numpy和/或scipy [4];
- 或者使用自己动手解决方案。
Numpy可能是功能最全面的解决方案,但它也有一些缺点
- 对于许多用途来说,它可能过于复杂。numpy的文档甚至警告说“很难知道numpy中有哪些函数可用。这不是一个完整的列表,但它涵盖了大部分函数。”[5]
然后继续列出了超过270个函数,其中只有少量与统计相关。
- Numpy的目标是那些从事大量数值工作的人,对于那些没有计算数学和计算机科学背景的人来说,它可能令人生畏。例如,
numpy.mean
接受四个参数mean(a, axis=None, dtype=None, out=None)
尽管幸运的是,对于初学者或偶尔使用numpy的用户来说,其中三个是可选的,并且
numpy.mean
在简单的情况下会做出正确的处理>>> numpy.mean([1, 2, 3, 4]) 2.5
- 对于许多人来说,安装numpy可能很困难或不可能。例如,企业环境中的人员可能必须经历一个复杂且耗时的过程,才能被允许安装第三方软件。对于偶尔使用Python的用户来说,为了计算一列数字的平均值而必须学习如何安装第三方软件,这实在是不幸的。
这导致了选项2,即DIY统计函数。乍一看,这似乎是一个有吸引力的选择,因为常见的统计函数看起来很简单。例如
def mean(data):
return sum(data)/len(data)
def variance(data):
# Use the Computational Formula for Variance.
n = len(data)
ss = sum(x**2 for x in data) - (sum(data)**2)/n
return ss/(n-1)
def standard_deviation(data):
return math.sqrt(variance(data))
在进行简单的测试时,上述代码似乎是正确的
>>> data = [1, 2, 4, 5, 8]
>>> variance(data)
7.5
但是,向每个数据点添加一个常数不应该改变方差
>>> data = [x+1e12 for x in data]
>>> variance(data)
0.0
而且方差永远不应该为负
>>> variance(data*100)
-1239429440.1282566
相比之下,提议的参考实现对于前两个示例得到了完全正确的答案7.5,对于第三个示例得到了一个相当接近的答案:6.012。numpy也做不到更好[6]。
即使是简单的统计计算也包含着对粗心大意的人的陷阱,从计算公式本身开始。尽管名称如此,它在数值上是不稳定的,并且可能极其不准确,如上所示。它完全不适合计算机计算[7]。这个问题困扰着许多编程语言的用户,而不仅仅是Python [8],因为程序员一遍又一遍地重新发明相同的数值不精确代码[9],或者建议其他人这样做[10]。
不仅仅是方差和标准差。即使是平均值也不像看起来那样简单。上述实现似乎过于简单,不可能出现问题,但它确实存在问题
- 内置的
sum
在处理数量级差异很大的浮点数时可能会丢失精度。因此,上述简单的mean
无法通过这个“折磨测试”assert mean([1e30, 1, 3, -1e30]) == 1
返回0而不是1,这是一个100%的纯计算错误。
- 在
mean
内部使用math.fsum
将使其在使用浮点数据时更加准确,但它也具有将任何参数转换为浮点数的副作用,即使在不需要时也是如此。例如,我们应该期望分数列表的平均值是一个分数,而不是一个浮点数。
虽然上述平均值实现没有像简单的方差那样灾难性地失败,但标准库函数可以比DIY版本做得更好。
上面的示例涉及一组特别糟糕的数据,但即使对于更现实的数据集,精度也很重要。解释数据变化(包括处理病态数据)的第一步通常是将其标准化为方差为1(并且通常均值为0)的序列。这种标准化需要准确计算原始序列的均值和方差。简单的均值和方差计算可能会很快失去精度。由于精度限制准确性,因此使用最精确的算法来计算实用中的均值和方差非常重要,否则标准化的结果本身就毫无用处。
与其他语言/包的比较
提议的统计库并非旨在与numpy/scipy等第三方库竞争,或与针对专业统计学家(如Minitab、SAS和Matlab)的专有功能丰富的统计软件包竞争。它的目标是达到图形和科学计算器的水平。
大多数编程语言对统计函数的支持很少或没有。一些例外情况
R语言
R语言(及其专有版本S语言)是一种为统计工作而设计的编程语言。它在统计学家中非常受欢迎,并且功能非常丰富[11]。
C#
C# LINQ包包含用于计算可枚举对象平均值的扩展方法[12]。
Ruby
Ruby没有自带标准的统计模块,尽管似乎存在一些需求[13]。Statsample似乎是一个功能丰富的第三方库,旨在与R语言竞争[14]。
PHP
PHP有一套功能极其丰富(但大部分没有文档)的高级统计函数[15]。
Delphi
Delphi在其Math库中包含标准的统计函数,包括平均值、求和、方差、总体方差、矩偏度峰度[16]。
GNU科学库
GNU科学库包含标准的统计函数、百分位数、中位数等[17]。我借鉴了GSL的一个创新之处,即允许调用者在计算方差和标准差时可以选择指定样本的预先计算的均值(或先验已知的总体均值)[18]。
模块的设计决策
我的目的是从小开始,根据需要扩展库,而不是一开始就试图包含所有内容。因此,当前的参考实现仅包含少量函数:平均值、方差、标准差、中位数、众数。(有关完整列表,请参阅参考实现。)
我的目标是实现以下设计特性
- 正确性优先于速度。提高正确但速度慢的函数的速度比修复快速但有错误的函数更容易。
- 专注于序列中的数据,允许对数据进行两次遍历,而不是为了追求一次遍历算法而可能牺牲准确性。函数期望数据将作为列表或其他序列传递;如果给定迭代器,它们可能会在内部将其转换为列表。
- 函数应尽可能地尊重任何类型的数值数据。例如,十进制数列表的平均值应为十进制数,而不是浮点数。在不可能的情况下,将浮点数视为“最低公用数据类型”。
- 尽管函数支持浮点数、十进制数或分数的数据集,但不能保证支持混合数据集。(但另一方面,它们也没有被明确地拒绝。)
- 大量的文档,面向那些理解基本概念但可能不知道(例如)应该使用哪种方差(总体方差还是样本方差?)的读者。数学家和统计学家有一个糟糕的习惯,那就是在符号和术语上不一致[19],并且在花费了大量时间理解正在使用的相互矛盾/令人困惑的定义之后,我尽力阐明而不是模糊主题是公平的。
- 但避免陷入乏味的[20]数学细节。
API
库的初始版本将提供单变量(单变量)统计函数。通用API将基于函数模型function(data, ...) -> result
,其中data
是(通常是)数值数据的必需可迭代对象。
作者预计列表将是最常用的数据类型,但任何可迭代类型都应该可以接受。在必要时,函数可能会在内部转换为列表。在可能的情况下,函数预计会保留数据值的类型,例如,十进制数列表的平均值应为十进制数,而不是浮点数。
计算平均值、中位数和众数
mean
、median*
和mode
函数接受一个必需的参数并返回相应的统计量,例如
>>> mean([1, 2, 3])
2.0
提供的函数有
mean(data)
- data的算术平均值。
median(data)
- data的中位数(中间值),当值的数量为偶数时,取两个中间值的平均值。
median_high(data)
- data的高中位数,当项目数量为偶数时,取两个中间值中较大的那个。
median_low(data)
- 数据的中位数较低,当项目数为偶数时,取两个中间值中较小的一个。
median_grouped(data, interval=1)
- 分组数据的第 50 个百分位数,使用插值法。
mode(data)
- 最常见的数据点。
mode
是数据参数必须为数字这一规则的唯一例外。它还可以接受标称数据的可迭代对象,例如字符串。
计算方差和标准差
为了与科学计算器相似,statistics 模块将包含用于总体和样本方差和标准差的单独函数。所有四个函数都具有类似的签名,只有一个强制参数,即数值数据的可迭代对象,例如:
>>> variance([1, 2, 2, 2, 3])
0.5
所有四个函数还接受第二个可选参数,即数据的均值。这模仿了 GNU 科学库 [18] 提供的类似 API。使用此参数有三种用例,无特定顺序
- 均值是先验已知的。
- 您已经计算了均值,并且希望避免再次计算它。
- 您希望(滥用)方差函数来计算某个给定点(而不是均值)的二阶矩。
在每种情况下,确保给定参数有意义都是调用者的责任。
提供的函数有
variance(data, xbar=None)
- 数据的样本方差,可以选择使用xbar作为样本均值。
stdev(data, xbar=None)
- 数据的样本标准差,可以选择使用xbar作为样本均值。
pvariance(data, mu=None)
- 数据的总体方差,可以选择使用mu作为总体均值。
pstdev(data, mu=None)
- 数据的总体标准差,可以选择使用mu作为总体均值。
其他函数
还有一个公共函数
sum(data, start=0)
- 数值数据的高精度求和。
规范
由于提议的参考实现是用纯 Python 编写的,因此其他 Python 实现可以轻松地原封不动地使用该模块,或根据需要对其进行调整。
模块名称应该是什么?
这将是一个顶级模块 statistics
。
有人希望将 math
转换为包,并将此作为 math
的子模块,但最终普遍认为顶级模块更好。其他潜在但被拒绝的名称包括 stats
(与现有的 stat
模块过于容易混淆)和 statslib
(被描述为“过于 C 风格”)。
讨论和已解决的问题
此提案之前已在此处讨论过 [21]。
在 Python-Ideas 上的讨论和初始代码审查期间,解决了许多设计问题。人们非常担心在标准库中添加另一个 sum
函数,有关更多详细信息,请参阅下面的常见问题解答。此外,sum
的初始实现处理十进制数时存在一些舍入问题和其他设计问题。Oscar Benjamin 在解决此问题方面提供了宝贵的帮助。
另一个问题是处理迭代器形式的数据。方差的第一个实现会在数据是否以迭代器或序列的形式出现时在单遍和双遍算法之间静默切换。事实证明这是一个设计错误,因为计算出的方差可能会因所用算法的不同而略有不同,并且 variance
等被更改为在内部生成列表并始终使用更准确的双遍实现。
一个有争议的设计涉及计算中位数的函数,这些函数被实现为 median
可调用对象的属性,例如 median
、median.low
、median.high
等。尽管标准库中的 unittest.mock
中至少有一个现有用例使用了这种样式,但代码审查人员认为这对于标准库来说过于不寻常。因此,设计已更改为更传统的独立函数设计,并使用伪命名空间命名约定,median_low
、median_high
等。
代码审查人员关心的另一个问题是存在一个计算连续数据样本众数的函数,一些人质疑算法的选择,以及它是否足够常见以至于需要包含在内。因此,它已从 API 中删除,现在 mode
只实现了基于计数唯一值的简单教科书算法。
另一个重要的讨论点是计算 timedelta
对象的统计数据。尽管 statistics 模块不会直接支持 timedelta
对象,但可以通过首先使用 timedelta.total_seconds
方法将其转换为数字来支持此用例。
常见问题
这个模块不应该先在PyPI上发布一段时间,然后再考虑纳入标准库吗?
自 2010 年以来,此模块的旧版本已在 PyPI [22] 上可用。由于它比 numpy 简单得多,因此不需要多年的外部开发。
标准库真的还需要另一个版本的sum
吗?
事实证明,这是参考实现中最有争议的部分。从某种意义上说,三个求和函数显然太多了。但从另一方面来说,是的。此处描述了两个现有版本不合适的原因 [23],但简而言之
- 内置的 sum 函数在使用浮点数时可能会丢失精度;
- 内置的 sum 函数接受任何支持
+
运算符的非数字数据类型,除了字符串和字节; math.fsum
是高精度的,但会将所有参数强制转换为浮点数。
有人希望“修复”一个或另一个现有的 sum 函数。如果在 3.4 功能冻结之前发生这种情况,则可以重新考虑是否保留 statistics.sum
的决定。
这个模块会向旧版本的Python回溯移植吗?
该模块当前的目标是 3.3,我将在未来的一段时间内将其在 PyPI 上提供给 3.3。向旧版本的 3.x 系列移植的可能性很大(但尚未决定)。向 2.7 移植的可能性较小,但尚未排除。
这是打算取代numpy吗?
否。虽然它可能会随着时间的推移而增长(请参阅下面的未解决问题),但它并非旨在取代或甚至直接与 numpy 竞争。Numpy 是一个面向专业人士的全功能数值库,是 Python 生态系统中数值库的“核反应堆”。这只是一个电池,就像“随附电池”一样,其目标是在“使用 numpy”和“自己编写版本”之间找到一个中间水平。
未来工作
- 在此阶段,我不确定多元统计函数(如线性回归、相关系数和协方差)的最佳 API。可能的 API 包括
- x 和 y 数据的单独参数
function([x0, x1, ...], [y0, y1, ...])
- (x, y) 数据的单个参数
function([(x0, y0), (x1, y1), ...])
此 API 为 GvR 所偏好 [24]。
- 从二维数组中选择任意列
function([[a0, x0, y0, z0], [a1, x1, y1, z1], ...], x=1, y=2)
- 以上内容的某种组合。
由于缺乏多元统计的首选 API 的共识,我将推迟包含此类多元函数,直到 Python 3.5。
- x 和 y 数据的单独参数
- 同样,计算随机变量概率和推断检验(例如学生 t 检验)的函数也将推迟到 3.5。
- 人们非常希望包含单遍函数,这些函数可以从迭代器形式的数据中计算多个统计数据,而无需转换为列表。PyPI 上的实验性
stats
包包含统计函数的协程版本。包含这些将推迟到 3.5。
参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0450.rst