Когда я должен (не) хотеть использовать pandas apply() в моем коде?

Я видел много ответов, опубликованных на вопросы о переполнении стека, связанные с использованием метода Pandas apply. Я также видел, как пользователи комментировали их, говоря, что «apply медленный, и его следует избегать».

Я читал много статей на тему производительности, в которых объясняется, что apply работает медленно. Я также видел заявление об отказе от ответственности в документах о том, что apply является просто удобной функцией для передачи UDF (сейчас не могу найти). Таким образом, общее мнение состоит в том, что apply следует избегать, если это возможно. Однако это вызывает следующие вопросы:

  1. Если apply такой плохой, то зачем он в API?
  2. Как и когда мне сделать мой код свободным от apply?
  3. Были ли когда-нибудь ситуации, когда apply является хорошо (лучше, чем другие возможные решения)?
returns.add(1).apply(np.log) по сравнению с np.log(returns.add(1) — это случай, когда apply, как правило, будет немного быстрее, что показано в правом нижнем зеленом прямоугольнике на диаграмме jpp ниже.
Alexander 30.01.2019 19:27

@Александр спасибо. Не исчерпывающе указал на эти ситуации, но их полезно знать!

cs95 30.01.2019 19:42

Применить достаточно быстро и отличный API в 80% случаев. Так что я искренне не согласен с настроениями, которые предлагают не использовать его. Но, безусловно, полезно знать о его ограничениях и иметь некоторые приемы, изложенные в верхнем ответе, в заднем кармане, на случай, если apply действительно окажется слишком медленным.

Zephaniah Grunschlag 06.03.2021 23:12
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
163
3
33 594
3

Ответы 3

Не все 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 Есть исключения, но они, как правило, маргинальны или необычны. Несколько примеров:

  1. df['col'].apply(str) может немного опередить df['col'].astype(str).
  2. df.apply(pd.to_datetime), работающий со строками, плохо масштабируется со строками по сравнению с обычным циклом for.

@coldspeed, спасибо, в вашем посте нет ничего плохого (кроме некоторых противоречивых бенчмарков по сравнению с моими, но они могут быть основаны на вводе или настройке). Просто почувствовал, что есть другой взгляд на проблему.

jpp 30.01.2019 10:50

@jpp Я всегда использовал вашу отличную блок-схему в качестве руководства, пока сегодня не увидел, что построчный apply значительно быстрее, чем мое решение с any. Есть мысли по этому поводу?

Stef 09.08.2019 12:57

@Stef, сколько строк данных вы просматриваете? Создайте фрейм данных с 1 миллионом строк и попробуйте сравнить логику, apply должен быть медленнее. Также обратите внимание, что проблема может быть в mask (попробуйте вместо этого использовать np.where). Процесс, который занимает 3-5 миллисекунд, не годится для целей бенчмаркинга, поскольку на самом деле вы, вероятно, не заботитесь о производительности, когда время такое маленькое.

jpp 09.08.2019 13:26

@jpp: вы правы: для 1 млн строк x 100 столбцов any примерно в 100 раз быстрее, чем apply. Мои первые тесты проводились с 2000 строк x 1000 столбцов, и здесь apply был в два раза быстрее, чем any.

Stef 09.08.2019 15:03

@jpp Я хотел бы использовать ваше изображение в презентации / статье. Вы согласны с этим? Обязательно укажу источник. Спасибо

Erfan 19.12.2019 10:18

@Erfan, давай, давай.

jpp 19.12.2019 11:00

Бывают ли когда-нибудь ситуации, когда 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))?

jpp 25.02.2019 10:28

Поскольку это уже была серия панд, у меня возник соблазн использовать apply. Да, вы правы, лучше использовать list-comp, чем apply. хороший вариант использования.

BhishanPoudel 27.02.2019 06:35

Для 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)

Я был очень удивлен, обнаружив, что в некоторых случаях это дало мне лучшую производительность. Это было особенно полезно, когда мне нужно было сделать несколько вещей, каждая из которых имела свое подмножество значений столбца. Ответ «Все применения не похожи» может помочь выяснить, когда он может помочь, но его несложно проверить на выборке ваших данных.

denson 20.06.2019 02:56

Несколько советов: по производительности понимание списка превзойдет цикл for; zip(df, row[1:]) здесь достаточно; действительно, на данном этапе рассмотрите numba, если функция является числовым вычислением. См. пояснение в этот ответ.

jpp 09.08.2019 14:59

@jpp - если у вас есть функция получше, поделитесь. Я думаю, что это довольно близко к оптимальному из моего анализа. Да, numba быстрее, faster_df_apply предназначен для людей, которые просто хотят что-то эквивалентное, но быстрее, чем DataFrame.apply (который странно медленный).

Pete Cacioppi 09.08.2019 21:49

Это на самом деле очень близко к тому, как реализован .apply, но он делает одну вещь, которая значительно замедляет его, по сути, делает: row = pd.Series({f:v for f,v in zip(cols, row[1:])}), который добавляет много торможения. Я написал отвечать, в котором описана реализация, хотя я думаю, что она устарела, последние версии попытались использовать Cython в .apply, я полагаю (не цитируйте меня по этому поводу)

juanpa.arrivillaga 02.09.2020 02:59

@juanpa.arrivillaga это прекрасно объясняет! Большое спасибо.

Pete Cacioppi 03.10.2020 02:55

Другие вопросы по теме