Почему numpy ma.average в 24 раза медленнее, чем arr.mean?

Я нашел кое-что интересное в Python numpy. ma.average намного медленнее, чем arr.mean (arr — это массив)

>>> arr = np.full((3, 3), -9999, dtype=float)
array([[-9999., -9999., -9999.],
       [-9999., -9999., -9999.],
       [-9999., -9999., -9999.]])

%timeit np.ma.average(arr, axis=0)
The slowest run took 49.32 times longer than the fastest. This could mean that an intermediate result is being cached.
10000 loops, best of 3: 191 µs per loop

%timeit arr.mean(axis=0)
The slowest run took 6.63 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 7.41 µs per loop

со случайными числами

arr = np.random.random((3,3))

%timeit arr.mean(axis=0)
The slowest run took 6.17 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 7.78 µs per loop

%timeit np.ma.average(arr, axis=0)
1000 loops, best of 3: 186 µs per loop

--> Это почти в 24 раза медленнее.

Документация

numpy.ma.average(a, axis=None, weights=None, returned=False)

Возвращает weighted среднее значение массива по заданной оси.

numpy.mean(a, axis=None, dtype=None, out=None, keepdims)

Вычислить среднее арифметическое вдоль указанной оси.


Почему ma.average намного медленнее, чем arr.mean? Математически они одинаковы (поправьте меня, если я ошибаюсь). Я предполагаю, что это как-то связано с взвешенными параметрами в ma.average, но не должно ли быть резервного варианта, если веса не переданы?


person mumbala    schedule 08.08.2017    source источник
comment
Маскированные операции (вы видите .ma.?) работают медленно!   -  person sascha    schedule 08.08.2017
comment
Кроме того, тестирование на небольших объемах данных не является хорошей практикой: в чем разница между 7,78us и 186us? Немного. Вам нужно использовать большие матрицы.   -  person Willem Van Onsem    schedule 08.08.2017
comment
;) спасибо, не подумал об этом. В любом случае в данном массиве нет маски. нашел его в более старом коде, в котором раньше был замаскированный массив.   -  person mumbala    schedule 08.08.2017
comment
(наверное) не имеет значения. Различные вычислительные модели. Сравните np.mean и np.average (две функции без маски) и используйте большие данные!   -  person sascha    schedule 08.08.2017
comment
@WillemVanOnsem проверил его на большом количестве в проекте, это сократило время выполнения этой части на 7. 7,78 до 186 может иметь большое влияние, если он часто вызывается   -  person mumbala    schedule 08.08.2017
comment
@мумбала: я знаю это. Я лишь говорю, что в будущем лучше публиковать результаты испытаний на больших партиях. Если здесь ma.average будет перенаправлять на np.mean (чего нет), даже перенаправление может иметь огромное влияние.   -  person Willem Van Onsem    schedule 08.08.2017


Ответы (2)


Хороший способ узнать, почему что-то работает медленнее, — это профилировать его. Я буду использовать стороннюю библиотеку line_profiler и команду IPython %lprun (см., например, этот блог) здесь:

%load_ext line_profiler

import numpy as np
arr = np.full((3, 3), -9999, dtype=float)

%lprun -f np.ma.average np.ma.average(arr, axis=0)

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   519                                           def average(a, axis=None, weights=None, returned=False):
   ...
   570         1         1810   1810.0     30.5      a = asarray(a)
   571         1           15     15.0      0.3      m = getmask(a)
   572                                           
   573                                               # inspired by 'average' in numpy/lib/function_base.py
   574                                           
   575         1            5      5.0      0.1      if weights is None:
   576         1         3500   3500.0     59.0          avg = a.mean(axis)
   577         1          591    591.0     10.0          scl = avg.dtype.type(a.count(axis))
   578                                               else: 
   ...
   608                                           
   609         1            7      7.0      0.1      if returned:
   610                                                   if scl.shape != avg.shape:
   611                                                       scl = np.broadcast_to(scl, avg.shape).copy()
   612                                                   return avg, scl
   613                                               else:
   614         1            5      5.0      0.1          return avg

