从整数列表创建索引字典

9

我有一个由不同整数组成的数组a。现在我想创建一个字典,其中键是整数,值是在a中出现相应整数的索引数组。

import numpy

a = numpy.array([1, 1, 5, 5, 1])
u = numpy.unique(a)
d = {val: numpy.where(a==val)[0] for val in u}

print(d)

{1: array([0, 1, 4]), 5: array([2, 3])}

这段代码运行良好,但是似乎先调用unique函数,然后再跟着几个where函数有些浪费。

np.digitize也不是理想的选择,因为你必须事先指定bins。

有什么方法可以改进上述代码吗?


1
你真的需要字典吗?你打算用字典做什么?如果只有少量的值,每当需要时调用numpy.where()可能更好。这并不像听起来那么浪费——无论如何都需要遍历结果。 - Sven Marnach
1
你的少量整数是小的且非负吗? - Paul Panzer
1
@PaulPanzer 当然可以,如果有帮助的话。 - Nico Schlömer
@SvenMarnach 要使用 where,我至少需要找出哪些值存在,所以似乎无法避免使用 unique - Nico Schlömer
根据您的实际需求,np.unique函数的return_inverse=True选项可能对您有用。 - Mad Physicist
4个回答

5

方法一

基于排序的一种方法是 -

def group_into_dict(a): 
    # Get argsort indices
    sidx = a.argsort()

    # Use argsort indices to sort input array
    sorted_a = a[sidx]

    # Get indices that define the grouping boundaries based on identical elems
    cut_idx = np.flatnonzero(np.r_[True,sorted_a[1:] != sorted_a[:-1],True])

    # Form the final dict with slicing the argsort indices for values and
    # the starts as the keys
    return {sorted_a[i]:sidx[i:j] for i,j in zip(cut_idx[:-1], cut_idx[1:])}

示例运行 -


In [55]: a
Out[55]: array([1, 1, 5, 5, 1])

In [56]: group_into_dict(a)
Out[56]: {1: array([0, 1, 4]), 5: array([2, 3])}

对具有 1000000 元素和不同比例唯一数字的数组进行计时,以便将提出的算法与原始算法进行比较 -

# 1/100 unique numbers
In [75]: a = np.random.randint(0,10000,(1000000))

In [76]: %timeit {val: np.where(a==val)[0] for val in np.unique(a)}
1 loop, best of 3: 6.62 s per loop

In [77]: %timeit group_into_dict(a)
10 loops, best of 3: 121 ms per loop

# 1/1000 unique numbers
In [78]: a = np.random.randint(0,1000,(1000000))

In [79]: %timeit {val: np.where(a==val)[0] for val in np.unique(a)}
1 loop, best of 3: 720 ms per loop

In [80]: %timeit group_into_dict(a)
10 loops, best of 3: 92.1 ms per loop

# 1/10000 unique numbers
In [81]: a = np.random.randint(0,100,(1000000))

In [82]: %timeit {val: np.where(a==val)[0] for val in np.unique(a)}
10 loops, best of 3: 120 ms per loop

In [83]: %timeit group_into_dict(a)
10 loops, best of 3: 75 ms per loop

# 1/50000 unique numbers
In [84]: a = np.random.randint(0,20,(1000000))

In [85]: %timeit {val: np.where(a==val)[0] for val in np.unique(a)}
10 loops, best of 3: 60.8 ms per loop

In [86]: %timeit group_into_dict(a)
10 loops, best of 3: 60.3 ms per loop

所以,如果你只涉及到不超过20个唯一数,那就坚持使用原始方法继续往下看;否则,基于排序的方案似乎效果很好。


方法 #2

基于Pandas适用于极少量唯一数 -

In [142]: a
Out[142]: array([1, 1, 5, 5, 1])

In [143]: import pandas as pd

In [144]: {u:np.flatnonzero(a==u) for u in pd.Series(a).unique()}
Out[144]: {1: array([0, 1, 4]), 5: array([2, 3])}

在具有 1000000 元素和 20 个唯一元素的数组上的时间 -

In [146]: a = np.random.randint(0,20,(1000000))

In [147]: %timeit {u:np.flatnonzero(a==u) for u in pd.Series(a).unique()}
10 loops, best of 3: 35.6 ms per loop

# Original solution
In [148]: %timeit {val: np.where(a==val)[0] for val in np.unique(a)}
10 loops, best of 3: 58 ms per loop

对于较少的唯一元素 -

In [149]: a = np.random.randint(0,10,(1000000))

In [150]: %timeit {u:np.flatnonzero(a==u) for u in pd.Series(a).unique()}
10 loops, best of 3: 25.3 ms per loop

