RSS
菜单
全部文章快讯开发科技深度热点

第 61 章 面试踩坑:pandas 与 Python 概念问答篇

内容摘要

本篇是"面试踩坑精讲"系列第三篇,收拾 pandas 技术面里最容易被当场问倒的基础概念问答。11 个坑覆盖:视图与副本、链式赋值、groupby 语义、axis 方向、loc 与 iloc、apply/agg/transform、NaN 比较、Series 与 DataFrame 区别、read_csv 参数、布尔索引选择边界、SettingWithCopyWarning 现场排错。前置知识:建议先读[第 06 章 数据选择与索引](06-数据选择与索引.md)、[第 13 章 分组聚合:groupby](13-分组聚合:groupby.md)、[第 18 章 视图副本与 SettingWithCopyWarning](18-视图副本与SettingWithCopyWarning.md)。学完后你能把这些"看着简单、一说就错"的概念答出原理深度,而不是靠背 API。

📖 本篇简介:本篇是"面试踩坑精讲"系列第三篇,收拾 pandas 技术面里最容易被当场问倒的基础概念问答。11 个坑覆盖:视图与副本、链式赋值、groupby 语义、axis 方向、loc 与 iloc、apply/agg/transform、NaN 比较、Series 与 DataFrame 区别、read_csv 参数、布尔索引选择边界、SettingWithCopyWarning 现场排错。前置知识:建议先读第 06 章 数据选择与索引、第 13 章 分组聚合:groupby、第 18 章 视图副本与 SettingWithCopyWarning。学完后你能把这些"看着简单、一说就错"的概念答出原理深度,而不是靠背 API。
⚠️ 口径说明:本文基于通用面试经验,示例对话与数字均为示意;代码在本机 pandas 3.0.6 / numpy 2.5.3 实测通过。pandas 3.x 已默认并永久启用 Copy-on-Write(写时复制),文中视图/副本行为均按此口径说明。

目录

1. 坑 1:视图与副本分不清

2. 坑 2:链式赋值说不清

3. 坑 3:groupby 语义说不清

4. 坑 4:axis 方向答反

5. 坑 5:loc 与 iloc 混淆

6. 坑 6:apply agg transform 分不清

7. 坑 7:NaN 比较答不对

8. 坑 8:Series 与 DataFrame 区别说不清

9. 坑 9:read_csv 参数答不全

10. 坑 10:布尔索引选择方式边界不清

11. 坑 11:SettingWithCopyWarning 现场答不出

12. 本篇速查

13. 练习题


坑 1:视图与副本分不清

场景与错误示范

场景:面试官白板写了一段代码,问"改 sub 会不会影响原表 df"。

import pandas as pd

# 示意数据
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
sub = df[df["A"] > 1]   # 取出一个子集
sub["B"] = 999           # 改这个子集
print(df["B"].tolist())  # 原表 B 列变成什么?

错误示范对话:

面试官:sub 是 df 的视图还是副本?改 sub["B"] 原表会不会变?

>

候选人:(很自信)是视图!所以改 sub 原表也会跟着变。

>

面试官:那为什么老版本 pandas 会抛 SettingWithCopyWarning?

为什么是坑

  • "视图还是副本"在老版本里没有一刀切答案:.loc 切片、列选择、布尔过滤,不同操作是否共享底层内存,规则极其微妙,官方都写了"无法可靠预测"——这正是那个著名警告存在十几年的原因。
  • 考察点不是记结论,而是"你知道这事有坑":一口咬定"永远是视图"或"永远是副本",都说明你没被咬过。真正想听的是"我知道不确定,所以从不依赖它"。
  • pandas 3.x 已从根上解决:默认且永久启用 Copy-on-Write(CoW)——任何子对象在要被修改时都会自动拷贝。现在取出的 sub 可安全当副本,改它绝不动原表(本机实测):
import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
sub = df[df["A"] > 1]   # CoW 下,取出后可安全当作副本
sub["B"] = 999           # 只改 sub,不会回写原表
print(df["B"].tolist())
# 输出:[4, 5, 6]   ← 原表纹丝不动

正确做法

面试这样答,既准确又跟得上版本:

"老版本 pandas 里子对象可能是视图也可能是副本,规则不确定,所以有 SettingWithCopyWarning。但 pandas 2.0 推 CoW、3.0 起永久默认开启,现在子对象写入时一定自动拷贝,我可以放心当副本改;想要明确独立的数据就显式 .copy()。"
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})

clean = df[df["A"] > 1].copy()   # 要独立副本去折腾 → 显式 .copy()
clean["B"] = 999

df.loc[df["A"] > 1, "B"] = 999   # 要改原表 → .loc 一步到位(见坑 2)

加分点与延伸

  • 加分点:主动说出"CoW 后视图/副本对普通用户基本透明了",比还在背老规则高一个段位。
  • 延伸:老项目(pandas 1.x)里"改子对象原表跟着变"的诡异行为,升级到 3.x 后会自动消失,这是升级的隐性收益。

坑 2:链式赋值说不清

场景与错误示范

场景:白板题,要求"把 A>1 的行的 B 列改成 999"。

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})

# 错误示范:候选人这么写
df[df["A"] > 1]["B"] = 999

错误示范对话:

面试官:你这行能生效吗?

>

候选人:能啊,先筛出行再选列,赋值上去。

>

面试官:(跑出来原表 B 还是 [4,5,6])原表变了吗?

>

候选人:啊?怎么没改上……我平时都这么写的。

为什么是坑

  • 链式赋值是两次 `[]` 索引:df[df["A"] > 1] 先返回一个中间对象,再对它做 ["B"] = 999。CoW 下这个中间对象永远是副本,赋值落到一个马上丢弃的临时副本上——原表一动不动。
  • 它危险在"静默失败":不报错、结果悄悄不对,你拿着没更新的数据出报表,业务就错了。这比直接报错可怕十倍。
  • 考察点:能说出"这是两次索引、中间结果是副本"的人,和只会说"哦我以后用 loc"的人,差距一眼可见。

正确做法

一步到位,用 .loc[行, 列] 单步赋值:

import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df.loc[df["A"] > 1, "B"] = 999   # 行条件 + 列名写在同一个 loc 里
print(df["B"].tolist())
# 输出:[4, 999, 999]   ← 原表正确被更新

参考话术:

"链式 df[mask][col] = v 是两步索引,中间对象在 CoW 下是副本,赋值写在副本上原表收不到。正确姿势是 df.loc[mask, col] = v,行列同一步定位,pandas 就知道你要改原表。"

加分点与延伸

  • 加分点:补一句"pandas 3.x 里这种写法会直接抛 ChainedAssignmentError,不再静默失败"(见坑 11)。
  • 延伸:df.col 属性式取值在赋新列时也有坑,新列优先用 df["B"] = ... 的字典写法。

坑 3:groupby 语义说不清

场景与错误示范

场景:面试官问"groupby 完返回什么?分组键是索引还是列?"

错误示范对话:

面试官:groupby("类别") 之后,"类别"这一列还在不在?

>

候选人:在吧……

>

面试官:那 as_index=False 和事后 reset_index() 是不是一回事?

>

候选人:(沉默)

为什么是坑

  • groupby 默认把分组键变成行索引,而不是普通列:这是无数人"为什么我的表少了一列"的根源。下面实测把三种形态一次摆清:
import pandas as pd

g = pd.DataFrame({"类别": ["x", "x", "y", "y"], "金额": [10, 20, 30, 40]})

# 默认:分组键"类别"成了行索引,结果是 Series(x=30, y=70)
print(g.groupby("类别")["金额"].sum())

# as_index=False:分组键保留为普通列,结果是 DataFrame(两行:x=30, y=70)
print(g.groupby("类别", as_index=False)["金额"].sum())

# 先默认聚合,再 reset_index():把行索引"类别"还原成普通列
# 输出与上面 as_index=False 完全一致(两行:x=30, y=70)
  • groupby 返回的不是数据,是一个"分组对象":它本身不计算,要接 sum/mean/agg/transform/apply 才出结果(split-apply-combine 的第一步)。
  • 考察点:能不能说清"分组键去哪了、结果是 Series 还是 DataFrame、怎么变成你要的形状"——出报表、做 merge 全在这几个参数间切换。

