Spec-Zone.ru › pandas 0.18

Улучшение производительности

Cython (написание расширений на C для pandas)

Для многих случаев использования достаточно написания pandas на чистом Python и numpy. Однако в некоторых вычислительно сложных приложениях можно добиться значительного ускорения, переложив работу на cython.

В этом руководстве предполагается, что вы максимально переработали код на Python, например, попытались удалить циклы for и использовали векторизацию numpy. Стоит всегда сначала оптимизировать код на Python.

В этом руководстве показан «типичный» процесс цитонизации медленного вычисления. Мы используем пример из документации cython, но в контексте pandas. Наш окончательный цитонизированный код работает примерно в 100 раз быстрее, чем чистый Python.

Чистый Python

У нас есть DataFrame, к которому мы хотим применить функцию по строкам.

In [1]: df = pd.DataFrame({'a': np.random.randn(1000),
   ...:                    'b': np.random.randn(1000),
   ...:                    'N': np.random.randint(100, 1000, (1000)),
   ...:                    'x': 'x'})
   ...: 

In [2]: df
Out[2]: 
       N         a         b  x
0    585  0.469112 -0.218470  x
1    841 -0.282863 -0.061645  x
2    251 -1.509059 -0.723780  x
3    972 -1.135632  0.551225  x
4    181  1.212112 -0.497767  x
5    458 -0.173215  0.837519  x
6    159  0.119209  1.103245  x
..   ...       ...       ... ..
993  190  0.131892  0.290162  x
994  931  0.342097  0.215341  x
995  374 -1.512743  0.874737  x
996  246  0.933753  1.120790  x
997  157 -0.308013  0.198768  x
998  977 -0.079915  1.757555  x
999  770 -1.010589 -1.115680  x

[1000 rows x 4 columns]

Вот функция на чистом Python:

In [3]: def f(x):
   ...:     return x * (x - 1)
   ...: 

In [4]: def integrate_f(a, b, N):
   ...:     s = 0
   ...:     dx = (b - a) / N
   ...:     for i in range(N):
   ...:         s += f(a + i * dx)
   ...:     return s * dx
   ...: 

Мы достигаем результата, используя apply (по строкам):

In [7]: %timeit df.apply(lambda x: integrate_f(x['a'], x['b'], x['N']), axis=1)
10 loops, best of 3: 174 ms per loop

Но, очевидно, этого недостаточно для нас. Давайте посмотрим, где тратится время во время этой операции (ограничившись четырьмя наиболее трудоёмкими вызовами), используя магическую функцию ipython prun:

In [5]: %prun -l 4 df.apply(lambda x: integrate_f(x['a'], x['b'], x['N']), axis=1)
         671897 function calls (666888 primitive calls) in 0.323 seconds

   Ordered by: internal time
   List reduced from 126 to 4 due to restriction <4>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1000    0.158    0.000    0.243    0.000 <ipython-input-4-91e33489f136>:1(integrate_f)
   552423    0.077    0.000    0.077    0.000 <ipython-input-3-bc41a25943f6>:1(f)
     3000    0.010    0.000    0.053    0.000 base.py:1957(get_value)
     1000    0.008    0.000    0.008    0.000 {range}

Подавляющее большинство времени тратится внутри integrate_f или f, поэтому мы сосредоточим свои усилия на цитонизации этих двух функций.

Примечание

В Python 2 замена range её генераторной версией (xrange) означала бы исчезновение строки range. В Python 3 range уже является генератором.

Простой Cython

Сначала нам нужно импортировать магическую функцию cython в ipython (для версий cython >=0.21 вы можете использовать %load_ext Cython)

:

In [6]: %load_ext Cython

Теперь давайте просто скопируем наши функции в Cython как есть (суффикс здесь нужен для различения версий функций):

In [7]: %%cython
   ...: def f_plain(x):
   ...:     return x * (x - 1)
   ...: def integrate_f_plain(a, b, N):
   ...:     s = 0
   ...:     dx = (b - a) / N
   ...:     for i in range(N):
   ...:         s += f_plain(a + i * dx)
   ...:     return s * dx
   ...: 

