news 2026/10/27 22:06:53

Pandas数据筛选与过滤:布尔索引、loc/iloc与缺失值处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pandas数据筛选与过滤:布尔索引、loc/iloc与缺失值处理

做数据分析这行,日常被问得最多的一句话不是"模型怎么调",而是"帮我把这批数据里符合某某条件的行挑出来"。Pandas 数据筛选和过滤,说白了就是把人手在 Excel 里点筛选、勾条件、拉公式的那套动作,翻译成可复用、可批处理、可进版本库的代码。这一章我打算掰开讲四件事:布尔索引的地基逻辑、loc和iloc的边界、query()这类表达式写法的取舍、以及文本和缺失值这两块最脏的数据怎么过滤。适合已经能读写 CSV、会建 DataFrame,但一遇到多条件组合、链式赋值警告、NaN参与比较就心里没底的同学;也适合带新人、需要一套靠谱口径的老手对照。标题里的"(1)"不是凑数,这一篇只覆盖行级条件筛选的主干,按位置、按类型、按名称这类工具型 API 一并收进来,下一部分再谈分组后筛选、合并后筛选和索引层面的过滤。

1. 筛选这件事,先把它放回真实数据流里看

1.1 筛选在一条数据流水线上的真实位置

很多人学 Pandas 的顺序是先学read_csv,再学groupby,最后回头补筛选,结果就是写业务代码时到处是"先聚合再判断"的别扭逻辑。我实际的习惯恰好反过来:一条数据流水线里,筛选出现的频率远高于聚合。读进来先砍掉无关行,清洗阶段挑出异常行单独看,建模前切训练集和对照集,出报表前按维度过滤,全是筛选。

这里有个容易忽略的点:筛选本身不产生新信息,它只是缩小后续计算的输入规模。所以判断一次筛选写得好不好,标准不在于代码短不短,而在于它有没有尽早把数据量降下来,以及它有没有误杀。前者影响性能,后者影响结论。我见过太多因为NaN被静默丢掉、导致转化率算高了的案例,问题都不在聚合那一步,出在筛选那一步。

从工程角度看,筛选还承担一个隐性职责:它是最便宜的数据质量探针。你按某个条件去筛,返回 0 行,那大概率不是业务真的没有,而是字段类型不对、空格没清、编码不一致。所以我养成一个习惯,任何一个新数据集进来,先用几个最基础的筛选条件试一下行数变化,比看info()更能发现问题。

1.2 先想清楚的问题:你要的是标签还是位置

这是我认为整章最该先讲明白的一件事,比任何语法都重要。Pandas 的索引体系里同时存在两套坐标系:一套是标签,也就是index上挂的那个值,可能是整数、可能是字符串、可能是时间戳;另一套是位置,从 0 开始的第 N 个。这两套东西在默认的RangeIndex下长得一模一样,于是新手很容易产生"它们是一回事"的错觉,直到某天做了sort_values或者drop之后索引被打乱,代码就开始返回莫名其妙的结果。

我的判断口径很粗暴:只要是条件判断,一律用布尔索引或loc;只有明确要"前 N 行""第 3 到第 8 行"这种纯序数语义,才用iloc。理由很简单,条件判断表达的是业务含义,业务含义应该跟标签走;而序数语义表达的是物理位置,物理位置在数据一变之后就失效了。把这两者混着用,代码在测试环境能跑,换一批数据就翻车。

顺带说一句,索引乱掉之后最省事的做法不是reset_index()了事,而是先想清楚这个索引还有没有价值。如果它原本是从业务系统带过来的唯一键,那它就是你后面做关联、做校验的抓手,别随手扔掉;如果它只是个临时序号,那reset_index(drop=True)让一切回到干净状态,反而是更稳的选择。

1.3 这一篇的内容边界