正确做法

按"最后要什么形状"选写法,两者效果等价但推荐前者:

import pandas as pd

g = pd.DataFrame({"类别": ["x", "x", "y", "y"], "金额": [10, 20, 30, 40]})

res = g.groupby("类别", as_index=False)["金额"].sum()  # 一步到位
# res2 = g.groupby("类别")["金额"].sum().reset_index() # 事后补救,等价

参考话术:

"groupby 是惰性分组对象,接聚合才出数。默认分组键会变成行索引、结果是 Series;加 as_index=False 分组键保留为普通列、结果是 DataFrame;事后 reset_index() 也能把索引拉回列。出报表或要 merge 时我习惯直接 as_index=False。"

加分点与延伸

  • 加分点:说出 agg 能一次算多指标、transform 能把结果广播回原表长度(见坑 6)。
  • 延伸:分组键里有 NaN 时,默认整组被丢弃,dropna=False 可保留(见坑 7)——groupby 另一高频追问点。

坑 4:axis 方向答反

场景与错误示范

场景:面试官快问:"df.drop(0) 删行还是删列?df.sum(axis=1) 按行加还是按列加?"

错误示范对话:

候选人:axis=0 是行,axis=1 是列,这个我知道。

>

面试官:那 df.sum(axis=0) 是每列一个和,还是每行一个和?

>

候选人:(卡住)axis=0 是行……那按行加?

>

面试官:方向答反了——axis=0 恰恰是每列求和。

为什么是坑

  • "axis=0 是行"这句口诀没错,但容易在聚合上记反:drop 时 axis=0 沿行方向删即删整行;sum 时 axis=0 沿行方向"折叠",把上下压没了、结果落在列上——即每列求和。方向词和结果词是反的,这是 axis 问题的精髓。
  • 记忆法门:axis 指"被压缩掉的那个维度"。axis=0 压掉行(行没了)→ 剩下列,即按列聚合;axis=1 压掉列 → 剩下行,即按行聚合。
import pandas as pd

t = pd.DataFrame({"a": [1, 2], "b": [3, 4]})

print(t.drop(0, axis=0))      # axis=0 压行 → 删掉第 0 行,剩一行
print(t.drop("a", axis=1))     # axis=1 压列 → 删掉列 a,只剩列 b

print(t.sum(axis=0).tolist())  # 压掉行 → 每列和 [3, 7]
print(t.sum(axis=1).tolist())  # 压掉列 → 每行和 [4, 6]

正确做法

别背口诀,直接说"被压缩的维度":

"axis 我这样记:它指要被压缩掉的那个轴。axis=0 压掉行,聚合出来是每列一个数;axis=1 压掉列,聚合出来是每行一个数。删的时候同理:axis=0 删整行,axis=1 删整列。"

加分点与延伸

  • 加分点:新版支持 df.drop(index=0)、df.drop(columns="a") 具名写法,连 axis 都不用传——说出来显得跟得上演进。
  • 延伸:apply 同理,apply(axis=0) 对每列作用一次,apply(axis=1) 对每行作用一次。

坑 5:loc 与 iloc 混淆

场景与错误示范

场景:面试官给了非默认索引的 Series,问"loc 和 iloc 差在哪?切片包不包含右端?"

import pandas as pd

s = pd.Series([10, 20, 30], index=[100, 200, 300])

错误示范对话:

面试官:s.loc[100:200] 和 s.iloc[0:2] 切出来分别几个元素?

>

候选人:都是左闭右开,两个吧……

>

面试官:loc 按标签切片两端都包含;iloc 按位置切片才是左闭右开。这个你记反了。

为什么是坑

  • `loc` 看标签,`iloc` 看位置:loc[200] 找"标签等于 200 的行",不是第 200 行;iloc[1] 才是"第二个位置"。索引用日期、字符串或乱序数字时,这区别是致命的。
  • 切片端点行为不同,是最高频追问:loc 闭区间(含右端标签),iloc 左闭右开(不含右端位置)。实测:
import pandas as pd

