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用户来说,为了计算一组数字的平均值而不得不学习安装第三方包是很不幸的。
这就引出了第二个选项:自己动手编写统计函数。乍一看,这似乎是一个很有吸引力的选项,因为常见统计函数看似简单。例如:
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在其数学库中包含标准统计函数,包括均值、求和、方差、总方差、矩偏度峰度[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)
- data的低中位数,当项目数为偶数时取两个中间值中较小的一个。
median_grouped(data, interval=1)
- 分组data的50th百分位数,使用插值法。
mode(data)
- 最常见的data点。
mode
是数据参数必须为数值的唯一例外。它也接受名义数据(如字符串)的可迭代对象。
计算方差和标准差
为了与科学计算器相似,统计模块将包含用于总体和样本方差和标准差的单独函数。所有四个函数都具有相似的签名,带有一个强制参数,即数值数据的可迭代对象,例如:
>>> variance([1, 2, 2, 2, 3])
0.5
所有四个函数还接受第二个可选参数,即数据的均值。这模仿了GNU科学库提供的类似API[18]。使用此参数有三种用例,不分先后:
- 均值的值是先验已知的。
- 您已经计算出均值,并希望避免再次计算。
- 您希望(滥用)方差函数来计算关于除均值之外的某个给定点的二阶矩。
在每种情况下,调用者有责任确保给定参数是有意义的。
提供的函数包括:
variance(data, xbar=None)
- data的样本方差,可选使用xbar作为样本均值。
stdev(data, xbar=None)
- data的样本标准差,可选使用xbar作为样本均值。
pvariance(data, mu=None)
- data的总体方差,可选使用mu作为总体均值。
pstdev(data, mu=None)
- data的总体标准差,可选使用mu作为总体均值。
其他函数
还有另一个公共函数:
sum(data, start=0)
- 数值data的高精度求和。
规范
由于提议的参考实现是纯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
对象的统计数据。尽管统计模块不会直接支持timedelta
对象,但可以通过使用timedelta.total_seconds
方法将它们首先转换为数字来支持此用例。
常见问题
在被考虑加入标准库之前,这个模块是否应该在PyPI上发布一段时间?
该模块的旧版本自2010年起已在PyPI上提供[22]。它比numpy简单得多,不需要多年的外部开发。
标准库真的还需要另一个版本的sum
吗?
这被证明是参考实现中最具争议的部分。从某种意义上说,显然三个求和函数太多了。但从另一种意义上说,是的。现有两个版本不适合的原因在此处描述[23],但简短总结是:
- 内置的sum在处理浮点数时可能会失去精度;
- 内置的sum接受任何支持
+
运算符的非数值数据类型,除了字符串和字节; math.fsum
是高精度的,但会将所有参数强制转换为浮点数。
有人曾对“修复”现有两个求和函数中的一个或另一个感兴趣。如果这在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数据的单独参数
- 同样,计算随机变量概率和推断检验(例如Student的t检验)的函数也将推迟到3.5版本。
- 人们对包含可以在迭代器形式的数据中计算多个统计数据而无需转换为列表的单次遍历函数非常感兴趣。PyPI上的实验性
stats
包包含统计函数的协程版本。包含这些将推迟到3.5版本。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0450.rst