Булева индексация, query, isin и between
Помимо loc есть удобные инструменты фильтрации: query для читаемых условий, isin для множеств, between для диапазонов и at/iat для одиночных ячеек.
Булева индексация — отбор строк по маске True/False. query позволяет записать ту же маску строкой-выражением.
query: фильтр как читаемое выражение
Когда условий несколько, цепочка df[(df["a"] > 1) & (df["b"] < 2)] становится громоздкой: имя DataFrame повторяется в каждом сравнении. Метод query принимает условие строкой, где к столбцам обращаются по имени напрямую:
df.query("население > 1_000_000 and море == False")
df.query("город in ['Москва', 'Сочи']")
# подстановка переменной из окружения через @
porog = 500_000
df.query("население > @porog")
Внутри query можно писать and/or/not словами — это разбирает собственный парсер pandas, поэтому скобки и &/| не нужны. Внешние переменные подставляются через @имя. На больших таблицах query (с движком numexpr) может быть и быстрее за счёт оптимизации выражения, но главный его плюс — читаемость.
Когда же выбирать query, а когда обычную маску? Маска в коде (df[(...) & (...)]) выигрывает, когда условие строится динамически (собирается из переменных, функций, других Series) или когда нужен максимальный контроль и отладка по шагам — маску можно сохранить в переменную и посмотреть на неё. query выигрывает на статичных, длинных, «человеко-читаемых» условиях и в интерактивном анализе, где важна краткость. Многие используют оба: query для разведки в ноутбуке и явные маски в продакшен-коде, где условия зависят от параметров. Помните также, что query по умолчанию возвращает новый DataFrame (отфильтрованную копию), а не представление.
isin: проверка членства во множестве
Когда нужно «оставить строки, где значение входит в список», не пишите длинную цепочку (col == "a") | (col == "b") | .... Для этого есть isin:
cities = ["Москва", "Казань", "Сочи", "Тверь", "Уфа"]
allowed = {"Москва", "Сочи", "Уфа"} # множество для быстрой проверки
# .isin по смыслу — это "членство во множестве" для каждого элемента
mask = [city in allowed for city in cities]
selected = [c for c, keep in zip(cities, mask) if keep]
print("маска:", mask)
print("оставили:", selected)
Вывод:
маска: [True, False, True, False, True] оставили: ['Москва', 'Сочи', 'Уфа']
В pandas это df[df["город"].isin(allowed)]. Чтобы инвертировать («все, кроме этих»), оберните маску в ~: df[~df["город"].isin(allowed)].
between: попадание в диапазон
Вместо (df["x"] >= 10) & (df["x"] <= 20) можно написать короче и понятнее. По умолчанию between включает обе границы (inclusive="both"), но это настраивается:
df[df["цена"].between(1000, 5000)] # 1000 ≤ цена ≤ 5000
df[df["цена"].between(1000, 5000, inclusive="left")] # 1000 ≤ цена < 5000
at и iat: быстрый доступ к одной ячейке
loc/iloc умеют возвращать и срезы, и одиночные значения, но для доступа к одной ячейке есть более быстрые специализированные индексаторы: at (по меткам) и iat (по позициям). Они работают только со скаляром — строка и столбец, никаких списков и срезов — и за счёт этого ощутимо быстрее в циклах.
df.at["viper", "shield"] # одно значение по меткам — быстро
df.iat[1, 1] # одно значение по позициям — быстро
df.at["viper", "shield"] = 99 # быстрое присваивание в ячейку
Если вам нужно прочитать или записать ровно одну ячейку (например, в редком цикле), at/iat предпочтительнее loc/iloc. Но помните общий принцип: поэлементные циклы в pandas — крайняя мера, почти всегда есть векторный путь.
Отбор столбцов по условию и типу
Иногда фильтровать нужно не строки, а столбцы — например, «оставить только числовые» или «столбцы, имя которых начинается с date_». Для этого есть отдельные инструменты:
df.select_dtypes(include="number") # только числовые столбцы
df.select_dtypes(exclude="object") # всё, кроме строковых
df.filter(like="date") # столбцы, чьё имя содержит 'date'
df.filter(regex="^sum_") # столбцы по регулярке имени
select_dtypes особенно полезен в начале анализа: «дай мне все числовые столбцы, чтобы посчитать корреляции» или «покажи все строковые, чтобы почистить». Не путайте df.filter (фильтр по именам столбцов/строк) с булевым фильтром строк по значениям — это разные вещи, несмотря на похожее название.
Сравнение подходов
| Хочу | Инструмент |
| Фильтр по нескольким условиям, читаемо | query("...") |
| Фильтр по сложным маскам в коде | df[маска] / df.loc[маска] |
| Значение входит в список | isin([...]) |
| Значение в диапазоне | between(lo, hi) |
| Одна ячейка, максимально быстро | at / iat |
Подводные камни
- query и имена столбцов с пробелами. Если в имени пробел или спецсимвол, оберните его в обратные кавычки:
df.query("`общая сумма` > 100"). - Забытый @ в query. Без
@pandas будет искать столбец с таким именем, а не вашу переменную. - isin и NaN.
NaNне равен ничему, включая себя, поэтомуisin([np.nan])ведёт себя не так, как ожидается; пропуски ловите черезisna(). - at/iat только для скаляра. Передадите список или срез — получите ошибку; для этого есть
loc/iloc.
Лучшие практики
- Для длинных читаемых фильтров берите
query— код становится почти как SQL WHERE. - Членство во множестве — всегда
isin, а не цепочка==через|. - Диапазоны —
between, явно указываяinclusive, если границы важны. - Не пишите циклы с
at/iat, если задачу решает векторная операция или маска.
Итог
queryзаписывает фильтр строкой; переменные подставляются через@.isinпроверяет членство,~инвертирует маску.betweenзадаёт диапазон с настраиваемыми границами.at/iat— быстрый доступ к одной ячейке по меткам/позициям.