【问题标题】:Get cumulative count per 2d array获取每个二维数组的累积计数
【发布时间】:2026-01-06 23:15:02
【问题描述】:

我有一般数据,例如字符串:

np.random.seed(343)

arr = np.sort(np.random.randint(5, size=(10, 10)), axis=1).astype(str)
print (arr)
[['0' '1' '1' '2' '2' '3' '3' '4' '4' '4']
 ['1' '2' '2' '2' '3' '3' '3' '4' '4' '4']
 ['0' '2' '2' '2' '2' '3' '3' '4' '4' '4']
 ['0' '1' '2' '2' '3' '3' '3' '4' '4' '4']
 ['0' '1' '1' '1' '2' '2' '2' '2' '4' '4']
 ['0' '0' '1' '1' '2' '3' '3' '3' '4' '4']
 ['0' '0' '2' '2' '2' '2' '2' '2' '3' '4']
 ['0' '0' '1' '1' '1' '2' '2' '2' '3' '3']
 ['0' '1' '1' '2' '2' '2' '3' '4' '4' '4']
 ['0' '1' '1' '2' '2' '2' '2' '2' '4' '4']]

如果累积值计数器的差异,我需要重置计数,所以使用 pandas。

首先创建DataFrame:

df = pd.DataFrame(arr)
print (df)
   0  1  2  3  4  5  6  7  8  9
0  0  1  1  2  2  3  3  4  4  4
1  1  2  2  2  3  3  3  4  4  4
2  0  2  2  2  2  3  3  4  4  4
3  0  1  2  2  3  3  3  4  4  4
4  0  1  1  1  2  2  2  2  4  4
5  0  0  1  1  2  3  3  3  4  4
6  0  0  2  2  2  2  2  2  3  4
7  0  0  1  1  1  2  2  2  3  3
8  0  1  1  2  2  2  3  4  4  4
9  0  1  1  2  2  2  2  2  4  4

一栏的工作原理:

先比较移位后的数据,加上累加和:

a = (df[0] != df[0].shift()).cumsum()
print (a)
0    1
1    2
2    3
3    3
4    3
5    3
6    3
7    3
8    3
9    3
Name: 0, dtype: int32

然后拨打GroupBy.cumcount:

b = a.groupby(a).cumcount() + 1
print (b)
0    1
1    1
2    1
3    2
4    3
5    4
6    5
7    6
8    7
9    8
dtype: int64

如果想对所有列应用解决方案,可以使用apply

print (df.apply(lambda x: x.groupby((x != x.shift()).cumsum()).cumcount() + 1))
   0  1  2  3  4  5  6  7  8  9
0  1  1  1  1  1  1  1  1  1  1
1  1  1  1  2  1  2  2  2  2  2
2  1  2  2  3  1  3  3  3  3  3
3  2  1  3  4  1  4  4  4  4  4
4  3  2  1  1  1  1  1  1  5  5
5  4  1  2  2  2  1  1  1  6  6
6  5  2  1  1  3  1  1  1  1  7
7  6  3  1  1  1  2  2  2  2  1
8  7  1  2  1  1  3  1  1  1  1
9  8  2  3  2  2  4  1  1  2  2

但它很慢,因为数据量很大。是否可以创建一些快速的 numpy 解决方案?

我发现 solutions 仅适用于一维数组。