Примечание

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

In [4]: %timeit df.apply(lambda x: integrate_f_plain(x['a'], x['b'], x['N']), axis=1)
10 loops, best of 3: 85.5 ms per loop

Уже это сократило время на треть, неплохо для простого копирования и вставки.

Добавление типа

Мы получаем ещё одно значительное улучшение, просто добавив информацию о типе:

In [8]: %%cython
   ...: cdef double f_typed(double x) except? -2:
   ...:     return x * (x - 1)
   ...: cpdef double integrate_f_typed(double a, double b, int N):
   ...:     cdef int i
   ...:     cdef double s, dx
   ...:     s = 0
   ...:     dx = (b - a) / N
   ...:     for i in range(N):
   ...:         s += f_typed(a + i * dx)
   ...:     return s * dx
   ...: 
In [4]: %timeit df.apply(lambda x: integrate_f_typed(x['a'], x['b'], x['N']), axis=1)
10 loops, best of 3: 20.3 ms per loop

Теперь всё намного быстрее! Это теперь более чем в десять раз быстрее, чем исходное реализация на Python, и мы практически не меняли код. Давайте ещё раз посмотрим, что занимает много времени:

In [9]: %prun -l 4 df.apply(lambda x: integrate_f_typed(x['a'], x['b'], x['N']), axis=1)
         118472 function calls (113463 primitive calls) in 0.088 seconds

   Ordered by: internal time
   List reduced from 122 to 4 due to restriction <4>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     3000    0.011    0.000    0.061    0.000 base.py:1957(get_value)
     3000    0.006    0.000    0.069    0.000 series.py:580(__getitem__)
     3000    0.005    0.000    0.013    0.000 base.py:972(_convert_scalar_indexer)
     9021    0.005    0.000    0.011    0.000 {getattr}

Использование ndarray

Он многократно вызывает series! Он создаёт Series для каждой строки и получает значения из индекса и Series (трижды для каждой строки). Вызовы функций в Python медленные, поэтому, возможно, мы можем минимизировать их, цитонизируя часть apply.

Примечание

Сейчас мы передаём ndarray в функцию cython, и cython прекрасно работает с numpy.

In [10]: %%cython
   ....: cimport numpy as np
   ....: import numpy as np
   ....: cdef double f_typed(double x) except? -2:
   ....:     return x * (x - 1)
   ....: cpdef double integrate_f_typed(double a, double b, int N):
   ....:     cdef int i
   ....:     cdef double s, dx
   ....:     s = 0
   ....:     dx = (b - a) / N
   ....:     for i in range(N):
   ....:         s += f_typed(a + i * dx)
   ....:     return s * dx
   ....: cpdef np.ndarray[double] apply_integrate_f(np.ndarray col_a, np.ndarray col_b, np.ndarray col_N):
   ....:     assert (col_a.dtype == np.float and col_b.dtype == np.float and col_N.dtype == np.int)
   ....:     cdef Py_ssize_t i, n = len(col_N)
   ....:     assert (len(col_a) == len(col_b) == n)
   ....:     cdef np.ndarray[double] res = np.empty(n)
   ....:     for i in range(len(col_a)):
   ....:         res[i] = integrate_f_typed(col_a[i], col_b[i], col_N[i])
   ....:     return res
   ....: 

Реализация проста: создаётся массив нулей, и цикл проходит по строкам, применяет нашу integrate_f_typed, и записывает результат в массив нулей.

Предупреждение

В версии 0.13.0, поскольку Series внутренне был переработан, чтобы больше не быть подклассом ndarray, а вместо этого подклассом NDFrame, вы не можете передать Series напрямую как параметр типа ndarray в функцию cython. Вместо этого передайте фактический ndarray с помощью атрибута .values объекта Series.

До версии 0.13.0

apply_integrate_f(df['a'], df['b'], df['N'])

Используйте .values для получения базового ndarray

apply_integrate_f(df['a'].values, df['b'].values, df['N'].values)

