有没有numpy的分组函数?

142
有没有numpy函数可以按照下面数组的第一列进行分组?
在网络上找不到什么好的答案。
>>> a
array([[  1, 275],
       [  1, 441],
       [  1, 494],
       [  1, 593],
       [  2, 679],
       [  2, 533],
       [  2, 686],
       [  3, 559],
       [  3, 219],
       [  3, 455],
       [  4, 605],
       [  4, 468],
       [  4, 692],
       [  4, 613]])

希望的输出:

array([[[275, 441, 494, 593]],
       [[679, 533, 686]],
       [[559, 219, 455]],
       [[605, 468, 692, 613]]], dtype=object)
12个回答

104

Eelco Hoogendoorn的库启发,但不使用他的库,并利用您的数组的第一列始终增加这一事实(如果不是,则首先使用a = a[a [:,0] .argsort()]进行排序)

>>> np.split(a[:,1], np.unique(a[:, 0], return_index=True)[1][1:])
[array([275, 441, 494, 593]),
 array([679, 533, 686]),
 array([559, 219, 455]),
 array([605, 468, 692, 613])]

我没有使用"timeit"(参见下文),但这可能是实现该问题的更快方法:

  • 没有Python本机循环
  • 结果列表是NumPy数组,如果您需要对它们进行其他NumPy操作,则不需要进行新的转换
  • 复杂度看起来是O(n)(排序后为O(n log(n)))

[编辑于2021年9月]我在我的Macbook M1上运行了timeit,对于一个包含10k个随机整数的表格。 持续时间是1000次调用的时间。

>>> a = np.random.randint(5, size=(10000, 2))  # 5 different "groups"

# Only the sort
>>> a = a[a[:, 0].argsort()]
⏱ 116.9 ms

# Group by on the already sorted table
>>> np.split(a[:, 1], np.unique(a[:, 0], return_index=True)[1][1:])
⏱ 35.5 ms

# Total sort + groupby
>>> a = a[a[:, 0].argsort()]
>>> np.split(a[:, 1], np.unique(a[:, 0], return_index=True)[1][1:])
⏱ 153.0 ms 

# With numpy-indexed package (cf Eelco answer)
>>> npi.group_by(a[:, 0]).split(a[:, 1])
⏱ 353.3 ms

# With pandas (cf Piotr answer)
>>> df = pd.DataFrame(a, columns=["key", "val"]) # no timer for this line
>>> df.groupby("key").val.apply(pd.Series.tolist) 
⏱ 362.3 ms

# With defaultdict, the python native way (cf Piotr answer)
>>> d = defaultdict(list)
for key, val in a:
    d[key].append(val)
⏱ 3543.2 ms

# With numpy_groupies (cf Michael answer)
>>> aggregate(a[:,0], a[:,1], "array", fill_value=[])
⏱ 376.4 ms

第二个timeit场景,使用500个不同的分组而不是5个。 我对pandas感到惊讶,我运行了几次,但它在这种情况下表现不佳。

>>> a = np.random.randint(500, size=(10000, 2))

just the sort  141.1 ms
already_sorted 392.0 ms
sort+groupby   542.4 ms
pandas        2695.8 ms
numpy-indexed  800.6 ms
defaultdict   3707.3 ms
numpy_groupies 836.7 ms

[编辑] 我感谢ns63sr的回答Behzad Shayegh(参见评论)改进了答案。 也感谢TMBailey指出argsort的复杂度为n log(n)。


1
你对基于一列排序二维数组的方法是错误的。请使用a = a[a.T[0,:].argsort()]代替。 - Behzad Shayegh
2
如果你必须进行排序,那么如果项目尚未排序,复杂度不会增加到O(n log n)吗? - TMBailey
这些示例和时间比较让我意识到Pandas并不是一个坏的选择。 - culebrón
最近我花了一些时间来估计是否有可能创建比总排序+分组更快的东西。实际上,总排序(a = a[a[:, 0].argsort()])是一个问题。首先,它是O(n log n)的。其次,它消耗了80%的执行时间。numpynumba的开发人员不太可能很快对np.argsort进行优化。因此,只能加速剩下的20%。进一步优化的主要方向是用np.bincount替换np.unique,如果在numba中重新设计,可以进一步改进。注意:bincount不适用于大ID。 - mathfux
此外,我不建议在 OP 的代码设计中使用 np.split,因为它会创建一个空的 Pythonic list 并在每个 numpyic 数组切片上调用 list.append(在底层实现中)。 - mathfux
显示剩余3条评论

