小明的妈妈用一本手写账本记了十年家庭开支:每一笔花销占一行,抬头写着“日期、项目、类别、金额”。日子一久,账本厚得像砖头,想查“去年六月买球鞋花了多少钱”,得翻十几页。小明决定用 Python 的 Pandas 把这本旧账变成“电子账本”——这就是 DataFrame。你可以把它想象成一张会思考的表格:行是每一笔记录,列是信息的栏目,而 loc、iloc、筛选、apply 这些操作,就是给这张表装上的“查找目录”“红笔圈选”“批量换算”等十八般工具。读完这篇文章,你就能像账房先生拨算盘一样,行行列列随你摆布。

岗位关联

对 AI 从业者而言,DataFrame 是数据清洗与特征工程的“第一战场”,掌握它等于拿到了进入所有数据项目的入场券,能让你从“只会调库”升级为“随手捏数据”。

目录
  1. 可视化演示
  2. 创建与初识:账本从哪里来
  3. 索引与切片:翻到指定行
  4. 条件筛选:挑出大额花销
  5. 行列增删:增删账目与栏目
  6. 元素变换:逐格魔法
  7. 多层索引:分册分类的账本
  8. 链式管道:一气呵成的流水线
  9. 实战避坑:账房先生的守则
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

通过loc/iloc索引、条件筛选、apply变换三个模式,直观展示DataFrame行列的选取、过滤与逐元素变换流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

