Продуктивність Pandas застосовується проти np.vectorize для створення нового стовпця з існуючих стовпців


81

Я використовую фрейми даних Pandas і хочу створити новий стовпець як функцію існуючих стовпців. Я не бачив хорошого обговорення різниці швидкості між df.apply()і np.vectorize(), тому я подумав, що запитаю тут.

Функція Панди apply()працює повільно. З того, що я виміряв (показано нижче в деяких експериментах), використання np.vectorize()в 25 разів швидше (або більше), ніж використання функції DataFrame apply(), принаймні на моєму MacBook Pro 2016 року. Це очікуваний результат і чому?

Наприклад, припустимо, у мене є такий фрейм даних із Nрядками:

N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
#     A   B
# 0  78  50
# 1  23  91
# 2  55  62
# 3  82  64
# 4  99  80

Припустимо далі, що я хочу створити новий стовпець як функцію двох стовпців Aі B. У наведеному нижче прикладі я буду використовувати просту функцію divide(). Щоб застосувати функцію, я можу використовувати df.apply()або np.vectorize():

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)

df['result2'] = np.vectorize(divide)(df['A'], df['B'])

df.head()
#     A   B    result   result2
# 0  78  50  1.560000  1.560000
# 1  23  91  0.252747  0.252747
# 2  55  62  0.887097  0.887097
# 3  82  64  1.281250  1.281250
# 4  99  80  1.237500  1.237500

Якщо я збільшуся Nдо реальних розмірів, таких як 1 мільйон або більше, то я спостерігаю, що np.vectorize()це в 25 разів швидше або більше, ніж df.apply().

Нижче наведено деякий повний код тестування:

import pandas as pd
import numpy as np
import time

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

for N in [1000, 10000, 100000, 1000000, 10000000]:    

    print ''
    A_list = np.random.randint(1, 100, N)
    B_list = np.random.randint(1, 100, N)
    df = pd.DataFrame({'A': A_list, 'B': B_list})

    start_epoch_sec = int(time.time())
    df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
    end_epoch_sec = int(time.time())
    result_apply = end_epoch_sec - start_epoch_sec

    start_epoch_sec = int(time.time())
    df['result2'] = np.vectorize(divide)(df['A'], df['B'])
    end_epoch_sec = int(time.time())
    result_vectorize = end_epoch_sec - start_epoch_sec


    print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
            (N, result_apply, result_vectorize)

    # Make sure results from df.apply and np.vectorize match.
    assert(df['result'].equals(df['result2']))

Результати показані нижче:

N=1000, df.apply: 0 sec, np.vectorize: 0 sec

N=10000, df.apply: 1 sec, np.vectorize: 0 sec

N=100000, df.apply: 2 sec, np.vectorize: 0 sec

N=1000000, df.apply: 24 sec, np.vectorize: 1 sec

N=10000000, df.apply: 262 sec, np.vectorize: 4 sec

Якщо np.vectorize()взагалі завжди швидше ніж df.apply(), то чому np.vectorize()не згадується більше? Я бачу лише дописи StackOverflow, пов’язані з df.apply()такими, як:

pandas створюють новий стовпець на основі значень з інших стовпців

Як я використовую функцію Pandas 'apply' для кількох стовпців?

Як застосувати функцію до двох стовпців фрейму даних Pandas


Я не заглиблювався в деталі вашого запитання, але np.vectorizeв основному це forцикл python (це зручний метод), а applyз лямбда також у часі python
roganjosh

"Якщо np.vectorize () загалом завжди швидший, ніж df.apply (), то чому np.vectorize () більше не згадується?" Тому що ви не повинні використовувати applyпослідовно, якщо це не потрібно, і, очевидно, векторизована функція буде виконувати невекторизовану.
PMende,

1
@PMende, але np.vectorizeне векторизується. Це добре відоме неправильне
слово

1
@PMende, звичайно, я не мав на увазі іншого. Ви не повинні виводити свої думки щодо впровадження з таймінгу. Так, вони проникливі. Але вони можуть змусити вас припускати речі, які не відповідають дійсності.
jpp

3
@PMende має гру з прихильниками панд .str. У багатьох випадках вони повільніші, ніж розуміння списків. Ми припускаємо занадто багато.
roganjosh

Відповіді:


115

Я почати з того , що сила PANDAS і NumPy масивів походить від високопродуктивних vectorised розрахунків по числових масивів. 1 Вся суть векторизованих обчислень полягає у тому, щоб уникнути циклів рівня Python, перемістивши обчислення до високооптимізованого коду C та використовуючи суміжні блоки пам'яті. 2

