在 Pandas 中,MultiIndex 将行标签拆分成多个层级,这让基于标签的取值逻辑出现了分支。df.at 和 df.loc 虽然都支持通过行、列标签定位数据,但在 MultiIndex 下它们对行标签的要求并不完全相同。df.at 更强调标量级别的精确访问,而 df.loc 则提供了更丰富的索引表达能力。本文通过一个包含两层行索引的 DataFrame 对比两者的行为。

一、完整行标签访问的区别
先构造一个简单的 MultiIndex DataFrame:
import pandas as pd
index = pd.MultiIndex.from_tuples(
[('A', 1), ('A', 2), ('B', 1), ('B', 2)],
names=['group', 'sub']
)
df = pd.DataFrame(
{'value': [10, 20, 30, 40], 'score': [1.1, 2.2, 3.3, 4.4]},
index=index
)
print(df)
当使用完整行标签 ('A', 1) 时,df.at[('A', 1), 'value'] 和 df.loc[('A', 1), 'value'] 都会返回标量 10。从结果看,两者在完整标签访问上非常接近。
但底层机制不同。df.at 经过优化,只接受标量行标签和标量列标签,它会直接定位到内存中的对应位置。df.loc 则走通用索引器,可以解析更复杂的索引表达式。因此,在只需读取或写入单个元素时,df.at 通常比 df.loc 更快,尤其是在大 DataFrame 上循环访问时。
df.at 的严格性也体现在列参数上:df.at 必须同时给出行标签和列标签,不能只传一个参数获取整行。而 df.loc 可以只给行标签,返回 Series 或 DataFrame。例如 df.loc[('A', 1)] 会返回该行的两个列值。
二、部分索引与切片支持的差异
MultiIndex 的一个常见操作是按第一层索引筛选。df.loc 可以直接使用第一层标签 df.loc['A'],返回 group 为 A 的所有行,结果是一个 DataFrame。这是因为 df.loc 允许部分索引,未指定的层级会自动匹配所有值。
df.at 不支持部分索引。如果尝试执行 df.at['A'],会抛出 KeyError 或 ValueError,因为 df.at 要求提供的行标签必须完整覆盖所有索引层级。同样,在 MultiIndex 中使用切片,df.loc 可以写成 df.loc[('A', slice(None)), 'value'],返回 sub 为 1 和 2 的 value 列 Series;而 df.at 无法处理 slice(None),它会将切片对象视为非法标签。
这种差异的原因在于,df.loc 调用了通用的 _LocIndexer,内部会将元组拆解到各个索引层级,并支持切片、列表、布尔数组等。df.at 则基于 _AtIndexer,只实现了最简单的标量索引路径,跳过了解析和维度判断逻辑。因此,对于需要部分索引或多维切片的场景,应使用 df.loc。
更复杂的是,当 MultiIndex 未排序时,df.loc 进行部分索引可能触发 PerformanceWarning,或者在某些版本中直接报 KeyError,而 df.at 对完整元组标签通常不受排序影响,因为它只做点查找。不过,如果完整标签存在,df.loc 也能正常工作,只是部分索引依赖索引排序。
三、返回类型与写入行为
df.at 的返回类型非常单一:永远是标量。无论选择的行列对应的是整数、浮点数还是字符串,只要标签有效,df.at 都返回单个 Python 对象。这种确定性让它特别适合在循环中读取或写入单元格,例如逐行更新某个指标。
# 使用 df.at 更新单个值
df.at[('A', 1), 'value'] = 99
print(df.loc[('A', 1), 'value']) # 99
df.loc 的返回类型取决于索引表达式。如果行和列都精确到标量,则返回标量;如果只指定行,则返回 Series;如果行使用列表或切片,则返回 DataFrame。这种灵活性在批量操作中很有用,但在期望标量的地方可能引入类型不一致,比如将 df.loc[('A', 1)] 直接参与数学运算时,返回的是 Series,可能造成广播行为与预期不符。
在写入方面,df.at 执行的是单元素赋值,而 df.loc 可以一次修改多个元素。例如 df.loc['A', 'value'] = 0 会把 A 组所有行的 value 列置零。df.at 无法完成这种批量赋值,因为它不接受部分索引。因此,写入行为进一步体现了 df.at 的标量定位特性和 df.loc 的批量操作能力。
四、异常表现与选择建议
缺失标签下,两者的异常类型并不相同。df.at 通常会抛出 KeyError,提示标签不存在;df.loc 在精确行标签缺失时也抛 KeyError,但如果是部分索引,可能会返回空 DataFrame 或引发 KeyError,具体取决于索引结构和 Pandas 版本。需要特别小心的是,如果 MultiIndex 中包含重复标签,两者的行为会变得更加复杂,最好先通过 is_unique 检查索引唯一性。
一个常见的误区是以为 df.at 比 df.loc 只是语法缩写,在 MultiIndex 下可以互换。事实上,当需要按外层标签筛选或切片时,df.at 完全不适用。相反,当循环中只涉及完整行标签和列标签的单元格操作,df.at 能减少解析开销,并且代码意图更明确。
总结来说,选择规则可以简化为:需要单点读写时优先考虑 df.at;需要部分索引、切片、列表或布尔筛选,或者期望返回 DataFrame/Series 时,必须使用 df.loc。理解这两个访问器在 MultiIndex 下的解析差异,可以避免很多不必要的 KeyError 和数据类型困惑。
另外,如果计划对 MultiIndex 进行高频部分索引,建议先使用 sort_index() 对索引排序。虽然 df.at 不依赖排序,但 df.loc 的部分索引在排序后更稳定,也能减少性能警告。测试代码中可以用 df_sorted = df.sort_index() 来观察行为变化。
Pandas MultiIndexdf.atdf.loc修改时间:2026-09-24 08:50:03