s = pd.Series([10, 20, 30], index=[100, 200, 300])

print(s.loc[200])    # 20  ← 按"标签 200"找
print(s.iloc[1])     # 20  ← 按"第 2 个位置"找

print(s.loc[100:200].tolist())   # [10, 20]  loc 含右端标签 200
print(s.iloc[0:2].tolist())      # [10, 20]  iloc 左闭右开,不含位置 2
  • 越界行为也不同:loc 取不存在的单个标签报 KeyError;iloc 取越界位置报 IndexError。
  • 考察点:分清"按什么定位"——业务里常用日期做索引,用错 loc/iloc 轻则取错数重则报错。

正确做法

一句话原则:

"loc 按标签、切片闭区间;iloc 按整数位置、切片左闭右开。索引不是默认 RangeIndex 时,我一律先想清楚是要标签还是位置,绝不混用。"
import pandas as pd

df = pd.DataFrame({"金额": [10, 20, 30]}, index=["1月", "2月", "3月"])

print(df.loc["2月":"3月"])   # 要"2到3月"两行 → 按标签,闭区间(含 3 月)
# 输出两行:2月=20, 3月=30
print(df.iloc[0:2])          # 要"前两行" → 按位置,左闭右开(不含位置2)

加分点与延伸

  • 加分点:补充 df[["A","B"]] 选列、df[mask] 选行,而 loc 是行列都能精确定位的万能钥匙。
  • 延伸:.at/.iat 是 loc/iloc 的单值快取版,取单个单元格更快——知道这个说明你写过性能敏感代码。

坑 6:apply agg transform 分不清

场景与错误示范

场景:面试官问"groupby 后 agg、transform、apply 区别?各自返回什么形状?"

错误示范对话:

面试官:我想给每个元素减去它所在组的均值,用哪个?

>

候选人:用 agg 吧?

>

面试官:agg 每组只压一个数,你要的是和原表一样长,形状对不上。再想想?

>

候选人:那……transform?

>

面试官:对。那 apply 又差在哪?

为什么是坑

  • 三者最大区别是"返回形状":
  • agg:每组压成标量,结果比原表短(每组一行);
  • transform:结果与原表等长,把组级结果广播回每个元素;
  • apply:最灵活,输入整组 Series,想返回什么形状都行,但最慢、最易出意外。
import pandas as pd

a = pd.DataFrame({"grp": ["x", "x", "y", "y"], "v": [1, 2, 3, 4]})

print(a.groupby("grp")["v"].agg(["mean", "sum"]))   # agg:结果变短(x:1.5/3, y:3.5/7)
print(a.groupby("grp")["v"].transform("mean").tolist())  # transform:等长广播 [1.5,1.5,3.5,3.5]
print(a.groupby("grp")["v"].apply(lambda x: x - x.mean()).tolist())  # apply:形状自由 [-0.5,0.5,-0.5,0.5]
  • "每个元素减组均值"是 transform 的招牌场景:天生等长,配合原列做减法一步到位。
  • 考察点:你是"会调 API"还是"理解形状约束"——面试官就爱用"返回什么形状"区分初中级。

正确做法

按"要什么形状"选工具:

import pandas as pd

a = pd.DataFrame({"grp": ["x", "x", "y", "y"], "v": [1, 2, 3, 4]})
a["组均值"] = a.groupby("grp")["v"].transform("mean")   # 等长贴回
a["组内标准化"] = a["v"] - a["组均值"]
# 结果:每行"组内标准化"= 该元素减所在组均值,行数与原表一致(4 行)

参考话术:

"我按返回形状选:要每组压成汇总数用 agg;要结果和原表等长、把组统计广播回去用 transform,组内标准化就是它的经典场景;apply 最灵活但最慢,前面两个都满足不了才用。"

加分点与延伸

  • 加分点:提到非分组版 df.apply 对行/列整体作用、逐元素用 df.map(旧称 applymap),常和分组版一起被追问。
  • 延伸:性能敏感时优先向量化/内置聚合,apply 传 lambda 是 Python 层循环,大数据量慢。

坑 7:NaN 比较答不对

场景与错误示范

场景:面试官快问快答。

