3.2 排序、搜索与条件选择
分析表保存的是一组组关系:订单编号、配送用时与区域共同属于同一条观测。只有当这些关系一起移动时,排序才是安全的。本节将建立一个可复用模式:先计算位置,再把这些位置应用到每个对齐的数组上。
import numpy as np
order_ids = np.array(["A104", "A105", "A106", "A107", "A108"])
minutes = np.array([42, 27, 42, 35, 51])
zones = np.array(["North", "South", "West", "Central", "North"])排序数值与排序记录并不相同
np.sort 返回排序后的值,通常不会修改输入:
sorted_minutes = np.sort(minutes)
print(sorted_minutes) # [27 35 42 42 51]
print(minutes) # 原顺序保持不变如果分别排序 order_ids、minutes 和 zones,每一列都可能显得井然有序,组合出的行却是虚构记录。正确做法是使用 np.argsort 进行间接排序(indirect sorting),让它返回源位置:
order = np.argsort(minutes, kind="stable")
print(order) # 类似 [1 3 0 2 4] 的位置
print(order_ids[order]) # 对齐的编号
print(minutes[order]) # 对齐的用时
print(zones[order]) # 对齐的区域argsort 的输出不是排序后的数据,而是一个置换(permutation):一串整数位置,可复用于沿同一条轴对齐的所有数组。
若要降序排列,可以反转升序置换,或对数值键的相反数排序:
descending = np.argsort(-minutes, kind="stable")稳定排序(stable sort)会保留相同键值的原始先后顺序。两个 42 分钟记录会维持输入时的顺序。当输入顺序本身有含义,或需要分阶段按多个键排序时,稳定性很有用。但它不会自动产生有业务意义的并列规则;若并列时必须按编号或时间戳排序,就要明确声明。np.lexsort 可以从多个键生成置换:
# 最后一个键是主键:先按 minutes,再用 ID 打破并列。
order = np.lexsort((order_ids, minutes))对于大型数组,如果只需要最小或最大的 个元素,np.argpartition 可以避免完整排序:
k = 3
candidate_positions = np.argpartition(minutes, k - 1)[:k]
top_k_order = candidate_positions[
np.argsort(minutes[candidate_positions], kind="stable")
]分区得到的候选项不保证有序,因此第二步仍要对较小的候选集合排序。
沿指定轴排序
对于二维数组,axis 指定哪些线段分别进行排序:
matrix = np.array([
[8, 3, 5],
[2, 9, 4],
])
np.sort(matrix, axis=1) # 每行内部排序
np.sort(matrix, axis=0) # 每列内部排序
np.sort(matrix, axis=None) # 展平后排序全部六个值独立排序表格的每一列通常会破坏记录。只有当每行或每列本来就是彼此独立的数值序列时,沿轴排序才合适;若多个字段共同组成一条观测,就应对行使用同一个置换。
在有序边界中搜索
np.searchsorted 会在已经排好序的一维数组中寻找插入位置。它使用二分搜索,因此适合重复执行边界查询:
boundaries = np.array([36, 50])
observed = np.array([24, 35, 36, 50, 61])
bucket = np.searchsorted(boundaries, observed, side="right")
print(bucket) # [0 0 1 2 2]这里第一条边界是 36,因为测量值采用整数分钟,并且不超过 35 分钟算准时。当 side="right" 时,等于某个边界的值会插在该边界之后,因此 36 分钟算延迟,50 分钟算严重延迟。三个桶分别表示“准时”“延迟”和“严重延迟”。若改为 side="left",等于边界的值会归入较低的桶。两者都不是普遍正确的答案:业务规则必须明确每一个精确边界归谁所有。
输入边界必须有序。searchsorted 不会验证边界的业务含义,也不会替你排序。当边界来自配置时,应检查它们是否单调递增:
assert np.all(boundaries[:-1] <= boundaries[1:])使用向量化条件选择值
三个参数的 np.where 会在每个位置从两个值中选择一个:
status = np.where(minutes <= 35, "on-time", "late")条件和两个选项都遵循广播规则,结果采用它们广播后的形状。这个写法不同于只有一个参数的 np.where(condition),后者返回索引数组。对于一维数据,np.flatnonzero(condition) 通常更清楚:
late_positions = np.flatnonzero(minutes > 35)
late_ids = order_ids[late_positions]若结果超过两种,可以使用 np.select:
conditions = [
minutes >= 50,
minutes > 35,
]
choices = ["critical", "late"]
status = np.select(conditions, choices, default="on-time")np.select 会采用第一个为真的条件。因此,当规则存在重叠时,必须从最具体或优先级最高的规则排到最宽泛的规则。如果把 minutes > 35 放在前面,所有不小于 50 的值都会先进入 late 分支,critical 将永远无法到达。
构造条件时,请记住这些规则:
- 组合数组条件要使用
&、|和~,不要使用处理单个布尔值的 Pythonand、or和not。
- 每个比较都要加括号,例如
(minutes > 35) & (zones == "North")。
- 明确决定缺失值如何路由;与
np.nan的比较通常为假。
- 测试恰好等于边界的值,不要只测每个类别内部的典型值。
排序和条件选择是确定性的:相同输入与规则会产生相同结果。下一节将引入受控随机性;要让随机过程可复现,必须同时记录生成器状态和操作序列。