47

numpy_indexed包(免责声明:我是它的作者)旨在填补numpy中的这个空白。 numpy-indexed中的所有操作都是完全向量化的,制作此库期间没有损害任何O(n ^ 2)算法。

import numpy_indexed as npi
npi.group_by(a[:, 0]).split(a[:, 1])

请注意,通常直接在这些分组上计算相关属性(即group_by(keys)。mean(values)),比先将其拆分为列表/不规则数组更有效率。


2
谢谢。我的意思是,使用On2算法本质上是痛苦的,即使对于该算法本身也是如此。但是,我想你必须假设On2算法自己也意识到它的劣势,这样这句话才有意义。 - Eelco Hoogendoorn
1
没有使用O(n^2)算法,为什么要对它们“友好”?相反,应该对它们进行伤害:迫使它们变得更加精简。 - WestCoastProjects
请注意,此 group_by 实现更改输出组的顺序,以便按 group_by 参数的值进行排序。Pandas 的 groupby 保留原始顺序。 - Roland Pihlakas
如果组键是整数,以便 len(set(group_keys)) == max(group_keys) + 1 and min(group_keys) == 0,那么您可以通过稍后手动按 groupby 参数值对返回的数组进行索引来恢复原始顺序。(_, result) = npi.group_by(group_keys).mean(values[:, :]); result = result[group_keys, :] - Roland Pihlakas

29

Numpy在这里并不是很方便,因为所需的输出不是整数数组(而是列表对象数组)。

我建议使用纯Python方法...

from collections import defaultdict

%%timeit
d = defaultdict(list)
for key, val in a:
    d[key].append(val)
10.7 µs ± 156 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

# result:
defaultdict(list,
        {1: [275, 441, 494, 593],
         2: [679, 533, 686],
         3: [559, 219, 455],
         4: [605, 468, 692, 613]})

...或者按照熊猫的方式:

import pandas as pd

%%timeit
df = pd.DataFrame(a, columns=["key", "val"])
df.groupby("key").val.apply(pd.Series.tolist)
979 µs ± 3.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# result:
key
1    [275, 441, 494, 593]
2         [679, 533, 686]
3         [559, 219, 455]
4    [605, 468, 692, 613]
Name: val, dtype: object

2
pandas 的性能下降相当严重。不知道 datatable 是否可以胜任此任务。 - WestCoastProjects

15
n = np.unique(a[:,0])
np.array( [ list(a[a[:,0]==i,1]) for i in n] )

输出:

array([[275, 441, 494, 593], [679, 533, 686], [559, 219, 455],
       [605, 468, 692, 613]], dtype=object)

1
为了得到与他想要的完全相同的答案,可以使用以下代码:array([[x] for x in [ list(a[a[:,0]==i,1]) for i in n]]) - efirvida
是的,你的解决方案返回了他所要求的内容。但我只是假设他实际上想要一个列表数组,而不是一个无用的包含单个元素的列表数组。 - Gioelelm
12
请注意,此解决方案需要进行O(n^2)次操作,因此效率非常低。 - Eelco Hoogendoorn
1
使用 np.unique 代替 unique 可以使您的代码更加清晰。 - afruzan
1
运行完美。虽然我不明白“1”在list(a[a[:,0]==i,1])语句中扮演什么角色。 - partizanos
1
@partizanos,因为应该对第一列中的项目进行分组。 - Ulf Aslak

11
简化 Vincent J的答案,并考虑HS-nebula的评论,可以使用 return_index = True 替代 return_counts = True,并摆脱 cumsum
np.split(a[:,1], np.unique(a[:,0], return_index = True)[1])[1:]

输出

[array([275, 441, 494, 593]),
 array([679, 533, 686]),
 array([559, 219, 455]),
 array([605, 468, 692, 613])]

