Я использую фреймы данных 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 «применить» к нескольким столбцам?
np.vectorize
в основном этоfor
цикл Python (это удобный метод), аapply
лямбда также находится во времени Pythonapply
построчно, если только вам не нужно, и, очевидно, векторизованная функция будет превосходить не векторизованную.np.vectorize
не векторизован. Это известное неправильное название.str
аксессуарами pandas . Во многих случаях они медленнее, чем понимание списков. Мы слишком много предполагаем.Ответы:
Я начну с того, что мощь массивов 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
Некоторые выводы:
tuple
основанные методы (первые 4) являются фактор более эффективным , чемpd.Series
основанные методы (последние 3).np.vectorize
, понимание списка +zip
иmap
методы, т. е. тройка лучших, имеют примерно одинаковую производительность. Это потому, что они используютtuple
и обходят некоторые накладные расходы Pandas изpd.DataFrame.itertuples
.raw=True
сpd.DataFrame.apply
против без. Этот параметр передает массивы NumPy пользовательской функции вместоpd.Series
объектов.pd.DataFrame.apply
: просто еще один циклЧтобы точно увидеть объекты, которые передает Pandas, вы можете тривиально изменить свою функцию:
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
есть следующее примечание:«Правила вещания» здесь неактуальны, поскольку входные массивы имеют одинаковые размеры. Параллель с
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:
источник
parallel
аргументом@njit(parallel=True)
дает мне дальнейшее улучшение по сравнению с just@njit
. Возможно, вы тоже можете это добавить.Чем сложнее становятся ваши функции (т. Е. Чем меньше
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)
Делаем некоторые тайминги:
Использование Apply
Полученные результаты:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
С помощью
np.vectorize
Полученные результаты:
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
Полученные результаты:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
np.vectorize
Полученные результаты:
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()
Сроки:
Полученные результаты:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
источник
size=1000000
(1 миллиона)?Я новичок в питоне. Но в приведенном ниже примере «применить», кажется, работает быстрее, чем «векторизация», или я что-то упустил.
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))
источник