简介:《Pandas面试题.pdf》面向准备数据分析岗位面试的 Python 学习者与初、中级开发者,围绕 Pandas 核心概念、常用操作与进阶特性梳理问答,帮助系统查漏补缺。压缩包仅含 1 个 PDF 文件,大小约 263KB,篇幅紧凑、便于在电脑或移动端随时翻阅。内容按基础、中级、高级及额外探讨展开,覆盖 Series 与 DataFrame、CSV 读写、条件筛选、排序合并、缺失值处理、分组聚合、索引重置、日期时间、MultiIndex、透视表、拼接、窗口函数、内存优化与大型数据集分块处理等主题,题目后附思路或代码示例,适合面试前快速复习和日常查阅。已有 1288 人学习下载,可作为数据分析入门后的专项练习清单,也可用于对照实际项目中的数据处理流程,定位知识盲区并补充常用写法。
1. 到底什么是 Pandas:一张会自己算数的电子表格
很多人第一次听到 Pandas,会以为是某种动物,或者某个小众工具的名字,直到翻到一份数据分析岗位的要求,才发现它出现的频率高得离谱。通俗地说,Pandas 是 Python 里专门处理「表格型数据」的一套工具,你可以把它想成一张电子表格,但这张表能被代码指挥:筛选、排序、分组、合并、补空缺,一行指令就能完成。真正值得搞懂的地方在于,数据分析里大量时间花在「把数据整理成能算的样子」上,Pandas 把这部分动作压缩成了固定套路。面试题考的也正是这套套路背后的概念边界,而不是谁能背下更多命令。
2. 为什么数据分析绕不开 DataFrame:从「表格」到「可计算的结构」
2.1 Series 与 DataFrame:一列和一张表的分工
先看两个名词。Series 是一维的、带标签的一串数据,可以理解成表格里被单独抽出来的一列,只不过它比普通列表多了一个「标签数组」,也就是索引。DataFrame 是二维的,由多列共享同一套索引拼起来,所以它既知道每一列叫什么,也知道每一行是谁。
打个比方:Series 像一张写着日期的便签条,每行一个日期,旁边标着它是第几条;DataFrame 像一整本账本,每页有列标题,每行有行号。账本里你可以只撕下「金额」那一页,它就是一条 Series,但这一页本身还带着原来的行号,所以它并没有和账本失去联系。
import pandas as pd import numpy as np # Series:一维 + 索引 s = pd.Series([3, 5, 8], index=['a', 'b', 'c'], name='score') # DataFrame:多列,每列一个 dtype,共享索引 df = pd.DataFrame({ 'name': ['A', 'B', 'C'], # 字符串列 'score': [88, 92, 75], # 整数列 'passed': [True, True, False] # 布尔列 }, index=['x', 'y', 'z'])这段代码里,index参数决定了每一行的「名字」;不写就默认从 0 开始编号。name参数给 Series 起了个名字,之后把它塞回 DataFrame 时列名就来自这里。一个容易被忽略的事实是:DataFrame 里每一列可以是完全不同的类型,但同一列内部通常只有一个统一类型,这决定了内存怎么排、计算快不快,也是后面讲内存优化的起点。
2.2 索引不是行号:自动对齐才是 Pandas 的灵魂
很多人把索引当成 Excel 最左边那列序号,这是个很要命的误解。索引是身份标识,Pandas 在多个对象之间做运算时,是拿索引去对,而不是按位置一个个排着对。这个机制叫「对齐」,它是 Pandas 里最像玄学、也最容易翻车的地方。
s1 = pd.Series([1, 2, 3], index=['a', 'b', 'c']) s2 = pd.Series([10, 20, 30], index=['b', 'c', 'd']) print(s1 + s2) # a NaN # b 22 # c 33 # d NaN按位置算的话,结果应该是 a=11、b=22、c=33。实际输出里出现了 NaN,因为 a 只在 s1 里有、d 只在 s2 里有,对不上位置就留空。这个行为一开始让人抓狂,但它也正是安全感来源:如果你有一份订单表和一份用户表,两边行序完全打乱,按索引对齐能保证金额和用户始终绑在一起,不会因为谁在第几行就张冠李戴。
想把缺位当 0 处理,可以用s1.add(s2, fill_value=0)。理解了对齐,再去看 merge、join、concat,你会发现它们的差别本质上就是「拿什么当对齐依据」。
2.3 读进来才算开始:IO 与第一眼的形状检查
在面试题里,读取文件、查看前几行、描述性统计这些看似简单的问题占了很大比重,原因是数据工作的第一步永远是「确认手里到底是什么」。read_csv有一堆参数,真正天天用的大概就那几个。
df = pd.read_csv( 'orders.csv', sep=',', # 分隔符,制表符文件要改成 '\t' usecols=['order_id', 'amount', 'created_at'], # 只读需要的列 dtype={'order_id': 'int64'}, # 显式指定类型,避免被猜成 object parse_dates=['created_at'], # 直接解析成日期类型 nrows=1000 # 先读一小段试试水 ) print(df.shape) # (行数, 列数) print(df.dtypes) # 每一列的真实类型 print(df.head(3)) # 前 3 行,看数据长什么样参数说明:usecols在文件很大时能省下大量内存,因为没被选中的列根本不会被解析;dtype是防止 Pandas 自作主张的关键,一列本该是数字但混进一个空字符串,整列就会退化成 object,后续的加减乘除全部失效;parse_dates让日期列从字符串变成时间类型,只有这样才能做按月分组、算间隔这类操作;nrows适合先做一次小样本体检。
一个实用的顺序是:先 shape 看规模,再 dtypes 看类型,再 head 看长相,最后 describe 看数值分布。这四步跑完,你对数据的信任度才有基础。
3. 三种取数姿势:loc、iloc 和布尔索引到底差在哪
3.1 loc 按标签、iloc 按位置:两套坐标系并存
Pandas 同时维护两套坐标系:一套是索引上的名字,一套是物理位置。loc认名字,iloc认位置,两者不能混着用,也不能互相顶替。
df = pd.DataFrame( {'amount': [100, 250, 80]}, index=['order_a', 'order_b', 'order_c'] ) df.loc['order_b', 'amount'] # 250,按索引名取 df.iloc[1, 0] # 250,按位置取 df.at['order_b', 'amount'] # 单个值,label 版,速度更快 df.iat[1, 0] # 单个值,position 版这里有个典型的踩坑点:loc的切片包含右端点,iloc的切片是常见的左闭右开。写df.loc['a':'c']会取到 a、b、c 三行,写df.iloc[0:3]也是三行,但如果你写成df.iloc[0:2],只会拿到两行。两种规则都合理,混在一起用就会数错行数。日常记住一句话:只要你在心里数「第几行」,就该用 iloc;只要你嘴里念的是「那个叫某某的行」,就该用 loc。
at和iat是为单个值服务的快捷通道,当你要在一个大表里循环取几百个单元格时,它们的开销明显低于loc和iloc。
3.2 布尔索引与 query:把筛选条件写成句子
筛选的本质是造一个和行数等长的布尔序列,再拿它去「套」。手动写条件时,最容易出问题的是运算符优先级。
target_city = 'A' # 每个条件都要用括号包起来,否则 & 会先算错 mask = (df['amount'] > 100) & (df['city'] == target_city) result = df.loc[mask, ['order_id', 'amount']] # query 版本:读起来更像自然语言,@ 用来引用外部变量 result2 = df.query('amount > 100 and city == @target_city')&表示「且」,|表示「或」,~表示「非」,这三个符号必须用括号把每个比较表达式裹住。原因在于 Python 里位运算符的优先级高于比较运算符,df.a > 1 & df.b < 2会被解析成完全不同的东西,然后报一个看起来毫不相干的错。
query的好处是可读性,尤其是在条件特别长的时候。@符号是它引用外部 Python 变量的方式,这一点很多人第一次见会懵。需要注意的是,列名里如果带空格或者和关键字重名,query 里得用反引号包一下。
3.3 赋值与 SettingWithCopy:那个让人半夜惊醒的警告
筛选和赋值凑在一起时,Pandas 会抛出一个警告,大意是「你正在修改一个切片」。这个警告背后是视图和副本的区别:有些操作返回的是原数据的一个窗口,有些返回的是一份独立拷贝,而 Pandas 无法百分百确定你拿到的是哪种。
# 不推荐:先筛再赋值,可能只改到了临时副本 df[df['amount'] > 100]['flag'] = 1 # 推荐:定位和赋值一次性完成 df.loc[df['amount'] > 100, 'flag'] = 1第一种写法里,df[df['amount'] > 100]先产生一个中间结果,后面的赋值作用在这个中间结果上,原表可能纹丝不动,而你等了半天以为改完了,这就是血泪经验里最常见的一种。
规避方式很朴素:赋值时永远用一次loc同时给出「行条件」和「列名」;确实需要一个独立子表时,显式加.copy(),把意图说清楚。另外,inplace=True也不是什么自动挡,它让方法不返回新对象而是就地改,代价是链式调用会拿到 None,写起来容易断链。多数场景下,写成df = df.dropna()这种「重新赋值」的形式反而更清楚。
4. 缺失值、合并与分组:日常真正吃时间的三件事
4.1 缺失值不是 0:NaN 的三种身份
缺失值在 Pandas 里有好几副面孔:数值列里通常是浮点的 NaN,对象列里可能是 None,时间列里是 NaT。它们看起来都叫「空」,但类型不同,判断方式也不同,统一用isna()或notna()最省心。
df.isna().sum() # 每列各缺多少个 df.dropna(subset=['order_id']) # 只在关键列缺失时才丢这一行 df['amount'] = df['amount'].fillna(0) # 数值缺失按 0 补 df['city'] = df['city'].fillna('unknown') # 类别缺失单独给个标签dropna的subset参数决定了「看哪几列的空」,不写就是任何一列有空就丢整行,太粗暴;how='all'则相反,全空才丢;thresh可以设定「至少有几个非空值才保留」。fillna除了给固定值,还能按列传一个字典,让不同列填不同的东西。
有一个细节值得单独说:整数列一旦出现缺失值,整列会自动变成浮点,于是你会看到 1.0、2.0 这种别扭的数字。这不是 bug,是 NaN 本身只能存在浮点类型里。较新的 Pandas 提供了可空整数类型,能避免这个转换,遇到时查一下类型说明即可。
4.2 merge、join、concat:三种「拼」不是一回事
这三者经常被统称为「合并」,但它们对齐的依据完全不同,用错一个,得到的结果行数可能翻好几倍。
| 方法 | 对齐依据 | 典型场景 | 结果行数 |
|---|---|---|---|
| merge | 某一列或多列的值 | 两张表有共同业务键 | 由 how 决定 |
| join | 索引 | 两边索引已对齐或已设好 | 由 how 决定 |
| concat | 轴向堆叠 | 结构相同的多份数据接在一起 | 直接相加 |
# 按 user_id 这一列的值对齐,左表全保留 merged = pd.merge( orders, users, on='user_id', how='left', validate='many_to_one' # 校验右表 user_id 是否唯一 )how的四个取值很像 SQL:inner 只留两边都有的,left 保左表,right 保右表,outer 全都要。validate是一个很好用的后悔药式参数,它会在合并前检查对应关系是否成立,如果右表某个 user_id 出现两次,就直接报错,而不是悄悄把行数撑大。很多数据对不上的事故,都是因为少了这一步检查。
concat的axis参数决定是上下接(axis=0)还是左右接(axis=1),左右接时同样是按索引对齐,对不上的位置会补空。
4.3 groupby:拆分、应用、合并的三段式
分组操作的思路是三步:按某列把行分成若干堆,对每堆算一个结果,再把结果拼回一个表。关键在于「拼回」的形状,这也是 agg、transform、filter 三个方法的差别所在。
g = df.groupby('city')['amount'] g.sum() # 每个城市一行,行数变少 g.transform('sum') # 每行都补上所属城市的总和,行数不变 g.agg(['sum', 'mean', 'count']) # 一次算多个指标 g.filter(lambda x: x.sum() > 1000) # 整组保留或整组丢弃agg是「降维」,每组变成一个或几个数;transform是「广播」,把组级结果原样铺回每一行,做「每笔订单占本城市总额的比例」这类计算时非它不可;filter是按组的整体情况做取舍,返回的是原始行,不是统计值。至于apply,它最灵活也最慢,能塞任意自定义函数,但用之前先想想能不能用前三个替代。
还有一个概念是惰性:groupby本身不做任何计算,只是记下「按哪列分」,真正触发计算的是后面接的聚合动作。理解了这一点,就能明白为什么有些链式写法看起来很省事,实际上每一步都在生成新对象。
5. 常见误区:五组长得像、用起来完全不同的概念
5.1 map、apply 与按元素操作:作用对象不一样
易混点在于三个名字都带「应用」的意思。之所以会混,是因为它们都能接一个函数,写起来长得也像。区分办法是看作用对象和返回形状:map只服务于 Series,逐个元素替换或转换,返回等长的 Series;apply可以作用在 Series 上,也可以作用在 DataFrame 上按行或按列扫,返回形状由函数决定;DataFrame 上还有一类「逐元素」的操作,不同版本叫法不太一样,报错时先确认当前版本支持哪个名字。
判断标准很简单:你想改的是每个格子,就用逐元素那类;你想对整行或整列做一次计算,就用 apply;你只是想把一列的值按映射关系换掉,map 最直接。
5.2 pivot_table 与 crosstab:汇总和计数不是一回事
两者都能产出交叉表,肉眼看上去很像。混淆的原因是默认行为下它们都能把两个字段摆成行列。区别在于:pivot_table是通用汇总器,你要明确告诉它「值是什么、用什么函数聚合」,默认求平均;crosstab天生就是数频次,给两个字段就能出「每个组合出现多少次」。
如果你的需求是「每个城市每个月的销售额」,那是 pivot_table;如果是「每个城市每个月的订单笔数」,crosstab 更省事,但用 pivot_table 加 count 也能做。别在需要求和的时候顺手用了 crosstab,它默认不给你求和。
5.3 melt 与 pivot、stack 与 unstack:宽长互转的两组开关
这四者说的都是形状变换,方向刚好相反。melt把宽表压成长表,列名变成一列「变量名」,值变成一列「数值」;pivot把长表撑回宽表,是 melt 的逆向操作。stack把最内层列压进索引,unstack把索引层级提回列,这两者操作的是索引层级,不是普通的列。
混在一起的原因是结果看起来都是「行列互换」。区分办法看输入形态:源数据是一行一个观测、多列是不同变量,那是宽表,要变长就用 melt;源数据是一行一条记录、某列存着「这是哪个指标」,那是长表,要变宽就用 pivot。绘图库和很多统计工具偏好长表,人眼阅读偏好宽表,来回转换是常态。
5.4 inplace 与重新赋值:改没改到原对象
易混点是「我明明调了方法,为什么表没变」。原因是很多方法默认返回新对象,原对象不动;加了inplace=True才就地修改,但此时方法返回 None,把它接在链式调用后面就会拿到一个空值。
区分办法是养成统一习惯:要么全程用重新赋值df = df.xxx(),要么全程用 inplace,不要一半一半。前者更好读,也更容易在中间插一句打印看结果。
5.5 object 类型不等于字符串类型
看到某列 dtypes 显示 object,很多人默认它是文本。实际上 object 只是「Python 对象」的容器,里面可能混着数字、字符串、列表甚至字典。混淆的原因是 Pandas 在读入混杂数据时会退化成这个最宽松的类型。
区分办法是先抽样看几个值,再用astype明确转换。转之前要清理脏值,否则一个无法解析的字符就会让整列转换失败。
6. 什么时候该翻这份面试题,什么时候别硬啃
6.1 适合用它的三种场景
第一种是准备数据岗面试。分档整理的题目有个好处:基础档帮你确认「概念有没有漏洞」,中级档帮你把零散用法串成体系,高级档逼你想清楚内存、性能和边界条件。按档刷一遍,比漫无目的地搜零散答案更省时间。
第二种是「会写但说不清」。很多人能照着记忆拼出能跑的代码,可一旦被问「loc 和 iloc 为什么不能换」,就哑了。这类题目的价值不在于答案本身,而在于它逼你把直觉翻译成一句能说出口的话。
第三种是日常清洗时的速查。碰到「宽表怎么变长」「分组后怎么保留原行数」这类具体问题时,按关键词定位到对应题目,比翻文档快。
6.2 不建议硬啃的情况
完全没写过 Python 的人,先补基础语法和循环、函数这些概念,直接看题会变成背答案。想靠背题代替动手的人也要小心,Pandas 的行为细节太多,不亲手跑一遍,你永远不知道那个 NaN 是从哪冒出来的。还有一种情况是特定业务的性能调优,通用题给的是普适思路,真正的瓶颈往往藏在你的数据分布里。
下面这段是一个很典型的清洗顺序,把前面几章的概念串在一起:读入时定好类型和日期,先处理关键列缺失,再对数值做合理的边界处理,最后按业务口径聚合。
df = pd.read_csv('orders.csv', parse_dates=['created_at']) # 关键标识缺失的行没有分析价值,直接丢 df = df.dropna(subset=['order_id']) # 金额缺失按 0 处理,负数视为录入错误,截到 0 df['amount'] = df['amount'].fillna(0).clip(lower=0) # 按自然月聚合,一次算出总额和单量 report = ( df.groupby(df['created_at'].dt.to_period('M'))['amount'] .agg(['sum', 'count']) .rename(columns={'sum': 'gmv', 'count': 'orders'}) )dt.to_period('M')把时间戳降到「月」这个粒度,比先造一列年月字符串再分组更省事;clip(lower=0)是给异常值兜底;agg一次算两个指标,避免对同一份数据分组两遍;最后的rename只是让输出更好读。整段没有一句循环,这也是 Pandas 的核心使用习惯:能用向量化表达,就别写 for。
刚接触这套东西的时候,我也觉得那些方法名像黑匣子,同一件事有好几种写法,不知道哪种才算「对」。后来发现判断标准其实很朴素:先想清楚这一步是「改值」「改形状」还是「改粒度」,再去找对应的方法,基本不会走偏。从那以后我每次拿到一张新表,都强制先跑一遍 shape、dtypes、isna().sum() 这三行,再动手写任何业务逻辑,翻车的次数肉眼可见地少了。希望帮到你。
本文还有配套的精品资源,点击获取