Примечание

Циклы такого рода были бы очень медленными в Python, но в Cython циклы по массивам numpy быстрые.

In [4]: %timeit apply_integrate_f(df['a'].values, df['b'].values, df['N'].values)
1000 loops, best of 3: 1.25 ms per loop

Мы получили ещё одно значительное улучшение. Давайте ещё раз проверим, где тратится время:

In [11]: %prun -l 4 apply_integrate_f(df['a'].values, df['b'].values, df['N'].values)
         208 function calls in 0.003 seconds

   Ordered by: internal time
   List reduced from 53 to 4 due to restriction <4>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.003    0.003    0.003    0.003 {_cython_magic_85b221d07d85a050b6deda5726e00134.apply_integrate_f}
        3    0.000    0.000    0.000    0.000 internals.py:3312(iget)
        9    0.000    0.000    0.000    0.000 generic.py:2674(__setattr__)
        3    0.000    0.000    0.000    0.000 frame.py:1973(__getitem__)

Как можно было ожидать, большая часть времени сейчас тратится в apply_integrate_f, поэтому, если мы хотим добиться ещё большей эффективности, мы должны продолжить сосредоточить усилия здесь.

Более продвинутые техники

Ещё есть надежда на улучшение. Вот пример использования некоторых более продвинутых техник cython:

In [12]: %%cython
   ....: cimport cython
   ....: cimport numpy as np
   ....: import numpy as np
   ....: cdef double f_typed(double x) except? -2:
   ....:     return x * (x - 1)
   ....: cpdef double integrate_f_typed(double a, double b, int N):
   ....:     cdef int i
   ....:     cdef double s, dx
   ....:     s = 0
   ....:     dx = (b - a) / N
   ....:     for i in range(N):
   ....:         s += f_typed(a + i * dx)
   ....:     return s * dx
   ....: @cython.boundscheck(False)
   ....: @cython.wraparound(False)
   ....: cpdef np.ndarray[double] apply_integrate_f_wrap(np.ndarray[double] col_a, np.ndarray[double] col_b, np.ndarray[int] col_N):
   ....:     cdef int i, n = len(col_N)
   ....:     assert len(col_a) == len(col_b) == n
   ....:     cdef np.ndarray[double] res = np.empty(n)
   ....:     for i in range(n):
   ....:         res[i] = integrate_f_typed(col_a[i], col_b[i], col_N[i])
   ....:     return res
   ....: 
In [4]: %timeit apply_integrate_f_wrap(df['a'].values, df['b'].values, df['N'].values)
1000 loops, best of 3: 987 us per loop

Ещё быстрее, с оговоркой, что ошибка в нашем коде cython (например, ошибка смещения на единицу) может привести к ошибке segfault, потому что доступ к памяти не проверяется.

Использование numba

Недавняя альтернатива статическому компиляции кода cython - использование динамического JIT-компилятора, numba.

Numba даёт вам возможность ускорить ваши приложения с помощью высокопроизводительных функций, написанных непосредственно на Python. С помощью нескольких аннотаций, ориентированные на массивы и интенсивные математические вычисления Python, код может быть скомпилирован в машинные инструкции на лету (just-in-time), достигая производительности, сопоставимой с C, C++ и Fortran, без необходимости переключения языков или интерпретаторов Python.

Numba работает, генерируя оптимизированный машинный код с использованием инфраструктуры компилятора LLVM во время импорта, выполнения или статически (с помощью включённого инструмента pycc). Numba поддерживает компиляцию Python для выполнения как на аппаратном обеспечении CPU, так и GPU, и предназначена для интеграции с научной программной платформой Python.

Примечание

Вам нужно будет установить numba. Это легко сделать с conda, используя: conda install numba, см. установку с использованием miniconda.

Примечание

Начиная с версии numba 0.20, объекты pandas не могут быть переданы напрямую в функции, скомпилированные numba. Вместо этого необходимо передать массив numpy подлежащий объекту pandas в функцию, скомпилированную numba, как показано ниже.

Jit

Использование numba для компиляции кода на лету. Мы просто берём код на чистом Python из примера выше и аннотируем его декоратором @jit.

import numba

@numba.jit
def f_plain(x):
   return x * (x - 1)

@numba.jit
def integrate_f_numba(a, b, N):
   s = 0
   dx = (b - a) / N
   for i in range(N):
       s += f_plain(a + i * dx)
   return s * dx

@numba.jit
def apply_integrate_f_numba(col_a, col_b, col_N):
   n = len(col_N)
   result = np.empty(n, dtype='float64')
   assert len(col_a) == len(col_b) == n
   for i in range(n):
      result[i] = integrate_f_numba(col_a[i], col_b[i], col_N[i])
   return result

def compute_numba(df):
   result = apply_integrate_f_numba(df['a'].values, df['b'].values, df['N'].values)
   return pd.Series(result, index=df.index, name='result')

Обратите внимание, что мы напрямую передаём массивы numpy в функцию numba. compute_numba - просто обёртка, которая предоставляет более удобный интерфейс, передавая/возвращая объекты pandas.

In [4]: %timeit compute_numba(df)
1000 loops, best of 3: 798 us per loop

Vectorize

numba также может быть использовано для написания векторизованных функций, которые не требуют от пользователя явного прохода по наблюдениям вектора; векторизованная функция будет применяться к каждой строке автоматически. Рассмотрим следующий пример удвоения каждого наблюдения:

import numba

def double_every_value_nonumba(x):
    return x*2

@numba.vectorize
def double_every_value_withnumba(x):
    return x*2


# Custom function without numba
In [5]: %timeit df['col1_doubled'] = df.a.apply(double_every_value_nonumba)
1000 loops, best of 3: 797 us per loop

# Standard implementation (faster than a custom function)
In [6]: %timeit df['col1_doubled'] = df.a*2
1000 loops, best of 3: 233 us per loop

# Custom function with numba
In [7]: %timeit df['col1_doubled'] = double_every_value_withnumba(df.a.values)
1000 loops, best of 3: 145 us per loop

Ограничения

Примечание

numba будет выполняться для любой функции, но может ускорить только определённые типы функций.

numba лучше всего ускоряет функции, применяющие числовые функции к массивам numpy. Когда ей передаётся функция, которая использует только операции, которые она умеет ускорять, она будет работать в режиме nopython.

Если numba получает функцию, которая включает в себя что-то, с чем она не умеет работать (в настоящее время это множества, списки, словари или строковые функции), она вернётся к режиму object mode. В режиме object mode, numba выполнится, но ваш код не ускорится значительно. Если вы предпочитаете, чтобы numba выбрасывала ошибку, если она не может скомпилировать функцию таким образом, чтобы ускорить ваш код, передайте numba аргумент nopython=True (например, @numba.jit(nopython=True)). Более подробную информацию об устранении неполадок режимов numba см. на странице устранения неполадок numba.

Дополнительную информацию см. в документации numba.

Вычисление выражений через eval() (Экспериментально)

Новая функция в версии 0.13.

Функция верхнего уровня pandas.eval() реализует вычисление выражений для объектов Series и DataFrame.

Примечание

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

Суть использования eval() для вычисления выражений вместо простого Python заключается в двух вещах: 1) большие объекты DataFrame обрабатываются более эффективно и 2) большие арифметические и логические выражения вычисляются единым блоком, с использованием базового движка (по умолчанию используется numexpr для вычислений).

Примечание

Не следует использовать eval() для простых выражений или для выражений, включающих небольшие DataFrame. Фактически, eval() на много порядков медленнее для небольших выражений/объектов, чем обычный Python. Правило хорошего тона - использовать eval() только тогда, когда у вас есть DataFrame с более чем 10 000 строк.

eval() поддерживает все арифметические выражения, поддерживаемые движком, а также некоторые расширения, доступные только в pandas.

Примечание

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

Поддерживаемый синтаксис