1
如果第一列没有排序怎么办?我们能否将其排序并创建分组? - Vidak
@Vidak a.sort(axis=0) 会按照第一列(假设索引存储在那里)就地对数组进行排序。 - ns63sr
@ns63sr idx 是什么? - jtlz2
1
这个答案没有产生正确的输出。如果你设置 idx = a[:,0],那么它就可以工作了,完整的代码是 np.split(a[:,1], np.unique(a[:,0], return_index = True)[1])[1:] - m13op22
很好的解决方案,但它有一个限制。如果存在缺失索引(比如说2),这个方法就无法工作了。它只会返回一个长度为3的列表,但你将无法通过索引访问新列表中的某些元素,因为它们的索引已经丢失了。有没有办法在不存在的索引处返回一个空列表呢? - jpmorr

2

虽然有些晚了,但是无论如何。如果你计划不仅要对数组进行分组,还想在它们上面执行诸如求和、平均值等操作,并且你考虑速度的话,你也可能需要考虑numpy_groupies。所有这些群组操作都经过了优化并使用了numba进行了编译。它们很容易就能胜过其他提到的解决方案。

from numpy_groupies.aggregate_numpy import aggregate
aggregate(a[:,0], a[:,1], "array", fill_value=[])
>>> array([array([], dtype=int64), array([275, 441, 494, 593]),
           array([679, 533, 686]), array([559, 219, 455]),
           array([605, 468, 692, 613])], dtype=object)
aggregate(a[:,0], a[:,1], "sum")
>>> array([   0, 1803, 1898, 1233, 2378])


2
很明显,a = a[a[:, 0].argsort()] 是所有竞争分组算法的瓶颈,感谢 Vincent J 澄清了这一点。超过80%的处理时间都花费在了这个 argsort 方法上,没有简单的方法可以替换或优化它。 numba 包可以加速许多算法,希望未来有人能对 argsort 进行改进。假设第一列的索引很小,那么分组的剩余部分可以显著改进。

TL;DR

大多数分组方法的剩余部分包含 np.unique 方法,当组的值很小时,它相当缓慢和冗余。用 np.bincount 替换它更有效率,后者可以在 numba 中进一步改进。以下是一些关于如何改进剩余部分的结果:

def _custom_return(unique_id, a, split_idx, return_groups):
    '''Choose if you want to also return unique ids'''
    if return_groups:
        return unique_id, np.split(a[:,1], split_idx)
    else: 
        return np.split(a[:,1], split_idx)

def numpy_groupby_index(a, return_groups=False):
    '''Code refactor of method of Vincent J'''
    u, idx = np.unique(a[:,0], return_index=True) 
    return _custom_return(u, a, idx[1:], return_groups)

def numpy_groupby_counts(a, return_groups=False):
    '''Use cumsum of counts instead of index'''
    u, counts = np.unique(a[:,0], return_counts=True)
    idx = np.cumsum(counts)
    return _custom_return(u, a, idx[:-1], return_groups)

def numpy_groupby_diff(a, return_groups=False):
    '''No use of any np.unique options'''
    u = np.unique(a[:,0])
    idx = np.flatnonzero(np.diff(a[:,0])) + 1
    return _custom_return(u, a, idx, return_groups)

def numpy_groupby_bins(a, return_groups=False):  
    '''Replace np.unique by np.bincount'''
    bins = np.bincount(a[:,0])
    nonzero_bins_idx = bins != 0
    nonzero_bins = bins[nonzero_bins_idx]
    idx = np.cumsum(nonzero_bins[:-1])
    return _custom_return(np.flatnonzero(nonzero_bins_idx), a, idx, return_groups)

def numba_groupby_bins(a, return_groups=False):  
    '''Replace np.bincount by numba_bincount'''
    bins = numba_bincount(a[:,0])
    nonzero_bins_idx = bins != 0
    nonzero_bins = bins[nonzero_bins_idx]
    idx = np.cumsum(nonzero_bins[:-1])
    return _custom_return(np.flatnonzero(nonzero_bins_idx), a, idx, return_groups)

所以numba_bincount的工作方式与np.bincount相同,它的定义如下:

from numba import njit

@njit
def _numba_bincount(a, counts, m):
    for i in range(m):
        counts[a[i]] += 1

