Статьи
April 24, 2023

Осваиваем query() и eval() в Pandas для лаконичной фильтрации и вычислений

Если вы работаете с данными в Python, библиотека pandas наверняка является одним из ваших основных инструментов. Она предоставляет удобные и гибкие средства для манипулирования табличными данными. Однако при выполнении фильтрации или вычислений стандартный синтаксис pandas с множеством скобок и логических операторов (&, |) может приводить к довольно громоздким и не всегда легко читаемым конструкциям кода.

Возможно, вы сталкивались с ситуациями, когда сложная логика отбора данных превращалась в трудночитаемый блок кода. Существует подход, который позволяет писать такие операции более чисто и лаконично, используя синтаксис, близкий к естественному языку Python. Речь идет о методах query() и eval(). В этой статье мы подробно разберем, как они работают и в каких случаях их использование может значительно улучшить ваш код.

Классика Pandas: когда стандартный синтаксис становится громоздким

Прежде чем мы перейдем к query() и eval(), рассмотрим пример – представим, что мы анализируем набор данных о фильмах. У нас есть название, жанр, год выпуска, рейтинг и продолжительность. Для иллюстрации создадим небольшой набор данных.

import pandas as pd
import numpy as np

data = {
    'title': ['The Shawshank Redemption', 'The Godfather', 'The Dark Knight', 'Pulp Fiction', 'Forrest Gump', 
              'Inception', 'The Matrix', 'Goodfellas', 'Spirited Away', 'Interstellar', 
              'Parasite', 'Joker', 'Whiplash', 'La La Land', 'Mad Max: Fury Road'],
    'genre': ['Drama', 'Crime', 'Action', 'Crime', 'Drama', 
              'Sci-Fi', 'Sci-Fi', 'Crime', 'Animation', 'Sci-Fi', 
              'Thriller', 'Drama', 'Drama', 'Musical', 'Action'],
    'year': [1994, 1972, 2008, 1994, 1994, 
             2010, 1999, 1990, 2001, 2014, 
             2019, 2019, 2014, 2016, 2015],
    'rating': [9.3, 9.2, 9.0, 8.9, 8.8, 
                    8.8, 8.7, 8.7, 8.6, 8.6, 
                    8.5, 8.4, 8.5, 8.0, 8.1],
    'duration_min': [142, 175, 152, 154, 142, 
                     148, 136, 146, 125, 169, 
                     132, 122, 106, 128, 120]
}
df_movies = pd.DataFrame(data)
Исходный DataFrame с фильмами: df_movies

Поставим задачу: отобрать фильмы, которые относятся к жанру "Sci-Fi" ИЛИ (являются "Action" И были выпущены строго после 2010 года). Дополнительно, все отобранные фильмы должны иметь рейтинг не ниже 8.5 И продолжительность менее 160 минут. После фильтрации рассчитаем средний рейтинг для полученной выборки.

Стандартный способ решения этой задачи в Pandas:

filtered_movies_complex = df_movies[
    (
        (df_movies['genre'] == 'Sci-Fi') | 
        ((df_movies['genre'] == 'Action') & (df_movies['year'] > 2010))
    ) &
    (df_movies['rating'] >= 8.5) &
    (df_movies['duration_min'] < 160)
]

average_rating_complex = filtered_movies_complex['rating'].mean()

# Выведем только нужные колонки для краткости
display(filtered_movies_complex[['title', 'genre', 'year', 'rating', 'duration_min']])
print(f"Средний рейтинг (стандартный метод): {average_rating_complex:.2f}")

Обратите внимание на конструкцию условия. Несколько уровней вложенности круглых скобок () для группировки логических выражений, квадратные скобки [] для доступа к столбцам и самому DataFrame, операторы & (логическое И) и | (логическое ИЛИ). С увеличением числа условий и их сложности, читаемость такого кода заметно снижается. Поддерживать и отлаживать подобные выражения становится сложнее.

Хотя этот метод абсолютно корректен и эффективен, особенно в простых случаях, для более комплексных запросов он может приводить к коду, который трудно воспринимать с первого взгляда. Именно для таких ситуаций query() и eval() предлагают более изящную альтернативу.

query(): элегантная фильтрация

Итак, при росте сложности условий стандартный механизм фильтрации в Pandas может порождать довольно ветвистые и не всегда очевидные конструкции. Метод DataFrame.query() предлагает альтернативный путь — он позволяет фильтровать строки DataFrame, используя выражения, записанные в виде строки. Это часто делает код более читаемым, так как строка запроса может быть очень похожа на то, как вы бы описали это условие на естественном языке или в стандартном Python.

Вместо того чтобы многократно обращаться к df['column_name'] и комбинировать условия с помощью & и |, вы просто пишете строку, где имена столбцов используются напрямую, а логика выражается словами and, or, not. Давайте разберемся, как это работает.

Основной синтаксис и операторы

Внутри строки, передаваемой в query(), вы можете использовать имена столбцов датафрейма так, как если бы это были обычные переменные. Pandas парсит эту строку и применяет условия к нашему датафрейму.

1. Обращение к столбцам и базовые сравнения:
Имена столбцов указываются напрямую, без кавычек. Если имя столбца содержит пробелы или специальные символы (кроме подчеркивания), его нужно заключить в обратные апострофы (backticks), например, `My Column Name`.

print("Фильмы с рейтингом > 8.8:")
display(df_movies.query('rating > 8.8'))
print("\nФильмы, выпущенные до 2000 года:")
display(df_movies.query('year < 2000'))
print("\nФильмы продолжительностью 142 минуты:")
display(df_movies.query('duration_min == 142'))

Поддерживаются все стандартные операторы сравнения: ==, !=, >, <, >=, <=.

2. Логические операторы
Для комбинирования условий можно использовать логические операторы and, or, not. Они работают так же, как их аналоги в Python. Также допустимо использовать & для "И" и | для "ИЛИ", но and и or часто делают выражение более читаемым. Оператор not используется для отрицания условия.

print("Жанр 'Sci-Fi' AND рейтинг >= 8.7:")
display(df_movies.query("genre == 'Sci-Fi' and rating >= 8.7"))
print("\nЖанр 'Drama' OR 'Musical':")
display(df_movies.query("genre == 'Drama' or genre == 'Musical'"))
print("\nНЕ жанр 'Animation':")
display(df_movies.query("not genre == 'Animation'"))
# Или, что то же самое: display(df_movies.query("genre != 'Animation'"))

Круглые скобки () используются для группировки условий и задания порядка их выполнения, точно так же, как в Python. Например, df_movies.query("(genre == 'Sci-Fi' or genre == 'Action') and year > 2000").

3. Операторы принадлежности in и not in
Эти операторы позволяют проверить, содержится ли значение столбца в заданном списке. Это аналог метода .isin() в стандартном Pandas.

target_genres = ['Crime', 'Thriller', 'Sci-Fi']
print(f"Жанры в списке {target_genres}:")
display(df_movies.query("genre in ['Crime', 'Thriller', 'Sci-Fi']"))
print("\nГод выпуска НЕ 1994 и НЕ 2019:")
display(df_movies.query("year not in [1994, 2019]"))

Строковые значения внутри списка также должны быть в кавычках (одинарных или двойных).

4. Сравнение со списком (как альтернатива in для == и !=):
Если правая часть оператора == или != является списком, это работает аналогично in и not in соответственно.

# Эквивалентно "genre in ['Drama', 'Musical']"
print("genre == ['Drama', 'Musical']")
display(df_movies.query("genre == ['Drama', 'Musical']"))
# Эквивалентно "year not in [1994, 2019]"
print("\nyear != [1994, 2019] (эквивалент not in):")
display(df_movies.query("year != [1994, 2019]"))

Хотя это работает, использование операторов in и not in обычно более явно передает намерение при работе со списками значений.

Это основы синтаксиса query(). Как видите, он довольно интуитивен и позволяет формировать условия фильтрации более естественным образом, используя уже знакомые нам данные о фильмах. Далее мы рассмотрим, как использовать в запросах внешние переменные Python.

Доступ к внешним переменным через @

Часто возникает необходимость использовать в запросе query() значения, которые хранятся в сторонних переменных, а не являются статическими литералами внутри строки запроса. Например, у нас может быть пороговое значение, вычисленное ранее, или список значений, определенный где-то в коде.

Для ссылки на такие переменные внутри строки запроса используется префикс @.

