Урок курса

groupby и transform: групповые признаки на уровне строк и фильтрация групп

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

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

transform: групповой агрегат на уровне строк

Когда вызываешь groupby(col).agg('mean'), pandas собирает группы и возвращает сжатую таблицу — одна строка на уникальное значение ключа. Индекс результата состоит из значений группировочного столбца, и длина результата равна числу уникальных групп, а не числу строк исходного DataFrame.

transform работает иначе. Он тоже разбивает данные на группы и вычисляет агрегат внутри каждой из них, но затем «разворачивает» результат обратно по исходному индексу. Каждая строка получает значение агрегата своей группы. Длина возвращаемой Series всегда равна len(df), индекс совпадает с исходным.

Представь таблицу с тремя сотрудниками из отдела HR и двумя из IT. После groupby('dept')['salary'].agg('mean') получаешь две строки — по одной на отдел. После groupby('dept')['salary'].transform('mean') — пять строк: каждый сотрудник несёт среднюю зарплату своего отдела рядом со своей строкой.

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

Добавление группового показателя как нового столбца

Процедура укладывается в два шага: вызвать transform и присвоить результат новому столбцу.

import pandas as pd

df = pd.DataFrame({
    'dept': ['HR', 'IT', 'HR', 'IT', 'HR'],
    'salary': [60000, 120000, 70000, 110000, 80000]
})

df['dept_mean'] = df.groupby('dept')['salary'].transform('mean')
df['dept_sum']  = df.groupby('dept')['salary'].transform('sum')

print(df)

Вывод:

  dept  salary   dept_mean  dept_sum
0   HR   60000   70000.0    210000
1   IT  120000  115000.0    230000
2   HR   70000   70000.0    210000
3   IT  110000  115000.0    230000
4   HR   80000   70000.0    210000

Что здесь происходит. Pandas разбивает строки по значению dept, считает среднее и сумму внутри каждой группы, а затем возвращает значение обратно в каждую строку по её исходной позиции. HR получает среднее (60000 + 70000 + 80000) / 3 = 70000, IT — (120000 + 110000) / 2 = 115000. Длина результата — ровно 5, как у исходного df.

В строку присваивания df['dept_mean'] = ... результат transform ложится без каких-либо дополнительных операций — индексы совпадают, pandas выравнивает значения автоматически.

Аргумент func в transform — строковый псевдоним агрегата: 'mean', 'sum', 'max', 'min', 'std' и другие стандартные агрегатные имена, которые принимает pandas. Передавать туда NumPy-функцию или лямбду тоже можно, но строковые имена работают быстрее, потому что pandas использует оптимизированные пути вычисления.

Самостоятельная практика. Попробуйте самостоятельно добавить групповой показатель и собрать отчёт: в подборке есть задачи на groupby, transform и merge. Перейти к практике группировок и соединений. Полный маршрут от массивов до обработки таблиц доступен в бесплатном тренажёре NumPy и pandas с проверкой решений. Продолжить обучение pandas бесплатно.

Фильтрация строк по условию на групповом агрегате

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

Представь задачу: есть таблица продаж по магазинам, и нужно оставить только строки из магазинов, чья суммарная выручка превышает пороговое значение. Наивное решение — посчитать groupby('store')['revenue'].agg('sum'), получить сжатую таблицу, отфильтровать там нужные магазины, а потом вернуться к исходному DataFrame и выкинуть строки через merge или isin. Это работает, но требует нескольких шагов.

С transform паттерн короче:

import pandas as pd

df = pd.DataFrame({
    'store': ['A', 'B', 'A', 'C', 'B', 'C', 'A'],
    'revenue': [100, 50, 200, 30, 80, 20, 150]
})

group_sum = df.groupby('store')['revenue'].transform('sum')
mask = group_sum > 200
df_filtered = df.loc[mask]
print(df_filtered)

Вывод:

  store  revenue
0     A      100
2     A      200
6     A      150

Что происходит по шагам. transform('sum') для каждой строки возвращает суммарную выручку её магазина: строки магазина A получают 450, строки B — 130, строки C — 50. Результат — Series длиной 7 с теми же индексами, что у df. Сравнение > 200 превращает её в булевую маску той же длины. df.loc[mask] отбирает только строки, для которых маска истинна, — то есть все строки магазина A.

Ключевое здесь: маска живёт в пространстве строк, а решение принято на уровне группы. Магазины B и C не проходят порог целиком — ни одна их строка не попадает в результат, даже если отдельные транзакции большие. Это именно групповая фильтрация, а не строчная.

