Pandas mask / где методы против NumPy np.где

я часто использую панды mask и where методы для более чистой логики при обновлении значений в серии условно. Однако для относительно критичного к производительности кода я замечаю значительное снижение производительности относительно numpy.where.

хотя я рад принять это для конкретных случаев, мне интересно знать:

  1. Do Pandas mask / where методы предлагают дополнительные функциональные возможности, помимо inplace / errors / try-cast параметры? Я понимаю эти 3 параметра, но редко использую их. Например, я понятия не имею, что level параметр ссылается на.
  2. есть ли нетривиальный встречный пример, где 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)