Оценка динамических выражений в пандах с использованием pd.eval () - PullRequest
0 голосов
/ 14 декабря 2018

Учитывая два кадра данных

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 в качестве аргумента внутри строкивыражение?

Ответы [ 2 ]

0 голосов
/ 29 января 2019

Учебное пособие уже отлично, но имейте в виду, что, прежде чем приступить к использованию 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

0 голосов
/ 14 декабря 2018

В этом ответе рассматриваются различные функции и функции, предлагаемые pd.eval, df.query и df.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'))
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")

Список всех поддерживаемых функций и синтаксиса можно найти в документации .В итоге,

  • Арифметические операции, за исключением операторов левого (<<) и правого (>>), например, 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, циклы и выражения, а также выражения генератора.

FromИз списка очевидно, что вы также можете передавать выражения, включающие индекс, такие как

pd.eval('df1.A * (df1.index > 1)')

ParВыбор ser: 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.

Backend Selection: engine=... аргумент

Есть два варианта - numexpr (по умолчанию) и python.Опция numexpr использует серверную часть numbersxpr , оптимизированную для производительности.

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

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

К сожалению, этот метод предлагает преимущества производительности no по сравнению с numexpr, и естьочень мало мер безопасности для обеспечения того, чтобы опасные выражения не оценивались, поэтому ИСПОЛЬЗУЙТЕ НА СВОЙ СТРАХ И РИСК !Как правило, не рекомендуется менять этот параметр на 'python', если вы не знаете, что делаете.

local_dict и global_dict arguments

Иногда полезно предоставить значения для переменных, используемых внутри выражений, но в настоящее время не определенных в вашем пространстве имен.Вы можете передать словарь в 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 <b>_eval(expr, inplace=inplace, **kwargs)</b>

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 ".Таким образом, приведенное выше выражение может быть записано как 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 при запросе или фильтрации строк на основе условного выражения.

...