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
.
График строился с использованием 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"> см. Здесь для получения дополнительной информации .)