2-2 布尔索引:按症状筛人 本节摘要:布尔索引用一个与数据等长的真值序列筛行,条件用&、|、组合且必须加括号。本节还讲isin、between两件替代器械,以及布尔条件在缺失值上的失效行为,为第03章内科检查埋好伏笔。 上一节学会了按号取行,但分诊的真实场景是"把金额大于一千的线上交易都叫来"。这要求的不是位置也不是标签,而是症状本身。 掩码是怎么工作的 理解了"先造一张等长的真假条、再按条放行",布尔索引的所有限就都好懂了:掩码长度不匹配会报IndexingError,掩码里有非布尔值会抛TypeError。 组合条件的括号纪律 ⚠️ 常见坑:位运算符优先级高于比较。每个条件必须用括号包住,没有例外。
本节摘要:布尔索引用一个与数据等长的真值序列筛行,条件用&、|、~组合且必须加括号。本节还讲isin、between两件替代器械,以及布尔条件在缺失值上的失效行为,为第03章内科检查埋好伏笔。
上一节学会了按号取行,但分诊的真实场景是"把金额大于一千的线上交易都叫来"。这要求的不是位置也不是标签,而是症状本身。
import pandas as pd df = pd.DataFrame({ '门店': ['A', 'B', 'A', 'C', 'A'], '金额': [1280, 960, 1732, 880, 2100], '渠道': ['线上', '线下', '线上', '线上', '线下'], }) # 第一步:比较运算生成掩码——与df等长的真值序列 mask = df['金额'] > 1000 print(mask) # 0 True # 1 False # 2 True # 3 False # 4 True # Name: 金额, dtype: bool # 第二步:掩码放进索引位,True的行被叫出来 print(df[mask]) # 门店 金额 渠道 # 0 A 1280 线上 # 2 A 1732 线上 # 4 A 2100 线下
理解了"先造一张等长的真假条、再按条放行",布尔索引的所有限就都好懂了:掩码长度不匹配会报IndexingError,掩码里有非布尔值会抛TypeError。
# 症状叠加:金额超一千 且 线上渠道 # and不行,必须用 & 且两侧加括号 print(df[(df['金额'] > 1000) & (df['渠道'] == '线上')]) # 门店 金额 渠道 # 0 A 1280 线上 # 2 A 1732 线上 # 或与非 high = df['金额'] > 1000 online = df['渠道'] == '线上' print(df[high | ~online].shape) # (3, 3) # 常见报错现场:漏括号 # df[df['金额'] > 1000 & df['渠道'] == '线上'] # 抛出 TypeError:& 的优先级高于比较运算 # 系统先算 1000 & df['渠道'],再拿结果比较,彻底错乱
⚠️ 常见坑:位运算符优先级高于比较。每个条件必须用括号包住,没有例外。见到"cannot compare str and int"这类离奇报错,九成是漏了括号。
复杂条件有更省事的写法。分诊场景里最高频的两件:
# isin:症状属于一个集合 print(df[df['门店'].isin(['A', 'B'])].shape) # (4, 3) print(df[~df['门店'].isin(['A'])].shape) # 非A门店,(2, 3) # between:区间内,含两端 print(df[df['金额'].between(900, 1800)]) # 门店 金额 渠道 # 0 A 1280 线上 # 1 B 960 线下 # 2 A 1732 线上 # str访问器:症状藏在文本里 print(df[df['渠道'].str.startswith('线')].shape) # (5, 3)
sick = pd.DataFrame({'金额': [100, None, 300], '渠道': ['线上', '线下', None]}) print(sick[sick['金额'] > 150]) # 金额 渠道 # 2 300.0 None
注意第1行:金额缺失(None),比较结果既非True也非False而是NaN,放进索引位被当作False处理。也就是说布尔筛选会静默丢掉缺失行——你不会收到任何警告。如果分诊的目的是"筛出可疑交易去人工复核",缺失金额恰恰最可疑,这时要先单独把缺失行捞出来:
missing_amount = sick[sick['金额'].isna()] review = pd.concat([sick[sick['金额'] > 150], missing_amount]) print(review.shape) # (2, 2) 大额行与缺失行都进入复核名单

同样的条件筛选,还有一件更省笔墨的器械:query。它把条件写成一句话字符串,and、or直接用,括号纪律都免了:
print(df.query('金额 > 1000 and 渠道 == "线上"')) # 门店 金额 渠道 # 0 A 1280 线上 # 2 A 1732 线上 # 局部变量用@引入,区间用between的写法也直接支持 low, high = 900, 1800 print(df.query('@low <= 金额 <= @high').shape) # (3, 3)
两种写法结果完全一致,选择标准是可读性与规模:条件三四个以上、数据量上十万行时query的字符串表达式更清爽也更省内存(它不在Python层面生成一串中间布尔列);探索阶段、需要逐步调试每个子条件时,传统掩码写法更直观。query的完整展开在第08章的性能话题里,这里先混个脸熟。
把缺失分流的知识串成一个真实流程:复核名单要包含"金额大于一千五"与"金额缺失"两类,并标注原因。
sick = pd.DataFrame({'单号': ['T1', 'T2', 'T3', 'T4'], '金额': [1600, 900, None, 2100]}) big = sick[sick['金额'] > 1500].assign(原因='大额复核') miss = sick[sick['金额'].isna()].assign(原因='金额缺失') review = pd.concat([big, miss]).sort_values('单号') print(review) # 单号 金额 原因 # 0 T1 1600.0 大额复核 # 2 T3 NaN 金额缺失 # 3 T4 2100.0 大额复核
写复核逻辑时最容易犯的就是漏掉缺失行——布尔条件对NaN静默返回False,没有任何提示。养成"筛选之后对一遍行数"的习惯:筛选结果加缺失行数应该等于符合业务预期的总量,数字对不上就说明有分支被静默吞了。
下一节处理分诊台的立体结构:多层索引。