Цикли рівня Python

Тепер ми можемо поглянути на деякі терміни. Нижче наведені всі цикли рівня Python, які створюють або об'єкти pd.Series, np.ndarrayабо listоб'єкти, що містять однакові значення. Для цілей присвоєння ряду в рамках кадру даних результати можна порівняти.

# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0

np.random.seed(0)
N = 10**5

%timeit list(map(divide, df['A'], df['B']))                                   # 43.9 ms
%timeit np.vectorize(divide)(df['A'], df['B'])                                # 48.1 ms
%timeit [divide(a, b) for a, b in zip(df['A'], df['B'])]                      # 49.4 ms
%timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)]     # 112 ms
%timeit df.apply(lambda row: divide(*row), axis=1, raw=True)                  # 760 ms
%timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1)              # 4.83 s
%timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()]  # 11.6 s

Деякі винос:

  1. Методи на tupleоснові (перші 4) є фактором ефективнішим, ніж pd.Seriesметоди на основі (останні 3).
  2. np.vectorize, розуміння списку + zipта mapметоди, тобто 3 найкращі, мають приблизно однакову ефективність. Це тому, що вони використовують tuple та обходять деякі накладні витрати від Pandas pd.DataFrame.itertuples.
  3. Існує значне поліпшення швидкості від використання raw=Trueз pd.DataFrame.applyпроти і без. Цей параметр подає масиви NumPy до користувацької функції замість pd.Seriesоб’єктів.

pd.DataFrame.apply: просто черговий цикл

Щоб точно бачити об'єкти, які проходить Панда, ви можете тривіально змінити свою функцію:

def foo(row):
    print(type(row))
    assert False  # because you only need to see this once
df.apply(lambda row: foo(row), axis=1)

Вихід: <class 'pandas.core.series.Series'>. Створення, передача та запит об’єкта серії Pandas несе значні накладні витрати щодо масивів NumPy. Це не повинно дивувати: серії Pandas містять пристойну кількість риштування для вміщення індексу, значень, атрибутів тощо.

Знову зробіть ту саму вправу, raw=Trueі ви побачите <class 'numpy.ndarray'>. Все це описано в документах, але бачити це переконливіше.

np.vectorize: фальшива векторизація

Документи для np.vectorizeмають наступну примітку:

Векторизована функція обчислює pyfuncпослідовні кортежі вхідних масивів, як функція python map, за винятком того, що вона використовує правила трансляції numpy.

"Правила мовлення" тут не мають значення, оскільки вхідні масиви мають однакові розміри. Паралель до mapє повчальною, оскільки mapверсія вище має майже однакову продуктивність. У вихідному коді показує , що відбувається: np.vectorizeперетворює вхідну функцію в функцію універсальної ( «ufunc») через np.frompyfunc. Існує певна оптимізація, наприклад кешування, що може призвести до певного покращення продуктивності.

Коротше кажучи, np.vectorizeробить те, що повинен робити цикл на рівні Python , але pd.DataFrame.applyдодає об’ємні накладні витрати. Немає жодної компіляції JIT, яку ви бачите numba(див. Нижче). Це просто зручність .

Справжня векторизація: що слід використовувати

Чому вищезазначені відмінності ніде не згадані? Оскільки виконання справді векторизованих обчислень робить їх неактуальними:

%timeit np.where(df['B'] == 0, 0, df['A'] / df['B'])       # 1.17 ms
%timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0)  # 1.96 ms

Так, це ~ 40 разів швидше, ніж найшвидше з наведених вище циклічних рішень. Будь-яке з них є прийнятним. На мою думку, перше - лаконічне, читабельне та ефективне. Подивіться лише на інші методи, наприклад, numbaнижче, якщо продуктивність є критичною, і це є частиною вашого вузького місця.

numba.njit: більша ефективність

Коли цикли будуть розглянуті життєздатними , вони, як правило , оптимізовані з допомогою numbaз основною Numpy масиви рухатися якомога більше , щоб C.

Дійсно, numbaпокращує продуктивність до мікросекунд . Без певної громіздкої роботи важко буде набагато ефективніше цього.

from numba import njit

@njit
def divide(a, b):
    res = np.empty(a.shape)
    for i in range(len(a)):
        if b[i] != 0:
            res[i] = a[i] / b[i]
        else:
            res[i] = 0
    return res

