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

第 30 章 pandas 面试题精讲:基础篇

内容摘要

本篇把前 29 章里最容易被面试官追问的基础考点整理成一套"面试题精讲":Series/DataFrame 的创建与结构、loc/iloc/布尔索引/query 的选择、缺失值处理、数据类型与转换、map/apply/applymap 的区别,以及视图与副本、链式赋值、axis 混淆三大经典坑。每道题按"题目 → 考点分析 → 参考答案代码(带丰富中文注释)→ 追问与延伸"组织,文末配速查表与易错点总结。前置知识:读完第 1~18 章即可。学完后你能在面试中快速、准确地写出 pandas 基础题答案,并知道每个 API 的适用边界。

📖 本篇简介:本篇把前 29 章里最容易被面试官追问的基础考点整理成一套"面试题精讲":Series/DataFrame 的创建与结构、loc/iloc/布尔索引/query 的选择、缺失值处理、数据类型与转换、map/apply/applymap 的区别,以及视图与副本、链式赋值、axis 混淆三大经典坑。每道题按"题目 → 考点分析 → 参考答案代码(带丰富中文注释)→ 追问与延伸"组织,文末配速查表与易错点总结。前置知识:读完第 1~18 章即可。学完后你能在面试中快速、准确地写出 pandas 基础题答案,并知道每个 API 的适用边界。
前面 29 章我们系统学完了 pandas 的方方面面。从本篇开始,我们用三篇文章做一次"面试视角"的复盘:本篇是第 18 章之前的基础篇,重点是"写得对、写得稳、知道坑"。

目录

1. Series 与 DataFrame 的创建与结构

2. loc / iloc / 布尔索引 / query:选择四件套

3. 缺失值:识别、删除、填充的套路

4. 数据类型:dtype 体系与类型转换

5. map / apply / applymap:三兄弟的分工

6. 高频坑一:视图与副本、SettingWithCopyWarning

7. 高频坑二:链式赋值(chained indexing)

8. 高频坑三:axis=0 还是 axis=1

9. 基础考点速查表

10. 易错点 Top 10 与自测练习


1. Series 与 DataFrame 的创建与结构

题目 1.1:请说出 Series 和 DataFrame 的关系,并各用 3 种方式创建出来。

考点分析:考察对 pandas 核心数据结构的理解——Series 是"带索引的一维数组",DataFrame 是"由多个 Series 按列拼成的二维表"。面试官想听你说出"DataFrame 的每一列其实都是一个 Series",以及 index(行索引)和 columns(列索引)的概念。

import pandas as pd
import numpy as np

# ---------- Series 的常见创建方式 ----------
# 方式一:从列表创建(index 自动为 0,1,2...)
s1 = pd.Series([10, 20, 30], name="销售额")

# 方式二:从字典创建(key 自动成为 index)
s2 = pd.Series({"北京": 100, "上海": 90, "广州": 80}, name="门店数")

# 方式三:从 numpy 数组创建,并显式指定 index
s3 = pd.Series(np.random.randint(1, 100, 3), index=["一季度", "二季度", "三季度"])

# ---------- DataFrame 的常见创建方式 ----------
# 方式一:从"字典 + 等长列表"创建(最常用,key 变列名)
df1 = pd.DataFrame({
    "城市": ["北京", "上海", "广州"],
    "销量": [100, 90, 80],
    "利润": [30, 25, 20],
})

# 方式二:从二维 numpy 数组创建,同时指定 columns 和 index
df2 = pd.DataFrame(
    data=np.random.randint(1, 100, (3, 2)),
    columns=["销量", "利润"],
    index=["周一", "周二", "周三"],
)

# 方式三:从"字典组成的列表"创建(每一个字典是一行,列名自动对齐并补 NaN)
df3 = pd.DataFrame([
    {"城市": "北京", "销量": 100},
    {"城市": "上海", "销量": 90, "利润": 25},   # 比上一行多一个 key,缺的列自动补 NaN
])

print(df1)
#    城市   销量  利润
# 0  北京  100  30
# 1  上海   90  25
# 2  广州   80  20

