前言
NumPy 是第三方库,不是标准库,使用前需要pip install numpy。本机没有 Python 解释器也没有装 NumPy,所以本文的示例无法在本机运行验证,只能逐行人工推演;函数签名、返回值和版本差异一律以 NumPy 官方文档为准。
标题里的「排序与过滤」看着简单,实际藏了几个高频陷阱。最典型的一个是:numpy.sort返回的是排好序的副本,而ndarray.sort方法是原地排序、返回None。用过list.sort()的人对「原地排序返回None」有印象,但换成 NumPy 时又常常把它俩弄反——写成a = np.sort(a)固然没错,写成a = a.sort()就会把a变成None,然后在下游报一个和排序毫无关系的错误。
第二个陷阱是过滤手段选错。where、extract、布尔索引三者的结果不完全一样,尤其是where只有条件一个参数时,它返回的是一个下标元组,不是元素本身。
第三个是清洗顺序。缺测值(NaN)如果不清掉就去排序或裁剪,结果往往不是预期的——NaN 在排序里会被排到末尾,而裁剪也「裁」不掉它。
本文按「排序 → 取顺序 → 去重计数 → 过滤 → 清洗」的顺序,把这几组常用接口放在一起讲清楚,并给出可以对照官方文档的示例。
一、排序:numpy.sort是副本,ndarray.sort是原地
先看两者的签名和语义:
| 写法 | 是否原地 | 返回值 | 说明 |
|---|
numpy.sort(a, axis=-1, kind=None, order=None) | 否 | 排好序的副本 | 默认沿最后一根轴排序 |
ndarray.sort(axis=-1, kind=None, order=None) | 是 | None | 直接改动数组本身 |
注意两个地方:一是默认排序轴是最后一根(axis=-1),不是「把整个数组展平再排」——对二维数组,默认是按行各排各的;二是ndarray.sort的返回值是None,官方的签名里就没有返回值的概念,它就是原地操作。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.array([3, 1, 2])
b = np.sort(a) # 副本
print(b) # [1 2 3]
print(a) # [3 1 2] —— a 没变
c = np.array([3, 1, 2])
ret = c.sort() # 原地
print(ret) # None
print(c) # [1 2 3]版本方面有两条值得记住:numpy.sort与numpy.argsort的stable关键字是 NumPy 2.0.0 新增的,用来要求稳定排序(它等价于选kind='stable');descending关键字是 NumPy 2.5.0 新增的,用来直接要求降序。如果你的环境是更早的版本,降序要靠切片反转(np.sort(a)[::-1])或argsort的负数技巧,不要写这两个关键字。
另外kind的可选值包括'quicksort'(默认)、'mergesort'、'heapsort'、'stable'。官方文档说明:稳定排序会保持相等元素的相对顺序;对整数类型,'mergesort'和'stable'会映射到基数排序。
二、只想要「顺序」:argsort、argpartition、lexsort
大多数时候你并不想重排数据本身,而是想要「谁排在前面」这个信息,那就用返回下标的版本。
numpy.argsort(a, axis=-1, kind=None, order=None, *, stable=None, descending)返回的是把数组排好序所需的下标(stable与descending是关键字专用的可选参数,descending带一个内部的哨兵默认值,含义是「不指定」):
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.array([3, 1, 2])
print(np.argsort(a)) # [1 2 0] —— a[1]=1 最小,a[2]=2 次之,a[0]=3 最大
print(a[np.argsort(a)]) # [1 2 3],用下标取回排序结果如果只需要「最小的前 k 个」,用numpy.argpartition(a, kth, axis=-1, kind='introselect')更划算:它做的是部分排序,保证第kth个位置上是「排好序后应该在那个位置」的元素,且它左边的都不大于它、右边的都不小于它——左右两边内部是无序的。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.array([9, 1, 8, 2, 7, 3])
idx = np.argpartition(a, 3) # kth=3
print(a[idx[:3]]) # 最小的三个数(内部无序)多键排序用numpy.lexsort(keys, axis=-1)。它最容易记错的一点是:keys里最后一个键是主键,其余键按从后往前的顺序作为次键。也就是说,要让a做主键、b做次键,得写成np.lexsort((b, a))——顺序是反过来的。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
surname = np.array([2, 1, 2, 1])
score = np.array([90, 95, 85, 88])
order = np.lexsort((score, surname)) # 主键是 surname(最后一个),次键是 score
print(order) # [3 1 2 0]
print(surname[order]) # [1 1 2 2]
print(score[order]) # [88 95 85 90]三、去重与计数:numpy.unique
numpy.unique(ar, return_index=False, return_inverse=False, return_counts=False, axis=None, *, equal_nan=True, sorted=True)一个函数解决了「去重」「排序」「计数」三件事:
| 参数 | 作用 | 返回内容 |
|---|
| 默认 | 只返回排序后的唯一值 | 一个数组 |
return_index=True | 每个唯一值在原数组中首次出现的下标 | 多返回一个数组 |
return_inverse=True | 用下标重建原数组所需的映射 | 多返回一个数组 |
return_counts=True | 每个唯一值出现的次数 | 多返回一个数组 |
axis= | 按行或按列去重(默认为None,即展平处理) | — |
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.array([3, 1, 3, 2, 1])
print(np.unique(a)) # [1 2 3]
vals, counts = np.unique(a, return_counts=True)
print(vals) # [1 2 3]
print(counts) # [2 1 2]注意默认行为里已经包含了「排序」——unique返回的值是按升序排好的。官方文档还给出两条版本说明:一是从 1.21 起,和numpy.sort一样,NaN 会被排到末尾;二是sorted与equal_nan这两个关键字是较新版本加入的(equal_nan默认True,表示把多个 NaN 视作同一个值来去重)。不用这两个参数时不必关心,但如果你要写跨版本兼容的代码,最好显式确认。
四、过滤:布尔索引、numpy.where、numpy.extract
布尔索引是最直接的过滤方式:把一个布尔数组传进去,得到满足条件的元素组成的新数组。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.array([1, 5, 3, 8])
print(a > 4) # [False True False True]
print(a[a > 4]) # [5 8]numpy.where有两种完全不同的用法,签名写成where(condition, [x, y], /),两个参数是可选的:
- 三个参数(条件 +
x+y):条件为真的位置取x,否则取y,返回同形状的新数组; - 只有一个参数(条件):官方说明它等价于
np.asarray(condition).nonzero(),返回的是下标,而且是一个元组。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.array([1, 5, 3, 8])
print(np.where(a > 4, a, 0)) # [0 5 0 8] —— 三参数:按位置挑选
print(np.where(a > 4)) # (array([1, 3]),) —— 单参数:返回下标元组这是最容易踩的一处:单参数的where返回的是元组,不是数组。想拿满足条件的元素,要么用布尔索引a[a > 4],要么用三参数where。官方也提示,如果只是为了取下标,直接用nonzero更清楚。
numpy.extract(condition, arr)是另一个过滤函数,语义上和arr[condition]接近,返回一维结果。它在链式表达式中偶尔更顺手,但不如布尔索引直观,日常用布尔索引就够了。
顺带记一个统计函数:numpy.count_nonzero(a, axis=None, *, keepdims=False)用来数非零元素;因为True在布尔数组里就是1,所以np.count_nonzero(a > 4)正好是「满足条件的元素个数」。
五、清洗:numpy.clip与numpy.isnan的组合
numpy.clip把数值限制在区间内。官方说明它等价于但快于np.minimum(a_max, np.maximum(a, a_min)),并且不检查a_min是否小于a_max。
a_min > a_max时的行为,官方文档专门写了一条:这时clip会返回一个所有值都等于a_max的数组。这个行为不太符合直觉,但它是被明文规定的,不是「未定义行为」,所以不要去依赖它之外的任何东西。
还有一个版本细节:min和max这两个关键字参数是 NumPy 2.1.0 新增的,它们是a_min/a_max的替代写法。写新代码可以用新名字,但如果要兼容旧版本,还是用a_min/a_max更保险。
numpy.isnan(x, /, out=None, *, where=True, ...)是一个 ufunc,返回与输入同形状的布尔数组,标记每个位置是不是 NaN。注意它的第一个参数是仅位置参数(签名里那个/的含义),只能按位置传,不能写成isnan(x=...)。
清洗的标准顺序是:先去 NaN,再裁剪,反过来做会被 NaN 干扰:
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
b = np.array([1.0, np.nan, 5.0, 20.0, -3.0])
print(np.isnan(b)) # [False True False False False]
cleaned = b[~np.isnan(b)] # 先用布尔索引剔掉 NaN
clipped = np.clip(cleaned, 0.0, 10.0)
print(clipped) # [ 1. 5. 10. 0.]这里的关键点是~np.isnan(b):np.isnan给出「是 NaN」的掩码,取反(~)才得到「不是 NaN」的位置。这一步不能省——因为clip并不会把 NaN 修掉(np.maximum/np.minimum会传播 NaN,所以 NaN 通常在裁剪后原样保留)。如果你在一个含 NaN 的数组上直接clip,再拿去算均值,得到的还是 NaN。
需要注意的是,np.clip(b, 0.0, 10.0)里0.0和10.0是按位置传的,顺序是「下界、上界」,别写反。写反了虽然不会报错(按上面的规则会得到全是上界的数组),但结果毫无意义。
常见坑点
- ❌ 写
a = a.sort(),以为拿到了排序结果。
✅ndarray.sort是原地排序、返回None,这行会让a变成None;要副本用a = np.sort(a),要原地就直接a.sort()。
- ❌ 认为
np.sort(a)会改动a。
✅np.sort返回副本;只有ndarray.sort方法才原地修改。
- ❌ 对二维数组调用
np.sort(a),期待得到整个数组的全局排序。
✅ 默认axis=-1,是沿最后一根轴排序;要全局排序先a.ravel()或传axis=None。
- ❌ 用
np.where(a > 4)拿元素,然后对返回结果做数组运算。
✅ 单参数where返回的是下标元组;取元素用布尔索引a[a > 4],或改用三参数where。
- ❌ 写
np.lexsort((primary, secondary)),以为第一个键是主键。
✅最后一个键才是主键;主键是a、次键是b时应写np.lexsort((b, a))。
- ❌ 在含有 NaN 的数组上直接
clip,以为缺口会被顺带处理掉。
✅clip不会修掉 NaN;先用b[~np.isnan(b)]剔除,再裁剪,顺序不能反。
- ❌ 以为
clip会检查下界是否小于上界,或者依赖「下界大于上界」时的某种直觉行为。
✅ 官方明确:不检查两者大小关系,且当a_min > a_max时返回的数组所有值都等于a_max;别把参数写反。
- ❌ 在旧版本 NumPy 上用
np.sort(a, stable=True)或descending=True。
✅stable是 NumPy 2.0.0 新增、descending是 2.5.0 新增;旧版本请用kind='stable'或切片反转。
总结
| 需求 | 该用 | 关键点 |
|---|
| 排序并保留原数组 | np.sort(a) | 返回副本,默认沿最后一轴 |
| 原地排序 | a.sort() | 返回None |
| 要排序后的下标 | np.argsort(a) | 结果用于a[idx] |
| 只要前 k 小 | np.argpartition(a, k) | 只保证第k个位置正确,两侧无序 |
| 多键排序 | np.lexsort((次键, 主键)) | 最后一个键是主键 |
| 去重 + 计数 | np.unique(a, return_counts=True) | 结果已排序,NaN 排末尾 |
| 按条件取元素 | a[a > 4]布尔索引 | 返回副本,不是视图 |
| 按条件替换 | np.where(cond, x, y) | 单参数形式返回下标元组 |
| 限制取值范围 | np.clip(a, 下界, 上界) | 不检查上下界大小;min/max关键字需 2.1.0+ |
| 处理缺测值 | np.isnan+ 布尔索引 | 先去 NaN 再裁剪 |
这几组接口的共同点是:它们大多返回新数组,而不是就地改。所以写代码时先问一句「这个调用返回什么、我把它接到哪里了」,就能避开本文里大半的坑——尤其是sort方法返回None、单参数where返回元组这两个。至于清洗,记住「先剔缺测、再裁剪、最后统计」的顺序,NaN 就不会再悄悄污染你的结果。
参考:numpy.sort、numpy.argsort、numpy.argpartition、numpy.lexsort、numpy.unique、numpy.where、numpy.extract、numpy.clip、numpy.isnan、numpy.count_nonzero的签名与版本说明,均以 NumPy 官方文档为准;NumPy 为第三方库,需pip install numpy;本文代码未在本机运行,仅作人工推演。