Групи: groupby
Накратко: groupby в pandas разделя редовете по ключ, смята във всяка група (sum, mean, agg) и събира резултата в една таблица.
Проблем
„Колко е средната поръчка?“
Управителят пита колко пазарува средно един клиент на поръчка. Имаш поръчките за годината и редовете им: всеки ред е един продукт в една поръчка, с бройката и цената. Отказаните поръчки не са оборот, затова ги махаш с маска, както в урока за филтрите:
Редовете са 25 069, а поръчките 10 459. Средната на колоната revenue е 4,44 €, но това е средният ред, а не средната поръчка. Една поръчка има няколко реда и за нея трябва първо да събереш редовете ѝ.
Трябва ти сбор за всяка поръчка: 10 459 числа от 25 069 реда. С for и речник става, но е бавно и дълго. pandas има за това една операция.
Идеята
Раздели, сметни, събери
groupby('order_id') разделя редовете на групи: по една група за всяка различна стойност на order_id. После избираш колона и сметка: ['revenue'].sum() събира оборота във всяка група. Накрая pandas слага резултатите в една Series с индекс order_id.
Това се казва split-apply-combine: раздели, сметни, събери. Ето го върху първите 7 реда:
Поръчка 1 има един ред (31,6 €), а 2, 3 и 4 имат по два. Поръчка 3 е 37,5 + 8,4 = 45,9 €. От 7 реда излизат 4 числа, по едно на група. Ключът order_id е индексът на резултата и затова стои под името на колоната.
Вместо sum може всяко обобщение от NumPy: mean, min, max, count.
Виж го
Как groupby минава през редовете
Същите 7 реда бавно: всеки ред отива в групата на своя order_id, групата се събира в едно число и числата се подреждат в резултата.
import pandas as pd
items = pd.read_csv('order_items.csv').head(7)items['revenue'] = items['quantity'] * items['unit_price']print(items[['order_id', 'revenue']])items.groupby('order_id')['revenue'].sum()Стъпка по стъпка, с думи
- 7 реда от order_items с оборота на всеки ред: поръчки 1, 2, 3 и 4.
- Раздели (split): groupby('order_id') слага всеки ред в групата на своя order_id.
- 4 различни order_id, 4 групи. Поръчка 1 има един ред, другите по 2.
- Сметни (apply): ['revenue'].sum() във всяка група. Поръчка 3: 37,5 + 8,4 = 45,9.
- Събери (combine): 4-те сбора стават една Series с индекс order_id.
- 7 реда влязоха, 4 числа излязоха: по едно за всяка група.
Пробвай
Кой продукт се поръчва най-много
В 60-те поръчки от order_items.csv: колко бройки са поръчани от всеки продукт? Запиши ги в units, а в top3 трите продукта с най-много бройки.
Решение
import pandas as pd
items = pd.read_csv('order_items.csv')
units = items.groupby('product_id')['quantity'].sum()
top3 = units.nlargest(3)
print(top3)units = items.groupby('product_id')['quantity'].sum() дава по едно число за всеки продукт, а units.nlargest(3) дава продукти 13, 5 и 10: краставиците (31 броя), прясното мляко (27) и ябълките (25). Краставиците са с наличност 0: свършиха, защото се търсят. С count() вместо sum() щеше да преброиш редовете, а един ред може да е 4 броя.
Идеята
Няколко сметки наведнъж: agg
Често за всяка група ти трябват няколко числа. agg приема списък от сметки и дава по колона за всяка:
Имената на колоните са имената на сметките. За отчет е по-добре да ги кръстиш сам. Това са именувани обобщения (named aggregation): ново_име=('колона', 'сметка'), и всяка може да е от различна колона:
Поръчка 3 има 2 реда, 7 броя и 45,9 € оборот. Таблицата има по ред за поръчка и по колона за всеки въпрос, който си задал.
Идеята
Броят: size или count
Двата брояча изглеждат еднакво, но броят различни неща. size() брои редовете във всяка група. count() брои попълнените стойности във всяка колона: NaN не се брои. Ето клиентите по година на регистрация:
През 2025 г. са се регистрирали 16 клиенти, но в колоната city има 15 стойности: при един клиент градът липсва. Ако искаш „колко клиенти“, size() е верният отговор. count() по колона отговаря на „за колко знаем града“.
Идеята
Няколко ключа
Ключът може да е списък от колони. Тогава групата е всяка двойка стойности. Ето бройките за годината по продукт и по цена:
Продуктите са 25, а групите 26: един продукт се е продавал на две цени. Това е кафето (продукт 17): 245 пакета по 13,90 € и 444 пакета по 14,90 €, след като поскъпна на 1 март. Резултатът има индекс на две нива, и units.loc[17] взима групите на кафето.
По-точно, за любопитните
Групите с NaN в ключа изпадат: подаръчната карта е без категория и products.groupby('category_id') я пропуска тихо. groupby('category_id', dropna=False) я пази като група NaN. Провери с len колко реда влизат и колко излизат от групите.
Идеята
Числото на групата на всеки ред: transform
Понякога не искаш да свиеш групата, а да сложиш числото ѝ до всеки неин ред. Например: колко е скъп продуктът спрямо средната цена в своята категория. transform('mean') смята средната на групата и я повтаря на всеки ред, като пази реда и индекса на таблицата:
transform дава 25 числа, по едно на продукт, а mean() дава 6, по едно на категория. Затова колоната cat_avg пасва направо в таблицата. Кафето струва 14,90 €, а средната в напитките е 5,35 €: 2,79 пъти повече. Прахът за пране е 2,19 пъти над средната за почистване.
По-точно, за любопитните
Резултатът на mean() има индекс category_id, а таблицата products има индекс 0, 1, 2… Ако го присвоиш като колона, pandas подрежда по индекса, а не по категорията, и почти всичко става NaN. transform пази индекса на таблицата. Подаръчната карта е без категория и затова cat_avg при нея е NaN.
Пробвай
Делът на реда в поръчката
За всеки ред от order_items.csv: каква част от оборота на поръчката си носи? Запиши в items колона order_total с оборота на цялата поръчка и колона share с дела на реда.
Решение
import pandas as pd
items = pd.read_csv('order_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
items['order_total'] = items.groupby('order_id')['revenue'].transform('sum')
items['share'] = items['revenue'] / items['order_total']
print(items[items['order_id'] == 3])items.groupby('order_id')['revenue'].transform('sum') слага оборота на поръчката на всеки неин ред, а items['revenue'] / items['order_total'] дава дела. В поръчка 3 прахът за пране носи 37,5 от 45,9 €, около 82%, а ябълките около 18%. Делът на редовете в една поръчка винаги дава 1 общо. С .sum() вместо transform колоната щеше да е почти цялата NaN: сборът е по поръчки, а таблицата е по редове.
Идеята
Ключът като колона: as_index=False
Резултатът на groupby има ключа в индекса. За да го филтрираш, да го слепиш с друга таблица или да го сложиш на графика, ти трябва обикновена колона. Има два начина, с един и същ резултат:
as_index=False казва това още при групирането, а reset_index() го прави после: индексът става колона, а редовете се номерират 0, 1, 2… Колоните са order_id и revenue.
Упражнение
Средната поръчка
Отговори на управителя от първата карта. Запиши в order_totals оборота на всяка поръчка за годината без отказаните, а в avg_order средната поръчка, закръглена до 2 знака.
Решение
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
cancelled = orders.loc[orders['status'] == 'cancelled', 'id']
items = items[~items['order_id'].isin(cancelled)]
items['revenue'] = items['quantity'] * items['unit_price']
order_totals = items.groupby('order_id')['revenue'].sum()
avg_order = round(order_totals.mean(), 2)
print(len(order_totals), avg_order)order_totals = items.groupby('order_id')['revenue'].sum() дава 10 459 числа, по едно на поръчка, а round(order_totals.mean(), 2) дава 10,63 €. Средният ред беше 4,44 €: поръчката има средно 2,4 реда. С отказаните поръчките щяха да са 11 116, а средната 10,61 €, близо, но не вярно: отказаната поръчка не е оборот.
Колко клиенти са поръчали поне веднъж през годината и колко от тях само веднъж? Запиши в per_customer броя поръчки на всеки клиент, в n_customers броя клиенти, а в once колко са с точно една поръчка.
Решение
import pandas as pd
orders = pd.read_csv('year_orders.csv')
per_customer = orders.groupby('customer_id').size()
n_customers = len(per_customer)
once = (per_customer == 1).sum()
print(n_customers, once, per_customer.max())per_customer = orders.groupby('customer_id').size() дава по едно число за всеки клиент: 1 390 клиенти. (per_customer == 1).sum() брои 224 с една поръчка, а най-верният клиент има 121 поръчки, по 2–3 на седмица. len(orders) щеше да даде поръчките (11 116), а per_customer.min() само най-малкия брой, 1.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Избиране: [], loc и iloc · Филтри и подреждане · Нови колони · Типове и липсващи стойности
- Продължава в: Съединяване: merge · Широко и дълго: pivot_table и melt · Дати и времеви редове · Първата графика
- Задачи по темата: Подреждане на данни