apply
, Функция Удобства Вам Никогда Не Было нужно , Мы запускаем путем рассматривания вопросов в OP, один за другим.
" , Если применяются , так плохо, то, почему это находится в API? "
DataFrame.apply
и Series.apply
функции удобства определены на объекте DataFrame и Ряда соответственно. apply
принимает любую определяемую пользователем функцию, которая применяет преобразование/агрегирование на DataFrame. apply
эффективно серебряная пуля, которая делает то, что не может сделать любая существующая функция панд.
Некоторые вещи apply
могут сделать:
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)
В целом, ищут векторизованные альтернативы, если это возможно.
обеспечивают "векторизованные" строковые функции в большинстве ситуаций, но существуют редкие случаи, где те функции... "не применяются", так сказать.
типичная проблема А состоит в том, чтобы проверить, присутствует ли значение в столбце в другом столбце той же строки.
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)
И так далее...
astype
по сравнению с [1 153] Это походит на особенность API. Используя [1 154] для преобразования целых чисел в Ряду для строкового представления сопоставимо (и иногда быстрее), чем использование astype
.
график был построен с помощью 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, , посмотрите здесь для получения дополнительной информации .)
apply
с не подобны ниже диаграммы, предлагает, когда рассмотреть apply
<глоток> 1 глоток>. Зеленый означает возможно эффективный; красный избегают.
приблизительно [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 глоток> существуют исключения, но они являются обычно крайними или редкими. Несколько примеров:
df['col'].apply(str)
может немного превзойти по характеристикам df['col'].astype(str)
. df.apply(pd.to_datetime)
работа над строками не масштабируется хорошо со строками по сравнению с постоянным клиентом for
цикл. Для 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)
Есть ли когда-нибудь какие-либо ситуации, где 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.