Python 汇总和计数进行统计分析,基于函数sum()
和len()
,可以给出算术平均值的简单定义,如下所示:
def mean(items):
return sum(items) / len(items)
虽然这个定义很简洁,却不能用于可迭代对象,只能用于支持len()
函数的集合,这一点通过添加类型标示很容易发现。定义mean(items: Iterable) -> float
不成立,因为Iterable
类型不支持len()
函数。
诚然,对可迭代对象即使是求简单的平均值或者标准差都是很困难的。在Python中,我们要么实例化这些序列对象,要么转而使用一些更复杂的运算来完成求值。
正确的定义方法如下:
from collections import Sequence
def mean(items: Sequence) -> float:
return sum(items) / len(items)
定义正确的类型标示以确保sum()
和len()
能正常工作。
下面的定义用简洁的表达式定义了平均值和标准差。
import math
s0 = len(data) # sum(x ** 0 for x in data)
s1 = sum(data) # sum(x ** 1 for x in data)
s2 = sum(x ** 2 for x in data)
mean = s1 / s0
stdev = math.sqrt(s2 / s0 - (s1 / s0) ** 2)
s0
、s1
和s2
是3种求和,实现过程都比较简单,结构也类似。通过它们可以很方便地算出平均值。计算标准差虽然稍复杂一些,但仍然可行。
更复杂的统计函数仍然具备这种良好的对称性,包括相关度和最小方差线性回归。
两个样本之间的相关度可以通过它们的标准值计算出来,如下所示:
def z(x, μ_x: float, σ_x: float) -> float:
return (x - μ_x) / σ_x
计算过程是对每个样本x
减去平均值μ_x
,再除以标准差σ_x
,返回结果以σ为单位。大约2/3的情况下偏差在±1σ
范围内,偏差越大,出现的概率越小,超过±3σ
的概率则小于1/100。
如下所示使用该标量函数:
>>> d = [2, 4, 4, 4, 5, 5, 7, 9]
>>> list(z(x, mean(d), stdev(d)) for x in d)
[-1.5, -0.5, -0.5, -0.5, 0.0, 0.0, 1.0, 2.0]
这里首先对变量d
中的值进行归一化,把计算结果实例化到一个列表中。使用生成器表达式,将标量函数z()
应用于序列对象。
利用上面的函数,可以给出函数mean()
和stdev()
的实现方法。
def mean(samples: Sequence) -> float:
return s1(samples) / s0(samples)
def stdev(samples: Sequence) -> float:
N = s0(samples)
return sqrt((s2(samples) / N) - (s1(samples) / N) ** 2)
用类似的方法,如下改写前面3个求和函数:
def s0(samples: Sequence) -> float:
return sum(1 for x in samples) # or len(data)
def s1(samples: Sequence) -> float:
return sum(x for x in samples) # or sum(data)
def s2(samples: Sequence) -> float:
return sum(x * x for x in samples)
尽管简洁明了,但该实现仍无法应用于可迭代对象。计算平均值时,需要对可迭代对象做一次汇总和一次计数。计算标准差时,需要做两次汇总和一次计数,针对这类统计相关的处理,必须将序列对象实例化,才能多次使用数据。
计算两个样本集相关性的函数实现如下:
def corr(samples1: Sequence, samples2: Sequence) -> float:
m_1, s_1 = mean(samples1), stdev(samples1)
m_2, s_2 = mean(samples2), stdev(samples2)
z_1 = (z( x, m_1, s_1 ) for x in samples1)
z_2 = (z( x, m_2, s_2 ) for x in samples2)
r = (sum(zx1 * zx2 for zx1, zx2 in zip(z_1, z_2))
/ len(samples1))
return r
上面的相关性计算用到了样本集的基本统计特征值:平均值和标准差。基于这些特征值,我们定义了两个生成器函数,计算每个样本集的归一值。接着用zip()
函数(见下个示例)把两个归一化序列中的元素组对,计算每对的乘积。这个乘积序列的平均值即表示两个样本集的相关度。
计算两个样本集之间的相关度如下所示:
>>> # Height (m)
>>> xi = [1.47, 1.50, 1.52, 1.55, 1.57, 1.60, 1.63, 1.65,
... 1.68, 1.70, 1.73, 1.75, 1.78, 1.80, 1.83,]
>>> # Mass (kg)
>>> yi = [52.21,53.12,54.48,55.84,57.20,58.57,59.93,61.29,
... 63.11, 64.47, 66.28, 68.10, 69.92, 72.19, 74.46,]
>>> round(corr( xi, yi ), 5)
0.99458
这里的两个数据点序列xi
和yi
的相关度超过了0.99,表明二者关系紧密。
以上代码示例很好地体现了函数式编程的优点。我们用几个函数构建出了一个简单易用的统计模块,且每个函数只是简单的表达式。作为反例,不妨假设把corr()
函数写成一个长而复杂的表达式,函数内部有很多只用了一次的内部变量,用复制粘贴的方法把它们替换成各自代表的表达式。不难看出,虽然这里的corr()
函数仅由6行Python代码组成,但仍属于函数式编程。