Pandas数据筛选与选择:loc、iloc 与布尔索引
筛数据是 pandas 里调用最频繁的操作,也是最容易出静默错误的地方。取错了行不会报错,分析照跑,结论就偏了。三种取数方式要分清楚:[] 加条件做布尔筛选,.loc 按标签取,.iloc 按位置取。区别集中在两点上——边界闭不闭,以及你给的是标签还是位置。
准备一份数据
Section titled “准备一份数据”sklearn 的 iris 列名里带空格和括号,写条件表达式时要加反引号才能用 query,先改成蛇形命名(snake_case)省事。
import pandas as pdfrom sklearn.datasets import load_iris
df = load_iris(as_frame=True).framedf.columns = ["sepal_len", "sepal_wid", "petal_len", "petal_wid", "species"]df["species"] = df["species"].map({0: "setosa", 1: "versicolor", 2: "virginica"})
print(df.head(3)) sepal_len sepal_wid petal_len petal_wid species0 5.1 3.5 1.4 0.2 setosa1 4.9 3.0 1.4 0.2 setosa2 4.7 3.2 1.3 0.2 setosa布尔索引:条件表达式本身就是掩码
Section titled “布尔索引:条件表达式本身就是掩码”df[条件] 里的条件是一个与行数等长的布尔 Series,True 的行留下。这和 R 里 filter() 的思路完全一致,只是 Python 要显式写出列名。
print(df[df["petal_len"] > 6.5])print(len(df[df["petal_len"] > 6.5])) sepal_len sepal_wid petal_len petal_wid species105 7.6 3.0 6.6 2.1 virginica117 7.7 3.8 6.7 2.2 virginica118 7.7 2.6 6.9 2.3 virginica122 7.7 2.8 6.7 2.0 virginica4注意结果里的行号是 105、117、118、122,不是 0 到 3。筛选不会重置索引,原始行号被保留下来了。这在追踪异常样本时是好事——看到 118 号样本可以直接回去查原始记录;但要在筛选后的结果上按位置取数就会踩坑,后面 iloc 那节会说。想重新编号就接一句 reset_index(drop=True)。
多条件:必须用 & |,且每个条件加括号
Section titled “多条件:必须用 & |,且每个条件加括号”最常见的报错长这样:
df[(df["petal_len"] > 5.0) and (df["species"] == "virginica")]ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().原因值得说清楚:and 是 Python 的关键字,它需要把两边都求值成一个布尔值,而 df["petal_len"] > 5.0 是一整个布尔 Series,Python 不知道该取哪一个元素来判断真假。pandas 里要用按位运算符 &(与)、|(或)、~(非),它们逐元素运算。
括号也不能省。& 的优先级比比较运算符高,df["petal_len"] > 5.0 & df["species"] == "x" 会被解释成 df["petal_len"] > (5.0 & df["species"]) == "x",报错或者给出莫名其妙的结果。
out = df[(df["petal_len"] > 5.0) & (df["species"] == "virginica")]
print(out.head(3))print(len(out)) sepal_len sepal_wid petal_len petal_wid species100 6.3 3.3 6.0 2.5 virginica101 5.8 2.7 5.1 1.9 virginica102 7.1 3.0 5.9 2.1 virginica41结论:Python 里写筛选条件,每个条件一对括号,中间用 & 或 |。没有例外。
loc:按标签取,右边界是闭的
Section titled “loc:按标签取,右边界是闭的”loc 的第一个参数是行标签,第二个是列名。切片时两端都包含,这点和 Python 的列表切片相反,是从 R 的取数习惯过来的。
print(df.loc[0:2, "sepal_len":"petal_len"]) sepal_len sepal_wid petal_len0 5.1 3.5 1.41 4.9 3.0 1.42 4.7 3.2 1.30:2 取到了 0、1、2 三行。列名切片 "sepal_len":"petal_len" 按照列在原表中的先后顺序取,也包含两端。
列选择可以传列表,这时候不受顺序限制:
print(df.loc[2:4, ["species", "petal_len"]]) species petal_len2 setosa 1.33 setosa 1.54 setosa 1.4iloc:按位置取,左闭右开
Section titled “iloc:按位置取,左闭右开”iloc 只认整数位置,规则和 Python 列表切片一致:包含起点,不含终点。上面那个 loc[0:2] 的三行结果,用 iloc 要写 0:3。
print(df.iloc[0:2, 0:3]) sepal_len sepal_wid petal_len0 5.1 3.5 1.41 4.9 3.0 1.4两个都常用,记混的代价是悄悄多一行或者少一行。判断标准很简单:数字是行号还是位置。原始数据带业务编号(比如问卷编号 1001、1002)时,loc[1001:1005] 取五条记录,iloc[1001:1005] 取第 1001 到 1004 条——如果表只有 150 行,后者直接抛 IndexError,反而安全。
筛选之后的行索引是跳号的(105、117、118……),两种取法的差别在这时候最清楚:
long_petal = df[df["petal_len"] > 6.5]
print(long_petal.iloc[0]["petal_len"]) # 位置 0:第一行print(long_petal.loc[105, "petal_len"]) # 标签 105:同一行6.66.6两条语句指向同一行:iloc[0] 不关心原始编号,loc[105] 依赖原始编号存在。筛选结果接 reset_index(drop=True) 之后,loc[105] 就取不到了,而 iloc[0] 照常工作。
单列与多列:Series 还是 DataFrame
Section titled “单列与多列:Series 还是 DataFrame”差一对方括号,返回类型就变了:
print(type(df["sepal_len"]).__name__, type(df[["sepal_len"]]).__name__)Series DataFrame单列是 Series(一维),多列是 DataFrame(二维)。差别在这里会咬人:df["species"].str.upper() 能跑,df[["species"]].str.upper() 直接 AttributeError,因为 DataFrame 没有 .str。分组统计也一样,df.groupby("species")["petal_len"].mean() 返回 Series,把中括号改成 [["petal_len"]] 就返回 DataFrame——表结构会一路影响下去,取单列时想清楚后面要做什么:要接着做属性访问就用单括号,要留在二维结构里就用双括号。
isin、between:范围判断的两个快捷方式
Section titled “isin、between:范围判断的两个快捷方式”print(df[df["species"].isin(["setosa", "virginica"])].shape)print(len(df[df["petal_len"].between(1.4, 1.5)]))(100, 5)26isin 等价于一串 | 连接的等值比较,取值多的时候写法干净得多。between(1.4, 1.5) 两侧都是闭区间,等价于 (df["petal_len"] >= 1.4) & (df["petal_len"] <= 1.5)。
浮点数的等值判断要小心。CSV 里写的 1.4 读进来确实等于字面量 1.4,但经过求和、求均值之后就不是了——0.1 + 0.2 == 0.3 在 Python 里返回 False。凡是拿计算结果去比较,都改用 between 或者 np.isclose 给一个容差,别用 ==。
query:把条件写成字符串
Section titled “query:把条件写成字符串”条件多、列名短的时候,query 可读性更好:
print(df.query("petal_len > 6.5 and species == 'virginica'").head(3))print(df.query("`sepal_len` > 7.5").shape) sepal_len sepal_wid petal_len petal_wid species105 7.6 3.0 6.6 2.1 virginica117 7.7 3.8 6.7 2.2 virginica118 7.7 2.6 6.9 2.3 virginica(6, 5)query 里可以用 and、or、not,它自己解析字符串,不受前面布尔运算符的限制。列名不合法的(带空格、括号)要用反引号包起来(键盘左上角那个键)。query 支持引用外部变量,写法是在变量名前加 @,例如 df.query("petal_len > @threshold")——循环里换阈值跑筛选时很方便。
代价是条件写在字符串里,编辑器不会帮你检查列名拼写,拼错了要到运行时才报错。列名多、条件复杂时优先用布尔索引,至少 IDE 能提示。
字符串列:.str 访问器
Section titled “字符串列:.str 访问器”.str 把 Python 的字符串方法逐元素应用到整列上:
print(df["species"].str.startswith("v").sum())print(df[df["species"].str.contains("color")].shape)100(50, 5)startswith 返回布尔 Series,直接 .sum() 就是计数——True 按 1 算,versicolor 与 virginica 各 50 行,所以是 100。contains 那行取的是名字里带 color 的,只有 versicolor 命中。
两个细节:.str 只在字符串列上有效,列里混了数字时非字符串位置会返回 NaN 而不是报错,用之前先看一眼 dtype;contains 默认把参数当正则表达式,做纯文本查找要显式写 regex=False,否则搜索内容里的 .、*、( 会被当成模式,匹配结果和预期不同。
用 loc 赋值,别链式赋值
Section titled “用 loc 赋值,别链式赋值”筛选之后要给子集写新值,标准写法是用 loc 一次完成:
d = df.copy()d.loc[d["petal_len"] >= 5.0, "size"] = "long"d.loc[d["petal_len"] < 5.0, "size"] = "short"
print(d["size"].value_counts())sizeshort 104long 46Name: count, dtype: int64链式写法(先取列、再按条件赋值)是错的:
d2 = df.copy()d2["petal_len"][d2["petal_len"] > 6.5] = 0print(d2["petal_len"].max())6.9pandas 3.0 会打印一条 ChainedAssignmentError 警告,提示改用 .loc;更早的版本里它是彻底静默的——你看到代码跑完了,以为改了,其实改的是中间那份临时副本。判断方法很直接:赋值语句左边出现两次 [,就是链式赋值。改成 d2.loc[d2["petal_len"] > 6.5, "petal_len"] = 0 才对。
筛选和选择解决的是「取哪些行」,下一步通常是把取出来的行按组汇总,见 Pandas分组聚合。如果你习惯 R 的写法,dplyr核心动词 里的 filter()、select()、slice() 干的是同一件事,对照着看能更快建立对应关系——R 用 filter(df, x > 5),pandas 用 df[df["x"] > 5],语义一致,只是条件写在哪里的区别。