标题叫"(1)",我把范围划成这样:行级条件筛选的主干方法,包括布尔索引、loc/iloc、query()、文本方法、缺失值判断、按数据类型和名称筛。这些覆盖了日常 80% 以上的场景。至于分组之后的组内筛选(groupby+filter)、多表合并之后的筛选、以及基于多层索引的切片,属于另一个话题,放到后面单独说,因为它们各自有独立的坑,硬塞进一篇会讲不透。

有一点提前说明:本文所有代码基于 Pandas 2.x 的接口,1.x 大部分也适用,个别地方我会标出版本差异,比如between()的inclusive参数。数据全部是模拟的订单表,不涉及任何真实业务。

2. 布尔索引:所有筛选动作的地基

2.1 一行布尔表达式背后到底发生了什么

df[df['amount'] > 100]这行代码,很多人当成一个原子操作,实际上它是三步。第一步,df['amount'] > 100取出这一列,底层是一个 NumPy 数组,做逐元素的向量化比较,产出一个长度相同的bool数组;第二步,把这个bool数组当作"掩码"传给df.__getitem__;第三步,Pandas 按掩码为True的位置把行取出来,组装成一个新的 DataFrame。

理解这三步的价值在于:第一步的产物是一个独立的 Series,你可以先存下来复用。多条件场景下这一点特别有用。我经常这么写:

mask_valid = (df['status'] == 'paid') & df['amount'].notna() mask_big = mask_valid & (df['amount'] > 500) print(mask_valid.sum(), mask_big.sum()) df_big = df[mask_big]

先把掩码存成变量,一是能打印sum()看看命中了多少行,二是同一个掩码在后续好几处筛选里都能复用,省掉重复计算。对于百万行以上的表,一个布尔掩码的构建成本在几十毫秒量级,复用的收益是实打实的。

还有一层:这个布尔数组每个元素占 1 字节。1000 万行的表,一个掩码就是约 10MB 内存,三个条件同时存在于内存里就是 30MB。听起来不多,但在内存吃紧的环境里,把int64的金额列先转成float32、把重复度高的状态列转成category,能省下的往往是几百 MB,这时候一个 10MB 的中间掩码就不算事了。优化的优先级永远是大头先动。

2.2 多条件组合:&|~和括号的血泪教训

这一段是新手翻车率最高的地方,我把三个必踩的坑摆出来。

第一个坑,用and/or代替&/|。Python 的and依赖bool()求值,而一个长度大于 1 的 Series 是无法转成单一布尔值的,直接抛ValueError: The truth value of a Series is ambiguous。必须用位运算符。这三个符号在 Pandas 里的语义是逐元素逻辑与、或、非,跟 Python 的短路逻辑完全是两回事。

第二个坑,运算符优先级。&的优先级高于>和==,所以df['a'] > 1 & df['b'] < 2会被解析成df['a'] > (1 & df['b']) < 2,报错或者给出完全错误的结果。每个条件都用括号包起来,不是风格问题,是正确性问题。我自己的写法习惯是连最外层也包一层,虽然冗余,但复制粘贴到别的表达式里不会出事。

第三个坑,空值在比较里的静默行为。这一条最隐蔽,单独开一小节说。

三个以上条件时,可读性会急剧下降。这时候我会做一件事:给每个条件起个名字。

cond_status = df['status'].isin(['paid', 'shipped']) cond_amount = df['amount'].between(100, 2000) cond_channel = df['channel'] != 'internal_test' cond_time = df['created_at'] >= '2024-01-01' result = df[cond_status & cond_amount & cond_channel & cond_time]

条件一多,出问题时你能立刻定位是哪个条件写错了,而不是对着一行两百字符的表达式数括号。

2.3between()和isin():把区间和集合写干净

连续区间和离散集合这两类条件,用比较运算符硬写也能写,但有更省事的写法,而且能顺带避开空值问题的一部分。

between(left, right)等价于>= left且<= right,但从 Pandas 1.3 开始多了inclusive参数,可以取'both'(默认)、'neither'、'left'、'right'。做分箱统计、划分金额档位时这个参数很顺手。注意它对NaN的处理和普通比较一致:NaN参与between结果仍是False,而不是NaN。

