【问题标题】:Is there any numpy group by function?是否有任何 numpy 按功能分组?
【发布时间】:2016-10-27 02:17:56
【问题描述】:

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)

【问题讨论】:

    标签: python arrays numpy


    【解决方案1】:

    很明显a = a[a[:, 0].argsort()] 是所有竞争性分组算法的瓶颈,非常感谢Vincent J 澄清了这一点。这个argsort 方法只是浪费了超过80% 的处理时间,并且没有简单的方法可以替换或优化它。 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])])
    

    性能测试

    在我的计算机上对 1 亿个项目(具有 10 个不同的 ID)进行排序大约需要 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)
    

    毫无疑问,numba-powered bincount 是包含小 ID 的数据集的新赢家。它有助于将排序数据的分组减少约 5 倍,即总运行时间的约 10%。

    【讨论】:

      【解决方案2】:

      Eelco Hoogendoorn's library 启发,但没有他的库,并且使用数组的第一列总是增加的事实(如果不是,请先使用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])]
      

      我没有“计时”([编辑]见下文),但这可能是解决问题的更快方法:

      • 没有 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
      

      第二个场景,有 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's answerBehzad Shayegh(参见评论) 还要感谢 TMBailey 注意到 argsort 的复杂性是 n log(n)。

      【讨论】:

      • 很好的答案。而且很容易记住!好吧,第二个可能不是那么多。添加到我的技巧中。
      • 您对基于一列的二维数组进行排序的方法不正确。请改用a = a[a.T[0,:].argsort()]
      • 真的!这种排序正在洗牌第二列。我编辑了答案。谢谢
      • 如果您必须进行排序,如果项目尚未排序,那么复杂度会不会上升到 O(n log n)?
      • 这些例子和时间比较让我意识到 Pandas 并不是一个糟糕的选择。
      【解决方案3】:

      聚会迟到了,但无论如何。如果您不仅打算对数组进行分组,还想对它们进行求和、均值等操作,并且考虑到速度,您可能还需要考虑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])
      
      

      【讨论】:

        【解决方案4】:

        简化answer of 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])]
        

        【讨论】:

        • 如果第一列没有排序怎么办?我们能否以某种方式将排序和创建组结合起来?
        • @Vidak a.sort(axis=0) 将按数组的第一列就地排序(假设索引存储在那里)
        • @ns63sr idx 是什么?
        • 这个答案没有产生正确的输出。如果您设置idx = a[:,0] 以便完整代码为np.split(a[:,1], np.unique(a[:,0], return_index = True)[1])[1:],则它可以工作
        • 很好的解决方案,但它有一个限制。如果缺少索引(比如说 2),这将不起作用。它只会返回一个 3 项长的列表,但是您将无法通过索引访问新列表,因为会丢失一些索引。有什么方法可以为不存在的索引返回一个空列表?
        【解决方案5】:

        我们可能还会发现生成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 函数,它会变得更有趣。稍后会放上去。

        【讨论】:

        • 当你使用 numpy 时,回到 python dicts 通常会大大降低速度。如果您使用更大的数组,请坚持使用 numpy 功能。
        • 当然 - 但通常足够的任务是“小数据”。如果任务是比@vincentj 的答案更大的数据——我已经对此表示赞同和评论——效果会更好。但这并不完全是舌尖
        【解决方案6】:
        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)
        

        【讨论】:

        • 得到完全相同的答案,因为他想要array([[x] for x in [ list(a[a[:,0]==i,1]) for i in n]])
        • 请注意,此方案需要 O(n^2) 次操作,效率非常低。
        • 使用np.unique而不是unique来清除你的代码。
        • 完美运行。虽然我不明白“1”在list(a[a[:,0]==i,1]) 语句中起什么作用
        • @partizanos,因为应该对第一列中的项目进行分组。
        【解决方案7】:

        给定 X 作为您想要分组的项目数组和 y(一维数组)作为相应的组,以下函数使用 numpy 进行分组:

        def groupby(X, y):
            y = np.asarray(y)
            X = np.asarray(X)
            y_uniques = np.unique(y)
            return [X[y==yi] for yi in y_uniques]
        

        所以,groupby(a[:,1], a[:,0]) 返回 [array([275, 441, 494, 593]), array([679, 533, 686]), array([559, 219, 455]), array([605, 468, 692, 613])]

        【讨论】:

          【解决方案8】:

          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
          

          【讨论】:

          • pandas 表现有点残酷。想知道datatable是否可以做到这一点
          【解决方案9】:

          我使用 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)
          

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

          【讨论】:

            【解决方案10】:

            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))通常更有效,而不是首先拆分为列表/锯齿状数组。

            【讨论】:

            • 谢谢。我的意思是,使用 On2 算法本质上是痛苦的,即使对于算法本身也是如此。但是,是的,我想你必须假设 On2 算法也意识到它的自卑感,这句话才有意义..
            • "没有 O(n^2) 算法受到伤害" .. 你为什么要对他们“友好”?而是伤害他们:强迫他们“变得更瘦”
            • 请注意,group_by 的此实现更改了输出组的顺序,以便它们按group_by 的参数值排序。熊猫的groupby保持原来的顺序。
            • 如果组键是整数,因此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, :]
            猜你喜欢
            • 2014-12-18
            • 2021-08-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多