Почему loc и iloc — это не просто "два способа выбрать данные"
Каждый, кто начинает работать с Pandas, сталкивается с этой дилеммой: loc или iloc? На первый взгляд разница кажется минимальной — один использует метки, другой позиции. Но на практике неправильный выбор приводит к ошибкам, которые сложно отловить, особенно когда датасет меняется динамически.
Представьте: вы написали скрипт, который прекрасно работал на тестовых данных. Запускаете на продакшене — и получаете KeyError или IndexError. Потому что кто-то добавил строку в начало файла, и все ваши позиционные индексы съехали.
В Pandas 2.6.0 (актуально на февраль 2026) обе функции получили оптимизации производительности, но логика работы осталась прежней. Если вы до сих пор путаетесь — эта статья для вас.
Создаём реалистичный датасет для экспериментов
Давайте возьмём не абстрактные цифры, а что-то похожее на реальную задачу. Допустим, у нас данные о продажах в интернет-магазине за первую неделю февраля 2026:
import pandas as pd
import numpy as np
# Создаём датасет с продажами
np.random.seed(42)
dates = pd.date_range('2026-02-01', periods=7, freq='D')
data = {
'order_id': [f'ORD-{i:04d}' for i in range(1001, 1008)],
'customer_id': np.random.choice(['C001', 'C002', 'C003', 'C004', 'C005'], 7),
'product': ['Ноутбук', 'Смартфон', 'Наушники', 'Планшет', 'Мышь', 'Клавиатура', 'Монитор'],
'category': ['Электроника', 'Электроника', 'Аксессуары', 'Электроника', 'Аксессуары', 'Аксессуары', 'Электроника'],
'price': [899.99, 599.50, 129.99, 449.00, 24.99, 89.99, 299.99],
'quantity': [1, 1, 2, 1, 3, 2, 1],
'discount': [0.1, 0.0, 0.15, 0.05, 0.2, 0.1, 0.0],
'date': dates
}
df = pd.DataFrame(data)
df['total'] = df['price'] * df['quantity'] * (1 - df['discount'])
# Устанавливаем order_id как индекс (это важно!)
df.set_index('order_id', inplace=True)
print(df.head())Получаем такой DataFrame:
| order_id | customer_id | product | category | price | quantity | discount | date | total |
|---|---|---|---|---|---|---|---|---|
| ORD-1001 | C004 | Ноутбук | Электроника | 899.99 | 1 | 0.10 | 2026-02-01 | 809.99 |
| ORD-1002 | C002 | Смартфон | Электроника | 599.50 | 1 | 0.00 | 2026-02-02 | 599.50 |
loc: когда метки важнее позиций
loc работает с метками индекса и названиями столбцов. Это как обращаться к человеку по имени, а не по номеру в списке.
1Базовый синтаксис loc
# Выбираем одну строку по метке индекса
single_row = df.loc['ORD-1003']
print(single_row['product']) # Наушники
# Выбираем несколько строк по списку меток
multiple_rows = df.loc[['ORD-1001', 'ORD-1004', 'ORD-1007']]
print(multiple_rows.shape) # (3, 8)
# Выбираем строки и столбцы
row_col = df.loc['ORD-1002', ['product', 'price', 'total']]
print(row_col)2Срезы с loc — ловушка для новичков
Вот где начинается путаница. С loc срезы работают по-другому:
# Срез по меткам — ВКЛЮЧИТЕЛЬНЫЙ
slice_inclusive = df.loc['ORD-1002':'ORD-1005']
print(len(slice_inclusive)) # 4 строки (1002, 1003, 1004, 1005)
# Сравните с обычным Python срезом списка:
list_slice = ['a', 'b', 'c', 'd', 'e'][1:4] # ['b', 'c', 'd'] - ИСКЛЮЧИТЕЛЬНЫЙ
# Срез по столбцам тоже работает
cols_slice = df.loc[:, 'product':'quantity']
print(cols_slice.columns.tolist()) # ['product', 'category', 'price', 'quantity']Включительность срезов в loc — это одновременно и удобство, и источник ошибок. Если метки не отсортированы, срез вернёт пустой DataFrame без ошибки.
3Булева маска — самая мощная фича loc
Здесь loc показывает свою настоящую силу:
# Все заказы дороже 500 рублей
expensive = df.loc[df['total'] > 500]
print(expensive.index.tolist()) # ['ORD-1001', 'ORD-1002']
# Заказы из категории "Электроника" с количеством больше 1
electronics_multiple = df.loc[(df['category'] == 'Электроника') & (df['quantity'] > 1)]
print(electronics_multiple) # Пусто — у нас нет таких
# Комбинируем с выбором столбцов
filtered_cols = df.loc[df['discount'] > 0, ['product', 'price', 'discount', 'total']]
print(filtered_cols)Булевы маски — это то, что делает Pandas таким выразительным. Но есть нюанс: маска должна быть того же размера, что и DataFrame. Если вы создали маску на основе подмножества данных — получите ошибку.
iloc: когда позиции надёжнее меток
iloc работает с целочисленными позициями. Это как обращаться "третий человек в шеренге", неважно как его зовут.
4Базовый синтаксис iloc
# Первая строка (позиция 0)
first_row = df.iloc[0]
print(first_row.name) # ORD-1001
# Последняя строка
last_row = df.iloc[-1]
print(last_row.name) # ORD-1007
# Несколько строк по позициям
positions = df.iloc[[0, 2, 4]]
print(positions.index.tolist()) # ['ORD-1001', 'ORD-1003', 'ORD-1005']
# Строки и столбцы по позициям
subset = df.iloc[1:4, 0:3] # Строки 1-3, столбцы 0-2
print(subset.columns.tolist()) # ['customer_id', 'product', 'category']Важное отличие: срезы в iloc исключительные, как в обычном Python. df.iloc[1:4] вернёт строки с позициями 1, 2, 3. Не 4.
5Когда iloc незаменим
Есть ситуации, где iloc — единственный разумный выбор:
# 1. Когда нужны первые/последние N строк
first_three = df.iloc[:3]
last_two = df.iloc[-2:]
# 2. Когда индексы — не строки, а что-то сложное
# (хотя в нашем случае это строки, но представьте даты или составные ключи)
# 3. Когда вы делаете алгоритмическую обработку
for i in range(0, len(df), 2): # Каждая вторая строка
row = df.iloc[i]
# Какая-то обработка
# 4. Когда метки индекса не уникальны (да, так бывает!)
df_duplicates = df.copy()
df_duplicates.index = ['A', 'B', 'A', 'C', 'D', 'E', 'F'] # Дублирующийся индекс
# df.loc['A'] вернёт ДВЕ строки
# df.iloc[0] и df.iloc[2] вернут разные строкиРеальная задача: анализ продаж по неделям
Давайте применим оба метода к практической задаче. У нас есть данные за неделю, нужно:
- Найти самый дорогой заказ
- Посчитать средний чек по категориям
- Выбрать все заказы со скидкой больше 10%
- Создать отчёт по первым трём дням
# 1. Самый дорогой заказ (используем loc для точности)
max_total_idx = df['total'].idxmax() # Находим метку индекса максимального значения
most_expensive = df.loc[max_total_idx]
print(f"Самый дорогой заказ: {most_expensive['product']} за {most_expensive['total']:.2f} руб.")
# 2. Средний чек по категориям (здесь loc не нужен, но покажем для сравнения)
# Способ 1: Без loc/iloc
avg_by_category = df.groupby('category')['total'].mean()
# Способ 2: С использованием loc для фильтрации
electronics_avg = df.loc[df['category'] == 'Электроника', 'total'].mean()
accessories_avg = df.loc[df['category'] == 'Аксессуары', 'total'].mean()
# 3. Заказы со скидкой > 10% (идеально для loc с булевой маской)
big_discounts = df.loc[df['discount'] > 0.1]
print(f"Заказы со скидкой >10%: {len(big_discounts)} шт.")
# 4. Отчёт по первым трём дням (здесь iloc логичнее)
first_three_days = df.iloc[:3].copy()
first_three_days['discount_amount'] = first_three_days['price'] * first_three_days['quantity'] * first_three_days['discount']
print(first_three_days[['product', 'total', 'discount_amount']])Типичные ошибки и как их избежать
Я видел эти ошибки в десятках проектов. Запомните их — сэкономите часы отладки.
| Ошибка | Почему происходит | Как исправить |
|---|---|---|
| KeyError при использовании loc с числом | Пытаетесь df.loc[0], но индекс — строки | Используйте df.iloc[0] или измените индекс |
| Пустой результат среза loc | Метки в срезе не отсортированы | Проверьте df.index.is_monotonic_increasing |
| SettingWithCopyWarning | Изменяете срез, а не оригинал | Используйте .copy() или .loc[:, 'новая_колонка'] = ... |
| Несоответствие размеров маски | Маска создана для подмножества данных | Убедитесь, что mask.shape[0] == df.shape[0] |
loc vs iloc: окончательный вердикт
После всего сказанного, вот простое правило:
- Используйте loc когда работаете с конкретными метками, делаете фильтрацию по условиям, или когда ваша логика зависит от содержимого данных, а не от их позиции.
- Используйте iloc когда нужны первые/последние N строк, когда делаете алгоритмическую обработку по позициям, или когда индексы ненадёжны (дублируются, меняются).
Но есть и третий путь: иногда лучше вообще не использовать ни loc, ни iloc напрямую. Например, для сложной фильтрации посмотрите 10 элегантных способов фильтрации Pandas DataFrame. А если работаете с огромными датасетами, возможно, вам пригодится техника streaming из Datasets.
Продвинутые трюки, о которых мало кто знает
Pandas постоянно развивается. Вот что появилось или стало стабильным в последних версиях:
# 1. Callable в loc (Pandas 2.1+)
# Можно передавать функцию для выбора строк
def high_value_orders(df):
return df['total'] > df['total'].median()
high_value = df.loc[high_value_orders]
# 2. Поддержка типов в iloc (Pandas 2.2+)
# Теперь можно явно указывать типы возвращаемых данных
from pandas.api.types import is_numeric_dtype
numeric_cols = [i for i, col in enumerate(df.columns) if is_numeric_dtype(df[col])]
numeric_data = df.iloc[:, numeric_cols]
# 3. Оптимизация производительности (Pandas 2.6+)
# Обе функции теперь используют более эффективные алгоритмы
# Особенно заметно на больших DataFrame с MultiIndexЧто делать, когда ничего не работает
Бывает такое: вы уверены, что код правильный, но loc или iloc возвращают не то, что ожидаете. Вот чек-лист:
- Проверьте тип индекса:
print(type(df.index)) - Убедитесь, что индекс уникален:
print(df.index.is_unique) - Посмотрите на фактические метки:
print(df.index.tolist()[:5]) - Проверьте, не изменился ли DataFrame после каких-то операций (особенно после группировок или слияний)
- Используйте
df._is_viewчтобы понять, работаете вы с view или копией
И последний совет: если вы часто путаете loc и iloc, возможно, проблема не в вас, а в дизайне индекса. Хорошо спроектированный индекс делает использование loc естественным. Плохой индекс заставляет постоянно использовать iloc и надеяться на удачу.
Кстати, если вы работаете с текстовыми данными и думаете, что Pandas — это сложно, попробуйте поработать с OLMocr 2 для чтения документов или LLM для чистки справочников. Тогда Pandas покажется райским садом простоты.
А теперь идите и напишите три строки кода с loc и три с iloc. Без практики эта статья — просто текст на экране.