【问题讨论】:

    标签: python arrays numpy counter cumulative-sum


    【解决方案1】:

    总体思路

    考虑我们执行此累积计数的一般情况,或者如果您将它们视为范围,我们可以将它们称为 - 分组范围。

    现在,这个想法很简单 - 比较沿各自轴的一次性切片以查找不等式。在每行/列的开头使用True 填充(取决于计数轴)。

    然后,它变得复杂 - 设置一个 ID 数组,目的是我们将得到一个最终的 cumsum,这将是其扁平顺序所需的输出。因此,设置从初始化一个与输入数组具有相同形状的1s 数组开始。在输入中的每个组开始处,将 ID 数组与之前的组长度进行偏移。按照代码(应该提供更多见解)了解我们将如何为每一行执行此操作 -

    def grp_range_2drow(a, start=0):
        # Get grouped ranges along each row with resetting at places where
        # consecutive elements differ
        
        # Input(s) : a is 2D input array
        
        # Store shape info
        m,n = a.shape
        
        # Compare one-off slices for each row and pad with True's at starts
        # Those True's indicate start of each group
        p = np.ones((m,1),dtype=bool)
        a1 = np.concatenate((p, a[:,:-1] != a[:,1:]),axis=1)
        
        # Get indices of group starts in flattened version
        d = np.flatnonzero(a1)
    
        # Setup ID array to be cumsumed finally for desired o/p 
        # Assign into starts with previous group lengths. 
        # Thus, when cumsumed on flattened version would give us flattened desired
        # output. Finally reshape back to 2D  
        c = np.ones(m*n,dtype=int)
        c[d[1:]] = d[:-1]-d[1:]+1
        c[0] = start
        return c.cumsum().reshape(m,n)
    

    我们将扩展它以解决行和列的一般情况。对于列的情况,我们将简单地转置,提供给较早的行解决方案,最后转回,就像这样 -

    def grp_range_2d(a, start=0, axis=1):
        # Get grouped ranges along specified axis with resetting at places where
        # consecutive elements differ
        
        # Input(s) : a is 2D input array
    
        if axis not in [0,1]:
            raise Exception("Invalid axis")
    
        if axis==1:
            return grp_range_2drow(a, start=start)
        else:
            return grp_range_2drow(a.T, start=start).T
    

    示例运行

    让我们考虑一个示例运行,它将沿着每列找到分组范围,每个组以 1 开头 -

    In [330]: np.random.seed(0)
    
    In [331]: a = np.random.randint(1,3,(10,10))
    
    In [333]: a
    Out[333]: 
    array([[1, 2, 2, 1, 2, 2, 2, 2, 2, 2],
           [2, 1, 1, 2, 1, 1, 1, 1, 1, 2],
           [1, 2, 2, 1, 1, 2, 2, 2, 2, 1],
           [2, 1, 2, 1, 2, 2, 1, 2, 2, 1],
           [1, 2, 1, 2, 2, 2, 2, 2, 1, 2],
           [1, 2, 2, 2, 2, 1, 2, 1, 1, 2],
           [2, 1, 2, 1, 2, 1, 1, 1, 1, 1],
           [2, 2, 1, 1, 1, 2, 2, 1, 2, 1],
           [1, 2, 1, 2, 2, 2, 2, 2, 2, 1],
           [2, 2, 1, 1, 2, 1, 1, 2, 2, 1]])
    
    In [334]: grp_range_2d(a, start=1, axis=0)
    Out[334]: 
    array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
           [1, 1, 1, 1, 1, 1, 1, 1, 1, 2],
           [1, 1, 1, 1, 2, 1, 1, 1, 1, 1],
           [1, 1, 2, 2, 1, 2, 1, 2, 2, 2],
           [1, 1, 1, 1, 2, 3, 1, 3, 1, 1],
           [2, 2, 1, 2, 3, 1, 2, 1, 2, 2],
           [1, 1, 2, 1, 4, 2, 1, 2, 3, 1],
           [2, 1, 1, 2, 1, 1, 1, 3, 1, 2],
           [1, 2, 2, 1, 1, 2, 2, 1, 2, 3],
           [1, 3, 3, 1, 2, 1, 1, 2, 3, 4]])
    

    因此,为了解决我们的数据帧输入和输出问题,它会是 -

    out = grp_range_2d(df.values, start=1,axis=0)
    pd.DataFrame(out,columns=df.columns,index=df.index)
    

    【讨论】:

      【解决方案2】:

      还有 numba 解决方案。对于这样棘手的问题,它总是胜出,在这里是 numpy 的 7 倍,因为只完成了一次 res 传递。

      from numba import njit 
      @njit
      def thefunc(arrc):
          m,n=arrc.shape
          res=np.empty((m+1,n),np.uint32)
          res[0]=1
          for i in range(1,m+1):
              for j in range(n):
                  if arrc[i-1,j]:
                      res[i,j]=res[i-1,j]+1
                  else : res[i,j]=1
          return res 
      
      def numbering(arr):return thefunc(arr[1:]==arr[:-1])
      

      我需要将arr[1:]==arr[:-1] 外部化,因为 numba 不支持字符串。

      In [75]: %timeit numbering(arr)
      13.7 µs ± 373 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
      
      In [76]: %timeit grp_range_2dcol(arr)
      111 µs ± 18.3 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
      

      对于更大的数组(100 000 行 x 100 列),间隙不是那么宽:

      In [168]: %timeit a=grp_range_2dcol(arr)
      1.54 s ± 11.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      
      In [169]: %timeit a=numbering(arr)
      625 ms ± 43.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      

      如果arr可以转换为'S8',我们可以赢得很多时间:

      In [398]: %timeit arr[1:]==arr[:-1]
      584 ms ± 12.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      
      In [399]: %timeit arr.view(np.uint64)[1:]==arr.view(np.uint64)[:-1]
      196 ms ± 18.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      

      【讨论】:

      • 也可以为大数据创建计时,例如。 G。 100k 行和 100 列?谢谢。
      • @jezrael 我只是用numpy 添加了另一种方式,所以我对大数据做了一些计时,似乎numba 无论如何都更快
      【解决方案3】:

      按列使用Divakar 的方法相当快,即使这样也可能有一个完全向量化的方法。

      #function of Divakar
      def grp_range(a):
          idx = a.cumsum()
          id_arr = np.ones(idx[-1],dtype=int)
          id_arr[0] = 0
          id_arr[idx[:-1]] = -a[:-1]+1
          return id_arr.cumsum()
      
      #create the equivalent of (df != df.shift()).cumsum() but faster
      arr_sum = np.vstack([np.ones(10), np.cumsum((arr != np.roll(arr, 1, 0))[1:],0)+1])
      
      #use grp_range column wise on arr_sum
      arr_result = np.array([grp_range(np.unique(arr_sum[:,i],return_counts=1)[1]) 
                             for i in range(arr_sum.shape[1])]).T+1
      

      检查相等性:

      # of the cumsum
      print (((df != df.shift()).cumsum() == 
               np.vstack([np.ones(10), np.cumsum((arr != np.roll(arr, 1, 0))[1:],0)+1]))
               .all().all())
      #True
      
      print ((df.apply(lambda x: x.groupby((x != x.shift()).cumsum()).cumcount() + 1) ==
              np.array([grp_range(np.unique(arr_sum[:,i],return_counts=1)[1]) 
                        for i in range(arr_sum.shape[1])]).T+1)
              .all().all())
      #True
      

      还有速度:

      %timeit df.apply(lambda x: x.groupby((x != x.shift()).cumsum()).cumcount() + 1)
      #19.4 ms ± 2.97 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
      
      %%timeit
      arr_sum = np.vstack([np.ones(10), np.cumsum((arr != np.roll(arr, 1, 0))[1:],0)+1])
      arr_res = np.array([grp_range(np.unique(arr_sum[:,i],return_counts=1)[1]) 
                          for i in range(arr_sum.shape[1])]).T+1
      
      #562 µs ± 82.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
      

      编辑:使用Numpy,您也可以使用np.maximum.accumulatenp.arange

      def accumulate(arr):
          n,m = arr.shape
          arr_arange = np.arange(1,n+1)[:,np.newaxis]
          return np.concatenate([ np.ones((1,m)), 
                                 arr_arange[1:] - np.maximum.accumulate(arr_arange[:-1]*
                            (arr[:-1,:] != arr[1:,:]))],axis=0)
      

      一些时机

      arr_100 = np.sort(np.random.randint(50, size=(100000, 100)), axis=1).astype(str)
      

      np.maximum.accumulate的解决方案

      %timeit accumulate(arr_100)
      #520 ms ± 72 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      

      Divakar的解决方案

      %timeit grp_range_2drow(arr_100.T, start=1).T
      #1.15 s ± 64.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      

      使用B. M. 的 Numba 解决方案

      %timeit numbering(arr_100)
      #228 ms ± 31.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      

      【讨论】: