Ответ 1
apply
, удобная функция, в которой вы никогда не нуждались
Мы начнем с рассмотрения вопросов в ОП, один за другим.
"Если применять это так плохо, то почему это в API?"
DataFrame.apply
и Series.apply
являются вспомогательными функциями, определенными для объекта DataFrame и Series соответственно. apply
принимает любую пользовательскую функцию, которая применяет преобразование/агрегацию к DataFrame. apply
- это по сути серебряная пуля, которая делает то, что ни одна из существующих функций панд не может выполнить.
apply
может кое-что сделать:
- Запустите любую пользовательскую функцию для DataFrame или Series
- Примените функцию либо по строке (
axis=1
), либо по столбцу (axis=0
) в кадре данных - Выполните выравнивание индекса при применении функции
- Выполняйте агрегацию с помощью пользовательских функций (однако в этих случаях мы обычно предпочитаем
agg
илиtransform
) - Выполнять поэлементные преобразования
- Трансляция агрегированных результатов в исходные строки (см. аргумент
result_type
). - Принимайте позиционные/ключевые аргументы для передачи пользовательским функциям.
... среди других. Для получения дополнительной информации см. Приложение функции строки или столбца в документации.
Итак, со всеми этими функциями, почему apply
плохо? Это потому что apply
медленнее slow. Pandas не делает никаких предположений о природе вашей функции, и поэтому итеративно применяет вашу функцию к каждой строке/столбцу по мере необходимости. Кроме того, обработка всех вышеперечисленных ситуаций означает, что apply
влечет за собой некоторые серьезные издержки на каждой итерации. Кроме того, apply
потребляет намного больше памяти, что является проблемой для приложений, ограниченных в памяти.
Есть очень немного ситуаций, когда apply
подходит для использования (подробнее об этом ниже). Если вы не уверены, следует ли вам использовать apply
, вам, вероятно, не следует.
Давайте ответим на следующий вопрос.
"Как и когда я должен применить свой код -free?"
Перефразируя, вот некоторые распространенные ситуации, когда вы хотите избавиться от любых вызовов на apply
.
Числовые данные
Если вы работаете с числовыми данными, вероятно, уже есть векторизованная функция Cython, которая делает именно то, что вы пытаетесь сделать (если нет, пожалуйста, задайте вопрос о Qaru или откройте запрос функции на 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)
В общем, ищите векторизованные альтернативы, если это возможно.
Строка /Regex
Pandas предоставляет "векторизованные" строковые функции в большинстве ситуаций, но в редких случаях эти функции не... "применяются", так сказать.
Распространенной проблемой является проверка наличия значения в столбце в другом столбце той же строки.
df = pd.DataFrame({
'Name': ['mickey', 'donald', 'minnie'],
'Title': ['wonderland', "welcome to donald castle", 'Minnie mouse clubhouse'],
'Value': [20, 10, 86]})
df
Name Value Title
0 mickey 20 wonderland
1 donald 10 welcome to donald 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 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 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))
в/с
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
.
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
в версиях для панд & lt; 0,25 (исправлено для 0,25, смотрите здесь для получения дополнительной информации.)