Ответ 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)
Версия для компакт-дисков примерно в 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
Совсем другая ситуация:
- pandas не использует
PyArray_Where
под капотом - самым известным временем-потребителем являетсяvm_engine_iter_task
, что является numexpr-функциональностью. - происходит некоторая интенсивная
__memmove_ssse3_back
памяти -__memmove_ssse3_back
использует около25
% времени! Вероятно, некоторые из функций ядра также связаны с доступом к памяти.
Фактически, pandas-0.19 использовал PyArray_Where
под капотом, для более старой версии перфо-отчет будет выглядеть так:
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 в версии pansas 0.19, - это просто добавляет накладные расходы к функциональности numpy. Версия Pandas 0.23.3 - совершенно другая история - здесь используется numexpr-модуль, очень возможно, что есть сценарии, для которых версия pandas (по крайней мере, немного) быстрее.
Я не уверен, что это копирование памяти действительно вызвано/необходимо - возможно, даже можно назвать это ошибкой производительности, но я просто недостаточно уверен, чтобы быть уверенным.
Мы могли бы помочь пандам не копировать, отрывая некоторые косвенные действия (передавая 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, которая в основном отвечает за накладные расходы.
Мой ключ от него:
-
pandas может быть как минимум немного быстрее, чем numpy (потому что это возможно быть быстрее). Тем не менее, непрозрачная обработка данных pandas при копировании данных затрудняет прогнозирование, когда этот потенциал омрачается (ненужным) копированием данных.
-
когда производительность
where
/mask
является узким местом, я бы использовал numba/cython для повышения производительности - см. мои довольно наивные попытки использовать numba и cython далее ниже.
Идея состоит в том, чтобы принять
np.where(df[0] > 0.5, df[0]*2, df[0])
версии и устранить необходимость создания временного - т.е. 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)