
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