Улучшение производительности
В этой части учебника мы рассмотрим, как ускорить определенные функции, работающие с pandas DataFrame, используя три различных техники: Cython, Numba и pandas.eval(). Мы увидим улучшение скорости примерно в 200 раз, когда используем Cython и Numba для тестовой функции, работающей по строкам в DataFrame. Использование pandas.eval() позволит ускорить суммирование примерно в 2 раза.
Примечание
В дополнение к выполнению шагов в этом руководстве, пользователям, заинтересованным в улучшении производительности, настоятельно рекомендуется установить рекомендуемые зависимости для pandas. Эти зависимости часто не устанавливаются по умолчанию, но обеспечат улучшение скорости, если они присутствуют.
Cython (написание расширений C для pandas)
Для многих случаев использования достаточно написать pandas на чистом Python и NumPy. Однако в некоторых вычислительно сложных приложениях можно добиться значительного ускорения, перенеся вычисления на cython.
В этом учебнике предполагается, что вы максимально оптимизировали код на Python, например, попытались удалить циклы for и использовать векторизацию NumPy. Всегда стоит сначала оптимизировать код на Python.
Этот учебник описывает типичный процесс cythonization медленного вычисления. Мы используем пример из документации Cython, но в контексте pandas. Наш окончательный cythonized-решение примерно в 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]:
a b N x
0 0.469112 -0.218470 585 x
1 -0.282863 -0.061645 841 x
2 -1.509059 -0.723780 251 x
3 -1.135632 0.551225 972 x
4 1.212112 -0.497767 181 x
.. ... ... ... ..
995 -1.512743 0.874737 374 x
996 0.933753 1.120790 246 x
997 -0.308013 0.198768 157 x
998 -0.079915 1.757555 977 x
999 -1.010589 -1.115680 770 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
...:
Мы достигаем результата, используя DataFrame.apply() (по строкам):
In [5]: %timeit df.apply(lambda x: integrate_f(x["a"], x["b"], x["N"]), axis=1)
122 ms +- 2.09 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)
Но, очевидно, этого недостаточно. Давайте посмотрим, где тратится время во время этой операции (ограничено четырьмя наиболее затратными вызовами), используя магическую функцию prun ipython:
In [6]: %prun -l 4 df.apply(lambda x: integrate_f(x["a"], x["b"], x["N"]), axis=1) # noqa E999
621314 function calls (621294 primitive calls) in 0.235 seconds
Ordered by: internal time
List reduced from 223 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
1000 0.132 0.000 0.200 0.000 <ipython-input-4-c2a74e076cf0>:1(integrate_f)
552423 0.067 0.000 0.067 0.000 <ipython-input-3-c138bdd570e3>:1(f)
3000 0.006 0.000 0.024 0.000 series.py:967(__getitem__)
3000 0.003 0.000 0.012 0.000 series.py:1075(_get_value)
Подавляющее большинство времени тратится внутри integrate_f или f, поэтому мы сосредоточим усилия на cythonization этих двух функций.
Обычный Cython
Сначала нам нужно импортировать магическую функцию Cython в IPython:
In [7]: %load_ext Cython
Теперь просто скопируем наши функции в Cython как есть (суффикс здесь для различения версий функций):
In [8]: %%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 [9]: %timeit df.apply(lambda x: integrate_f_plain(x["a"], x["b"], x["N"]), axis=1)
61.5 ms +- 401 us per loop (mean +- std. dev. of 7 runs, 10 loops each)
Уже это сократило время в три раза, неплохо для простого копирования и вставки.
Добавление типов
Мы получим ещё одно значительное улучшение, просто добавив информацию о типах:
In [10]: %%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 [11]: %timeit df.apply(lambda x: integrate_f_typed(x["a"], x["b"], x["N"]), axis=1)
12.9 ms +- 223 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
Теперь всё значительно быстрее! Это теперь более чем в десять раз быстрее, чем исходное решение на Python, и мы практически не изменили код. Посмотрим снова, что занимает много времени:
In [12]: %prun -l 4 df.apply(lambda x: integrate_f_typed(x["a"], x["b"], x["N"]), axis=1)
68891 function calls (68871 primitive calls) in 0.032 seconds
Ordered by: internal time
List reduced from 222 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
3000 0.005 0.000 0.022 0.000 series.py:967(__getitem__)
3000 0.003 0.000 0.011 0.000 series.py:1075(_get_value)
16173 0.002 0.000 0.003 0.000 {built-in method builtins.isinstance}
3000 0.002 0.000 0.003 0.000 base.py:3756(get_loc)
Использование ndarray
Видно много вызовов series! Создаётся Series для каждой строки, и вызываются get из индекса и ряда (три раза для каждой строки). Вызовы функций в Python дороги, поэтому, возможно, мы можем минимизировать их, cythonizing часть apply.
Примечание
Теперь мы передаём ndarrays в функцию Cython, и Cython прекрасно работает с NumPy.
In [13]: %%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, и записывая результат в массив нулей.
Предупреждение
Вы не можете напрямую передать Series как параметр типа ndarray в функцию Cython. Вместо этого передавайте фактический ndarray с использованием Series.to_numpy(). Причина в том, что определение Cython относится к ndarray, а не к переданному Series.
Поэтому не делайте так:
apply_integrate_f(df["a"], df["b"], df["N"])
А вместо этого используйте Series.to_numpy() для получения основного ndarray:
apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
Примечание
Циклы такого рода будут очень медленными в Python, но в Cython циклы по массивам NumPy быстрые.
In [14]: %timeit apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
1.28 ms +- 57.8 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
Мы получили ещё одно значительное улучшение. Посмотрим снова, что занимает много времени:
In [15]: %prun -l 4 apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
85 function calls in 0.001 seconds
Ordered by: internal time
List reduced from 24 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.001 0.001 {built-in method _cython_magic_7ad580c77c493a861f5c06a1c31a77c6.apply_integrate_f}
1 0.000 0.000 0.001 0.001 {built-in method builtins.exec}
3 0.000 0.000 0.000 0.000 frame.py:3756(__getitem__)
3 0.000 0.000 0.000 0.000 base.py:428(to_numpy)
Как можно было ожидать, основное время сейчас тратится в apply_integrate_f, поэтому, если мы хотим ещё улучшить производительность, мы должны сосредоточить усилия именно здесь.
Более продвинутые методы
Есть ещё надежда на улучшение. Вот пример использования более продвинутых методов Cython:
In [16]: %%cython
....: cimport cython
....: cimport numpy as np
....: import numpy as np
....: cdef np.float64_t f_typed(np.float64_t x) except? -2:
....: return x * (x - 1)
....: cpdef np.float64_t integrate_f_typed(np.float64_t a, np.float64_t b, np.int64_t N):
....: cdef np.int64_t i
....: cdef np.float64_t s = 0.0, dx
....: 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[np.float64_t] apply_integrate_f_wrap(
....: np.ndarray[np.float64_t] col_a,
....: np.ndarray[np.float64_t] col_b,
....: np.ndarray[np.int64_t] col_N
....: ):
....: cdef np.int64_t i, n = len(col_N)
....: assert len(col_a) == len(col_b) == n
....: cdef np.ndarray[np.float64_t] res = np.empty(n, dtype=np.float64)
....: for i in range(n):
....: res[i] = integrate_f_typed(col_a[i], col_b[i], col_N[i])
....: return res
....:
In [17]: %timeit apply_integrate_f_wrap(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
1.1 ms +- 6.49 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
Ещё быстрее, с оговоркой, что ошибка в нашем коде Cython (например, ошибка «плюс-минус один») может привести к ошибке сегментации, потому что проверка доступа к памяти не выполняется. Подробнее о boundscheck и wraparound, см. в документации Cython по директивам компилятора.
Numba (JIT-компиляция)
Альтернативой статической компиляции кода Cython является использование динамического компилятора Just-in-time (JIT) с Numba.
Numba позволяет вам написать чисто Python-функцию, которая может быть JIT-скомпилирована в машинные инструкции, аналогично по производительности C, C++ и Fortran, путем декорирования вашей функции с помощью @jit.
Numba генерирует оптимизированный машинный код с использованием инфраструктуры компилятора LLVM во время импорта, выполнения или статически (с помощью инструмента pycc). Numba поддерживает компиляцию Python для выполнения на аппаратном обеспечении CPU или GPU и разработана для интеграции с Python-научным стеком.
Примечание
Компиляция с помощью @jit добавит накладные расходы к времени выполнения функции, поэтому преимущества производительности могут не быть реализованы, особенно при использовании небольших наборов данных. Рассмотрите возможность кеширования вашей функции, чтобы избежать накладных расходов на компиляцию каждый раз, когда ваша функция выполняется.
Numba может использоваться двумя способами с pandas:
Укажите ключевое слово
engine="numba"в выбранных методах pandasОпределите собственную Python-функцию, декорированную
@jit, и передайте базовое NumPy-массивSeriesилиDataFrame(используяto_numpy()).
Двигатель pandas Numba
Если Numba установлена, можно указать engine="numba" в выбранных методах pandas, чтобы выполнить метод с использованием Numba. Методы, которые поддерживают engine="numba", также будут иметь ключевое слово engine_kwargs, которое принимает словарь, позволяющий указать ключи "nogil", "nopython" и "parallel" с булевыми значениями для передачи в декоратор @jit. Если engine_kwargs не указан, он по умолчанию равен {"nogil": False, "nopython": True, "parallel": False}, если не указано иное.
С точки зрения производительности, первое выполнение функции с помощью двигателя Numba будет медленным, так как Numba потребует накладных расходов на компиляцию функции. Однако скомпилированные JIT-функции кешируются, и последующие вызовы будут быстрыми. В целом, двигатель Numba эффективен с большим количеством точек данных (например, 1+ миллион).
In [1]: data = pd.Series(range(1_000_000)) # noqa: E225
In [2]: roll = data.rolling(10)
In [3]: def f(x):
...: return np.sum(x) + 5
# Run the first time, compilation time will affect performance
In [4]: %timeit -r 1 -n 1 roll.apply(f, engine='numba', raw=True)
1.23 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
# Function is cached and performance will improve
In [5]: %timeit roll.apply(f, engine='numba', raw=True)
188 ms ± 1.93 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [6]: %timeit roll.apply(f, engine='cython', raw=True)
3.92 s ± 59 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Если ваше вычислительное оборудование содержит несколько процессоров, наибольший прирост производительности может быть достигнут путем установки parallel в True для использования более одного процессора. Внутри pandas использует numba для распараллеливания вычислений по столбцам DataFrame; следовательно, эта выгода от производительности полезна только для DataFrame с большим количеством столбцов.
In [1]: import numba
In [2]: numba.set_num_threads(1)
In [3]: df = pd.DataFrame(np.random.randn(10_000, 100))
In [4]: roll = df.rolling(100)
In [5]: %timeit roll.mean(engine="numba", engine_kwargs={"parallel": True})
347 ms ± 26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [6]: numba.set_num_threads(2)
In [7]: %timeit roll.mean(engine="numba", engine_kwargs={"parallel": True})
201 ms ± 2.97 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Примеры пользовательских функций
Пользовательская Python-функция, декорированная с помощью @jit, может использоваться с объектами pandas, передавая их представления NumPy-массивов с помощью to_numpy().
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"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy()
)
return pd.Series(result, index=df.index, name="result")
In [4]: %timeit compute_numba(df)
1000 loops, best of 3: 798 us per loop
В этом примере использование Numba было быстрее, чем Cython.
Numba также может быть использована для написания векторизованных функций, которые не требуют от пользователя явного перебора наблюдений вектора; векторизованная функция будет применяться к каждой строке автоматически. Рассмотрим следующий пример удвоения каждого наблюдения:
import numba
def double_every_value_nonumba(x):
return x * 2
@numba.vectorize
def double_every_value_withnumba(x): # noqa E501
return x * 2
# Custom function without numba
In [5]: %timeit df["col1_doubled"] = df["a"].apply(double_every_value_nonumba) # noqa E501
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"].to_numpy())
1000 loops, best of 3: 145 us per loop
Ограничения
Numba лучше всего ускоряет функции, которые применяют числовые функции к NumPy-массивам. Если вы попытаетесь @jit функцию, которая содержит неподдерживаемый Python или NumPy код, компиляция вернётся в объектный режим, что вряд ли ускорит вашу функцию. Если вы хотите, чтобы Numba выдавала ошибку, если не может скомпилировать функцию таким образом, чтобы ускорить ваш код, передайте Numba аргумент nopython=True (например, @jit(nopython=True)). Подробнее о устранении неполадок с режимами Numba см. на странице устранения неполадок Numba.
Использование parallel=True (например, @jit(parallel=True)) может привести к SIGABRT, если уровень потоков приводит к небезопасному поведению. Вы можете сначала указать безопасный уровень потоков перед запуском JIT-функции с parallel=True.
В общем случае, если вы столкнулись с ошибкой сегментации (SIGSEGV) при использовании Numba, пожалуйста, сообщите об этом в трекер проблем Numba.
Вычисление выражений с помощью eval()
Функция верхнего уровня pandas.eval() реализует вычисление выражений для объектов Series и DataFrame.
Примечание
Чтобы воспользоваться функцией eval(), вам необходимо установить numexpr. Подробности см. в разделе рекомендуемых зависимостей.
Использование eval() для вычисления выражений вместо обычного Python обусловлено двумя причинами: 1) большие объекты DataFrame обрабатываются эффективнее, и 2) большие арифметические и логические выражения вычисляются единовременно с помощью подчинённого движка (по умолчанию используется numexpr для вычислений).
Примечание
Не следует использовать eval() для простых выражений или выражений, включающих небольшие DataFrames. Фактически, 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_boollistи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иlog10.
Этот синтаксис Python не разрешен:
-
Выражения
Вызовы функций, кроме математических функций.
is/is notоперацииifвыраженияlambdaвыраженияlist/set/dictгенераторыЛитералы
dictиsetвыраженияyieldвыраженияГенераторы
Логические выражения, состоящие только из скалярных значений
-
Операторы
Разрешены ни простые, ни составные операторы. Это включает в себя такие конструкции как
for,while, иif.
eval() примеры
pandas.eval() хорошо работает с выражениями, содержащими большие массивы.
Сначала давайте создадим несколько достаточно больших массивов для работы:
In [18]: nrows, ncols = 20000, 100
In [19]: df1, df2, df3, df4 = [pd.DataFrame(np.random.randn(nrows, ncols)) for _ in range(4)]
Теперь давайте сравним сложение их с помощью обычного Python и eval():
In [20]: %timeit df1 + df2 + df3 + df4
26.4 ms +- 1.39 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)
In [21]: %timeit pd.eval("df1 + df2 + df3 + df4")
12.8 ms +- 335 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
Теперь сделаем то же самое, но с сравнениями:
In [22]: %timeit (df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)
23 ms +- 1.61 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)
In [23]: %timeit pd.eval("(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)")
31 ms +- 2.51 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)
eval() также работает с невыровненными объектами pandas:
In [24]: s = pd.Series(np.random.randn(50))
In [25]: %timeit df1 + df2 + df3 + df4 + s
34.2 ms +- 623 us per loop (mean +- std. dev. of 7 runs, 10 loops each)
In [26]: %timeit pd.eval("df1 + df2 + df3 + df4 + s")
13.9 ms +- 130 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
Примечание
Операции, такие как
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()
В дополнение к функции верхнего уровня pandas.eval() вы также можете оценить выражение в «контексте» DataFrame.
In [27]: df = pd.DataFrame(np.random.randn(5, 2), columns=["a", "b"])
In [28]: df.eval("a + b")
Out[28]:
0 -0.246747
1 0.867786
2 -1.626063
3 -1.134978
4 -1.027798
dtype: float64
Любое выражение, которое является допустимым выражением pandas.eval(), также является допустимым выражением DataFrame.eval() с дополнительным преимуществом, что вам не нужно добавлять имя DataFrame к столбцу(ам), которые вы хотите оценить.
Кроме того, вы можете выполнять присваивание столбцов внутри выражения. Это позволяет выполнять формульные вычисления. Цель назначения может быть новым именем столбца или существующим именем столбца, и она должна быть допустимым идентификатором Python.
Ключевое слово inplace определяет, будет ли это назначение выполнено на исходном DataFrame или возвращена копия с новым столбцом.
In [29]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [30]: df.eval("c = a + b", inplace=True)
In [31]: df.eval("d = a + b + c", inplace=True)
In [32]: df.eval("a = 1", inplace=True)
In [33]: df
Out[33]:
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 [34]: df
Out[34]:
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 [35]: df.eval("e = a - c", inplace=False)
Out[35]:
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 [36]: df
Out[36]:
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 [37]: df.eval(
....: """
....: c = a + b
....: d = a + b + c
....: a = 1""",
....: inplace=False,
....: )
....:
Out[37]:
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 [38]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [39]: df["c"] = df["a"] + df["b"]
In [40]: df["d"] = df["a"] + df["b"] + df["c"]
In [41]: df["a"] = 1
In [42]: df
Out[42]:
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
Метод DataFrame.query имеет ключевое слово inplace, которое определяет, изменяет ли запрос исходный фрейм.
In [43]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [44]: df.query("a > 2")
Out[44]:
a b
3 3 8
4 4 9
In [45]: df.query("a > 2", inplace=True)
In [46]: df
Out[46]:
a b
3 3 8
4 4 9
Локальные переменные
Вы должны явно ссылаться на любые локальные переменные, которые вы хотите использовать в выражении, помещая символ @ перед именем. Например,
In [47]: df = pd.DataFrame(np.random.randn(5, 2), columns=list("ab"))
In [48]: newcol = np.random.randn(len(df))
In [49]: df.eval("b + @newcol")
Out[49]:
0 -0.173926
1 2.493083
2 -0.881831
3 -0.691045
4 1.334703
dtype: float64
In [50]: df.query("b < @newcol")
Out[50]:
a b
0 0.863987 -0.115998
2 -2.621419 -1.297879
Если вы не добавите префикс @ к локальной переменной, pandas выведет исключение, сообщая, что переменная не определена.
При использовании DataFrame.eval() и DataFrame.query(), это позволяет вам иметь локальную переменную и столбец DataFrame с одинаковым именем в выражении.
In [51]: a = np.random.randn()
In [52]: df.query("@a < a")
Out[52]:
a b
0 0.863987 -0.115998
In [53]: df.loc[a < df["a"]] # same as the previous expression
Out[53]:
a b
0 0.863987 -0.115998
При использовании pandas.eval() вы не можете использовать префикс @ вообще, потому что он не определён в этом контексте. pandas сообщит вам об этом, если вы попытаетесь использовать @ в вызове pandas.eval() на верхнем уровне. Например,
In [54]: a, b = 1, 2
In [55]: pd.eval("@a + b")
Traceback (most recent call last):
File ~/micromamba/envs/test/lib/python3.8/site-packages/IPython/core/interactiveshell.py:3378 in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
Cell In [55], line 1
pd.eval("@a + b")
File ~/work/pandas/pandas/pandas/core/computation/eval.py:343 in eval
_check_for_locals(expr, level, parser)
File ~/work/pandas/pandas/pandas/core/computation/eval.py:168 in _check_for_locals
raise SyntaxError(msg)
File <string>
SyntaxError: The '@' prefix is not allowed in top-level eval calls.
please refer to your variables by name without the '@' prefix.
В этом случае вам следует просто обратиться к переменным, как вы делали бы в стандартном Python.
In [56]: pd.eval("a + b")
Out[56]: 3
pandas.eval() парсеры
Существуют два разных парсера и два разных движка, которые можно использовать в качестве бэкенда.
По умолчанию 'pandas' парсер позволяет более интуитивно выражать операции типа запросов (сравнения, союзы и дизъюнкции). В частности, приоритет операторов & и | равен приоритету соответствующих булевых операций and и or.
Например, вышеуказанное объединение можно записать без скобок. В качестве альтернативы, вы можете использовать парсер 'python' для соблюдения строгих семантик Python.
In [57]: expr = "(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)"
In [58]: x = pd.eval(expr, parser="python")
In [59]: expr_no_parens = "df1 > 0 & df2 > 0 & df3 > 0 & df4 > 0"
In [60]: y = pd.eval(expr_no_parens, parser="pandas")
In [61]: np.all(x == y)
Out[61]: True
Это же выражение может быть «и» с помощью слова and:
In [62]: expr = "(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)"
In [63]: x = pd.eval(expr, parser="python")
In [64]: expr_with_ands = "df1 > 0 and df2 > 0 and df3 > 0 and df4 > 0"
In [65]: y = pd.eval(expr_with_ands, parser="pandas")
In [66]: np.all(x == y)
Out[66]: True
Операторы and и or здесь имеют тот же приоритет, что и в обычном Python.
pandas.eval() бэкенды
Также есть возможность сделать eval() идентичным простому Python.
Примечание
Использование движка 'python' обычно не полезно, за исключением тестирования других движков оценки. Вы не получите никаких преимуществ в производительности, используя eval() с engine='python', и фактически можете столкнуться с потерей производительности.
Вы можете увидеть это, используя pandas.eval() с движком 'python'. Он немного медленнее (ненамного), чем оценка того же выражения в Python.
In [67]: %timeit df1 + df2 + df3 + df4
22 ms +- 646 us per loop (mean +- std. dev. of 7 runs, 10 loops each)
In [68]: %timeit pd.eval("df1 + df2 + df3 + df4", engine="python")
23.3 ms +- 296 us per loop (mean +- std. dev. of 7 runs, 10 loops each)
pandas.eval() производительность
eval() предназначен для ускорения определенных типов операций. В частности, те операции, которые включают сложные выражения с большими объектами DataFrame/Series, должны показать значительное улучшение производительности. Ниже приведена диаграмма, показывающая время выполнения pandas.eval() в зависимости от размера фрейма, участвующего в вычислении. Две линии соответствуют двум различным движкам.
Примечание
Операции с небольшими объектами (около 15 000-20 000 строк) выполняются быстрее с использованием обычного Python:
Этот график был создан с использованием DataFrame с 3 столбцами, каждый из которых содержит значения с плавающей точкой, сгенерированные с помощью numpy.random.randn().
Технические детали, касающиеся оценки выражений
Выражения, которые приведут к типу данных объекта или включают операции с датой и временем (из-за NaT) должны быть оценены в пространстве Python. Основная причина этого поведения — сохранение обратной совместимости с версиями NumPy < 1.7. В этих версиях NumPy вызов ndarray.astype(str) будет усекать любые строки, длина которых превышает 60 символов. Во-вторых, мы не можем передавать object массивы в numexpr, поэтому сравнения строк должны оцениваться в пространстве Python.
Вывод заключается в том, что это только относится к выражениям типа объекта. Итак, если у вас есть выражение, например
In [69]: df = pd.DataFrame(
....: {"strings": np.repeat(list("cba"), 3), "nums": np.repeat(range(3), 3)}
....: )
....:
In [70]: df
Out[70]:
strings nums
0 c 0
1 c 0
2 c 0
3 b 1
4 b 1
5 b 1
6 a 2
7 a 2
8 a 2
In [71]: df.query("strings == 'a' and nums == 1")
Out[71]:
Empty DataFrame
Columns: [strings, nums]
Index: []
числовая часть сравнения (nums == 1) будет оценена numexpr.
В общем случае, DataFrame.query()/pandas.eval() будут оценивать подвыражения, которые могут быть оценены numexpr и те, которые должны быть оценены в пространстве Python, прозрачно для пользователя. Это делается путем вывода типа результата выражения из его аргументов и операторов.
© 2008–2022, 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/1.5.0/user_guide/enhancingperf.html