如何在numpy中反转排列数组

45

给定一个自索引(不确定是否是正确的术语)的numpy数组,例如:

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

这表示这个排列=> 是一个箭头):

0 => 3
1 => 2
2 => 0
3 => 1

我想用纯numpy的方法创建一个表示逆变换的数组,而不是在Python中手动计算。我希望得到上面例子中的结果:

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

等价于

0 <= 3                0 => 2
1 <= 2       or       1 => 3
2 <= 0                2 => 1
3 <= 1                3 => 0

看起来很简单,但我就是想不出该如何做。我尝试过搜索,但没有找到任何相关的东西。


a = np.array([1, 1, 1, 1]) 应该返回什么? - eumiro
2
您可以假设这种情况不会出现。 - Lauritz V. Thaulow
3个回答

60

简短回答

def invert_permutation(p):
    """Return an array s with which np.array_equal(arr[p][s], arr) is True.
    The array_like argument p must be some permutation of 0, 1, ..., len(p)-1.
    """
    p = np.asanyarray(p) # in case p is a tuple, etc.
    s = np.empty_like(p)
    s[p] = np.arange(p.size)
    return s

在这里进行排序是过度设计了。 这只是一个单遍线性时间算法,内存需求恒定:


from __future__ import print_function
import numpy as np

p = np.array([3, 2, 0, 1])
s = np.empty(p.size, dtype=np.int32)
for i in np.arange(p.size):
    s[p[i]] = i

print('s =', s)

以上代码输出
 s = [2 3 1 0]

以下是关于以上for循环的高效向量化问题。如果只想知道结论,请跳到该答案的末尾。


(原始回答时间为2014年8月27日;时间对于NumPy 1.8有效。随后使用NumPy 1.11进行了更新。)

单遍,线性时间算法预计比np.argsort更快;有趣的是,上述for循环的平凡向量化(s[p] = xrange(p.size), 请参见索引数组)实际上略慢于np.argsort,只要p.size < 700000(在我的机器上,您的情况可能会不同):

import numpy as np

def np_argsort(p):
    return np.argsort(p)
    
def np_fancy(p):
    s = np.zeros(p.size, p.dtype) # np.zeros is better than np.empty here, at least on Linux
    s[p] = xrange(p.size) 
    return s

def create_input(n):
    np.random.seed(31)
    indices = np.arange(n, dtype = np.int32)
    return np.random.permutation(indices)

来自我的IPython笔记本:

p = create_input(700000)
%timeit np_argsort(p)
10 loops, best of 3: 72.7 ms per loop
%timeit np_fancy(p)
10 loops, best of 3: 70.2 ms per loop

最终渐进复杂度开始起作用(对于argsortO(n log n),而对于单次遍历算法是O(n)),在足够大的n = p.size(在我的机器上阈值约为70万)后,单次遍历算法将始终更快。

然而,有一种不那么直接的方法可以使用np.put对上述for循环进行向量化:

def np_put(p):
    n = p.size
    s = np.zeros(n, dtype = np.int32)
    i = np.arange(n, dtype = np.int32)
    np.put(s, p, i) # s[p[i]] = i 
    return s

对于n = 700,000(与上面相同的大小),会得到:

p = create_input(700000)
%timeit np_put(p)
100 loops, best of 3: 12.8 ms per loop

这几乎是零成本的5.6倍速提升!

公正地说,对于较小的n(在我的机器上约为n = 1210),np.argsort仍然比np.put方法更快:

p = create_input(1210)
%timeit np_argsort(p)
10000 loops, best of 3: 25.1 µs per loop
%timeit np_fancy(p)
10000 loops, best of 3: 118 µs per loop
%timeit np_put(p)
10000 loops, best of 3: 25 µs per loop

这很可能是因为我们在np.arange()调用时,使用了np_put方法分配并填充了一个额外的数组。
虽然您没有要求Cython解决方案,但出于好奇,我也测试了以下使用类型化内存视图的Cython解决方案的时间。
import numpy as np
cimport numpy as np

