Ответ 1
Этот ответ раскрывает различные функции и возможности, предлагаемые pd.eval
, df.query
и df.eval
.
Настройка
Примеры будут включать эти DataFrames (если не указано иное).
np.random.seed(0)
df1 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df2 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df3 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df4 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
pandas.eval
- "Отсутствует руководство"
Примечание
Из трех обсуждаемых функцийpd.eval
является наиболее важной.df.eval
иdf.query
вызовpd.eval
под капотом. Поведение и использование более или менее согласованы между тремя функциями, с некоторыми незначительными семантическими варианты, которые будут выделены позже. Этот раздел будет представить функциональность, которая является общей для всех трех функций - это включает (но не ограничивается) разрешенный синтаксис, правила приоритета и аргументы ключевых слов.
pd.eval
может оценивать арифметические выражения, которые могут состоять из переменных и/или литералов. Эти выражения должны быть переданы в виде строк. Итак, , чтобы ответить на вопрос, как указано, вы можете сделать
x = 5
pd.eval("df1.A + (df1.B * x)")
Некоторые вещи, на которые следует обратить внимание:
- Все выражение представляет собой строку
df1
,df2
иx
ссылаются на переменные в глобальном пространстве имен, которые выбираютсяeval
при синтаксическом анализе выражения- Доступ к определенным столбцам осуществляется с помощью индекса атрибута доступа. Вы также можете использовать
"df1['A'] + (df1['B'] * x)"
для того же эффекта.
Я рассмотрю конкретную проблему переназначения в разделе, объясняющем атрибут target=...
ниже. Но сейчас приведем более простые примеры допустимых операций с pd.eval
:
pd.eval("df1.A + df2.A") # Valid, returns a pd.Series object
pd.eval("abs(df1) ** .5") # Valid, returns a pd.DataFrame object
... и так далее. Условные выражения также поддерживаются таким же образом. Приведенные ниже утверждения являются действительными выражениями и будут оцениваться движком.
pd.eval("df1 > df2")
pd.eval("df1 > 5")
pd.eval("df1 < df2 and df3 < df4")
pd.eval("df1 in [1, 2, 3]")
pd.eval("1 < 2 < 3")
Список, подробно описывающий все поддерживаемые функции и синтаксис, можно найти в документации documentation. В итоге,
- Арифметические операции, за исключением операторов левого сдвига (
<<
) и правого сдвига (>>
), например,df + 2 * pi / s ** 4 % 42
- the_golden_ratio- Операции сравнения, включая цепные сравнения, например,
2 < df < df2
- Логические операции, например,
df < df2 and df3 < df4
илиnot df_bool
Литералыlist
иtuple
, например,[1, 2]
или(1, 2)
- Доступ к атрибутам, например,
df.a
- Подстрочные выражения, например,
df[0]
- Простая оценка переменной, например,
pd.eval('df')
(это не очень полезно)- Математические функции: sin, cos, exp, log, expm1, log1p, sqrt, sinh, cosh, tanh, arcsin, arccos, arctan, arccosh, arcsinh, arctanh, abs и arctan2.
В этом разделе документации также указаны синтаксические правила, которые не поддерживаются, включая литералы set
/dict
, операторы if-else, циклы и выражения, а также выражения генератора.
Из списка очевидно, что вы также можете передавать выражения, включающие индекс, такие как
pd.eval('df1.A * (df1.index > 1)')
Выбор парсера: аргумент parser=...
pd.eval
поддерживает две разные опции парсера при разборе строки выражения для генерации синтаксического дерева: pandas
и python
. Основное различие между ними выделено немного отличающимися правилами приоритета.
При использовании синтаксического анализатора по умолчанию pandas
перегруженные побитовые операторы &
и |
, которые реализуют векторизованные операции И и ИЛИ с объектами панд, будут иметь тот же приоритет операторов, что и and
и or
. Итак,
pd.eval("(df1 > df2) & (df3 < df4)")
Будет таким же, как
pd.eval("df1 > df2 & df3 < df4")
# pd.eval("df1 > df2 & df3 < df4", parser='pandas')
А также как
pd.eval("df1 > df2 and df3 < df4")
Здесь необходимы круглые скобки. Чтобы сделать это условно, парены должны были бы переопределить более высокий приоритет побитовых операторов:
(df1 > df2) & (df3 < df4)
Без этого мы получим
df1 > df2 & df3 < df4
ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
Используйте parser='python'
, если вы хотите поддерживать согласованность с действительными правилами приоритета операторов python при оценке строки.
pd.eval("(df1 > df2) & (df3 < df4)", parser='python')
Другое различие между двумя типами синтаксических анализаторов заключается в семантике операторов ==
и !=
с узлами списков и кортежей, которые имеют семантику, аналогичную in
и not in
соответственно, при использовании 'pandas'
парсер. Например,
pd.eval("df1 == [1, 2, 3]")
Действителен и будет работать с той же семантикой, что и
pd.eval("df1 in [1, 2, 3]")
OTOH, pd.eval("df1 == [1, 2, 3]", parser='python')
выдаст ошибку NotImplementedError
.
Выбор внутреннего интерфейса: аргумент engine=...
Есть два варианта - numexpr
(по умолчанию) и python
. Опция numexpr
использует бэкэнд numbersxpr, который оптимизирован для производительности.
С бэкэндом 'python'
ваше выражение оценивается аналогично простой передаче выражения в функцию python eval
. Вы можете делать больше внутри выражений, например, таких как строковые операции.
df = pd.DataFrame({'A': ['abc', 'def', 'abacus']})
pd.eval('df.A.str.contains("ab")', engine='python')
0 True
1 False
2 True
Name: A, dtype: bool
К сожалению, этот метод не дает никаких преимуществ в производительности по сравнению с механизмом numexpr
, и очень мало мер безопасности для обеспечения того, чтобы опасные выражения не оценивались, поэтому ИСПОЛЬЗУЙТЕ НА СВОЙ СТРАХ И РИСК! Обычно не рекомендуется менять эту опцию на 'python'
, если вы не знаете, что делаете.
local_dict
и global_dict
аргументы
Иногда полезно предоставить значения для переменных, используемых внутри выражений, но не определенных в вашем пространстве имен. Вы можете передать словарь в local_dict
Например,
pd.eval("df1 > thresh")
UndefinedVariableError: name 'thresh' is not defined
Сбой, потому что thresh
не определен. Тем не менее, это работает:
pd.eval("df1 > thresh", local_dict={'thresh': 10})
Это полезно, когда у вас есть переменные для подачи из словаря. В качестве альтернативы, с двигателем 'python'
вы можете просто сделать это:
mydict = {'thresh': 5}
# Dictionary values with *string* keys cannot be accessed without
# using the 'python' engine.
pd.eval('df1 > mydict["thresh"]', engine='python')
Но это, возможно, будет намного медленнее, чем использование механизма 'numexpr'
и передача словаря в local_dict
или global_dict
. Надеюсь, это послужит убедительным аргументом в пользу использования этих параметров.
Аргумент target
(+ inplace
) и выражения назначения
Это не часто является обязательным требованием, потому что обычно есть более простые способы сделать это, но вы можете присвоить результат pd.eval
объекту, который реализует __getitem__
, например dict
s, и (как вы уже догадались) DataFrames.
Рассмотрим пример из вопроса
x = 5 df2['D'] = df1['A'] + (df1['B'] * x)
Чтобы присвоить столбец "D" df2
, мы делаем
pd.eval('D = df1.A + (df1.B * x)', target=df2)
A B C D
0 5 9 8 5
1 4 3 0 52
2 5 0 2 22
3 8 1 3 48
4 3 7 0 42
Это не модификация df2
на месте (но ее можно... читать дальше). Рассмотрим другой пример:
pd.eval('df1.A + df2.A')
0 10
1 11
2 7
3 16
4 10
dtype: int32
Если вы хотите (например) назначить это обратно в DataFrame, вы можете использовать аргумент target
следующим образом:
df = pd.DataFrame(columns=list('FBGH'), index=df1.index)
df
F B G H
0 NaN NaN NaN NaN
1 NaN NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN NaN NaN NaN
4 NaN NaN NaN NaN
df = pd.eval('B = df1.A + df2.A', target=df)
# Similar to
# df = df.assign(B=pd.eval('df1.A + df2.A'))
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
Если вы хотите выполнить мутацию на месте df
, установите inplace=True
.
pd.eval('B = df1.A + df2.A', target=df, inplace=True)
# Similar to
# df['B'] = pd.eval('df1.A + df2.A')
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
Если inplace
установлен без цели, повышается ValueError
.
Несмотря на то, что с аргументом target
забавно играть, вам редко понадобится его использовать.
Если бы вы хотели сделать это с df.eval
, вы бы использовали выражение, включающее присваивание:
df = df.eval("B = @df1.A + @df2.A")
# df.eval("B = @df1.A + @df2.A", inplace=True)
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
Примечание
Одно из непреднамеренных применений pd.eval
- это разбор литеральных строк способом, очень похожим на ast.literal_eval
:
pd.eval("[1, 2, 3]")
array([1, 2, 3], dtype=object)
Он также может анализировать вложенные списки с помощью механизма 'python'
:
pd.eval("[[1, 2, 3], [4, 5], [10]]", engine='python')
[[1, 2, 3], [4, 5], [10]]
И списки строк:
pd.eval(["[1, 2, 3]", "[4, 5]", "[10]"], engine='python')
[[1, 2, 3], [4, 5], [10]]
Однако проблема заключается в списках длиной более 100:
pd.eval(["[1]"] * 100, engine='python') # Works
pd.eval(["[1]"] * 101, engine='python')
AttributeError: 'PandasExprVisitor' object has no attribute 'visit_Ellipsis'
Подробнее об этой ошибке, причинах, исправлениях и обходных путях можно узнать здесь здесь.
DataFrame.eval
- Сопоставление с pandas.eval
Как упоминалось выше, df.eval
вызывает pd.eval
под капотом. Исходный код v0.23 показывает это:
def eval(self, expr, inplace=False, **kwargs):
from pandas.core.computation.eval import eval as _eval
inplace = validate_bool_kwarg(inplace, 'inplace')
resolvers = kwargs.pop('resolvers', None)
kwargs['level'] = kwargs.pop('level', 0) + 1
if resolvers is None:
index_resolvers = self._get_index_resolvers()
resolvers = dict(self.iteritems()), index_resolvers
if 'target' not in kwargs:
kwargs['target'] = self
kwargs['resolvers'] = kwargs.get('resolvers', ()) + tuple(resolvers)
return _eval(expr, inplace=inplace, **kwargs)
eval
создает аргументы, выполняет небольшую проверку и передает аргументы pd.eval
.
Более подробную информацию вы можете прочитать: , когда использовать DataFrame.eval() против pandas.eval() или python eval()
Различия в использовании
Выражения с помощью выражений DataFrames v/s Series
Для динамических запросов, связанных со всеми фреймами данных, вы должны предпочесть pd.eval
. Например, нет простого способа указать эквивалент pd.eval("df1 + df2")
при вызове df1.eval
или df2.eval
.
Указание имен столбцов
Другое важное отличие заключается в том, как осуществляется доступ к столбцам. Например, чтобы добавить два столбца "A" и "B" в df1
, вы должны вызвать pd.eval
со следующим выражением:
pd.eval("df1.A + df1.B")
Для df.eval вам нужно только указать имена столбцов:
df1.eval("A + B")
Поскольку в контексте df1
ясно, что "A" и "B" относятся к именам столбцов.
Вы также можете ссылаться на индекс и столбцы, используя index
(если только индекс не назван, в этом случае вы бы использовали имя).
df1.eval("A + index")
Или, в более общем случае, для любого DataFrame с индексом, имеющим 1 или более уровней, вы можете ссылаться на уровень k th индекса в выражении, используя переменную "ilevel_k", которая обозначает для "i ndex на уровне k". IOW, вышеприведенное выражение может быть записано как df1.eval("A + ilevel_0")
.
Эти правила также применяются к query
.
Доступ к переменным в локальном/глобальном пространстве имен
Переменные, передаваемые внутри выражений, должны начинаться с символа "@", чтобы избежать путаницы с именами столбцов.
A = 5
df1.eval("A > @A")
То же самое относится и к query
.
Само собой разумеется, что имена ваших столбцов должны соответствовать правилам, чтобы действительные имена идентификаторов в python были доступны внутри eval
. См. здесь для списка правил именования идентификаторов.
Многострочные запросы и назначение
Малоизвестный факт заключается в том, что eval
поддерживает многострочные выражения, связанные с присваиванием. Например, чтобы создать два новых столбца "E" и "F" в df1 на основе некоторых арифметических операций над некоторыми столбцами и третий столбец "G" на основе ранее созданных "E" и "F", мы можем сделать
df1.eval("""
E = A + B
F = @df2.A + @df2.B
G = E >= F
""")
A B C D E F G
0 5 0 3 3 5 14 False
1 7 9 3 5 16 7 True
2 2 4 7 6 6 5 True
3 8 8 1 6 16 9 True
4 7 7 8 1 14 10 True
... Острота! Однако обратите внимание, что это не поддерживается query
.
eval
v/s query
- Последнее слово
Это помогает думать о df.query
как о функции, которая использует pd.eval
в качестве подпрограммы.
Как правило, query
(как следует из названия) используется для оценки условных выражений (то есть выражений, которые приводят к значениям True/False) и возвращает строки, соответствующие результату True
. Затем результат выражения передается в loc
(в большинстве случаев) для возврата строк, которые удовлетворяют выражению. Согласно документации,
Результат оценки этого выражения сначала передается
DataFrame.loc
и если это не удается из-за многомерного ключа (например, DataFrame), тогда результат будет переданDataFrame.__getitem__()
.Этот метод использует функцию верхнего уровня
pandas.eval()
для оценки пропущенный запрос.
С точки зрения сходства, query
и df.eval
одинаковы в том, как они получают доступ к именам столбцов и переменным.
Эта ключевая разница между ними, как упоминалось выше, заключается в том, как они обрабатывают результат выражения. Это становится очевидным, когда вы фактически запускаете выражение через эти две функции. Например, рассмотрим
df1.A
0 5
1 7
2 2
3 8
4 7
Name: A, dtype: int32
df1.B
0 9
1 3
2 0
3 1
4 7
Name: B, dtype: int32
Чтобы получить все строки, где "A"> = "B" в df1
, мы будем использовать eval
следующим образом:
m = df1.eval("A >= B")
m
0 True
1 False
2 False
3 True
4 True
dtype: bool
m
представляет промежуточный результат, полученный путем вычисления выражения "A> = B". Затем мы используем маску для фильтрации df1
:
df1[m]
# df1.loc[m]
A B C D
0 5 0 3 3
3 8 8 1 6
4 7 7 8 1
Однако, с query
, промежуточный результат "m" напрямую передается loc
, поэтому с query
вам просто нужно сделать
df1.query("A >= B")
A B C D
0 5 0 3 3
3 8 8 1 6
4 7 7 8 1
Производительность мудрая, она точно такая же.
df1_big = pd.concat([df1] * 100000, ignore_index=True)
%timeit df1_big[df1_big.eval("A >= B")]
%timeit df1_big.query("A >= B")
14.7 ms ± 33.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
14.7 ms ± 24.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Но последний вариант более лаконичен и выражает ту же операцию за один шаг.
Обратите внимание, что вы также можете делать странные вещи с query
, как это (скажем, возвращать все строки, проиндексированные df1.index)
df1.query("index")
# Same as df1.loc[df1.index] # Pointless,... I know
A B C D
0 5 0 3 3
1 7 9 3 5
2 2 4 7 6
3 8 8 1 6
4 7 7 8 1
Но не надо.
Итог: пожалуйста, используйте query
при запросе или фильтрации строк на основе условного выражения.