1. 项目概述为什么loc和iloc是Pandas的灵魂如果你用过Pandas处理数据那对loc和iloc这两个方法一定不陌生。它们就像是打开数据宝库的两把钥匙一把叫“标签”一把叫“位置”。但很多朋友包括我刚开始用的时候都容易把它们搞混或者只停留在“loc用标签iloc用整数”这个浅层理解上。结果就是写出来的代码要么运行报错要么选出来的数据不是自己想要的调试半天效率极低。我见过不少同事在处理一个简单的数据筛选任务时因为对这两个方法的边界条件理解不透写出的df.loc[0:5]本意是想选前6行结果在索引不是默认整数时选出来的数据完全不对导致后续分析全盘皆错。这不仅仅是浪费了时间更可能因为数据错误得出误导性的结论。所以今天我就想结合我这些年踩过的坑和积累的经验把loc和iloc掰开了、揉碎了讲清楚。这不仅仅是两个函数的用法更是理解Pandas数据索引底层逻辑的基石。掌握了它们你才能说真正会“定位”Pandas里的数据无论是处理几十行的测试数据还是操纵百万行级别的商业数据都能得心应手。简单来说loc和iloc的核心区别在于索引的参照系不同。loc是基于轴标签index和columns的“名字”进行选择的而iloc是基于轴位置从0开始的整数位置进行选择的。这个根本性的差异衍生出了它们在切片、布尔索引、函数应用等几乎所有场景下的不同行为。接下来我们就从最基础的场景开始一步步深入到高级用法和那些容易踩坑的细节里。2. 核心概念辨析标签与位置的本质差异在深入代码之前我们必须先建立正确的心理模型。你可以把DataFrame想象成一张Excel表格。这张表格有行和列Pandas为它们分别设置了“坐标系统”。2.1 标签索引loc按“名字”找人loc使用的坐标系统是标签。行的标签就是索引index列的标签就是列名columns。这就像你用“姓名”和“部门”来定位一个同事一样。关键特性包含端点在使用切片时loc是包含起始和结束标签的。这是与Python原生列表切片以及iloc最显著的区别之一。灵活的数据类型索引和列名可以是整数、字符串、甚至日期时间类型。loc不关心它们的实际值是什么类型只把它们当作唯一的标识符。与索引顺序强相关loc的查找效率与索引的排序和唯一性有很大关系。如果索引是排序且唯一的Pandas会用更快的哈希查找否则可能需要遍历。举个例子假设我们有一个员工信息表import pandas as pd data { ‘姓名‘: [‘张三‘, ‘李四‘, ‘王五‘, ‘赵六‘], ‘部门‘: [‘技术部‘, ‘市场部‘, ‘技术部‘, ‘人事部‘], ‘年龄‘: [28, 32, 25, 35], ‘入职年份‘: [2019, 2017, 2020, 2015] } df pd.DataFrame(data) df.set_index(‘姓名‘, inplaceTrue) # 将‘姓名‘列设为索引 print(df)输出部门 年龄 入职年份 姓名 张三 技术部 28 2019 李四 市场部 32 2017 王五 技术部 25 2020 赵六 人事部 35 2015现在我想获取“李四”的信息并只看他的“部门”和“年龄”。用loc就是这样# 选取单个标签行 print(df.loc[‘李四‘]) # 输出一个Series部门-市场部 年龄-32 入职年份-2017 # 选取单个标签行和单个标签列 print(df.loc[‘李四‘, ‘部门‘]) # 输出标量‘市场部‘ # 选取单个标签行和多个标签列 print(df.loc[‘李四‘, [‘部门‘, ‘年龄‘]]) # 输出一个Series2.2 位置索引iloc按“座位号”找人iloc使用的坐标系统是整数位置。无论你的索引和列名是什么它只认从0开始计数的第几行、第几列。这就像电影院里的座位第1排第3座不管坐的是谁位置是固定的。关键特性不包含端点在使用切片时iloc遵循Python的“左闭右开”规则。df.iloc[0:3]选取的是第0, 1, 2行不包括第3行。只接受整数iloc的索引器必须是整数、整数列表、整数切片或布尔数组。传入字符串会直接报错。与数据顺序强相关iloc的性能非常稳定且高效因为它直接根据内存偏移量计算位置与索引内容无关。继续用上面的df# 选取第2行位置1因为从0开始 print(df.iloc[1]) # 输出李四的信息 # 选取第2行第1列位置[1, 0] print(df.iloc[1, 0]) # 输出‘市场部‘ # 选取前两行所有列 print(df.iloc[0:2, :]) # 等价于 df.iloc[:2] # 输出张三和李四的信息注意这里有一个新手超级易错点df.iloc[0:2]选取的是位置0和1的行而df.loc[0:2]如果索引是整数0,1,2,3选取的是标签为0,1,2的行。如果索引不是连续的整数或者有其他类型df.loc[0:2]的行为会完全不同。务必时刻清楚你用的是哪把“钥匙”。2.3 何时该用loc何时该用iloc这个选择没有绝对的对错但有一些最佳实践用loc当你的索引有意义时如果你的行索引是ID、日期、姓名等有业务含义的标签那么loc是更直观、更安全的选择。你的代码读起来就像在说“给我找‘张三’的数据”。用iloc当你只关心顺序位置时当你需要“前100个样本”、“每隔5行取一个”、“最后10行”时iloc是更直接的选择。它不依赖于索引的具体内容行为可预测。在循环或性能敏感代码中考虑ilociloc的纯整数索引通常比loc的标签查找更快尤其是在索引未排序或非唯一的情况下。修改数据时慎用链式赋值无论是loc还是iloc都尽量避免df[‘a‘].loc[0] 1这种链式索引赋值因为它可能触发SettingWithCopyWarning。正确的做法是使用df.loc[0, ‘a‘] 1。3. 单维度与多维度的数据选取实战理解了基本概念后我们来看看它们在实际选取数据时的各种花样。选取可以分为单点选取、多点选取和区间切片选取。3.1 选取单个元素标量这是最简单的操作目的是获取一个具体的值。使用loc:# 语法df.loc[行标签 列标签] value df.loc[‘李四‘, ‘年龄‘] print(value) # 输出32如果只传入行标签会返回该行的Series。如果行、列标签都传入则返回该位置的标量值。使用iloc:# 语法df.iloc[行位置 列位置] value df.iloc[1, 1] # 第2行第2列‘年龄‘列 print(value) # 输出32实操心得在Jupyter Notebook或交互式环境中当你用df.loc[‘某个标签‘]获取一个Series时显示可能很长。如果你只想快速看一眼某个特定列的值使用df.loc[‘某个标签‘, ‘特定列名‘]会更高效。另外当索引是日期时间类型时loc可以直接用字符串查询如df.loc[‘2023-01-01‘]非常方便。3.2 选取单行或单列Series选取整行或整列会返回一个Pandas Series对象。选取单行# loc 按标签选行 row_by_name df.loc[‘王五‘] # 返回‘王五‘这一行的Series # iloc 按位置选行 row_by_position df.iloc[2] # 返回第3行的Series (位置2)选取单列虽然更常见的列选取方式是df[‘列名‘]但loc/iloc也能做而且在与行选择结合时更统一。# loc 按标签选列 col_by_name df.loc[:, ‘年龄‘] # 选取‘年龄‘这一整列返回Series # 注意前面的冒号‘:‘表示选取所有行 # iloc 按位置选列 col_by_position df.iloc[:, 1] # 选取第2列位置1返回Series注意事项df[‘年龄‘]和df.loc[:, ‘年龄‘]在大多数情况下结果一样但后者是更明确的“标签索引”操作。当列名恰好是整数时df[1]会报错因为会被解释为选取列名为1的列而df.iloc[:, 1]则能正确选取第二列。为了代码清晰我个人的习惯是选取列时简单的用df[‘col‘]复杂的、需要行列组合的统一用loc/iloc。3.3 选取多行多列DataFrame子集这是数据分析中最常见的操作之一用于从原数据中裁剪出感兴趣的部分。使用列表进行多点选取# loc选取指定的多个行和列 sub_df df.loc[[‘张三‘, ‘赵六‘], [‘部门‘, ‘入职年份‘]] # 得到一个只包含张三和赵六的‘部门‘和‘入职年份‘的DataFrame # iloc选取指定的多个位置 sub_df df.iloc[[0, 3], [0, 2]] # 选取第1、4行第1、3列使用切片进行区间选取这里是核心差异点# 假设df的索引是默认的0,1,2,3 df_default_index df.reset_index() # 重置索引为0,1,2,3 # loc 切片包含结束点 slice_loc df_default_index.loc[1:3] # 选取标签为1,2,3的行共3行 print(slice_loc.shape) # 可能是 (3, 4) # iloc 切片不包含结束点 slice_iloc df_default_index.iloc[1:3] # 选取位置为1,2的行共2行 print(slice_iloc.shape) # (2, 4)高级切片与布尔索引结合布尔索引是过滤数据的利器loc与之结合是天作之合。# 选取‘技术部‘的所有员工 tech_employees df.loc[df[‘部门‘] ‘技术部‘] # df[‘部门‘] ‘技术部‘ 产生一个布尔Seriesloc用它来筛选行 # 选取‘技术部‘且年龄大于26的员工 tech_senior df.loc[(df[‘部门‘] ‘技术部‘) (df[‘年龄‘] 26)] # 注意多个条件要用括号括起来并用 与、|或、~非连接 # 在筛选的基础上再选取特定列 tech_senior_info df.loc[(df[‘部门‘] ‘技术部‘) (df[‘年龄‘] 26), [‘年龄‘, ‘入职年份‘]]iloc也可以结合布尔索引但通常需要先通过条件计算出布尔数组列表。# 获取‘年龄‘大于30的布尔序列 condition df[‘年龄‘] 30 # 将这个布尔序列转换为列表用于iloc不常用更推荐用loc做布尔筛选 positions condition[condition].index.tolist() # 获取为True的索引标签列表 # 但此时我们已经有了标签直接用loc更简单df.loc[condition]踩坑记录布尔索引中的条件运算符,|,~的优先级高于,等比较运算符。因此务必给每个条件加上括号否则会引发不可预知的错误或报错。(df[‘A‘] 1) (df[‘B‘] 2)是正确的df[‘A‘] 1 df[‘B‘] 2是错误的。4. 高级技巧与性能优化指南当你熟练掌握了基本选取后一些高级技巧可以让你代码更简洁、运行更高效。4.1 使用at和iat进行快速标量访问如果你百分之百确定只是要获取或设置一个单一单元格的值那么at标签和iat位置比loc和iloc更快。# 获取单个值 fast_value_loc df.at[‘李四‘, ‘年龄‘] # 类比 df.loc[‘李四‘, ‘年龄‘] fast_value_iloc df.iat[1, 1] # 类比 df.iloc[1, 1] # 设置单个值 df.at[‘李四‘, ‘年龄‘] 33 # 比 df.loc[‘李四‘, ‘年龄‘] 33 稍快 df.iat[1, 1] 33它们的语法更简洁并且在底层做了优化对于在循环中频繁访问单个元素的情况性能提升明显。但在可读性上loc/iloc更胜一筹因为大家更熟悉。我个人的建议是在明确需要性能优化的热点代码中使用at/iat在一般业务逻辑中使用loc/iloc以保持清晰。4.2 使用isin()进行多值筛选当需要筛选某列值属于一个特定集合的记录时isin()方法非常有用它比写多个OR条件简洁得多。# 筛选部门为‘技术部‘或‘市场部‘的员工 target_departments [‘技术部‘, ‘市场部‘] employees_in_target df.loc[df[‘部门‘].isin(target_departments)]4.3 利用query()方法进行表达式查询对于复杂的多条件筛选query()方法提供了一种更接近自然语言的写法尤其适合条件很多的时候。# 使用query筛选 result df.query(‘部门 “技术部“ and 年龄 25‘) # 等价于 df.loc[(df[‘部门‘] ‘技术部‘) (df[‘年龄‘] 25)]query()的优点是字符串表达式更清晰特别是列名包含空格或特殊字符时需要用反引号包裹。但它的性能在简单条件下可能略逊于直接的布尔索引且表达式中的变量引用需要注意使用符号。在可读性和简单性要求高时推荐使用。4.4 性能考量与最佳实践避免在循环中使用loc/iloc逐行处理这是Pandas性能的“头号杀手”。Pandas是向量化计算的库应该尽量用整个Series或DataFrame的操作代替循环。例如不要用for i in range(len(df)): df.loc[i, ‘new_col‘] ...而应该用df[‘new_col‘] df[‘old_col‘].apply(func)或向量化运算。索引的重要性对于loc一个排序且唯一的索引能极大提升查询速度。如果你的数据经常需要按某个键查询考虑使用set_index()将其设为索引。使用df.sort_index()对索引排序也能提升loc范围查询的性能。ilocvsloc性能在索引是简单整数且已排序的情况下两者性能差异不大。但如果索引是复杂的字符串或未排序iloc通常更快因为它是直接的位置寻址。使用.copy()避免链式赋值警告当你通过筛选得到一个子DataFrame并想修改它时Pandas可能会分不清你是想修改视图还是副本。最安全的做法是显式拷贝sub_df df.loc[condition].copy()。这样后续对sub_df的修改就不会触发SettingWithCopyWarning。5. 常见问题排查与避坑实录即使理解了原理在实际编码中还是会遇到各种奇怪的问题。下面是我总结的几个高频“坑点”及其解决方案。5.1 KeyError: ‘[label] not found in index‘这是使用loc时最常见的错误。原因你试图用loc访问一个不存在的行或列标签。排查检查标签拼写是否正确包括大小写和空格。使用df.index和df.columns查看当前确切的索引和列名。如果标签是动态生成的先使用label in df.index或label in df.columns进行判断。解决label_to_find ‘某个名字‘ if label_to_find in df.index: data df.loc[label_to_find] else: print(f“标签 ‘{label_to_find}‘ 不存在“) # 或者进行其他处理如使用默认值 data None5.2 使用切片时结果与预期不符问题df.loc[0:5]我以为选了6行为什么结果不对原因混淆了loc和iloc的切片语义或者索引不是连续的整数。loc[0:5]选取索引标签从0到5包含5的所有行。如果索引是[0,1,3,5,7]那么它会选取标签0,1,3,5。iloc[0:5]选取位置0到4不包含5的行即前5行。解决明确你的意图是按标签选还是按位置选如果索引是整数但不是连续的想按位置选前N行务必使用iloc[:N]。打印df.index查看索引的实际值。5.3 SettingWithCopyWarning 警告问题在对数据进行赋值时看到令人困惑的SettingWithCopyWarning。原因Pandas无法确定你的操作是在原始数据的视图view上还是副本copy上进行修改。在视图上修改可能会影响原始数据这是不明确且危险的行为。典型链式索引场景# 危险可能触发警告 df[df[‘年龄‘] 30][‘新列‘] ‘高级‘ # 或 df.loc[df[‘年龄‘] 30][‘新列‘] ‘高级‘ # 注意这里有两个‘[ ]‘解决使用单次loc赋值这是最推荐、最清晰的方式。df.loc[df[‘年龄‘] 30, ‘新列‘] ‘高级‘这条语句明确地通过loc在原始df上对满足条件的行进行赋值。需要副本时显式拷贝如果你确实想在一个筛选后的副本上操作并保留原始数据不变请显式使用.copy()。df_old df.copy() sub_df df_old.loc[df_old[‘年龄‘] 30].copy() # 显式创建副本 sub_df[‘新列‘] ‘高级‘ # 安全地在副本上修改5.4 布尔索引条件组合错误问题多个条件组合时结果不对或直接报错ValueError: The truth value of a Series is ambiguous...。原因使用了Python原生的and,or,not而不是Pandas的位运算符,|,~并且没有给条件加括号。错误示例# 错误1使用and/or df.loc[df[‘A‘] 1 and df[‘B‘] 2] # 报错 # 错误2条件未加括号 df.loc[df[‘A‘] 1 df[‘B‘] 2] # 逻辑错误先计算 1 df[‘B‘]正确示例# 正确使用 每个条件用括号包裹 df.loc[(df[‘A‘] 1) (df[‘B‘] 2)] # 复杂条件组合 df.loc[(df[‘部门‘] ‘技术部‘) ((df[‘年龄‘] 25) | (df[‘年龄‘] 35))]5.5 处理多层索引MultiIndex当DataFrame有行和列的多层索引时loc的用法需要扩展。示例# 创建一个简单的多层索引DataFrame arrays [[‘A‘, ‘A‘, ‘B‘, ‘B‘], [1, 2, 1, 2]] index pd.MultiIndex.from_arrays(arrays, names(‘first‘, ‘second‘)) df_multi pd.DataFrame({‘value‘: [10, 20, 30, 40]}, indexindex) # 使用loc选取 print(df_multi.loc[‘A‘]) # 选取第一层索引为‘A‘的所有行 print(df_multi.loc[(‘A‘, 1)]) # 选取第一层为‘A‘第二层为1的行 print(df_multi.loc[‘A‘, :]) # 同上选取所有列 print(df_multi.loc[(‘A‘, 1), :]) # 选取特定组合对于多层索引loc的索引器可以是一个元组(level1_key, level2_key, ...)。你也可以使用slice(None)来指定某一层全选或者使用pd.IndexSlice进行更复杂的切片。6. 综合案例一个完整的数据清洗与筛选流程让我们通过一个模拟的真实场景把loc和iloc的各种用法串起来。假设我们有一个销售数据表sales_df。import pandas as pd import numpy as np # 创建示例数据 np.random.seed(42) dates pd.date_range(‘20230101‘, periods100, freq‘D‘) products [‘Product_A‘, ‘Product_B‘, ‘Product_C‘] regions [‘North‘, ‘South‘, ‘East‘, ‘West‘] data { ‘Date‘: np.random.choice(dates, 200), ‘Product‘: np.random.choice(products, 200), ‘Region‘: np.random.choice(regions, 200), ‘Sales‘: np.random.randint(50, 500, 200), ‘Customer_ID‘: range(1000, 1200) # 假设有200个订单 } sales_df pd.DataFrame(data) # 为了演示我们故意制造一些缺失值和异常值 sales_df.loc[10:15, ‘Sales‘] np.nan sales_df.loc[50, ‘Sales‘] 10000 # 一个异常高值 print(“原始数据概览“) print(sales_df.head()) print(sales_df.info())任务清洗数据并分析“Product_A”在“North”地区2023年第一季度1-3月的销售情况。步骤1设置合适的索引由于我们要按日期和产品查询将Date和Product设为多层索引可能更高效。# 先按Date排序这对基于时间的切片很重要 sales_df.sort_values(‘Date‘, inplaceTrue) # 设置索引 sales_df.set_index([‘Date‘, ‘Product‘], inplaceTrue) print(“设置索引后的数据“) print(sales_df.head())步骤2处理缺失值和异常值# 1. 识别缺失值使用loc配合isna() missing_sales sales_df.loc[sales_df[‘Sales‘].isna()] print(f“有 {len(missing_sales)} 条销售记录缺失。“) # 用该产品在该地区的平均销售额填充缺失值假设策略如此 # 先临时重置索引方便分组计算 temp_df sales_df.reset_index() # 计算每个产品-地区的平均销售额忽略NaN mean_sales temp_df.groupby([‘Product‘, ‘Region‘])[‘Sales‘].mean() # 定义一个填充函数 def fill_na(row): if pd.isna(row[‘Sales‘]): key (row[‘Product‘], row[‘Region‘]) return mean_sales.get(key, row[‘Sales‘]) # 如果找不到对应均值返回原值NaN return row[‘Sales‘] # 应用填充这里为了演示我们创建一个新列。实际中可能直接修改 temp_df[‘Sales_Filled‘] temp_df.apply(fill_na, axis1) # 再设回索引 sales_df_filled temp_df.set_index([‘Date‘, ‘Product‘]) sales_df_filled.drop(columns‘Sales‘, inplaceTrue) sales_df_filled.rename(columns{‘Sales_Filled‘: ‘Sales‘}, inplaceTrue) # 2. 处理异常值假设我们认为销售额大于3000的是异常值 # 使用布尔索引定位异常值 outliers sales_df_filled.loc[sales_df_filled[‘Sales‘] 3000] print(f“识别到 {len(outliers)} 条异常销售记录“) print(outliers) # 策略将异常值截断为99%分位数 percentile_99 sales_df_filled[‘Sales‘].quantile(0.99) sales_df_filled.loc[sales_df_filled[‘Sales‘] 3000, ‘Sales‘] percentile_99 print(f“已将大于3000的销售额替换为99%分位数: {percentile_99:.2f}“)步骤3执行核心筛选任务# 目标筛选 Product_A 在 North 地区2023年第一季度的数据 # 注意索引现在是 (Date, Product) # 方法A使用loc的多层索引查询 # 我们需要筛选行Date在Q1且Product为‘Product_A‘列需要Region为‘North‘ # 对于行我们需要对第一层(Date)切片第二层(Product)精确匹配 start_date ‘2023-01-01‘ end_date ‘2023-03-31‘ # 由于索引是DatetimeIndexloc可以直接用字符串切片 # 但是我们的索引是MultiIndex需要用到pd.IndexSlice idx pd.IndexSlice target_data sales_df_filled.loc[idx[start_date:end_date, ‘Product_A‘], :] # 现在target_data包含了Q1所有Product_A的记录 # 再从这些记录中筛选Region为‘North‘的 target_data_north target_data.loc[target_data[‘Region‘] ‘North‘] print(f“Product_A在North地区Q1的销售记录有 {len(target_data_north)} 条。“) print(target_data_north.head()) # 方法B重置索引使用更直观的布尔索引当查询条件复杂时可能更清晰 # sales_df_reset sales_df_filled.reset_index() # target_data_b sales_df_reset[ # (sales_df_reset[‘Product‘] ‘Product_A‘) # (sales_df_reset[‘Region‘] ‘North‘) # (sales_df_reset[‘Date‘] ‘2023-01-01‘) # (sales_df_reset[‘Date‘] ‘2023-03-31‘) # ] # 两种方法结果一致选择哪种取决于个人习惯和索引结构。步骤4对筛选结果进行分析if not target_data_north.empty: # 计算基本统计量 total_sales target_data_north[‘Sales‘].sum() avg_sales target_data_north[‘Sales‘].mean() max_sale target_data_north[‘Sales‘].max() min_sale target_data_north[‘Sales‘].min() print(f“\n分析结果“) print(f“总销售额: {total_sales:.2f}“) print(f“平均每单销售额: {avg_sales:.2f}“) print(f“最高单笔销售额: {max_sale:.2f}“) print(f“最低单笔销售额: {min_sale:.2f}“) # 也许我们还想看销售额的分布使用iloc快速选取前几行查看 print(f“\n销售额前5的订单“) # 先按销售额降序排序 top_sales target_data_north.sort_values(by‘Sales‘, ascendingFalse) # 用iloc选取前5行 print(top_sales.iloc[:5]) else: print(“没有找到符合条件的数据。“)通过这个案例你可以看到loc和iloc如何与Pandas的其他功能如缺失值处理、分组聚合、时间序列切片协同工作完成一个从数据准备到分析的全流程。关键在于根据数据的结构索引和你的查询意图灵活选择最合适的“钥匙”。当索引设计得当时loc能让查询语句非常直观而当需要进行基于顺序或位置的操作时iloc则无可替代。