追问与延伸:

  • df1["销量"] 取出来的类型是什么?——是 Series,名字叫"销量",索引和 DataFrame 行索引一致。
  • df1.values、df1.index、df1.columns 分别是什么?——二维 numpy 数组、行索引对象、列索引对象。
  • 想把某一列变成行索引用什么?——set_index("城市");想还原回来用 reset_index()。

题目 1.2:df.head() / df.tail() / df.shape / df.info() / df.describe() 分别看什么?

考点分析:这是入门必考题,考察拿到一张表后的"第一反应"。

df = pd.DataFrame({
    "城市": ["北京", "上海", "广州", "深圳", "杭州"],
    "销量": [100, 90, 80, None, 70],          # 故意留一个缺失值
    "成本": [60, 55, 50, 45, 40],
})

print(df.head(2))        # 看前 2 行,快速认识长什么样
print(df.tail(2))        # 看后 2 行
print(df.shape)          # (5, 3):(行数, 列数),元组
print(df.dtypes)         # 每列的数据类型(缺失值会把整数列"提升"成 float64)
df.info()                # 更详细:行数、列名、非空数量、dtype、内存占用
print(df.describe())     # 数值列的 count/mean/std/min/四分位数/max,快速看分布
print(df.describe(include="all"))   # include='all' 把文本列也统计进来

追问与延伸:

  • df.dtypes 显示"销量"是 float64 而不是 int64,为什么?——因为里面有 None/NaN,pandas 的整数没有空值表示,只能整体提升为浮点。
  • 想看某一列有哪些唯一值?——df["城市"].unique();想看唯一值个数?——df["城市"].nunique()。

2. loc / iloc / 布尔索引 / query:选择四件套

题目 2.1:loc 和 iloc 的区别是什么?

考点分析:这是 pandas 面试出现频率最高的题之一。一句话:loc 用"标签",iloc 用"位置";loc 切片是闭区间(含右端),iloc 切片是左闭右开(和 Python 一致)。

df = pd.DataFrame(
    data={"销量": [100, 90, 80, 70, 60], "利润": [30, 25, 20, 18, 15]},
    index=["周一", "周二", "周三", "周四", "周五"],   # 行索引是"星期",不是 0~4
)

# loc:按"标签"取(行名/列名),切片含右端
print(df.loc["周一":"周三", "销量"])     # 周一、周二、周三 三行都出来(含周三!)
# 周一    100
# 周二     90
# 周三     80

# iloc:按"位置"取(0 开始),切片左闭右开
print(df.iloc[0:3, 0])                  # 第 0、1、2 行,不含第 3 行
# 周一    100
# 周二     90
# 周三     80

# 取单个元素
print(df.loc["周二", "利润"])            # 25(标签定位)
print(df.iloc[1, 1])                    # 25(位置定位)

# 按列表取
print(df.loc[["周一", "周五"], ["销量"]])

追问与延伸:

  • df[行切片] 到底走哪个?——df[0:3] 是位置切片(行),df["列名"] 是列选择;容易混淆,面试建议直接说"生产代码我一律写 loc/iloc,不再用裸 []"。
  • 想同时按条件和标签?——df.loc[df["销量"] > 80, ["城市", "销量"]]:布尔掩码放行,列名放列。

题目 2.2:布尔索引怎么写?多个条件怎么组合?

考点分析:考察"条件筛选"的基本功,重点是每个条件要加括号、用 & | ~ 而不是 and/or/not。

df = pd.DataFrame({
    "城市": ["北京", "上海", "广州", "深圳", "杭州"],
    "销量": [100, 90, 80, 70, 60],
    "是否达标": [True, True, False, False, False],
})

# 单条件:销量大于 80
mask = df["销量"] > 80                  # 得到一个布尔 Series(长度与 df 相同)
print(df[mask])

# 多条件:销量 > 70 且 城市不是"北京"
# 注意:每个条件必须用圆括号包起来;& 表示"且",| 表示"或",~ 表示"取反"
cond = (df["销量"] > 70) & (~df["城市"].isin(["北京"]))
print(df[cond])

# isin:在一组值里
print(df[df["城市"].isin(["北京", "上海", "杭州"])])

# between:闭区间
print(df[df["销量"].between(70, 90)])   # 含 70 和 90

# 配合 loc 只保留需要的列
print(df.loc[df["销量"] > 80, ["城市", "销量"]])

追问与延伸:

  • 为什么不能写 df["销量"] > 80 and df["城市"] == "北京"?——and 是 Python 的标量布尔,pandas 会试图把整个 Series 转成单个布尔值,直接报 ValueError: The truth value of a Series is ambiguous。
  • 文本条件怎么写?——df["城市"].str.startswith("北")、df["城市"].str.contains("京")。

题目 2.3:query 是什么?什么时候用它?

考点分析:query 用类 SQL 字符串做筛选,条件多时更清爽、可读性更好,大表上还略快。

df = pd.DataFrame({
    "城市": ["北京", "上海", "广州", "深圳", "杭州"],
    "销量": [100, 90, 80, 70, 60],
    "利润": [30, 25, 20, 18, 15],
})

# 传统写法(条件一多就很长)
print(df[(df["销量"] > 70) & (df["利润"] / df["销量"] > 0.2)])

# query 写法:字符串里直接用列名,写起来像 SQL
print(df.query("销量 > 70 and 利润 / 销量 > 0.2"))

# 引用外部变量用 @
threshold = 70
print(df.query("销量 > @threshold"))

# not in
print(df.query("城市 not in ['北京', '上海']"))

追问与延伸:

  • query 的局限?——列名带空格或特殊字符时要写反引号 列 名 ;不适合超复杂的 lambda 逻辑。
  • 性能?——在十万行以上、条件列多时 query 通常更快(底层用 numexpr),但日常小数据差别可忽略。

3. 缺失值:识别、删除、填充的套路

题目 3.1:isna / notna / dropna / fillna 怎么用?

考点分析:缺失值是面试必考。要能区分"识别"(isna)、"删除"(dropna)、"填充"(fillna)三类操作,并知道 axis、how、thresh、subset 参数。

df = pd.DataFrame({
    "城市": ["北京", "上海", None, "深圳", "杭州"],
    "销量": [100, None, 80, 70, None],
    "利润": [30, 25, None, 18, 15],
})

# ---------- 识别 ----------
print(df.isna())                 # 每个单元格是不是缺失(布尔 DataFrame)
print(df.isna().sum())           # 每列缺几个(最常用:一眼看缺失分布)
print(df.isna().mean().round(2)) # 每列缺失比例

# ---------- 删除 ----------
print(df.dropna())                          # 删掉"任何一列有缺失"的行
print(df.dropna(how="all"))                 # 只删"整行全空"的行
print(df.dropna(axis=1))                    # 删掉"有缺失"的列
print(df.dropna(subset=["城市"]))           # 只看"城市"列是否缺失,其他列不管
print(df.dropna(thresh=2))                  # 一行至少要有 2 个非缺失值才保留

# ---------- 填充 ----------
print(df.fillna(0))                         # 全部填 0
print(df["销量"].fillna(df["销量"].mean())) # 用该列均值填(数值列常见做法)
print(df["城市"].fillna("未知"))            # 文本列用众数或"未知"填
print(df.ffill())                           # forward fill:用上面一行的值填下来
print(df.bfill())                           # backward fill:用下面一行的值填上去

# 按列分别填(最实用)
df_filled = df.copy()
df_filled["销量"] = df_filled["销量"].fillna(df_filled["销量"].median())
df_filled["城市"] = df_filled["城市"].fillna("未知")
print(df_filled)

追问与延伸:

  • None 和 np.nan 在 pandas 里一样吗?——数值列里基本等价(都被识别为 NaN);但 object 列里 None 是真 None,np.nan 是 float,判断缺失统一用 pd.isna(),不要用 x == np.nan(永远 False)。
  • 缺失值能不能直接删?——面试话术:先 isna().sum() 看缺失比例,>40% 的列考虑直接删;少量缺失用均值/中位数/众数填充;时序数据用 ffill/bfill 或插值。

题目 3.2:df.drop_duplicates() 和 df.duplicated() 怎么用?

考点分析:常和缺失值一起考。

