上个月,同事让我帮他看一段数据处理的代码。逻辑很简单:从一份销售表里取出前 100 行,按地区筛选,然后更新某列的数值。他写完之后跑了一遍,结果对不上,但代码没报错,数据也没缺,就是数字不对。
他把代码发给我,我扫了一眼,问题出在两行上:
df.loc[0:100, 'amount'] = df.loc[0:100, 'amount'] * 1.1
他说:“取前 100 行,乘个 1.1,有什么问题?”
我说:“在 loc 里,0:100 取的是 101 行,不是 100 行。”
他愣了一下:“切片不都是左闭右开吗?”
在 Python 里是,在 pandas 的 loc 里不是。
这就是 loc 和 iloc 最容易让人栽跟头的地方。它们的语法看起来几乎一样,都是 df[行, 列] 的形式,但底层逻辑完全不同。一个是按标签找,一个是按位置找。一个切片包含右端点,一个不包含。一个接受任意标签类型,一个只接受整数。
混用它们,代码不会报错,但结果会悄悄偏掉。
最根本的区别:标签 vs 位置
loc 的全称是 location by label,按标签定位。iloc 的全称是 integer location,按整数位置定位。
用一个具体的 DataFrame 来看:
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'David'],
'score': [85, 92, 78, 95]
}, index=['a', 'b', 'c', 'd'])
这个 DataFrame 的索引是 'a'、'b'、'c'、'd',位置是 0、1、2、3。
df.loc['b'] 取的是索引为 'b' 的那一行,也就是 Bob。df.iloc[1] 取的是位置为 1 的那一行,也是 Bob。这里两者恰好指向同一行,因为索引标签和位置碰巧对上了。
但如果索引不是默认的 0、1、2、3 呢?比如把索引改成 [10, 20, 30, 40]:
df.index = [10, 20, 30, 40]
现在 df.loc[10] 取的是索引为 10 的那一行,也就是第一行 Alice。但 df.iloc[10] 会直接报 IndexError,因为只有 4 行,位置 10 不存在。
反过来,df.iloc[0] 取第一行 Alice,df.loc[0] 会报 KeyError,因为索引里没有 0 这个标签。
这就是核心差异:loc 认的是索引里真实存在的标签,iloc 认的是从 0 开始数的位置编号。两者只在索引恰好是 0、1、2、3……的时候才会重合。一旦索引被设置成日期、字符串、或者乱序的整数,它们就分道扬镳了。
切片行为:loc 包含右端点,iloc 不包含
Python 的切片 list[0:3] 取的是索引 0、1、2,不包含 3。这是大多数人的肌肉记忆。
iloc 遵循这个规则:
df.iloc[0:3] # 取位置 0、1、2,共 3 行
但 loc 不一样:
df.loc['a':'c'] # 取 'a'、'b'、'c',共 3 行,包含 'c'
loc 的切片是闭区间,左右都包含。这个设计其实有它的道理:标签是离散的、有意义的,你说“从 a 到 c”,自然应该把 c 也包括进去。而位置是连续的编号,Python 传统上就是左闭右开。
但问题在于,很多人写代码时不会刻意去想这个差异。尤其是当索引是整数的时候:
df = pd.DataFrame({'x': range(10)})
df.loc[0:5] # 取 0、1、2、3、4、5,共 6 行
df.iloc[0:5] # 取 0、1、2、3、4,共 5 行
一个取 6 行,一个取 5 行。如果你在循环里用 loc 做分批处理,每批的大小就会比你预期多一行。批次之间还会重叠,因为上一批的末尾和下一批的开头是同一行。
我见过有人在训练模型时用 loc 做数据切分,训练集和验证集有重叠样本,模型评估结果虚高,排查了很久才发现是切片包含右端点导致的。
布尔索引:loc 支持,iloc 不支持
loc 可以直接接受布尔 Series 或布尔数组:
df.loc[df['score'] > 80]
这行代码选出 score 大于 80 的所有行。loc 会把布尔序列对齐到索引上,True 的位置保留,False 的位置丢弃。
iloc 不接受布尔序列。df.iloc[df['score'] > 80] 会报错,因为它期望的是整数位置,不是布尔值。
想在 iloc 里实现同样的效果,得先算出满足条件的行位置:
import numpy as np
positions = np.where(df['score'] > 80)[0]
df.iloc[positions]
麻烦得多,而且容易出错。所以条件筛选基本都用 loc,这是它的主场。
整数索引的歧义
loc 和 iloc 最危险的场景,是索引本身就是整数,而且不是从 0 开始的连续整数。
df = pd.DataFrame({'x': [10, 20, 30]}, index=[1, 2, 3])
df.loc[1] 取的是索引为 1 的那一行,也就是 x=10。df.iloc[1] 取的是位置为 1 的那一行,也就是 x=20。
同样写 [1],一个拿第一行,一个拿第二行。如果代码里混用了 loc 和 iloc,或者从别处复制了一段代码但没改定位方式,结果就会错位。
更隐蔽的是 df[1] 这种写法。方括号直接索引在 pandas 里行为很复杂:对列名来说,它取列;对切片来说,它取行;对整数来说,它可能按位置也可能按标签,取决于索引类型。这种模糊性正是 pandas 官方建议永远明确使用 loc 或 iloc 的原因。
赋值时的坑
loc 和 iloc 在读取时的差异已经够多了,赋值时更危险。
用 loc 赋值:
df.loc[df['score'] > 80, 'grade'] = 'A'
这是标准做法,安全、清晰。
用 iloc 赋值:
df.iloc[0:3, 1] = 0
把位置 0 到 2 的行的第 1 列设为 0。这也有效。
但如果链式使用,比如 df['score'].iloc[0] = 100,pandas 会抛 SettingWithCopyWarning,因为 df['score'] 返回的是一个副本还是一个视图是不确定的,修改可能不会反映到原 DataFrame 上。
loc 在链式赋值时同样有这个问题:df['score'].loc[0] = 100 也会警告。
正确的做法始终是在一次 loc 或 iloc 调用里完成行列定位:
df.loc[0, 'score'] = 100
df.iloc[0, 1] = 100
不要先取列再取行,也不要先取行再取列。
一个真实场景
我之前处理一份订单表,索引是订单号,是一串不连续的整数,比如 1001、1003、1007、1012……。我需要做两件事:一是取出前 50 条记录做快速检查,二是把金额大于 1000 的订单标记为高价值。
第一件事我写了:
sample = df.loc[0:50]
结果取出来 51 条,而且因为索引不是从 0 开始的,loc[0:50] 实际匹配的是标签在 0 到 50 之间的行——但这个范围里可能一条都没有,因为订单号是从 1001 开始的。最后返回的是一个空 DataFrame。
正确的写法是:
sample = df.iloc[0:50]
按位置取前 50 条,和索引标签无关。
第二件事我写了:
df.loc[df['amount'] > 1000, 'level'] = 'high'
这个没问题,因为 loc 天然支持布尔索引。
但如果我错误地写成 df.iloc[df['amount'] > 1000, ...],就会直接报错。iloc 不接受布尔序列。
这两个操作放在一起,恰好展示了 loc 和 iloc 各自最适合的场景:按条件筛选用 loc,按位置取数用 iloc。
性能差异
loc 和 iloc 在性能上也有区别,虽然大多数时候可以忽略,但在大数据集上值得注意。
iloc 按位置定位,底层直接操作 numpy 数组的整数索引,速度最快。
loc 按标签定位,需要先在索引里查找标签对应的位置,多了一步哈希查找或二分查找。对于普通规模的 DataFrame,这个开销可以忽略。但对于几百万行的数据,频繁用 loc 做逐行访问会比 iloc 慢不少。
不过这个差异不应该成为选择依据。正确性永远优先于性能。该用 loc 的地方用 loc,该用 iloc 的地方用 iloc。如果确实遇到性能瓶颈,再考虑把索引转成位置、用 iloc 批量操作,或者直接上 numpy。
一张表说清楚
| 维度 | loc | iloc |
| 定位依据 | 索引标签 | 整数位置 |
| 切片右端点 | 包含 | 不包含 |
| 布尔索引 | 支持 | 不支持 |
| 接受类型 | 标签、标签列表、切片、布尔序列 | 整数、整数列表、整数切片 |
| 索引为整数时 | 按标签解释 | 按位置解释 |
| 赋值 | 支持 | 支持 |
| 越界行为 | KeyError | IndexError |
怎么避免踩坑
第一,永远明确写 loc 或 iloc,不要用 df[...] 做模糊索引。方括号的直接索引在 pandas 里行为太多变,读代码的人无法一眼判断是按列还是按行、是按标签还是按位置。
第二,索引不是默认整数时,优先用 iloc 做位置操作。只要你想表达的是“第几行”“前几行”“某几行”,就用 iloc。只要你想表达的是“索引为某个值的行”,就用 loc。
第三,切片时想一想右端点。用 loc 切片,问自己是否真的想包含右端点。用 iloc 切片,确认自己用的是左闭右开的习惯。
第四,条件筛选只用 loc。iloc 不支持布尔序列,硬要绕路只会让代码更难读。
第五,注意索引类型。如果索引是整数,务必清楚 loc 和 iloc 会给出不同结果。可以在代码里加注释说明索引的含义,或者干脆用 reset_index() 把索引变成默认的 0、1、2、3,消除歧义。
最后
loc 和 iloc 的区别,本质上是标签语义和位置语义的区别。一个回答“哪个标签”,一个回答“第几个”。
它们长得像,但规则不同:切片右端点一个包含一个不包含,布尔索引一个支持一个不支持,整数索引下同一个数字指向不同的行。
这些差异不会让代码报错,只会让结果偏掉。而数据处理的 bug,最难查的就是这种“不报错但不对”的问题。
写 pandas 的时候,每次敲下 loc 或 iloc,花一秒钟想一下:我要的是标签还是位置?这一秒钟,能省下后面几个小时的排查。