Паттерн одинаков для любого агрегата. Хочешь оставить группы, где среднее выше порога — меняешь 'sum' на 'mean' и подбираешь нужный threshold. Хочешь отбросить группы с единственной строкой — transform('count') > 1. Маска в любом случае строится за одно выражение и напрямую передаётся в df.loc без промежуточных таблиц.

Типичная ошибка: agg вместо transform при создании группового признака

Когда нужно добавить в DataFrame столбец с групповым средним, иногда пишут через agg — по аналогии с тем, как только что считали сводные показатели. Код выглядит похоже, но результат оказывается полностью сломан.

import pandas as pd

df = pd.DataFrame({
    'dept': ['HR', 'IT', 'HR', 'IT', 'HR'],
    'salary': [60000, 120000, 70000, 110000, 80000]
})

# Неверный вариант: agg вместо transform
df['dept_mean_wrong'] = df.groupby('dept')['salary'].agg('mean')
print(df)

Вывод:

  dept  salary  dept_mean_wrong
0   HR   60000              NaN
1   IT  120000              NaN
2   HR   70000              NaN
3   IT  110000              NaN
4   HR   80000              NaN

Почему все значения — NaN. agg('mean') возвращает Series с двумя элементами и индексом ['HR', 'IT']: это метки групп, а не позиции строк. Исходный df имеет числовой индекс [0, 1, 2, 3, 4]. Когда pandas присваивает Series в столбец, он выравнивает значения по меткам индекса. Метка 'HR' не совпадает ни с одной из меток 0..4, метка 'IT' — тоже. Совпадений нет вообще, поэтому все пять строк получают NaN. Pandas не сообщает об ошибке — он молча выполняет присваивание, и баг легко пропустить без проверки результата.

Исправление прямое: заменить agg на transform.

# Верный вариант
df['dept_mean'] = df.groupby('dept')['salary'].transform('mean')
print(df[['dept', 'salary', 'dept_mean']])

Вывод:

  dept  salary  dept_mean
0   HR   60000    70000.0
1   IT  120000   115000.0
2   HR   70000    70000.0
3   IT  110000   115000.0
4   HR   80000    70000.0

transform возвращает Series длиной 5 с числовым индексом [0, 1, 2, 3, 4] — точно как у df. Выравнивание при присваивании проходит корректно, каждая строка получает среднее своей группы.

Правило простое: если нужен групповой показатель как столбец в исходной таблице — transform. Если нужна сводная статистика отдельной таблицей — agg.

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

Если используете группировки в отчётах, продолжите открытым уроком о взвешенном среднем: почему среднее по группам может дать неверный общий результат. Урок входит в платную практическую аналитику. Посмотреть продолжение.

Задача 1

DataFrame df содержит столбцы 'team' и 'score'. Выполнен код:
result = df.groupby('team')['score'].transform('mean')
Что верно описывает result?

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

Задача 2

DataFrame df содержит столбцы 'dept' и 'salary'. Нужно оставить только строки сотрудников из отделов, где суммарная зарплата превышает 200000. Какой подход использует transform('sum'), возвращает Series с тем же индексом и длиной, что у df, и позволяет сразу передать булеву маску в df.loc?

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

Задача 3

Фильтрация магазинов по суммарной выручке через transform + булевую маску

Что уже дано

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

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

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

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит три токена через пробел: store (строка без пробелов), product (строка без пробелов), revenue (целое число). Пример строки: Alpha Laptop 300.
  3. Создаёт pd.DataFrame с именем df из словаря списков с ключами 'store', 'product', 'revenue'.
  4. Добавляет в df столбец 'store_total' — суммарную выручку каждого магазина для каждой строки — через df.groupby('store')['revenue'].transform('sum').
  5. Читает из stdin целое число threshold.
  6. Строит булевую маску mask = df['store_total'] > threshold и создаёт df_big = df.loc[mask].
  7. Выводит в stdout ровно три блока:
  • Блок 1. Строку store_total values:, затем значения столбца 'store_total' через пробел (в порядке исходных строк).
  • Блок 2. Строку filtered rows: <k>, где k — число строк в df_big.
  • Блок 3. Для каждой строки df_big (в порядке возрастания исходного индекса) одну строку вида <store> <product> <revenue>.

Ввод и вывод

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

Формат ввода

n
store1 product1 revenue1
...
storeN productN revenueN
threshold

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

store_total values:
<v1> <v2> ... <vN>
filtered rows: <k>
<store> <product> <revenue>
...
РешениеPython

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

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

Задача 4

Флаг «выше среднего по региону»: transform('mean') и векторное сравнение

