62
задан 24 September 2019 в 23:33

4 ответа

apply, Функция Удобства Вам Никогда Не Было нужно

, Мы запускаем путем рассматривания вопросов в OP, один за другим.

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

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

Некоторые вещи apply могут сделать:

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

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

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

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

<час>

Позволяют нам рассмотреть следующий вопрос.

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

Для перефразирования вот некоторые общие ситуации, где Вы захотите к [1 190], избавляются из любых вызовов к [1 140].

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

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

Контраст производительность [1 141] для простой операции сложения.

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

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

%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

обеспечивают "векторизованные" строковые функции в большинстве ситуаций, но существуют редкие случаи, где те функции... "не применяются", так сказать.

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

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".

Используя применяются, это было бы сделано с помощью [11 132]

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)

вещь отметить вот состоит в том, что повторяющиеся стандартные программы, оказывается, быстрее, чем [1 143] из-за более низких издержек. Если необходимо обработать NaNs и недопустимый dtypes, можно основываться на этом использовании пользовательской функции, которую можно затем вызвать с аргументами в понимании списка.

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

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

Read больше в эти документы .

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

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

более оптимальным вариантом А является к listify столбец, и передайте его фунту. 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 хорошо? "

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

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

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

Это - допустимый случай для [1 148]:

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

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

Примечание, что это также имело бы смысл к [1 149] или просто использовало бы явный цикл. Все эти опции немного быстрее, чем использование 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)

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

Ряд Преобразования к [1 151]: astype по сравнению с [1 153]

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

enter image description here график был построен с помощью perfplot библиотека.

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, последовательно с такой скоростью, как, или немного быстрее, чем [1 158]. Таким образом, это имеет отношение к тому, что данные в тесте являются целым типом.

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

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

Одно общее требование состоит в том, чтобы выполнить GroupBy и затем две главных операции, такие как "изолированный cumsum":

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

Вам были бы нужны два последовательных вызова groupby здесь:

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

Используя [1 163], можно сократить это к единственный вызов.

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

Это поведение также замечено в [1 169] на версиях панд < 0.25 (это было зафиксировано для 0,25, , посмотрите здесь для получения дополнительной информации .)

60
ответ дан 31 October 2019 в 14:19

Весь apply с не подобны

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

enter image description here

приблизительно [1 126] из этого интуитивно: 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 обычно прекрасен здесь, предоставил методы, которые Вы используете в своей пользовательской функции, самостоятельно векторизованы. Иногда нет никакого собственного метода Панд для groupwise агрегирования, которое Вы хотите применить. В этом случае, для небольшого количества групп 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 цикл.
26
ответ дан 31 October 2019 в 14:19

Для 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)
3
ответ дан 31 October 2019 в 14:19

Есть ли когда-нибудь какие-либо ситуации, где 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. Но я забывал плоскость ol понимание списка благодаря напоминанию @jpp.

1
ответ дан 31 October 2019 в 14:19

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

Похожие вопросы: