loc против iloc: метки и позиции

Два индексатора pandas работают в разных системах координат, и путаница между ними — источник большинства ошибок отбора.

loc отбирает по меткам индекса, iloc — по целочисленным позициям. Это две независимые системы координат.

Зачем вообще два индексатора

У DataFrame есть метки (имена столбцов и метки строк) и есть позиции (0, 1, 2…). Иногда вы знаете метку («строка с id=5», «столбец Цена»), иногда — позицию («первые три строки», «последний столбец»). Эти задачи решают разные инструменты, и явное разделение убирает двусмысленность, которая преследует обычные квадратные скобки.

Базовая форма у обоих одинаковая: df.loc[строки, столбцы] и df.iloc[строки, столбцы]. Разница — что вы кладёте внутрь: метки или позиции.

import pandas as pd

df = pd.DataFrame(
    {"max_speed": [1, 4, 7], "shield": [2, 5, 8]},
    index=["cobra", "viper", "sidewinder"],
)
#             max_speed  shield
# cobra               1       2
# viper               4       5
# sidewinder          7       8

df.loc["viper"]            # строка по метке  → Series (4, 5)
df.iloc[1]                 # строка по позиции 1 → та же Series
df.loc["cobra", "shield"] # одно значение по меткам → 2
df.iloc[0, 1]             # то же по позициям → 2

Главная ловушка: срезы по-разному включают конец

Это различие надо знать наизусть. Срез loc включает конец, срез iloc — нет. Причина логична: позиционный срез копирует поведение Python (list[0:2] — два элемента, конец не входит), а срез по меткам не знал бы, какой элемент «следующий за концом», поэтому конец включают.

df.loc["cobra":"viper"]   # cobra И viper — обе строки (конец включён)
df.iloc[0:2]              # позиции 0 и 1 — viper, конец НЕ включён

Оба примера выше возвращают одни и те же две строки, но loc до метки viper включительно, а iloc до позиции 2 не включая. Перепутать тут — значит молча захватить или потерять строку.

Почему так? Логика на самом деле стройная. Позиционный срез копирует поведение списков Python, где list[0:2] по соглашению означает «два элемента, начиная с нулевого» — это удобно для арифметики длин (len = stop - start). А вот для меток правило «не включая конец» было бы бессмысленным: что значит «до метки viper, не включая её», если pandas не знает, какая метка идёт «перед» viper в произвольном индексе из строк? Поэтому срез по меткам берёт обе границы — вы явно называете и начало, и конец диапазона. Запомнить помогает фраза: «метки — это имена, ты называешь обе границы; позиции — это длины, конец не входит».

Выбор столбцов

Для столбцов работают те же правила. Один столбец удобнее доставать обычными скобками, но loc/iloc дают полный контроль над строками и столбцами одновременно:

df["shield"]                    # один столбец → Series
df[["max_speed", "shield"]]     # список столбцов → DataFrame
df.loc[:, "max_speed":"shield"] # срез столбцов по меткам (включительно)
df.iloc[:, 0]                   # первый столбец по позиции
df.loc[df["shield"] > 4, "max_speed"]  # маска по строкам + выбор столбца

Двоеточие : означает «все по этой оси». df.loc[:, "a"] — все строки, столбец «a». Последняя строка — самый частый реальный паттерн: «возьми столбец max_speed только для строк, где shield > 4».

Булевы маски внутри loc

Булева маска — это Series из True/False той же длины, что и строки. loc оставляет строки, где True. Маски строятся сравнениями и комбинируются операторами & (и), | (или), ~ (не) — обязательно в скобках, потому что у этих операторов высокий приоритет.

Реализуем фильтрацию по маске на чистом Python, чтобы увидеть механику «оставить строки, где условие истинно»:

rows = [
    {"город": "Москва", "население": 13_000_000},
    {"город": "Казань", "население": 1_300_000},
    {"город": "Сочи", "население": 470_000},
    {"город": "Тверь", "население": 420_000},
]

