Урок курса

groupby и agg: сводные показатели с явными именами агрегатов

NumPy и pandas: практический тренажёр

До этого урока мы работали с данными построчно или целыми столбцами. Теперь задача другая: не просто посчитать среднее по всей таблице, а получить отдельный агрегат для каждой группы строк — например, суммарные продажи по каждому региону или среднюю цену по каждой категории товара. Именно для этого нужен groupby.

groupby по одному столбцу: механика разбиения и индекс результата

Когда пишешь df.groupby('region'), pandas не вычисляет ничего сразу. Он разбивает DataFrame на группы — по одной на каждое уникальное значение столбца region — и возвращает объект DataFrameGroupBy. Это просто инструкция «как делить»; результат появится только после вызова агрегирующего метода.

Посмотрим на минимальном примере:

import pandas as pd

df = pd.DataFrame({
    'region': ['Север', 'Юг', 'Север', 'Юг', 'Север'],
    'sales':  [100, 200, 150, 300, 80]
})

result = df.groupby('region')['sales'].sum()
print(result)

Вывод:

region
Север    330
Юг       500
Name: sales, dtype: int64

Обрати внимание: region здесь не столбец, а индекс результата. Pandas переносит ключ группировки в индекс по умолчанию — это сознательное поведение, а не баг. Пока это Series, всё выглядит терпимо. Но когда результат становится DataFrame с несколькими агрегатами, индекс в роли ключа создаёт неудобства: ключ пропадает из result.columns, и дальнейшая работа с таблицей усложняется.

Чтобы ключ остался обычным столбцом, используй параметр as_index=False:

result = df.groupby('region', as_index=False)['sales'].sum()
print(result)
print(result.columns.tolist())

Вывод:

  region  sales
0  Север    330
1    Юг    500
['region', 'sales']

Теперь region — обычный столбец с целочисленным RangeIndex. Именно эта форма удобна для дальнейшей фильтрации, сортировки или объединения таблиц.

Концептуально groupby делает три шага: split — разбивает DataFrame на группы по значениям ключевого столбца; apply — применяет агрегирующую функцию к каждой группе; combine — собирает результаты в одну таблицу. Этот паттерн split-apply-combine лежит в основе всей работы с groupby в pandas.

В следующем разделе мы подключим к groupby вызов .agg() с именованными агрегатами, чтобы за один раз вычислять несколько показателей с читаемыми именами столбцов.

Named aggregation в agg: несколько агрегатов за один вызов

Когда у тебя есть сгруппированный объект, следующий вопрос — что именно вычислять по каждой группе. Самый гибкий способ — вызов .agg() с именованными парами.

Синтаксис выглядит так:

df.groupby('region', as_index=False).agg(
    total_sales=('sales', 'sum'),
    avg_units=('units', 'mean')
)

Каждая именованная пара — это отдельный аргумент вида имя_результата=(столбец, функция). Слева от = — имя, которое получит новый столбец в итоговой таблице. В кортеже справа — исходный столбец, из которого берутся данные, и строковое имя агрегирующей функции.

В этом курсе используются строки 'sum', 'mean', 'min', 'max', 'count' — это не исчерпывающий список агрегатов pandas, но достаточный для большинства задач. Их можно комбинировать в любом порядке и количестве в одном вызове.

Важный нюанс про 'count': он считает непропущенные значения в указанном столбце. Если в sales есть NaN, такая строка не попадёт в счётчик. Это поведение соответствует логике «сколько реальных продаж зафиксировано», а не «сколько строк в группе».

Вот полный пример с несколькими агрегатами одновременно:

import pandas as pd

df = pd.DataFrame({
    'region':   ['Север', 'Юг', 'Север', 'Юг', 'Север'],
    'category': ['A', 'A', 'B', 'B', 'A'],
    'sales':    [100, 200, 150, 300, 80],
    'units':    [10, 20, 15, 30, 8]
})

result = df.groupby('region', as_index=False).agg(
    total_sales=('sales', 'sum'),
    avg_units=('units', 'mean'),
    min_sales=('sales', 'min'),
    nonmissing_count=('sales', 'count')
)
print(result)

Вывод:

  region  total_sales  avg_units  min_sales  nonmissing_count
0  Север          330       11.0         80                 3
1    Юг           500       25.0        200                 2

Четыре именованные пары — четыре столбца в результате. Каждый столбец берёт данные из нужного исходного столбца независимо: total_sales и min_sales смотрят в sales, avg_units — в units, nonmissing_count считает непропущенные значения в sales.

