Pandas mask / где методы против NumPy np.где
я часто использую панды mask
и where
методы для более чистой логики при обновлении значений в серии условно. Однако для относительно критичного к производительности кода я замечаю значительное снижение производительности относительно numpy.where
.
хотя я рад принять это для конкретных случаев, мне интересно знать:
- Do Pandas
mask
/where
методы предлагают дополнительные функциональные возможности, помимоinplace
/errors
/try-cast
параметры? Я понимаю эти 3 параметра, но редко использую их. Например, я понятия не имею, чтоlevel
параметр ссылается на. - есть ли нетривиальный встречный пример, где
mask
/where
превосходитnumpy.where
? Если такой пример существует, он может повлиять на то, как я выбираю соответствующие методы продвижения вперед.
для справки, вот некоторые бенчмаркинг на панд 0.19.2 / Python 3.6.0:
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
assert (df[0].mask(df[0] > 0.5, 1).values == np.where(df[0] > 0.5, 1, df[0])).all()
%timeit df[0].mask(df[0] > 0.5, 1) # 145 ms per loop
%timeit np.where(df[0] > 0.5, 1, df[0]) # 113 ms per loop
производительность, похоже, расходится дальше для не скалярных значений:
%timeit df[0].mask(df[0] > 0.5, df[0]*2) # 338 ms per loop
%timeit np.where(df[0] > 0.5, df[0]*2, df[0]) # 153 ms per loop
1 ответов
я использую pandas 0.23.3 и Python 3.6, поэтому я вижу реальную разницу во времени работы только для вашего второго примера.
но давайте рассмотрим немного другую версию вашего второго примера (так что мы получаем2*df[0]
С дороги). Вот наша базовая линия на моей машине:
twice = df[0]*2
mask = df[0] > 0.5
%timeit np.where(mask, twice, df[0])
# 61.4 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit df[0].mask(mask, twice)
# 143 ms ± 5.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
версия Numpy примерно в 2,3 раза быстрее, чем панды.
Итак, давайте профилировать обе функции, чтобы увидеть разницу-профилирование-хороший способ получить большой представьте, когда вы не очень хорошо знакомы с основой кода: это быстрее, чем отладка и менее подвержены ошибкам, чем попытка выяснить, что происходит, просто прочитав код.
я на Linux и использовать perf
. Для версии numpy мы получаем (для листинга см. Приложение A):
>>> perf record python np_where.py
>>> perf report
Overhead Command Shared Object Symbol
68,50% python multiarray.cpython-36m-x86_64-linux-gnu.so [.] PyArray_Where
8,96% python [unknown] [k] 0xffffffff8140290c
1,57% python mtrand.cpython-36m-x86_64-linux-gnu.so [.] rk_random
как мы видим, львиная доля времени тратится на PyArray_Where
- около 69%. Неизвестный символ - это функция ядра (на самом деле clear_page
) - я бегу без привилегий root, поэтому символ не разрешен.
и для панд мы получаем (см. приложение B для код):
>>> perf record python pd_mask.py
>>> perf report
Overhead Command Shared Object Symbol
37,12% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
23,36% python libc-2.23.so [.] __memmove_ssse3_back
19,78% python [unknown] [k] 0xffffffff8140290c
3,32% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
1,48% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
совсем другая ситуация:
- панды не использовать
PyArray_Where
под капотом - самое заметное время-потребительvm_engine_iter_task
, которая составляет numexpr-функции. - происходит какое-то тяжелое копирование памяти -
__memmove_ssse3_back
использует о25
% времени! Вероятно, некоторые из функций ядра также подключены к памяти обращается.
на самом деле, панды-0.19 использовать PyArray_Where
под капотом, для более старой версии perf-отчет будет выглядеть так:
Overhead Command Shared Object Symbol
32,42% python multiarray.so [.] PyArray_Where
30,25% python libc-2.23.so [.] __memmove_ssse3_back
21,31% python [kernel.kallsyms] [k] clear_page
1,72% python [kernel.kallsyms] [k] __schedule
так что в основном он будет использовать np.where
под капотом + некоторые накладные расходы (все выше копирования данных, см. __memmove_ssse3_back
) тогда.
я не вижу сценария, в котором панды могли бы стать быстрее, чем numpy в версии панд 0.19 - это просто добавляет накладные расходы на функциональность numpy. Версия панды 0.23.3-это совсем другая история-здесь используется numexpr-модуль, очень возможно, что есть сценарии, для которых версия панды (по крайней мере, немного) быстрее.
я не уверен, что это копирование памяти действительно требуется/необходимо - возможно, его даже можно назвать ошибкой производительности, но я просто не знаю достаточно, чтобы быть уверенным.
мы могли бы помочь панд не копировать, отслаивая некоторые косвенные (проходя np.array
вместо pd.Series
). Для пример:
%timeit df[0].mask(mask.values > 0.5, twice.values)
# 75.7 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
теперь панды только на 25% медленнее. Перф говорит:
Overhead Command Shared Object Symbol
50,81% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
14,12% python [unknown] [k] 0xffffffff8140290c
9,93% python libc-2.23.so [.] __memmove_ssse3_back
4,61% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
2,01% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
гораздо меньше копирования данных, но все же больше, чем в версии numpy, которая в основном отвечает за накладные расходы.
мой ключ вынимает из него:
панды имеют потенциал, чтобы быть по крайней мере немного быстрее, чем numpy (потому что это возможно, чтобы быть быстрее). Однако несколько непрозрачная обработка pandas копирования данных затрудняет прогнозируйте, когда этот потенциал будет омрачен (ненужным) копированием данных.
при выполнении
where
/mask
является узким местом, я бы использовал numba / cython для повышения производительности - см. Мои довольно наивные попытки использовать numba и cython ниже.
идея взять
np.where(df[0] > 0.5, df[0]*2, df[0])
версия и исключить потребность создать временное-я.e,df[0]*2
.
как было предложено @max9111, используя numba:
import numba as nb
@nb.njit
def nb_where(df):
n = len(df)
output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert(np.where(df[0] > 0.5, twice, df[0])==nb_where(df[0].values)).all()
%timeit np.where(df[0] > 0.5, df[0]*2, df[0])
# 85.1 ms ± 1.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit nb_where(df[0].values)
# 17.4 ms ± 673 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
что примерно в 5 раз быстрее, чем версия numpy!
и вот моя гораздо менее успешная попытка улучшить производительность с помощью Cython:
%%cython -a
cimport numpy as np
import numpy as np
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[np.float64_t] output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert (df[0].mask(df[0] > 0.5, 2*df[0]).values == cy_where(df[0].values)).all()
%timeit cy_where(df[0].values)
# 66.7± 753 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
дает 25% скорости. Не уверен, почему cython настолько медленнее, чем numba.
объявления:
A: np_where.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
for _ in range(50):
np.where(df[0] > 0.5, twice, df[0])
B: pd_mask.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
mask = df[0] > 0.5
for _ in range(50):
df[0].mask(mask, twice)