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]}, index=index) 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时,先花一秒钟想清楚:我要按“名字”找,还是按“位置”找?想清楚了,代码自然就清晰了。