def in_cython(np.ndarray[np.int32_t] p):    
    cdef int i
    cdef int[:] pmv
    cdef int[:] smv 
    pmv = p
    s = np.empty(p.size, dtype=np.int32)
    smv = s
    for i in xrange(p.size):
        smv[pmv[i]] = i
    return s

时间:

p = create_input(700000)
%timeit in_cython(p)
100 loops, best of 3: 2.59 ms per loop

因此,np.put的解决方案仍然不够快(输入大小为12.8毫秒; argsort花费了72.7毫秒)。

2017年2月3日更新-使用NumPy 1.11

Jamie、Andris和Paul在下面的评论中指出了使用fancy indexing的性能问题已得到解决。Jamie表示该问题已在NumPy 1.9中得到解决。我在2014年使用的机器上测试了Python 3.5和NumPy 1.11。

def invert_permutation(p):
    s = np.empty(p.size, p.dtype)
    s[p] = np.arange(p.size)
    return s

时间:

p = create_input(880)
%timeit np_argsort(p)
100000 loops, best of 3: 11.6 µs per loop
%timeit invert_permutation(p)
100000 loops, best of 3: 11.5 µs per loop

确实是一项重大改进!


结论

总的来说,我会选择在代码清晰度方面提到的简短回答方法。在我看来,它比argsort更不明显,并且对于大输入大小而言速度更快。如果速度成为问题,我会选择Cython解决方案。


+1 伟大的思想总是相似的!;-) 当你在回答一个两年前的问题时,我正在向numpy的unique函数发送一个使用非常类似于这个技术的PR,请参见此处。顺便说一句,在numpy 1.9中,np.put相对于花式索引的优势已经基本消失了。 - Jaime
@Jaime 谢谢你的好消息!我发现使用花式索引是最干净的解决方案;很遗憾它曾经非常缓慢。很高兴知道在 NumPy 1.9 中它已经大部分消失了。 - Ali
1
s[p]=np.arange(p.size)这行代码更加简洁明了,在我的机器上比np.put快两倍(我知道,我知道)。 - Andris Birkmanis
1
@Paul 谢谢你的信息!确实,自从 NumPy 1.9 版本以来,使用 np.put() 已经没有意义了。我会尽快更新我的答案! - Ali
1
@MateenUlhaq 你认为使用len(p)代替p.size如何? - Ali
显示剩余10条评论

41

排列p的逆是对np.arange(n)进行排序得到p的索引数组s

p[s] == np.arange(n)

必须全部为真。这样的s正是np.argsort返回的。

>>> p = np.array([3, 2, 0, 1])
>>> np.argsort(p)
array([2, 3, 1, 0])
>>> p[np.argsort(p)]
array([0, 1, 2, 3])

1
@lazyr:加上一个解释。 - Fred Foo
6
有一个更简单的单次遍历算法:基本上任务是 s[p] = xrange(p.size),请查看我的答案。 - Ali

11
我想为larsmans的正确答案提供更多背景。使用矩阵表示置换,可以找到argsort正确的原因。置换矩阵P的数学优势在于该矩阵“作用于向量”,即置换矩阵乘以一个向量会对向量进行置换。
你的置换看起来像这样:
import numpy as np
a   = np.array([3,2,0,1])
N   = a.size
rows = np.arange(N)
P   = np.zeros((N,N),dtype=int)
P[rows,a] = 1

[[0 0 0 1]
 [0 0 1 0]
 [1 0 0 0]
 [0 1 0 0]]

给定一个置换矩阵,我们可以通过乘以它的逆矩阵P^-1来“撤销”乘法。置换矩阵的美妙之处在于它们是正交的,因此P*P^(-1)=I,或者换句话说P^-1=P^T,即逆矩阵等于转置矩阵。这意味着我们可以取转置矩阵的索引来找到反转的置换向量:

inv_a = np.where(P.T)[1]
[2 3 1 0]

如果你仔细想一想,这其实和找到排序P列的索引是完全相同的!


非常感谢您的解释!收益匪浅。 - Lauritz V. Thaulow

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