Эти операции поддерживаются pandas.eval():

  • Арифметические операции, за исключением операторов левого сдвига (<<) и правого сдвига (>>) например, df + 2 * pi / s ** 4 % 42 - the_golden_ratio
  • Операции сравнения, включая цепочечные сравнения, например, 2 < df < df2
  • Логические операции, например, df < df2 and df3 < df4 or 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.

Этот синтаксис Python не разрешен:

  • Выражения
    • Вызовы функций, отличные от математических функций.
    • is/is not операции
    • if выражения
    • lambda выражения
    • list/set/dict выражения
    • Литеральные dict и set выражения
    • yield выражения
    • Выражения генераторов
    • Булевы выражения, состоящие только из скалярных значений
  • Операторы
    • Не разрешены ни простые, ни составные операторы. Это включает в себя такие вещи, как for, while, и if.

eval() Примеры

pandas.eval() хорошо работает с выражениями, содержащими большие массивы.

Сначала создадим несколько массивов приличных размеров, чтобы поиграть с ними:

In [13]: nrows, ncols = 20000, 100

In [14]: df1, df2, df3, df4 = [pd.DataFrame(np.random.randn(nrows, ncols)) for _ in range(4)]

Теперь сравним сложение их с помощью обычного Python по сравнению с eval():

In [15]: %timeit df1 + df2 + df3 + df4
10 loops, best of 3: 23.9 ms per loop
In [16]: %timeit pd.eval('df1 + df2 + df3 + df4')
100 loops, best of 3: 14.4 ms per loop

Теперь сделаем то же самое, но с сравнениями:

In [17]: %timeit (df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)
10 loops, best of 3: 56.4 ms per loop
In [18]: %timeit pd.eval('(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)')
10 loops, best of 3: 24.1 ms per loop

eval() также работает с невыровненными объектами pandas:

In [19]: s = pd.Series(np.random.randn(50))

In [20]: %timeit df1 + df2 + df3 + df4 + s
10 loops, best of 3: 41.1 ms per loop
In [21]: %timeit pd.eval('df1 + df2 + df3 + df4 + s')
100 loops, best of 3: 15.7 ms per loop

Примечание

Операции, такие как

1 and 2  # would parse to 1 & 2, but should evaluate to 2
3 or 4  # would parse to 3 | 4, but should evaluate to 3
~1  # this is okay, but slower when using eval

должны выполняться в Python. Если вы попытаетесь выполнить какие-либо булевы/битовые операции со скалярными операндами, которые не являются типами bool или np.bool_, будет выброшено исключение. Вновь, такие операции следует выполнять в обычном Python.

Метод DataFrame.eval (Экспериментальный)

Введено в версии 0.13.

В дополнение к функции верхнего уровня pandas.eval() вы также можете оценить выражение в «контексте» DataFrame.

In [22]: df = pd.DataFrame(np.random.randn(5, 2), columns=['a', 'b'])

In [23]: df.eval('a + b')
Out[23]: 
0   -0.246747
1    0.867786
2   -1.626063
3   -1.134978
4   -1.027798
dtype: float64

Любое выражение, являющееся допустимым выражением pandas.eval(), также является допустимым выражением DataFrame.eval(), с дополнительным преимуществом, что вам не нужно добавлять имя DataFrame к столбцу(ам), которые вы хотите оценить.

Кроме того, вы можете выполнять присваивание столбцов в рамках выражения. Это позволяет выполнять *формульное вычисление*. Цель присваивания может быть новым именем столбца или существующим именем столбца, и она должна быть допустимым идентификатором Python.

Введено в версии 0.18.0.

Ключевое слово inplace определяет, будет ли это присваивание выполняться в исходном DataFrame или будет возвращена копия с новым столбцом.

Предупреждение

Для обратной совместимости, inplace по умолчанию равно True, если не указано иное. Это изменится в будущей версии pandas — если ваш код зависит от присваивания inplace, вы должны обновить его, явно установив inplace=True

In [24]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))

In [25]: df.eval('c = a + b', inplace=True)

In [26]: df.eval('d = a + b + c', inplace=True)

In [27]: df.eval('a = 1', inplace=True)