Что уже дано

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

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

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

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит три токена через пробел: region (строка без пробелов), manager (строка без пробелов), sales (целое число). Пример строки: Север Аня 200.
  3. Создаёт pd.DataFrame с именем df из словаря списков с ключами 'region', 'manager', 'sales'.
  4. Добавляет столбец 'region_avg' — среднее sales по каждому region для каждой строки — через df.groupby('region')['sales'].transform('mean').
  5. Добавляет булев столбец 'above_avg' — True, если sales данной строки строго больше region_avg. Используйте векторное сравнение без циклов.
  6. Выводит в stdout ровно n + 2 строк:
  • Строки 1…n: для каждой строки (в исходном порядке) одну строку вида <manager> <region_avg_rounded_to_1> <above_avg>, где region_avg округлён до одного знака после запятой, above_avg — True или False.
  • Строка n+1: above_count: <k> — число строк, где above_avg равен True (через df['above_avg'].sum()).
  • Строка n+2: shape: <shape> — кортеж df.shape.

Ввод и вывод

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

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

Формат ввода

n
region1 manager1 sales1
...
regionN managerN salesN

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

manager1 region_avg1 above_avg1
...
managerN region_avgN above_avgN
above_count: <k>
shape: (<n>, <cols>)
РешениеPython

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

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

Задача 5

Найди и исправь: agg вместо transform и неверный порог фильтрации

Что уже дано

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

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

  1. Столбец 'cat_total' вычисляется через df.groupby('category')['value'].agg('sum') — но agg возвращает Series с индексом из меток групп, поэтому при присваивании в df['cat_total'] все значения станут NaN. Нужно заменить agg на transform.
  2. Маска mask строится как df['cat_total'] >= threshold, хотя по условию нужно отбирать строки, где суммарное значение категории строго больше порога. Нужно заменить >= на >.
  3. Вывод числа отфильтрованных строк написан как print(len(df_filtered)), хотя по условию нужно вывести строку вида filtered: <k> (например, filtered: 3). Нужно исправить на print(f'filtered: {len(df_filtered)}').

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

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

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

  1. Читала из stdin целое число n — количество строк таблицы.
  2. Читала следующие n строк; каждая содержит три токена через пробел: category (строка без пробелов), item (строка без пробелов), value (целое число).
  3. Создавала pd.DataFrame с именем df из словаря списков с ключами 'category', 'item', 'value'.
  4. Добавляла столбец 'cat_total' — суммарное значение value по каждой category для каждой строки — через transform('sum').
  5. Читала целое число threshold.
  6. Строила маску mask = df['cat_total'] > threshold и создавала df_filtered = df.loc[mask].
  7. Выводила ровно три блока:
  • Строку cat_total: <v1> <v2> ... <vN> — значения столбца 'cat_total' в порядке исходных строк через пробел.
  • Строку filtered: <k> — число строк в df_filtered.
  • Строку categories: <уникальные категории через пробел> — отсортированный список уникальных значений category в df_filtered.

Ввод и вывод

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

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

Формат ввода

n
category1 item1 value1
...
categoryN itemN valueN
threshold

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

cat_total: <v1> <v2> ... <vN>
filtered: <k>
categories: <cat1> <cat2> ...
РешениеPython

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

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

Задача 6

Фильтрация отделов по числу сотрудников через transform('count')

Что уже дано

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

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

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

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит три токена через пробел: dept (строка без пробелов), employee (строка без пробелов), hours (целое число). Пример строки: Dev Alpha 40.
  3. Создаёт pd.DataFrame с именем df из словаря списков с ключами 'dept', 'employee', 'hours'.
  4. Добавляет столбец 'dept_hours_sum' — суммарные часы по каждому dept для каждой строки — через df.groupby('dept')['hours'].transform('sum').
  5. Добавляет столбец 'dept_count' — число сотрудников в каждом dept для каждой строки — через df.groupby('dept')['hours'].transform('count').
  6. Читает из stdin целое число min_count — минимальный размер отдела.
  7. Строит булевую маску mask = df['dept_count'] >= min_count и создаёт df_big = df.loc[mask].
  8. Выводит в stdout ровно четыре блока:
  • Блок 1. Строку dept_hours_sum:, затем значения столбца 'dept_hours_sum' через пробел (в порядке исходных строк).
  • Блок 2. Строку dept_count:, затем значения столбца 'dept_count' через пробел.
  • Блок 3. Строку big_dept rows: <k>, где k — число строк в df_big.
  • Блок 4. Для каждой строки df_big (в порядке исходного индекса) одну строку вида <dept> <employee> <hours>.

Ввод и вывод

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

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

Формат ввода

n
dept1 employee1 hours1
...
deptN employeeN hoursN
min_count

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

dept_hours_sum:
<v1> <v2> ... <vN>
dept_count:
<c1> <c2> ... <cN>
big_dept rows: <k>
<dept> <employee> <hours>
...
РешениеPython

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

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

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

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

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

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

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