loc/iloc索引定位 loc基于标签索引,切片包含右端点 iloc基于位置索引,切片不包含右端点 df.loc[\'a\':\'c\'] vs df.iloc[0:3] 标签索引和位置索引的切片行为不同

创建与初识:账本从哪里来

妈妈从抽屉里拿出一本崭新的空白账本,第一件事不是动笔,而是先画好表头栏:日期、项目、类别、金额。表头一行,下面留出几十行空白等着填——这本子的“骨架”就立住了。Pandas 里的 DataFrame 也一样,它不关心你填什么,但要求你先说清楚“有哪些列、每一列是什么类型”。列是栏目标题,行是每一笔记录,行与列交汇的格子就是具体的数值。

账本可以从不同渠道诞生:妈妈手写是“从零创建”,小明用 Excel 导入则是“读取文件”。在 Pandas 中,最常用的创建方式是把字典传给 pd.DataFrame(),键名自动变成列名,值成为列数据。也可以用 pd.read_csv() 直接读 CSV 文件。创建之后,df.head() 就像快速浏览账本前几页,df.info() 则像数一数账本有几页、每页几栏。

小明把这本账本的三根“量尺”都摸清了:df.shape 量外面有多大,df.columns 看有哪些栏目标题,df.index 列出每一行的行号。有了这本电子账本,接下来他就要学怎么快速翻页、怎么圈重点了。

df=[v11⋯v1n⋮⋱⋮vm1⋯vmn]\displaystyle \text{df} = \begin{bmatrix} v_{11} & \cdots & v_{1n} \\ \vdots & \ddots & \vdots \\ v_{m1} & \cdots & v_{mn} \end{bmatrix}
这一公式描述了 DataFrame 的“骨架”:m 行记录、n 列字段、每个格子 v 都是行列定位的一个值。
  1. 行数 mdf.shape 返回的第 0 个数代表账本一共记了多少笔账,对应公式里的符号 m。
    m=df.shape[0]\displaystyle m = \text{df.shape}[0]
  2. 列数 ndf.shape 返回的第 1 个数代表每笔账记了几项信息,比如日期、金额,对应公式中的 n。
    n=df.shape[1]\displaystyle n = \text{df.shape}[1]
  3. 单元格 v第 i 行第 j 列的那个具体值,可以用 df.iloc[i, j] 像翻页签一样单独取出。
    vij=df.iloc[i,j]\displaystyle v_{ij} = \text{df.iloc}[i, j]

索引与切片:翻到指定行

翻纸质账本有两种思路:按“日期标签”翻——比如直接翻到 1 月 5 日那一页;或者按“页码位置”翻——比如翻开第 3 页不管上面写的是哪一天。Pandas 为这两种翻法配了两把专属钥匙:loc 和 iloc。loc 认标签,iloc 认位置,就像“按名字找人”和“按排队序号找人”的区别。

用 loc 时,你可以传行标签和列名:df.loc['2025-01-05', '金额'],语义是“取 1 月 5 日这一行、金额这一列”。用 iloc 时则传位置序号:df.iloc[2, 3] 表示“取第 2 行第 3 列”。注意一个关键的“脾气差异”:loc 切片是左闭右闭,包含末尾;iloc 切片是左闭右开,不包含末尾——前者像说“从 1 号到 5 号都算”,后者像说“第 0 个到第 3 个之间,第三个不算”。

小明试了试:df.loc['2025-01-05'] 一翻就翻到那笔“球鞋”账,df.iloc[2] 则拿到第三行“电费”。他还学会了用 df.at 和 df.iat 直接取单个格子——就像从账本里飞快地抽出一张便签。

df.loc[a:b]  ⟺  df.iloc[i:j]\displaystyle \text{df.loc}[a:b] \;\Longleftrightarrow\; \text{df.iloc}[i:j]
两个表达式分别按标签和位置取连续段,但区间开闭规则不同,用错会“多取一行”或“少取一行”。
  1. loc 标签段 ab:按标签取连续多行,结束标签 b 包含在内,适合“从 1 月到 3 月”这种有名字的区间。
    df.loc[’2025-01-01’:’2025-03-01’]\displaystyle \text{df.loc}[\text{'2025-01-01'}:\text{'2025-03-01'}]
  2. iloc 位置段 ij:按整数位置取连续多行,结束位置 j 不包含,适合“取前 3 行”这类无标签场景。
    df.iloc[0:3]  ⇒  第 0,1,2 行\displaystyle \text{df.iloc}[0:3] \;\Rightarrow\; \text{第 }0,1,2 \text{ 行}
  3. 等价关系当行索引恰好是从 0 开始的连续整数时,两种写法指到的行几乎一致,只是区间习惯不同。
    df.loc[i]  ≈  df.iloc[i]\displaystyle \text{df.loc}[i] \;\approx\; \text{df.iloc}[i]

条件筛选:挑出大额花销

妈妈以前查账有个习惯:拿红笔把超过 100 元的支出一圈一圈标出来,看看钱都花哪了。这个过程在 DataFrame 里叫“布尔筛选”——你先写一个判断规则,Pandas 会拿着规则去问每一行“你满足吗?”,满足的行留下,不满足的行退场。

写法很直接:df[df['金额'] > 100]。内层的 df['金额'] > 100 会生成一串 True/False 组成的“成绩单”,外层再把这串成绩单传回给 df,它就自动只显示“及格”的行。多个条件要组合时,用 &(且)、|(或)、~(非),每个条件都要加括号,就像写数学表达式时要加括号确定优先级。想判断“是否属于某几个类别”,可以用 df['类别'].isin(['食品','衣物']);想写得更像自然语言,还可以用 df.query('金额 > 100 and 类别 == "食品"')。

小明用这个技巧一秒就把妈妈的红笔活干完了——他甚至在 DataFrame 里玩出了更精细的花样:筛出“金额大于 100 且类别是衣物”的账,再按金额排序,立刻知道“球鞋 268 元”是全家最大的单笔开销。

S={ i∈M  ∣  amti>100  ∧  cati=食品 }\displaystyle S = \{\, i \in M \;\big|\; \text{amt}_i > 100 \;\land\; \text{cat}_i = \text{食品} \,\}
S 是最终留下的行号集合:每一行 i 必须同时满足金额大于 100 且类别为食品,两个条件用“且”连接。
  1. 候选集合 MM 是所有行号的集合,相当于账本的每一页都先进入候选池。
    M={0,1,…,m−1}\displaystyle M = \{0, 1, \ldots, m-1\}
  2. 金额条件检查第 i 行的金额列是否超过 100,这是第一道关卡。
    amti>100\displaystyle \text{amt}_i > 100
  3. 类别条件再检查第 i 行的类别是否为“食品”,这是第二道关卡。
    cati=食品\displaystyle \text{cat}_i = \text{食品}
  4. 逻辑与运算两道关卡用“且”连接,只有同时通过的行才被收进结果集合 S。
    S={i∣amti>100  ∧  cati=食品}\displaystyle S = \{i \mid \text{amt}_i > 100 \;\land\; \text{cat}_i = \text{食品}\}

行列增删:增删账目与栏目

账本是活的:今天忘记一笔要补上去,发现某笔记错了要划掉,想把“备注”栏改成“说明”栏,还想把账目按金额从大到小重新排一遍。DataFrame 对行列的增删改,本质上就是这些纸面动作的电子版。

新增一列最简单:直接 df['星级'] = ...,就像在账本最后加一栏并逐行填值;新增一行可以用 df.loc[新索引] = [...] 或 pd.concat 拼接。删除用 drop:删列写 df.drop(columns=['备注']),删行写 df.drop(index=['2025-01-01']),注意默认不修改原表,要加 inplace=True 或重新赋值才算真正撕掉那页。改栏目名用 df.rename(columns={'备注': '说明'});换顺序用 df.sort_values('金额', ascending=False),就像把账本按金额大小重新装订。

小明把账本收拾得井井有条:补上了漏记的早餐,划掉了重复记账的那行,把“备注”改名成“说明”,最后按金额排序后,账本读起来一目了然。他意识到,DataFrame 的增删改,要害是搞清楚“哪些操作会动原件、哪些只给你一份复印件”。

C′=C∪{cnew},R′=R∖{rdel}\displaystyle C' = C \cup \{c_{\text{new}}\}, \qquad R' = R \setminus \{r_{\text{del}}\}
增加列是对列集合做“并集”,删除行是对行集合做“差集”,一加一减都是集合操作。
  1. 列集合并集新列加入后,列集合从 C 变成 C’,多了一个新成员 c_new。
    C′=C∪{cnew}\displaystyle C' = C \cup \{c_{\text{new}}\}
  2. 行集合差集删除某行后,行集合从 R 变成 R’,去掉了被删掉的行 r_del。
    R′=R∖{rdel}\displaystyle R' = R \setminus \{r_{\text{del}}\}
  3. 不修改原则多数操作默认返回新表,就像复印后在复印件上改,也不动原件。
    df2=df.drop(index=[r])  ≠  df\displaystyle \text{df}_2 = \text{df.drop(index=[r])} \;\neq\; \text{df}

元素变换:逐格魔法

妈妈的账本记的是美元,可家里用人民币,她得拿计算器把每一格金额都乘以汇率 7.2。Pandas 给了三种“计算器”:map、apply、applymap。它们的区别就像三种干活方式——map 是查字典,给一个映射关系表,遇到 “Food“ 就翻译成 ”食品”;apply 是请一位师傅处理一整行或一整列;applymap 则是师傅挨个格子都摸一遍。

map 适合单列的“翻译”:df['类别'].map({'食品': 'Food'}) 把整列的值按字典替换。apply 更强大,传一个函数进去,它会自动把一行(或一列)作为一个整体交给函数处理:比如 df.apply(lambda row: row['金额'] * 7.2, axis=1) 按行算总金额。replace 则是精准替换某些具体值,比如把 "N/A" 全换成空值。这些操作都不会修改原表,返回的是新表,想要原表变化记得重新赋值。

小明给账本做了一次全面“换血”:用 map 把所有英文类别翻译成中文,用 apply 把美元金额全部换成人民币,再用 applymap 把整个表里的空字符串都清成标准空值。一本账本,几秒钟就完成了过去妈妈一下午的换算工作。

vij′=f(vij),new_coli=g(rowi)\displaystyle v'_{ij} = f(v_{ij}), \qquad \text{new\_col}_i = g(\text{row}_i)
第一个式子说“每个格子都被函数 f 加工一遍”;第二个式子说“每一行被函数 g 加工出新的列”。
  1. 元素级变换applymap 或 map 对每个格子 v 套用函数 f,得到新值 v'。
    vij′=f(vij)\displaystyle v'_{ij} = f(v_{ij})
  2. 行级变换apply 按行处理,把第 i 行整个交给函数 g,产出一个新列的值。
    new_coli=g(rowi)\displaystyle \text{new\_col}_i = g(\text{row}_i)
  3. 替换运算replace 本质是查表,将旧值直接换成新值,适合处理异常字符。
    v′=replace(v)  ⇒  ’N/A’↦NaN\displaystyle v' = \text{replace}(v) \;\Rightarrow\; \text{'N/A'} \mapsto \text{NaN}

多层索引:分册分类的账本

账本越来越厚,妈妈想了个办法:先按月份装订成 12 册,每册里再按类别分节——食品、衣物、账单各一小摞。这样查账时,先摸到月份那册,再翻到类别那一节,位置就锁定了。DataFrame 的多层索引(MultiIndex)就是干这个的。

用 df.set_index(['月份', '类别']) 可以把两列变成层级索引,之后 df.loc[('2025-01', '食品')] 就能精准取到“一月食品类”的所有记录,像在档案柜里先开抽屉再取文件夹。配合 groupby(['月份', '类别'])['金额'].sum(),可以把每个小分组的金额汇总起来,形成一张“月份 × 类别”的透视汇总表。想调整视角,用 unstack() 把最内层索引变成列,宽表变窄表、窄表变宽表,像把叠好的衣服从平铺改成竖放。

小明给十年账本建立了“年 → 月 → 类别”的三级目录,几秒就能回答妈妈的任何问题:“2023 年 6 月食品平均花了多少?”曾经要翻半小时账本的问题,现在一条命令就出来了。

vˉm,c=1∣Tm,c∣∑t∈Tm,cvt\displaystyle \bar{v}_{m,c} = \frac{1}{|T_{m,c}|} \sum_{t \in T_{m,c}} v_t
对每一组(月份 m、类别 c)内的所有记录求平均,得到按两层索引组织的数据。
  1. 分组 TT 表示“某月某类别”下所有账单记录的集合,比如一月食品类的那几笔。
    Tm,c={ t∣月份(t)=m  ∧  类别(t)=c }\displaystyle T_{m,c} = \{\, t \mid \text{月份}(t)=m \;\land\; \text{类别}(t)=c \,\}
  2. 组内求和把该组所有金额加总,相当于把这一小摞账单的金额汇总。
    ∑t∈Tm,cvt\displaystyle \sum_{t \in T_{m,c}} v_t
  3. 组内平均除以组内记录条数,得到该组的平均金额。
    vˉm,c=1∣Tm,c∣∑t∈Tm,cvt\displaystyle \bar{v}_{m,c} = \frac{1}{|T_{m,c}|} \sum_{t \in T_{m,c}} v_t

链式管道:一气呵成的流水线

妈妈的记账流程渐渐固定成五步:先记一笔,再标类别,然后换算货币,再筛出大额,最后汇总合计。过去她每步都要抄一遍账本,费纸费神。DataFrame 的链式操作,就是把这五步连成一条流水线——上一步的输出直接喂给下一步,中间不需要临时变量占地方。

链式写法的精髓是“方法后面接方法”:df.query('金额 > 0').sort_values('金额', ascending=False).assign(星级='高'),一条语句完成筛选、排序、加列。pipe 则是把整条流水线打包成一个函数再调用,适合复用固定的处理流程。此外,concat 像把几本账本沿行方向粘在一起,merge 像把两本账按“日期”这种公共键对接起来——前者叠放,后者拼合。

小明把妈妈的五步记成了五道工序,用链式写法一口气跑完。更妙的是,他把这套流程包成一个 pipe 函数,以后每月底只需调用一次,账本自动从“原始记录”变成“月度报告”。

out=fk(⋯f2(f1(df)))\displaystyle \text{out} = f_k\Big(\cdots f_2\big(f_1(\text{df})\big)\Big)
数据像流水线上的工件,依次经过 f1、f2……fk 多道工序,每道工序只做一件事。
  1. 第一道工序 f1df 先经过筛选函数 f1,丢掉不满足条件的行。
    df1=f1(df)\displaystyle \text{df}_1 = f_1(\text{df})
  2. 中间工序传递每道工序的输出成为下一道的输入,中间结果不单独保存。
    df2=f2(df1),df3=f3(df2)\displaystyle \text{df}_2 = f_2(\text{df}_1), \quad \text{df}_3 = f_3(\text{df}_2)
  3. 最终输出全部工序完成后得到最终结果,一条链子把流程串到底。
    out=fk(dfk−1)\displaystyle \text{out} = f_k\big(\text{df}_{k-1}\big)

实战避坑:账房先生的守则

老账房先生有个规矩:墨迹未干时不要复印,复印件上改了字别以为原件也跟着改了。DataFrame 里最著名的坑就是“视图与副本”:当你用链式筛选得到子表并给它赋值时,Pandas 会提示 SettingWithCopyWarning——它在警告你“你改的可能是复印件,原表没动”。

处理这个坑的守则是:想改原表就用 loc 明确操作,想生成新表就大方地重新赋值,别在中间态上动手脚。另外,账本里常有几格空白(NaN),用 fillna(0) 补零或用 dropna() 直接撕掉空行;数据类型不合适时用 astype() 转换,比如把金额从字符串转成浮点数,就像把“二十五元”改成 25.0。性能上,批量向量化永远优于逐行 for 循环——一个算盘一次拨一排珠子,远比一颗颗拨快。

小明吃了几次“改副本”的亏后,把守则贴在了电脑前:“要改就 loc 明确说,要新表就新变量。”他还把账本里的空白全部用均值补上,把金额列从字符串转成浮点,再用向量化计算代替了循环——妈妈的账本从此又快又稳,再也没出过“改了等于没改”的怪事。

v^ij={vˉj,vij=NaNvij,否则\displaystyle \hat{v}_{ij} = \begin{cases} \bar{v}_j, & v_{ij} = \text{NaN} \\ v_{ij}, & \text{否则} \end{cases}
缺失值处理的分支逻辑:遇到空值就用该列均值填补,否则保留原值。
  1. 判断是否缺失检查第 i 行第 j 列的格子是否为 NaN,即账本上有没有空着没填。
    vij=NaN\displaystyle v_{ij} = \text{NaN}
  2. 均值兜底若缺失,用第 j 列所有非空值的平均来填补。
    v^ij=vˉj\displaystyle \hat{v}_{ij} = \bar{v}_j
  3. 保留原值若没有缺失,原值保持不变。
    v^ij=vij\displaystyle \hat{v}_{ij} = v_{ij}

AI 应用场景

风控欺诈特征工程

阶段:特征工程 / 特征变换

解决的问题:风控建模前,原始交易流水是千万级的长表,需要按用户、按时间窗口聚合出“月均消费、最大单笔、高频时段”等特征。传统手工写循环统计,跑一次要几小时,且容易错漏。

具体案例:某支付平台风控团队用 Pandas 对用户交易流水做 groupby(['用户ID','月份']) 多层索引聚合,再配合 apply 计算每个窗口的金额均值、方差,最后用链式 pipe 把“清洗 → 聚合 → 变换”打包成固定流水线。原本 3 小时的特征计算压缩到 3 分钟,模型上线速度大幅提升。

电商 RFM 客户分群

阶段:数据分析 / 用户画像

解决的问题:运营需要把用户按“最近购买时间(R)、购买频率(F)、消费金额(M)”分成高价值、沉睡等群体。用 Excel 手工透视效率低,且难复用。

具体案例:某电商运营用 df.groupby('用户ID').agg({'最近购买': 'max', '订单数': 'count', '金额': 'sum'}) 算出三列,再用条件筛选给每个用户打上“高/中/低”标签,最后按标签分组统计人数和 GMV,直接产出了季度用户分层报告。

日志数据清洗 ETL

阶段:数据清洗 / 数据预处理

解决的问题:后端日志格式混乱,有空值、有类型错乱、有重复记录,直接进模型会崩。手写脚本解析易碎,需要标准化清洗流程。

具体案例:某数据工程师每天把服务器日志读入 DataFrame,用 drop_duplicates() 去重,fillna() 补齐缺失 IP,astype() 把时间戳转成 datetime 类型,再用 query 筛掉状态码非 200 的无效记录。清洗后的数据直接落库供推荐模型使用,任务从人工 1 小时缩短到定时 5 分钟。

Python 演示

python51 行
import pandas as pd # 1. 创建电子账本:字典的键变成列名df = pd.DataFrame({    'date': ['2025-01-01', '2025-01-02', '2025-01-05', '2025-01-06', '2025-01-07'],    'item': ['牛奶', '球鞋', '电费', '橙子', '话费'],    'category': ['食品', '衣物', '账单', '食品', '账单'],    'amount': [25, 268, 88, 12, 45]})print('原始账本:')print(df) # 2. 条件筛选:挑出金额大于50的大额支出big = df[df['amount'] > 50]print('\n大额支出(amount>50):')print(big) # 3. 多条件组合:食品类且金额小于30cheap_food = df[(df['category'] == '食品') & (df['amount'] < 30)]print('\n便宜食品:')print(cheap_food) # 4. 元素级变换:用map把类别翻译成英文df['cat_en'] = df['category'].map({'食品': 'food', '衣物': 'clothing', '账单': 'bill'})print('\n新增英文类别列:')print(df) # 5. apply逐行计算:每笔金额换算成美元(假设汇率7.2)df['usd'] = df['amount'].apply(lambda x: round(x / 7.2, 2))print('\n金额换算成美元:')print(df[['item', 'amount', 'usd']]) # 6. 多层索引+分组聚合:按月与类别统计金额合计df['month'] = df['date'].str[:7]summary = df.groupby(['month', 'category'])['amount'].sum()print('\n分组汇总(月 x 类别):')print(summary) # 7. 链式操作:一条龙完成筛选、排序、新增列pipeline = (df            .query('amount > 0')            .sort_values('amount', ascending=False)            .assign(level=lambda d: d['amount'].apply(lambda x: '高' if x > 100 else '低')))print('\n链式管道结果:')print(pipeline[['item', 'amount', 'level']]) # 8. 处理缺失值与类型转换df.loc[3, 'amount'] = Nonedf['amount'] = df['amount'].fillna(df['amount'].mean()).astype(float)print('\n缺失值用均值填补后:')print(df[['item', 'amount']])
目的:完整演示了 DataFrame 从创建、筛选、变换、分组到链式管道的全流程,相当于把“家庭账本”从手写升级为自动化分析工具。
  1. pd.DataFrame({...})用字典创建表,键名自动成为列名,就像画好表头的账本。
  2. df[df['amount'] > 50]布尔筛选,逐行问“金额是否大于 50”,满足的行留下。
  3. (df['category'] == '食品') & (df['amount'] < 30)多条件组合,两组括号缺一不可,像数学里的括号优先级。
  4. df['category'].map({'食品': 'food'})map 查字典式映射,把中文类别替换成英文。
  5. df['amount'].apply(lambda x: x / 7.2)apply 把函数作用到每一行的金额上,逐笔换算汇率。
  6. df.groupby(['month', 'category'])['amount'].sum()先按月份和类别分组,再求和,形成多层索引汇总。
  7. .query(...).sort_values(...).assign(...)链式调用,一条语句串起筛选、排序、新增列三道工序。
  8. df['amount'].fillna(df['amount'].mean())用该列均值填补缺失值,再用 astype 转成浮点类型。

常见误区

误区我经常写 `df.loc[2]` 想取“第 2 行”,但一直报 KeyError,明明数据有 10 行啊。难道 loc 不能用数字?

正解loc 认的是“标签”,不是“位置”。如果行索引恰好是 0、1、2 这样的整数,df.loc[2] 取的是“索引标签为 2 的那一行”;但若索引是日期或字符串,df.loc[2] 就会因为找不到标签而报错。想按位置取就用 iloc,它才是按排队序号取人的。就像“按学号找人”和“按第几个排队找人”绝不能混用。

为何常见:初学时 loc 和 iloc 长得太像,而 DataFrame 默认索引恰是整数,容易让人误以为 loc 也接受位置。

误区我写了 `df[df['amount'] > 100]['level'] = 'high'`,运行后没报错,但再看原表根本没有 level 列,数据丢了?

正解链式赋值先筛出一个“子表”,再给子表赋值,但这个子表可能只是原表的复印件(副本),改它不会动原件。Pandas 会提示 SettingWithCopyWarning。正确做法是用 df.loc[df['amount'] > 100, 'level'] = 'high',一次明确指定“行条件 + 列名”,在原表上直接操作。就像在复印件上写字之前,先确认你手里拿的是原件。

为何常见:Python 的链式写法太顺手,让人忘了每一步都可能生成新对象,从而误把副本当原件。

误区我以为 `df.apply(func)` 会按行处理每笔记录,结果发现函数收到的参数有点怪,算出来的结果和预期完全对不上。

正解df.apply(func) 默认 axis=0,是逐列应用——func 收到的是每一列 Series。想按行处理必须写成 axis=1。类比一下:axis=0 像“竖着切蛋糕,每刀切一列”,axis=1 像“横着切,每刀切一行”。方向反了,函数拿到的数据形状都变了。

为何常见:很多人直觉认为“遍历表格 = 一行行来”,没意识到 DataFrame 默认把列当“特征维度”优先处理。

误区我习惯了写 `for i in range(len(df)): if df.iloc[i]['amount'] > 50: ...` 来筛选,虽然慢点但思路清晰,应该没问题吧?

正解在 Pandas 里,向量化操作远比 Python 循环快几十上百倍。df[df['amount'] > 50] 一行就完成了循环要写五行的事,底层是 C 语言实现的高效运算。就像用计算器按一次“求和”键,而不是把每个数逐个敲进算盘。数据量一大,循环的性能差距会从“慢一点”变成“跑不完”。

为何常见:很多编程初学者把“能跑”当作“够好”,忽略了 Pandas 的向量化设计哲学,导致代码又长又慢。

面试题

Q1. 小明用 df.iloc[0:3] 切片,会得到账本中的哪几行?

单选题 · easy

  • A.第 1、2、3 行
  • B.第 0、1、2 行← 正确答案
  • C.第 0、1、2、3 行
  • D.第 3、4、5 行

答案:B。iloc 的切片规则是左闭右开,包含起始位置 0,但不包含结束位置 3,所以取到的是第 0、1、2 行。就像排队取号“从 0 号到 3 号之间”只包含 0、1、2 三个号。

Q2. loc 与 iloc 的核心区别是什么?

单选题 · easy

  • A.loc 只能选行,iloc 只能选列
  • B.loc 按标签或布尔条件,iloc 按整数位置← 正确答案
  • C.loc 返回副本,iloc 返回视图
  • D.两者完全等价

答案:B。loc 认的是行/列标签(如日期、姓名),也可传布尔条件;iloc 则严格按 0 开始的整数位置取值,就像“按名字找人”和“按排队序号找人”的区别。

Q3. df[df['amount'] > 100] 的返回结果是什么类型?

单选题 · easy

  • A.Series
  • B.DataFrame← 正确答案
  • C.单个数值
  • D.布尔数组

答案:B。内层 df['amount'] > 100 生成布尔 Series,外层 df[...] 用这个条件筛选行,返回的是满足条件的所有行和全部列,仍然是一张二维表,即 DataFrame。

Q4. df.apply(自定义函数) 默认 axis=0,意味着函数会被应用在什么上面?

单选题 · mid

  • A.每一行
  • B.每一列← 正确答案
  • C.每个单元格
  • D.整个表

答案:B。axis=0 表示沿“竖切方向”逐列处理,函数收到的参数是每一列的 Series;想按行处理需要写 axis=1。这是最常见的混淆点,就像“横着切”与“竖着切”的方向问题。

Q5. 想给账本 DataFrame 新增一列“备注”,最直接的正确写法是?

单选题 · mid

  • A.df.insert('备注')
  • B.df['备注'] = 值← 正确答案
  • C.df.add('备注')
  • D.df.append('备注', axis=1)

答案:B。Pandas 中新增列的标准方式是直接对列名赋值 df['备注'] = ...,就像在账本最后加一栏并逐行填值;insert 需要指定位置参数,add 是数值加法,append 已废弃且用于拼接行。

Q6. 已知账本索引是日期字符串,例如 "2025-01-01",想选出这一天的那行记录,以下哪些写法正确?

多选题 · mid

  • A.df.loc['2025-01-01']← 正确答案
  • B.df.iloc[0](假设这行恰好位于第 0 位)← 正确答案
  • C.df['2025-01-01']
  • D.df.at[0, 'date']

答案:AB。loc 按标签索引取值,所以传日期字符串正确;iloc 按位置取,若该行处于第 0 位也能取到。df['2025-01-01'] 是列选择语法,会找列名而非行;df.at[0, 'date'] 则是取“位置 0 行、date 列”的单元格,语义不符。

Q7. 运行链式赋值 df[df['amount'] > 100]['level'] = 'high' 后,发现原表没有变化且出现警告,最可能的原因是?

单选题 · mid

  • A.用 loc 修改单元格出错
  • B.对筛选出的子表(副本)赋值,原表未被修改← 正确答案
  • C.读取 CSV 时编码出错
  • D.多层索引导致缺数据

答案:B。先筛选再赋值的链式写法可能作用在 DataFrame 的副本上,Pandas 会发出 SettingWithCopyWarning 警告。正确做法是用 df.loc[条件, 'level'] = 'high' 在原表上直接操作,就像在复印件上写字,原件当然不会变化。

Q8. 关于多层索引(MultiIndex),下列说法正确的有?

多选题 · hard

  • A.set_index 可以把多列变成层级索引← 正确答案
  • B.groupby(['月份','类别']).sum() 能得到按月、类别的分组汇总← 正确答案
  • C.loc 无法对多层索引取值
  • D.stack / unstack 可以在宽表和窄表之间重塑数据← 正确答案

答案:ABD。set_index(['月份','类别']) 正是构建多层索引的常用方式;groupby 多列分组等价于按层级汇总;stack/unstack 用于行列维度的重塑。而 loc 完全可以传元组如 df.loc[('2025-01','食品')] 来取多层索引数据。

Q9. 执行 df.set_index(['月份','类别']) 后,想取“一月食品类”的金额数据,以下写法正确的是?

单选题 · hard

  • A.df.loc['一月', '食品']
  • B.df.loc[('一月', '食品')]← 正确答案
  • C.df.iloc['一月', '食品']
  • D.df['一月']['食品']

答案:B。多层索引要用元组作为 loc 的参数,df.loc[('一月', '食品')] 才能精确定位到两级索引的交汇处,就像先打开“一月”抽屉再抽“食品”文件夹。loc['一月','食品'] 在旧版本可能被解析为行列两个维度;iloc 只接受位置不接受标签。

Q10. 面对上百万行的交易数据,以下哪些做法能显著提升 Pandas 操作性能?

多选题 · extreme

  • A.用向量化运算替代 for 循环逐行处理← 正确答案
  • B.对取值有限的类别型列指定 dtype='category' 减少内存占用← 正确答案
  • C.把所有列都转成 object 类型方便灵活存取
  • D.尽量避免对每一行调用 apply 处理← 正确答案

答案:ABD。向量化是 Pandas 高性能的核心,循环和频繁 apply 会退化为 Python 逐行解释执行,性能损失巨大;category 类型能用整数映射大幅压缩重复字符串的内存;把列全转 object 反而丢失类型优化的机会,是反优化做法。