isin(values)判断是否属于某个集合,底层用的是哈希查找,比逐个==再|起来快得多,特别是集合里有十几个值的时候。它对NaN的处理有个细节值得记一下:

import numpy as np import pandas as pd s = pd.Series(['a', 'b', np.nan]) print(s.isin(['a'])) # [True, False, False] print(s.isin(['a', np.nan])) # [True, False, True]

也就是说isin是能把NaN匹配出来的,只要你显式把np.nan放进候选集合里。这跟==的行为完全不同(np.nan == np.nan是False),知道这一点在做"排除某几个值"时能救命。

还有一个组合技巧:取反用~包isin,比写一串!=加&清晰得多。比如排除测试渠道:

df_clean = df[~df['channel'].isin(['internal_test', 'sandbox'])]

但要注意,~作用于isin的结果时,NaN的isin结果是False,取反就变成True,于是NaN行会被保留。这通常是你想要的,但你得知道它发生了。

2.4 空值在布尔条件里的诡异表现

NaN的比较行为可以用一句话总结:除了!=,所有比较都返回False。NaN > 100是False,NaN < 100是False,NaN == 100也是False。这听起来很合理,但它带来一个非常反直觉的后果:df[df['amount'] != 100]不会包含amount为NaN的行。

为什么?因为NaN != 100在浮点语义里是True,但 Pandas 在做掩码时,如果比较结果里出现NaN(对象类型或可空类型时),会把它当非布尔处理;更常见的情况是数值列里的NaN参与比较直接得到False,于是!=的反向筛选就漏掉了空值行。

绕开的办法有两个。一是显式补上:df[(df['amount'] != 100) | df['amount'].isna()]。二是先决定空值算哪边,再用fillna统一处理。我自己的习惯是先统计空值占比,占比很低就单独拿出来看,占比高就说明这个字段本身有问题,得回头找上游。

还有一类坑出在可空类型(Int64、boolean、string)上。Pandas 引入的这套扩展类型保留了pd.NA,比较结果可能是pd.NA而不是True/False,拿去做布尔索引会报cannot mask with non-boolean array containing NA / nan values。这不是 bug,是在提醒你"这个条件的语义没定义清楚"。解决办法是fillna(False),或者用.astype(bool)之前先确认没有 NA。

3.loc与iloc:把标签和位置彻底分开

3.1loc的切片是闭区间,这点坑过太多人

df.loc['a':'c']会把'a'、'b'、'c'三行都取出来,而df.iloc[0:2]只取第 0、1 两行。loc闭区间,iloc左闭右开。这条规则记不住,做时间序列切片时会少一天或者多一天,而且不会报错,只会让统计结果差一点,是最难查的那种问题。

我自己的记法是:loc是"我要从 A 到 B",业务语义上通常包含两端,比如"1 月 1 日到 1 月 31 日"当然包含 31 日;iloc是"我要第 0 个开始数 2 个",这是编程语义,当然是左闭右开。按语义记,比死记硬背靠谱。

时间索引上还有个额外的坑:如果你用字符串'2024-01'去切DatetimeIndex,Pandas 会做部分字符串匹配,把整个 1 月都切出来。这个特性很方便,但要求索引必须是有序的,否则可能返回空或者报KeyError。做时间切片之前sort_index()一下,是我多年养成的肌肉记忆。

3.2iloc的纯位置逻辑与两种越界

iloc只认位置,不认标签,所以它最大的优势是行为可预测:不管索引被sort_values打乱成什么样,iloc[0]永远是物理第一行。取首行、取末行、取前 N 行这类操作,我基本都用它或者head()/tail()。

越界有两种形态。硬越界是iloc[100]但表只有 50 行,直接IndexError,这个好办。软越界是iloc[45:100],不会报错,只是给你 5 行,悄无声息。做分页或者分块处理时,这种"少给了也没提示"的行为要特别注意,通常得自己校验返回长度。

