Оценка динамических выражений в пандах с использованием pd.eval()

Дано два кадра данных

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'))

df1
   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

df2
   A  B  C  D
0  5  9  8  9
1  4  3  0  3
2  5  0  2  3
3  8  1  3  3
4  3  7  0  1

Я хотел бы выполнить арифметику для одного или нескольких столбцов, используя pd.eval. В частности, я хотел бы перенести следующий код:

x = 5
df2['D'] = df1['A'] + (df1['B'] * x) 

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

Я пытаюсь лучше понять аргументы engine и parser, чтобы определить, как лучше всего решить мою проблему. Я ознакомился с документацией, но мне это не объяснили.

  1. Какие аргументы следует использовать, чтобы мой код работал с максимальной производительностью?
  2. Есть ли способ присвоить результат выражения обратно df2?
  3. Кроме того, чтобы усложнить задачу, как передать x в качестве аргумента внутри строкового выражения?

Ответы

Ответ 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)")  

Некоторые вещи, на которые следует обратить внимание:

  1. Все выражение представляет собой строку
  2. df1, df2 и x ссылаются на переменные в глобальном пространстве имен, которые выбираются eval при синтаксическом анализе выражения
  3. Доступ к определенным столбцам осуществляется с помощью индекса атрибута доступа. Вы также можете использовать "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 при запросе или фильтрации строк на основе условного выражения.

Ответ 2

Отличный учебник, но имейте в виду, что, прежде чем приступить к использованию eval/query привлекаемого более простым синтаксисом, у него возникнут серьезные проблемы с производительностью, если в вашем наборе данных менее 15 000 строк.

В этом случае просто используйте df.loc[mask1, mask2].

См. Https://pandas.pydata.org/pandas-docs/version/0.22/enhancingperf.html#enhancingperf-eval.

enter image description here