# Использование числовой переменной
min_rating_threshold = 8.8
print(f"Фильмы с рейтингом выше {min_rating_threshold}:")
display(df_movies.query("rating > @min_rating_threshold"))))

# Использование строковой переменной
target_genre_variable = 'Drama'
print(f"\nФильмы жанра '{target_genre_variable}':")
display(df_movies.query("genre == @target_genre_variable"))
# Важно: саму переменную target_genre_variable не нужно заключать в кавычки в строке запроса,
# в отличие от строковых литералов, как 'Sci-Fi'.
# Использование списка в качестве переменной
years_to_exclude = [1994, 2008, 2019]
print(f"\nФильмы, год выпуска которых НЕ в списке {years_to_exclude}:")
display(df_movies.query("year not in @years_to_exclude"))
# Использование переменной в более сложном выражении
release_year_cutoff = 2000
min_duration_cutoff = 130
required_genre = 'Sci-Fi'

print(f"\n{required_genre} фильмы, выпущенные после {release_year_cutoff} И продолжительностью более {min_duration_cutoff} минут:")
query_complex_vars = """
genre == @required_genre and \
year > @release_year_cutoff and \
duration_min > @min_duration_cutoff
"""
# Обратите внимание на переносы строк (\)
display(df_movies.query(query_complex_vars))plex_vars))

Важно помнить:

  • Переменная, на которую вы ссылаетесь с помощью @, должна существовать в той области видимости, где вызывается query().
  • Нельзя использовать @ для доступа к атрибутам объектов или элементам словарей напрямую в строке запроса типа @my_object.attribute или @my_dict['key']. Если вам нужно такое значение, сначала присвойте его отдельной локальной переменной, а затем используйте ее с @.
    Например, НЕПРАВИЛЬНО: df.query('year > @config.min_year')
    ПРАВИЛЬНО:
config = {'min_year': 2000}
current_min_year = config['min_year']
df.query('year > @current_min_year')

Использование префикса @ делает query() еще более гибким, позволяя интегрировать его с остальной логикой вашего Python-кода. Теперь, когда мы разобрались с основами синтаксиса и использованием переменных, давайте вернемся к нашему первоначальному "проблемному" примеру и посмотрим, как query() его преобразит.

Улучшаем изначальный пример

Вернёмся к задаче, которую мы сформулировали в самом начале. Напомню, нам нужно было отобрать фильмы, которые относятся к жанру "Sci-Fi" ИЛИ (являются "Action" И были выпущены строго после 2010 года). Дополнительно, все отобранные фильмы должны иметь рейтинг не ниже 8.5 И продолжительность менее 160 минут. Затем мы рассчитывали средний рейтинг для полученной выборки.

Вот как выглядел наш "классический" вариант решения:

filtered_movies_complex_classic = df_movies[
    (
        (df_movies['genre'] == 'Sci-Fi') | 
        ((df_movies['genre'] == 'Action') & (df_movies['year'] > 2010))
    ) &
    (df_movies['rating'] >= 8.5) &
    (df_movies['duration_min'] < 160)
]
average_rating_complex_classic = filtered_movies_complex_classic['rating'].mean()

А теперь применим query() для решения той же задачи:

query_string_for_movies = """
((genre == 'Sci-Fi') or \
    (genre == 'Action' and year > 2010)) and \
rating >= 8.5 and \
duration_min < 160
"""

filtered_movies_with_query = df_movies.query(query_string_for_movies)
average_rating_with_query = filtered_movies_with_query['rating'].mean()

Вывод обоих блоков кода будет идентичен.

Сравните строку query_string_for_movies с выражением в квадратных скобках в классическом подходе. Версия с query() выглядит заметно чище:

  • Нет многократных повторений df_movies['column_name'].
  • Логические операторы and и or более привычны, чем & и |.
  • Общая структура запроса легче воспринимается визуально, особенно когда условия становятся более разветвленными.

Можно сделать этот запрос еще более гибким, если вынести пороговые значения в переменные:

# Используем переменные с @
min_rating_val = 8.5
max_duration_val = 160
action_year_gt_val = 2010
genre1_val = 'Sci-Fi'
genre2_val = 'Action'

query_with_vars = """
((genre == @genre1_val) or \
    (genre == @genre2_val and year > @action_year_gt_val)) and \
rating >= @min_rating_val and \
duration_min < @max_duration_val
"""

filtered_movies_query_vars = df_movies.query(query_with_vars)
avg_rating_query_vars = filtered_movies_query_vars['rating'].mean()

Этот вариант сохраняет читаемость и добавляет гибкости, так как значения для фильтрации могут легко меняться без необходимости редактировать саму строку запроса.

Как видите, query() позволяет писать более декларативные и менее "шумные" условия фильтрации, что положительно сказывается на читаемости и поддерживаемости кода, особенно в сложных случаях.

query() в цепочках: элегантные последовательности операций

Одно из преимуществ query() (как и многих других методов Pandas) заключается в том, что он возвращает новый DataFrame (или копию, если не используется inplace=True). Это позволяет легко выстраивать цепочки операций, где результат одного вызова query() передается на вход следующему методу, например, другому query(), assign() для создания новых столбцов, или агрегирующим функциям.

Такой подход часто делает код более читаемым и линейным, избегая необходимости создавать множество промежуточных переменных для хранения результатов каждого шага.

Пример 1: Последовательная фильтрация

Допустим, мы хотим сначала отобрать все фильмы жанра "Sci-Fi", а затем из этой выборки найти те, что были выпущены после 2000 года и имеют рейтинг выше 8.5.

# Цепочка из двух query()
sci_fi_highly_rated_post_2000 = (
    df_movies.query("genre == 'Sci-Fi'")  # Первый шаг: выбираем Sci-Fi
              .query("year > 2000 and rating > 8.5") # Второй шаг: из результата первого выбираем по году и рейтингу
)

print("Sci-Fi фильмы, выпущенные после 2000 с рейтингом > 8.5:")
display(sci_fi_highly_rated_post_2000[['title', 'year', 'rating']])

В этом примере df_movies.query("genre == 'Sci-Fi'") возвращает DataFrame, содержащий только научно-фантастические фильмы. К этому новому DataFrame затем применяется второй query("year > 2000 and rating > 8.5"). Это лаконично и понятно.

Пример 2: query() в сочетании с assign() для создания нового столбца и последующей фильтрации по нему

Представим, что мы хотим добавить столбец, указывающий, является ли фильм "длинным" (например, дольше 150 минут), а затем отфильтровать только "длинные" фильмы определенного жанра.

# Определяем порог для "длинного" фильма
long_movie_threshold = 150

long_crime_movies = (
    df_movies.assign(is_long_movie = lambda df: df['duration_min'] > long_movie_threshold) # Создаем столбец is_long_movie
             .query("is_long_movie == True and genre == 'Crime'") # Фильтруем по новому столбцу и жанру
)
# Важно: в query() мы можем ссылаться на столбец 'is_long_movie', 
# так как он был создан на предыдущем шаге цепочки методом assign().

print(f"\nДлинные (>{long_movie_threshold} мин) криминальные фильмы:")
display(long_crime_movies[['title', 'genre', 'duration_min', 'is_long_movie']])

Тут мы модифицируем DataFrame (добавляем столбец) и тут же фильтруем по результату этой модификации, все в одном потоке операций. query() органично вписывается в такие конструкции, так как он "видит" столбцы, созданные предыдущими методами в цепочке.

Использование query() в цепочках помогает поддерживать код в стиле "fluent interface", что ценится в Pandas за его выразительность и читаемость при выполнении сложных многоэтапных операций с данными.

Использование функций в query()

Возможности query() не ограничиваются только арифметическими и логическими операторами. Вы можете использовать различные функции и методы прямо внутри строки запроса для выполнения более сложной фильтрации.

1. Использование встроенных математических функций

Метод query() для вычисления строковых выражений использует мощный движок (по умолчанию это numexpr), который поддерживает множество математических функций, таких как sin, cos, log, exp, sqrt, floor, ceil и другие. Можно вызывать их по имени, как если бы они были глобально доступны внутри запроса.

import numpy as np

# Найти фильмы, натуральный логарифм продолжительности которых больше 5
# Это может быть полезно при работе с лог-нормальными распределениями или для масштабирования
print("Фильмы, где ln(продолжительность) > 5:")
display(df_movies.query("log(duration_min) > 5"))
# Другой пример: найти фильмы, чей рейтинг является целым числом (без дробной части)
# Используем floor для "округления вниз"
print("\nФильмы с целым рейтингом:")
display(df_movies.query("rating == floor(rating)"))

Важно: вызываем log, floor и другие функции напрямую, без префикса @ или np.. Pandas сам сопоставит эти имена с соответствующими функциями numpy.

2. Использование строковых методов

Если у вас есть столбец с текстовыми данными, вы можете использовать все стандартные методы строковой обработки, доступные через accessor .str. Это удобно для фильтрации по шаблону, подстроке и т.д.

# Найти все фильмы, в названии которых есть слово "The" (с учетом регистра)
print("Фильмы, содержащие 'The' в названии:")
display(df_movies.query("title.str.contains('The')"))
# Можно выстраивать цепочку из строковых методов.
# Например, найти фильмы, название которых (в нижнем регистре) заканчивается на 'knight'
display(df_movies.query("title.str.lower().str.endswith('knight')"))

Внутри query() имя столбца title интерпретируется как объект pandas.Series, поэтому вы можете вызывать его атрибуты и методы, такие как .str.contains(), напрямую, без каких-либо специальных префиксов.

Совет: query() по умолчанию использует движок numexpr для высокой производительности. Однако он поддерживает не все операции Python. Если вам нужна более сложная логика (например, лямбда-функции или вызовы произвольных методов), вы можете переключиться на движок Python: df.query('some_complex_condition', engine='python'). Учтите, что это может быть медленнее, но предоставляет максимальную гибкость.

eval(): вычисления без синтаксического шума

В то время как query() специализируется на фильтрации строк на основе некоторого условия, eval() предназначен для выполнения вычислений с использованием столбцов DataFrame. Этот метод также работает со строковыми выражениями, что позволяет производить сложные операции в очень компактной и читаемой форме.

Создание новых столбцов "на лету"

Наиболее частый и удобный сценарий использования eval() — создание новых столбцов или модификация существующих. Синтаксис внутри строки eval() очень похож на тот, что мы видели в query(): вы обращаетесь к столбцам по их именам и используете стандартные математические операторы и функции.

Если выражение содержит знак присваивания (=), eval() работает аналогично методу .assign(): он создает новый столбец (или изменяет существующий) и возвращает новый DataFrame с этим изменением.

# Создадим столбец 'rating_density' прямо в DataFrame
df_with_density = df_movies.eval('rating_density = rating / duration_min')

print("DataFrame с новым столбцом 'rating_density':")
display(df_with_density[['title', 'rating', 'duration_min', 'rating_density']].head())
# Можно также изменить существующий столбец.
# Например, приведем рейтинг к 100-балльной шкале.
df_rating_100 = df_movies.eval('rating = rating * 10')
display(df_rating_100[['title', 'rating']].head())

Как и query(), eval() отлично встраивается в цепочки преобразований:

# Рассчитаем рейтинг в 100-балльной шкале и добавим столбец с годом от премьеры
current_year = 2025

df_transformed = (
    df_movies
    .eval('rating_100 = rating * 10')
    .eval('years_since_release = @current_year - year')
)

df_transformed.head()

Как видите, eval() предоставляет очень удобный и лаконичный синтаксис для выполнения вычислений и создания новых признаков, идеально вписываясь в цепочки преобразований данных.

df.eval() vs pd.eval(): в чем разница?

Важно различать два метода: DataFrame.eval() и pandas.eval().

  • df.eval() — это метод экземпляра DataFrame. Он работает в контексте этого DataFrame, и все имена переменных в строке выражения ищутся сначала среди его столбцов, а затем — в локальной области видимости Python.
  • pd.eval() — это функция верхнего уровня в pandas. Она не привязана к конкретному DataFrame и позволяет выполнять вычисления с несколькими датафреймами одновременно. Для обращения к столбцам нужно использовать имя самого объекта DataFrame.

Это различие даёт возможность pd.eval() делать операции между несколькими таблицами. Представим, у нас есть два DataFrame: df1 с данными о продажах, а df2 — с поправочными коэффициентами.