Главное преимущество такого синтаксиса — имена столбцов результата ты задаёшь явно, прямо в вызове. Без named aggregation pandas генерировал бы технические имена вроде sales или строил MultiIndex в заголовках — ни то ни другое не читается в реальном коде. С именованными парами таблица сразу выходит с понятными заголовками, которые не нужно переименовывать потом через rename().

Один вызов .agg() с несколькими именованными парами заменяет несколько последовательных вычислений и возвращает всё в одной таблице. Именно этот паттерн используется в курсе как стандартный способ агрегирования.

Два способа получить плоскую таблицу: as_index=False и reset_index()

Мы уже видели, что groupby по умолчанию превращает ключ группировки в индекс результата. Это становится проблемой, как только хочешь работать с результатом как с обычной таблицей: фильтровать по ключу, сортировать или передавать дальше по пайплайну. Посмотрим, как это проявляется на практике и как это исправить.

Ловушка: ключ пропал из columns

Если вызываешь groupby без as_index=False, ключ группировки уходит в индекс и исчезает из result.columns:

import pandas as pd

df = pd.DataFrame({
    'region':   ['Север', 'Юг', 'Север', 'Юг', 'Север'],
    'category': ['A', 'A', 'B', 'B', 'A'],
    'sales':    [100, 200, 150, 300, 80],
    'units':    [10, 20, 15, 30, 8]
})

result = df.groupby('region').agg(
    total_sales=('sales', 'sum'),
    avg_units=('units', 'mean')
)

print(result.columns.tolist())
# ['total_sales', 'avg_units']

print(result.index.name)
# region

region есть — но только в индексе. В columns его нет. Если попробуешь написать result['region'], получишь KeyError. Именно здесь ломаются скрипты, где после агрегации ожидается обычная плоская таблица.

Способ 1: as_index=False прямо в groupby

Простейшее решение — передать параметр сразу:

result1 = df.groupby('region', as_index=False).agg(
    total_sales=('sales', 'sum'),
    avg_units=('units', 'mean')
)

print(result1)
#   region  total_sales  avg_units
# 0  Север          330       11.0
# 1    Юг           500       25.0

print(result1.columns.tolist())
# ['region', 'total_sales', 'avg_units']

region возвращается в обычные столбцы, индекс — стандартный RangeIndex. Таблица сразу готова к любым операциям.

Способ 2: reset_index() после agg

Если по каким-то причинам написал groupby без as_index=False, можно добавить reset_index() в конце цепочки:

result2 = df.groupby('region').agg(
    total_sales=('sales', 'sum'),
    avg_units=('units', 'mean')
).reset_index()

print(result2)
#   region  total_sales  avg_units
# 0  Север          330       11.0
# 1    Юг           500       25.0

Результат идентичен. reset_index() вынимает ключ из индекса и помещает его первым столбцом — именно то, что делает as_index=False автоматически.

Какой способ выбирать

Оба способа корректны и дают одинаковую таблицу. На практике as_index=False лаконичнее: параметр виден сразу при чтении строки с groupby, и не нужно помнить о дополнительном вызове в конце. reset_index() полезен, когда получаешь уже сгруппированный результат и хочешь его «распрямить» постфактум.

В этом курсе для группировки по одному ключу оба варианта используются; для нескольких ключей — только as_index=False, чтобы не сталкиваться с составными типами индекса.

groupby по нескольким столбцам с as_index=False

Один ключ группировки — это частный случай. Чаще в реальных данных нужно смотреть на комбинации: например, продажи по каждому региону внутри каждой категории. Для этого в groupby передаётся список столбцов.

Синтаксис не меняется — добавляется только список вместо одной строки:

import pandas as pd

df = pd.DataFrame({
    'region':   ['Север', 'Юг', 'Север', 'Юг', 'Север'],
    'category': ['A', 'A', 'B', 'B', 'A'],
    'sales':    [100, 200, 150, 300, 80],
    'units':    [10, 20, 15, 30, 8]
})

result = df.groupby(['region', 'category'], as_index=False).agg(
    total_sales=('sales', 'sum'),
    avg_units=('units', 'mean')
)
print(result)

Вывод:

  region category  total_sales  avg_units
0  Север        A          180       9.0
1  Север        B          150      15.0
2    Юг        A          200      20.0
3    Юг        B          300      30.0

Оба ключа — region и category — остаются обычными столбцами. Индекс — стандартный RangeIndex. Таблица плоская и сразу готова к фильтрации или сортировке.