In [28]: df
Out[28]: 
   a  b   c   d
0  1  5   5  10
1  1  6   7  14
2  1  7   9  18
3  1  8  11  22
4  1  9  13  26

Когда inplace установлено в False, возвращается копия DataFrame с новыми или измененными столбцами, а исходная таблица остается неизменной.

In [29]: df
Out[29]: 
   a  b   c   d
0  1  5   5  10
1  1  6   7  14
2  1  7   9  18
3  1  8  11  22
4  1  9  13  26

In [30]: df.eval('e = a - c', inplace=False)
Out[30]: 
   a  b   c   d   e
0  1  5   5  10  -4
1  1  6   7  14  -6
2  1  7   9  18  -8
3  1  8  11  22 -10
4  1  9  13  26 -12

In [31]: df
Out[31]: 
   a  b   c   d
0  1  5   5  10
1  1  6   7  14
2  1  7   9  18
3  1  8  11  22
4  1  9  13  26

Введено в версии 0.18.0.

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

In [32]: df.eval("""
   ....: c = a + b
   ....: d = a + b + c
   ....: a = 1""", inplace=False)
   ....: 
Out[32]: 
   a  b   c   d
0  1  5   6  12
1  1  6   7  14
2  1  7   8  16
3  1  8   9  18
4  1  9  10  20

Эквивалент на стандартном Python:

In [33]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))

In [34]: df['c'] = df.a + df.b

In [35]: df['d'] = df.a + df.b + df.c

In [36]: df['a'] = 1

In [37]: df
Out[37]: 
   a  b   c   d
0  1  5   5  10
1  1  6   7  14
2  1  7   9  18
3  1  8  11  22
4  1  9  13  26

Введено в версии 0.18.0.

Метод query получил ключевое слово inplace, определяющее, изменяет ли запрос исходную таблицу.

In [38]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))

In [39]: df.query('a > 2')
Out[39]: 
   a  b
3  3  8
4  4  9

In [40]: df.query('a > 2', inplace=True)

In [41]: df
Out[41]: 
   a  b
3  3  8
4  4  9

Предупреждение

В отличие от eval, значение по умолчанию для inplace в query равно False. Это согласуется с предыдущими версиями pandas.

Локальные переменные

В pandas версии 0.14 API локальных переменных изменился. В pandas 0.13.x вы могли ссылаться на локальные переменные так же, как и в стандартном Python. Например,

df = pd.DataFrame(np.random.randn(5, 2), columns=['a', 'b'])
newcol = np.random.randn(len(df))
df.eval('b + newcol')

UndefinedVariableError: name 'newcol' is not defined

Как видно из сгенерированного исключения, этот синтаксис больше не разрешен. Вы должны *явно ссылаться* на любую локальную переменную, которую вы хотите использовать в выражении, поместив символ @ перед именем. Например,

In [42]: df = pd.DataFrame(np.random.randn(5, 2), columns=list('ab'))

In [43]: newcol = np.random.randn(len(df))

In [44]: df.eval('b + @newcol')
Out[44]: 
0   -0.173926
1    2.493083
2   -0.881831
3   -0.691045
4    1.334703
dtype: float64

In [45]: df.query('b < @newcol')
Out[45]: 
          a         b
0  0.863987 -0.115998
2 -2.621419 -1.297879

Если вы не добавите префикс @ к локальной переменной, pandas выбросит исключение, сообщая, что переменная не определена.

При использовании DataFrame.eval() и DataFrame.query(), это позволяет иметь локальную переменную и столбец DataFrame с одинаковым именем в выражении.

In [46]: a = np.random.randn()

In [47]: df.query('@a < a')
Out[47]: 
          a         b
0  0.863987 -0.115998

In [48]: df.loc[a < df.a]  # same as the previous expression
Out[48]: 
          a         b
0  0.863987 -0.115998

С pandas.eval() вы не можете использовать префикс @ *вообще*, потому что он не определен в этом контексте. pandas сообщит вам об этом, если вы попытаетесь использовать @ в вызове верхнего уровня pandas.eval(). Например,

