小明的妈妈用一本手写账本记了十年家庭开支:每一笔花销占一行,抬头写着“日期、项目、类别、金额”。日子一久,账本厚得像砖头,想查“去年六月买球鞋花了多少钱”,得翻十几页。小明决定用 Python 的 Pandas 把这本旧账变成“电子账本”——这就是 DataFrame。你可以把它想象成一张会思考的表格:行是每一笔记录,列是信息的栏目,而 loc、iloc、筛选、apply 这些操作,就是给这张表装上的“查找目录”“红笔圈选”“批量换算”等十八般工具。读完这篇文章,你就能像账房先生拨算盘一样,行行列列随你摆布。
对 AI 从业者而言,DataFrame 是数据清洗与特征工程的“第一战场”,掌握它等于拿到了进入所有数据项目的入场券,能让你从“只会调库”升级为“随手捏数据”。
目录
可视化演示
通过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 列出每一行的行号。有了这本电子账本,接下来他就要学怎么快速翻页、怎么圈重点了。
- 行数 mdf.shape 返回的第 0 个数代表账本一共记了多少笔账,对应公式里的符号 m。
- 列数 ndf.shape 返回的第 1 个数代表每笔账记了几项信息,比如日期、金额,对应公式中的 n。
- 单元格 v第 i 行第 j 列的那个具体值,可以用 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 直接取单个格子——就像从账本里飞快地抽出一张便签。
- loc 标签段 ab:按标签取连续多行,结束标签 b 包含在内,适合“从 1 月到 3 月”这种有名字的区间。
- iloc 位置段 ij:按整数位置取连续多行,结束位置 j 不包含,适合“取前 3 行”这类无标签场景。
- 等价关系当行索引恰好是从 0 开始的连续整数时,两种写法指到的行几乎一致,只是区间习惯不同。
条件筛选:挑出大额花销
妈妈以前查账有个习惯:拿红笔把超过 100 元的支出一圈一圈标出来,看看钱都花哪了。这个过程在 DataFrame 里叫“布尔筛选”——你先写一个判断规则,Pandas 会拿着规则去问每一行“你满足吗?”,满足的行留下,不满足的行退场。
写法很直接:df[df['金额'] > 100]。内层的 df['金额'] > 100 会生成一串 True/False 组成的“成绩单”,外层再把这串成绩单传回给 df,它就自动只显示“及格”的行。多个条件要组合时,用 &(且)、|(或)、~(非),每个条件都要加括号,就像写数学表达式时要加括号确定优先级。想判断“是否属于某几个类别”,可以用 df['类别'].isin(['食品','衣物']);想写得更像自然语言,还可以用 df.query('金额 > 100 and 类别 == "食品"')。
小明用这个技巧一秒就把妈妈的红笔活干完了——他甚至在 DataFrame 里玩出了更精细的花样:筛出“金额大于 100 且类别是衣物”的账,再按金额排序,立刻知道“球鞋 268 元”是全家最大的单笔开销。
- 候选集合 MM 是所有行号的集合,相当于账本的每一页都先进入候选池。
- 金额条件检查第 i 行的金额列是否超过 100,这是第一道关卡。
- 类别条件再检查第 i 行的类别是否为“食品”,这是第二道关卡。
- 逻辑与运算两道关卡用“且”连接,只有同时通过的行才被收进结果集合 S。
行列增删:增删账目与栏目
账本是活的:今天忘记一笔要补上去,发现某笔记错了要划掉,想把“备注”栏改成“说明”栏,还想把账目按金额从大到小重新排一遍。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’,多了一个新成员 c_new。
- 行集合差集删除某行后,行集合从 R 变成 R’,去掉了被删掉的行 r_del。
- 不修改原则多数操作默认返回新表,就像复印后在复印件上改,也不动原件。
元素变换:逐格魔法
妈妈的账本记的是美元,可家里用人民币,她得拿计算器把每一格金额都乘以汇率 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 把整个表里的空字符串都清成标准空值。一本账本,几秒钟就完成了过去妈妈一下午的换算工作。
- 元素级变换applymap 或 map 对每个格子 v 套用函数 f,得到新值 v'。
- 行级变换apply 按行处理,把第 i 行整个交给函数 g,产出一个新列的值。
- 替换运算replace 本质是查表,将旧值直接换成新值,适合处理异常字符。
多层索引:分册分类的账本
账本越来越厚,妈妈想了个办法:先按月份装订成 12 册,每册里再按类别分节——食品、衣物、账单各一小摞。这样查账时,先摸到月份那册,再翻到类别那一节,位置就锁定了。DataFrame 的多层索引(MultiIndex)就是干这个的。
用 df.set_index(['月份', '类别']) 可以把两列变成层级索引,之后 df.loc[('2025-01', '食品')] 就能精准取到“一月食品类”的所有记录,像在档案柜里先开抽屉再取文件夹。配合 groupby(['月份', '类别'])['金额'].sum(),可以把每个小分组的金额汇总起来,形成一张“月份 × 类别”的透视汇总表。想调整视角,用 unstack() 把最内层索引变成列,宽表变窄表、窄表变宽表,像把叠好的衣服从平铺改成竖放。
小明给十年账本建立了“年 → 月 → 类别”的三级目录,几秒就能回答妈妈的任何问题:“2023 年 6 月食品平均花了多少?”曾经要翻半小时账本的问题,现在一条命令就出来了。
- 分组 TT 表示“某月某类别”下所有账单记录的集合,比如一月食品类的那几笔。
- 组内求和把该组所有金额加总,相当于把这一小摞账单的金额汇总。
- 组内平均除以组内记录条数,得到该组的平均金额。
链式管道:一气呵成的流水线
妈妈的记账流程渐渐固定成五步:先记一笔,再标类别,然后换算货币,再筛出大额,最后汇总合计。过去她每步都要抄一遍账本,费纸费神。DataFrame 的链式操作,就是把这五步连成一条流水线——上一步的输出直接喂给下一步,中间不需要临时变量占地方。
链式写法的精髓是“方法后面接方法”:df.query('金额 > 0').sort_values('金额', ascending=False).assign(星级='高'),一条语句完成筛选、排序、加列。pipe 则是把整条流水线打包成一个函数再调用,适合复用固定的处理流程。此外,concat 像把几本账本沿行方向粘在一起,merge 像把两本账按“日期”这种公共键对接起来——前者叠放,后者拼合。
小明把妈妈的五步记成了五道工序,用链式写法一口气跑完。更妙的是,他把这套流程包成一个 pipe 函数,以后每月底只需调用一次,账本自动从“原始记录”变成“月度报告”。
- 第一道工序 f1df 先经过筛选函数 f1,丢掉不满足条件的行。
- 中间工序传递每道工序的输出成为下一道的输入,中间结果不单独保存。
- 最终输出全部工序完成后得到最终结果,一条链子把流程串到底。
实战避坑:账房先生的守则
老账房先生有个规矩:墨迹未干时不要复印,复印件上改了字别以为原件也跟着改了。DataFrame 里最著名的坑就是“视图与副本”:当你用链式筛选得到子表并给它赋值时,Pandas 会提示 SettingWithCopyWarning——它在警告你“你改的可能是复印件,原表没动”。
处理这个坑的守则是:想改原表就用 loc 明确操作,想生成新表就大方地重新赋值,别在中间态上动手脚。另外,账本里常有几格空白(NaN),用 fillna(0) 补零或用 dropna() 直接撕掉空行;数据类型不合适时用 astype() 转换,比如把金额从字符串转成浮点数,就像把“二十五元”改成 25.0。性能上,批量向量化永远优于逐行 for 循环——一个算盘一次拨一排珠子,远比一颗颗拨快。
小明吃了几次“改副本”的亏后,把守则贴在了电脑前:“要改就 loc 明确说,要新表就新变量。”他还把账本里的空白全部用均值补上,把金额列从字符串转成浮点,再用向量化计算代替了循环——妈妈的账本从此又快又稳,再也没出过“改了等于没改”的怪事。
- 判断是否缺失检查第 i 行第 j 列的格子是否为 NaN,即账本上有没有空着没填。
- 均值兜底若缺失,用第 j 列所有非空值的平均来填补。
- 保留原值若没有缺失,原值保持不变。
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 演示
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']])
pd.DataFrame({...})用字典创建表,键名自动成为列名,就像画好表头的账本。df[df['amount'] > 50]布尔筛选,逐行问“金额是否大于 50”,满足的行留下。(df['category'] == '食品') & (df['amount'] < 30)多条件组合,两组括号缺一不可,像数学里的括号优先级。df['category'].map({'食品': 'food'})map 查字典式映射,把中文类别替换成英文。df['amount'].apply(lambda x: x / 7.2)apply 把函数作用到每一行的金额上,逐笔换算汇率。df.groupby(['month', 'category'])['amount'].sum()先按月份和类别分组,再求和,形成多层索引汇总。.query(...).sort_values(...).assign(...)链式调用,一条语句串起筛选、排序、新增列三道工序。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] 切片,会得到账本中的哪几行?
- 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 的核心区别是什么?
- A.loc 只能选行,iloc 只能选列
- B.loc 按标签或布尔条件,iloc 按整数位置← 正确答案
- C.loc 返回副本,iloc 返回视图
- D.两者完全等价
答案:B。loc 认的是行/列标签(如日期、姓名),也可传布尔条件;iloc 则严格按 0 开始的整数位置取值,就像“按名字找人”和“按排队序号找人”的区别。
Q3. df[df['amount'] > 100] 的返回结果是什么类型?
- A.Series
- B.DataFrame← 正确答案
- C.单个数值
- D.布尔数组
答案:B。内层 df['amount'] > 100 生成布尔 Series,外层 df[...] 用这个条件筛选行,返回的是满足条件的所有行和全部列,仍然是一张二维表,即 DataFrame。
Q4. df.apply(自定义函数) 默认 axis=0,意味着函数会被应用在什么上面?
- A.每一行
- B.每一列← 正确答案
- C.每个单元格
- D.整个表
答案:B。axis=0 表示沿“竖切方向”逐列处理,函数收到的参数是每一列的 Series;想按行处理需要写 axis=1。这是最常见的混淆点,就像“横着切”与“竖着切”的方向问题。
Q5. 想给账本 DataFrame 新增一列“备注”,最直接的正确写法是?
- A.
df.insert('备注') - B.
df['备注'] = 值← 正确答案 - C.
df.add('备注') - D.
df.append('备注', axis=1)
答案:B。Pandas 中新增列的标准方式是直接对列名赋值 df['备注'] = ...,就像在账本最后加一栏并逐行填值;insert 需要指定位置参数,add 是数值加法,append 已废弃且用于拼接行。
Q6. 已知账本索引是日期字符串,例如 "2025-01-01",想选出这一天的那行记录,以下哪些写法正确?
- 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' 后,发现原表没有变化且出现警告,最可能的原因是?
- A.用 loc 修改单元格出错
- B.对筛选出的子表(副本)赋值,原表未被修改← 正确答案
- C.读取 CSV 时编码出错
- D.多层索引导致缺数据
答案:B。先筛选再赋值的链式写法可能作用在 DataFrame 的副本上,Pandas 会发出 SettingWithCopyWarning 警告。正确做法是用 df.loc[条件, 'level'] = 'high' 在原表上直接操作,就像在复印件上写字,原件当然不会变化。
Q8. 关于多层索引(MultiIndex),下列说法正确的有?
- 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(['月份','类别']) 后,想取“一月食品类”的金额数据,以下写法正确的是?
- A.
df.loc['一月', '食品'] - B.
df.loc[('一月', '食品')]← 正确答案 - C.
df.iloc['一月', '食品'] - D.
df['一月']['食品']
答案:B。多层索引要用元组作为 loc 的参数,df.loc[('一月', '食品')] 才能精确定位到两级索引的交汇处,就像先打开“一月”抽屉再抽“食品”文件夹。loc['一月','食品'] 在旧版本可能被解析为行列两个维度;iloc 只接受位置不接受标签。
Q10. 面对上百万行的交易数据,以下哪些做法能显著提升 Pandas 操作性能?
- A.用向量化运算替代 for 循环逐行处理← 正确答案
- B.对取值有限的类别型列指定
dtype='category'减少内存占用← 正确答案 - C.把所有列都转成 object 类型方便灵活存取
- D.尽量避免对每一行调用 apply 处理← 正确答案
答案:ABD。向量化是 Pandas 高性能的核心,循环和频繁 apply 会退化为 Python 逐行解释执行,性能损失巨大;category 类型能用整数映射大幅压缩重复字符串的内存;把列全转 object 反而丢失类型优化的机会,是反优化做法。