Ответ 1
Начну с того, что мощь массивов Pandas и NumPy получена из высокопроизводительных векторизованных вычислений на числовых массивах. 1 Вся точка векторизованных вычислений состоит в том, чтобы избежать петель уровня, перемещая вычисления в высоко оптимизированный код С и используя смежные блоки памяти. 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
-based (первые 4) являются более эффективными, чемpd.Series
-based (последние 3). -
np.vectorize
, понимание списка + методыzip
иmap
, т.е. верхние 3, имеют примерно такую же производительность. Это связано с тем, что они используютtuple
и обходят некоторые изpd.DataFrame.itertuples
с помощью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
функция вычисляет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 Существует как минимум 2 причины, по которым операции NumPy эффективны по сравнению с Python:
- Все в Python - это объект. Это включает, в отличие от чисел C. Поэтому типы Python имеют накладные расходы, которые не существуют с родными типами C.
- Методами NumPy обычно являются C -based. Кроме того, по возможности используются оптимизированные алгоритмы.