
1. 从“取数”说起为什么行列选择是数据分析的基石刚接触数据分析尤其是用Python的pandas库时很多人会卡在一个看似简单的问题上我有一张表DataFrame怎么把里面我需要的那几行、那几列的数据准确地“抠”出来这个问题几乎是你用pandas做的每一件事的起点。无论是想查看某个产品的销售数据还是想筛选出特定条件的用户或者只是想看看数据的“长相”你都得先学会怎么“取数”。我见过不少新手面对一个DataFrame要么用df.head()看个大概要么就一股脑儿print(df)结果数据太多刷屏真正需要的信息淹没在汪洋大海里。更常见的是他们知道用df[‘column_name’]取列但一到需要按条件取行或者取一个不规则的区域比如第1、3、5行和第A、C列就开始手忙脚乱写出一堆又慢又容易出错的循环代码。其实pandas提供了两套极其强大且直观的工具来应对这些场景基于标签的.loc和基于整数位置的.iloc。理解它们就像是拿到了打开数据宝库的两把不同钥匙。一把钥匙.loc通过“名字”行/列索引的标签来开门另一把钥匙.iloc通过“门牌号”从0开始的整数位置来开门。用对了钥匙取数就是一瞬间的事用错了要么拿不到数据要么拿到一堆乱七八糟的东西。这篇文章我就结合自己这些年处理各种奇形怪状数据表的经验把.loc和.iloc怎么用、什么时候用、有哪些坑掰开揉碎了讲清楚。我们会从最基础的连续区域选取讲到进阶的不连续跳跃式选取最后再聊聊那些官方文档里不一定写但实践中天天遇到的“骚操作”和避坑指南。目标很简单让你以后取数据时心里有谱手下不慌。2. 核心武器拆解.loc与.iloc的本质区别在深入具体用法之前我们必须先从根本上理解.loc和.iloc的设计哲学。这不是简单的语法差异而是两种完全不同的数据定位思想。混淆它们是绝大多数错误的根源。2.1.loc基于标签的精确制导.loc全称“location”它的核心是标签索引。你可以把它想象成一张地图行和列都有明确的“地名”标签。你要取数据就告诉它行和列的“地名”是什么。关键特性闭区间当使用切片时如df.loc[‘A’:‘C’]它包含起始标签‘A’和结束标签‘C’。这是和Python原生列表切片左闭右开最大的不同。使用方括号内的标签行和列的参数必须是DataFrame索引index和列名columns中实际存在的标签或者是布尔数组。索引类型无关无论你的行索引是字符串、日期还是其他类型.loc都适用。举个例子假设我们有一个学生成绩表import pandas as pd data { ‘数学‘: [90, 85, 78, 92], ‘语文‘: [88, 76, 95, 80], ‘英语‘: [85, 90, 88, 79] } df pd.DataFrame(data, index[‘张三‘, ‘李四‘, ‘王五‘, ‘赵六‘]) print(df)输出数学 语文 英语 张三 90 88 85 李四 85 76 90 王五 78 95 88 赵六 92 80 79现在我想取‘李四‘和‘王五‘的‘语文‘和‘英语‘成绩。用.loc我直接“点名”result df.loc[[‘李四‘, ‘王五‘], [‘语文‘, ‘英语‘]] print(result)输出语文 英语 李四 76 90 王五 95 88你看代码几乎就是自然语言的直译“给我行标签是[‘李四‘, ‘王五‘]的列标签是[‘语文‘, ‘英语‘]的数据。” 非常直观。2.2.iloc基于位置的快速定位.iloc全称“integer location”它的核心是整数位置索引。它不关心行和列叫什么名字只关心它们排在第几位从0开始计数。就像电影院找座位你只关心是第几排第几座而不在乎这个座位有没有名字。关键特性左闭右开区间当使用切片时如df.iloc[1:4]它包含位置1但不包含位置4即取第1, 2, 3行。这和Python列表的切片规则完全一致。使用整数行和列的参数必须是整数或整数列表/切片或布尔数组。与索引标签无关即使你把行索引改成乱七八糟的字符串df.iloc[0]永远返回第一行。用同样的数据我想取第1行到第2行即‘李四‘, ‘王五‘第1列到第2列即‘语文‘, ‘英语‘的数据。注意位置是从0开始的。result df.iloc[1:3, 1:3] # 行1:3 取位置1和2列1:3取位置1和2 print(result)输出语文 英语 李四 76 90 王五 95 88这里1:3是典型的Python切片取1和2不取3。所以.iloc的思维是“数格子”你需要对数据的位置顺序有清晰的概念。一个重要的心法当你需要根据数据在表格中的“物理位置”来选取时比如“不管名字我就要前100行”、“每隔5行取一次”用.iloc。当你需要根据数据的“业务含义”来选取时比如“选取‘张三‘到‘王五‘的数据”、“选取‘销售额‘这一列”用.loc。很多时候你的DataFrame索引本身就是有业务意义的比如用户ID、日期这时.loc会更方便。3. 连续数据选取切片艺术的实战连续选取指的是选取一个连续范围内的行或列比如“第5行到第20行”、“‘A‘列到‘D‘列”。这是最常用的操作.loc和.iloc在切片上的行为差异是必须熟练掌握的。3.1 行的连续切片使用.loc进行行切片# 选取从‘李四‘到‘赵六‘的所有行包含赵六 rows_loc df.loc[‘李四‘:‘赵六‘] print(rows_loc)输出数学 语文 英语 李四 85 76 90 王五 78 95 88 赵六 92 80 79注意‘李四‘和‘赵六‘都被包含在内了。如果你的索引是日期比如df.loc[‘2023-01-01‘:‘2023-01-31‘]它会非常方便地取出整个一月份的数据。使用.iloc进行行切片# 选取第1行到第3行不包含第3行即取第1,2行 rows_iloc df.iloc[1:3] print(rows_iloc)输出数学 语文 英语 李四 85 76 90 王五 78 95 88这里1:3遵循Python惯例取1和2。如果你想取到最后一行可以用df.iloc[1:]。3.2 列的连续切片列的切片逻辑完全一样只是需要在逗号后面指定列的部分。使用.loc进行列切片# 选取从‘语文‘到‘英语‘的所有列包含英语 cols_loc df.loc[:, ‘语文‘:‘英语‘] print(cols_loc)输出语文 英语 张三 88 85 李四 76 90 王五 95 88 赵六 80 79这里的:表示“所有行”。同样这是一个闭区间。使用.iloc进行列切片# 选取第1列到第3列不包含第3列即取第1,2列也就是‘语文‘和‘英语‘ cols_iloc df.iloc[:, 1:3] print(cols_iloc)输出与上面一致。3.3 行列同时连续切片将行和列的切片组合起来可以取出一个矩形区域。# 用.loc取‘李四‘到‘王五‘行‘数学‘到‘语文‘列 area_loc df.loc[‘李四‘:‘王五‘, ‘数学‘:‘语文‘] print(area_loc) # 用.iloc取第1到第2行位置1,2第0到第1列位置0,1 area_iloc df.iloc[1:3, 0:2] print(area_iloc)两者输出都是数学 语文 李四 85 76 王五 78 95实战经验处理默认整数索引的坑。很多时候我们读入数据比如从CSV后DataFrame的行索引是默认的从0开始的整数RangeIndex。这时df.loc[0:5]和df.iloc[0:5]看起来都能用但结果天差地别df.loc[0:5]因为是基于标签的闭区间会取出索引标签为0,1,2,3,4,5的行共6行。而df.iloc[0:5]是基于位置的左闭右开只取出位置0,1,2,3,4的行共5行。如果你的数据恰好有100行df.loc[0:99]能取全df.iloc[0:99]则会漏掉最后一行。一个简单的原则当行索引是纯整数时为了清晰和避免混淆我强烈建议使用.iloc进行位置切片。如果你确实想用.loc最好先通过df.index确认一下索引的标签到底是什么。4. 不连续数据选取灵活组合的进阶技巧现实中的数据选取需求很少是规规矩矩的矩形。更多时候我们需要的是“跳跃式”选取第1、3、5行或者‘A‘、‘C‘、‘F‘列。这就需要用到列表或数组来传递不连续的标签或位置。4.1 选取不连续的行或列使用列表进行不连续选取这是最直接的方法。无论是.loc还是.iloc你都可以传递一个标签列表或整数位置列表。# .loc: 选取‘张三‘和‘王五‘的行所有列 dis_rows_loc df.loc[[‘张三‘, ‘王五‘], :] print(dis_rows_loc) # .iloc: 选取第0行和第2行即张三和王五所有列 dis_rows_iloc df.iloc[[0, 2], :] print(dis_rows_iloc) # .loc: 选取所有行‘数学‘和‘英语‘列 dis_cols_loc df.loc[:, [‘数学‘, ‘英语‘]] print(dis_cols_loc) # .iloc: 选取所有行第0列和第2列即数学和英语 dis_cols_iloc df.iloc[:, [0, 2]] print(dis_cols_iloc)4.2 行列同时不连续选取行列参数都传入列表可以实现“点阵式”选取。# 选取‘张三‘、‘赵六‘的行 和 ‘语文‘、‘英语‘的列 point_loc df.loc[[‘张三‘, ‘赵六‘], [‘语文‘, ‘英语‘]] print(point_loc)输出语文 英语 张三 88 85 赵六 80 79# 选取第0、3行 和 第1、2列 point_iloc df.iloc[[0, 3], [1, 2]] print(point_iloc)输出相同注意位置对应关系第0行是‘张三‘第3行是‘赵六‘第1列是‘语文‘第2列是‘英语‘。4.3 混合选取切片与列表的组合你可以混合使用切片和列表实现更复杂的模式。例如取连续的行但不连续的列或者反过来。# 取‘李四‘到‘赵六‘的连续行但只取‘数学‘和‘英语‘这两列 mix_selection df.loc[‘李四‘:‘赵六‘, [‘数学‘, ‘英语‘]] print(mix_selection) # 取第1行到最后一行连续第0列和第2列不连续 mix_selection_iloc df.iloc[1:, [0, 2]] print(mix_selection_iloc)性能与可读性权衡。当需要选取的不连续项非常多时使用一个大列表可能会影响性能因为pandas需要为每个标签或位置进行查找。一种优化思路是如果这些不连续项在业务逻辑上可以归类考虑先通过条件筛选布尔索引得到一个较小的、连续的视图再进行操作。例如与其用.loc列出100个特定的用户ID不如先通过df[df[‘department‘] ‘Sales‘]筛选出销售部所有人再进行后续计算。代码可读性也会更好。5. 布尔索引基于条件的动态选取上面讲的都是“静态”选取你知道要哪几行哪几列。但更多时候我们的选取条件是动态的“找出所有数学成绩大于85分的学生”、“找出销售额低于平均值的产品”。这就需要用到布尔索引它是.loc的杀手级功能。布尔索引的本质是传递一个由True/False组成的布尔序列或数组pandas会返回所有对应True位置的数据。5.1 单条件筛选# 筛选出数学成绩大于85分的学生 condition df[‘数学‘] 85 print(‘条件序列‘, condition) high_math df.loc[condition] # 等价于 df[df[‘数学‘] 85] print(high_math)输出条件序列 张三 True 李四 False 王五 False 赵六 True Name: 数学, dtype: bool 数学 语文 英语 张三 90 88 85 赵六 92 80 795.2 多条件组合筛选使用与、|或、~非来组合条件注意每个条件必须用括号括起来因为运算符优先级问题。# 筛选数学大于85分 且 语文大于等于80分的学生 condition_complex (df[‘数学‘] 85) (df[‘语文‘] 80) good_students df.loc[condition_complex] print(good_students) # 筛选数学大于85分 或 英语大于88分的学生 condition_or (df[‘数学‘] 85) | (df[‘英语‘] 88) good_students_or df.loc[condition_or] print(good_students_or)5.3 结合列选取布尔索引通常用于筛选行但也可以同时指定列。# 找出数学大于85分的学生并只查看他们的‘语文‘和‘英语‘成绩 result df.loc[df[‘数学‘] 85, [‘语文‘, ‘英语‘]] print(result)避坑指南.loc与布尔索引的“链式赋值”问题。这是一个经典大坑。假设你想把数学成绩大于85分的学生的语文成绩都设为100。错误做法df[df[‘数学‘] 85][‘语文‘] 100。这行代码可能不会报错但很可能修改不成功或者产生一个SettingWithCopyWarning警告。因为df[df[‘数学‘] 85]返回的可能是一个视图view也可能是一个副本copy直接在上面赋值行为不确定。正确做法永远使用.loc进行条件赋值它保证在原始DataFrame上进行修改。# 正确做法 df.loc[df[‘数学‘] 85, ‘语文‘] 100 print(df)这个语法清晰明了在df这个对象上定位到行满足条件列名为‘语文‘的单元格赋值为100。这是pandas官方推荐的安全赋值方式。6..at与.iat针对单个标量的极速访问当你明确只需要获取或修改DataFrame中的单个单元格的值时使用.loc或.iloc有点“杀鸡用牛刀”的感觉。pandas提供了更快的专门方法.at和.iat。.at基于行、列标签访问单个值。.iat基于行、列整数位置访问单个值。它们的速度比.loc/.iloc快得多因为省去了处理切片、数组的 overhead。# 获取‘李四‘的‘语文‘成绩 score_at df.at[‘李四‘, ‘语文‘] print(f“使用.at获取的成绩{score_at}“) # 获取第1行第1列的值即‘李四‘的‘语文‘ score_iat df.iat[1, 1] print(f“使用.iat获取的成绩{score_iat}“) # 修改单个值 df.at[‘王五‘, ‘英语‘] 95 # 将王五的英语成绩改为95 df.iat[0, 0] 95 # 将第0行第0列张三的数学改为95 print(df)使用场景建议在循环中逐个修改或访问大量单元格时使用.at/.iat能带来显著的性能提升。但在绝大多数向量化操作即对整个Series或DataFrame进行操作的场景下应避免使用循环直接使用.loc的布尔索引或向量化函数如.apply(),.map()才是更“pandas”的做法效率也更高。7. 综合实战与疑难排坑理论讲完了我们来处理几个更贴近真实场景的例子并梳理一些常见的“坑”。7.1 案例处理带有多层索引MultiIndex的数据有时我们的数据索引不止一层比如既有“年份”又有“季度”。这时.loc的用法需要稍作调整。# 创建一个多层索引的DataFrame arrays [ [‘2022‘, ‘2022‘, ‘2023‘, ‘2023‘], [‘Q1‘, ‘Q2‘, ‘Q1‘, ‘Q2‘] ] index pd.MultiIndex.from_arrays(arrays, names(‘Year‘, ‘Quarter‘)) df_multi pd.DataFrame({‘Sales‘: [100, 150, 120, 180], ‘Profit‘: [20, 30, 25, 40]}, indexindex) print(df_multi)输出Sales Profit Year Quarter 2022 Q1 100 20 Q2 150 30 2023 Q1 120 25 Q2 180 40选取特定年份的所有数据print(df_multi.loc[‘2022‘])选取特定年份和季度的数据需要传入元组print(df_multi.loc[(‘2023‘, ‘Q1‘)])使用.xs方法进行交叉选取另一种常用方法# 选取所有‘Q1‘季度的数据无论年份 print(df_multi.xs(‘Q1‘, level‘Quarter‘))7.2 案例处理重复的索引标签如果DataFrame的行索引有重复标签使用.loc会返回所有匹配该标签的行。df_dup pd.DataFrame({‘A‘: [1, 2, 3, 4]}, index[‘x‘, ‘x‘, ‘y‘, ‘z‘]) print(df_dup) print(df_dup.loc[‘x‘]) # 返回两行这在某些场景下是需要的比如同一个用户有多条记录但如果你期望返回单行这就会导致错误。确保索引唯一性是数据清洗中常见的一步可以使用df.index.is_unique检查或用df.reset_index()重置索引。7.3 常见错误与排查KeyError错误使用.loc时传递了不存在的标签。解决检查df.index和df.columns确认标签是否存在注意大小写和空格。IndexingError错误使用.iloc时传递了超出范围的位置。解决检查DataFrame的形状df.shape确保位置索引在[0, 行数-1]和[0, 列数-1]范围内。SettingWithCopyWarning警告如前所述通常是因为链式索引赋值引起的。解决统一使用.loc[row_indexer, col_indexer] value的格式进行赋值。切片结果与预期不符最常见的是混淆了.loc的闭区间和.iloc的左闭右开区间。解决时刻提醒自己用的是哪种索引器对于整数索引的DataFrame优先使用.iloc进行位置切片以避免歧义。性能缓慢对大型DataFrame进行复杂的、基于列表的不连续选取或循环操作。解决尝试使用向量化操作布尔索引、query()方法或NumPy函数替代循环。如果必须循环考虑使用.itertuples()或.apply()方法它们通常比纯Python循环快。7.4 使用query()方法进行条件筛选对于复杂的条件筛选除了布尔索引df.query()方法提供了另一种更简洁尤其是列名包含空格时的写法它使用字符串表达式。# 等价于 df.loc[(df[‘数学‘] 85) (df[‘语文‘] 80)] result_query df.query(‘数学 85 and 语文 80‘) print(result_query)query()方法的优点是表达式清晰特别是当列名是字符串时不需要反复写df[‘xxx‘]。但它对于非常复杂的、依赖外部变量的条件可能不如布尔索引灵活。8. 从原理到实践理解视图与副本这是pandas中一个高级但至关重要的概念直接影响数据的修改和内存效率。当你使用.loc、.iloc或普通的[]索引时返回的可能是原始数据的一个视图也可能是它的一个副本。视图就像数据库的“视图”它只是原始数据的一个观察窗口。通过视图修改数据会直接影响原始数据。副本是原始数据的一份独立拷贝。修改副本原始数据不受影响。pandas并不总是明确告诉你返回的是视图还是副本这取决于具体的操作和数据的底层内存布局。一个黄金法则是如果你打算修改筛选出来的数据并且希望改动反映到原始DataFrame上那么最安全的方式是使用.loc或.iloc进行赋值操作而不是先获取一个子集再修改。例如下面两种写法第二种更安全、更推荐# 写法一可能不安全 sub_df df.loc[df[‘数学‘] 85] sub_df[‘语文‘] 100 # 这可能修改的是副本原始df不变并可能引发警告 # 写法二安全 df.loc[df[‘数学‘] 85, ‘语文‘] 100 # 直接定位并修改原始数据如果你不确定一个操作是否创建了副本或者你明确需要一份独立的数据进行操作例如后续的修改不应影响原始数据可以主动使用.copy()方法df_copy df.loc[df[‘数学‘] 85].copy() df_copy[‘语文‘] 100 # 放心修改与原始df无关理解视图和副本能帮助你避免很多难以调试的数据不一致问题。当你发现数据“莫名其妙”没被修改或者收到SettingWithCopyWarning时首先就应该检查这里。掌握.loc和.iloc不仅仅是记住了几个API的调用方式更是建立起了一套高效、准确操作数据的思维模式。从基于业务含义的标签选取到基于物理位置的位置切片再到灵活强大的布尔条件筛选这套工具组合能覆盖你日常数据分析中99%的数据提取需求。剩下的1%可能需要结合更高级的索引技巧如pd.IndexSlice用于多层索引但那些都是建立在此基础之上的延伸。下次当你面对一个DataFrame时先花一秒钟想清楚我要按“名字”找还是按“位置”找想清楚了代码自然就清晰了。