Булева индексация, 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 — быстрый доступ к одной ячейке по меткам/позициям.
Проверьте себя
1. Как в df.query() подставить значение внешней переменной porog?
Adf.query('x > porog')
Bdf.query('x > @porog')
Cdf.query('x > {porog}')
Ddf.query('x > $porog')
2. Какой способ корректнее всего отобрать строки, где город входит в список из 5 значений?
Adf[(df['город']=='A') | (df['город']=='B') | ...]
Bdf[df['город'].isin(['A','B','C','D','E'])]
Cdf.query('город == [список]')
Ddf[df['город'].between('A','E')]
3. Когда стоит предпочесть at/iat вместо loc/iloc?
AДля выбора нескольких строк
BДля срезов столбцов
CДля доступа к одной ячейке по метке/позиции — это быстрее
DДля булевых масок
Поддержать проект