📖 本篇简介:本篇是"面试踩坑精讲"系列的第六篇,聚焦技术面最硬核的一关——白板或共享编辑器上手写 pandas 代码。11 个坑覆盖:不澄清需求就开写、不写边界测试、变量命名差、列名硬编码、忽略空值与重复值、硬写循环不用内置 API、不主动说复杂度、axis方向写错、报错不改乱试、不验证结果就交、时间不够不会取舍。前置知识:建议先读第 30 章与第 32 章,并熟悉第 13 章。学完后你能在 25 分钟手写题里,交出一份"需求确认清楚、边界有测试、结果有验证"的代码。
手写题考察的从来不是"你能不能背 API",而是"你像不像一个真正写过生产代码的人"——面试官在白板对面看的是你的思考过程、习惯和对错误的反应速度。
⚠️ 口径说明:本文对话与数字均为通用面试经验的示意,不指向任何真实公司或个人;示例数据均为手写小表并注明"示意数据"。
目录
1. 坑 1:不澄清需求就开写
2. 坑 2:不写边界测试
3. 坑 3:变量命名差
4. 坑 4:列名硬编码
5. 坑 5:忽略空值与重复值
6. 坑 6:硬写循环不用内置API
7. 坑 7:不主动说复杂度
8. 坑 8:axis方向写错
9. 坑 9:报错不改乱试
10. 坑 10:不验证结果就交
11. 坑 11:时间不够不会取舍
12. 本篇速查
13. 练习题
坑 1:不澄清需求就开写
场景与错误示范
场景:面试官说"给你一张员工表,字段有部门和薪资,算一下部门的平均薪资",候选人立刻低头敲代码。
错误示范对话:
候选人:(埋头敲)df["salary"].mean()……这样?
>
面试官:(停顿)这个平均,是每个部门一个,还是全公司一个?
>
候选人:呃……我以为是全公司平均。
>
面试官:题目说"部门的平均薪资"。空值怎么处理?结果要排序吗?这些你都不问就写?
错误示范代码:
import pandas as pd
# 示意数据:员工表
df = pd.DataFrame({"dept": ["销售","销售","技术","技术"],
"sal": [10, 20, 30, 40]}) # 单位:千元
# 不澄清需求,直接对整列求均值
print(df["sal"].mean()) # 实跑 25.0 —— 全公司平均,根本不是"分部门"
实跑对照:按部门分组应是 技术 35.0 / 销售 15.0。一个 25.0 交上去,方向就错了。
为什么是坑
- 手写题第一题考的就是需求拆解:面试官故意把题目说短,就是要看你会不会主动追问。直接开写,等于放弃展示工程思维的机会。
- 模糊需求会被代码放大:"平均"背后至少四个分叉——分组维度、空值口径、极端值是否截断、输出形态。不澄清,写的每一行都可能白写。
- 本质考察点:数据岗日常对接的就是说不清需求的业务方。你连面试官的需求都不追问,入职后怎么跟业务方对齐?
正确做法
动手前先用 30 秒问清四件事:输入(表多大、有哪些列)、输出(一个数还是一张表、排不排序)、口径(空值算 0 还是剔除、去不去重)、边界与性能(量级、超时要求)。
参考话术:
我先确认三点:一,"部门平均"是按 dept 分组每个部门一个均值,对吗?二,薪资有空值的话,是剔除还是按 0?三,结果返回两列表即可,还是要排序?确认完我就开始写。
确认后再落笔:
import pandas as pd
df = pd.DataFrame({"dept": ["销售","销售","技术","技术"], "sal": [10,20,30,40]})
# 确认"按部门分组、剔除空值"后
result = df.groupby("dept")["sal"].mean() # 技术 35.0 / 销售 15.0
加分点与延伸
- 加分点:边问边在白板写下"需求三行"(输入/输出/口径),面试官会默认你以后写需求文档也是这个习惯。
- 延伸:面试官若说"先按你的理解写",就大胆说出假设("我假设空值剔除、按部门分组"),把假设摆上台面,比闷头猜强十倍。
坑 2:不写边界测试
场景与错误示范
场景:候选人写完"分组求均值",面试官问"空表、全是空值时你的代码输出什么?"
错误示范对话:
面试官:给你一张空表,或者所有薪资都是空值,你输出什么?
>
候选人:空表……应该不会有问题吧?
>
面试官:你试一下。
>
候选人:(跑)空表是空 Series,全空值是 NaN……那这算成功还是报错?
>
面试官:你自己都不确定,怎么保证线上不崩?
边界实跑记录:
import pandas as pd
# 空表 / 全 NaN / 单行
empty = pd.DataFrame(columns=["dept","sal"])
print(empty.groupby("dept")["sal"].mean().tolist()) # 实跑 []
alld = pd.DataFrame({"dept": ["a","a"], "sal": [None,None]})
print(alld.groupby("dept")["sal"].mean().tolist()) # 实跑 [nan]
single = pd.DataFrame({"dept": ["a"], "sal": [10]})
print(single.groupby("dept")["sal"].mean().tolist()) # 实跑 [10.0]
三种边界都不报错,但"全 NaN 得到一个 NaN"在下游做比较时会悄悄变成假数据——不崩,却错。
为什么是坑
- 数据脏是常态,不是意外:空表、全空值、重复键、单行、极端值每天都在发生。只在理想小表上跑通,等于没写完。
- pandas 的"温柔"会骗人:聚合函数遇空表/空值不报错,返回空对象或 NaN。你不主动测,面试官就知道你没吃过数据脏的亏。
- 考察点:写边界测试 = 有防御性编程意识,这是区分"学生代码"和"工程代码"的分水岭。
正确做法
主逻辑后主动补最小用例,并对空结果/全 NaN 给出明确判断:
import pandas as pd
def check_edge(name, s):
if len(s) == 0:
print(f"{name}: 空结果,需上游告警")
elif s.isna().all():
print(f"{name}: 全 NaN,需确认口径")
else:
print(f"{name}: 正常,均值={s.mean():.2f}")
df = pd.DataFrame({"dept": ["a","b"], "sal": [10,20]})
check_edge("正常", df.groupby("dept")["sal"].mean())
参考话术:我补三个边界:空表、全空值、单行。空表返回空 Series 我会在外层加判断直接告警;全空值得到 NaN,我会和业务确认是不是预期,而不是把 NaN 当 0 传下去。
加分点与延伸
- 加分点:主动提"极端值"(薪资 0、异常大额)——说明你知道均值对极端值敏感,会考虑中位数。
- 延伸:重复键属于另一个边界,详见第 07 章;面试时把"去重了吗"挂在嘴边,是非常稳的习惯。
坑 3:变量命名差
场景与错误示范
场景:白板上 30 行代码全是 df1、df2、tmp、x,还有一个魔法数字 1.5。
错误示范对话:
面试官:df1 和 df2 分别是什么?
>
候选人:df1 是原始数据,df2 是……处理过的?
>
面试官:这个 1.5 是哪来的?
>
候选人:就是一个比例,我当时随手写的。
>
面试官:三个月后你回头看,还认得吗?
错误示范代码:
# 反面教材:df1/df2/tmp 无意义,1.5 是魔法数字
df1 = pd.read_csv("data.csv")
df2 = df1[df1["age"] > 1.5 * 18] # 1.5*18=27 是什么?没人知道
tmp = df2.groupby("g").sum()
(反面教材:仅演示命名问题,不作为可运行代码。)
为什么是坑
- 白板代码是"朗读"出来的:面试官会边看边念变量名。
df1/df2念不出口,你的讲解就频繁卡壳,显得思路混乱。 - 魔法数字 = 需求没落地:
1.5、0.05这种裸数字,代表你没想清"为什么是这个值",追问十有八九答不上。 - 考察点:命名是可读性的下限。数据岗代码常要交付给业务和下游,"别人能不能看懂"比"机器能不能跑"更重要。
正确做法
import pandas as pd
ADULT_AGE = 18 # 常量大写,说明语义
MULTIPLIER = 1.5 # 含义要能一句话说清
orders = pd.DataFrame({ # 示意数据:用户订单(年龄/城市/消费额)
"age": [22, 19, 31, 16], "city": ["北京", "上海", "上海", "广州"],
"amount": [120, 80, 300, 40],
})
adult_users = orders[orders["age"] >= ADULT_AGE] # 过滤成年用户
spend_by_city = adult_users.groupby("city")["amount"].sum() # 按城市汇总消费
print(spend_by_city)
参考话术:我把中间表按内容命名——orders 是原始订单,adult_users 是过滤后的成年用户,spend_by_city 是按城市汇总。那个 1.5 提成了常量 MULTIPLIER,代表"消费额达到中位数 1.5 倍算高价值用户",口径写在名字里。
加分点与延伸
- 加分点:长变量名不怕长,怕模糊。
spend_by_city比df_result信息量大十倍。 - 延伸:循环里别用
i/j/k指代业务对象——for order in orders远胜for i in range(len(df)),这也是第 10 章强调的向量化习惯。
坑 4:列名硬编码
场景与错误示范
场景:候选人凭记忆写 df["user_name"],真实列名却是 "User Name"(带空格、大小写不同)。
错误示范对话:
候选人:df["user_name"]……
>
屏幕:KeyError: 'user_name'
>
候选人:啊?列名不对?(head 一下)哦,是 "User Name"。
>
面试官:你为什么不先看一眼列名?如果 CSV 是业务方给的,列名有空格、有中文、还有 " User_id " 首尾带空格,你的代码要崩几次?错误示范代码(实跑复现报错):
import pandas as pd
# 示意数据:真实列名带空格、大小写混合
df = pd.DataFrame({"user_id": [1,2], "User Name": ["张三","李四"]})
print(df["user_name"]) # 实跑 KeyError: 'user_name'
报错已经直白告诉你"这列不存在",但硬编码写法的人往往要试三四次才反应过来。
为什么是坑
- 列名是"外部接口",不由你控制:CSV 是产品导的、Excel 是运营给的。凭记忆写列名,等于把代码建在"列名永远长这样"的幻觉上。
- 空格、大小写、首尾空格、同名歧义是四大重灾区:
"User Name"vs"user_name",肉眼分不出,代码却一个字符都不能错。 - 考察点:写代码前先
df.columns看一眼,是"对数据有敬畏心"的体现。连列名都不确认,后面的 merge、groupby 全是空中楼阁。
正确做法
先看列名 → 规范化 → 之后统一引用规范名:
import pandas as pd
df = pd.DataFrame({"user_id":[1,2], "User Name":["张三","李四"], " amt ":[100,200]})
print(df.columns.tolist()) # 先看一眼真实列名
# 规范化:去首尾空格、空格转下划线、统一小写
df.columns = df.columns.str.strip().str.replace(" ","_").str.lower()
print(df.columns.tolist()) # ['user_id', 'user_name', 'amt']
print(df[["user_id","amt"]]) # 之后一律用规范名
列可能不存在时,先 if target in df.columns 判断再取,避免 KeyError 直接崩掉整个流程。
加分点与延伸
- 加分点:主动说"我会在读取后立刻做一次列名规范化",一句话就把你和"凭记忆写代码"的候选人区分开。
- 延伸:
read_csv的usecols=能在读取阶段就锁定列名;这类报错的系统排查见第 43 章。
坑 5:忽略空值与重复值
场景与错误示范
场景:候选人算"各部门平均薪资",直接 groupby.mean(),既不看空值也不查重复行。
错误示范对话:
候选人:好了,销售部平均薪资 10K。
>
面试官:销售部四个人,其中一人薪资是空值。你这 10K 怎么来的?
>
候选人:应该……自动跳过空值了吧。
>
面试官:对,mean 默认 skipna=True,它悄悄把空值丢了,只拿剩下一个 10 平均。空值那个人是"没发工资"还是"没采集到"?这两种情况结果完全不一样。
错误示范代码(实跑记录):
import pandas as pd
# 示意数据:销售部有一人薪资为空
df = pd.DataFrame({"dept":["销售","销售","技术","技术"], "sal":[10,None,20,20]})
g = df.groupby("dept")["sal"].mean()
print(g["销售"]) # 实跑 10.0 —— 只对一个非空值求了平均,不是 (10+0)/2
重复值同样危险(实跑):
df2 = pd.DataFrame({"uid":[1,1,2], "pay":[100,100,50]})
print("未去重:", df2["pay"].sum()) # 实跑 250
print("去重后:", df2.drop_duplicates()["pay"].sum()) # 实跑 150
为什么是坑
- skipna=True 是"沉默的口径":pandas 默认跳过 NaN 求均值,却从不告诉你"跳过了几个"。不主动
isna().sum()看一眼,就把样本量减少当成理所当然。 - 重复行直接放大金额:求和、计数对重复行零防御。实跑 250 vs 150,差出整整 100——真实报表里就是"GMV 虚高 67%"级别的事故。
- 考察点:数据质量检查(空值多少、重复多少、主键是否唯一)应该是分析第一步,而不是出了错再回头查。
正确做法
主逻辑之前先跑三行"数据体检":
import pandas as pd
df = pd.DataFrame({"dept":["销售","销售","技术","技术"], "sal":[10,None,20,20]})
print("各列空值数:\n", df.isna().sum()) # 体检1:空值分布
print("重复行数:", df.duplicated().sum()) # 体检2:重复行
# 按口径剔除空值,并在结果里附 count 说明样本量
result = df.dropna(subset=["sal"]).groupby("dept")["sal"].agg(["mean","count"])
print(result)
参考话术:我先做数据体检:空值用 isna().sum() 看分布,重复行用 duplicated().sum() 数一遍。销售部这个空值我选择剔除而不是当 0,并在结果里附上 count,让看报表的人知道均值基于几个人算的。
加分点与延伸
- 加分点:均值旁边永远配一个
count——样本量和均值同等重要,这是分析师的职业本能。 - 延伸:空值不是非删即填,剔除/填 0/插补对应三种业务含义,能说出"我按业务语义选"就赢了;系统方法见第 07 章。
坑 6:硬写循环不用内置API
场景与错误示范
场景:候选人要"按部门算平均薪资",写了双重 for 循环逐行累加。
错误示范对话:
面试官:(看白板)你外层遍历每个部门,内层逐行判断属于哪个部门?
>
候选人:对,这样逻辑清楚。
>
面试官:如果 20 万行、1000 个部门呢?
>
候选人:(算了下)那就是 2 亿次判断……会慢。
>
面试官:groupby 一行就够,而且底层是 C 实现的。你为什么不用?
错误示范代码(实跑,结果其实是对的):
import pandas as pd
df = pd.DataFrame({"dept":["a","a","b","b","a"], "sal":[10,20,30,40,50]})
# 错误做法:双重 for 循环逐行判断
res = {}
for dept in df["dept"].unique():
s, c = 0, 0
for _, r in df.iterrows():
if r["dept"] == dept:
s += r["sal"]; c += 1
res[dept] = s / c
print(res) # 实跑 {'a': 26.67, 'b': 35.0}
正确做法(向量化,结果一致):
v = df.groupby("dept")["sal"].mean().round(2).to_dict()
print(v) # 实跑 {'a': 26.67, 'b': 35.0}
两者结果一致,但写法差十倍代码、上万倍速度(见坑 7 实测)。
为什么是坑
- iterrows 是 pandas 最慢的操作之一:每行都被包装成 Series,触发对象创建和类型推断,比底层 C 向量化慢几个数量级。
- 手写循环暴露"编程课思维":groupby、transform、map 就是为这类问题设计的。不用它们,说明你还在把 pandas 当 Excel 逐格算。
- 考察点:面试官想看你"第一反应是向量化",而不是"第一反应是写循环"。这是 pandas 熟练与否的硬指标。
正确做法
记住"能向量化就不循环"的优先级:整列运算直接加减;分组聚合用 groupby.agg();分组内广播用 transform;真要行级复杂逻辑才用 apply 并说明原因。
import pandas as pd
df = pd.DataFrame({"dept":["a","a","b","b"], "sal":[10,20,30,40]})
print(df.groupby("dept")["sal"].mean()) # 分组聚合
df["dept_avg"] = df.groupby("dept")["sal"].transform("mean") # 广播回原表
print(df)
加分点与延伸
- 加分点:主动说"这个用 transform 就能广播回去,不用我循环合并"——一句话展示你懂 pandas 惯用法。
- 延伸:什么时候真能循环?——逻辑高度依赖外部状态、向量化确实表达不了时。系统优化思路见第 23 章。
坑 7:不主动说复杂度
场景与错误示范
场景:候选人写完一段双重循环,面试官问"时间复杂度多少",候选人愣住。
错误示范对话:
面试官:说说你这段的时间复杂度?
>
候选人:应该是 O(n) 吧?
>
面试官:外层遍历 k 个部门、内层遍历 n 行,这是 O(n×k)。数据大会怎样?
>
候选人:……会慢。
>
面试官:慢到什么程度?我们实测一下?
实测对比(实跑记录,2 万行 / 100 组):
import pandas as pd, numpy as np, time
rng = np.random.default_rng(0)
df = pd.DataFrame({"g": rng.integers(0,100,20000), "v": rng.random(20000)})
# 向量化 groupby
t = time.time(); df.groupby("g")["v"].mean(); t_vec = time.time()-t
# 逐行双重循环
t = time.time()
for g_ in df["g"].unique():
s, c = 0.0, 0
for _, r in df.iterrows():
if r["g"] == g_:
s += r["v"]; c += 1
t_loop = time.time()-t
print(f"向量化 {t_vec*1000:.1f} ms vs 循环 {t_loop:.2f} s")
实跑输出:向量化 2.2 ms vs 逐行循环 83.69 s——2 万行就差了约 4 万倍。这就是"不说复杂度"的代价。
为什么是坑
- 手写题默认你会分析复杂度:这是基本功,不是加分项。写完不主动说 O(n) 还是 O(n²),面试官默认你没概念。
- 复杂度决定方案选型:O(n) 的 groupby 和 O(nk) 的循环在小数据上都能跑,数据翻 100 倍后一个 0.2 秒、一个几小时。不分析就选不出对的方案。
- 考察点:面试官想听的是"我这段 O(n),数据涨到千万级我会改分块处理"这种有规模感的表达。
正确做法
写完代码后,主动用一句话交代"复杂度 + 规模预案":
参考话术:
我这段分组聚合是 O(n):groupby 底层做一次哈希分组,遍历一遍就出结果,空间上额外存一个分组字典。如果数据量涨到内存装不下,我会考虑分块读取(chunksize)或者把计算下推到数仓,而不是在 pandas 里硬扛。
加分点与延伸
- 加分点:主动说"空间复杂度"——groupby 需要 O(分组数) 的额外内存,merge 会临时放大中间表。同时讲时间和空间,立刻拉开档次。
- 延伸:题目量级是"千万级以上"时,优先谈"能不能不把数据全读进内存",见第 42 章。
坑 8:axis方向写错
场景与错误示范
场景:候选人想删一列,写成 df.drop("a", axis=0),结果报错。
错误示范对话:
候选人:df.drop("a", axis=0)。
>
屏幕:KeyError: "['a'] not found in axis"
>
候选人:咦?(换成 axis=1)……好了?
>
面试官:你知道 axis=0、axis=1 分别沿哪个方向吗?那 df.sum(axis=1) 是按行还是按列求和?>
候选人:(卡壳)
错误示范代码(实跑复现):
import pandas as pd
df = pd.DataFrame({"a":[1,2], "b":[3,4]})
print(df.drop("a", axis=0)) # 实跑 KeyError: "['a'] not found in axis"
正确写法是 df.drop(columns=["a"])(实跑后只剩 b 列)。
为什么是坑
- axis 的语义是"沿哪个方向移动",不是"删哪个方向":
axis=0沿行方向往下走,作用在行上(删行、按列聚合);axis=1沿列方向横着走,作用在列上(删列、按行聚合)。这个反直觉定义是 pandas 最经典的坑。 - groupby 里同样易混:
df.groupby("dept").mean()默认对每一列聚合。写反聚合维度不报错,只给错结果——比报错更可怕。 - 考察点:axis 是 pandas 基础概念必问题(呼应第 30 章)。白板上写错 = 基础不牢。
正确做法
记忆法:axis 指"沿哪个轴移动,那个轴的标签会被吃掉"。axis=0 沿行方向 → 删行标签;axis=1 沿列方向 → 删列标签。更稳的是:能用命名参数就不写 axis。
import pandas as pd
df = pd.DataFrame({"a":[1,2], "b":[3,4]})
print(df.drop(columns=["a"])) # 删列,彻底不用记 axis
print(df.sum(axis=1)) # 实跑 0 行=4,1 行=6(沿列方向把每行加起来)
参考话术:axis 我一般不硬记——删列直接用 columns=,求和用命名写法。要解释的话,axis=0 沿行方向往下删行,axis=1 横着走删列。
加分点与延伸
- 加分点:主动说出"groupby 默认 axis=0,也就是对每一列聚合",说明你真懂而不是背。
- 延伸:删除/聚合后记得
inplace或重新赋值——视图副本的坑见第 18 章,和 axis 并称 pandas 两大经典陷阱。
坑 9:报错不改乱试
场景与错误示范
场景:代码报错后,候选人不读错误信息,开始换参数、删代码、到处加括号。
错误示范对话:
屏幕:AttributeError: 'Series' object has no attribute 'meam'
>
候选人:(不读报错,直接换写法)df.mean()?不对……(删两行)这样?
>
面试官:(看不下去)你读一下报错最后一行。
>
候选人:(念)'Series' object has no attribute 'meam'……哦,拼成 meam 了,应该是 mean。
>
面试官:你刚才折腾三分钟,报错第一行就写了答案。
错误示范代码(实跑复现):
import pandas as pd
s = pd.Series([1,2,3,None])
print(s.meam()) # 实跑 AttributeError: 'Series' object has no attribute 'meam'
报错里连"方法名拼错"都暗示得明明白白。
为什么是坑
- 报错信息是 pandas 白送的调试提示:KeyError 告诉你列名不对、AttributeError 告诉你方法拼错、ValueError 告诉你维度不匹配。不读报错就乱试,是把免费线索扔了。
- 乱试的本质是"在猜":猜三次还不对,面试官就给你打上"不会调试"的标签。线上故障排查靠的就是读日志读报错,不是瞎改。
- 考察点:工程师一半时间在调试。面试官看的是你遇错的第一反应——冷静读信息,还是慌了乱试。
正确做法
报错四步法:①先读最后一行(Exception 类型+信息);②再看箭头指向哪一行;③对号入座(KeyError 查列名、AttributeError 查拼写、ValueError 查维度);④改完只跑一次验证,不连续盲改。
把 meam 改回 mean 即可——s.mean() 实跑 2.0(自动跳过 None)。
参考话术:我先读报错——这是 AttributeError,说 Series 没有 meam 方法,应该是我方法名拼错了,正确的是 mean。改完跑一下验证。
加分点与延伸
- 加分点:遇到 SettingWithCopyWarning 这类"警告型报错"时,主动说"这是视图副本问题,我会用 .loc 重新赋值"——说明你不仅会读报错,还懂常见陷阱。
- 延伸:系统化报错排查见第 43 章及其续篇;把"读报错"练成肌肉记忆,比背 100 个 API 都管用。
坑 10:不验证结果就交
场景与错误示范
场景:候选人写完 merge,直接说"好了",既不看合并后长什么样,也不数行数。
错误示范对话:
候选人:orders 和 users 按 uid merge 完了。
>
面试官:合并完多少行?
>
候选人:……2 行吧,orders 本来就 2 行。
>
面试官:你 head 一下看看。
>
候选人:(打出来)啊?怎么变 4 行了?
>
面试官:users 里同一个 uid 有两条,merge 做了笛卡尔放大,每笔订单金额被复制成两份。下游按这个汇总,GMV 就翻倍了。
错误示范代码(实跑复现):
import pandas as pd
orders = pd.DataFrame({"oid":[1,2], "uid":[10,10], "amt":[100,200]})
users = pd.DataFrame({"uid":[10,10], "city":["北京","上海"]}) # 同一 uid 两条
m = orders.merge(users, on="uid")
print("合并后行数:", len(m)) # 实跑 4(orders 才 2 行):每笔订单被复制成两行
为什么是坑
- merge 是最容易"静默出错"的操作:键重复时它不报错,而是做笛卡尔积放大行数。你不数行数、不 head,错误结果就顺顺当当流进下游报表。
- "能跑通"和"结果对"是两回事:手写题里面试官最想看到你主动 print 中间结果、对照预期输出。不验证就交,等于把"对不对"全押在直觉上。
- 考察点:分析师产出要对业务负责。一个未经验证的数字流到周报上就是事故。验证习惯是职业底线。
正确做法
交付前三件事:看一眼长什么样、数一下行数、对一个已知答案。
import pandas as pd
orders = pd.DataFrame({"oid":[1,2], "uid":[10,10], "amt":[100,200]})
users = pd.DataFrame({"uid":[10,10], "city":["北京","上海"]})
# 合并前先确认键唯一性——users 的 uid 重复了
print("users.uid 唯一值数:", users["uid"].nunique(), "总行数:", len(users))
m = orders.merge(users, on="uid")
assert len(m) >= len(orders), "合并后行数不该少于左表"
print("合并后 amt 总和:", m["amt"].sum()) # 实跑 600 → 已知应为 300,立刻发现异常
参考话术:我合并后做两个验证:一数行数——左表 2 行、结果 4 行,说明键重复放大了,我会先把 users 按 uid 聚合再合;二对已知总量——amt 应该是 300,现在变成 600,这就是验证帮我抓住的错误。
加分点与延伸
- 加分点:用
assert把验证写进代码("行数不该少于左表"),比口头说"我检查过了"更有说服力。 - 延伸:merge 前检查键唯一性是铁律;更隐蔽的视图副本问题见第 18 章。
坑 11:时间不够不会取舍
场景与错误示范
场景:手写题限时 25 分钟,候选人卡在一个细节上死磕,最后功能没写完,复杂度也没讲。
错误示范对话:
面试官:(还剩 5 分钟)怎么样了?
>
候选人:(还在调一个边界 case)快了,这个特殊情况马上好……
>
面试官:你的主逻辑能跑吗?
>
候选人:主逻辑……还没串起来。
>
面试官:那你这 20 分钟写了一半优化、没有能跑的主流程,也没跟我讲思路。时间管理是不是有点问题?
为什么是坑
- 手写题是"限时交付",不是"做完美":打分看的是"你完成了多少、讲清楚了多少",不是"最后一个边界 case 调得多漂亮"。
- 死磕细节的机会成本极高:一个 5 分钟没搞定的边界,会占掉你讲思路、做验证、分析复杂度的时间。后者任何一项都比那个边界更影响评分。
- 考察点:这本质是"工程优先级"题。真实工作里需求永远做不完,知道先保什么、后补什么,比完美主义重要得多。
正确做法
三段式时间分配(以 25 分钟为例):前 3 分钟确认需求 + 讲思路;中间 15 分钟写能跑的主流程;最后 7 分钟补边界 + 讲复杂度 + 验证。取舍顺序:先保功能正确 → 再讲思路和复杂度 → 最后才是优化和完美边界。
import pandas as pd
def avg_by_dept(df):
# 先写"能跑通主流程"的最小版本;空表单独兜住,其余边界稍后补
if df.empty:
return pd.Series(dtype=float)
return df.groupby("dept")["sal"].mean()
参考话术(时间快到了主动说):我先把主流程跑通给您看,边界我用一句话说明处理思路——空表返回空、空值剔除后附 count。如果时间允许再展开性能优化。我先讲整体思路可以吗?
加分点与延伸
- 加分点:主动说"我先用 30 秒讲下思路,再开始写"——思路讲在前面,即使代码没写完,面试官也能给方向分。
- 延伸:讲思路优先于硬写,是手写题通用策略——代码没写完但思路清晰,往往比写完了但讲不出来得分高;完整面试策略见第 40 章。
本篇速查
| 坑号 | 坑名 | 一句话要点 | 正确做法关键词 |
|---|---|---|---|
| 坑 1 | 不澄清需求就开写 | 直接写 = 方向可能全错 | 先问输入/输出/口径/性能,再落笔 |
| 坑 2 | 不写边界测试 | 空表/空值/重复/单行都不报错但会错 | 补最小边界用例,主动加判断 |
| 坑 3 | 变量命名差 | df1/df2/魔法数字 = 三个月后没人懂 | 见名知意、常量大写、表名说内容 |
| 坑 4 | 列名硬编码 | 凭记忆写列名必被 KeyError | 先看 columns、规范化、存在性判断 |
| 坑 5 | 忽略空值与重复值 | skipna 沉默跳过、重复行放大金额 | 先体检 isna/duplicated,结果附 count |
| 坑 6 | 硬写循环不用内置API | iterrows 比 groupby 慢万倍 | 整列运算/groupby/transform 向量化优先 |
| 坑 7 | 不主动说复杂度 | 写完不讲 O(n) 等于没概念 | 主动说时间/空间复杂度 + 规模预案 |
| 坑 8 | axis方向写错 | axis=0 作用在行,写反不报错只错结果 | 用 columns=/index= 命名参数代替 axis |
| 坑 9 | 报错不改乱试 | 报错最后一行就是答案 | 读最后一行→定位行→对号改→跑一次 |
| 坑 10 | 不验证结果就交 | merge 键重复静默放大行数 | head + 数行数 + 对照已知总量 |
| 坑 11 | 时间不够不会取舍 | 死磕边界 = 丢了主流程和思路分 | 先功能再思路后优化,讲思路优先 |
练习题
题目 1:面试官说"给你一张销售表,算一下每个区域的销售额"。请写出动手前会追问的三个问题,并给出最小正确代码骨架。
参考答案要点:问①是"每个区域一个总额"还是"全公司总额";②空订单/重复订单要不要先剔除;③输出要不要按金额排序。骨架:先 isna().sum() / duplicated().sum() 体检,再 groupby("region")["amount"].sum(),最后 len() 和 head 验证。
题目 2:你写完 df.drop("col_a", axis=0) 报了 KeyError。请按"报错四步法"说出处理过程,并写出正确写法。
参考答案要点:读最后一行确认 KeyError → 定位到 drop 行 → 判断是想删列却误用了 axis=0(axis=0 作用在行)→ 改用 df.drop(columns=["col_a"]),跑一次验证。
题目 3:merge 之后结果表行数从左表的 1 万行变成 3 万行,金额总和也明显偏大。请分析原因并给出验证与修复思路。
参考答案要点:右表连接键有重复值,触发笛卡尔积放大;修复:合并前 nunique() 检查键唯一性,对右表先聚合或去重;合并后用 len() 和已知总量对照验证,发现异常立刻停下排查。
题目 4:手写题还剩 5 分钟,主功能刚跑通,但有一个边界 case 没处理好。你会怎么取舍?
参考答案要点:停止死磕该边界,转而去做三件高回报的事:把主流程讲清楚(思路分)、补一句复杂度分析(O(n) groupby + 规模预案)、说明边界处理思路(空表返回空、空值剔除附 count)。讲思路优先于硬写。
下一篇预告:第 64 章 面试踩坑:SQL 面试篇 —— 手写代码之后,SQL 是数据岗面试的第二大必考题:窗口函数、分组去重、连续登录,把最容易翻车的 SQL 坑一次扫清。
文章回复
0 条公开回复