还有一个实用但少人用的点:iloc支持负数,iloc[-1]是最后一行。但要注意iloc[-0]等价于iloc[0],不是倒数第一个。

3.3 行列同时筛选,以及链式索引那个老问题

loc的完整签名是df.loc[行选择器, 列选择器],两个维度可以同时给条件。这在只需要几列的场景下很有价值:

sub = df.loc[(df['amount'] > 500) & df['status'].eq('paid'), ['order_id', 'amount', 'created_at']]

一次性拿到需要的列,比先筛行再切列少一次中间对象的构造。数据量大的时候,这个差别不算小。

然后是那个绕不开的SettingWithCopyWarning。它的成因是:df[df['amount'] > 100]['flag'] = 1这种链式写法,第一步返回的可能是一个视图也可能是副本,Pandas 无法确定,于是第二步赋值可能改在原表上,也可能改在一个即将被丢弃的临时对象上。解决办法不是加copy()压警告,而是改写成单次loc:

df.loc[df['amount'] > 100, 'flag'] = 1

一行搞定,语义明确,改的一定是df本身。这个写法我建议直接刻进肌肉记忆,凡是"按条件改某列的值",一律用df.loc[条件, 列] = 值。

3.4at/iat做标量访问

df.at[行标签, 列标签]和df.iat[行位置, 列位置]是单值访问,比loc/iloc快,因为省掉了数组包装的开销。日常写代码用得不多,但在循环里取值、或者写测试断言时很顺手。要注意at一次只能取一个值,传切片会报错,这一点和loc不同。

4.query():把条件写成一句话

4.1 语法糖背后的性能甜点

query()让你用字符串写筛选条件,列名不用反复写df['xxx']:

df.query("amount > 500 and status == 'paid'")

注意这里可以用and/or,因为整个表达式是被解析成类 Python 语法再编译的,不是在 Python 层面做布尔求值。这也是它相比布尔索引的一个便利点:不用记着换成&和|。

性能上,如果环境里装了numexpr,query()会把表达式编译成字节码并对大型数组做分块并行计算,在千万行量级、条件包含多个算术运算时优势比较明显。没装numexpr的话,它会退化成普通的求值路径,性能跟布尔索引差不多,甚至略慢一点,因为多了字符串解析的开销。

4.2 引用外部变量、带空格的列名、字符串匹配

三个高频用法。

引用外部变量用@:

lower, upper = 100, 2000 df.query("amount >= @lower and amount <= @upper")

列名带空格或者跟 Python 关键字冲突时,用反引号包起来:df.query("`order status` == 'paid'")。

字符串匹配直接在表达式里写:

df.query("channel.str.startswith('online')", engine='python')

这里必须加engine='python',因为默认的numexpr引擎不支持.str方法。这一点不写清楚很容易让人以为query不支持字符串操作。

4.3 什么时候别用query()

query()不是万能的。首先,它不支持所有表达式,比如某些复杂的自定义函数调用、多层索引的某些访问方式。其次,条件里的变量如果名字跟列名撞了,@忘了写就会静默取列而不是取变量,这种 bug 极难查。第三,条件动态拼接时容易出现字符串注入式的错误,比如值里带引号。

我的口径是:条件简单、一次性的探索分析用query(),写进生产代码的筛选用布尔索引或loc。原因是生产代码需要静态可检查,字符串里的错误编辑器发现不了,跑起来才报。探索阶段图快,query()确实省打字。

5. 文本与缺失值:真实数据里最脏的两块

5.1str方法的正确姿势与 NA 处理

文本筛选离不开.str访问器。df['name'].str.contains('张')、.str.startswith(...)、.str.len() > 3、.str.upper() == 'ABC',这几个覆盖了绝大部分需求。

最关键的一条:.str.contains()的结果里,源数据是NaN的位置返回NaN,不是False。拿这个结果直接做布尔索引,会报cannot mask with non-boolean array containing NA / nan values。正确写法是显式指定:

mask = df['remark'].str.contains('退款', na=False)