错误示范对话:

面试官:np.nan == np.nan 是 True 还是 False?

>

候选人:True 吧?都是空值嘛。

>

面试官:错,是 False。那怎么判断一个值是不是 NaN?

>

候选人:用 == np.nan?

>

面试官:……那它永远是 False,你一行都筛不出来。正确是 pd.isna()。

为什么是坑

  • NaN 不等于任何值,包括它自己:这是 IEEE 754 浮点数的规定,不是 pandas 的锅。所以 x == np.nan 永远 False,必须用 pd.isna():
import numpy as np
import pandas as pd

print(np.nan == np.nan)   # False  ← 连自己都不等于自己
print(pd.isna(np.nan))    # True   ← 判断空值的正确姿势
  • 聚合函数默认跳过 NaN:sum/mean/min 把 NaN 当不存在(如 pd.Series([1.0,None,3.0]).sum() 得 4.0,不是 NaN),不会让整个结果变 NaN。
  • groupby 里 NaN 有两层行为,极易答漏:① 作为组内数值参与聚合时被跳过;② 作为分组键时整组默认被丢弃:
import pandas as pd

# ① NaN 作组内数值 → 聚合跳过
gn = pd.DataFrame({"g": ["a", "a", "b"], "v": [1.0, None, 2.0]})
print(gn.groupby("g")["v"].sum())   # a=1.0, b=2.0(组 a 的空值被跳过)

# ② NaN 作分组键 → 该组默认整个消失
gk = pd.DataFrame({"g": ["a", "a", None, "b"], "v": [1, 2, 3, 4]})
print(gk.groupby("g")["v"].sum())   # 只有 a=3, b=4;g=None 那组(v=3)不见了!

正确做法

背下三句口径:

"第一,NaN == NaN 是 False,判空只能用 pd.isna();第二,sum/mean 默认跳过 NaN,想让它传染成 NaN 就传 skipna=False;第三,NaN 作分组键默认整组丢弃,要保留就传 dropna=False。"
import pandas as pd

gk = pd.DataFrame({"g": ["a", "a", None, "b"], "v": [1, 2, 3, 4]})
print(gk.groupby("g", dropna=False)["v"].sum())   # 空值分组被保留为 NaN 组

加分点与延伸

  • 加分点:说清 pd.isna() 和 pd.isnull() 是别名,notna/notnull 是取反。
  • 延伸:筛选排除空值写 df[df["col"].notna()],千万别写 df[df["col"] != np.nan]——后者一行都筛不出。

坑 8:Series 与 DataFrame 区别说不清

场景与错误示范

场景:白板题:"df['A'] 和 df[['A']] 返回的是同一种东西吗?"

错误示范对话:

面试官:df["A"] 是什么类型?df[["A"]] 呢?

>

候选人:都是这一列吧,有区别吗?

>

面试官:一个是一维 Series,一个是二维 DataFrame,调 .sum() 结果不一样、喂建模函数也不认。

为什么是坑

  • Series 一维、DataFrame 二维:df["A"] 取出来就"降级"成一维 Series;df[["A"]](多套一层列表)仍保持二维 DataFrame。形状差一个维度,很多函数直接报错。
import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3]})

print(type(df["A"]).__name__, df["A"].shape, df["A"].ndim)    # Series (3,) 1
print(type(df[["A"]]).__name__, df[["A"]].shape, df[["A"]].ndim)  # DataFrame (3, 1) 2
  • dtype 语义不同:Series 只有一列,自己有一个 dtype;DataFrame 每列 dtype 可能不同,所以 df.dtypes 返回"每列一个 dtype"。说"DataFrame 的 dtype 是 int64"本身就是错的。
  • "一列"和"一行"也不同:df.loc[0] 取出的一行是 Series(索引变成原列名),和取列的 Series 索引含义完全不同。
  • 考察点:类型和形状是 pandas 地基,搞混后 merge、喂模型、作图全是雷。

正确做法

用"维度 + 索引"两句话说清:

"Series 是一维带标签数组,自己一个 dtype;DataFrame 是二维表,每列可不同 dtype,dtypes 逐列返回。取单列 df['A'] 得 Series,想保留二维就 df[['A']]。喂 sklearn、merge、concat 前我先 .shape 确认维度。"(创建上:pd.Series([1,2,3]) 建一维,pd.DataFrame({"A":[...]}) 建二维。)

加分点与延伸

  • 加分点:知道 df.loc[0](取行)和 df["A"](取列)都返回 Series,但 index 含义不同。
  • 延伸:model.fit(X, y) 里 X 要二维 DataFrame、y 一维 Series——X[["col"]] 和 X["col"] 的区别就在这里。

坑 9:read_csv 参数答不全

场景与错误示范

场景:面试官:"给你一个 10G 的 CSV,中文列名、有日期列、空值标记是 NA,你怎么读?"

错误示范对话:

候选人:pd.read_csv("文件.csv") 就好了。

>

面试官:中文乱码怎么办?日期想直接是时间类型?10G 内存装不下?里面 NA 识别成空值了吗?

>

候选人:乱码加 encoding……参数叫什么来着?

为什么是坑

  • `read_csv` 是数据工程师第一行代码,参数答不全 = "熟练 pandas"存疑。高频追问就那几个,每个对应一个真实痛点:
  • encoding:中文乱码(utf-8 / gbk);
  • dtype:防止空值把整数列顶成 float、长 ID 变科学计数;
  • parse_dates:日期列直接解析成 datetime;
  • usecols:只读需要的列,省内存;
  • chunksize:分块迭代读,对付大文件;
  • na_values:把 NA 等自定义标记识别成缺失值。
import pandas as pd

# 示意:带日期、带 NA 标记的小 CSV(本机实跑通过)
csv_text = "id,name,amount,date,note\n1,张三,10.5,2026-01-02,备注A\n2,李四,,2026-01-03,NA\n"
with open("_tmp_demo.csv", "w", encoding="utf-8") as f:
    f.write(csv_text)

rd = pd.read_csv(
    "_tmp_demo.csv",
    dtype={"id": "int64"},            # id 强制按整数读
    parse_dates=["date"],             # date 直接解析成日期
    usecols=["id", "amount", "date"],  # 只读三列,省内存
    na_values=["NA"],                 # 文本 NA 识别成缺失值
)
print(rd)
#    id  amount       date
# 0   1    10.5 2026-01-02
# 1   2     NaN 2026-01-03
print(rd.dtypes)   # id:int64, amount:float64, date:datetime64[us]

# chunksize:分块迭代,不一次性进内存(每块 1 行)
for chunk in pd.read_csv("_tmp_demo.csv", chunksize=1):
    print("块:", chunk["id"].tolist())   # 块: [1] / 块: [2]

正确做法

按场景报参数,体现"真搬过大文件":

"中文乱码先试 encoding='utf-8',不行换 'gbk';不想让空值把整数列顶成 float 就 dtype 指定;日期列上 parse_dates;只分析几列就 usecols 省内存;超大文件用 chunksize 分块;NA 这类自定义空值用 na_values 告诉它。"

加分点与延伸

  • 加分点:提一句 nrows(先读几行试水)、sep(分隔符)、thousands(千分位),说明处理过格式刁钻的真实数据。
  • 延伸:超大 CSV 还可配合 dtype 用更小类型(category、int32)降内存,引出第 22 章读写进阶。

坑 10:布尔索引选择方式边界不清

场景与错误示范

场景:白板给了订单表,面试官连出四题"分别怎么取"。

错误示范对话:

面试官:取"城市是北京或广州"的行,怎么写?

>

候选人:q[(q["城市"]=="北京") or (q["城市"]=="广州")]?

>

面试官:错——pandas 多条件不能用 or,要用 | 且每个条件加括号。"金额在 100 到 200 之间"呢?区间含不含端点?

为什么是坑

  • 四种选择方式各有边界,常被混为一谈:
  • 布尔索引 df[条件]:最通用;多条件用 &/|/~,且每个条件必须加括号(and/or 在 Series 上不成立);
  • query df.query("表达式"):字符串写条件,不用反复写表名前缀;
  • isin:成员判断(在某列表里),比一串 ==...|... 干净;
  • between:范围,左右端点都包含。
