apply
, функція зручності, яка вам ніколи не потрібна
Почнемо з вирішення питань в ОП, по одному.
" Якщо заявка настільки погана, то чому це в API? "
DataFrame.apply
і Series.apply
це функції зручності, визначені на об'єкті DataFrame та Series відповідно. apply
приймає будь-яку визначену користувачем функцію, яка застосовує перетворення / агрегацію на DataFrame. apply
Насправді срібна куля, яка не може виконувати будь-яку функцію панд.
Деякі речі apply
:
- Запустіть будь-яку визначену користувачем функцію на DataFrame або Series
- Застосуйте функцію або в рядку (
axis=1
), або у стовпці ( axis=0
) на DataFrame
- Виконайте вирівнювання індексу під час застосування функції
- Виконувати агрегацію з визначеними користувачем функціями (однак, ми зазвичай віддаємо перевагу
agg
або transform
в цих випадках)
- Виконайте елементарні перетворення
- Трансляція зведених результатів у вихідні рядки (див.
result_type
Аргумент).
- Прийміть позиційні / ключові аргументи, щоб перейти до визначених користувачем функцій.
...Серед інших. Для отримання додаткової інформації див. Додаток щодо функцій рядків або стовпців у документації.
Отже, з усіма цими особливостями, чому це apply
погано? Це тому apply
, що повільно . Pandas не передбачає припущень щодо природи вашої функції, тому ітеративно застосовує вашу функцію до кожного рядка / стовпця, якщо це необхідно. Крім того, обробка всіх вищезазначених ситуацій означає, що apply
кожен ітератор має значні накладні витрати. Крім того, apply
витрачається набагато більше пам’яті, що є проблемою для обмежених пам’яттю програм.
Є дуже мало ситуацій, коли apply
доцільно використовувати (докладніше про це нижче). Якщо ви не впевнені, чи варто вам користуватися apply
, ви, мабуть, не повинні.
Давайте розглянемо наступне питання.
" Як і коли я можу зробити так, щоб мій код було застосовано безкоштовно? "
Перефразовуючи, ось кілька поширених ситуацій, коли ви хочете позбутися від будь-яких дзвінків apply
.
Числові дані
Якщо ви працюєте з числовими даними, ймовірно, вже є векторизована функція цитону, яка робить саме те, що ви намагаєтеся зробити (якщо ні, будь ласка, задайте питання щодо переповнення стека або відкрийте запит на функцію на 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)
Загалом, по можливості шукайте векторизовані альтернативи.
Рядок / Регекс
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".
Використовуючи застосувати, це можна зробити за допомогою
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
через нижчі накладні витрати. Якщо вам потрібно обробити NaNs та недійсні типи, ви можете використовувати це за допомогою спеціальної функції, яку ви можете викликати з аргументами всередині списку.
Для отримання додаткової інформації про те, коли розуміння списків слід вважати хорошим варіантом, див. Моє написання: Для циклів із пандами - Коли мені потрібно піклуватися? .
Примітка.
Операції з датою та датою також мають векторні версії. Так, наприклад, вам слід віддати перевагу 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
добре? "
Застосування - це функція зручності, тому бувають ситуації, коли накладні витрати є незначними, щоб пробачити. Це дійсно залежить від того, скільки разів викликається функція.
Функції, які векторизовані для серії, але не DataFrames
Що робити, якщо ви хочете застосувати рядову операцію до кількох стовпців? Що робити, якщо ви хочете перетворити кілька стовпців на дату? Ці функції векторизовані лише для 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
версіях панд <0,25 (було встановлено 0,25, див. Тут для отримання додаткової інформації .)
returns.add(1).apply(np.log)
vs.np.log(returns.add(1)
- це випадок, колиapply
зазвичай буде незначно швидше, що є зеленим полем знизу праворуч на діаграмі jpp нижче.