Улучшение производительности
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)
671915 function calls (666906 primitive calls) in 0.379 seconds
Ordered by: internal time
List reduced from 128 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
1000 0.193 0.000 0.290 0.000 <ipython-input-4-91e33489f136>:1(integrate_f)
552423 0.089 0.000 0.089 0.000 <ipython-input-3-bc41a25943f6>:1(f)
3000 0.011 0.000 0.060 0.000 base.py:2146(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 cythonmagic):
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)
118490 function calls (113481 primitive calls) in 0.093 seconds
Ordered by: internal time
List reduced from 124 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
3000 0.011 0.000 0.064 0.000 base.py:2146(get_value)
3000 0.006 0.000 0.072 0.000 series.py:600(__getitem__)
3000 0.005 0.000 0.014 0.000 base.py:1131(_convert_scalar_indexer)
9024 0.005 0.000 0.012 0.000 {getattr}
Использование ndarray
Это вызывает series... очень много! Это создает Series из каждой строки и получает данные из индекса и series (три раза для каждой строки). Вызовы функций в Python медленные, поэтому, возможно, мы можем минимизировать их, цитонизируя часть apply.
Примечание
Сейчас мы передаем ndarray в цитоновскую функцию, к счастью, 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 в цитоновскую функцию. Вместо этого передайте фактический 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.002 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.002 0.002 0.002 0.002 {_cython_magic_40485b2751cb6bc085f3a7be0856f402.apply_integrate_f}
3 0.000 0.000 0.000 0.000 internals.py:4031(__init__)
9 0.000 0.000 0.000 0.000 generic.py:2746(__setattr__)
3 0.000 0.000 0.000 0.000 internals.py:3565(iget)
Как можно было ожидать, большая часть времени сейчас тратится в 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-коды могут быть скомпилированы в машинные инструкции в режиме реального времени с производительностью, сопоставимой с 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
Векторизация
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: 24.6 ms per loop
In [16]: %timeit pd.eval('df1 + df2 + df3 + df4')
100 loops, best of 3: 8.36 ms per loop
Теперь давайте сделаем то же самое, но с сравнениями:
In [17]: %timeit (df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0) 10 loops, best of 3: 30.9 ms per loop
In [18]: %timeit pd.eval('(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)')
100 loops, best of 3: 16.4 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: 38.4 ms per loop
In [21]: %timeit pd.eval('df1 + df2 + df3 + df4 + s')
100 loops, best of 3: 9.31 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=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: 24.2 ms per loop
In [63]: %timeit pd.eval('df1 + df2 + df3 + df4', engine='python')
10 loops, best of 3: 25.2 ms per loop
pandas.eval() Производительность
eval() предназначен для ускорения определённых типов операций. В частности, те операции, включающие сложные выражения с большими объектами DataFrame/Series, должны получить значительное ускорение. На этом графике показано время выполнения pandas.eval() в зависимости от размера фрейма, участвующего в вычислениях. Две линии соответствуют двум различным движкам.
Примечание
Операции с небольшими объектами (около 15-20 тысяч строк) выполняются быстрее с помощью обычного Python:
Этот график был создан с использованием фрейма 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.19.2/enhancingperf.html