df = pd.DataFrame({
    "订单ID": [1, 2, 2, 3, 3, 3],
    "城市": ["北京", "上海", "上海", "北京", "北京", "广州"],
    "金额": [100, 200, 200, 50, 50, 30],
})

print(df.duplicated())                 # 每行是否是"重复行"(第一次出现不算重复)
print(df.duplicated(subset=["订单ID"]))# 只看"订单ID"列是否重复
print(df.drop_duplicates())            # 删掉完全重复的行,保留第一次出现
print(df.drop_duplicates(subset=["订单ID"], keep="last"))  # 按订单ID去重,保留最后一条
print(df.drop_duplicates(subset=["订单ID"], keep=False))  # keep=False:把所有重复的都删掉

追问与延伸:

  • keep 参数三个选项?——first(默认,留第一条)、last(留最后一条)、False(重复的全删)。
  • 去重前要不要排序?——要!keep="last" 保留的是"排序后的最后一条",所以通常先按时间排序再去重。

4. 数据类型:dtype 体系与类型转换

题目 4.1:pandas 常见 dtype 有哪些?怎么转换?

考点分析:考察对 dtype 的理解——为什么读进来的数字是 object?为什么 0.1+0.2 不对?类别数据为什么用 category?

df = pd.DataFrame({
    "订单号": ["A001", "A002", "A003"],           # 文本
    "金额": ["100", "200", "300"],                # 读进来是字符串!
    "日期": ["2024-01-01", "2024-01-02", "2024-01-03"],
    "是否有效": ["是", "否", "是"],
})

print(df.dtypes)   # 全是 object,因为混了字符串

# ---------- 类型转换 ----------
# to_numeric:字符串数字 -> 数值,errors='coerce' 把转不动的变成 NaN
df["金额"] = pd.to_numeric(df["金额"], errors="coerce")

# to_datetime:字符串 -> 时间戳
df["日期"] = pd.to_datetime(df["日期"], format="%Y-%m-%d")

# astype:通用类型转换
df["是否有效"] = df["是否有效"].map({"是": 1, "否": 0}).astype(int)

# category:低基数文本列省内存、加速 groupby
df["订单号"] = df["订单号"].astype("category")

print(df.dtypes)
# 订单号    category
# 金额       int64
# 日期     datetime64[ns]
# 是否有效    int64

追问与延伸:

  • int64 为什么不能存 NaN?——传统整型没有"空值"概念,有缺失就被迫提升成 float64;pandas 1.0+ 提供了可空整型 Int64(注意大写 I),写 df["金额"].astype("Int64") 即可。
  • object 列有什么坏处?——失去向量化加速、占内存大、容易混进奇怪字符。看到 dtype: object 的数值列第一反应是"赶紧 to_numeric"。

5. map / apply / applymap:三兄弟的分工

题目 5.1:map、apply、applymap 有什么区别?

考点分析:高频面试题。一句话总结:

  • map:Series 专用,逐元素映射(常配合字典)。
  • apply:Series 逐元素 或 DataFrame 按行/按列 调用函数。
  • applymap:DataFrame 逐单元格 调用函数(pandas 2.1+ 推荐用 df.map)。
df = pd.DataFrame({
    "城市": ["北京", "上海", "广州"],
    "销量": [100, 90, 80],
    "利润": [30, 25, 20],
})

# ---------- map:Series 逐元素映射 ----------
# 最典型用法:用字典做"翻译"
city_level = {"北京": "一线", "上海": "一线", "广州": "新一线"}
df["城市等级"] = df["城市"].map(city_level)     # map 接受字典 / Series / 函数
print(df)

# ---------- apply:Series 逐元素,或 DataFrame 沿轴 ----------
# 对单列(Series)逐元素:等价于 map
df["销量_万元"] = df["销量"].apply(lambda x: x / 10000)

# 对 DataFrame 按行:axis=1,函数收到的是一行(Series)
df["利润率"] = df.apply(lambda row: row["利润"] / row["销量"], axis=1)

# 对 DataFrame 按列:axis=0(默认),函数收到的是一列(Series)
print(df[["销量", "利润"]].apply("mean"))      # 每列均值

# ---------- applymap:DataFrame 逐单元格(pandas>=2.1 用 df.map)----------
# 把所有数值格式化成两位小数字符串
print(df[["销量", "利润"]].applymap(lambda x: f"{x:.2f}"))

追问与延伸:

  • apply 和向量化哪个快?——向量化永远快。df["利润"] / df["销量"] 比 df.apply(lambda r: r["利润"]/r["销量"], axis=1) 快 10~100 倍。面试话术:"能向量化就别 apply,apply 是兜底。"
  • map 和 replace 的区别?——map 没匹配到的值会变成 NaN;replace 不匹配的保持原样。想"只翻译一部分"用 replace 更安全。

6. 高频坑一:视图与副本、SettingWithCopyWarning

题目 6.1:什么是 SettingWithCopyWarning?怎么避免?

考点分析:这是 pandas 最著名的警告,也是第 18 章的主题。面试官想听你说出:"对切片结果赋值时,pandas 不确定你改的是原表还是副本,所以警告。"

df = pd.DataFrame({
    "城市": ["北京", "上海", "广州"],
    "销量": [100, 90, 80],
})

# ---- 错误写法:链式赋值,触发 SettingWithCopyWarning ----
df[df["销量"] > 80]["销量"] = 999
# 拆解:df[df["销量"] > 80] 是"切片副本/视图",再 ["销量"]=999 不知道改在哪

# ---- 正确写法:用 loc 一步到位 ----
df.loc[df["销量"] > 80, "销量"] = 999
print(df)
#    城市   销量
# 0  北京  999
# 1  上海  999
# 2  广州   80

# ---- 如果你确实想基于筛选结果做一张新表,先显式 copy ----
sub = df[df["销量"] > 80].copy()     # 显式 copy,告诉 pandas:这是新表
sub["新列"] = 1                      # 不会再警告

追问与延伸:

  • "视图"和"副本"的区别?——视图共享内存,改视图会改原表;副本是新对象,互不影响。pandas 为了性能,某些切片返回视图、某些返回副本,规则不透明,所以官方建议:写操作一律用 loc。
  • 怎么彻底关掉这个警告?——pd.options.mode.chained_assignment = None,但面试不推荐这么答,要先说"正确做法是改写 loc"。

7. 高频坑二:链式赋值(chained indexing)

题目 7.1:下面这段代码有什么问题?怎么改?

# 面试题原题:
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df[df["A"] > 1]["B"] = 0

考点分析:这就是链式赋值的典型写法。df[df["A"] > 1] 第一次索引,["B"] = 0 第二次索引,中间可能产生副本,赋值丢失。

# 错误(同上):
# df[df["A"] > 1]["B"] = 0

# 正确:loc 一步完成"行筛选 + 列定位 + 赋值"
df.loc[df["A"] > 1, "B"] = 0
print(df)
#    A  B
# 0  1  4
# 1  2  0
# 2  3  0

追问与延伸:

  • 多层列的时候呢?——df.loc[row_mask, ("A", "B")] = 0,元组定位。
  • 为什么有时候链式赋值"看起来也行"?——碰巧那次切片返回了视图,改到了原表。但这是未定义行为,pandas 版本一变就可能失效,绝对不能依赖。

8. 高频坑三:axis=0 还是 axis=1

题目 8.1:df.mean(axis=0) 和 df.mean(axis=1) 分别算什么?

考点分析:axis 是新手第一大混淆点。记忆口诀:"axis=0 沿着行方向往下压(按列聚合),axis=1 沿着列方向往右压(按行聚合)"。

df = pd.DataFrame({
    "语文": [90, 80, 70],
    "数学": [85, 95, 65],
}, index=["张三", "李四", "王五"])

# axis=0(默认):沿着"行号"方向压扁 -> 结果是"每一列"的统计量
print(df.mean(axis=0))
# 语文    80.0
# 数学    81.666...

# axis=1:沿着"列名"方向压扁 -> 结果是"每一行"的统计量
print(df.mean(axis=1))
# 张三    87.5
# 李四    87.5
# 王五    67.5

# dropna 也是同理:axis=0 删"有缺失的行",axis=1 删"有缺失的列"
# concat 也是同理:axis=0 上下拼(行增多),axis=1 左右拼(列增多)

追问与延伸:

  • 记忆技巧?——把 axis 想成"被消灭的那个维度"。axis=0 消灭行号,剩下列,所以输出按列;axis=1 消灭列名,剩下行,所以输出按行。
  • df.drop(columns=["A"]) 和 df.drop("A", axis=1) 等价吗?——等价,新写法推荐 drop(columns=...),可读性更好。

9. 基础考点速查表

| 考点 | 必会 API | 一句话记忆 |

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

| 创建 Series | pd.Series(列表/字典) | 字典 key 自动变 index |

| 创建 DataFrame | pd.DataFrame(字典=列表) | 最常用,key 变列名 |

| 看结构 | df.shape / dtypes / info() / describe() | shape 看长宽,info 看类型 |

| 标签选择 | df.loc[行, 列] | 切片含右端 |

| 位置选择 | df.iloc[行, 列] | 切片左闭右开 |

| 条件筛选 | df[df["列"] > v] | 多条件加括号、用 &\|~ |

| 类 SQL 筛选 | df.query("列 > v") | 引用外部变量用 @ |

| 缺失识别 | df.isna().sum() | 每列缺几个 |

| 缺失删除 | df.dropna(subset=[...]) | 按指定列判断 |

| 缺失填充 | df.fillna(值) / ffill | 数值用均值/中位数 |

| 去重 | df.drop_duplicates(subset=[...]) | keep 控制留哪条 |

| 类型转换 | pd.to_numeric / to_datetime | 比 astype 更鲁棒 |

| 元素映射 | s.map(字典) | Series 专用 |

| 行/列函数 | df.apply(func, axis=1) | 兜底,能向量化就别用 |

| 单元格函数 | df.applymap(func) | 逐单元格 |

| 安全赋值 | df.loc[mask, 列] = 值 | 永远别写链式赋值 |

| 方向 | axis=0 按列 / axis=1 按行 | 被消灭的是那个维度 |


10. 易错点 Top 10 与自测练习

易错点 Top 10

1. `df[mask]["列"] = 值` —— 链式赋值,改写 df.loc[mask, "列"] = 值。

2. 多条件用 `and/or` —— 改用 & | ~,每个条件加括号。

3. `x == np.nan` 判断缺失 —— 永远 False,用 pd.isna(x)。

4. loc 切片写 `0:3` —— loc 是标签切片含右端,写位置切片请用 iloc。

5. `df.mean(axis=1)` 以为是按列 —— axis=1 是按行。

6. 读进来的数字列是 object —— pd.to_numeric(errors="coerce") 修一下。

7. 整数列出现 NaN 后变成 float —— 这是正常现象,想用可空整型写 Int64。

8. `drop_duplicates` 不排序就 keep="last" —— 保留的可能不是你想要的那条。

9. `apply(axis=1)` 做向量化能做的事 —— 慢 10~100 倍,能拆列算就拆列。

10. 改切片结果忘了 `.copy()` —— 触发 SettingWithCopyWarning,新表先 copy。

自测练习

1. 用 3 种不同方式创建一个 3 行 2 列的 DataFrame,并说出每列的 dtype。

2. 写出"筛选销量 > 80 且城市不是北京"的 loc 写法,再用 query 重写一遍。

3. 给定一列含缺失值的收入数据,分别用均值、中位数、前向填充三种方式补全,比较结果。

4. 把 df[df["A"]>1]["B"] = 0 改写为安全写法,并解释为什么原写法危险。

5. df = pd.DataFrame({"a":[1,2,3],"b":[4,5,6]}),分别求每列均值和每行均值,说出 axis 参数。

6. map、apply、applymap 各举一个适用场景,并说明为什么"能向量化就别 apply"。


下一篇预告:第 31 章 pandas 面试题精讲:进阶篇 —— groupby 三兄弟、merge 的各种 join、透视表、时间序列与向量化性能,把面试进阶题一次讲透。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 30 章 pandas 面试题精讲:基础篇》完整推广海报
DISCUSSION

文章回复

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