Я удалил несколько ненужных строк.

Так что на самом деле 30% времени тратится на np.ma.asarray (то, что arr.mean делать не нужно!).

Однако относительное время резко меняется, если вы используете больший массив:

arr = np.full((1000, 1000), -9999, dtype=float)

%lprun -f np.ma.average np.ma.average(arr, axis=0)
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   519                                           def average(a, axis=None, weights=None, returned=False):
   ...
   570         1          609    609.0      7.6      a = asarray(a)
   571         1           14     14.0      0.2      m = getmask(a)
   572                                           
   573                                               # inspired by 'average' in numpy/lib/function_base.py
   574                                           
   575         1            7      7.0      0.1      if weights is None:
   576         1         6924   6924.0     86.9          avg = a.mean(axis)
   577         1          404    404.0      5.1          scl = avg.dtype.type(a.count(axis))
   578                                               else:
   ...
   609         1            6      6.0      0.1      if returned:
   610                                                   if scl.shape != avg.shape:
   611                                                       scl = np.broadcast_to(scl, avg.shape).copy()
   612                                                   return avg, scl
   613                                               else:
   614         1            6      6.0      0.1          return avg

На этот раз функция np.ma.MaskedArray.mean занимает почти 90% времени.

Примечание. Вы также можете копнуть глубже и изучить np.ma.asarray, np.ma.MaskedArray.count или np.ma.MaskedArray.mean и проверить профили их линий. Но я просто хотел показать, что есть много вызываемых функций, которые добавляют накладные расходы.

Итак, следующий вопрос: изменилось ли также относительное время между np.ndarray.mean и np.ma.average? И, по крайней мере, на моем компьютере разница теперь намного меньше:

%timeit np.ma.average(arr, axis=0)
# 2.96 ms ± 91 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit arr.mean(axis=0)
# 1.84 ms ± 23.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

На этот раз даже не в 2 раза медленнее. Я предполагаю, что для еще больших массивов разница станет еще меньше.


Это также то, что на самом деле довольно часто встречается в NumPy:

Постоянные коэффициенты довольно высоки даже для простых функций numpy (см., например, мой ответ на вопрос "Производительность в другом методе векторизации в numpy"< /а>). Для np.ma эти постоянные факторы еще больше, особенно если вы не используете np.ma.MaskedArray в качестве входных данных. Но даже несмотря на то, что постоянные коэффициенты могут быть высокими, эти функции превосходно работают с большими массивами.

person MSeifert    schedule 08.08.2017
comment
Большое спасибо. Лучшего ответа и быть не могло! Также спасибо за line_profiler с примерами - person mumbala; 08.08.2017

Благодаря @WillemVanOnsem и @sascha в комментариях выше

Изменить: применяется к небольшим массивам, см. принятый ответ для получения дополнительной информации

  • Маскированные операции выполняются медленно, чтобы избежать этого:

    mask = self.local_pos_history[:, 0] > -9
    local_pos_hist_masked = self.local_pos_history[mask]
    avg = local_pos_hist_masked.mean(axis=0)
    

    старый в маске

    mask = np.ma.masked_where(self.local_pos_history > -9, self.local_pos_history)
    local_pos_hist_mask = self.local_pos_history[mask].reshape(len(self.local_pos_history) // 3, 3)
    avg_pos = self.local_pos_history
    
  • np.average почти равен arr.mean:

    %timeit np.average(arr, axis=0)
    The slowest run took 5.81 times longer than the fastest. This could mean that an intermediate result is being cached.
    100000 loops, best of 3: 9.89 µs per loop
    
    %timeit np.mean(arr, axis=0)
    The slowest run took 6.44 times longer than the fastest. This could mean that an intermediate result is being cached.
    100000 loops, best of 3: 8.74 µs per loop
    

просто для уточнения еще тесты на мелкой партии

person mumbala    schedule 08.08.2017