Python中计算多维数组中数组出现次数

8

我有以下类型的数组:

a = array([[1,1,1],
           [1,1,1],
           [1,1,1],
           [2,2,2],
           [2,2,2],
           [2,2,2],
           [3,3,0],
           [3,3,0],
           [3,3,0]])

我希望能够统计每种数组类型的出现次数,例如

[1,1,1]:3, [2,2,2]:3, and [3,3,0]: 3 

我该如何用Python实现这个功能?是否可以不使用for循环和计算字典来实现呢? 必须快速且处理时间应不超过0.1秒左右。 我查看了Counter,numpy bincount等工具,但它们只适用于单个元素而非数组。

谢谢。


可能是在numpy.array中查找唯一行的重复问题。 - Daniel
5个回答

2
如果您不介意将其映射到元组以获取计数,则可以使用Counter字典,它在我的机器上使用python3运行时间为28.5 µs,远低于您的阈值:
In [5]: timeit Counter(map(tuple, a))
10000 loops, best of 3: 28.5 µs per loop

In [6]: c = Counter(map(tuple, a))

In [7]: c
Out[7]: Counter({(2, 2, 2): 3, (1, 1, 1): 3, (3, 3, 0): 3})

没问题,实际上在Python2中只使用map函数会更快。 - Padraic Cunningham

2
`collections.Counter`可以很方便地完成这个任务,几乎就像给出的例子一样。
>>> from collections import Counter
>>> c = Counter()
>>> for x in a:
...   c[tuple(x)] += 1
...
>>> c
Counter({(2, 2, 2): 3, (1, 1, 1): 3, (3, 3, 0): 3})

这将每个子列表转换为元组,因为它们是不可变的,所以可以用作字典中的键。列表是可变的,因此不能用作字典键。
您为什么想要避免使用for循环?
与@padraic-cunningham的更酷的答案类似:
>>> Counter(tuple(x) for x in a)
Counter({(2, 2, 2): 3, (1, 1, 1): 3, (3, 3, 0): 3})
>>> Counter(map(tuple, a))
Counter({(2, 2, 2): 3, (1, 1, 1): 3, (3, 3, 0): 3})

应避免使用带有numpy的Python循环,因为它们可能比numpy解决方案慢多倍。因此,如果您正在使用numpy和Python循环,则可能是“错误的做法”。在两个Python答案中,您都将整个数组复制到一个更不紧凑的数据类型中,这尤其令人担忧。 - Daniel
@Ophion,你有更快的numpy解决方案吗? - Padraic Cunningham
2
@PadraicCunningham Divakar的解决方案对于规模适中的问题将更快。这个问题在过去已经被问过、回答过和基准测试过很多次了。我认为标准答案在这里 - Daniel
@Ophion 关于“将整个数组复制到一个更少紧凑的数据类型”,这是一个很好的观点。虽然我考虑过使用哈希来进一步担心,但后来也认为这样做过于复杂了。我要添加的一个编辑/补充是检索某个元素的计数,这在本例中实质上是一个字典查找。因此,要打印(例如)其中一个计数,而不是 print c[(3, 3, 0)],可以使用 print c.getcounts(3, 3, 0)(或用 getcounts 的代码覆盖 __getitem__),从而实现类似的结果。 - aneroid
此外,人们必须决定他们在原始项目方面的关注程度。对于一个“相当大”的数组/列表,如果对象不多重复,拥有一个实际副本加上一个计数器会使其变得不合理。因此,最好完全失去该值并对对象使用哈希或str,以用作字典键。我正在编写一个通用的HashedCounter,但意识到一旦覆盖了涉及键的所有所需方法,就会最终编写大部分内容。但不是针对Numpy特定的。 - aneroid
2
在我的解决方案中,添加了一个相当大的输入数组用例的基准测试。这种基准测试趋势也持续存在于中等大小的输入情况下。我认为@Ophion说NumPy解决方案针对这种情况进行了优化是有道理的,这符合其哲学。 - Divakar