Что здесь происходит: pandas находит все уникальные комбинации значений region и category, считает агрегаты по строкам, попавшим в каждую комбинацию, и собирает результат. Строки ('Север', 'A') из оригинального DataFrame — это индексы 0 и 4, их sales суммируется как 100 + 80 = 180.

Параметр as_index=False здесь особенно важен. Без него pandas создал бы составной индекс из двух уровней — один для region, другой для category. Работать с таким результатом через стандартные операции заметно сложнее. Поэтому для многоключевой группировки as_index=False — единственный рекомендуемый паттерн в этом курсе.

Если нужно добавить третий ключ — просто расширяешь список: groupby(['region', 'category', 'month'], as_index=False). Количество ключей не ограничено; синтаксис agg с именованными парами при этом не меняется.

В следующем уроке groupby появится в другой роли: вместо agg, который сжимает таблицу до одной строки на группу, будет transform — он возвращает групповой показатель для каждой строки исходного DataFrame, не меняя его длину.

Закрепите тему на задачах

Определите гранулярность результата: одна строка на группу. Сверьте сумму групп с исходной таблицей и учтите поведение группировки с пустым ключом.

groupby и agg: сводные показатели с явными именами агрегатов — задача по NumPy и pandas

pandas groupby и agg: сводка по двум ключам

pandas groupby и agg: группировка, rename и sort_values

Откройте условие и решите задачу перед чтением следующей темы. В задачах на код проверяется программа, в тестовых заданиях — выбранный ответ. Для последовательного обучения вернитесь в программу бесплатного курса.

Задача 1

DataFrame df содержит столбцы 'city', 'amount', 'count'. Выполнен код:

result = df.groupby('city').agg(
total=('amount', 'sum'),
n=('count', 'sum')
)

Чем является result.index?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Дан код:
import pandas as pd
df = pd.DataFrame({
'team': ['Alpha', 'Beta', 'Alpha', 'Beta'],
'score': [80, 90, 70, 60]
})
result = df.groupby('team', as_index=False).agg(
max_score=('score', 'max'),
min_score=('score', 'min')
)
Чему равен result.loc[result['team'] == 'Alpha', 'max_score'].iloc[0]?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Один ключ, два способа плоской таблицы: as_index=False и reset_index

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит три токена через пробел: store (строка без пробелов), qty (целое число), revenue (целое число). Пример строки: Alpha 10 5000.
  3. Создаёт pd.DataFrame из словаря списков с ключами 'store', 'qty', 'revenue'.
  4. Способ A. Группирует DataFrame по 'store' с as_index=False и вычисляет за один вызов agg два агрегата: total_qty=('qty', 'sum') и total_rev=('revenue', 'sum'). Сохраняет результат в result_a.
  5. Способ B. Группирует тот же DataFrame по 'store' без as_index=False и вычисляет те же два агрегата с теми же именами. Затем применяет .reset_index(). Сохраняет в result_b.
  6. Выводит в stdout три служебные строки, а затем по одной строке для каждой строки result_a:
  • columns_a=<список> — list(result_a.columns) в виде Python-списка;
  • columns_b=<список> — list(result_b.columns);
  • equal=<bool> — результат result_a.equals(result_b) (True если таблицы идентичны);
  • для каждой строки result_a, отсортированной по 'store', выводит одну строку вида <store> <total_qty> <total_rev>.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: n >= 1.

Формат ввода

n
store1 qty1 rev1
store2 qty2 rev2
...

Формат вывода

columns_a=['store', 'total_qty', 'total_rev']
columns_b=['store', 'total_qty', 'total_rev']
equal=True
<store> <total_qty> <total_rev>
...
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 4

Два ключа, четыре агрегата: groupby по region и channel с as_index=False

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит четыре токена через пробел: region (строка без пробелов), channel (строка без пробелов), amount (целое число), orders (целое число). Пример строки: Север online 5000 10.
  3. Создаёт pd.DataFrame из словаря списков с ключами 'region', 'channel', 'amount', 'orders'.
  4. Группирует DataFrame по ['region', 'channel'] с as_index=False и вычисляет за один вызов agg четыре именованных агрегата:
  • total_amount=('amount', 'sum')
  • avg_amount=('amount', 'mean')
  • max_orders=('orders', 'max')
  • tx_count=('orders', 'count')

Сохраняет результат в переменную result.
5. Выводит в stdout:

  • строку columns=<список> — list(result.columns) в виде Python-списка;
  • строку shape=<кортеж> — result.shape;
  • для каждой строки result, отсортированной по 'region', затем по 'channel', выводит одну строку вида <region> <channel> <total_amount> <avg_amount> <max_orders> <tx_count>, где avg_amount округлён до двух знаков после запятой.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: n >= 1.