# 1) строим булеву маску: True там, где население > 1 млн
mask = [r["население"] > 1_000_000 for r in rows]
print("маска:", mask)

# 2) оставляем строки, где маска True (это и делает loc)
selected = [r for r, keep in zip(rows, mask) if keep]
for r in selected:
    print(r["город"], r["население"])

Вывод:

маска: [True, True, False, False]
Москва 13000000
Казань 1300000

В pandas это пишется как df.loc[df["население"] > 1_000_000] — маска строится векторно, а loc применяет её ко всем столбцам сразу.

Маска — это не просто True/False, это выровненная по индексу Series. Поэтому когда вы пишете df.loc[df["a"] > 0], pandas сопоставляет маску с DataFrame по индексу, а не по позиции. Обычно это незаметно, но если маску построили из другого объекта с иным индексом, результат может удивить — pandas выровняет по меткам, и часть строк выпадет в NaN/выпадет вовсе. Отсюда практическое правило: стройте маску из того же DataFrame, к которому её применяете. Ещё один частый приём — callable внутри loc: df.loc[lambda d: d["a"] > 0]. Это особенно удобно в цепочках методов, где промежуточный DataFrame не имеет имени, но к нему нужно применить условие.

Когда использовать loc, а когда iloc

ЗадачаИнструмент
Строка/столбец по имени или idloc
Фильтр по условию (маска)loc
Первые/последние N строк, «каждая вторая»iloc
Доступ по позиции, когда метки не важныiloc
Присваивание в подмножество строк/столбцовloc (безопасно)

Подводные камни

  • Срезы ведут себя по-разному. loc["a":"c"] включает «c», iloc[0:3] — нет. Самая частая ошибка новичков.
  • Приоритет операторов в масках. df["a"] > 1 & df["b"] < 2 без скобок интерпретируется неправильно. Всегда: (df["a"] > 1) & (df["b"] < 2).
  • and/or не работают с масками. Питоновские and/or ждут одного булева значения, а маска — целый массив. Нужны поэлементные &/|.
  • iloc и булева Series. iloc принимает булев массив, но не выровненную булеву Series; для маски-Series используйте loc.

Лучшие практики

  • По умолчанию используйте loc — он явный и совпадает с тем, как вы думаете о данных («строка с id», «столбец Цена»).
  • Берите iloc только когда действительно важна позиция, а не смысл метки.
  • Для присваивания в подмножество всегда применяйте одну операцию df.loc[маска, столбец] = значение — это и корректно, и безопасно (см. урок про Copy-on-Write).

Итог

  • loc — метки, iloc — позиции; это разные системы координат.
  • Срез loc включает конец, срез iloc — нет.
  • Маски комбинируются через & | ~ в скобках, не через and/or.
  • Форма df.loc[строки, столбцы] даёт полный контроль над выборкой и присваиванием.
Проверьте себя
1. Что вернёт df.loc['a':'c'] по сравнению с df.iloc[0:3] для индекса ['a','b','c','d']?
AОба вернут строки a, b (без c)
Bloc вернёт a, b, c (включая c); iloc вернёт a, b, c (позиции 0,1,2)
Cloc вернёт a, b (без c); iloc вернёт a, b, c, d
DОба идентичны и всегда включают конец
2. Как правильно отфильтровать строки, где a > 1 И b < 2?
Adf[df['a'] > 1 and df['b'] < 2]
Bdf[(df['a'] > 1) & (df['b'] < 2)]
Cdf[df['a'] > 1 & df['b'] < 2]
Ddf.loc[df['a'] > 1 or df['b'] < 2]
3. Нужно взять первые 5 строк независимо от меток индекса. Что выбрать?
Adf.loc[0:5]
Bdf.iloc[:5]
Cdf.loc[:5]
Ddf[5]
Поддержать проект