import pandas as pd

q = pd.DataFrame({"城市": ["北京", "上海", "北京", "广州"], "金额": [100, 200, 150, 300]})

# ① 布尔索引:多条件用 & 且每个条件加括号
print(q[(q["金额"] > 120) & (q["城市"] == "北京")])   # 城市=北京, 金额=150

# ② query:表达式写在字符串里,省掉重复的 q.
print(q.query("金额 > 120 and 城市 == '北京'"))         # 同上

# ③ isin:在集合里
print(q[q["城市"].isin(["北京", "广州"])])   # 3 行:北京×2 + 广州×1

# ④ between:双侧闭区间,含 100 和 200
print(q[q["金额"].between(100, 200)])        # 3 行:100/150/200(含端点)
  • 最高频错误就是 `and/or` 报错:ValueError: The truth value of a Series is ambiguous,本质是把 Python 布尔运算符用在了逐元素比较上。
  • 考察点:写取数逻辑的熟练度,选对工具直接决定代码干净不干净。

正确做法

按条件复杂度选最干净的写法:

"单条件布尔索引最直接;多条件记住 & | ~ 和括号;成员判断用 isin 比一串 or 清爽;连续区间用 between(闭区间);条件很长或类 SQL 场景用 query,不用反复写表名。"

加分点与延伸

  • 加分点:说清 df.filter(按列名/正则选列)和布尔索引选行不是一回事——名字像,作用域不同。
  • 延伸:between 默认两端都含;想不含某端传 inclusive= 参数,边界需求要想清楚。

坑 11:SettingWithCopyWarning 现场答不出

场景与错误示范

场景:候选人跑代码,终端滚出一行警告(大意是 "A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value"),面试官让他解释。

错误示范对话:

面试官:这行警告什么意思?怎么修?

>

候选人:哦这个啊,我一般看到警告就不管,反正代码能跑。

>

面试官:那你改的那个字段,原表真的改上了吗?

>

候选人:应该……改上了吧?我也没核对过。

为什么是坑

  • 这是 pandas 历史上最著名的警告。成因就是坑 1 + 坑 2 的合体:先取了切片(可能是副本),又链式地给它某列赋值,结果写到副本上、原表没动。它警告的是"你的赋值可能根本没生效"。
  • 老版本(pandas 1.x~2.x):这个警告叫 SettingWithCopyWarning,因为视图/副本不确定,pandas 只能猜、猜错了就警告。
  • pandas 3.0 永久启用 CoW 后行为彻底变了:链式赋值现在直接抛 ChainedAssignmentError,SettingWithCopyWarning 这个名字在 3.0 里已被移除;且 CoW 下中间对象永远是副本,原表一定不会被误改。问题从"悄悄改错"变成"明确报错 + 明确告诉你没改原表"。本机 pandas 3.0.6 实测:
import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df[df["A"] > 1]["B"] = 999   # 链式赋值
print(df["B"].tolist())
# 终端打印(pandas 3.x):
# ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series
# through chained assignment. ... Try using '.loc[row_indexer, col_indexer] = value' ...
# 原表:[4, 5, 6]   ← 明确没改上,不再是"悄悄失败"

正确做法

看到警告/报错,标准三步:

1. 识别:链式赋值写到了副本上,原表没被更新;

2. 改写法:两步索引合并成一步 df.loc[行条件, 列] = 值;

3. 核对:改完打印一眼原表,确认数据真的变了。

import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df.loc[df["A"] > 1, "B"] = 999   # 正确:一步 loc
print(df["B"].tolist())          # [4, 999, 999]  ← 原表正确更新

sub = df[df["A"] > 1].copy()     # 本来就想改独立副本 → 先 .copy()
sub["B"] = 0

参考话术:

"这个警告是链式赋值引起的:先取切片再选列赋值,写在副本上、原表没动。修法是 df.loc[mask, col] = v 一步到位。补充一下,pandas 3.0 永久启用 CoW 后,老的 SettingWithCopyWarning 已演变成明确的 ChainedAssignmentError,原表也永远不会被误改了。"