def numba_bincount(arr): #just a refactor of Python count
    M = np.max(arr)
    counts = np.zeros(M + 1, dtype=int)
    _numba_bincount(arr, counts, len(arr))
    return counts

使用方法:

a = np.array([[1,275],[1,441],[1,494],[1,593],[2,679],[2,533],[2,686],[3,559],[3,219],[3,455],[4,605],[4,468],[4,692],[4,613]])
a = a[a[:, 0].argsort()]
>>> numpy_groupby_index(a, return_groups=False)
[array([275, 441, 494, 593]),
 array([679, 533, 686]),
 array([559, 219, 455]),
 array([605, 468, 692, 613])]
>>> numpy_groupby_index(a, return_groups=True)
(array([1, 2, 3, 4]),
 [array([275, 441, 494, 593]),
  array([679, 533, 686]),
  array([559, 219, 455]),
  array([605, 468, 692, 613])])

性能测试

在我的电脑上(有10个不同的ID),对1亿个项目进行排序需要约30秒。现在,让我们测试剩余部分中的方法运行需要多长时间:

%matplotlib inline
benchit.setparams(rep=3)

sizes = [3*10**(i//2) if i%2 else 10**(i//2) for i in range(17)]
N = sizes[-1]
x1 = np.random.randint(0,10, size=N)
x2 = np.random.normal(loc=500, scale=200, size=N).astype(int)
a = np.transpose([x1, x2])

arr = a[a[:, 0].argsort()]
fns = [numpy_groupby_index, numpy_groupby_counts, numpy_groupby_diff, numpy_groupby_bins, numba_groupby_bins]
in_ = {s/1000000: (arr[:s], ) for s in sizes}
t = benchit.timings(fns, in_, multivar=True, input_name='Millions of events')
t.plot(logx=True, figsize=(12, 6), fontsize=14)

enter image description here

毫无疑问,使用numba提供的bincount函数是处理包含小ID数据集的最佳选择。它可以将排序后的数据分组减少约5倍,相当于总运行时间的10%。


1

我使用了np.unique()函数,紧接着使用了np.extract()函数。

unique = np.unique(a[:, 0:1])
answer = []
for element in unique:
    present = a[:,0]==element
    answer.append(np.extract(present,a[:,-1]))
print (answer)

[数组([275, 441, 494, 593]), 数组([679, 533, 686]), 数组([559, 219, 455]), 数组([605, 468, 692, 613])]


1

另一种建议来自Ashwini Chaudhary,可能是您正在寻找的。将其放入一个简单的函数中

def np_groupby(x, index):
    return np.split(x, np.where(np.diff(x[:,index]))[0]+1)

x = numpy数组

索引 = 列索引

[0] + 1 根据Ashwini的说法,...任何非零的东西都意味着它旁边的项目不同,我们可以使用numpy.where查找非零项的索引,然后加1,因为这样的项的实际索引比返回的索引多1;...numpy.diff用于找出项目实际更改的位置。


1
我们可能会发现生成一个字典很有用:

dict

def groupby(X): 
    X = np.asarray(X) 
    x_uniques = np.unique(X) 
    return {xi:X[X==xi] for xi in x_uniques} 

让我们试一下:

X=[1,1,2,2,3,3,3,3,4,5,6,7,7,8,9,9,1,1,1]
groupby(X)                                                                                                      
Out[9]: 
{1: array([1, 1, 1, 1, 1]),
 2: array([2, 2]),
 3: array([3, 3, 3, 3]),
 4: array([4]),
 5: array([5]),
 6: array([6]),
 7: array([7, 7]),
 8: array([8]),
 9: array([9, 9])}

请注意,这本身并不是非常引人注目 - 但如果我们将 X 设为 objectnamedtuple,然后提供一个 groupby 函数,它就变得更有趣了。稍后会加入这个功能。

2
当你使用numpy时,通常回到python字典会大大降低速度。如果您使用更大的数组,请坚持使用numpy功能。 - Michael
当然可以 - 但通常任务都是“小数据”。如果任务的数据比@vincentj的答案更大 - 我已经点赞并评论了 - 那么它的效果更好。但这不是很容易想到的。 - WestCoastProjects

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接