优化群索引操作的工具:汇总求和等。
项目描述
numpy-groupies
本包包含一组优化的工具库,用于执行可大致认为是“分组索引操作”的任务。其中最突出的工具是 aggregate
,其详细描述在页面下方。
安装
如果您有 pip
,则只需
pip install numpy_groupies
请注意,numpy_groupies
没有任何强制依赖项(甚至 numpy
也是可选的),因此即使没有包管理器,您也应该能够相当容易地安装它。如果您只想安装 aggregate
的一个特定实现(例如 aggregate_numpy.py
),您可以下载该文件,并将 utils.py
文件的内容复制粘贴到该文件的顶部(替换 from .utils import (...)
行)。
aggregate
import numpy as np
import numpy_groupies as npg
group_idx = np.array([ 3, 0, 0, 1, 0, 3, 5, 5, 0, 4])
a = np.array([13.2, 3.5, 3.5,-8.2, 3.0,13.4,99.2,-7.1, 0.0,53.7])
npg.aggregate(group_idx, a, func='sum', fill_value=0)
# >>> array([10.0, -8.2, 0.0, 26.6, 53.7, 92.1])
aggregate
接收一个值数组,以及一个数组,其中包含每个值的分组编号。然后它返回每个组中值的总和(或平均值、标准差、任何...等)。您可能之前已经遇到过这个想法——参见 Matlab 的 accumarray
函数,或 pandas
分组概念,或 MapReduce 模式,或简单地是 基本直方图。
一些实现的功能并不减少数据,而是在迭代数据或排列它们时累积值。输出大小与输入大小匹配。
group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1])
a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8])
npg.aggregate(group_idx, a, func='cumsum')
# >>> array([3, 4, 5, 6,15, 9,15,22, 7, 0,15,17, 6,14,31,39])
输入
该函数接受各种不同的输入组合,产生各种不同的输出形状。我们简要描述了输入的一般含义,然后更详细地介绍不同的组合。
group_idx
- 非负整数数组,用作对a
中值进行分组的“标签”。a
- 要聚合的值数组。func='sum'
- 用于聚合的函数。有关更多详细信息,请参阅下面的部分。size=None
- 输出数组的形状。如果None
,则group_idx
中的最大值将设置输出的大小。fill_value=0
- 用于在group_idx
输入数组中任何地方都没有出现的输出组中的值。order='C'
- 对于多维输出,这控制内存中的布局,可以是'F'
以 Fortran 风格。dtype=None
- 输出的dtype
。None
表示为给定的a
、func
和fill_value
选择一个合理的类型。axis=None
- 解释如下。ddof=0
- 传递给方差和标准差的计算(请参阅函数部分)。
- 形式 1 是最简单的,它接收长度匹配的 1D
group_idx
和a
,并产生一个 1D 输出。 - 形式 2 与形式 1 类似,但接收标量
a
,该标量被广播到group_idx
的长度。请注意,这通常并不那么有用。 - 形式 3 更复杂。
group_idx
的长度与a.shape[axis]
相同。组被广播到a
的其他轴/轴上,因此输出形状为n_groups x a.shape[0] x ... x a.shape[axis-1] x a.shape[axis+1] x ... a.shape[-1]
,即输出有两个或更多维度。 - 形式 4 也产生具有两个或更多维度的输出,但与形式 3 的原因非常不同。在这里,
a
是 1D,group_idx
是 2D,而在形式 3 中,a
是ND
,group_idx
是1D
,我们提供了axis
的值。a
的长度必须匹配group_idx.shape[1]
,group_idx.shape[0]
的值确定输出中的维度数,即group_idx[:,99]
给出a[99]
的(x,y,z)
组索引。 - 表单5与表单4相同,但包含标量
a
。与表单2一样,这很少有帮助。
性能注意事项。 输出的顺序
不太可能影响聚合
的性能(尽管它可能影响您对该输出的后续使用),然而多维a
或group_idx
的顺序可能会影响性能:在表单4中,如果group_idx
中的列在内存中是连续的,则最佳,即group_idx[:, 99]
对应于连续的内存块;在表单3中,如果group_idx[i]
的a
中的所有数据都是连续的,则最佳,例如如果axis=1
,则我们希望a[:, 55]
是连续的。
可用函数
默认情况下,aggregate
假设您想要对每个组内的值求和,但是您可以使用func
关键字参数指定另一个函数。这个func
可以是任何自定义的可调用对象,但是您可能想要以下优化函数之一。请注意,并非所有实现都提供所有函数。
'sum'
- 每个组内项的和(见上面的示例)。'prod'
- 每个组内项的乘积'mean'
- 每个组内项的平均值'var'
- 每个组内项的方差。使用ddof
关键字参数表示自由度。计算中使用的除数是N - ddof
,其中N
表示元素的数量。默认情况下,ddof
为零。'std'
- 每个组内项的标准差。使用ddof
关键字参数表示自由度(见上面的var
)。'min'
- 每个组内项的最小值。'max'
- 每个组内项的最大值。'first'
- 每个组内a
中的第一个项。'last'
- 每个组内a
中的最后一个项。'argmax'
- 每个组内a
中最大值的索引。'argmin'
- 每个组内a
中最小值的索引。
上述函数也有一个nan
形式,它们跳过计算结果中的nan
值,而不是传播到计算结果中
'nansum'
、'nanprod'
、'nanmean'
、'nanvar'
、'nanstd'
、'nanmin'
、'nanmax'
、'nanfirst'
、'nanlast'
、'nanargmax'
、'nanargmin'
以下函数与上述函数略有不同,因为它们总是返回布尔值。它们对nan
的处理也与上述不同
'all'
- 如果组内的所有项都是真值,则返回True
。请注意,np.all(nan)
是True
,即nan
实际上是真值。'any'
- 如果组内的任何项是真值,则返回True
。'allnan'
- 如果组内的所有项都是nan
,则返回True
。'anynan'
- 如果组内的任何项是nan
,则返回True
。
以下函数不会减少数据,而是生成与输入大小匹配的输出
'cumsum'
- 每个组内项的累积和。'cumprod'
- 每个组内项的累积乘积。(仅numba)'cummin'
- 每个组内项的累积最小值。(仅numba)'cummax'
- 每个组内项的累积最大值。(仅numba)'sort'
- 以升序对每个组内的项进行排序,使用reverse=True
来反转顺序。
最后,有三个函数不会将每个组减少为单个值,而是返回组内的完整项集
'array'
- 简单地返回分组项,使用与a
中出现的相同顺序。(仅numpy)
示例
计算连续整数的和,然后计算这些连续整数的乘积。
group_idx = np.arange(5).repeat(3)
# group_idx: array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4])
a = np.arange(group_idx.size)
# a: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
x = npg.aggregate(group_idx, a) # sum is default
# x: array([ 3, 12, 21, 30, 39])
x = npg.aggregate(group_idx, a, 'prod')
# x: array([ 0, 60, 336, 990, 2184])
忽略NaN值计算方差,将所有NaN值组设置为nan
。
x = npg.aggregate(group_idx, a, func='nanvar', fill_value=nan)
计算每个组中元素的数量。请注意,这相当于执行np.bincount(group_idx)
,实际上这就是NumPy实现的方式。
x = npg.aggregate(group_idx, 1)
将1000个值累加成一个15x15x15的三维立方体。注意,在这个例子中,三个维度的大小相同,但这不必是这种情况。
group_idx = np.random.randint(0, 15, size=(3, 1000))
a = np.random.random(group_idx.shape[1])
x = npg.aggregate(group_idx, a, func="sum", size=(15,15,15), order="F")
# x.shape: (15, 15, 15)
# np.isfortran(x): True
使用自定义函数生成一些字符串。
group_idx = np.array([1, 0, 1, 4, 1])
a = np.array([12.0, 3.2, -15, 88, 12.9])
x = npg.aggregate(group_idx, a,
func=lambda g: ' or maybe '.join(str(gg) for gg in g), fill_value='')
# x: ['3.2', '12.0 or maybe -15.0 or maybe 12.9', '', '', '88.0']
使用axis
参数同时进行三行的求和聚合。
a = np.array([[99, 2, 11, 14, 20],
[33, 76, 12, 100, 71],
[67, 10, -8, 1, 9]])
group_idx = np.array([[3, 3, 7, 0, 0]])
x = npg.aggregate(group_idx, a, axis=1)
# x : [[ 34, 0, 0, 101, 0, 0, 0, 11],
# [171, 0, 0, 109, 0, 0, 0, 12],
# [ 10, 0, 0, 77, 0, 0, 0, -8]]
多种实现
提供了多种aggregate
的实现。如果您使用from numpy_groupies import aggregate
,则将自动选择最佳可用实现。否则,您可以直接选择特定版本,例如from numpy_groupies import aggregate_nb as aggregate
或从实现模块导入aggregate from numpy_groupies.aggregate_weave import aggregate
。
目前有以下实现
- numpy - 这是默认实现。它使用纯
numpy
,主要依赖于np.bincount
和基本的索引魔术。它不包含除numpy
以外的其他依赖项,并且对于偶尔的使用表现合理。 - numba - 这是性能最好的实现,基于numba和LLVM提供的jit编译。
- 纯Python - 这种实现没有依赖项,仅使用标准库。它非常慢,只有在没有numpy的情况下才应该使用。
- numpy ufunc - 仅用于基准测试。这种实现使用NumPy的
ufunc
的.at
方法(例如add.at
),这似乎是为执行与aggregate
相同的计算而设计的,然而NumPy实现相当不完整。 - pandas - 仅用于参考。 Pandas的
groupby
概念与aggregate
执行的任务相同。但是,Pandas实际上并不比默认的NumPy实现快。另外,请注意,在这里使用Pandas可能有改进的空间。最值得注意的是,当计算相同数据的多个聚合(例如'min'
和'max'
)时,Pandas可能更有效地使用。
所有实现都具有相同的调用语法和产生相同的输出,但存在一些浮点误差。但是,某些实现仅支持有效输入的子集,有时会抛出NotImplementedError
。
基准测试
此存储库中包含测试和基准测试脚本。要进行基准测试,请从此存储库的根目录运行python -m numpy_groupies.benchmarks.generic
。
下面我们使用从[0, 1000)
中均匀选择的500,000
个索引。a
的值从区间[0,1)
中均匀选择,小于0.2
的值设置为0(以便在布尔运算中充当假值)。对于nan-
运算,另外20%的值设置为nan,剩余的值位于区间[0.2,0.8)
。
基准测试结果以ms为单位给出,在2.40GHz的i7-7560U上运行
函数 | ufunc | numpy | numba | pandas |
---|---|---|---|---|
求和 | 1.950 | 1.728 | 0.708 | 11.832 |
乘积 | 2.279 | 2.349 | 0.709 | 11.649 |
最小值 | 2.472 | 2.489 | 0.716 | 11.686 |
最大值 | 2.457 | 2.480 | 0.745 | 11.598 |
长度 | 1.481 | 1.270 | 0.635 | 10.932 |
所有 | 37.186 | 3.054 | 0.892 | 12.587 |
任何 | 35.278 | 5.157 | 0.890 | 12.845 |
任何NaN | 5.783 | 2.126 | 0.762 | 144.740 |
所有NaN | 7.971 | 4.367 | 0.774 | 144.507 |
平均值 | ---- | 2.500 | 0.825 | 13.284 |
标准差 | ---- | 4.528 | 0.965 | 12.193 |
方差 | ---- | 4.269 | 0.969 | 12.657 |
第一个 | ---- | 1.847 | 0.811 | 11.584 |
最后一个 | ---- | 1.309 | 0.581 | 11.842 |
argmax | ---- | 3.504 | 1.411 | 293.640 |
argmin | ---- | 6.996 | 1.347 | 290.977 |
nan求和 | ---- | 5.388 | 1.569 | 15.239 |
nan乘积 | ---- | 5.707 | 1.546 | 15.004 |
nan最小值 | ---- | 5.831 | 1.700 | 14.292 |
nan最大值 | ---- | 5.847 | 1.731 | 14.927 |
nan长度 | ---- | 3.170 | 1.529 | 14.529 |
nan所有 | ---- | 6.499 | 1.640 | 15.931 |
nan任何 | ---- | 8.041 | 1.656 | 15.839 |
nan平均值 | ---- | 5.636 | 1.583 | 15.185 |
nan方差 | ---- | 7.514 | 1.682 | 15.643 |
nan标准差 | ---- | 7.292 | 1.666 | 15.104 |
nan第一个 | ---- | 5.318 | 2.096 | 14.432 |
nan最后一个 | ---- | 4.943 | 1.473 | 14.637 |
nanargmin | ---- | 7.977 | 1.779 | 298.911 |
nanargmax | ---- | 5.869 | 1.802 | 301.022 |
累积和 | ---- | 71.713 | 1.119 | 8.864 |
累积乘积 | ---- | ---- | 1.123 | 12.100 |
累积最大值 | ---- | ---- | 1.062 | 12.133 |
累积最小值 | ---- | ---- | 0.973 | 11.908 |
任意 | ---- | 147.853 | 46.690 | 129.779 |
排序 | ---- | 167.699 | ---- | ---- |
Linux(x86_64), Python 3.10.12, Numpy 1.25.2, Numba 0.58.0, Pandas 2.0.2
开发
该项目由@ml31415启动,其中numba和weave的实现也是由他完成的。纯Python和numpy的实现由@d1manson编写。
作者希望numpy的ufunc.at方法或其他numpy或scipy中的aggregate实现最终足够快,使这个包变得不再必要。实际上,Numpy 1.25确实包含了关于ufunc速度的重大改进,大大缩小了numpy和numba实现之间的速度差距。
项目详情
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。