df1 = pd.DataFrame({'revenue': [100, 150], 'items_sold': [10, 12]})
df2 = pd.DataFrame({'correction_factor': [1.05, 0.98]})

# pd.eval() позволяет комбинировать их в одном выражении
final_revenue = pd.eval('df1.revenue * df2.correction_factor')
display(final_revenue)

Сделать то же самое с помощью df.eval() было бы невозможно.

Использование переменных и сложных выражений

eval() раскрывает всю свою мощь при расчете сложных, многокомпонентных формул. Для этого идеально подходит цепочка вызовов eval(), где каждый шаг добавляет один необходимый компонент.

# Зададим веса для нашей формулы и другие параметры
weight_rating = 0.6
weight_freshness = 0.3
weight_duration = 0.1
current_year = 2024

# Строим цепочку вызовов eval():
hype_index_df = (
    df_movies
    # Эти простые выражения отлично обрабатываются быстрым движком numexpr (по умолчанию)
    .eval("age_in_years = @current_year - year")
    .eval("freshness_factor = 1 - (age_in_years / age_in_years.max())")
    .eval("normalized_duration = duration_min / duration_min.max()")
    # А для этого сложного выражения мы явно указываем движок 'python'
    .eval(
        "hype_index = (rating / 10 * @weight_rating + "
        "freshness_factor * @weight_freshness + "
        "normalized_duration * @weight_duration)",
        engine='python'
    )
)

hype_index_df[['title', 'rating', 'year', 'hype_index']].sort_values(
    'hype_index', ascending=False
).head(10)

Этот пример наглядно демонстрирует, насколько eval() может быть выразительнее и чище, чем создание каждого промежуточного столбца (normalized_duration, freshness_factor) по отдельности с помощью стандартного синтаксиса.

💣 Осторожно! Вопросы безопасности с query() и eval()

При всём своём удобстве, методы query() и eval() несут в себе потенциальную угрозу безопасности, о которой необходимо знать. Она возникает в том случае, если вы используете эти методы для обработки внешнего, неконтролируемого пользовательского ввода.

Методы query() и eval() парсят и выполняют код, переданный им в виде строки. Если эта строка формируется на основе данных, введенных пользователем (например, через веб-форму или API-запрос), злоумышленник может составить специальную строку, которая будет содержать вредоносный код. Это называется Code Injection (инъекция кода).

Представьте, что у вас есть простое веб-приложение, где пользователь может отфильтровать данные, введя год в текстовое поле. Вы берете это значение и напрямую вставляете в query():

# !!! ОПАСНЫЙ И НЕПРАВИЛЬНЫЙ КОД - НИКОГДА ТАК НЕ ДЕЛАЙТЕ !!!
user_input = "2000" # Пришло от пользователя

# Если пользователь ввел "2000", все в порядке
df_movies.query(f"year > {user_input}") 

# А что, если пользователь введет вредоносную строку?
# Представим, что у вас есть модуль os, импортированный где-то в коде
import os 
malicious_input = "1900) or 1 == 1; os.system('echo Malicious command executed!')"

# Ваш код превратится в:
df_movies.query(f"year > {malicious_input}")
# Что раскроется в:
df_movies.query("year > 1900) or 1 == 1; os.system('echo Malicious command executed!')")

# В зависимости от движка и контекста, это может привести к выполнению
# команды os.system(...) и компрометации вашей системы.

Хотя pandas имеет некоторые встроенные механизмы защиты, и прямой вызов os.system может не сработать с движком по умолчанию numexpr (который имеет ограниченный набор команд), использование движка python (engine='python') значительно расширяет поверхность атаки. Злоумышленник может попытаться получить доступ к глобальным переменным, выполнить нежелательные вычисления или даже вызвать удаление файлов, если ему удастся построить правильную строку.

Никогда не передавайте в query() или eval() необработанные строки, полученные из внешних источников или от пользователей.

Если вам необходимо фильтровать данные на основе пользовательского ввода, всегда используйте параметризованный подход с классической индексацией:

  1. Валидируйте и очищайте ввод: Убедитесь, что пользователь ввел именно то, что вы ожидаете (например, число, а не строку с кодом).
  2. Используйте классическую индексацию: Она безопасна, так как не исполняет код, а только сравнивает данные.

