Я видел много ответов, опубликованных на вопросы о переполнении стека, связанные с использованием метода Pandas apply. Я также видел, как пользователи комментировали их, говоря, что «apply медленный, и его следует избегать».
Я читал много статей на тему производительности, в которых объясняется, что apply работает медленно. Я также видел заявление об отказе от ответственности в документах о том, что apply является просто удобной функцией для передачи UDF (сейчас не могу найти). Таким образом, общее мнение состоит в том, что apply следует избегать, если это возможно. Однако это вызывает следующие вопросы:
apply такой плохой, то зачем он в API?apply?apply является хорошо (лучше, чем другие возможные решения)?@Александр спасибо. Не исчерпывающе указал на эти ситуации, но их полезно знать!
Применить достаточно быстро и отличный API в 80% случаев. Так что я искренне не согласен с настроениями, которые предлагают не использовать его. Но, безусловно, полезно знать о его ограничениях и иметь некоторые приемы, изложенные в верхнем ответе, в заднем кармане, на случай, если apply действительно окажется слишком медленным.






applys одинаковыВ приведенной ниже таблице показано, когда следует рассматривать apply1. Зеленый означает возможно эффективный; красный избегать.
Немного этого интуитивно понятен: pd.Series.apply — это построчный цикл на уровне Python, то же самое pd.DataFrame.apply построчный (axis=1). Злоупотребления ими многочисленны и разнообразны. В другом посте они рассматриваются более подробно. Популярными решениями являются использование векторизованных методов, списков (предполагает чистые данные) или эффективных инструментов, таких как конструктор pd.DataFrame (например, чтобы избежать apply(pd.Series)).
Если вы используете pd.DataFrame.apply построчно, часто полезно указать raw=True (где это возможно). На этом этапе numba обычно является лучшим выбором.
GroupBy.apply: общепризнанныйПовторение операций groupby, чтобы избежать apply, снизит производительность. GroupBy.apply здесь обычно подходит, при условии, что методы, которые вы используете в своей пользовательской функции, сами векторизованы. Иногда нет собственного метода Pandas для групповой агрегации, которую вы хотите применить. В этом случае для небольшого количества групп apply с пользовательской функцией все же может предложить приемлемую производительность.
pd.DataFrame.apply по колонке: смешанная ситуацияpd.DataFrame.apply по столбцам (axis=0) — интересный случай. Для небольшого количества строк по сравнению с большим количеством столбцов это почти всегда дорого. Для большого количества строк по отношению к столбцам, что является более распространенным случаем, вы можете иногда увидеть значительное улучшение производительности при использовании apply:
# Python 3.7, Pandas 0.23.4
np.random.seed(0)
df = pd.DataFrame(np.random.random((10**7, 3))) # Scenario_1, many rows
df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns
# Scenario_1 | Scenario_2
%timeit df.sum() # 800 ms | 109 ms
%timeit df.apply(pd.Series.sum) # 568 ms | 325 ms
%timeit df.max() - df.min() # 1.63 s | 314 ms
%timeit df.apply(lambda x: x.max() - x.min()) # 838 ms | 473 ms
%timeit df.mean() # 108 ms | 94.4 ms
%timeit df.apply(pd.Series.mean) # 276 ms | 233 ms
1 Есть исключения, но они, как правило, маргинальны или необычны. Несколько примеров:
df['col'].apply(str) может немного опередить df['col'].astype(str).df.apply(pd.to_datetime), работающий со строками, плохо масштабируется со строками по сравнению с обычным циклом for.@coldspeed, спасибо, в вашем посте нет ничего плохого (кроме некоторых противоречивых бенчмарков по сравнению с моими, но они могут быть основаны на вводе или настройке). Просто почувствовал, что есть другой взгляд на проблему.
@jpp Я всегда использовал вашу отличную блок-схему в качестве руководства, пока сегодня не увидел, что построчный apply значительно быстрее, чем мое решение с any. Есть мысли по этому поводу?
@Stef, сколько строк данных вы просматриваете? Создайте фрейм данных с 1 миллионом строк и попробуйте сравнить логику, apply должен быть медленнее. Также обратите внимание, что проблема может быть в mask (попробуйте вместо этого использовать np.where). Процесс, который занимает 3-5 миллисекунд, не годится для целей бенчмаркинга, поскольку на самом деле вы, вероятно, не заботитесь о производительности, когда время такое маленькое.
@jpp: вы правы: для 1 млн строк x 100 столбцов any примерно в 100 раз быстрее, чем apply. Мои первые тесты проводились с 2000 строк x 1000 столбцов, и здесь apply был в два раза быстрее, чем any.
@jpp Я хотел бы использовать ваше изображение в презентации / статье. Вы согласны с этим? Обязательно укажу источник. Спасибо
@Erfan, давай, давай.
Бывают ли когда-нибудь ситуации, когда apply хорош?
Да, иногда.
Задача: декодировать строки Unicode.
import numpy as np
import pandas as pd
import unidecode
s = pd.Series(['mañana','Ceñía'])
s.head()
0 mañana
1 Ceñía
s.apply(unidecode.unidecode)
0 manana
1 Cenia
Обновлять
Я ни в коем случае не выступал за использование apply, просто подумал, что, поскольку NumPy не может справиться с описанной выше ситуацией, он мог бы быть хорошим кандидатом на pandas apply. Но я забыл о простом понимании списка благодаря напоминанию @jpp.
Ну нет. Чем это лучше, чем [unidecode.unidecode(x) for x in s] или list(map(unidecode.unidecode, s))?
Поскольку это уже была серия панд, у меня возник соблазн использовать apply. Да, вы правы, лучше использовать list-comp, чем apply. хороший вариант использования.
Для axis=1 (т.е. построчные функции) вы можете просто использовать следующую функцию вместо apply. Интересно, почему это не поведение pandas. (Не тестировался с составными индексами, но кажется, что он намного быстрее, чем apply)
def faster_df_apply(df, func):
cols = list(df.columns)
data, index = [], []
for row in df.itertuples(index=True):
row_dict = {f:v for f,v in zip(cols, row[1:])}
data.append(func(row_dict))
index.append(row[0])
return pd.Series(data, index=index)
Я был очень удивлен, обнаружив, что в некоторых случаях это дало мне лучшую производительность. Это было особенно полезно, когда мне нужно было сделать несколько вещей, каждая из которых имела свое подмножество значений столбца. Ответ «Все применения не похожи» может помочь выяснить, когда он может помочь, но его несложно проверить на выборке ваших данных.
Несколько советов: по производительности понимание списка превзойдет цикл for; zip(df, row[1:]) здесь достаточно; действительно, на данном этапе рассмотрите numba, если функция является числовым вычислением. См. пояснение в этот ответ.
@jpp - если у вас есть функция получше, поделитесь. Я думаю, что это довольно близко к оптимальному из моего анализа. Да, numba быстрее, faster_df_apply предназначен для людей, которые просто хотят что-то эквивалентное, но быстрее, чем DataFrame.apply (который странно медленный).
Это на самом деле очень близко к тому, как реализован .apply, но он делает одну вещь, которая значительно замедляет его, по сути, делает: row = pd.Series({f:v for f,v in zip(cols, row[1:])}), который добавляет много торможения. Я написал отвечать, в котором описана реализация, хотя я думаю, что она устарела, последние версии попытались использовать Cython в .apply, я полагаю (не цитируйте меня по этому поводу)
@juanpa.arrivillaga это прекрасно объясняет! Большое спасибо.
returns.add(1).apply(np.log)по сравнению сnp.log(returns.add(1)— это случай, когдаapply, как правило, будет немного быстрее, что показано в правом нижнем зеленом прямоугольнике на диаграмме jpp ниже.