Формат ввода

n
region1 channel1 amount1 orders1
region2 channel2 amount2 orders2
...

Формат вывода

columns=['region', 'channel', 'total_amount', 'avg_amount', 'max_orders', 'tx_count']
shape=(<строк>, 6)
<region> <channel> <total_amount> <avg_amount> <max_orders> <tx_count>
...
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Найди и исправь: три ошибки в groupby, agg и reset_index

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.

В редакторе уже есть сломанный код. В нём ровно три ошибки в трёх независимых строках:

  1. В первой группировке используется groupby('dept', as_index=True), хотя нужно получить плоскую таблицу с 'dept' как обычным столбцом — нужен as_index=False.
  2. В именованном агрегате avg_bonus=('bonus', 'mean') исходный столбец написан как 'salary' вместо 'bonus' — среднее считается не по тому столбцу.
  3. Вторая группировка вычисляет total_bonus=('bonus', 'sum'), но после .agg(...) не вызван .reset_index(), из-за чего 'dept' и 'name' остаются индексом, а не столбцами.

Каждая ошибка находится в отдельной строке кода. Исправьте все три.

Что нужно сделать

Исправьте три ошибки так, чтобы программа:

  1. Читала из stdin целое число n — количество строк таблицы.
  2. Читала следующие n строк; каждая содержит четыре токена через пробел: dept (строка), name (строка), salary (целое число), bonus (целое число).
  3. Строила pd.DataFrame с именем df и столбцами 'dept', 'name', 'salary', 'bonus'.
  4. Выполняла первую группировку по 'dept' с as_index=False и вычисляла за один agg три агрегата:
  • total_sal=('salary', 'sum')
  • avg_bonus=('bonus', 'mean') ← исходный столбец должен быть 'bonus', не 'salary'
  • head_count=('name', 'count')

Сохраняла в summary. Выводила list(summary.columns), затем каждую строку summary, отсортированную по 'dept', в формате <dept> <total_sal> <avg_bonus> <head_count>.

  1. Выполняла вторую группировку по ['dept', 'name'] без as_index=False и вычисляла total_bonus=('bonus', 'sum'), затем вызывала .reset_index(). Сохраняла в by_person. Выводила list(by_person.columns), затем каждую строку by_person, отсортированную по 'dept', потом по 'name', в формате <dept> <name> <total_bonus>.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: n >= 1.

Формат ввода

n
dept1 name1 salary1 bonus1
dept2 name2 salary2 bonus2
...

Формат вывода

['dept', 'total_sal', 'avg_bonus', 'head_count']
<dept> <total_sal> <avg_bonus> <head_count>
...
['dept', 'name', 'total_bonus']
<dept> <name> <total_bonus>
...
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

Пайплайн: groupby + agg + rename + sort, затем группировка по двум ключам

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит четыре токена через пробел: month (строка без пробелов), category (строка без пробелов), amount (целое число), transactions (целое число). Пример строки: Jan Food 1200 12.
  3. Создаёт pd.DataFrame из словаря списков с ключами 'month', 'category', 'amount', 'transactions'.
  4. Шаг A. Группирует DataFrame по 'month' с as_index=False и вычисляет за один вызов agg три именованных агрегата: month_total=('amount', 'sum'), month_avg=('amount', 'mean'), month_tx=('transactions', 'sum'). Сохраняет в monthly.
  5. Шаг B. Переименовывает столбец 'month_total' в 'total' через rename(columns=...) и сохраняет результат обратно в monthly.
  6. Шаг C. Сортирует monthly по 'total' по возрастанию через sort_values. Выводит строку monthly_columns=<список> — list(monthly.columns), затем для каждой строки отсортированного monthly выводит одну строку вида <month> <total> <round(month_avg, 1)> <month_tx>.
  7. Шаг D. Группирует исходный DataFrame по ['month', 'category'] с as_index=False и вычисляет cat_total=('amount', 'sum'). Сохраняет в by_cat. Сортирует by_cat по 'month', затем по 'category'. Выводит строку by_cat_columns=<список>, затем для каждой строки выводит <month> <category> <cat_total>.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: n >= 1.

Формат ввода

n
month1 category1 amount1 transactions1
month2 category2 amount2 transactions2
...

Формат вывода

monthly_columns=['month', 'total', 'month_avg', 'month_tx']
<month> <total> <month_avg> <month_tx>
...
by_cat_columns=['month', 'category', 'cat_total']
<month> <category> <cat_total>
...
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку