简介:面向数据分析岗求职者与 Python 学习者的 Pandas 面试题整理,围绕核心数据结构与常见分析操作,系统梳理 60 道高频题目,覆盖入门到进阶的知识层级。内容按基础、中级、高级分层:基础部分聚焦 Series 与 DataFrame、CSV 读写、列筛选、缺失值处理、排序合并与分组统计;中级延伸到多条件筛选、时间序列、MultiIndex、melt、字符串与正则处理;高级涉及内存优化、分块读取大数据、Categorical 性能、SQL 交互、窗口函数与自定义聚合。每题配有要点说明和可运行代码示例,便于对照练习、查漏补缺,也可作为面试前的快速复习清单。压缩包内为 1 个 PDF 文件,约 263KB,轻量易读,电脑与手机均可直接打开。目前已有 1288 人学习下载,尤其适合准备数据分析实习与校招面试时按模块刷题,也可用于日常开发中的 API 速查。
1. 到底什么是 Pandas:一张会自己算数的 Excel 表
很多人第一次听到 Pandas 这个名字,会以为是那只圆滚滚的动物,或者某个和熊猫有关的项目。其实它是 Python 里一个专门处理表格数据的库,名字来自“面板数据”的缩写。你可以先把它想成一张 Excel 表:有行有列、有表头,能筛选、能排序、能求和、能做透视。区别在于,Excel 靠手点,Pandas 靠一行代码;Excel 十万行就开始卡,Pandas 处理几百万行还能跑;Excel 的操作没法留痕,Pandas 的每一步都写在脚本里,明天重跑一遍结果一模一样。对做数据的人来说,这张“会自己算数的表”几乎是每天都要碰的工具,也正因如此,Pandas 成了数据岗位面试里绕不开的第一道门槛。
2. 为什么面试总从 Series 和 DataFrame 问起:两种容器撑起全部操作
2.1 一维的 Series 与二维的 DataFrame
Pandas 里只有两种核心容器,剩下的几百个方法都长在它们身上。Series 是一维的,可以理解成“带标签的一列数”;DataFrame 是二维的,是“共用同一套行标签的多列”。这句话听着平淡,但它解释了一个新手经常困惑的现象:为什么我从 DataFrame 里取出一列,类型就变了?
import pandas as pd # 一维:索引 + 值,索引默认是 0,1,2... s = pd.Series([88, 92, 75], index=['A', 'B', 'C']) print(s['B']) # 92,用标签取值 # 二维:多列共享同一套行索引 df = pd.DataFrame({ 'name': ['A', 'B', 'C'], 'score': [88, 92, 75], 'class': ['x', 'x', 'y'], }) print(type(df['score'])) # <class 'pandas.core.series.Series'>上面这段的关键在于最后一行:df['score']返回的不是 DataFrame,而是 Series。参数说明上,pd.Series(data, index=...)的index决定标签;pd.DataFrame(dict)的字典键直接变成列名,行索引自动生成 0 到 n-1。理解了这个层级关系,你才能明白为什么有些方法只能在 DataFrame 上用,有些只能在 Series 上用,也才能在报错'Series' object has no attribute 'columns'时立刻知道自己在哪一层。
Series 真正的威力在“对齐”。两个 Series 相加时,Pandas 不是按位置加,而是按标签加:标签对得上的相加,对不上的位置给 NaN。这个设计和 Excel 里两个单元格直接相加的直觉完全不同,很多人第一次遇到结果里冒出一堆 NaN,就是因为两边的索引根本不一致。
a = pd.Series([1, 2, 3], index=['x', 'y', 'z']) b = pd.Series([10, 20, 30], index=['y', 'z', 'w']) print(a + b) # x NaN ← 只有 a 有 # y 12.0 # z 23.0 # w NaN ← 只有 b 有提示:看到莫名其妙的 NaN,先别急着填,先检查两边的 index 是不是同一套。
2.2 读写与查看:所有分析的入口都在最前面几步
面试题里“如何读取 CSV”“如何查看前几行”看起来像送分题,其实考的是你有没有真正踩过坑。pd.read_csv()默认会自己猜每一列的类型,猜得不准的时候,后果比报错更麻烦:以 0 开头的编号会被当成数字,前导零消失;长数字 ID 会被读成浮点数,末尾变成.0;日期列被读成字符串,后面做时间运算直接翻车。
# 关键参数:把“猜”变成“指定” df = pd.read_csv( 'orders.csv', dtype={'order_id': str}, # 强制按字符串读,保住前导零 parse_dates=['created_at'], # 直接解析成日期类型 usecols=['order_id', 'amount', 'created_at'], # 只读需要的列,省内存 nrows=1000 # 先读一小部分探探结构 )参数逐个说:dtype用字典精确指定某几列的类型;parse_dates让 Pandas 在读入时就把字符串转成时间类型,省掉后面一次to_datetime;usecols是省内存最直接的手段,一张几十列的表往往只需要三五列;nrows适合在正式读之前先看看长什么样。这几个参数组合起来,基本能避开读文件阶段八成的坑。
读进来之后是三件套:head(n)看前 n 行,默认 5 行;info()看每列的类型和非空数量,这是发现缺失值最快的方式;describe()看数值列的计数、均值、标准差、四分位数。三者各管一件事,head管长相,info管结构,describe管分布。面试里如果只答“用 head 看前五行”,通常会被追问一句“那你怎么知道这列有没有缺失”,答案就在info()的非空计数里。
2.3 索引不是行号:这是 Pandas 第一个容易让人困惑的地方
初学者最容易把“索引”和“第几行”当成一回事。默认情况下它们确实重合,因为自动生成的就是 0、1、2、3。但只要你做过一次筛选或排序,这层关系就断了:筛完之后索引还是原来那些数字,中间缺了几个,而位置早就变了。
df2 = df[df['score'] > 80] print(df2.index.tolist()) # 可能是 [0, 1],也可能是 [1, 5, 9] # 想要干净的 0,1,2...,需要显式重置 df2 = df2.reset_index(drop=True) # drop=True 表示丢掉旧索引,不把它变成新列set_index('列名')是把某一列提升为行标签,常用于时间序列(把日期设成索引)和关联查询(把主键设成索引);reset_index()是反向操作,把标签降回普通列,drop=True则直接丢弃。这两个方法经常成对出现,构成“设索引—按标签取—重置”的循环。
换一个角度说:索引是给“名字”,位置是给“第几个”。当你心里想的是“我要第 3 行”,用位置;当你想的是“我要编号为 A007 的那行”,用标签。这个区分在下一章的loc和iloc里会变成一道分水岭,也是面试里区分“用过”和“用熟”最快的一问。
3. 筛选、排序、合并:三件每天都要干的活,也是面试的高频区
3.1 布尔索引与 query:把条件写成一句话
筛选是 Pandas 使用频率最高的操作,没有之一。基本形式是把一个布尔序列塞进方括号:df[df['amount'] > 100]。括号里那串东西每个位置是 True 或 False,Pandas 只保留 True 对应的行。理解了这一层,多条件组合就不再神秘。
# 多条件:与用 &,或用 |,每个条件都要单独加括号 result = df[(df['amount'] > 100) & (df['city'] == '北京')] # 等价的 query 写法,可读性更好,@ 用来引用外部变量 threshold = 100 result = df.query('amount > @threshold and city == "北京"')这里有个几乎人人踩过的坑:&的优先级比比较运算符高,不写括号就会变成df['amount'] > (100 & df['city']),然后报一串看不懂的错。记不住优先级没关系,把每个条件都括起来就行,这是最省心的习惯。
query()的好处是把条件写成接近自然语言的字符串,列名里带空格时可以用反引号包起来;@变量名是它特有的语法,用来引用 Python 环境里的变量。缺点也明显:字符串里的表达式没法被编辑器检查,写错列名要跑到运行时才报错。两种写法没有优劣,写长条件时query更清楚,写简单条件时布尔索引更直接。
还有一种情况是“条件选择后赋值”。直接写df[df['score'] < 60]['level'] = '不及格'会触发链式赋值警告,因为它可能作用在副本上,原表根本没变。正确写法是一步到位:df.loc[df['score'] < 60, 'level'] = '不及格',用loc同时锁定行条件和列名,改动才落回原表。
3.2 排序与索引重置:顺序变了,标签没变
sort_values(by='列名')按值排序,ascending=False是降序,na_position='first'把空值放最前。多列排序传列表即可,by=['city', 'amount']表示先按城市分组,组内再按金额排。
# 多列排序:城市升序,金额降序 df_sorted = df.sort_values( by=['city', 'amount'], ascending=[True, False], na_position='last' )参数ascending接受列表时,长度必须和by一致,一位控制一列。排序完索引是乱的,这是正常现象,Pandas 不会自动帮你重排。如果后续要用位置取值,先reset_index(drop=True)会更省心;如果后续要用标签取值,保留原索引反而更安全,因为标签还指向原来那行。
另一个常被问到的点是“怎么取某列最大的前 n 行”。标准答案是nlargest(n, '列名'),比先排序再切片快得多,因为它内部只维护一个大小为 n 的堆,不需要把整表排一遍。nsmallest是对应的最小值版本。
3.3 merge 的四种 how:面试最爱追问的那一句
合并两张表是数据处理里最像数据库操作的部分,也是面试追问最密集的地方。pd.merge(left, right, on='key', how='inner')里,how决定保留哪些键:inner只保留两边都有的,left保留左表全部,right保留右表全部,outer两边都保留、缺失处补 NaN。
用一个具体场景看差别。左表是订单(每单一条),右表是客户(每个客户一条),用客户编号关联:
| how | 结果行数 | 什么情况会用到 |
|---|---|---|
| inner | 只含两边都匹配上的订单 | 只分析有客户资料的订单 |
| left | 等于左表行数(客户唯一时) | 保留全部订单,缺资料的补空 |
| right | 等于右表行数(客户唯一时) | 保留全部客户,没下单的也留着 |
| outer | 两边并集 | 对账、找两边不一致的记录 |
真正的坑不在how,在“关联键不唯一”。如果右表同一个客户有多条记录,左表一行就会被复制成多行,合并后行数暴涨。这种膨胀很多时候不报错,只是数字变得不对劲,等发现时已经算错了好几版报表。
# validate 参数能在合并时就拦住“一对多”的意外 merged = pd.merge( orders, customers, on='customer_id', how='left', validate='many_to_one' # 声明左表多、右表一,不满足就报错 )validate接受one_to_one、one_to_many、many_to_one、many_to_many四种声明。它的价值在于把“事后发现数字不对”提前成“合并当场报错”。写分析脚本时加这一行,能省掉大量回头排查的时间,这是很多人的血泪经验。
顺带说清另外两个容易和 merge 混的操作:join默认按索引关联,适合两边索引已经对齐的场景;concat是拼接不是关联,axis=0竖着摞(加行),axis=1横着并(加列),后者按索引对齐,对不上就补 NaN。
4. 常见误区与易混概念:长得像、结果差很远的几组函数
4.1 loc 与 iloc:标签和位置,差一个字母差一条命
易混点:两个都是取数据,写出来只差一个字母,效果却可能完全不同。
为什么混:默认索引正好是 0、1、2 的时候,df.loc[0]和df.iloc[0]拿到的是同一行,于是很多人以为它们可以互换。一旦做过筛选、排序或者把某列设成索引,两者立刻分道扬镳。
怎么区分:loc认标签,iloc认位置。还有一个细节常被忽略:切片时loc是两端都包含,iloc是左闭右开。
df = df.set_index('code') # 索引变成字符串编号 df.loc['A007'] # 按标签取,取不到就报 KeyError df.iloc[0] # 按位置取第一行,永远有结果 df.loc['A001':'A005'] # 包含 A005 df.iloc[0:5] # 第 0 到第 4 行,不含第 5 行判断标准很简单:你脑子里想的是“哪个编号/哪一天”,用loc;想的是“第几行第几列”,用iloc。取单个格子时还有更快的at(标签)和iat(位置),在循环里频繁取值时差距明显。
4.2 apply、map、applymap:作用对象不同,性能也不同
易混点:三个都是“把函数套到数据上”,名字又都是 a 开头。
为什么混:日常需求往往是“把某列每个值处理一下”,这时候三种写法有时都能跑通,于是随便挑一个能用就行。
怎么区分:看作用对象。Series.map作用于 Series 的每个元素,只能用在单列上;Series.apply也是逐元素,但能接受更复杂的函数,甚至可以返回聚合结果;DataFrame.apply默认按列(axis=0)把整列当参数传进去,axis=1则按行;DataFrame.applymap作用于每个格子,是最慢的一种。
# 单列逐元素转换,map 最快 df['level'] = df['score'].map(lambda x: '高' if x >= 90 else '低') # 按行计算,需要同时用到多列 df['total'] = df.apply(lambda row: row['price'] * row['qty'], axis=1)axis=1的按行 apply 本质上是在 Python 层循环,几十万行时慢得明显。能向量化就向量化:上面那行直接写df['price'] * df['qty']快几十倍。这也是面试里“如何优化性能”最常见的答案:先看能不能不用 apply。
4.3 cut 与 qcut:等宽和等频,分箱的两种哲学
易混点:两个都是把连续数值切成几段,名字只差一个字母。
为什么混:需求都是“分成 5 档”,结果都能得到一个类别列。
怎么区分:cut按数值区间等宽切,每段跨度一样,但落在每段里的数据量可能极不均匀;qcut按分位数切,每段数据量基本一样,但区间宽度各不相同。
# 等宽:0-20, 20-40, 40-60, 60-80, 80-100 pd.cut(df['score'], bins=5) # 等频:每段大约 20% 的数据 pd.qcut(df['score'], q=5)选哪个取决于目的。想看“高分区间和低分区间的人数对比”,用cut,因为区间含义固定;想做“前 20%、中间 60%、后 20%”的客群分层,用qcut,因为要保证每层人数相当。qcut遇到大量重复值时可能报错说边界不唯一,这时加duplicates='drop'让它合并重复边界。
4.4 pivot_table 与 crosstab、replace 与 where
易混点:前一对都是做透视,后一对都是“改值”。
为什么混:crosstab完全可以用pivot_table写出来,where也能实现一部分replace的效果。
怎么区分:pivot_table是通用透视,能指定values、index、columns、aggfunc,默认求均值;crosstab是“数频次”的快捷方式,默认统计组合出现的次数,做交叉分析表时更省事。replace是“把 A 换成 B”,精确匹配;where是“条件为真的留下,为假的替换掉”,默认替换成 NaN,它关心的是条件而不是具体值。和where配对的是mask,逻辑正好反过来:条件为真的替换掉。
# replace:把具体的错误标记换成缺失值 df['age'] = df['age'].replace({'未知': None, -1: None}) # where:保留 amount > 0 的值,其余置为 NaN df['amount'] = df['amount'].where(df['amount'] > 0) # mask:反过来,amount > 0 的置为 NaN df['amount'] = df['amount'].mask(df['amount'] > 0)记住一句话就不会混:replace认“值”,where/mask认“条件”。面试里如果被问到“这两个有什么不一样”,答出这一句基本就够了,再补一个使用场景会更稳。
5. 分组、时间与内存:从“能算出来”到“算得动”
5.1 groupby 的心智模型:拆分、应用、合并
分组聚合是 Pandas 里最像 SQL 的部分,也是最值得在脑子里建模型的部分。整个过程分三步:按某个键把表拆成若干小组,对每组独立算一个结果,再把结果拼回一张表。理解了这三步,agg、transform、apply的区别就清楚了。
# 每个城市一条记录:金额总和与订单数 summary = df.groupby('city').agg( total=('amount', 'sum'), orders=('order_id', 'count'), avg=('amount', 'mean') )agg是“多行变一行”,输出行数等于分组数;transform是“多行还是多行”,把每组的计算结果广播回原来的每一行,常用来算占比、算组内均值再相减:
# 每行占本城市总额的比例,输出和原表一样长 df['share'] = df['amount'] / df.groupby('city')['amount'].transform('sum')这个“组内占比”的写法在实际分析里出现频率极高,而很多人第一反应是写循环,慢且容易错。transform一行就解决,而且自动对齐索引。
还有一条常被问到的:分组后先求和再求平均怎么写。答案是链式聚合,先agg('sum')得到一张新表,再对新表做一次mean();或者在agg里传多个函数,得到多层列名,再用pd.concat或列名元组去取。掌握“聚合结果仍然是 DataFrame,可以继续操作”这个认知,这类问题就不再是背答案。
5.2 时间序列与 MultiIndex:Pandas 真正的主场
Pandas 最早就是为金融时间序列设计的,这块能力比大多数同类工具都完整。核心只有三个动作:把字符串变成时间类型,把时间设成索引,按时间频率重采样。
df['date'] = pd.to_datetime(df['date'], errors='coerce') # 转不了的变 NaT df = df.set_index('date').sort_index() # 时间做索引并排序 # 按周汇总,sum 是聚合方式,也可以换成 mean / count weekly = df.resample('W')['amount'].sum() # 7 天滚动平均,min_periods 控制前几行不足时是否出值 df['ma7'] = df['amount'].rolling(window=7, min_periods=1).mean()errors='coerce'是个实用参数:遇到无法解析的字符串不抛异常,而是变成NaT,方便你先看看有多少脏数据。resample的字符串是频率别名,D是天、W是周、M是月。rolling的window是窗口长度,min_periods决定开头数据不够时给不给结果。
时区是另一个高频追问点。tz_localize是“给一个没有时区的时间打上时区标签”,tz_convert是“把一个已经有时区的时间换算到另一个时区”。前者用于“这批数据本来就是北京时间”,后者用于“我要看纽约时间下的同一时刻”。用错这两个,结果会差好几个小时,而且不会报错。
MultiIndex 则是“用多列当行标签”,常见于多维数据的分组结果。取值时传元组:df.loc[('北京', 'A')];只想取某一层用xs。它读起来别扭,但在处理“地区 + 品类 + 月份”这类多层结构时,比拍平成一堆列要清楚得多。
5.3 内存优化:让大表跑得动
数据量一上去,第一件事不是换工具,而是看类型用得对不对。Pandas 里最耗内存的往往是字符串列和默认的 64 位数值类型,而实际数据根本用不到那么宽。
# 把低基数的字符串列转成类别型,内存能降一个数量级 df['city'] = df['city'].astype('category') # 数值降位宽:确认取值范围后选最小够用的类型 df['qty'] = pd.to_numeric(df['qty'], downcast='integer') df['amount'] = pd.to_numeric(df['amount'], downcast='float')category适合重复值多的列,比如城市、状态、品类;每个不同值只存一份,其余位置存编号,代价是排序和新增类别时要小心。downcast会自动挑能装下当前取值范围的最小类型,但前提是你确认过数据没有超出范围,否则会静默溢出。
再大一点就要靠分块读取。read_csv的chunksize参数返回一个可迭代对象,每次只读一小块,处理完就丢,内存占用稳定在一个小水位上:
total = 0 for chunk in pd.read_csv('big.csv', chunksize=100_000): total += chunk['amount'].sum() # 每块只算一个数,不保留原始数据这个模式的要点是“每块只产出一个中间结果”,最后再合并。如果每块都要保留整块数据,那分块就失去意义了。还有eval和query,对超大表做简单算术时能减少中间对象的创建,写法上就是把表达式写成字符串,收益在数据量大时才明显。
6. 什么时候该用 Pandas,什么时候该换工具
Pandas 不是万能的,它有一个比较清晰的舒适区。判断标准可以从三个维度看:数据规模、数据形态、结果用途。
| 场景 | 适合用 Pandas | 建议换思路 |
|---|---|---|
| 数据量 | 单机内存能装下(大致百万到千万行级别) | 远超内存,或需要多人并发查询 |
| 数据形态 | 结构化表格、时间序列 | 大量嵌套 JSON、图结构、超宽稀疏矩阵 |
| 任务性质 | 探索性分析、清洗、特征构造、一次性报表 | 长期在线的查询服务、强事务一致性 |
| 协作方式 | 单人脚本、Notebook 里迭代 | 多人共享的稳定数据管道 |
一个具体的例子能说得更清楚。假设手上有三千万行订单日志,要算每个城市每月的成交额。用 Pandas 的常规写法,先把整表读进内存,再groupby两列求和,机器内存不够时就会直接卡死或者被系统杀掉。这时候有两条路:一条是继续用 Pandas,但改成按块读、每块先聚合、最后把中间结果再聚合一次;另一条是换一个按列组织、支持流式聚合的引擎,把这件事交给它做。选哪条取决于你后面还要不要在这份数据上做大量交互式探索:要,就咬牙上更合适的引擎;不要,只是出一张月度报表,按块聚合就够了。
反过来,如果数据只有几十万行,非要上分布式框架,那就是另一方向的过度设计。调度开销、启动时间、调试难度加起来,可能比直接用 Pandas 慢十倍,而且排查问题时看不到中间结果,等于给自己加了一个黑匣子。判断依据其实很朴素:能在一台机器上用几秒钟算完的事,就别引入第二台机器。
还有一类容易忽略的边界:需要精确的、可复现的字符串处理和大规模正则匹配时,Pandas 的逐元素操作会变得很慢,先用原生字符串方法批量处理再回填,往往比直接apply快得多。同理,涉及复杂业务逻辑的逐行计算,如果逻辑本身有十几行,写成函数再用apply调用是合理的,但如果有几十万行,就该考虑把逻辑向量化或者换工具了。
我自己的习惯是:动手前先看三件事,行数、列数、每列类型。行数超过内存能舒适承载的量级,就先想分块;列数远超实际需要,就先usecols;字符串列重复率高,就先转category。这三步做完,很多“跑不动”的问题其实已经不存在了。从那以后我每次接新数据都强制走一遍这个检查,宁可多花两分钟,也不想跑到一半发现内存爆了再回头重来。
希望这些能帮你在被问到 Pandas 的时候,答的不只是“用过”,而是“知道它为什么这么设计、什么时候不该用它”。这份面试题资料里从基础到高级一共整理了九十道,按数据结构、读写、筛选、合并、分组、时间序列、性能优化分块排列,适合对着自己的薄弱环节一块一块过。
本文还有配套的精品资源,点击获取