Производительность Pandas применяется против np.vectorize для создания нового столбца из существующих столбцов - PullRequest
0 голосов
/ 06 октября 2018

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

Функция Pandas 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

Ответы [ 2 ]

0 голосов
/ 06 октября 2018

Я начну , сказав, что мощность массивов Pandas и NumPy основана на высокопроизводительных векторизованных вычислениях для числовых массивов. 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 и , обходя некоторые накладные расходы панд с 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, за исключением того, что она использует правила широковещания 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 Есть по крайней мере две причины, по которым операции NumPy эффективны по сравнению с Python:

  • Все в Python является объектом.Это включает, в отличие от C, числа.Поэтому у типов Python есть издержки, которых нет у нативных типов C.
  • Методы NumPy обычно основаны на C.Кроме того, по возможности используются оптимизированные алгоритмы.
0 голосов
/ 06 октября 2018

Чем сложнее становятся ваши функции (т. Е. Чем меньше 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 в numpy ufunc объекты при вызове np.vectorize.Как это происходит, я на самом деле не знаю - вам придется копать больше во внутренностях NumPy, чем я готов в банкомате.Тем не менее, здесь, похоже, лучше справиться с простыми числовыми функциями, чем с этой строковой функцией.

Увеличение размера до 1 000 000:

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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...