4.2 安全选择行与列
第 3 章使用 NumPy 位置和布尔掩码。pandas 增加了标签,因此每次选择都必须回答两个问题:坐标是标签还是位置?结果是否应该保持二维?明确作出选择,可以避免行重新排序或新增列后,代码含义悄悄变化。
本节使用下面这张表:
import pandas as pd
deliveries = pd.DataFrame(
{
"zone": ["North", "South", "West", "Central", "North"],
"minutes": [31, 52, 44, 38, 47],
"fee": [8.5, 9.0, 7.5, 8.0, 10.0],
"delivered": [True, True, True, False, True],
},
index=pd.Index(
["A104", "A105", "A106", "A107", "A108"],
name="order_id",
),
)方括号选择列,但结果维数取决于写法
单个列标签返回 Series:
minutes = deliveries["minutes"]
print(minutes.ndim) # 1列标签列表返回 DataFrame,即使列表只有一项:
one_column_table = deliveries[["minutes"]]
print(one_column_table.ndim) # 2
summary = deliveries[["zone", "minutes"]]这个差异会影响后续操作。一个期望 DataFrame 的函数可能依赖 .columns,而 Series 使用的是 .name。应先选择结果契约,不要靠反复增减方括号碰运气。
在长期维护的代码中,避免 deliveries.zone 这样的属性访问。列名含空格时它无法使用,与 DataFrame 方法重名时会冲突,也无法自然表达动态列变量。deliveries["zone"] 更明确。
.loc 按标签选择,.iloc 按位置选择
.loc 接收行标签和列标签:
by_label = deliveries.loc[
"A105":"A107",
["zone", "minutes"],
]标签切片通常同时包含 A105 和 A107。它表达的是当前索引顺序中的标签范围。当标签未排序或存在重复时,范围切片可能令人意外甚至无效;如果顺序契约不清楚,应优先传入明确标签列表。
.iloc 接收从零开始的整数位置:
by_position = deliveries.iloc[1:4, [0, 1]]结果同样是三行两列。位置切片遵循 Python 半开规则,因此位置 4 不包含在内。
当标签本身是整数时,两种选择器仍然不同:
sample = pd.DataFrame(
{"value": [10, 20, 30]},
index=[100, 1, 200],
)
sample.loc[1] # 标签为 1 的行
sample.iloc[1] # 当前位于位置 1 的行这里两者碰巧指向同一行,但重新排列索引就可能把它们分开。想表达“第二行”时不要使用 .loc;想表达“编号为 1 的订单”时不要使用 .iloc。
若只读取一个标量,.at[行标签, 列标签] 与 .iat[行位置, 列位置] 是更聚焦的写法。只有意图确实是标量访问时才使用它们,不要把它们当作过早优化技巧。
选择实验是一台可执行的坐标控制台。你要亲手写出 .loc 或 .iloc 切片并运行,再在命令不变的情况下重排物理行。源表高亮与返回 DataFrame 会让标签终点包含、位置终点排除,以及选错坐标系统的后果都能被直接检验。
布尔选择保留条件为真的行
在 Series 上进行比较,会产生带有同一索引的布尔 Series:
slow = deliveries["minutes"] > 40
late_orders = deliveries.loc[slow]索引也是掩码的一部分。pandas 会按照行标签对齐布尔 Series,从而防止重新排序导致错选:
reversed_mask = slow.sort_index(ascending=False)
same_rows = deliveries.loc[reversed_mask]虽然掩码顺序改变了,每个真值仍通过标签连接到正确订单。若布尔 Series 无法与表格对齐,pandas 会报错,而不会悄悄按位置使用。
NumPy 布尔数组没有标签,因此 pandas 只能按位置应用它。只有当位置耦合是有意设计且已经检查长度时才这样做。在带标签分析中,保留 Series 索引通常更安全。
一些辅助方法可以生成易读条件:
target_zone = deliveries["zone"].isin(["North", "South"])
ordinary_duration = deliveries["minutes"].between(30, 50)
complete = deliveries["minutes"].notna()
selected = deliveries.loc[
target_zone & ordinary_duration & complete,
["zone", "minutes"],
]逐元素布尔运算应使用 &、| 和 ~,每个比较都要加括号。Python 的 and、or 与 not 需要一个真值,无法判断整个 Series 的真假。
可空布尔条件中可能包含 pd.NA。用于索引时,缺失掩码项会被当作假,这可能排除资格未知的记录。选择之前,应决定是拒绝、检查,还是显式填充缺失条件。
严格选择应验证自身契约
用 .loc 选择不存在的标签会引发 KeyError:
deliveries.loc[["A104", "A999"]]当每笔请求订单都必须存在时,这个失败很有价值。如果允许返回部分结果,就应明确表达策略,而不是捕获所有 KeyError:
requested = pd.Index(["A104", "A999"])
missing = requested.difference(deliveries.index)
if len(missing):
print("Missing orders:", missing.tolist())
partial = deliveries.reindex(requested)reindex 会构造请求的标签集合,并插入缺失行。它表达的是“即使数据缺失,也保留这个请求布局”,与严格 .loc 选择含义不同。
对于可复用分析函数,应定义并测试:
- 行列标签是否必须存在。
- 行索引是否必须唯一。
- 顺序来自源表,还是调用方请求的标签顺序。
- 单列结果采用 Series 还是 DataFrame。
- 空选择是有效结果还是错误。
契约实验像一台小型 API 模糊测试器。你可以编写源索引、正常与对抗请求,以及缺失标签、重复项、顺序、维度和空输入政策。每次编辑都会重新执行全部调用,暴露契约在不同情况下究竟返回、补空、换序还是抛错。
选择操作应回答问题,而不修改源表。下一节将组合筛选条件、建立确定性排序、创建派生列,并避免含义模糊的链式赋值,准确更新指定单元格。