agg, transform и apply: в чём разница
У groupby три способа «применить функцию», и они дают результаты разной формы — путаница между ними рождает большинство ошибок.
agg схлопывает группу в одно значение; transform возвращает значение на каждую исходную строку; apply — самый гибкий и самый медленный.
agg: несколько агрегатов сразу
agg (синоним aggregate) применяет одну или несколько агрегирующих функций и возвращает по одной строке на группу. Можно передать список функций или словарь «столбец → функции»:
# одна функция
df.groupby("город")["чек"].agg("sum")
# несколько функций к одному столбцу
df.groupby("город")["чек"].agg(["min", "max", "mean"])
# разные функции к разным столбцам
df.groupby("город").agg({"чек": ["sum", "mean"], "шт": "sum"})
Именованные агрегации: плоский и понятный результат
Список функций в agg даёт многоуровневые имена столбцов, с которыми неудобно работать. Современный рекомендуемый способ — именованные агрегации: вы прямо задаёте имя выходного столбца и пару «(столбец, функция)». Результат — плоская таблица с понятными названиями:
df.groupby("город").agg(
средний_чек=("чек", "mean"),
макс_чек=("чек", "max"),
всего_штук=("шт", "sum"),
)
# средний_чек макс_чек всего_штук
# город
# Москва 1200.0 1800 12
# Сочи 900.0 1000 7
Это и читаемо, и сразу даёт нужные имена столбцов без последующего переименования — предпочитайте этот синтаксис.
transform: результат по форме исходных данных
Главное отличие transform от agg: он возвращает столько же строк, сколько было, «разливая» групповой результат обратно по строкам группы. Это незаменимо, когда нужно сравнить каждую строку с её группой — например, добавить столбец «средний чек по городу этой строки».
Покажем суть руками. agg даёт одно число на группу, transform — то же число, но на каждую строку:
orders = [
("Москва", 1200), ("Сочи", 800), ("Москва", 1800),
("Сочи", 1000), ("Москва", 600),
]
# split
groups = {}
for city, chek in orders:
groups.setdefault(city, []).append(chek)
# среднее по группе (это "agg": одно число на группу)
avg = {city: sum(v) / len(v) for city, v in groups.items()}
print("agg (одно на группу):", avg)
# transform: то же среднее, но НА КАЖДУЮ исходную строку
for city, chek in orders:
group_avg = avg[city]
diff = chek - group_avg
print(f"{city:7} чек={chek:5} ср.по группе={group_avg:7.1f} откл={diff:+.1f}")
Вывод:
agg (одно на группу): {'Москва': 1200.0, 'Сочи': 900.0}
Москва чек= 1200 ср.по группе= 1200.0 откл=+0.0
Сочи чек= 800 ср.по группе= 900.0 откл=-100.0
Москва чек= 1800 ср.по группе= 1200.0 откл=+600.0
Сочи чек= 1000 ср.по группе= 900.0 откл=+100.0
Москва чек= 600 ср.по группе= 1200.0 откл=-600.0
В pandas правый столбец — это ровно df.groupby("город")["чек"].transform("mean"): на выходе пять значений (как строк), а не два. Дальше легко посчитать отклонение: df["откл"] = df["чек"] - df.groupby("город")["чек"].transform("mean"). Через agg так не сделать — он вернул бы две строки, которые не выровнять с пятью исходными без отдельного merge.
apply: гибкий, но осторожно
apply в groupby получает на вход целый под-DataFrame группы и может вернуть что угодно — скаляр, Series или DataFrame. Это максимальная гибкость: например, «взять топ-2 заказа в каждом городе» или применить функцию, которой нужны сразу несколько столбцов группы.
# топ-2 строки по чеку в каждом городе — это работа для apply
df.groupby("город", group_keys=False).apply(
lambda g: g.nlargest(2, "чек")
)
Плата за гибкость — скорость: apply вызывает Python-функцию на каждую группу, поэтому медленнее agg/transform, у которых есть оптимизированные пути для встроенных функций. Берите apply, только когда задача не выражается через agg или transform.
Типичные задачи для transform
Чтобы transform закрепился, вот частые практические применения, где он незаменим:
| Задача | Код |
| Доля строки в сумме её группы | df["чек"] / df.groupby("город")["чек"].transform("sum") |
| Отклонение от среднего группы | df["чек"] - df.groupby("город")["чек"].transform("mean") |
| Нормировка внутри группы (z-score) | (x - g.transform("mean")) / g.transform("std") |
| Заполнить пропуски средним по группе | df["чек"].fillna(df.groupby("город")["чек"].transform("mean")) |
Объединяет их одно: всем нужен результат той же длины, что исходные данные, чтобы записать его обратно столбцом. Последний пример особенно элегантен: «заполнить пропуск цены средним по городу этого товара» — через transform это одна строка, а через agg+merge получился бы громоздкий код.
Сводная таблица различий
| Метод | Форма результата | Когда |
agg | одна строка на группу | сводки: сумма, среднее, мин/макс по группам |
transform | столько же строк, сколько было | добавить групповую характеристику к каждой строке |
apply | любая (скаляр / Series / DataFrame) | сложная логика, не выразимая через agg/transform |
Подводные камни
- Путаница формы. Главная ошибка — ждать от
aggрезультат на каждую строку (или наоборот). Сначала решите, нужна вам сводка (agg) или столбец той же длины (transform). - Многоуровневые столбцы после agg([...]). Список функций создаёт MultiIndex по столбцам; именованные агрегации этого избегают.
- apply медленный и неоднозначный. На больших данных и при наличии альтернативы он проигрывает; к тому же его поведение зависит от того, что вернула функция.
- transform требует возврата той же длины. Функция в
transformдолжна вернуть либо скаляр (разольётся), либо массив длины группы; иначе ошибка.
Лучшие практики
- Для сводок используйте
aggс именованными агрегациями — плоский результат и понятные имена. - Нужно «сравнить строку с её группой» (доля, отклонение, нормировка) — это
transform. apply— последнее средство, когда ниagg, ниtransformне подходят.- Всегда осознанно выбирайте между «строка на группу» и «строка на исходную строку».
Итог
agg— одна строка на группу; именованные агрегации дают плоский читаемый результат.transform— результат той же длины, что исходные данные; для сравнения строки с группой.apply— максимально гибкий, но медленный; только когда иначе нельзя.- Форма результата — главный критерий выбора метода.