Python科学计算(第2版)
上QQ阅读APP看书,第一时间看更新

2.4.3 大小与排序

与本节内容对应的Notebook为:02-numpy/numpy-410-functions-sort.ipynb。

本节介绍的函数如表2-5所示。

表2-5 本节要介绍的函数

用min()和max()可以计算数组的最小值和最大值,它们都有axis、out、keepdims等参数。这些参数的用法和sum()基本相同,但是axis参数不支持序列。此外,ptp()计算最大值和最小值之间的差,有axis和out参数。这里就不再多举例了,请读者自行查看函数的文档。minimum()和maximum()用于比较两个数组对应下标的元素,返回数组的形状为两参数数组广播之后的形状。

    a = np.array([1, 3, 5, 7])
    b = np.array([2, 4, 6])
    np.maximum(a[None, :], b[:, None])
    array([[2, 3, 5, 7],
           [4, 4, 5, 7],
           [6, 6, 6, 7]])

用argmax()和argmin()可以求最大值和最小值的下标。如果不指定axis参数,则返回平坦化之后的数组下标,例如下面的程序找到a中最大值的下标,有多个最值时得到第一个最值的下标:

    np.random.seed(42)
    a = np.random.randint(0, 10, size=(4, 5))
    max_pos = np.argmax(a)
    max_pos
    5

下面查看a平坦化之后的数组中下标为max_pos的元素:

    a.ravel()[max_pos]  np.max(a)
    ------------------  ---------
    9                   9        

可以通过unravel_index()将一维数组的下标转换为多维数组的下标,它的第一个参数为一维数组的下标,第二个参数是多维数组的形状:

    idx = np.unravel_index(max_pos, a.shape)
    idx   a[idx]
    ------  ------
    (1, 0)  9     

当使用axis参数时,可以沿着指定轴计算最大值的下标。例如下面的结果表示,在数组a中第0行的最大值的下标为2,第1行的最大值的下标为0:

    idx = np.argmax(a, axis=1)
    idx
    array([2, 0, 1, 2])

使用下面的语句可以通过idx选出每行的最大值:

    a[np.arange(a.shape[0]), idx]
    array([7, 9, 7, 7])

数组的sort()方法对数组进行排序,它会改变数组的内容;而sort()函数则返回一个新数组,不改变原始数组。它们的axis默认值都为-1,即沿着数组的最终轴进行排序。sort()函数的axis参数可以设置为None,此时它将得到平坦化之后进行排序的新数组。在下面的例子中,np.sort(a)对a中每行的数值进行排序,而np.sort(a, axis=0)对数组a每列上的数值进行排序。

        np.sort(a)    np.sort(a, axis=0)
    -----------------  ------------------
    [[3, 4, 6, 6, 7],  [[3, 1, 6, 2, 1], 
     [2, 4, 6, 7, 9],   [4, 2, 7, 4, 4], 
     [2, 3, 5, 7, 7],   [6, 3, 7, 5, 5], 
     [1, 1, 4, 5, 7]]   [9, 7, 7, 7, 6]] 

argsort()返回数组的排序下标,参数axis的默认值为-1:

    sort_axis1 = np.argsort(a)
    sort_axis0 = np.argsort(a, axis=0)
        sort_axis1         sort_axis0   
    -----------------  -----------------
    [[1, 3, 0, 4, 2],  [[2, 3, 1, 2, 3],
     [1, 4, 2, 3, 0],   [3, 1, 0, 0, 1],
     [3, 0, 4, 1, 2],   [0, 0, 2, 3, 2],
     [1, 4, 0, 3, 2]]   [1, 2, 3, 1, 0]]

为了使用sort_axis0和sort_axis1计算排序之后的数组,即np.sort(a)的结果,需要产生非排序轴的下标。下面使用ogrid对象产生第0轴和第1轴的下标axis0和axis1:

    axis0, axis1 = np.ogrid[:a.shape[0], :a.shape[1]]

然后使用这些下标数组得到排序之后的数组:

    a[axis0, sort_axis1]  a[sort_axis0, axis1]
    --------------------  --------------------
    [[3, 4, 6, 6, 7],     [[3, 1, 6, 2, 1],
     [2, 4, 6, 7, 9],      [4, 2, 7, 4, 4],
     [2, 3, 5, 7, 7],      [6, 3, 7, 5, 5],
     [1, 1, 4, 5, 7]]      [9, 7, 7, 7, 6]]

使用这种方法可以对两个相关联的数组进行排序,即从数组a产生排序下标数组,然后使用它对数组b进行排序。排序相关的函数或方法还可以通过kind参数指定排序算法,对于结构数组可以通过order参数指定排序所使用的字段。

lexsort()类似于Excel中的多列排序。它的参数是一个形状为(k, N)的数组,或者包含k个长度为N的数组的序列,可以把它理解为Excel中N行k列的表格。lexsort()返回排序下标,注意数组中最后的列为排序的主键。在下面的例子中,按照“姓名-年龄”的顺序对数据排序:

    names = ["zhang", "wang", "li", "wang", "zhang"]
    ages = [37, 33, 32, 31, 36]
    idx = np.lexsort([ages, names])
    sorted_data = np.array(zip(names, ages), "O")[idx]
          idx          sorted_data  
    ---------------  ---------------
    [2, 3, 1, 4, 0][['li', 32],
    ['wang', 31],
    ['wang', 33],
    ['zhang', 36],
    ['zhang', 37]]