2
您可以使用元素作为二维索引,使用 np.ravel_multi_index 将这些行转换为一维数组。然后,使用 np.unique 来给出每个唯一行的起始位置,并且有一个可选参数 return_counts 来给出计数。因此,实现会像这样 -
def unique_rows_counts(a):

    # Calculate linear indices using rows from a
    lidx = np.ravel_multi_index(a.T,a.max(0)+1 )

    # Get the unique indices and their counts
    _, unq_idx, counts = np.unique(lidx, return_index = True, return_counts=True)

    # return the unique groups from a and their respective counts
    return a[unq_idx], counts

样例运行 -

In [64]: a
Out[64]: 
array([[1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [2, 2, 2],
       [2, 2, 2],
       [2, 2, 2],
       [3, 3, 0],
       [3, 3, 0],
       [3, 3, 0]])

In [65]: unqrows, counts = unique_rows_counts(a)

In [66]: unqrows
Out[66]: 
array([[1, 1, 1],
       [2, 2, 2],
       [3, 3, 0]])
In [67]: counts
Out[67]: array([3, 3, 3])

基准测试

假设您可以接受numpy数组或集合作为输出,那么可以这样对迄今为止提供的解决方案进行基准测试 -

函数定义:

import numpy as np
from collections import Counter

def unique_rows_counts(a):
    lidx = np.ravel_multi_index(a.T,a.max(0)+1 )
    _, unq_idx, counts = np.unique(lidx, return_index = True, return_counts=True)
    return a[unq_idx], counts

def map_Counter(a):
    return Counter(map(tuple, a))    

def forloop_Counter(a):      
    c = Counter()
    for x in a:
        c[tuple(x)] += 1
    return c

时间:

In [53]: a = np.random.randint(0,4,(10000,5))

In [54]: %timeit map_Counter(a)
10 loops, best of 3: 31.7 ms per loop

In [55]: %timeit forloop_Counter(a)
10 loops, best of 3: 45.4 ms per loop

In [56]: %timeit unique_rows_counts(a)
1000 loops, best of 3: 1.72 ms per loop

2
@PadraicCunningham 我可以添加运行时测试,但是 OP 可能希望将字典作为输出,这样就不公平了 :) - Divakar
我将不得不等待那个踩我的人发布他们更好的解决方案;) - Padraic Cunningham
@PadraicCunningham /我仍在屏息以待地等待这个高超的回答;-) - aneroid
1
@PadraicCunningham 没有测试正确性,但类似于 - base = a.min(0); lidx = np.ravel_multi_index(a.T- base.T[:,None],a.max(0)-base+1 ) - Divakar
1
@PadraicCunningham 哦,我明白了,很高兴知道它仍在使用! - Divakar
显示剩余14条评论

1

numpy_indexed软件包(免责声明:我是其作者)包含了高效的向量化功能,用于这些类型的操作:

import numpy_indexed as npi
unique_rows, row_count = npi.count(a, axis=0)

请注意,此方法适用于任何维度或数据类型的数组。

完美的答案,但我们如何将其作为一个输出呈现:例 [[[1,1,9],10],[[1,1,0],2]]。 - Moustafa Mahmoud
zip(*npi.count(..)会给出那个结果;但那不是很符合numpythonic的风格;或者你可以创建一个具有复合dtype的结构化数组,并将结果分配给它,如果你坚持的话。但更有可能的是,如果你坚持使用numpy本地喜欢组织事物的方式,你会得到一个更有效的解决方案。 - Eelco Hoogendoorn

1
自从numpy-1.13.0版本以后,np.unique函数可以使用axis参数:
>>> np.unique(a, axis=0, return_counts=True)

(array([[1, 1, 1],
        [2, 2, 2],
        [3, 3, 0]]), array([3, 3, 3]))

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