In [49]: a, b = 1, 2

In [50]: pd.eval('@a + b')
  File "<string>", line unknown
SyntaxError: The '@' prefix is not allowed in top-level eval calls, 
please refer to your variables by name without the '@' prefix

В этом случае вам просто нужно обратиться к переменным так, как вы бы это сделали в стандартном Python.

In [51]: pd.eval('a + b')
Out[51]: 3

pandas.eval() Парсеры

Есть два разных парсера и два разных движка, которые можно использовать в качестве бэкенда.

По умолчанию парсер 'pandas' допускает более интуитивный синтаксис для выражения операций типа запроса (сравнения, союзы и дизъюнкции). В частности, приоритет операторов & и | приравнивается к приоритету соответствующих логических операций and и or.

Например, приведенное выше соединение можно записать без скобок. Кроме того, вы можете использовать парсер 'python' для обеспечения строгой семантики Python.

In [52]: expr = '(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)'

In [53]: x = pd.eval(expr, parser='python')

In [54]: expr_no_parens = 'df1 > 0 & df2 > 0 & df3 > 0 & df4 > 0'

In [55]: y = pd.eval(expr_no_parens, parser='pandas')

In [56]: np.all(x == y)
Out[56]: True

То же самое выражение можно «логически умножить» вместе со словом and:

In [57]: expr = '(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)'

In [58]: x = pd.eval(expr, parser='python')

In [59]: expr_with_ands = 'df1 > 0 and df2 > 0 and df3 > 0 and df4 > 0'

In [60]: y = pd.eval(expr_with_ands, parser='pandas')

In [61]: np.all(x == y)
Out[61]: True

Операторы and и or здесь имеют тот же приоритет, что и в обычном Python.

pandas.eval() Бэкэнды

Также есть возможность заставить eval() работать точно так же, как и обычный Python.

Примечание

Использование движка 'python' обычно *не* полезно, за исключением тестирования других движков оценки. Вы не получите никаких преимуществ по производительности, используя eval() с engine='python', и, на самом деле, можете столкнуться с потерей производительности.

Вы можете увидеть это, используя pandas.eval() с движком 'python'. Он немного медленнее (не сильно), чем оценка того же выражения в Python

In [62]: %timeit df1 + df2 + df3 + df4
10 loops, best of 3: 23.6 ms per loop
In [63]: %timeit pd.eval('df1 + df2 + df3 + df4', engine='python')
10 loops, best of 3: 23.9 ms per loop

pandas.eval() Производительность

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

_images/eval-perf.png

Примечание

Операции с небольшими объектами (около 15 000–20 000 строк) выполняются быстрее с использованием обычного Python:

_images/eval-perf-small.png

Этот график был создан с использованием DataFrame, содержащего 3 столбца, каждый из которых содержит значения с плавающей точкой, сгенерированные с помощью numpy.random.randn().

Технические нюансы оценивания выражений

Выражения, которые приведут к объектному типу данных или включают операции с датой и временем (из-за NaT) должны быть вычислены в пространстве Python. Основная причина этого поведения заключается в сохранении обратной совместимости с версиями numpy < 1.7. В этих версиях numpy вызов ndarray.astype(str) будет усекать любые строки, длина которых превышает 60 символов. Во-вторых, мы не можем передавать object массивы в numexpr, поэтому сравнения строк должны вычисляться в пространстве Python.

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

In [64]: df = pd.DataFrame({'strings': np.repeat(list('cba'), 3),
   ....:                    'nums': np.repeat(range(3), 3)})
   ....: 

In [65]: df
Out[65]: 
   nums strings
0     0       c
1     0       c
2     0       c
3     1       b
4     1       b
5     1       b
6     2       a
7     2       a
8     2       a

In [66]: df.query('strings == "a" and nums == 1')
Out[66]: 
Empty DataFrame
Columns: [nums, strings]
Index: []

числовая часть сравнения (nums == 1) будет вычислена numexpr.

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

© 2008–2012, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/0.18.1/enhancingperf.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API