БЕЗОПАСНЫЙ СПОСОБ:

user_input_year_str = "2000" # Пришло от пользователя

try:
# 1. Валидация и очистка
year_from_user = int(user_input_year_str)
# 2. Безопасное использование в классическом фильтре
    safe_filtered_df = df_movies[df_movies['year'] > year_from_user]
    display(safe_filtered_df.head())
except ValueError:
    print("Ошибка: Введено некорректное значение года.")

Используйте query() и eval() для улучшения читаемости вашего собственного, статически определенного в коде или полностью контролируемого кода. Для работы с внешними данными всегда отдавайте предпочтение безопасности и используйте стандартные, неисполняемые методы.

query() vs eval() vs классические []: когда что выбрать?

Мы подробно рассмотрели query(), eval() и в самом начале использовали "классический" подход с булевой индексацией через []. Все эти инструменты мощные, но у каждого есть своя ниша, сильные и слабые стороны. Давайте разберемся, когда какой из них предпочтительнее.

Сильные стороны query()/eval(): читаемость, краткость, удобство

Главный козырь query() и eval() — это чистота и читаемость кода, особенно при сложных, многосоставных условиях.

  • Меньше синтаксического шума: Отсутствие постоянных df['col_name'] и замена &/| на and/or делает код похожим на естественный язык.
  • Удобство в цепочках: Как мы видели, query() и eval() отлично встраиваются в цепочки методов, позволяя элегантно выполнять последовательные преобразования.
  • Производительность на больших данных: Для чисто численных операций на больших DataFrame движок numexpr (используемый по умолчанию) может дать выигрыш в производительности по сравнению с обычными операциями Pandas, которые выполняются в Python. Это происходит за счет оптимизированных низкоуровневых вычислений и более эффективного использования кэша процессора.

Аргументы в пользу классики: производительность и гибкость

Несмотря на все преимущества query()/eval(), традиционный синтаксис [] в некоторых случаях являются лучшим выбором.

  • Производительность на малых и средних данных: На небольших датафреймах накладные расходы на парсинг строкового выражения в query()/eval() могут "съесть" все преимущество от быстрых вычислений. Классический подход в таких случаях часто оказывается быстрее, так как он не требует этапа интерпретации строки.
  • Максимальная гибкость: Стандартная индексация не имеет ограничений на используемые функции. Вы можете применять абсолютно любые функции Python в ваших условиях, создавать сложные объекты "на лету" и так далее. Вы не ограничены набором функций, поддерживаемых numexpr.
  • Простота для простых случаев: Для простого условия вроде df[df['year'] > 2000] нет никакого смысла использовать query(). Классический синтаксис здесь короче и абсолютно понятен.

Наглядная табличка: когда что использовать?

В конечном счете, выбор инструмента — это всегда компромисс. query() и eval() — это не замена стандартным методам, а дополнение к вашему арсеналу, которое стоит применять там, где оно приносит максимальную пользу с точки зрения чистоты и поддерживаемости кода.

Заключение

Мы прошли путь от громоздких, но надежных "классических" конструкций pandas до хорошо читаемых выражений с query() и eval(). Давайте закрепим основные выводы.

Методы query() и eval() — это не просто синтаксический сахар, а мощные инструменты, которые при правильном использовании могут кардинально улучшить качество вашего кода.

  • query() избавляет от "скобочного ада" при сложной фильтрации, делая условия понятными с первого взгляда.
  • eval() предоставляет лаконичный синтаксис для вычислений и создания новых столбцов, идеально вписываясь в цепочки преобразований данных.

Конечно, они не являются универсальной заменой стандартным подходам. Классическая индексация df[...] и метод .assign() остаются незаменимыми инструментами, превосходящими query()/eval() в гибкости, простоте для элементарных задач и безопасности при работе с внешними данными.

Ключ к мастерству в pandas — это не слепое следование одному стилю, а умение выбирать правильный инструмент для конкретной задачи.

  • Простая фильтрация? Берите df[...].
  • Сложная логика отбора строк? query() — наш выбор.
  • Создать один новый столбец из существующих? eval() сделает это красиво.

Теперь, столкнувшись с очередной многоэтажной конструкцией фильтрации, вы будете знать, что есть более изящный путь. 🚀