Я начну , сказав, что мощность массивов 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
, то есть топ-3, все имеют примерно одинаковыеспектакль.Это потому, что они используют tuple
и , обходя некоторые накладные расходы панд с pd.DataFrame.itertuples
. - . Существенное улучшение скорости от использования
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.Кроме того, по возможности используются оптимизированные алгоритмы.