如何使用numpy在线性时间内通过唯一值获取累积计数?

piR*_*red 6 python numpy pandas

请考虑以下列表short_listlong_list

short_list = list('aaabaaacaaadaaac')
np.random.seed([3,1415])
long_list = pd.DataFrame(
    np.random.choice(list(ascii_letters),
                     (10000, 2))
).sum(1).tolist()
Run Code Online (Sandbox Code Playgroud)

如何按唯一值计算累积计数?

我想使用numpy并在线性时间内完成.我希望这可以将时间与其他方法进行比较.用我的第一个提出的解决方案来说明这可能是最简单的

def pir1(l):
    s = pd.Series(l)
    return s.groupby(s).cumcount().tolist()

print(np.array(short_list))
print(pir1(short_list))

['a' 'a' 'a' 'b' 'a' 'a' 'a' 'c' 'a' 'a' 'a' 'd' 'a' 'a' 'a' 'c']
[0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 1]
Run Code Online (Sandbox Code Playgroud)

我试图使用折磨自己,np.unique因为它返回一个计数数组,一个反向数组和一个索引数组.我确信我可以通过这些来解决问题.我得到的最好的是pir4在二次时间内缩放.另请注意,我不关心计数是从1还是零开始,因为我们可以简单地加1或减1.

以下是我的一些尝试(没有一个回答我的问题)

%%cython
from collections import defaultdict

def get_generator(l):
    counter = defaultdict(lambda: -1)
    for i in l:
        counter[i] += 1
        yield counter[i]

def pir2(l):
    return [i for i in get_generator(l)]
Run Code Online (Sandbox Code Playgroud)
def pir3(l):
    return [i for i in get_generator(l)]

def pir4(l):
    unq, inv = np.unique(l, 0, 1, 0)
    a = np.arange(len(unq))
    matches = a[:, None] == inv
    return (matches * matches.cumsum(1)).sum(0).tolist()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

在此输入图像描述

Div*_*kar 5

这是一个使用自定义分组范围创建功能和np.unique获取计数的矢量化方法-

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()

count = np.unique(A,return_counts=1)[1]
out = grp_range(count)[np.argsort(A).argsort()]
Run Code Online (Sandbox Code Playgroud)

样品运行 -

In [117]: A = list('aaabaaacaaadaaac')

In [118]: count = np.unique(A,return_counts=1)[1]
     ...: out = grp_range(count)[np.argsort(A).argsort()]
     ...: 

In [119]: out
Out[119]: array([ 0,  1,  2,  0,  3,  4,  5,  0,  6,  7,  8,  0,  9, 10, 11,  1])
Run Code Online (Sandbox Code Playgroud)

为了获得这一点count,可以提出很少的其他替代方案,重点关注性能 -

np.bincount(np.unique(A,return_inverse=1)[1])
np.bincount(np.fromstring('aaabaaacaaadaaac',dtype=np.uint8)-97)
Run Code Online (Sandbox Code Playgroud)

此外,通过A包含single-letter字符,我们可以简单地使用 -

np.bincount(np.array(A).view('uint8')-97)
Run Code Online (Sandbox Code Playgroud)


piR*_*red 5

设置

short_list = np.array(list('aaabaaacaaadaaac'))
Run Code Online (Sandbox Code Playgroud)

职能

  • dfill 接受一个数组并返回数组更改的位置并重复该索引位置直到下一次更改。

    # dfill
    # 
    # Example with short_list
    #
    #    0  0  0  3  4  4  4  7  8  8  8 11 12 12 12 15
    # [  a  a  a  b  a  a  a  c  a  a  a  d  a  a  a  c]
    #
    # Example with short_list after sorting
    #
    #    0  0  0  0  0  0  0  0  0  0  0  0 12 13 13 15
    # [  a  a  a  a  a  a  a  a  a  a  a  a  b  c  c  d]
    
    Run Code Online (Sandbox Code Playgroud)
  • argunsort返回给定argsort数组撤销排序所需的排列。我通过这篇文章知道了这种方法的存在. 有了这个,我可以获取argsort数组并用它对我的数组进行排序。然后我可以撤消排序,而无需再次排序的开销。
  • cumcount将数组排序吧,找到dfill数组。一个np.arange不太dfill会给我累计计数。然后我取消排序

    # cumcount
    # 
    # Example with short_list
    #
    # short_list:
    # [ a  a  a  b  a  a  a  c  a  a  a  d  a  a  a  c]
    # 
    # short_list.argsort():
    # [ 0  1  2  4  5  6  8  9 10 12 13 14  3  7 15 11]
    #
    # Example with short_list after sorting
    #
    # short_list[short_list.argsort()]:
    # [ a  a  a  a  a  a  a  a  a  a  a  a  b  c  c  d]
    # 
    # dfill(short_list[short_list.argsort()]):
    # [ 0  0  0  0  0  0  0  0  0  0  0  0 12 13 13 15]
    # 
    # np.range(short_list.size):
    # [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15]
    #
    # np.range(short_list.size) - 
    #     dfill(short_list[short_list.argsort()]):
    # [ 0  1  2  3  4  5  6  7  8  9 10 11  0  0  1  0]
    # 
    # unsorted:
    # [ 0  1  2  0  3  4  5  0  6  7  8  0  9 10 11  1]
    
    Run Code Online (Sandbox Code Playgroud)
  • foo @hpaulj 推荐的函数使用 defaultdict
  • div @Divakar 推荐的功能(旧的,我确定他会更新它)

代码

def dfill(a):
    n = a.size
    b = np.concatenate([[0], np.where(a[:-1] != a[1:])[0] + 1, [n]])
    return np.arange(n)[b[:-1]].repeat(np.diff(b))

def argunsort(s):
    n = s.size
    u = np.empty(n, dtype=np.int64)
    u[s] = np.arange(n)
    return u

def cumcount(a):
    n = a.size
    s = a.argsort(kind='mergesort')
    i = argunsort(s)
    b = a[s]
    return (np.arange(n) - dfill(b))[i]

def foo(l):
    n = len(l)
    r = np.empty(n, dtype=np.int64)
    counter = defaultdict(int)
    for i in range(n):
        counter[l[i]] += 1
        r[i] = counter[l[i]]
    return r - 1

def div(l):
    a = np.unique(l, return_counts=1)[1]
    idx = a.cumsum()
    id_arr = np.ones(idx[-1],dtype=int)
    id_arr[0] = 0
    id_arr[idx[:-1]] = -a[:-1]+1
    rng = id_arr.cumsum()
    return rng[argunsort(np.argsort(l))]
Run Code Online (Sandbox Code Playgroud)

示范

cumcount(short_list)

array([ 0,  1,  2,  0,  3,  4,  5,  0,  6,  7,  8,  0,  9, 10, 11,  1])
Run Code Online (Sandbox Code Playgroud)

时间测试

代码

functions = pd.Index(['cumcount', 'foo', 'foo2', 'div'], name='function')
lengths = pd.RangeIndex(100, 1100, 100, 'array length')
results = pd.DataFrame(index=lengths, columns=functions)

from string import ascii_letters

for i in lengths:
    a = np.random.choice(list(ascii_letters), i)
    for j in functions:
        results.set_value(
            i, j,
            timeit(
                '{}(a)'.format(j),
                'from __main__ import a, {}'.format(j),
                number=1000
            )
        )

results.plot()
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明