【问题标题】:Rank items in an array using Python/NumPy, without sorting array twice使用 Python/NumPy 对数组中的项目进行排名,无需对数组进行两次排序
【发布时间】:2011-07-14 04:01:42
【问题描述】:

我有一个数字数组,我想创建另一个数组来表示第一个数组中每个项目的排名。我正在使用 Python 和 NumPy。

例如:

array = [4,2,7,1]
ranks = [2,1,3,0]

这是我想出的最佳方法:

array = numpy.array([4,2,7,1])
temp = array.argsort()
ranks = numpy.arange(len(array))[temp.argsort()]

有没有更好/更快的方法可以避免对数组进行两次排序?

【问题讨论】:

  • 你的最后一行相当于ranks = temp.argsort()

标签: python sorting numpy


【解决方案1】:

两次使用argsort,先获取数组的顺序,再获取排名:

array = numpy.array([4,2,7,1])
order = array.argsort()
ranks = order.argsort()

在处理二维(或更高维)数组时,请务必将轴参数传递给 argsort 以在正确的轴上排序。

【讨论】:

  • 请注意,如果数字在您的输入数组中重复(例如[4,2,7,1,1]),输出将根据这些数字的数组位置对这些数字进行排名([3,2,4,0,1]
  • 排序两次效率低下。 @Sven Marnach 的回答展示了如何通过一次调用 argsort 来完成排名。
  • @WarrenWeckesser:我刚刚测试了两者之间的区别,你对大数组是正确的,但是对于任何更小的(n 要好得多。
  • @WarrenWeckesser:实际上,我错了,这种方法更容易掌握。这两种方法也比 scipy.stats 方法快得多。结果:gist.github.com/naught101/14042d91a2d0f18a6ae4
  • @naught101:您的脚本中存在错误。 array = np.random.rand(10) 应该是 array = np.random.rand(n)
【解决方案2】:

这个问题是几年前的问题,接受的答案很棒,但我认为以下内容仍然值得一提。如果不介意对scipy的依赖,可以使用scipy.stats.rankdata

In [22]: from scipy.stats import rankdata

In [23]: a = [4, 2, 7, 1]

In [24]: rankdata(a)
Out[24]: array([ 3.,  2.,  4.,  1.])

In [25]: (rankdata(a) - 1).astype(int)
Out[25]: array([2, 1, 3, 0])

rankdata 的一个很好的特性是method 参数提供了几个处理平局的选项。例如,b 中出现了 3 次 20 和 2 次 40:

In [26]: b = [40, 20, 70, 10, 20, 50, 30, 40, 20]

默认将平均排名分配给并列值:

In [27]: rankdata(b)
Out[27]: array([ 6.5,  3. ,  9. ,  1. ,  3. ,  8. ,  5. ,  6.5,  3. ])

method='ordinal' 分配连续排名:

In [28]: rankdata(b, method='ordinal')
Out[28]: array([6, 2, 9, 1, 3, 8, 5, 7, 4])

method='min' 将已绑定值的最小排名分配给所有已绑定值:

In [29]: rankdata(b, method='min')
Out[29]: array([6, 2, 9, 1, 2, 8, 5, 6, 2])

有关更多选项,请参阅文档字符串。

【讨论】:

  • 是的,这是在边缘情况很重要的任何地方的最佳答案。
  • 我觉得有趣的是rankdata 似乎使用与接受的答案相同的机制在内部生成初始排名。
【解决方案3】:

在最后一步使用左侧的高级索引

array = numpy.array([4,2,7,1])
temp = array.argsort()
ranks = numpy.empty_like(temp)
ranks[temp] = numpy.arange(len(array))

这通过在最后一步中反转排列来避免两次排序。

【讨论】:

  • 完美,谢谢!我知道有一个解决方案,一旦我看到它就会很明显。我用timeit做了一些测试,这个方法对于小数组来说稍微慢一些。在我的机器上,当数组有 2,000 个元素时它们是相等的。在 20,000 个元素中,您的方法大约快 25%。
  • 关于如何按行执行此操作的任何建议?
  • 对于超过 1 个暗淡,请参阅下面的答案。
  • 只是一个次要更正,间接ranks[temp]在技术上不是slicing,而是indexing(参见@ 987654321@)
  • 看起来有点像scipy.stats.rankdataimplementation(用于序号)。
【解决方案4】:

有关平均排名的矢量化版本,请参见下文。我喜欢 np.unique,它确实拓宽了代码可以和不能有效矢量化的范围。除了避免 python for 循环之外,这种方法还避免了 'a' 上的隐式双循环。

import numpy as np

a = np.array( [4,1,6,8,4,1,6])

a = np.array([4,2,7,2,1])
rank = a.argsort().argsort()

unique, inverse = np.unique(a, return_inverse = True)

unique_rank_sum = np.zeros_like(unique)
np.add.at(unique_rank_sum, inverse, rank)
unique_count = np.zeros_like(unique)
np.add.at(unique_count, inverse, 1)

unique_rank_mean = unique_rank_sum.astype(np.float) / unique_count

rank_mean = unique_rank_mean[inverse]

print rank_mean

【讨论】:

  • 顺便说一句;我编写此代码以产生与其他平均排名代码相同的输出,但我可以想象一组重复数字的最小排名也同样有效。这可以更容易地获得 >>> unique, index, inverse = np.unique(a, True, True) >>> rank_min = rank[index][inverse]
  • 我的解决方案出现以下错误(numpy 1.7.1):AttributeError: 'numpy.ufunc' object has no attribute 'at'
  • 这需要更新版本的numpy;你的很古老
【解决方案5】:

假设您逐行处理数组 (axis=1),我尝试将这两种解决方案扩展到一维以上的数组 A。

我用行循环扩展了第一个代码;可能还可以改进

temp = A.argsort(axis=1)
rank = np.empty_like(temp)
rangeA = np.arange(temp.shape[1])
for iRow in xrange(temp.shape[0]): 
    rank[iRow, temp[iRow,:]] = rangeA

第二个,按照 k.rooijers 的建议,变成:

temp = A.argsort(axis=1)
rank = temp.argsort(axis=1)

我随机生成了 400 个形状为 (1000,100) 的数组;第一个代码大约需要 7.5,第二个代码需要 3.8。

【讨论】:

    【解决方案6】:

    使用argsort() 两次即可:

    >>> array = [4,2,7,1]
    >>> ranks = numpy.array(array).argsort().argsort()
    >>> ranks
    array([2, 1, 3, 0])
    

    【讨论】:

    【解决方案7】:

    除了解决方案的优雅和简短之外,还有性能问题。这是一个小基准:

    import numpy as np
    from scipy.stats import rankdata
    l = list(reversed(range(1000)))
    
    %%timeit -n10000 -r5
    x = (rankdata(l) - 1).astype(int)
    >>> 128 µs ± 2.72 µs per loop (mean ± std. dev. of 5 runs, 10000 loops each)
    
    %%timeit -n10000 -r5
    a = np.array(l)
    r = a.argsort().argsort()
    >>> 69.1 µs ± 464 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)
    
    %%timeit -n10000 -r5
    a = np.array(l)
    temp = a.argsort()
    r = np.empty_like(temp)
    r[temp] = np.arange(len(a))
    >>> 63.7 µs ± 1.27 µs per loop (mean ± std. dev. of 5 runs, 10000 loops each)
    

    【讨论】:

    • 好主意,但为了公平比较,您应该使用rankdata(l, method='ordinal') - 1
    • 最好使用随机生成的数组 - l = random.choices(range(500),k=10000) - 改变样本、大小和迭代以获得更完整的图片。 @yupbank 的双切片看起来很有趣
    【解决方案8】:

    我尝试了上述方法,但失败了,因为我有很多 zeores。是的,即使使用浮动,重复项也可能很重要。

    所以我编写了一个修改后的一维解决方案,添加了一个平局检查步骤:

    def ranks (v):
        import numpy as np
        t = np.argsort(v)
        r = np.empty(len(v),int)
        r[t] = np.arange(len(v))
        for i in xrange(1, len(r)):
            if v[t[i]] <= v[t[i-1]]: r[t[i]] = r[t[i-1]]
        return r
    
    # test it
    print sorted(zip(ranks(v), v))
    

    我相信它会尽可能高效。

    【讨论】:

      【解决方案9】:

      argsort 和 slice 是对称操作。

      尝试两次 slice 而不是 argsort 两次。因为 slice 比 argsort 快

      array = numpy.array([4,2,7,1])
      order = array.argsort()
      ranks = np.arange(array.shape[0])[order][order]
      

      【讨论】:

        【解决方案10】:

        我喜欢 k.rooijers 的方法,但正如 rcoup 所写,重复的数字是根据数组位置排列的。这对我没有好处,所以我修改了版本以对排名进行后处理并将任何重复的数字合并为一个组合的平均排名:

        import numpy as np
        a = np.array([4,2,7,2,1])
        r = np.array(a.argsort().argsort(), dtype=float)
        f = a==a
        for i in xrange(len(a)):
           if not f[i]: continue
           s = a == a[i]
           ls = np.sum(s)
           if ls > 1:
              tr = np.sum(r[s])
              r[s] = float(tr)/ls
           f[s] = False
        
        print r  # array([ 3. ,  1.5,  4. ,  1.5,  0. ])
        

        我希望这对其他人也有帮助,我试图找到其他解决方案,但找不到任何...

        【讨论】:

          【解决方案11】:

          答案之一的更通用版本:

          In [140]: x = np.random.randn(10, 3)
          
          In [141]: i = np.argsort(x, axis=0)
          
          In [142]: ranks = np.empty_like(i)
          
          In [143]: np.put_along_axis(ranks, i, np.repeat(np.arange(x.shape[0])[:,None], x.shape[1], axis=1), axis=0)
          

          请参阅How to use numpy.argsort() as indices in more than 2 dimensions? 以推广到更多暗淡。

          【讨论】:

            猜你喜欢
            • 2015-01-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-10-15
            • 1970-01-01
            • 2010-10-11
            • 2016-01-15
            相关资源
            最近更新 更多