写在前面
前两篇讲了报错,这篇换个场景:面试。
Pandas 面试有个特点------不考记忆,考理解 。没人会问你 read_csv 有哪些参数(能查),但一定会问「loc 和 iloc 有什么区别」「为什么会有 SettingWithCopyWarning」------因为这些问题能看出你是否真的想过数据是怎么存的。
这篇挑出 10 道高频题,每题三段:面试官想听什么 → 标准答案 → 能跑的代码。代码不是装饰,面试时如果能说「我现场验证一下」,说服力完全不一样。
答案建议背「结论 + 一个例子」,别背长篇大论。
先确认示例数据在手边:
python
import os
import mkdata
if not os.path.exists("data/sales.csv"):
mkdata.main()
print("示例数据就绪:", os.path.exists("data/sales.csv"))
这段会检查第 1 篇生成的 data/ 目录在不在,不在就自动补一次;已经有数据时只会打印一行 示例数据就绪: True,不影响后面的结果。
第 1 题:Series 和 DataFrame 的关系
想听什么:你是不是把它当成「Excel 的一列和一张表」在理解。
答:Series 是带索引的一维数组,DataFrame 是共享同一套行索引的多个 Series。取一列得到 Series,取多列得到 DataFrame。
python
import pandas as pd
df = pd.DataFrame({"城市": ["杭州", "上海"], "金额": [149.7, 149.5]})
print("取一列:", type(df["城市"]).__name__)
print("取多列:", type(df[["城市"]]).__name__)
print("一列本身有索引:", df["城市"].index.tolist())
text
取一列: Series
取多列: DataFrame
一列本身有索引: [0, 1]
第 2 题:loc 和 iloc 的区别
想听什么:区不区分「标签」和「位置」。
答 :loc 按标签(索引值、列名),iloc 按位置(第几行第几列)。切片时 loc 含右端点、iloc 不含,和 Python 列表一致。
python
import pandas as pd
df = pd.DataFrame({"金额": [10, 20, 30, 40]}, index=[100, 200, 300, 400])
print("loc[200](标签):", df.loc[200, "金额"])
print("iloc[1](位置):", df.iloc[1, 0])
print("loc[100:300] 行数:", len(df.loc[100:300]), "(含右端点)")
print("iloc[0:2] 行数:", len(df.iloc[0:2]), "(不含)")
text
loc[200](标签): 20
iloc[1](位置): 20
loc[100:300] 行数: 3 (含右端点)
iloc[0:2] 行数: 2 (不含)
加分点:索引不是 0...n-1 时,loc[0] 会报 KeyError,因为它找的是标签 0。
第 3 题:视图和副本怎么区分
想听什么 :知不知道 SettingWithCopyWarning 背后的机制。
答 :df[条件] 返回的可能是视图也可能是副本,pandas 无法保证,所以赋值时给警告。规范写法是用 loc 一次完成,或显式 .copy()。
python
import pandas as pd
df = pd.DataFrame({"城市": ["杭州", "上海"], "金额": [1.0, None]})
sub = df[df["城市"] == "杭州"] # 可能是视图
sub["金额"] = sub["金额"].fillna(0) # 触发警告的经典写法
print("sub 已改:", sub["金额"].tolist())
print("原表未变:", df["金额"].tolist())
print("----")
df.loc[df["城市"] == "杭州", "金额"] = 0 # 推荐写法
print("loc 改完原表:", df["金额"].tolist())
text
sub 已改: [1.0]
原表未变: [1.0, nan]
----
loc 改完原表: [0.0, nan]
第 4 题:缺失值怎么处理,NaN 和 None 有什么区别
想听什么 :有没有「按业务选策略」的意识,而不是只会 dropna。
答 :None 是 Python 对象,NaN 是浮点缺失值,pandas 里数值列的缺失统一是 NaN。处理分三步:先量化(isna().sum()、缺失率),再判断(关键字段 vs 可补字段),最后才是删或填。
python
import numpy as np
import pandas as pd
df = pd.DataFrame({"金额": [100.0, np.nan, 300.0], "城市": ["杭州", None, "北京"]})
print("缺失统计:")
print(df.isna().sum().to_string())
print("缺失率:", (df.isna().mean() * 100).round(1).tolist())
print("----")
print("删:", len(df.dropna(subset=["金额"])), "行")
print("填中位数:", df["金额"].fillna(df["金额"].median()).tolist())
print("分类填未知:", df["城市"].fillna("未知").tolist())
text
缺失统计:
金额 1
城市 1
缺失率: [33.3, 33.3]
----
删: 2 行
填中位数: [100.0, 200.0, 300.0]
分类填未知: ['杭州', '未知', '北京']
第 5 题:groupby 的原理
想听什么:知不知道「拆分---应用---合并」这三步。
答 :groupby 先按键把行拆成若干组,对每组应用函数,再把结果按组键合并。理解这三步,就能解释「为什么 agg 返回的索引是组键」「为什么 transform 返回等长结果」。
python
import pandas as pd
df = pd.DataFrame({
"城市": ["杭州", "杭州", "上海", "上海"],
"金额": [100, 200, 300, 400],
})
print("agg(每组一行):")
print(df.groupby("城市")["金额"].sum().to_string())
print("----")
print("transform(贴回原表,等长):")
print(df.groupby("城市")["金额"].transform("sum").tolist())
print("----")
print("组内占比:", (df["金额"] / df.groupby("城市")["金额"].transform("sum")).round(3).tolist())
text
agg(每组一行):
城市
上海 700
杭州 300
----
transform(贴回原表,等长):
[300, 300, 700, 700]
----
组内占比: [0.333, 0.667, 0.429, 0.571]
第 6 题:merge 的四种连接
想听什么:知不知道「行数为什么会变」。
答 :inner 只留两边都有的键;left 保留左表全部;right 保留右表全部;outer 两边都保留。行数变化来自「键重复」和「键缺失」,可以用 validate= 防翻倍、indicator=True 查匹配。
python
import pandas as pd
left = pd.DataFrame({"id": [1, 2, 3], "金额": [10, 20, 30]})
right = pd.DataFrame({"id": [2, 3, 4], "城市": ["上海", "北京", "广州"]})
for how in ["inner", "left", "right", "outer"]:
m = left.merge(right, on="id", how=how)
print(f"{how:<6s} 行数 {len(m)},缺失城市 {m['城市'].isna().sum()}")
text
inner 行数 2,缺失城市 0
left 行数 3,缺失城市 1
right 行数 3,缺失城市 0
outer 行数 4,缺失城市 1
第 7 题:apply、map、applymap 的区别
想听什么:知不知道该用哪个、以及性能差异。
答 :map 作用于 Series 的每个元素(常配字典查表);apply 作用于 Series 或 DataFrame 的行/列,能拿到整行;applymap 已在 pandas 2.1 废弃,DataFrame 逐元素用 df.map()。性能上向量化 > map > apply > iterrows。
python
import pandas as pd
df = pd.DataFrame({"城市": ["杭州", "上海"], "金额": [100, 200]})
coef = {"杭州": 1.0, "上海": 1.05}
print("map 查表:", df["城市"].map(coef).tolist())
print("apply 按行:", df.apply(lambda r: r["金额"] * 2, axis=1).tolist())
print("DataFrame.map(替代 applymap):", df[["金额"]].map(lambda x: x + 1)["金额"].tolist())
text
map 查表: [1.0, 1.05]
apply 按行: [200, 400]
DataFrame.map(替代 applymap): [101, 201]
第 8 题:怎么处理重复值
想听什么:区不区分「完全重复」和「业务主键重复」。
答 :duplicated() 默认比所有列,subset=[...] 可指定业务主键,keep 控制保留哪一条。真实场景里业务主键重复更常见------比如同一订单被录了两次但金额不同。
python
import pandas as pd
df = pd.DataFrame({
"订单号": ["A1", "A1", "A2"],
"金额": [100, 100, 200],
})
print("完全重复行:", int(df.duplicated().sum()))
print("按订单号重复:", int(df.duplicated(subset=["订单号"]).sum()))
print("保留最后一条:", df.drop_duplicates(subset=["订单号"], keep="last").to_dict("records"))
text
完全重复行: 1
按订单号重复: 1
保留最后一条: [{'订单号': 'A1', '金额': 100}, {'订单号': 'A2', '金额': 200}]
第 9 题:怎么把宽表转成长表,反过来呢
想听什么 :知不知道 melt / pivot_table(或 stack/unstack)这一对。
答 :长转宽用 pivot_table 或 set_index().unstack();宽转长用 melt。做透视报表用前者,画图或喂模型用后者。
python
import pandas as pd
wide = pd.DataFrame({"城市": ["杭州", "上海"], "笔记本": [100, 200], "鼠标": [50, 60]})
print("宽表:")
print(wide.to_string(index=False))
long = wide.melt(id_vars="城市", var_name="产品", value_name="金额")
print("melt 之后:")
print(long.to_string(index=False))
print("----")
print("再 pivot 回去:")
print(long.pivot_table(index="城市", columns="产品", values="金额", aggfunc="sum").to_string())
text
宽表:
城市 笔记本 鼠标
杭州 100 50
上海 200 60
melt 之后:
城市 产品 金额
杭州 笔记本 100
上海 笔记本 200
杭州 鼠标 50
上海 鼠标 60
----
再 pivot 回去:
产品 笔记本 鼠标
城市
上海 200 60
杭州 100 50
第 10 题:数据量太大内存不够怎么办
想听什么 :有没有实战经验,而不是只会 read_csv。
答 :四个层次------读的时候只读需要的列(usecols)并指定类型(dtype);内存里降级(int8/float32)和文本转 category;实在装不下就分块(chunksize)或让数据库先聚合;长期方案换 Parquet 列式存储。
python
import numpy as np
import pandas as pd
df = pd.DataFrame({
"城市": np.random.choice(["杭州", "上海"], 1000),
"金额": np.random.rand(1000).round(4),
})
before = df.memory_usage(deep=True).sum() / 1024
df["城市"] = df["城市"].astype("category")
df["金额"] = df["金额"].astype("float32")
after = df.memory_usage(deep=True).sum() / 1024
print(f"内存:{before:.1f} KB -> {after:.1f} KB")
print("类型:", df.dtypes.to_dict())
text
内存:21.6 KB -> 5.0 KB
类型: {'城市': CategoricalDtype(categories=['上海', '杭州'], ordered=False, categories_dtype=str), '金额': dtype('float32')}
面试答题的三个技巧
- 先给结论,再给例子:面试官不要听定义复述,要听「所以什么情况下会踩坑」;
- 主动提边界 :比如答
loc/iloc时补一句「索引非默认值时loc[0]会报错」,这是加分项; - 不确定就现场验证 :
print(df.head())跑一下,比硬背可信。
常见问题
Q1:被问到没准备过的 API 怎么办?
说清「我知道它大概是干什么的,具体参数我会查文档」,然后讲你会怎么查、怎么验证。承认不知道 + 展示检索能力,比胡编好得多。
Q2:面试官问「你平时怎么处理脏数据」,怎么答得有层次?
按「探查 → 量化 → 判断 → 处理 → 校验」五步答,每步给一个方法(info / isna().sum() / 缺失率阈值 / fillna 或 dropna / duplicated().sum())。有流程感比堆方法名强。
Q3:要不要背代码?
不用背完整代码,背方法名 + 关键参数 :merge(validate=)、to_numeric(errors="coerce")、transform("sum")、drop_duplicates(subset=)。这些是「会用」和「听说过」的分界线。
小结
这 10 道题的共同点是:都在问「为什么」,而不是「怎么用」。Series 与 DataFrame 的关系、loc/iloc、视图副本、缺失值策略、groupby 三步、merge 四种连接、apply 家族、重复值口径、宽长表互转、大数据处理------把这十个讲清楚,Pandas 的基本功就到位了。
下一篇是下半场:10 道手写题,包括每组 TopN、连续日期、宽长表转换、去重找重复,全部给可运行答案。