In [151]: %timeit {val: np.where(a==val)[0] for val in np.unique(a)}
10 loops, best of 3: 44.9 ms per loop

In [152]: a = np.random.randint(0,5,(1000000))

In [153]: %timeit {u:np.flatnonzero(a==u) for u in pd.Series(a).unique()}
100 loops, best of 3: 17.9 ms per loop

In [154]: %timeit {val: np.where(a==val)[0] for val in np.unique(a)}
10 loops, best of 3: 34.4 ms per loop

如何使用pandas处理少量元素的问题?

基于排序的方法 #1,对于20个唯一元素的情况,获取argsort索引是瓶颈所在-

In [164]: a = np.random.randint(0,20,(1000000))

In [165]: %timeit a.argsort()
10 loops, best of 3: 51 ms per loop

现在,基于 pandas 的函数可以给我们提供唯一的元素,无论是负数还是其他任何类型,我们仅需要将其与输入数组中的元素进行比较,而不需要进行排序。让我们看看在这方面的改进:
In [166]: %timeit pd.Series(a).unique()
100 loops, best of 3: 3.17 ms per loop

当然,接下来它需要获取np.flatnonzero的索引,这仍然使其相对更有效。

2
非常好的解决方案。这个至少有可能比原帖尝试的更有效率,但需要进行基准测试以查看它是否真正有所帮助。而且它仍然是O(n log n),而问题可以轻松地在O(n)中解决。 - Sven Marnach
@SvenMarnach 是的,这就是所提出的方法的限制点,因此在其下面有评论 - “因此,如果您只处理20个或更少的唯一数字,请坚持使用原始方法;”。 - Divakar
1
基准测试似乎是 perfplot 的任务。 - Nico Schlömer
还有一件事我刚刚注意到 - 使用 numpy.diff() 可能比使用 np.r_[True,sorted_a[1:] != sorted_a[:-1],True] 更快。 - Sven Marnach
@SvenMarnach 不,diff 会更慢。np.flatnonzero(sorted_a[1:] != sorted_a[:-1]) 然后连接起来可能值得一试。 - Divakar
显示剩余3条评论

3

使用ns,nd = 样本数,不同样本数,您的解决方案效率非常低,为O(ns*nd)

使用defaultdict的简单O(ns)方法:

from collections import defaultdict
d=defaultdict(list)
for i,x in enumerate(a):d[x].append(i)

很不幸,由于Python循环速度较慢,因此速度较慢,但如果nd/ns>1%,则比您的快。

如果nd/ns<<1(在这里通过numba进行优化),则可以使用另一种O(ns)线性解决方案:

@numba.njit
def filldict_(a):
    v=a.max()+1
    cnts= np.zeros(v,np.int64)
    for x in a:
        cnts[x]+=1
    g=cnts.max()
    res=np.empty((v,g),np.int64)
    cnts[:]=0
    i=0
    for x in a:
        res[x,cnts[x]]=i
        cnts[x]+=1
        i+=1
    return res,cnts,v

def filldict(a):
    res,cnts,v=filldict_(a)
    return {x:res[x,:cnts[x]] for x in range(v) if cnts[x]>0}    

更快的随机数生成器,适用于小密钥。运行:
In [51]: a=numpy.random.randint(0,100,1000000)

In [52]: %timeit d=group_into_dict(a) #Divakar
134 ms ± 17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [53]: %timeit c=filldict(a)
11.2 ms ± 1.78 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

如果键是大整数且负载较小,可以添加查找表机制。

3

如果只有少量的不同,使用argpartition而不是argsort可能会更加高效。缺点是这需要进行压缩步骤:

import numpy as np

def f_pp_1(a):
    ws = np.empty(a.max()+1, int)
    rng = np.arange(a.size)
    ws[a] = rng
    unq = rng[ws[a] == rng]
    idx = np.argsort(a[unq])
    unq = unq[idx]
    ws[a[unq]] = np.arange(len(unq))
    compressed = ws[a]
    counts = np.cumsum(np.bincount(compressed))
    return dict(zip(a[unq], np.split(np.argpartition(a, counts[:-1]), counts[:-1])))

如果唯一值很小,我们可以省略压缩步骤:
def f_pp_2(a):
    bc = np.bincount(a)
    keys, = np.where(bc)
    counts = np.cumsum(bc[keys])
    return dict(zip(keys, np.split(np.argpartition(a, counts[:-1]), counts[:-1])))

data = np.random.randint(0, 10, (5,))[np.random.randint(0, 5, (10000000,))]


sol = f_pp_1(data)
for k, v in sol.items():
    assert np.all(k == data[v])