%timeit divide(df['A'].values, df['B'].values)  # 717 µs

Використання @njit(parallel=True)може забезпечити подальший приріст для більших масивів.


1 Числові типи включають в себе: int, float, datetime, bool, category. Вони виключають object dtype і можуть міститися в суміжних блоках пам'яті.

2 Існує щонайменше 2 причини, чому операції NumPy ефективніші порівняно з Python:

  • Усе в Python - це об’єкт. Сюди входять, на відміну від С, числа. Отже, типи Python мають накладні витрати, яких не існує з типовими типами C.
  • Методи NumPy, як правило, засновані на C. Крім того, там, де це можливо, використовуються оптимізовані алгоритми.

1
@jpp: Використання декоратора з parallelаргументами @njit(parallel=True)дає мені подальше покращення порівняно з просто @njit. Можливо, ви теж можете додати це.
Sheldore

1
У вас є подвійна перевірка на b [i]! = 0. Звичайна поведінка Python і Numba полягає в тому, щоб перевірити на 0 і вивести помилку. Це, ймовірно, порушує будь-яку векторизацію SIMD і зазвичай сильно впливає на швидкість виконання. Але ви можете змінити це в Numba на @njit (error_model = 'numpy'), щоб уникнути цієї подвійної перевірки на ділення на 0. Також рекомендується виділити пам'ять за допомогою np.empty і встановити результат на 0 в операторі else.
max9111

1
error_model numpy використовує те, що дає процесор у поділі на 0 -> NaN. Принаймні в Numba 0.41dev обидві версії використовують SIMD-векторизацію. Ви можете перевірити це, як описано тут numba.pydata.org/numba-doc/dev/user/faq.html (1.16.2.3. Чому мій цикл не векторизований?), Я просто додав би оператор else до вашої функції (res [ i] = 0.) і всікокатувати пам'ять np.empty. Це у поєднанні з error_model = 'numpy' може покращити продуктивність приблизно на 20%. У старих версіях Numba був більший вплив на продуктивність ...
max9111

2
@ stackoverflowuser2010, Універсальної відповіді "для довільних функцій" немає. Ви повинні вибрати правильний інструмент для правильної роботи, який є частиною розуміння програмування / алгоритмів.
jpp

1
Щасливих свят!
cs95

5

Чим складнішими стають ваші функції (тобто, чим менше numpyможна переходити до власних внутрішніх органів), тим більше ви побачите, що продуктивність не буде такою різною. Наприклад:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))

def parse_name(name):
    if name.lower().startswith('a'):
        return 'A'
    elif name.lower().startswith('e'):
        return 'E'
    elif name.lower().startswith('i'):
        return 'I'
    elif name.lower().startswith('o'):
        return 'O'
    elif name.lower().startswith('u'):
        return 'U'
    return name

parse_name_vec = np.vectorize(parse_name)

Виконання деяких термінів:

Використання Застосувати

%timeit name_series.apply(parse_name)

Результати:

76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Використовуючи np.vectorize

%timeit parse_name_vec(name_series)

Результати:

77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Numpy намагається перетворити функції python на ufuncоб'єкти numpy під час дзвінка np.vectorize. Як це відбувається, я насправді не знаю - вам довелося б копати більше у внутрішніх органах numpy, ніж я готовий до банкомату. Тим не менш, схоже, це робить кращу роботу з просто числовими функціями, ніж ця функція на основі рядків тут.

Розміщення розміру до 1000000:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))

apply

%timeit name_series.apply(parse_name)

Результати:

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

Результати:

794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Кращий ( векторизований ) спосіб із np.select:

cases = [
    name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
    name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
    name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()

Час:

%timeit np.select(cases, replacements, default=name_series)

Результати:

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Що робити, якщо ви прокрутили це до size=1000000(1 мільйона)?
stackoverflowuser2010,

2
Я майже впевнений, що ваші твердження тут неправильні.
Поки що

@ stackoverflowuser2010 Я оновив його разом із фактичним векторизованим підходом.
PMende,

0

Я новачок у python. Але у наведеному нижче прикладі "застосувати", здається, працює швидше, ніж "векторизувати", або я чогось пропускаю.

 import numpy as np
 import pandas as pd

 B = np.random.rand(1000,1000)
 fn = np.vectorize(lambda l: 1/(1-np.exp(-l)))
 print(fn(B))

 B = pd.DataFrame(np.random.rand(1000,1000))
 fn = lambda l: 1/(1-np.exp(-l))
 print(B.apply(fn))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.