如果需要对一个N行k列的数组以第一列为主键进行排序,可以先通过切片下标::-1反转数组的第1轴,然后对其转置进行lexsort()排序:

    b = np.random.randint(0, 10, (5, 3))
         b       b[np.lexsort(b[:, ::-1].T)]
    -----------  ---------------------------
    [[4, 0, 9],  [[3, 8, 2],
     [5, 8, 0],   [4, 0, 9],
     [9, 2, 6],   [4, 2, 6],
     [3, 8, 2],   [5, 8, 0],
     [4, 2, 6]]   [9, 2, 6]]

partition()和argpartition()对数组进行分割,可以很快地找出排序之后的前k个元素,由于它不需要对整个数组进行完整排序,因此速度比调用sort()之后再取前k个元素要快许多。下面从10万个随机数中找出前5个最小的数,注意partition()得到的前5个数值没有按照从小到大排序,如果需要,可以再调用sort()对这5个数进行排序即可:

    r = np.random.randint(10, 1000000, 100000)
       np.sort(r)[:5]     np.partition(r, 5)[:5]
    --------------------  ----------------------
    [15, 23, 25, 37, 47]  [15, 47, 25, 37, 23]  

下面用%timeit测试sort()和partition()的运行速度:

    %timeit np.sort(r)[:5]
    %timeit np.sort(np.partition(r, 5)[:5])
    100 loops, best of 3: 6.02 ms per loop
    1000 loops, best of 3: 348 µs per loop

用median()可以获得数组的中值,即对数组进行排序之后,位于数组中间位置的值。当长度是偶数时,则得到正中间两个数的平均值。它也可以指定axis和out参数:

    np.median(a, axis=1)
    array([ 6.,  6.,  5.,  4.])

percentile()用于计算百分位数,即将数值从小到大排列,计算处于p%位置上的值。下面的程序计算标准正态分布随机数的绝对值在68.3%、95.4%以及99.7%处的百分位数,它们应该约等于1倍、2倍和3倍的标准差:

    r = np.abs(np.random.randn(100000))
    np.percentile(r, [68.3, 95.4, 99.7])
    array([ 1.00029686,  1.99473003,  2.9614485 ])

当数组中的元素按照从小到大的顺序排列时,可以使用searchsorted()在数组中进行二分搜索。在下面的例子中,a是一个已经排好序的列表,v是需要搜索的数值列表。searchsorted()返回一个下标数组,将v中对应的元素插入到a中的位置,能够保持数据的升序排列。当v中的元素在a中出现时,通过side参数指定返回最左端的下标还是最右端的下标。在下面的例子中,16放到a的下标为3、4、5的位置都能保持升序排列,side参数为默认值"left"时返回3,而为"right"时返回5。

    a = [2, 4, 8, 16, 16, 32]
    v = [1, 5, 33, 16]
    np.searchsorted(a, v)  np.searchsorted(a, v, side="right")
    ---------------------  -----------------------------------
    [0, 2, 6, 3]           [0, 2, 6, 5]                       

searchsorted()可以用于在两个数组中查找相同的元素。下面看一个比较复杂的例子:有x和y两个一维数组,找到y中每个元素在x中的下标。若不存在,将下标设置为-1。

    x = np.array([3, 5, 7, 1, 9, 8, 6, 10])
    y = np.array([2, 1, 5, 10, 100, 6])
    
    def get_index_searchsorted(x, y):
        index = np.argsort(x) ❶
        sorted_x = x[index] ❷
        sorted_index = np.searchsorted(sorted_x, y) ❸
        yindex = np.take(index, sorted_index, mode="clip") ❹
        mask = x[yindex] != y ❺
        yindex[mask] = -1
        return yindex
    
    get_index_searchsorted(x, y)
    array([-1,  3,  1,  7, -1,  6])

❶由于x并不是按照升序排列,因此先调用argsort()获得升序排序的下标index。❷使用index获得将x排序之后的sorted_x。❸使用searchsorted()在sorte_x中搜索y中每个元素对应的下标sorted_index。

❹如果搜索的值大于x的最大值,那么下标会越界,因此这里调用take()函数,take(index, sorted_index)与index[sorted_index]的含义相同,但是能处理下标越界的情况。通过设置mode参数为"clip",将下标限定在0到len(x)-1之间。

❺使用yindex获取x中的元素并和y比较,若值相同则表示该元素确实存在于x之中,否则表示不存在。

这段算法有些复杂,但由于利用了NumPy提供的数组操作函数,它的运算速度比使用字典的纯Python程序要快。下面我们用两个较大的数组测试运算速度。为了比较的公平性,我们调用tolist()方法将数组转换成列表:

    x = np.random.permutation(1000)[:100]
    y = np.random.randint(0, 1000, 2000)
    xl, yl = x.tolist(), y.tolist()
    
    def get_index_dict(x, y):
        idx_map = {v:i for i,v in enumerate(x)}
        yindex = [idx_map.get(v, -1) for v in y]
        return yindex
    
    yindex1 = get_index_searchsorted(x, y)
    yindex2 = get_index_dict(xl, yl)
    print np.all(yindex1 == yindex2)
    
    %timeit get_index_searchsorted(x, y)
    %timeit get_index_dict(xl, yl)
    True
    10000 loops, best of 3: 122 µs per loop
    1000 loops, best of 3: 368 µs per loop