对于具有小数量的不同值时,如果我们可以避免使用unique,则where是一种竞争性的选择:

def f_OP_plus(a):
    ws = np.empty(a.max()+1, int)
    rng = np.arange(a.size)
    ws[a] = rng
    unq = rng[ws[a] == rng]
    idx = np.argsort(a[unq])
    unq = unq[idx]
    return {val: np.where(a==val)[0] for val in unq}

这里是我使用与 @Divakar 相同的测试数组(randint(0, nd, (ns,)) -- nd, ns = 不同数的数量,样本的数量)进行的定时(3次取最佳,每个块10个):

nd, ns: 5, 1000000
OP                   39.88609421 ms
OP_plus              13.04150990 ms
Divakar_1            44.14700069 ms
Divakar_2            21.64940450 ms
pp_1                 33.15216140 ms
pp_2                 22.43267260 ms
nd, ns: 10, 1000000
OP                   52.33878891 ms
OP_plus              17.14743648 ms
Divakar_1            57.76002519 ms
Divakar_2            30.70066951 ms
pp_1                 45.33982391 ms
pp_2                 34.71166079 ms
nd, ns: 20, 1000000
OP                   67.47841339 ms
OP_plus              26.41335099 ms
Divakar_1            71.37646740 ms
Divakar_2            43.09316459 ms
pp_1                 57.16468811 ms
pp_2                 45.55416510 ms
nd, ns: 50, 1000000
OP                   98.91191521 ms
OP_plus              51.15756912 ms
Divakar_1            72.72288438 ms
Divakar_2            70.31920571 ms
pp_1                 63.78925461 ms
pp_2                 53.00321991 ms
nd, ns: 100, 1000000
OP                  148.17743159 ms
OP_plus              92.62091429 ms
Divakar_1            85.02774101 ms
Divakar_2           116.78823209 ms
pp_1                 77.01576019 ms
pp_2                 66.70976470 ms

如果我们不使用前 nd 个整数来生成唯一值,而是从 010000 中随机选择:

nd, ns: 5, 1000000
OP                   40.11689581 ms
OP_plus              12.99256920 ms
Divakar_1            42.13181480 ms
Divakar_2            21.55767360 ms
pp_1                 33.21835019 ms
pp_2                 23.46851982 ms
nd, ns: 10, 1000000
OP                   52.84317869 ms
OP_plus              17.96655210 ms
Divakar_1            57.74175161 ms
Divakar_2            32.31985010 ms
pp_1                 44.79893579 ms
pp_2                 33.42640731 ms
nd, ns: 20, 1000000
OP                   66.46886449 ms
OP_plus              25.78120639 ms
Divakar_1            66.58960858 ms
Divakar_2            42.47685110 ms
pp_1                 53.67698781 ms
pp_2                 44.53037870 ms
nd, ns: 50, 1000000
OP                   98.95576960 ms
OP_plus              50.79147881 ms
Divakar_1            72.44545210 ms
Divakar_2            70.91441818 ms
pp_1                 64.19071071 ms
pp_2                 53.36350428 ms
nd, ns: 100, 1000000
OP                  145.62422500 ms
OP_plus              90.82918381 ms
Divakar_1            76.92769479 ms
Divakar_2           115.24481240 ms
pp_1                 70.85122908 ms
pp_2                 58.85340699 ms

0

pandas 解决方案1:使用 groupby 及其 indices 函数

df = pd.DataFrame(a)
d = df.groupby(0).indices

a = np.random.randint(0,10000,(1000000))
%%timeit
df = pd.DataFrame(a)
d = df.groupby(0).indices
42.6 ms ± 2.95 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

a = np.random.randint(0,100,(1000000))
%%timeit
df = pd.DataFrame(a)
d = df.groupby(0).indices
22.3 ms ± 5.11 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
pandas方案2:仅使用groupby(如果您已经知道键或可以通过其他方法快速获取键)
a = np.random.randint(0,100,(1000000))
%%timeit 
df = pd.DataFrame(a)
d = df.groupby(0)
206 µs ± 14 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

groupby 本身非常快,但它不会给你键。如果您已经知道键,则可以将 groupby 对象直接用作字典。用法:

d.get_group(key).index  # index part is what you need!
缺点:d.get_group(key)本身需要花费相当长的时间。
%timeit d.get_group(10).index 
496 µs ± 56.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

所以这取决于您的应用程序以及您是否知道密钥,以决定是否采用此方法。

如果您所有的值都是正数,您可以使用np.nonzero(np.bincount(a))[0]以合理的速度获取密钥。(对于a = np.random.randint(0,1000,(1000000)),平均需要1.57毫秒±78.2微秒)


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