Когда мне следует использовать pandas apply () в моем коде? - PullRequest
0 голосов
/ 30 января 2019

Это QnA с самостоятельным ответом, предназначенный для информирования пользователей о подводных камнях и преимуществах применения.

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

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

  1. Если apply настолько плох, то почему он в API?
  2. Как и когда я должен сделать свой код apply -бесплатным?
  3. Были ли ситуации, когда apply является хорошим (лучше, чем другие возможные решения)?

Ответы [ 4 ]

0 голосов
/ 20 мая 2019

Для 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)
0 голосов
/ 30 января 2019

Все apply не похожи

На приведенном ниже графике показано, когда следует рассмотреть apply 1 .Зеленый означает, возможно, эффективный;красный избегать.

enter image description here

Некоторые этого интуитивно понятны: pd.Series.apply - это поэтапный цикл на уровне Python, так жеpd.DataFrame.apply по строкам (axis=1).Злоупотребления ими многочисленны и разнообразны.В другом посте они рассматриваются более подробно.Популярные решения - использовать векторизованные методы, списки (предполагающие чистые данные) или эффективные инструменты, такие как pd.DataFrame конструктор (например, чтобы избежать apply(pd.Series)).

Если вы используете pd.DataFrame.apply row-мудро, указав raw=True (где это возможно) часто полезно.На этом этапе numba обычно является лучшим выбором.

GroupBy.apply: обычно предпочитают

Повторение groupby операций во избежание apply ухудшит производительность,GroupBy.apply здесь обычно хорошо, если методы, которые вы используете в своей пользовательской функции, сами векторизованы.Иногда нет собственного метода Pandas для групповой агрегации, которую вы хотите применить.В этом случае для небольшого числа групп apply с пользовательской функцией может по-прежнему предлагать разумную производительность.

pd.DataFrame.apply по столбцам: смешанная сумка

pd.DataFrame.apply column-мудрый (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.
0 голосов
/ 23 февраля 2019

Я хотел бы добавить свои два цента:

Были ли когда-нибудь ситуации, когда применять хорошо?Да, иногда.

Задача: декодировать строки 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.

0 голосов
/ 30 января 2019

apply, удобная функция, в которой вы никогда не нуждались

Мы начнем с того, что рассмотрим вопросы в ОП по одному.

" Если применить это так плохо, тогда почему это в API?"

DataFrame.apply и Series.apply являются вспомогательные функции , определенные для объекта DataFrame и Series соответственно.apply принимает любую пользовательскую функцию, которая применяет преобразование / агрегацию к DataFrame.apply - это, по сути, серебряная пуля, которая делает то, что ни одна из существующих функций панд не может выполнить.

Некоторые вещи, которые apply может выполнять:

  • Запуск любой пользовательской функции для DataFrame или Series
  • Применение функции по строкам (axis=1) или по столбцам (axis=0) в кадре данных
  • Выполнение выравнивания индекса при применении функции
  • Выполнение агрегации с помощью пользовательских функций (однако мы обычно предпочитаем agg или transform в этих случаях)
  • Выполнение поэлементных преобразований
  • Трансляция агрегированных результатов в исходные строки (см. аргумент result_type).
  • Принять позиционный /ключевые аргументы для передачи пользовательским функциям.

... Среди других.Для получения дополнительной информации см. Приложение функции строки или столбца в документации.

Итак, почему со всеми этими функциями apply плохо?Это , потому что apply это медленно .Pandas не делает никаких предположений о характере вашей функции, поэтому итеративно применяет вашу функцию к каждой строке / столбцу по мере необходимости.Кроме того, обработка all в описанных выше ситуациях означает, что apply влечет за собой значительные накладные расходы на каждой итерации.Кроме того, apply потребляет намного больше памяти, что является проблемой для приложений, ограниченных памятью.

Есть очень немного ситуаций, когда apply подходит для использования (подробнее об этом ниже). Если вы не уверены, следует ли вам использовать apply, вам, вероятно, не следует.


Давайте ответим на следующий вопрос.

" Как и когда я должен сделать свой код применить -free? "

Перефразируя, вот некоторые распространенные ситуации, в которых вы захотите избавиться от любых вызовов apply.

Числовые данные

Если вы работаете с числовыми данными, то, вероятно, уже векторизованоФункция Cython, которая делает именно то, что вы пытаетесь сделать (если нет, пожалуйста, задайте вопрос о переполнении стека или откройте запрос функции на GitHub).

Сравните производительность apply с простым добавлениемоперация.

df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df

   A   B
0  9  12
1  4   7
2  2   5
3  1   4

df.apply(np.sum)

A    16
B    28
dtype: int64

df.sum()

A    16
B    28
dtype: int64

По производительности, сравнения нет, цитонизированный эквивалент гораздо быстрее.Нет необходимости в графике, потому что разница очевидна даже для игрушечных данных.

%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Даже если вы разрешите передачу необработанных массивов с аргументом raw, это все равно в два раза медленнее.

%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Другой пример:

df.apply(lambda x: x.max() - x.min())

A    8
B    8
dtype: int64

df.max() - df.min()

A    8
B    8
dtype: int64

%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()

2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Как правило, ищет векторизованные альтернативы, если это возможно.

String / Regex

В большинстве случаев Pandas предоставляет «векторизованные» строковые функции, но в редких случаях эти функции не «применимы», так сказать.

Распространенной проблемой является проверка того,значение в столбце присутствует в другом столбце той же строки.

df = pd.DataFrame({
    'Name': ['mickey', 'donald', 'minnie'],
    'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
    'Value': [20, 10, 86]})
df

     Name  Value                       Title
0  mickey     20                  wonderland
1  donald     10  welcome to donald's castle
2  minnie     86      Minnie mouse clubhouse

Это должно вернуть строку второй и третьей строки, так как «donald» и «minnie» присутствуют в соответствующих столбцах «Title».

Используя apply, это будет сделаноиспользуя

df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)

0    False
1     True
2     True
dtype: bool

df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

Тем не менее, существует лучшее решение с использованием списочных представлений.

df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Здесь следует отметить, что итеративные процедуры выполняются быстрее, чем apply, из-за меньших накладных расходов.Если вам нужно обработать NaN и недопустимые типы dtypes, вы можете использовать эту функцию, используя пользовательскую функцию, которую затем можете вызывать с аргументами внутри понимания списка.

Для получения дополнительной информации о том, когда понимание списка следует рассматривать как хороший вариант,см. мою запись: Для циклов с пандами - Когда мне все равно? .

Примечание
Операции даты и даты и времени также имеют векторизованные версии.Так, например, вы должны предпочесть pd.to_datetime(df['date']), скажем, df['date'].apply(pd.to_datetime).

Подробнее на документах .

Распространенная ловушка: взрыв столбцов списков

s = pd.Series([[1, 2]] * 3)
s

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object

Люди испытывают желание использовать apply(pd.Series).Это ужасно с точки зрения производительности.

s.apply(pd.Series)

   0  1
0  1  2
1  1  2
2  1  2

Лучшим вариантом является прослушивание столбца и передача его в pd.DataFrame.

pd.DataFrame(s.tolist())

   0  1
0  1  2
1  1  2
2  1  2

%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())

2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Наконец,

" Есть ли ситуации, когда apply хорошо? "

Apply - удобная функция, поэтому в есть ситуации, когда накладные расходы незначительны, чтобы простить.Это действительно зависит от того, сколько раз вызывается функция.

Функции, которые векторизованы для рядов, но не для фреймов данных
Что если вы хотите применить строковую операцию к нескольким столбцам?Что если вы хотите конвертировать несколько столбцов в дату и время?Эти функции векторизованы только для Series, поэтому они должны применяться для каждого столбца, над которым вы хотите выполнить преобразование / работать.

df = pd.DataFrame(
         pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2), 
         columns=['date1', 'date2'])
df

       date1      date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30

df.dtypes

date1    object
date2    object
dtype: object

Это допустимый случай для apply:

df.apply(pd.to_datetime, errors='coerce').dtypes

date1    datetime64[ns]
date2    datetime64[ns]
dtype: object

Обратите внимание, что также имеет смысл stack или просто использовать явный цикл.Все эти параметры немного быстрее, чем при использовании apply, но разница достаточно мала, чтобы простить.

%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')

5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Вы можете сделать аналогичный случай для других операций, таких как строковые операции или преобразование в категорию.

u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))

v / s

u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
    v[c] = df[c].astype(category)

И так далее ...

Преобразование серии в str: astype против apply

Это выглядит как особенность API.Использование apply для преобразования целых чисел в Серии в строку сопоставимо (и иногда быстрее), чем использование astype.

enter image description here График строился с использованием perfplot library.

import perfplot

perfplot.show(
    setup=lambda n: pd.Series(np.random.randint(0, n, n)),
    kernels=[
        lambda s: s.astype(str),
        lambda s: s.apply(str)
    ],
    labels=['astype', 'apply'],
    n_range=[2**k for k in range(1, 20)],
    xlabel='N',
    logx=True,
    logy=True,
    equality_check=lambda x, y: (x == y).all())

С плавающей точкой, я вижу, что astype всегда так же быстро или немного быстрее, чем apply.Так что это связано с тем, что данные в тесте имеют целочисленный тип.

GroupBy операции с цепочечными преобразованиями

GroupBy.apply не обсуждалисьдо сих пор, но GroupBy.apply также является итеративной удобной функцией для обработки всего, что не имеют существующие функции GroupBy.

Одним из распространенных требований является выполнение GroupBy, а затем две простые операции, такие как «запаздывающая сумма»:

df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df

   A   B
0  a  12
1  a   7
2  b   5
3  c   4
4  c   5
5  c   4
6  d   3
7  d   2
8  e   1
9  e  10

Вам потребуется два последовательных групповых вызоваздесь:

df.groupby('A').B.cumsum().groupby(df.A).shift()

0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

Используя apply, вы можете сократить это до одного вызова.

df.groupby('A').B.apply(lambda x: x.cumsum().shift())

0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

Оценить производительность очень сложно, поскольку она зависит от данных.Но в целом, apply является приемлемым решением, если цель состоит в том, чтобы уменьшить вызов groupby (поскольку groupby также довольно дорогой).


Другие предупреждения

Помимо оговорок, упомянутых выше, также стоит упомянуть, что apply работает в первом ряду (или столбце) дважды.Это сделано, чтобы определить, имеет ли функция какие-либо побочные эффекты.В противном случае apply может быть в состоянии использовать быстрый путь для оценки результата, в противном случае он возвращается к медленной реализации.

df = pd.DataFrame({
    'A': [1, 2],
    'B': ['x', 'y']
})

def func(x):
    print(x['A'])
    return x

df.apply(func, axis=1)

# 1
# 1
# 2
   A  B
0  1  x
1  2  y

Такое поведение также наблюдается в GroupBy.apply на версиях pandas.<0,25 (было исправлено для 0,25, <a href="https://stackoverflow.com/a/56215416/4909087"> см. Здесь для получения дополнительной информации .)

...