加分点与延伸

  • 加分点:主动讲出"CoW 之后视图/副本不再需要用户操心",说明跟进过版本演进,而非停留在 1.x 记忆。
  • 延伸:老项目这个警告频繁出现,根因是大量中间变量链式赋值;迁到 3.x 后会从"满屏警告"变成"少数明确报错",反而更好修。

本篇速查

| 坑号 | 坑名 | 一句话要点 | 正确做法关键词 |

|---|---|---|---|

| 坑 1 | 视图与副本分不清 | CoW 后子对象写入即拷贝,可安全当副本 | 想独立副本 .copy()、想改原表用 .loc |

| 坑 2 | 链式赋值说不清 | df[mask][col]=v 写到副本,原表不动 | 合并成 df.loc[mask,col]=v 一步赋值 |

| 坑 3 | groupby 语义说不清 | 分组键默认成行索引、结果是 Series | as_index=False 或 reset_index() |

| 坑 4 | axis 方向答反 | axis 指"被压缩掉的维度" | axis=0 压行→按列聚合;axis=1 压列→按行聚合 |

| 坑 5 | loc 与 iloc 混淆 | loc 按标签闭区间、iloc 按位置左闭右开 | 先想"标签还是位置",绝不混用 |

| 坑 6 | apply agg transform 分不清 | 按返回形状选:agg 短、transform 等长、apply 灵活 | 组内标准化用 transform |

| 坑 7 | NaN 比较答不对 | NaN≠自己,判空用 isna;聚合跳过、分组键丢弃 | pd.isna()、skipna、dropna=False |

| 坑 8 | Series 与 DataFrame 区别说不清 | 一维 vs 二维;df['A'] 是 Series、df[['A']] 是 DataFrame | 传参前先 .shape 确认维度 |

| 坑 9 | read_csv 参数答不全 | 六大参数对应六大真实痛点 | encoding/dtype/parse_dates/usecols/chunksize/na_values |

| 坑 10 | 布尔索引选择方式边界不清 | 多条件用 &\|~ 且加括号;成员用 isin;区间用 between | 按条件复杂度选最干净写法 |

| 坑 11 | SettingWithCopyWarning 现场答不出 | 链式赋值写到副本;3.x 变 ChainedAssignmentError | 改 .loc 一步到位 + 打印核对原表 |

练习题

题目 1:给定 df = pd.DataFrame({"A":[1,2,3],"B":[4,5,6]}),要把 A>1 的行的 B 改成 0。请写出正确写法,并说明为什么 df[df["A"]>1]["B"] = 0 不行。

参考答案要点:正确写法 df.loc[df["A"]>1, "B"] = 0;链式写法是两次索引、中间对象在 CoW 下是副本,赋值落在被丢弃的临时副本上原表收不到;pandas 3.x 会抛 ChainedAssignmentError。

题目 2:df.groupby("city")["amount"].sum() 返回的"city"是列还是索引?想把它变回普通列有哪两种办法?

参考答案要点:默认是行索引、结果是 Series;办法一是 groupby(..., as_index=False),办法二是事后 .reset_index();两者效果等价,推荐前者一步到位。

题目 3:为什么 np.nan == np.nan 是 False?怎么正确判断空值?groupby 时一个分组键全是 NaN,默认会发生什么?

参考答案要点:IEEE 754 规定 NaN 不与任何值相等包括自身;判空用 pd.isna()/notna() 而非 ==;NaN 作分组键时该组默认被整组丢弃,dropna=False 可保留。

题目 4:df.sum(axis=0) 是每行一个和还是每列一个和?df.drop("col", axis=1) 删的是行还是列?用一句话讲清判断方法。

参考答案要点:axis=0 压掉行方向、结果落在列上 → 每列求和;drop(axis=1) 压掉列 → 删整列。判断方法:axis 指"被压缩/被删掉的那个维度"。


下一篇预告:第 62 章 面试踩坑:原理与性能问答篇 —— 概念关过了,还有更深一层的原理与性能追问:向量化、内存布局、groupby 内部机制与大数据量优化,把"为什么快"讲清楚。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 61 章 面试踩坑:pandas 与 Python 概念问答篇》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。