na=False的意思是"空值当作不匹配",绝大多数业务场景下这是对的。如果你反过来想把空值也算作命中,那就na=True。这个小参数漏写,是新手在文本筛选上最常见的报错来源。

另外.str方法对非字符串列会返回NaN,比如一列里混了数字和字符串,dtype是object,str.contains只对字符串元素生效,数字元素得到NaN。稳妥的做法是先astype('string')统一成可空字符串类型,再操作。

5.2 正则筛选的写法与性能权衡

str.contains默认就把参数当正则处理,所以df['code'].str.contains(r'^A\d{4}$')能直接筛出"A 加四位数字"的编码。regex=False可以关掉正则,纯字面量匹配会快一些,而且不用担心(、.、*这些字符被当成元字符。

性能上,正则匹配是逐元素跑的 C 层实现,比 Python 循环快得多,但仍然是全表扫描。几百万行的文本列做复杂正则,耗时可能到秒级。优化的思路是先用便宜的等值或前缀条件缩范围:

pre = df[df['code'].str.startswith('A', na=False)] hit = pre[pre['code'].str.contains(r'^A\d{4}$', regex=True, na=False)]

startswith走的是前缀比较,比全正则便宜,先砍掉大部分行再做精细匹配,实测能省一半以上时间。

还有str.match和str.contains的区别:match只匹配开头,等价于正则加^;fullmatch要求整串完全匹配。这三个选错,结果会莫名其妙地"多筛了"或"少筛了"。

5.3isna/notna:把缺失值当成一个维度来筛

isna()和notna()返回布尔 Series,可以直接用于索引,也可以配合any(axis=1)找出"任意一列为空"的行:

rows_with_any_na = df[df.isna().any(axis=1)] rows_all_na = df[df.isna().all(axis=1)]

df.isna().sum()是每次拿到新表必跑的一行,按列统计缺失数量,比df.info()更直观,因为它给出了具体数字,你能立刻判断哪些列需要处理。

有一点容易被忽略:空字符串''和NaN是两回事,isna()抓不到空字符串。上游系统导出的数据里,''和' '(空格)非常常见。所以我的清洗习惯是两步走,先replace把空白字符统一成NaN,再走isna逻辑:

df['name'] = df['name'].replace(r'^\s*$', np.nan, regex=True) df['name'] = df['name'].str.strip()

顺序很重要,先替换再 strip,或者先 strip 再替换都行,但不能只做一步,因为' '经过strip变成'',还需要再被识别为空。

6. 工具型 API:按类型、按名称、按大小筛

6.1select_dtypes:按数据类型批量挑列

做特征工程时经常要"把所有数值列拿出来统一处理"或者"把对象列单独看"。select_dtypes就是干这个的:

num_cols = df.select_dtypes(include='number').columns.tolist() obj_cols = df.select_dtypes(include=['object', 'string']).columns.tolist()

include和exclude可以同时用。有个细节要留意:bool在 NumPy 里算number的子类,所以include='number'会把布尔列也带出来。不想要的话加exclude='bool'。这个坑我踩过一次,把一个标记列当成数值列做了标准化,结果全是 0 和 1 被压成负数,排查了半天。

6.2filter()筛的是列名,不是内容

这是本章最需要强调的认知纠正:df.filter(like='amount')筛的是列名或索引名,不是按行内容筛。它的三个参数items(精确名称列表)、like(名称包含)、regex(名称正则)全都是在名字上做文章。想要按内容筛,回到布尔索引。

它的典型用途是从一个上百列宽表里挑出某一组同前缀的列,比如所有dim_开头的维度列:

dim_df = df.filter(regex='^dim_')

这个场景下它比手写列表推导清晰得多,而且不会因为记忆漏了某列。

6.3nlargest/nsmallest/sample/where/mask

几个高频小工具,放一起说。

nlargest(n, col)和nsmallest(n, col)是"排序后取前 N"的优化实现,内部用的是堆,比sort_values().head(n)快。有keep参数控制并列时的保留策略('first'/'last'/'all'),默认'first',做 TopN 排行榜时会用到'all'把并列的都留下。

sample(n=100, random_state=42)是随机抽样,做数据探查时比head()靠谱得多,因为head()只看开头,而排序过的数据开头往往不代表全体。固定random_state保证可复现,写进报告里别人能对着同一批样本看。

where(cond, other)和mask(cond, other)是条件替换,前者"不满足条件的替换成 other",后者"满足条件的替换成 other",两者互为反面。它们不改原表,返回新表。清洗时把异常值打成NaN很顺手:

df['amount_clean'] = df['amount'].where(df['amount'].between(0, 100000))

配合inplace参数也能改原表,但我不推荐用inplace,可读性差而且链式调用时不安全。

方法筛选维度典型场景是否常用
布尔索引df[...]行内容任意条件组合极高
loc行标签 + 列标签复杂条件 + 选列极高
iloc行列位置首尾行、前 N 行高
query行内容探索分析、条件较长中
filter列名 / 索引名按前缀挑列中
select_dtypes列的 dtype批量特征处理中
nlargest某列数值TopN中
where/mask行内容条件替换中

7. 完整实操:一份模拟订单表的筛选流程

7.1 先把数据造出来

光讲语法没意思,走一遍真实流程。下面这份模拟订单表,故意埋了几个常见的脏数据形态:状态有大小写混用、金额有空值、渠道里有测试入口、备注里有空字符串和NaN并存。

import numpy as np import pandas as pd rng = np.random.default_rng(7) n = 20000 df = pd.DataFrame({ 'order_id': [f'OD{i:06d}' for i in range(n)], 'status': rng.choice(['paid', 'Paid', 'PAID', 'pending', 'refunded'], n), 'channel': rng.choice(['online_web', 'online_app', 'offline', 'internal_test'], n, p=[0.4, 0.35, 0.2, 0.05]), 'amount': rng.gamma(2.0, 300, n).round(2), 'created_at': pd.date_range('2024-01-01', periods=n, freq='min'), 'remark': rng.choice(['', ' ', '正常', '退款申请', np.nan], n, p=[0.1, 0.05, 0.6, 0.15, 0.1]), }) # 人为制造缺失和异常 df.loc[rng.choice(n, 300, replace=False), 'amount'] = np.nan df.loc[rng.choice(n, 50, replace=False), 'amount'] = -999

跑完先做一遍体检,这一步别省:

print(df['status'].value_counts()) print(df.isna().sum()) print(df['remark'].replace(r'^\s*$', np.nan, regex=True).isna().sum())

status那一列你会看到paid、Paid、PAID三个值,如果不去重直接按== 'paid'筛,会丢掉三分之二的已支付订单。这是我在真实项目里遇到次数最多的一类问题,而且它不报错,只是结果偏小。

7.2 分步筛出目标人群

目标是:2024 年上半年、非测试渠道、状态为已支付(忽略大小写)、金额在 0 到 50000 之间、备注里提到"退款"的订单。

第一步,状态统一:

df['status_clean'] = df['status'].str.lower().str.strip()

第二步,逐个条件写清楚,方便单独验证:

c_status = df['status_clean'].eq('paid') c_channel = ~df['channel'].eq('internal_test') c_amount = df['amount'].between(0, 50000) # NaN 自动为 False c_time = df['created_at'].between('2024-01-01', '2024-06-30 23:59:59') c_remark = df['remark'].str.contains('退款', na=False) mask = c_status & c_channel & c_amount & c_time & c_remark print({k: int(v.sum()) for k, v in { 'status': c_status, 'channel': c_channel, 'amount': c_amount, 'time': c_time, 'remark': c_remark}.items()}) print('最终命中:', int(mask.sum()))

这一步的价值全在中间那行打印。每个条件的命中数单独看一眼,如果c_remark只有 3 行,而你的预期是几百行,那说明备注字段的清洗有问题,而不是数据真的没有。我见过太多人一口气把五个条件括起来,最后结果不对,只能一个个注释掉重跑。

第三步,拿到结果并只保留需要的列:

result = df.loc[mask, ['order_id', 'status_clean', 'amount', 'created_at', 'remark']] result = result.sort_values('amount', ascending=False)

第四步,顺手验证一下有没有漏网。把status的原始值和清洗值做个交叉对照:

print(pd.crosstab(df['status'], df['status_clean']))

如果出现某个原始值清洗后落到意料之外的类别,说明.str.lower()之外还有别的脏东西,比如前后空格或者全角字符。

7.3 几种写法的耗时对比

条件一样,写法不同,速度会有差别。下面这组数据是在一台普通开发笔记本上跑的,20 万行数据,重复 20 次取中位数,只看相对量级,别当绝对值:

写法相对耗时说明
布尔索引 +&1.0(基准)最直接,无额外解析开销
query()无 numexpr约 1.2多了表达式解析
query()有 numexpr约 0.7多条件算术运算时优势明显
.isin()代替多组==+|约 0.4集合越大优势越明显
nlargest代替sort_values().head()约 0.5n 远小于总行数时

几条结论:条件里的离散值超过 3 个,优先换isin;多条件涉及算术运算(比如amount * rate > 100)且数据量大,装个numexpr会有惊喜;TopN 场景别用排序。

还有一点容易被忽略:筛选要尽早做。如果你后面还要merge两个大表,先把无关行筛掉,再合并,内存峰值会低一大截。合并之后再筛是最费内存的顺序,因为中间结果全量存在。

8. 常见问题速查与踩坑记录

8.1 一张速查表

现象大概率原因处理方式
ValueError: truth value of a Series is ambiguous用了and/or换成&/|,每个条件加括号
cannot mask with non-boolean array containing NA条件结果含NaNstr.contains(..., na=False)或fillna(False)
筛选结果少了空值行!=把NaN排除了补| df[col].isna()
结果比预期少很多字符串大小写、前后空格不一致先str.lower().str.strip()再比
切片少了一行iloc左闭右开改用loc或把右端点 +1
SettingWithCopyWarning链式赋值改成df.loc[cond, col] = value
筛选 0 行但数据明明有列是字符串型而条件写的是数字确认 dtype,必要时astype
布尔索引返回 DataFrame 而不是按行筛列名重复,取出的是 DataFrame检查df.columns.duplicated()

8.2 我在实际用的时候总结的几条经验

第一,永远先看value_counts()和缺失统计。这不是流程里的可选项,是筛条件之前的必做动作。你对数据分布的假设,八成会在这一步被推翻。特别是那种"应该只有几个枚举值"的状态列,实际往往有十几个,还带着大小写变体。

第二,把掩码当中间变量存下来。一是能打印命中数做验证,二是能在后续多处复用,三是出了问题时可以单独拿这个掩码去df[mask].head()看实际命中的行长什么样。很多人调试筛选问题就是反复改条件重跑,其实把掩码拿出来看一眼样本行,问题立刻现形。

第三,改值一律用df.loc[cond, col] = value。不接受其他写法。链式索引这个坑,只要踩过一次导致数据被改错,就会变成肌肉记忆。

第四,条件多的时候先做减法再做加法。我的习惯是先按最严格、最便宜的条件筛一遍,把数据量降到一个能head(20)看完的规模,再在子集上逐步加条件验证。反过来先加所有条件、最后发现结果不对,排查成本高一个量级。

第五,别迷信一行流。网上很多"一行代码实现 XX 筛选"的写法,可读性极差,条件一改就崩。生产代码里我更愿意写四五行中间变量,每个变量名说清一个业务含义。代码是给人看的,机器只负责跑。

第六,筛选前后各记一次行数。print(len(df), len(result))这行看着傻,但它能在第一时间告诉你掉了几成数据。如果掉了 90% 而你预期只掉 10%,问题就在这一次筛选里,不用往下查。

8.3 几个容易混淆的边界情况

df[cond]和df.loc[cond]在大多数情况下等价,但列名重复时前者会出问题,后者更稳。所以我现在基本统一写loc。

df[['a', 'b']]是选列,df[cond]是筛行,同一个__getitem__根据传入类型走不同分支。传列表是选列,传布尔 Series 是筛行,传切片是筛行(且和iloc的切片语义在某些版本上有微妙差异)。知道这个分支逻辑,遇到怪问题时能猜到方向。

dropna()看起来是筛选,但它默认how='any',会把任意一列有空值的行全删掉。一个 30 列的表,只要有一列缺失率高,dropna()能把表删空。用之前一定加subset限定列,或者干脆别用,改用dropna(subset=['关键列'])。

最后提一个我自己的偏好:筛选逻辑尽量写成纯函数,输入 DataFrame 和参数,输出筛选后的 DataFrame,不做inplace修改。这样每一步都能单独测,出了问题能二分定位,比一坨写在一起的脚本强太多。数据量再大,也不差这一次拷贝的内存。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 22:06:49

工业机器人结构设计:核心部件与优化实践

1. 工业机器人结构设计概述工业机器人作为现代制造业的核心装备&#xff0c;其结构设计直接决定了机器人的性能、精度和使用寿命。一个优秀的工业机器人结构设计需要综合考虑机械强度、运动学特性、动力学性能以及实际工况需求等多方面因素。在实际项目中&#xff0c;我设计过六…

作者头像 李华
网站建设 2026/10/27 22:05:21

生产环境AI Agent部署实战:挑战与优化策略

1. 生产环境AI Agent部署的挑战与机遇把AI Agent从实验室搬到生产环境&#xff0c;就像把赛车改装成家用车——既要保留性能优势&#xff0c;又要适应真实路况。去年我们团队部署的客服Agent系统&#xff0c;上线首周就遭遇了200%的突发流量冲击&#xff0c;这让我深刻认识到生…

作者头像 李华
网站建设 2026/10/27 22:05:19

数学建模竞赛四大模型速查:预测分类优化评价选题指南

简介&#xff1a;这份PDF资料面向备战数学建模竞赛的高校学生与指导教师&#xff0c;系统梳理竞赛中高频出现的四大类模型&#xff0c;帮助读者在有限时间内建立清晰的模型选型框架。内容按预测、分类、优化、评价四条主线展开&#xff1a;预测部分涵盖神经网络、灰色预测、线性…

作者头像 李华
网站建设 2026/10/27 22:03:43

谷歌云Gemini版本对比与实战指南

1. 谷歌云Gemini版本全景解析作为谷歌云平台最新推出的AI服务矩阵&#xff0c;Gemini系列产品正在快速迭代中。目前主要分为三个版本层级&#xff1a;免费版&#xff08;Gemini Free&#xff09;、付费标准版&#xff08;Gemini Pro&#xff09;以及企业定制版&#xff08;Gemi…

作者头像 李华
网站建设 2026/10/27 22:03:42

营销技能的真相:不是文案技巧,而是解决问题的系统能力

做营销这些年&#xff0c;我越来越觉得“marketing skills”这个词被严重误解了。很多人以为营销技能就是会写文案、会做图、会投广告&#xff0c;甚至觉得会刷短视频就是懂营销。真不是这样。营销技能的本质&#xff0c;是“持续解决问题的系统能力”&#xff0c;是把用户、产…

作者头像 李华
网站建设 2026/10/27 22:02:30

端到端测试(e2e)从零落地:profile1配置化与稳定性治理实战

做测试这行久了&#xff0c;你就会发现一个特别尴尬的现象&#xff1a;单元测试写了一堆&#xff0c;覆盖率报告漂亮得很&#xff0c;可真要上线的时候&#xff0c;心里还是没底。说白了&#xff0c;测试金字塔底下那两层再扎实&#xff0c;也没法回答一个最根本的问题——用户…

作者头像 李华