Группировка с sum или mean сворачивает таблицу: было 2448 строк — стало пять. Но иногда итог группы нужен
рядом с каждой строкой: «какую долю выручки своего города дала эта покупка?», «насколько этот день теплее
обычного для своего месяца?». Для таких вопросов нужно сравнить строку с её группой.
Свернуть или раздать
Маленький пример: пять игроков, две команды.
import pandas as pd
scores = pd.DataFrame({
"team": ["A", "A", "B", "B", "B"],
"score": [10, 30, 5, 5, 20],
})
scores.groupby("team")["score"].mean()team A 20.0 B 10.0 Name: score, dtype: float64
Два числа — по одному на группу. Метод transform считает те же средние, но раздаёт их обратно
по строкам: у каждого игрока оказывается среднее его команды.
scores.groupby("team")["score"].transform("mean")0 20.0 1 20.0 2 10.0 3 10.0 4 10.0 Name: score, dtype: float64
Результат — Series той же длины и с тем же индексом, что у исходной таблицы. Поэтому его можно сразу записать в столбец и сравнить с ним каждую строку:
scores["team_mean"] = scores.groupby("team")["score"].transform("mean")
scores["diff"] = scores["score"] - scores["team_mean"]
scores| team | score | team_mean | diff | |
|---|---|---|---|---|
| 0 | A | 10 | 20.0 | -10.0 |
| 1 | A | 30 | 20.0 | 10.0 |
| 2 | B | 5 | 10.0 | -5.0 |
| 3 | B | 5 | 10.0 | -5.0 |
| 4 | B | 20 | 10.0 | 10.0 |
Название функции передают строкой: "sum", "mean", "max", "min", "count", "size" и другие.
А что будет, если записать в столбец результат обычного mean() — без transform?
scores["wrong"] = scores.groupby("team")["score"].mean()
scores[["team", "score", "team_mean", "wrong"]]| team | score | team_mean | wrong | |
|---|---|---|---|---|
| 0 | A | 10 | 20.0 | NaN |
| 1 | A | 30 | 20.0 | NaN |
| 2 | B | 5 | 10.0 | NaN |
| 3 | B | 5 | 10.0 | NaN |
| 4 | B | 20 | 10.0 | NaN |
Ошибки нет, но столбец wrong пуст. Дело в правиле, которое до сих пор работало незаметно: когда в столбец
записывают Series, pandas расставляет значения по меткам индекса, а не по порядку. У таблицы метки строк —
0, 1, 2…, а у результата mean() метки — A и B. Ни одна не совпала — и во всех строках пропуск.
transform возвращает Series с метками самой таблицы, поэтому его значения встают на свои места.
В таблице scores пять строк и две команды. Сколько значений в
scores.groupby("team")["score"].transform("sum")?
transform возвращает значение для каждой строки исходной таблицы — их пять. Два значения вернул бы .sum() без transform.Доля внутри группы
Добавьте в orders столбец city_total — выручку города, к которому относится строка, и столбец
city_share — долю выручки строки в выручке её города.
orders.groupby("city")["revenue"].transform("sum").orders["revenue"] / orders["city_total"].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
orders = pd.read_csv("data/shop_orders.csv")
orders["revenue"] = orders["price"] * orders["quantity"]
# добавьте в orders столбцы city_total и city_shareУпражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
orders = pd.read_csv("data/shop_orders.csv")
orders["revenue"] = orders["price"] * orders["quantity"]
orders["city_total"] = orders.groupby("city")["revenue"].transform("sum")
orders["city_share"] = orders["revenue"] / orders["city_total"]Проверка: доли внутри каждой группы должны давать в сумме единицу.
orders.groupby("city")["city_share"].sum()city Екатеринбург 1.0 Казань 1.0 Москва 1.0 Новосибирск 1.0 Санкт-Петербург 1.0 Name: city_share, dtype: float64
Итог заказа рядом с позицией
Одна строка таблицы — позиция заказа. Сумму всего заказа даёт группировка по order_id, а transform
ставит её рядом с каждой позицией:
orders["order_total"] = orders.groupby("order_id")["revenue"].transform("sum")
orders[["order_id", "product", "revenue", "order_total"]].head(4)| order_id | product | revenue | order_total | |
|---|---|---|---|---|
| 0 | 10001 | Кофемолка ручная | 6400 | 6660 |
| 1 | 10001 | Травяной сбор 50 г | 260 | 6660 |
| 2 | 10002 | Шоколад горький | 180 | 530 |
| 3 | 10002 | Чёрный чай 100 г | 350 | 530 |
Теперь строки можно отбирать по свойству всей группы — например, оставить позиции только крупных заказов:
big = orders[orders["order_total"] >= 10000]
print(len(big), big["order_id"].nunique())30 12
30 строк из 12 заказов. Обычным фильтром по revenue этого не сделать: условие относится к заказу целиком,
а не к отдельной строке.
Добавьте в orders столбец lines — сколько позиций (строк) в заказе, к которому относится строка. Сохраните
в multi строки заказов из трёх и более позиций, а в n_multi — число таких заказов.
orders.groupby("order_id")["product"].transform("size").orders[orders["lines"] >= 3]; число разных заказов — nunique() столбца order_id.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
# добавьте в orders столбец lines
multi = ...
n_multi = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
orders["lines"] = orders.groupby("order_id")["product"].transform("size")
multi = orders[orders["lines"] >= 3]
n_multi = multi["order_id"].nunique()Отклонение от нормы
Группировать можно по нескольким столбцам. Посчитаем для погоды «норму» — средний дневной максимум своего города в своём месяце:
weather = pd.read_csv("data/weather.csv", parse_dates=["date"])
weather["month"] = weather["date"].dt.month
weather["norm"] = weather.groupby(["city", "month"])["temp_max"].transform("mean")
weather[["date", "city", "temp_max", "norm"]].head(3)| date | city | temp_max | norm | |
|---|---|---|---|---|
| 0 | 2025-01-01 | Москва | -3.2 | -3.829032 |
| 1 | 2025-01-02 | Москва | -3.2 | -3.829032 |
| 2 | 2025-01-03 | Москва | -3.7 | -3.829032 |
Средний максимум января в Москве — −3.8 градуса, и это число стоит в каждой январской строке Москвы.
Метод abs возвращает модуль числа — значение без знака. Он пригодится, чтобы искать отклонения в обе
стороны.
Добавьте в weather столбец anomaly — отклонение temp_max от нормы norm (день минус норма). В warmest
сохраните строку дня с наибольшим отклонением вверх. В n_unusual — число дней, когда отклонение в любую
сторону больше 8 градусов.
weather.loc[weather["anomaly"].idxmax()].(weather["anomaly"].abs() > 8).sum().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
# добавьте в weather столбец anomaly
warmest = ...
n_unusual = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
weather["anomaly"] = weather["temp_max"] - weather["norm"]
warmest = weather.loc[weather["anomaly"].idxmax()]
n_unusual = (weather["anomaly"].abs() > 8).sum()Самый необычно тёплый день года — 29 марта в Сочи: 26.7 градуса при мартовской норме 14.7.
Лучшая строка каждой группы
Ещё один приём: сравнить значение строки с максимумом её группы. Совпало — значит, это и есть рекордная строка:
orders["cat_max"] = orders.groupby("category")["revenue"].transform("max")
orders.loc[orders["revenue"] == orders["cat_max"], ["category", "product", "quantity", "revenue"]]| category | product | quantity | revenue | |
|---|---|---|---|---|
| 0 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 7 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 348 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 661 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 780 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 962 | Посуда | Френч-пресс | 4 | 7560 |
| 1081 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 1144 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 1255 | Чай | Улун 100 г | 7 | 3780 |
| 1353 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 1378 | Аксессуары | Кофемолка ручная | 2 | 6400 |
| 1473 | Чай | Улун 100 г | 7 | 3780 |
| 1633 | Сладости | Миндаль в шоколаде | 8 | 2560 |
| 1739 | Чай | Улун 100 г | 7 | 3780 |
| 1911 | Кофе | Бразилия 1 кг | 7 | 9030 |
| 1995 | Кофе | Бразилия 1 кг | 7 | 9030 |
| 2349 | Кофе | Бразилия 1 кг | 7 | 9030 |
В отличие от sort_values и head(1) из прошлого урока, этот способ возвращает все строки с рекордным
значением, если их несколько. Здесь их много: две ручные кофемолки в одной позиции покупали не раз.
Добавьте в orders столбец cat_price — среднюю цену (price) по строкам категории — и столбец above_avg:
True, если цена строки выше cat_price. В above_share сохраните долю таких строк в каждой категории.
orders.groupby("category")["price"].transform("mean"); маска — orders["price"] > orders["cat_price"].orders.groupby("category")["above_avg"].mean().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
# добавьте в orders столбцы cat_price и above_avg
above_share = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
orders["cat_price"] = orders.groupby("category")["price"].transform("mean")
orders["above_avg"] = orders["price"] > orders["cat_price"]
above_share = orders.groupby("category")["above_avg"].mean()Итоги
groupby(...)[...].transform("функция")считает итог группы и возвращает его для каждой строки.- Результат той же длины, что таблица, — его записывают в столбец.
- Доля внутри группы — значение, делённое на
transform("sum"); отклонение — значение минусtransform("mean"). - Отбор строк по свойству группы:
transform+ обычная маска. sum()сворачивает,transform("sum")раздаёт: первое — для отчёта, второе — для расчётов по строкам.